summaryrefslogtreecommitdiff
path: root/java/com/android/incallui/answer/impl/AnswerFragment.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/android/incallui/answer/impl/AnswerFragment.java')
-rw-r--r--java/com/android/incallui/answer/impl/AnswerFragment.java1085
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;
+ }
+ }
+}