diff options
Diffstat (limited to 'java/com/android/incallui/answer/impl/AnswerFragment.java')
-rw-r--r-- | java/com/android/incallui/answer/impl/AnswerFragment.java | 1085 |
1 files changed, 1085 insertions, 0 deletions
diff --git a/java/com/android/incallui/answer/impl/AnswerFragment.java b/java/com/android/incallui/answer/impl/AnswerFragment.java new file mode 100644 index 000000000..1f0541c4b --- /dev/null +++ b/java/com/android/incallui/answer/impl/AnswerFragment.java @@ -0,0 +1,1085 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.incallui.answer.impl; + +import android.Manifest.permission; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.pm.PackageManager; +import android.location.Location; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.DrawableRes; +import android.support.annotation.FloatRange; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.StringRes; +import android.support.annotation.VisibleForTesting; +import android.support.v4.app.Fragment; +import android.text.TextUtils; +import android.transition.TransitionManager; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.AccessibilityDelegate; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.ViewTreeObserver.OnGlobalLayoutListener; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; +import android.widget.ImageView; +import com.android.dialer.common.Assert; +import com.android.dialer.common.FragmentUtils; +import com.android.dialer.common.LogUtil; +import com.android.dialer.common.MathUtil; +import com.android.dialer.compat.ActivityCompat; +import com.android.dialer.logging.DialerImpression; +import com.android.dialer.logging.Logger; +import com.android.dialer.multimedia.MultimediaData; +import com.android.dialer.util.ViewUtil; +import com.android.incallui.answer.impl.CreateCustomSmsDialogFragment.CreateCustomSmsHolder; +import com.android.incallui.answer.impl.SmsBottomSheetFragment.SmsSheetHolder; +import com.android.incallui.answer.impl.affordance.SwipeButtonHelper.Callback; +import com.android.incallui.answer.impl.affordance.SwipeButtonView; +import com.android.incallui.answer.impl.answermethod.AnswerMethod; +import com.android.incallui.answer.impl.answermethod.AnswerMethodFactory; +import com.android.incallui.answer.impl.answermethod.AnswerMethodHolder; +import com.android.incallui.answer.impl.utils.Interpolators; +import com.android.incallui.answer.protocol.AnswerScreen; +import com.android.incallui.answer.protocol.AnswerScreenDelegate; +import com.android.incallui.answer.protocol.AnswerScreenDelegateFactory; +import com.android.incallui.call.DialerCall.State; +import com.android.incallui.contactgrid.ContactGridManager; +import com.android.incallui.incall.protocol.ContactPhotoType; +import com.android.incallui.incall.protocol.InCallScreen; +import com.android.incallui.incall.protocol.InCallScreenDelegate; +import com.android.incallui.incall.protocol.InCallScreenDelegateFactory; +import com.android.incallui.incall.protocol.PrimaryCallState; +import com.android.incallui.incall.protocol.PrimaryInfo; +import com.android.incallui.incall.protocol.SecondaryInfo; +import com.android.incallui.maps.MapsComponent; +import com.android.incallui.sessiondata.AvatarPresenter; +import com.android.incallui.sessiondata.MultimediaFragment; +import com.android.incallui.util.AccessibilityUtil; +import com.android.incallui.video.protocol.VideoCallScreen; +import com.android.incallui.videotech.utils.VideoUtils; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** The new version of the incoming call screen. */ +@SuppressLint("ClickableViewAccessibility") +public class AnswerFragment extends Fragment + implements AnswerScreen, + InCallScreen, + SmsSheetHolder, + CreateCustomSmsHolder, + AnswerMethodHolder, + MultimediaFragment.Holder { + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + static final String ARG_CALL_ID = "call_id"; + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + static final String ARG_IS_VIDEO_CALL = "is_video_call"; + + static final String ARG_ALLOW_ANSWER_AND_RELEASE = "allow_answer_and_release"; + + static final String ARG_HAS_CALL_ON_HOLD = "has_call_on_hold"; + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + static final String ARG_IS_VIDEO_UPGRADE_REQUEST = "is_video_upgrade_request"; + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + static final String ARG_IS_SELF_MANAGED_CAMERA = "is_self_managed_camera"; + + private static final String STATE_HAS_ANIMATED_ENTRY = "hasAnimated"; + + private static final int HINT_SECONDARY_SHOW_DURATION_MILLIS = 5000; + private static final float ANIMATE_LERP_PROGRESS = 0.5f; + private static final int STATUS_BAR_DISABLE_RECENT = 0x01000000; + private static final int STATUS_BAR_DISABLE_HOME = 0x00200000; + private static final int STATUS_BAR_DISABLE_BACK = 0x00400000; + + private static void fadeToward(View view, float newAlpha) { + view.setAlpha(MathUtil.lerp(view.getAlpha(), newAlpha, ANIMATE_LERP_PROGRESS)); + } + + private static void scaleToward(View view, float newScale) { + view.setScaleX(MathUtil.lerp(view.getScaleX(), newScale, ANIMATE_LERP_PROGRESS)); + view.setScaleY(MathUtil.lerp(view.getScaleY(), newScale, ANIMATE_LERP_PROGRESS)); + } + + private AnswerScreenDelegate answerScreenDelegate; + private InCallScreenDelegate inCallScreenDelegate; + + private View importanceBadge; + private SwipeButtonView secondaryButton; + private SwipeButtonView answerAndReleaseButton; + private AffordanceHolderLayout affordanceHolderLayout; + // Use these flags to prevent user from clicking accept/reject buttons multiple times. + // We use separate flags because in some rare cases accepting a call may fail to join the room, + // and then user is stuck in the incoming call view until it times out. Two flags at least give + // the user a chance to get out of the CallActivity. + private boolean buttonAcceptClicked; + private boolean buttonRejectClicked; + private boolean hasAnimatedEntry; + private PrimaryInfo primaryInfo = PrimaryInfo.createEmptyPrimaryInfo(); + private PrimaryCallState primaryCallState; + private ArrayList<CharSequence> textResponses; + private SmsBottomSheetFragment textResponsesFragment; + private CreateCustomSmsDialogFragment createCustomSmsDialogFragment; + private SecondaryBehavior secondaryBehavior = SecondaryBehavior.REJECT_WITH_SMS; + private SecondaryBehavior answerAndReleaseBehavior; + private ContactGridManager contactGridManager; + private VideoCallScreen answerVideoCallScreen; + private Handler handler = new Handler(Looper.getMainLooper()); + + private enum SecondaryBehavior { + REJECT_WITH_SMS( + R.drawable.quantum_ic_message_white_24, + R.string.a11y_description_incoming_call_reject_with_sms, + R.string.a11y_incoming_call_reject_with_sms, + R.string.call_incoming_swipe_to_decline_with_message) { + @Override + public void performAction(AnswerFragment fragment) { + fragment.showMessageMenu(); + } + }, + + ANSWER_VIDEO_AS_AUDIO( + R.drawable.quantum_ic_videocam_off_white_24, + R.string.a11y_description_incoming_call_answer_video_as_audio, + R.string.a11y_incoming_call_answer_video_as_audio, + R.string.call_incoming_swipe_to_answer_video_as_audio) { + @Override + public void performAction(AnswerFragment fragment) { + fragment.acceptCallByUser(true /* answerVideoAsAudio */); + } + }, + + ANSWER_AND_RELEASE( + R.drawable.ic_end_answer_32, + R.string.a11y_description_incoming_call_answer_and_release, + R.string.a11y_incoming_call_answer_and_release, + R.string.call_incoming_swipe_to_answer_and_release) { + @Override + public void performAction(AnswerFragment fragment) { + fragment.performAnswerAndRelease(); + } + }; + + @DrawableRes public final int icon; + @StringRes public final int contentDescription; + @StringRes public final int accessibilityLabel; + @StringRes public final int hintText; + + SecondaryBehavior( + @DrawableRes int icon, + @StringRes int contentDescription, + @StringRes int accessibilityLabel, + @StringRes int hintText) { + this.icon = icon; + this.contentDescription = contentDescription; + this.accessibilityLabel = accessibilityLabel; + this.hintText = hintText; + } + + public abstract void performAction(AnswerFragment fragment); + + public void applyToView(ImageView view) { + view.setImageResource(icon); + view.setContentDescription(view.getContext().getText(contentDescription)); + } + } + + private void performAnswerAndRelease() { + restoreAnswerAndReleaseButtonAnimation(); + answerScreenDelegate.onAnswerAndReleaseCall(); + } + + private void restoreAnswerAndReleaseButtonAnimation() { + answerAndReleaseButton + .animate() + .alpha(0) + .withEndAction( + new Runnable() { + @Override + public void run() { + affordanceHolderLayout.reset(false); + secondaryButton.animate().alpha(1); + } + }); + } + + private final AccessibilityDelegate accessibilityDelegate = + new AccessibilityDelegate() { + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + if (host == secondaryButton) { + CharSequence label = getText(secondaryBehavior.accessibilityLabel); + info.addAction(new AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, label)); + } else if (host == answerAndReleaseButton) { + CharSequence label = getText(answerAndReleaseBehavior.accessibilityLabel); + info.addAction(new AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, label)); + } + } + + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + if (action == AccessibilityNodeInfo.ACTION_CLICK) { + if (host == secondaryButton) { + performSecondaryButtonAction(); + return true; + } else if (host == answerAndReleaseButton) { + performAnswerAndReleaseButtonAction(); + return true; + } + } + return super.performAccessibilityAction(host, action, args); + } + }; + + private final Callback affordanceCallback = + new Callback() { + @Override + public void onAnimationToSideStarted(boolean rightPage, float translation, float vel) {} + + @Override + public void onAnimationToSideEnded(boolean rightPage) { + if (rightPage) { + performAnswerAndReleaseButtonAction(); + } else { + performSecondaryButtonAction(); + } + } + + @Override + public float getMaxTranslationDistance() { + View view = getView(); + if (view == null) { + return 0; + } + return (float) Math.hypot(view.getWidth(), view.getHeight()); + } + + @Override + public void onSwipingStarted(boolean rightIcon) {} + + @Override + public void onSwipingAborted() {} + + @Override + public void onIconClicked(boolean rightIcon) { + affordanceHolderLayout.startHintAnimation(rightIcon, null); + getAnswerMethod() + .setHintText( + rightIcon + ? getText(answerAndReleaseBehavior.hintText) + : getText(secondaryBehavior.hintText)); + handler.removeCallbacks(swipeHintRestoreTimer); + handler.postDelayed(swipeHintRestoreTimer, HINT_SECONDARY_SHOW_DURATION_MILLIS); + } + + @Override + public SwipeButtonView getLeftIcon() { + return secondaryButton; + } + + @Override + public SwipeButtonView getRightIcon() { + return answerAndReleaseButton; + } + + @Override + public View getLeftPreview() { + return null; + } + + @Override + public View getRightPreview() { + return null; + } + + @Override + public float getAffordanceFalsingFactor() { + return 1.0f; + } + }; + + private Runnable swipeHintRestoreTimer = this::restoreSwipeHintTexts; + + private void performSecondaryButtonAction() { + secondaryBehavior.performAction(this); + } + + private void performAnswerAndReleaseButtonAction() { + answerAndReleaseBehavior.performAction(this); + } + + public static AnswerFragment newInstance( + String callId, + boolean isVideoCall, + boolean isVideoUpgradeRequest, + boolean isSelfManagedCamera, + boolean allowAnswerAndRelease, + boolean hasCallOnHold) { + Bundle bundle = new Bundle(); + bundle.putString(ARG_CALL_ID, Assert.isNotNull(callId)); + bundle.putBoolean(ARG_IS_VIDEO_CALL, isVideoCall); + bundle.putBoolean(ARG_IS_VIDEO_UPGRADE_REQUEST, isVideoUpgradeRequest); + bundle.putBoolean(ARG_IS_SELF_MANAGED_CAMERA, isSelfManagedCamera); + bundle.putBoolean(ARG_ALLOW_ANSWER_AND_RELEASE, allowAnswerAndRelease); + bundle.putBoolean(ARG_HAS_CALL_ON_HOLD, hasCallOnHold); + + AnswerFragment instance = new AnswerFragment(); + instance.setArguments(bundle); + return instance; + } + + @Override + @NonNull + public String getCallId() { + return Assert.isNotNull(getArguments().getString(ARG_CALL_ID)); + } + + @Override + public boolean isVideoUpgradeRequest() { + return getArguments().getBoolean(ARG_IS_VIDEO_UPGRADE_REQUEST); + } + + @Override + public void setTextResponses(List<String> textResponses) { + if (isVideoCall() || isVideoUpgradeRequest()) { + LogUtil.i("AnswerFragment.setTextResponses", "no-op for video calls"); + } else if (textResponses == null) { + LogUtil.i("AnswerFragment.setTextResponses", "no text responses, hiding secondary button"); + this.textResponses = null; + secondaryButton.setVisibility(View.INVISIBLE); + } else if (ActivityCompat.isInMultiWindowMode(getActivity())) { + LogUtil.i("AnswerFragment.setTextResponses", "in multiwindow, hiding secondary button"); + this.textResponses = null; + secondaryButton.setVisibility(View.INVISIBLE); + } else { + LogUtil.i("AnswerFragment.setTextResponses", "textResponses.size: " + textResponses.size()); + this.textResponses = new ArrayList<>(textResponses); + secondaryButton.setVisibility(View.VISIBLE); + } + } + + private void initSecondaryButton() { + secondaryBehavior = + isVideoCall() || isVideoUpgradeRequest() + ? SecondaryBehavior.ANSWER_VIDEO_AS_AUDIO + : SecondaryBehavior.REJECT_WITH_SMS; + secondaryBehavior.applyToView(secondaryButton); + + secondaryButton.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View v) { + performSecondaryButtonAction(); + } + }); + secondaryButton.setClickable(AccessibilityUtil.isAccessibilityEnabled(getContext())); + secondaryButton.setFocusable(AccessibilityUtil.isAccessibilityEnabled(getContext())); + secondaryButton.setAccessibilityDelegate(accessibilityDelegate); + + if (isVideoUpgradeRequest()) { + secondaryButton.setVisibility(View.INVISIBLE); + } else if (isVideoCall()) { + secondaryButton.setVisibility(View.VISIBLE); + } + + answerAndReleaseBehavior = SecondaryBehavior.ANSWER_AND_RELEASE; + answerAndReleaseBehavior.applyToView(answerAndReleaseButton); + answerAndReleaseButton.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View v) { + performAnswerAndReleaseButtonAction(); + } + }); + answerAndReleaseButton.setClickable(AccessibilityUtil.isAccessibilityEnabled(getContext())); + answerAndReleaseButton.setFocusable(AccessibilityUtil.isAccessibilityEnabled(getContext())); + answerAndReleaseButton.setAccessibilityDelegate(accessibilityDelegate); + + if (allowAnswerAndRelease()) { + answerAndReleaseButton.setVisibility(View.VISIBLE); + } else { + answerAndReleaseButton.setVisibility(View.INVISIBLE); + } + } + + @Override + public boolean allowAnswerAndRelease() { + return getArguments().getBoolean(ARG_ALLOW_ANSWER_AND_RELEASE); + } + + private boolean hasCallOnHold() { + return getArguments().getBoolean(ARG_HAS_CALL_ON_HOLD); + } + + @Override + public boolean hasPendingDialogs() { + boolean hasPendingDialogs = + textResponsesFragment != null || createCustomSmsDialogFragment != null; + LogUtil.i("AnswerFragment.hasPendingDialogs", "" + hasPendingDialogs); + return hasPendingDialogs; + } + + @Override + public void dismissPendingDialogs() { + LogUtil.i("AnswerFragment.dismissPendingDialogs", null); + if (textResponsesFragment != null) { + textResponsesFragment.dismiss(); + textResponsesFragment = null; + } + + if (createCustomSmsDialogFragment != null) { + createCustomSmsDialogFragment.dismiss(); + createCustomSmsDialogFragment = null; + } + } + + @Override + public boolean isShowingLocationUi() { + Fragment fragment = getChildFragmentManager().findFragmentById(R.id.incall_location_holder); + return fragment != null && fragment.isVisible(); + } + + @Override + public void showLocationUi(@Nullable Fragment locationUi) { + boolean isShowing = isShowingLocationUi(); + if (!isShowing && locationUi != null) { + // Show the location fragment. + getChildFragmentManager() + .beginTransaction() + .replace(R.id.incall_location_holder, locationUi) + .commitAllowingStateLoss(); + } else if (isShowing && locationUi == null) { + // Hide the location fragment + Fragment fragment = getChildFragmentManager().findFragmentById(R.id.incall_location_holder); + getChildFragmentManager().beginTransaction().remove(fragment).commitAllowingStateLoss(); + } + } + + @Override + public Fragment getAnswerScreenFragment() { + return this; + } + + private AnswerMethod getAnswerMethod() { + return ((AnswerMethod) + getChildFragmentManager().findFragmentById(R.id.answer_method_container)); + } + + @Override + public void setPrimary(PrimaryInfo primaryInfo) { + LogUtil.i("AnswerFragment.setPrimary", primaryInfo.toString()); + this.primaryInfo = primaryInfo; + updatePrimaryUI(); + updateImportanceBadgeVisibility(); + } + + private void updatePrimaryUI() { + if (getView() == null) { + return; + } + contactGridManager.setPrimary(primaryInfo); + getAnswerMethod().setShowIncomingWillDisconnect(primaryInfo.answeringDisconnectsOngoingCall); + getAnswerMethod() + .setContactPhoto( + primaryInfo.photoType == ContactPhotoType.CONTACT ? primaryInfo.photo : null); + updateDataFragment(); + + if (primaryInfo.shouldShowLocation) { + // Hide the avatar to make room for location + contactGridManager.setAvatarHidden(true); + } + } + + private void updateDataFragment() { + if (!isAdded()) { + return; + } + Fragment current = getChildFragmentManager().findFragmentById(R.id.incall_data_container); + Fragment newFragment = null; + + MultimediaData multimediaData = getSessionData(); + if (multimediaData != null + && (!TextUtils.isEmpty(multimediaData.getText()) + || (multimediaData.getImageUri() != null) + || (multimediaData.getLocation() != null && canShowMap()))) { + // Need message fragment + String subject = multimediaData.getText(); + Uri imageUri = multimediaData.getImageUri(); + Location location = multimediaData.getLocation(); + if (!(current instanceof MultimediaFragment) + || !Objects.equals(((MultimediaFragment) current).getSubject(), subject) + || !Objects.equals(((MultimediaFragment) current).getImageUri(), imageUri) + || !Objects.equals(((MultimediaFragment) current).getLocation(), location)) { + // Needs replacement + newFragment = + MultimediaFragment.newInstance( + multimediaData, + false /* isInteractive */, + !primaryInfo.isSpam /* showAvatar */, + primaryInfo.isSpam); + } + } else if (shouldShowAvatar()) { + // Needs Avatar + if (!(current instanceof AvatarFragment)) { + // Needs replacement + newFragment = new AvatarFragment(); + } + } else { + // Needs empty + if (current != null) { + getChildFragmentManager().beginTransaction().remove(current).commitNow(); + } + contactGridManager.setAvatarImageView(null, 0, false); + } + + if (newFragment != null) { + getChildFragmentManager() + .beginTransaction() + .replace(R.id.incall_data_container, newFragment) + .commitNow(); + } + } + + private boolean shouldShowAvatar() { + return !isVideoCall() && !isVideoUpgradeRequest(); + } + + private boolean canShowMap() { + return MapsComponent.get(getContext()).getMaps().isAvailable(); + } + + @Override + public void updateAvatar(AvatarPresenter avatarContainer) { + contactGridManager.setAvatarImageView( + avatarContainer.getAvatarImageView(), + avatarContainer.getAvatarSize(), + avatarContainer.shouldShowAnonymousAvatar()); + } + + @Override + public void setSecondary(@NonNull SecondaryInfo secondaryInfo) {} + + @Override + public void setCallState(@NonNull PrimaryCallState primaryCallState) { + LogUtil.i("AnswerFragment.setCallState", primaryCallState.toString()); + this.primaryCallState = primaryCallState; + contactGridManager.setCallState(primaryCallState); + } + + @Override + public void setEndCallButtonEnabled(boolean enabled, boolean animate) {} + + @Override + public void showManageConferenceCallButton(boolean visible) {} + + @Override + public boolean isManageConferenceVisible() { + return false; + } + + @Override + public void dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + contactGridManager.dispatchPopulateAccessibilityEvent(event); + // Add prompt of how to accept/decline call with swipe gesture. + if (AccessibilityUtil.isTouchExplorationEnabled(getContext())) { + event + .getText() + .add(getResources().getString(R.string.a11y_incoming_call_swipe_gesture_prompt)); + } + } + + @Override + public void showNoteSentToast() {} + + @Override + public void updateInCallScreenColors() {} + + @Override + public void onInCallScreenDialpadVisibilityChange(boolean isShowing) {} + + @Override + public int getAnswerAndDialpadContainerResourceId() { + throw Assert.createUnsupportedOperationFailException(); + } + + @Override + public Fragment getInCallScreenFragment() { + return this; + } + + @Override + public void onDestroy() { + super.onDestroy(); + } + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + Bundle arguments = getArguments(); + Assert.checkState(arguments.containsKey(ARG_CALL_ID)); + Assert.checkState(arguments.containsKey(ARG_IS_VIDEO_CALL)); + Assert.checkState(arguments.containsKey(ARG_IS_VIDEO_UPGRADE_REQUEST)); + + buttonAcceptClicked = false; + buttonRejectClicked = false; + + View view = inflater.inflate(R.layout.fragment_incoming_call, container, false); + secondaryButton = (SwipeButtonView) view.findViewById(R.id.incoming_secondary_button); + answerAndReleaseButton = (SwipeButtonView) view.findViewById(R.id.incoming_secondary_button2); + + affordanceHolderLayout = (AffordanceHolderLayout) view.findViewById(R.id.incoming_container); + affordanceHolderLayout.setAffordanceCallback(affordanceCallback); + + importanceBadge = view.findViewById(R.id.incall_important_call_badge); + importanceBadge + .getViewTreeObserver() + .addOnGlobalLayoutListener( + new OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + int leftRightPadding = importanceBadge.getHeight() / 2; + importanceBadge.setPadding( + leftRightPadding, + importanceBadge.getPaddingTop(), + leftRightPadding, + importanceBadge.getPaddingBottom()); + } + }); + updateImportanceBadgeVisibility(); + + contactGridManager = new ContactGridManager(view, null, 0, false /* showAnonymousAvatar */); + + Fragment answerMethod = + getChildFragmentManager().findFragmentById(R.id.answer_method_container); + if (AnswerMethodFactory.needsReplacement(answerMethod)) { + getChildFragmentManager() + .beginTransaction() + .replace( + R.id.answer_method_container, AnswerMethodFactory.createAnswerMethod(getActivity())) + .commitNow(); + } + + answerScreenDelegate = + FragmentUtils.getParentUnsafe(this, AnswerScreenDelegateFactory.class) + .newAnswerScreenDelegate(this); + + initSecondaryButton(); + + int flags = View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; + if (!ActivityCompat.isInMultiWindowMode(getActivity()) + && (getActivity().checkSelfPermission(permission.STATUS_BAR) + == PackageManager.PERMISSION_GRANTED)) { + LogUtil.i("AnswerFragment.onCreateView", "STATUS_BAR permission granted, disabling nav bar"); + // These flags will suppress the alert that the activity is in full view mode + // during an incoming call on a fresh system/factory reset of the app + flags |= STATUS_BAR_DISABLE_BACK | STATUS_BAR_DISABLE_HOME | STATUS_BAR_DISABLE_RECENT; + } + view.setSystemUiVisibility(flags); + if (isVideoCall() || isVideoUpgradeRequest()) { + if (VideoUtils.hasCameraPermissionAndAllowedByUser(getContext())) { + if (isSelfManagedCamera()) { + answerVideoCallScreen = new SelfManagedAnswerVideoCallScreen(getCallId(), this, view); + } else { + answerVideoCallScreen = new AnswerVideoCallScreen(getCallId(), this, view); + } + } else { + view.findViewById(R.id.videocall_video_off).setVisibility(View.VISIBLE); + } + } + + return view; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + FragmentUtils.checkParent(this, InCallScreenDelegateFactory.class); + } + + @Override + public void onViewCreated(final View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + createInCallScreenDelegate(); + updateUI(); + + if (savedInstanceState == null || !savedInstanceState.getBoolean(STATE_HAS_ANIMATED_ENTRY)) { + ViewUtil.doOnGlobalLayout(view, this::animateEntry); + } + } + + @Override + public void onResume() { + super.onResume(); + LogUtil.i("AnswerFragment.onResume", null); + restoreSwipeHintTexts(); + inCallScreenDelegate.onInCallScreenResumed(); + } + + @Override + public void onStart() { + super.onStart(); + LogUtil.i("AnswerFragment.onStart", null); + + updateUI(); + if (answerVideoCallScreen != null) { + answerVideoCallScreen.onVideoScreenStart(); + } + } + + @Override + public void onStop() { + super.onStop(); + LogUtil.i("AnswerFragment.onStop", null); + + handler.removeCallbacks(swipeHintRestoreTimer); + if (answerVideoCallScreen != null) { + answerVideoCallScreen.onVideoScreenStop(); + } + } + + @Override + public void onPause() { + super.onPause(); + LogUtil.i("AnswerFragment.onPause", null); + inCallScreenDelegate.onInCallScreenPaused(); + } + + @Override + public void onDestroyView() { + LogUtil.i("AnswerFragment.onDestroyView", null); + if (answerVideoCallScreen != null) { + answerVideoCallScreen = null; + } + super.onDestroyView(); + inCallScreenDelegate.onInCallScreenUnready(); + answerScreenDelegate.onAnswerScreenUnready(); + } + + @Override + public void onSaveInstanceState(Bundle bundle) { + super.onSaveInstanceState(bundle); + bundle.putBoolean(STATE_HAS_ANIMATED_ENTRY, hasAnimatedEntry); + } + + private void updateUI() { + if (getView() == null) { + return; + } + + if (primaryInfo != null) { + updatePrimaryUI(); + } + if (primaryCallState != null) { + contactGridManager.setCallState(primaryCallState); + } + + restoreBackgroundMaskColor(); + } + + @Override + public boolean isVideoCall() { + return getArguments().getBoolean(ARG_IS_VIDEO_CALL); + } + + public boolean isSelfManagedCamera() { + return getArguments().getBoolean(ARG_IS_SELF_MANAGED_CAMERA); + } + + @Override + public void onAnswerProgressUpdate(@FloatRange(from = -1f, to = 1f) float answerProgress) { + // Don't fade the window background for call waiting or video upgrades. Fading the background + // shows the system wallpaper which looks bad because on reject we switch to another call. + if (primaryCallState.state == State.INCOMING && !isVideoCall()) { + answerScreenDelegate.updateWindowBackgroundColor(answerProgress); + } + + // Fade and scale contact name and video call text + float startDelay = .25f; + // Header progress is zero over positiveAdjustedProgress = [0, startDelay], + // linearly increases over (startDelay, 1] until reaching 1 when positiveAdjustedProgress = 1 + float headerProgress = Math.max(0, (Math.abs(answerProgress) - 1) / (1 - startDelay) + 1); + fadeToward(contactGridManager.getContainerView(), 1 - headerProgress); + scaleToward(contactGridManager.getContainerView(), MathUtil.lerp(1f, .75f, headerProgress)); + + if (Math.abs(answerProgress) >= .0001) { + affordanceHolderLayout.animateHideLeftRightIcon(); + handler.removeCallbacks(swipeHintRestoreTimer); + restoreSwipeHintTexts(); + } + } + + @Override + public void answerFromMethod() { + acceptCallByUser(false /* answerVideoAsAudio */); + } + + @Override + public void rejectFromMethod() { + rejectCall(); + } + + @Override + public void resetAnswerProgress() { + affordanceHolderLayout.reset(true); + restoreBackgroundMaskColor(); + } + + private void animateEntry(@NonNull View rootView) { + if (!isAdded()) { + LogUtil.i( + "AnswerFragment.animateEntry", + "Not currently added to Activity. Will not start entry animation."); + return; + } + contactGridManager.getContainerView().setAlpha(0f); + Animator alpha = + ObjectAnimator.ofFloat(contactGridManager.getContainerView(), View.ALPHA, 0, 1); + Animator topRow = createTranslation(rootView.findViewById(R.id.contactgrid_top_row)); + Animator contactName = createTranslation(rootView.findViewById(R.id.contactgrid_contact_name)); + Animator bottomRow = createTranslation(rootView.findViewById(R.id.contactgrid_bottom_row)); + Animator important = createTranslation(importanceBadge); + Animator dataContainer = createTranslation(rootView.findViewById(R.id.incall_data_container)); + + AnimatorSet animatorSet = new AnimatorSet(); + AnimatorSet.Builder builder = animatorSet.play(alpha); + builder.with(topRow).with(contactName).with(bottomRow).with(important).with(dataContainer); + if (isShowingLocationUi()) { + builder.with(createTranslation(rootView.findViewById(R.id.incall_location_holder))); + } + animatorSet.setDuration( + rootView.getResources().getInteger(R.integer.answer_animate_entry_millis)); + animatorSet.addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + hasAnimatedEntry = true; + } + }); + animatorSet.start(); + } + + private ObjectAnimator createTranslation(View view) { + float translationY = view.getTop() * 0.5f; + ObjectAnimator animator = ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, translationY, 0); + animator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN); + return animator; + } + + private void acceptCallByUser(boolean answerVideoAsAudio) { + LogUtil.i("AnswerFragment.acceptCallByUser", answerVideoAsAudio ? " answerVideoAsAudio" : ""); + if (!buttonAcceptClicked) { + answerScreenDelegate.onAnswer(answerVideoAsAudio); + buttonAcceptClicked = true; + } + } + + private void rejectCall() { + LogUtil.i("AnswerFragment.rejectCall", null); + if (!buttonRejectClicked) { + Context context = getContext(); + if (context == null) { + LogUtil.w( + "AnswerFragment.rejectCall", + "Null context when rejecting call. Logger call was skipped"); + } else { + Logger.get(context) + .logImpression(DialerImpression.Type.REJECT_INCOMING_CALL_FROM_ANSWER_SCREEN); + } + buttonRejectClicked = true; + answerScreenDelegate.onReject(); + } + } + + private void restoreBackgroundMaskColor() { + answerScreenDelegate.updateWindowBackgroundColor(0); + } + + private void restoreSwipeHintTexts() { + if (getAnswerMethod() != null) { + if (allowAnswerAndRelease()) { + if (hasCallOnHold()) { + getAnswerMethod() + .setHintText(getText(R.string.call_incoming_default_label_answer_and_release_third)); + } else { + getAnswerMethod() + .setHintText(getText(R.string.call_incoming_default_label_answer_and_release_second)); + } + } else { + getAnswerMethod().setHintText(null); + } + } + } + + private void showMessageMenu() { + LogUtil.i("AnswerFragment.showMessageMenu", "Show sms menu."); + if (getChildFragmentManager().isDestroyed()) { + return; + } + + textResponsesFragment = SmsBottomSheetFragment.newInstance(textResponses); + textResponsesFragment.show(getChildFragmentManager(), null); + secondaryButton + .animate() + .alpha(0) + .withEndAction( + new Runnable() { + @Override + public void run() { + affordanceHolderLayout.reset(false); + secondaryButton.animate().alpha(1); + } + }); + } + + @Override + public void smsSelected(@Nullable CharSequence text) { + LogUtil.i("AnswerFragment.smsSelected", null); + textResponsesFragment = null; + + if (text == null) { + createCustomSmsDialogFragment = CreateCustomSmsDialogFragment.newInstance(); + createCustomSmsDialogFragment.show(getChildFragmentManager(), null); + return; + } + + if (primaryCallState != null && canRejectCallWithSms()) { + rejectCall(); + answerScreenDelegate.onRejectCallWithMessage(text.toString()); + } + } + + @Override + public void smsDismissed() { + LogUtil.i("AnswerFragment.smsDismissed", null); + textResponsesFragment = null; + answerScreenDelegate.onDismissDialog(); + } + + @Override + public void customSmsCreated(@NonNull CharSequence text) { + LogUtil.i("AnswerFragment.customSmsCreated", null); + createCustomSmsDialogFragment = null; + if (primaryCallState != null && canRejectCallWithSms()) { + rejectCall(); + answerScreenDelegate.onRejectCallWithMessage(text.toString()); + } + } + + @Override + public void customSmsDismissed() { + LogUtil.i("AnswerFragment.customSmsDismissed", null); + createCustomSmsDialogFragment = null; + answerScreenDelegate.onDismissDialog(); + } + + private boolean canRejectCallWithSms() { + return primaryCallState != null + && !(primaryCallState.state == State.DISCONNECTED + || primaryCallState.state == State.DISCONNECTING + || primaryCallState.state == State.IDLE); + } + + private void createInCallScreenDelegate() { + inCallScreenDelegate = + FragmentUtils.getParentUnsafe(this, InCallScreenDelegateFactory.class) + .newInCallScreenDelegate(); + Assert.isNotNull(inCallScreenDelegate); + inCallScreenDelegate.onInCallScreenDelegateInit(this); + inCallScreenDelegate.onInCallScreenReady(); + } + + private void updateImportanceBadgeVisibility() { + if (!isAdded()) { + return; + } + + if (!getResources().getBoolean(R.bool.answer_important_call_allowed) || primaryInfo.isSpam) { + importanceBadge.setVisibility(View.GONE); + return; + } + + MultimediaData multimediaData = getSessionData(); + boolean showImportant = multimediaData != null && multimediaData.isImportant(); + TransitionManager.beginDelayedTransition((ViewGroup) importanceBadge.getParent()); + // TODO (keyboardr): Change this back to being View.INVISIBLE once mocks are available to + // properly handle smaller screens + importanceBadge.setVisibility(showImportant ? View.VISIBLE : View.GONE); + } + + @Nullable + private MultimediaData getSessionData() { + if (primaryInfo == null) { + return null; + } + if (isVideoUpgradeRequest()) { + return null; + } + return primaryInfo.multimediaData; + } + + /** Shows the Avatar image if available. */ + public static class AvatarFragment extends Fragment implements AvatarPresenter { + + private ImageView avatarImageView; + + @Nullable + @Override + public View onCreateView( + LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) { + return layoutInflater.inflate(R.layout.fragment_avatar, viewGroup, false); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle bundle) { + super.onViewCreated(view, bundle); + avatarImageView = ((ImageView) view.findViewById(R.id.contactgrid_avatar)); + FragmentUtils.getParentUnsafe(this, MultimediaFragment.Holder.class).updateAvatar(this); + } + + @NonNull + @Override + public ImageView getAvatarImageView() { + return avatarImageView; + } + + @Override + public int getAvatarSize() { + return getResources().getDimensionPixelSize(R.dimen.answer_avatar_size); + } + + @Override + public boolean shouldShowAnonymousAvatar() { + return false; + } + } +} |