1/*
2 * Copyright (C) 2016 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License
15 */
16
17package com.android.incallui.answer.impl;
18
19import android.Manifest.permission;
20import android.animation.Animator;
21import android.animation.AnimatorListenerAdapter;
22import android.animation.AnimatorSet;
23import android.animation.ObjectAnimator;
24import android.annotation.SuppressLint;
25import android.content.Context;
26import android.content.pm.PackageManager;
27import android.location.Location;
28import android.net.Uri;
29import android.os.Bundle;
30import android.os.Handler;
31import android.os.Looper;
32import android.support.annotation.DrawableRes;
33import android.support.annotation.FloatRange;
34import android.support.annotation.NonNull;
35import android.support.annotation.Nullable;
36import android.support.annotation.StringRes;
37import android.support.annotation.VisibleForTesting;
38import android.support.v4.app.Fragment;
39import android.text.TextUtils;
40import android.transition.TransitionManager;
41import android.view.LayoutInflater;
42import android.view.View;
43import android.view.View.AccessibilityDelegate;
44import android.view.View.OnClickListener;
45import android.view.ViewGroup;
46import android.view.ViewTreeObserver.OnGlobalLayoutListener;
47import android.view.accessibility.AccessibilityEvent;
48import android.view.accessibility.AccessibilityNodeInfo;
49import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
50import android.widget.ImageView;
51import com.android.dialer.common.Assert;
52import com.android.dialer.common.FragmentUtils;
53import com.android.dialer.common.LogUtil;
54import com.android.dialer.common.MathUtil;
55import com.android.dialer.compat.ActivityCompat;
56import com.android.dialer.logging.DialerImpression;
57import com.android.dialer.logging.Logger;
58import com.android.dialer.multimedia.MultimediaData;
59import com.android.dialer.util.ViewUtil;
60import com.android.incallui.answer.impl.CreateCustomSmsDialogFragment.CreateCustomSmsHolder;
61import com.android.incallui.answer.impl.SmsBottomSheetFragment.SmsSheetHolder;
62import com.android.incallui.answer.impl.affordance.SwipeButtonHelper.Callback;
63import com.android.incallui.answer.impl.affordance.SwipeButtonView;
64import com.android.incallui.answer.impl.answermethod.AnswerMethod;
65import com.android.incallui.answer.impl.answermethod.AnswerMethodFactory;
66import com.android.incallui.answer.impl.answermethod.AnswerMethodHolder;
67import com.android.incallui.answer.impl.utils.Interpolators;
68import com.android.incallui.answer.protocol.AnswerScreen;
69import com.android.incallui.answer.protocol.AnswerScreenDelegate;
70import com.android.incallui.answer.protocol.AnswerScreenDelegateFactory;
71import com.android.incallui.call.DialerCall.State;
72import com.android.incallui.contactgrid.ContactGridManager;
73import com.android.incallui.incall.protocol.ContactPhotoType;
74import com.android.incallui.incall.protocol.InCallScreen;
75import com.android.incallui.incall.protocol.InCallScreenDelegate;
76import com.android.incallui.incall.protocol.InCallScreenDelegateFactory;
77import com.android.incallui.incall.protocol.PrimaryCallState;
78import com.android.incallui.incall.protocol.PrimaryInfo;
79import com.android.incallui.incall.protocol.SecondaryInfo;
80import com.android.incallui.maps.MapsComponent;
81import com.android.incallui.sessiondata.AvatarPresenter;
82import com.android.incallui.sessiondata.MultimediaFragment;
83import com.android.incallui.util.AccessibilityUtil;
84import com.android.incallui.video.protocol.VideoCallScreen;
85import com.android.incallui.videotech.utils.VideoUtils;
86import java.util.ArrayList;
87import java.util.List;
88import java.util.Objects;
89
90/** The new version of the incoming call screen. */
91@SuppressLint("ClickableViewAccessibility")
92public class AnswerFragment extends Fragment
93    implements AnswerScreen,
94        InCallScreen,
95        SmsSheetHolder,
96        CreateCustomSmsHolder,
97        AnswerMethodHolder,
98        MultimediaFragment.Holder {
99
100  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
101  static final String ARG_CALL_ID = "call_id";
102
103  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
104  static final String ARG_IS_VIDEO_CALL = "is_video_call";
105
106  static final String ARG_ALLOW_ANSWER_AND_RELEASE = "allow_answer_and_release";
107
108  static final String ARG_HAS_CALL_ON_HOLD = "has_call_on_hold";
109
110  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
111  static final String ARG_IS_VIDEO_UPGRADE_REQUEST = "is_video_upgrade_request";
112
113  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
114  static final String ARG_IS_SELF_MANAGED_CAMERA = "is_self_managed_camera";
115
116  private static final String STATE_HAS_ANIMATED_ENTRY = "hasAnimated";
117
118  private static final int HINT_SECONDARY_SHOW_DURATION_MILLIS = 5000;
119  private static final float ANIMATE_LERP_PROGRESS = 0.5f;
120  private static final int STATUS_BAR_DISABLE_RECENT = 0x01000000;
121  private static final int STATUS_BAR_DISABLE_HOME = 0x00200000;
122  private static final int STATUS_BAR_DISABLE_BACK = 0x00400000;
123
124  private static void fadeToward(View view, float newAlpha) {
125    view.setAlpha(MathUtil.lerp(view.getAlpha(), newAlpha, ANIMATE_LERP_PROGRESS));
126  }
127
128  private static void scaleToward(View view, float newScale) {
129    view.setScaleX(MathUtil.lerp(view.getScaleX(), newScale, ANIMATE_LERP_PROGRESS));
130    view.setScaleY(MathUtil.lerp(view.getScaleY(), newScale, ANIMATE_LERP_PROGRESS));
131  }
132
133  private AnswerScreenDelegate answerScreenDelegate;
134  private InCallScreenDelegate inCallScreenDelegate;
135
136  private View importanceBadge;
137  private SwipeButtonView secondaryButton;
138  private SwipeButtonView answerAndReleaseButton;
139  private AffordanceHolderLayout affordanceHolderLayout;
140  // Use these flags to prevent user from clicking accept/reject buttons multiple times.
141  // We use separate flags because in some rare cases accepting a call may fail to join the room,
142  // and then user is stuck in the incoming call view until it times out. Two flags at least give
143  // the user a chance to get out of the CallActivity.
144  private boolean buttonAcceptClicked;
145  private boolean buttonRejectClicked;
146  private boolean hasAnimatedEntry;
147  private PrimaryInfo primaryInfo = PrimaryInfo.createEmptyPrimaryInfo();
148  private PrimaryCallState primaryCallState;
149  private ArrayList<CharSequence> textResponses;
150  private SmsBottomSheetFragment textResponsesFragment;
151  private CreateCustomSmsDialogFragment createCustomSmsDialogFragment;
152  private SecondaryBehavior secondaryBehavior = SecondaryBehavior.REJECT_WITH_SMS;
153  private SecondaryBehavior answerAndReleaseBehavior;
154  private ContactGridManager contactGridManager;
155  private VideoCallScreen answerVideoCallScreen;
156  private Handler handler = new Handler(Looper.getMainLooper());
157
158  private enum SecondaryBehavior {
159    REJECT_WITH_SMS(
160        R.drawable.quantum_ic_message_white_24,
161        R.string.a11y_description_incoming_call_reject_with_sms,
162        R.string.a11y_incoming_call_reject_with_sms,
163        R.string.call_incoming_swipe_to_decline_with_message) {
164      @Override
165      public void performAction(AnswerFragment fragment) {
166        fragment.showMessageMenu();
167      }
168    },
169
170    ANSWER_VIDEO_AS_AUDIO(
171        R.drawable.quantum_ic_videocam_off_white_24,
172        R.string.a11y_description_incoming_call_answer_video_as_audio,
173        R.string.a11y_incoming_call_answer_video_as_audio,
174        R.string.call_incoming_swipe_to_answer_video_as_audio) {
175      @Override
176      public void performAction(AnswerFragment fragment) {
177        fragment.acceptCallByUser(true /* answerVideoAsAudio */);
178      }
179    },
180
181    ANSWER_AND_RELEASE(
182        R.drawable.ic_end_answer_32,
183        R.string.a11y_description_incoming_call_answer_and_release,
184        R.string.a11y_incoming_call_answer_and_release,
185        R.string.call_incoming_swipe_to_answer_and_release) {
186      @Override
187      public void performAction(AnswerFragment fragment) {
188        fragment.performAnswerAndRelease();
189      }
190    };
191
192    @DrawableRes public final int icon;
193    @StringRes public final int contentDescription;
194    @StringRes public final int accessibilityLabel;
195    @StringRes public final int hintText;
196
197    SecondaryBehavior(
198        @DrawableRes int icon,
199        @StringRes int contentDescription,
200        @StringRes int accessibilityLabel,
201        @StringRes int hintText) {
202      this.icon = icon;
203      this.contentDescription = contentDescription;
204      this.accessibilityLabel = accessibilityLabel;
205      this.hintText = hintText;
206    }
207
208    public abstract void performAction(AnswerFragment fragment);
209
210    public void applyToView(ImageView view) {
211      view.setImageResource(icon);
212      view.setContentDescription(view.getContext().getText(contentDescription));
213    }
214  }
215
216  private void performAnswerAndRelease() {
217    restoreAnswerAndReleaseButtonAnimation();
218    answerScreenDelegate.onAnswerAndReleaseCall();
219  }
220
221  private void restoreAnswerAndReleaseButtonAnimation() {
222    answerAndReleaseButton
223        .animate()
224        .alpha(0)
225        .withEndAction(
226            new Runnable() {
227              @Override
228              public void run() {
229                affordanceHolderLayout.reset(false);
230                secondaryButton.animate().alpha(1);
231              }
232            });
233  }
234
235  private final AccessibilityDelegate accessibilityDelegate =
236      new AccessibilityDelegate() {
237        @Override
238        public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
239          super.onInitializeAccessibilityNodeInfo(host, info);
240          if (host == secondaryButton) {
241            CharSequence label = getText(secondaryBehavior.accessibilityLabel);
242            info.addAction(new AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, label));
243          } else if (host == answerAndReleaseButton) {
244            CharSequence label = getText(answerAndReleaseBehavior.accessibilityLabel);
245            info.addAction(new AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, label));
246          }
247        }
248
249        @Override
250        public boolean performAccessibilityAction(View host, int action, Bundle args) {
251          if (action == AccessibilityNodeInfo.ACTION_CLICK) {
252            if (host == secondaryButton) {
253              performSecondaryButtonAction();
254              return true;
255            } else if (host == answerAndReleaseButton) {
256              performAnswerAndReleaseButtonAction();
257              return true;
258            }
259          }
260          return super.performAccessibilityAction(host, action, args);
261        }
262      };
263
264  private final Callback affordanceCallback =
265      new Callback() {
266        @Override
267        public void onAnimationToSideStarted(boolean rightPage, float translation, float vel) {}
268
269        @Override
270        public void onAnimationToSideEnded(boolean rightPage) {
271          if (rightPage) {
272            performAnswerAndReleaseButtonAction();
273          } else {
274            performSecondaryButtonAction();
275          }
276        }
277
278        @Override
279        public float getMaxTranslationDistance() {
280          View view = getView();
281          if (view == null) {
282            return 0;
283          }
284          return (float) Math.hypot(view.getWidth(), view.getHeight());
285        }
286
287        @Override
288        public void onSwipingStarted(boolean rightIcon) {}
289
290        @Override
291        public void onSwipingAborted() {}
292
293        @Override
294        public void onIconClicked(boolean rightIcon) {
295          affordanceHolderLayout.startHintAnimation(rightIcon, null);
296          getAnswerMethod()
297              .setHintText(
298                  rightIcon
299                      ? getText(answerAndReleaseBehavior.hintText)
300                      : getText(secondaryBehavior.hintText));
301          handler.removeCallbacks(swipeHintRestoreTimer);
302          handler.postDelayed(swipeHintRestoreTimer, HINT_SECONDARY_SHOW_DURATION_MILLIS);
303        }
304
305        @Override
306        public SwipeButtonView getLeftIcon() {
307          return secondaryButton;
308        }
309
310        @Override
311        public SwipeButtonView getRightIcon() {
312          return answerAndReleaseButton;
313        }
314
315        @Override
316        public View getLeftPreview() {
317          return null;
318        }
319
320        @Override
321        public View getRightPreview() {
322          return null;
323        }
324
325        @Override
326        public float getAffordanceFalsingFactor() {
327          return 1.0f;
328        }
329      };
330
331  private Runnable swipeHintRestoreTimer = this::restoreSwipeHintTexts;
332
333  private void performSecondaryButtonAction() {
334    secondaryBehavior.performAction(this);
335  }
336
337  private void performAnswerAndReleaseButtonAction() {
338    answerAndReleaseBehavior.performAction(this);
339  }
340
341  public static AnswerFragment newInstance(
342      String callId,
343      boolean isVideoCall,
344      boolean isVideoUpgradeRequest,
345      boolean isSelfManagedCamera,
346      boolean allowAnswerAndRelease,
347      boolean hasCallOnHold) {
348    Bundle bundle = new Bundle();
349    bundle.putString(ARG_CALL_ID, Assert.isNotNull(callId));
350    bundle.putBoolean(ARG_IS_VIDEO_CALL, isVideoCall);
351    bundle.putBoolean(ARG_IS_VIDEO_UPGRADE_REQUEST, isVideoUpgradeRequest);
352    bundle.putBoolean(ARG_IS_SELF_MANAGED_CAMERA, isSelfManagedCamera);
353    bundle.putBoolean(ARG_ALLOW_ANSWER_AND_RELEASE, allowAnswerAndRelease);
354    bundle.putBoolean(ARG_HAS_CALL_ON_HOLD, hasCallOnHold);
355
356    AnswerFragment instance = new AnswerFragment();
357    instance.setArguments(bundle);
358    return instance;
359  }
360
361  @Override
362  @NonNull
363  public String getCallId() {
364    return Assert.isNotNull(getArguments().getString(ARG_CALL_ID));
365  }
366
367  @Override
368  public boolean isVideoUpgradeRequest() {
369    return getArguments().getBoolean(ARG_IS_VIDEO_UPGRADE_REQUEST);
370  }
371
372  @Override
373  public void setTextResponses(List<String> textResponses) {
374    if (isVideoCall() || isVideoUpgradeRequest()) {
375      LogUtil.i("AnswerFragment.setTextResponses", "no-op for video calls");
376    } else if (textResponses == null) {
377      LogUtil.i("AnswerFragment.setTextResponses", "no text responses, hiding secondary button");
378      this.textResponses = null;
379      secondaryButton.setVisibility(View.INVISIBLE);
380    } else if (ActivityCompat.isInMultiWindowMode(getActivity())) {
381      LogUtil.i("AnswerFragment.setTextResponses", "in multiwindow, hiding secondary button");
382      this.textResponses = null;
383      secondaryButton.setVisibility(View.INVISIBLE);
384    } else {
385      LogUtil.i("AnswerFragment.setTextResponses", "textResponses.size: " + textResponses.size());
386      this.textResponses = new ArrayList<>(textResponses);
387      secondaryButton.setVisibility(View.VISIBLE);
388    }
389  }
390
391  private void initSecondaryButton() {
392    secondaryBehavior =
393        isVideoCall() || isVideoUpgradeRequest()
394            ? SecondaryBehavior.ANSWER_VIDEO_AS_AUDIO
395            : SecondaryBehavior.REJECT_WITH_SMS;
396    secondaryBehavior.applyToView(secondaryButton);
397
398    secondaryButton.setOnClickListener(
399        new OnClickListener() {
400          @Override
401          public void onClick(View v) {
402            performSecondaryButtonAction();
403          }
404        });
405    secondaryButton.setClickable(AccessibilityUtil.isAccessibilityEnabled(getContext()));
406    secondaryButton.setFocusable(AccessibilityUtil.isAccessibilityEnabled(getContext()));
407    secondaryButton.setAccessibilityDelegate(accessibilityDelegate);
408
409    if (isVideoUpgradeRequest()) {
410      secondaryButton.setVisibility(View.INVISIBLE);
411    } else if (isVideoCall()) {
412      secondaryButton.setVisibility(View.VISIBLE);
413    }
414
415    answerAndReleaseBehavior = SecondaryBehavior.ANSWER_AND_RELEASE;
416    answerAndReleaseBehavior.applyToView(answerAndReleaseButton);
417    answerAndReleaseButton.setOnClickListener(
418        new OnClickListener() {
419          @Override
420          public void onClick(View v) {
421            performAnswerAndReleaseButtonAction();
422          }
423        });
424    answerAndReleaseButton.setClickable(AccessibilityUtil.isAccessibilityEnabled(getContext()));
425    answerAndReleaseButton.setFocusable(AccessibilityUtil.isAccessibilityEnabled(getContext()));
426    answerAndReleaseButton.setAccessibilityDelegate(accessibilityDelegate);
427
428    if (allowAnswerAndRelease()) {
429      answerAndReleaseButton.setVisibility(View.VISIBLE);
430    } else {
431      answerAndReleaseButton.setVisibility(View.INVISIBLE);
432    }
433  }
434
435  @Override
436  public boolean allowAnswerAndRelease() {
437    return getArguments().getBoolean(ARG_ALLOW_ANSWER_AND_RELEASE);
438  }
439
440  private boolean hasCallOnHold() {
441    return getArguments().getBoolean(ARG_HAS_CALL_ON_HOLD);
442  }
443
444  @Override
445  public boolean hasPendingDialogs() {
446    boolean hasPendingDialogs =
447        textResponsesFragment != null || createCustomSmsDialogFragment != null;
448    LogUtil.i("AnswerFragment.hasPendingDialogs", "" + hasPendingDialogs);
449    return hasPendingDialogs;
450  }
451
452  @Override
453  public void dismissPendingDialogs() {
454    LogUtil.i("AnswerFragment.dismissPendingDialogs", null);
455    if (textResponsesFragment != null) {
456      textResponsesFragment.dismiss();
457      textResponsesFragment = null;
458    }
459
460    if (createCustomSmsDialogFragment != null) {
461      createCustomSmsDialogFragment.dismiss();
462      createCustomSmsDialogFragment = null;
463    }
464  }
465
466  @Override
467  public boolean isShowingLocationUi() {
468    Fragment fragment = getChildFragmentManager().findFragmentById(R.id.incall_location_holder);
469    return fragment != null && fragment.isVisible();
470  }
471
472  @Override
473  public void showLocationUi(@Nullable Fragment locationUi) {
474    boolean isShowing = isShowingLocationUi();
475    if (!isShowing && locationUi != null) {
476      // Show the location fragment.
477      getChildFragmentManager()
478          .beginTransaction()
479          .replace(R.id.incall_location_holder, locationUi)
480          .commitAllowingStateLoss();
481    } else if (isShowing && locationUi == null) {
482      // Hide the location fragment
483      Fragment fragment = getChildFragmentManager().findFragmentById(R.id.incall_location_holder);
484      getChildFragmentManager().beginTransaction().remove(fragment).commitAllowingStateLoss();
485    }
486  }
487
488  @Override
489  public Fragment getAnswerScreenFragment() {
490    return this;
491  }
492
493  private AnswerMethod getAnswerMethod() {
494    return ((AnswerMethod)
495        getChildFragmentManager().findFragmentById(R.id.answer_method_container));
496  }
497
498  @Override
499  public void setPrimary(PrimaryInfo primaryInfo) {
500    LogUtil.i("AnswerFragment.setPrimary", primaryInfo.toString());
501    this.primaryInfo = primaryInfo;
502    updatePrimaryUI();
503    updateImportanceBadgeVisibility();
504  }
505
506  private void updatePrimaryUI() {
507    if (getView() == null) {
508      return;
509    }
510    contactGridManager.setPrimary(primaryInfo);
511    getAnswerMethod().setShowIncomingWillDisconnect(primaryInfo.answeringDisconnectsOngoingCall);
512    getAnswerMethod()
513        .setContactPhoto(
514            primaryInfo.photoType == ContactPhotoType.CONTACT ? primaryInfo.photo : null);
515    updateDataFragment();
516
517    if (primaryInfo.shouldShowLocation) {
518      // Hide the avatar to make room for location
519      contactGridManager.setAvatarHidden(true);
520    }
521  }
522
523  private void updateDataFragment() {
524    if (!isAdded()) {
525      return;
526    }
527    Fragment current = getChildFragmentManager().findFragmentById(R.id.incall_data_container);
528    Fragment newFragment = null;
529
530    MultimediaData multimediaData = getSessionData();
531    if (multimediaData != null
532        && (!TextUtils.isEmpty(multimediaData.getText())
533            || (multimediaData.getImageUri() != null)
534            || (multimediaData.getLocation() != null && canShowMap()))) {
535      // Need message fragment
536      String subject = multimediaData.getText();
537      Uri imageUri = multimediaData.getImageUri();
538      Location location = multimediaData.getLocation();
539      if (!(current instanceof MultimediaFragment)
540          || !Objects.equals(((MultimediaFragment) current).getSubject(), subject)
541          || !Objects.equals(((MultimediaFragment) current).getImageUri(), imageUri)
542          || !Objects.equals(((MultimediaFragment) current).getLocation(), location)) {
543        // Needs replacement
544        newFragment =
545            MultimediaFragment.newInstance(
546                multimediaData,
547                false /* isInteractive */,
548                !primaryInfo.isSpam /* showAvatar */,
549                primaryInfo.isSpam);
550      }
551    } else if (shouldShowAvatar()) {
552      // Needs Avatar
553      if (!(current instanceof AvatarFragment)) {
554        // Needs replacement
555        newFragment = new AvatarFragment();
556      }
557    } else {
558      // Needs empty
559      if (current != null) {
560        getChildFragmentManager().beginTransaction().remove(current).commitNow();
561      }
562      contactGridManager.setAvatarImageView(null, 0, false);
563    }
564
565    if (newFragment != null) {
566      getChildFragmentManager()
567          .beginTransaction()
568          .replace(R.id.incall_data_container, newFragment)
569          .commitNow();
570    }
571  }
572
573  private boolean shouldShowAvatar() {
574    return !isVideoCall() && !isVideoUpgradeRequest();
575  }
576
577  private boolean canShowMap() {
578    return MapsComponent.get(getContext()).getMaps().isAvailable();
579  }
580
581  @Override
582  public void updateAvatar(AvatarPresenter avatarContainer) {
583    contactGridManager.setAvatarImageView(
584        avatarContainer.getAvatarImageView(),
585        avatarContainer.getAvatarSize(),
586        avatarContainer.shouldShowAnonymousAvatar());
587  }
588
589  @Override
590  public void setSecondary(@NonNull SecondaryInfo secondaryInfo) {}
591
592  @Override
593  public void setCallState(@NonNull PrimaryCallState primaryCallState) {
594    LogUtil.i("AnswerFragment.setCallState", primaryCallState.toString());
595    this.primaryCallState = primaryCallState;
596    contactGridManager.setCallState(primaryCallState);
597  }
598
599  @Override
600  public void setEndCallButtonEnabled(boolean enabled, boolean animate) {}
601
602  @Override
603  public void showManageConferenceCallButton(boolean visible) {}
604
605  @Override
606  public boolean isManageConferenceVisible() {
607    return false;
608  }
609
610  @Override
611  public void dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
612    contactGridManager.dispatchPopulateAccessibilityEvent(event);
613    // Add prompt of how to accept/decline call with swipe gesture.
614    if (AccessibilityUtil.isTouchExplorationEnabled(getContext())) {
615      event
616          .getText()
617          .add(getResources().getString(R.string.a11y_incoming_call_swipe_gesture_prompt));
618    }
619  }
620
621  @Override
622  public void showNoteSentToast() {}
623
624  @Override
625  public void updateInCallScreenColors() {}
626
627  @Override
628  public void onInCallScreenDialpadVisibilityChange(boolean isShowing) {}
629
630  @Override
631  public int getAnswerAndDialpadContainerResourceId() {
632    throw Assert.createUnsupportedOperationFailException();
633  }
634
635  @Override
636  public Fragment getInCallScreenFragment() {
637    return this;
638  }
639
640  @Override
641  public void onDestroy() {
642    super.onDestroy();
643  }
644
645  @Override
646  public View onCreateView(
647      LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
648    Bundle arguments = getArguments();
649    Assert.checkState(arguments.containsKey(ARG_CALL_ID));
650    Assert.checkState(arguments.containsKey(ARG_IS_VIDEO_CALL));
651    Assert.checkState(arguments.containsKey(ARG_IS_VIDEO_UPGRADE_REQUEST));
652
653    buttonAcceptClicked = false;
654    buttonRejectClicked = false;
655
656    View view = inflater.inflate(R.layout.fragment_incoming_call, container, false);
657    secondaryButton = (SwipeButtonView) view.findViewById(R.id.incoming_secondary_button);
658    answerAndReleaseButton = (SwipeButtonView) view.findViewById(R.id.incoming_secondary_button2);
659
660    affordanceHolderLayout = (AffordanceHolderLayout) view.findViewById(R.id.incoming_container);
661    affordanceHolderLayout.setAffordanceCallback(affordanceCallback);
662
663    importanceBadge = view.findViewById(R.id.incall_important_call_badge);
664    importanceBadge
665        .getViewTreeObserver()
666        .addOnGlobalLayoutListener(
667            new OnGlobalLayoutListener() {
668              @Override
669              public void onGlobalLayout() {
670                int leftRightPadding = importanceBadge.getHeight() / 2;
671                importanceBadge.setPadding(
672                    leftRightPadding,
673                    importanceBadge.getPaddingTop(),
674                    leftRightPadding,
675                    importanceBadge.getPaddingBottom());
676              }
677            });
678    updateImportanceBadgeVisibility();
679
680    contactGridManager = new ContactGridManager(view, null, 0, false /* showAnonymousAvatar */);
681
682    Fragment answerMethod =
683        getChildFragmentManager().findFragmentById(R.id.answer_method_container);
684    if (AnswerMethodFactory.needsReplacement(answerMethod)) {
685      getChildFragmentManager()
686          .beginTransaction()
687          .replace(
688              R.id.answer_method_container, AnswerMethodFactory.createAnswerMethod(getActivity()))
689          .commitNow();
690    }
691
692    answerScreenDelegate =
693        FragmentUtils.getParentUnsafe(this, AnswerScreenDelegateFactory.class)
694            .newAnswerScreenDelegate(this);
695
696    initSecondaryButton();
697
698    int flags = View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
699    if (!ActivityCompat.isInMultiWindowMode(getActivity())
700        && (getActivity().checkSelfPermission(permission.STATUS_BAR)
701            == PackageManager.PERMISSION_GRANTED)) {
702      LogUtil.i("AnswerFragment.onCreateView", "STATUS_BAR permission granted, disabling nav bar");
703      // These flags will suppress the alert that the activity is in full view mode
704      // during an incoming call on a fresh system/factory reset of the app
705      flags |= STATUS_BAR_DISABLE_BACK | STATUS_BAR_DISABLE_HOME | STATUS_BAR_DISABLE_RECENT;
706    }
707    view.setSystemUiVisibility(flags);
708    if (isVideoCall() || isVideoUpgradeRequest()) {
709      if (VideoUtils.hasCameraPermissionAndAllowedByUser(getContext())) {
710        if (isSelfManagedCamera()) {
711          answerVideoCallScreen = new SelfManagedAnswerVideoCallScreen(getCallId(), this, view);
712        } else {
713          answerVideoCallScreen = new AnswerVideoCallScreen(getCallId(), this, view);
714        }
715      } else {
716        view.findViewById(R.id.videocall_video_off).setVisibility(View.VISIBLE);
717      }
718    }
719
720    return view;
721  }
722
723  @Override
724  public void onAttach(Context context) {
725    super.onAttach(context);
726    FragmentUtils.checkParent(this, InCallScreenDelegateFactory.class);
727  }
728
729  @Override
730  public void onViewCreated(final View view, @Nullable Bundle savedInstanceState) {
731    super.onViewCreated(view, savedInstanceState);
732    createInCallScreenDelegate();
733    updateUI();
734
735    if (savedInstanceState == null || !savedInstanceState.getBoolean(STATE_HAS_ANIMATED_ENTRY)) {
736      ViewUtil.doOnGlobalLayout(view, this::animateEntry);
737    }
738  }
739
740  @Override
741  public void onResume() {
742    super.onResume();
743    LogUtil.i("AnswerFragment.onResume", null);
744    restoreSwipeHintTexts();
745    inCallScreenDelegate.onInCallScreenResumed();
746  }
747
748  @Override
749  public void onStart() {
750    super.onStart();
751    LogUtil.i("AnswerFragment.onStart", null);
752
753    updateUI();
754    if (answerVideoCallScreen != null) {
755      answerVideoCallScreen.onVideoScreenStart();
756    }
757  }
758
759  @Override
760  public void onStop() {
761    super.onStop();
762    LogUtil.i("AnswerFragment.onStop", null);
763
764    handler.removeCallbacks(swipeHintRestoreTimer);
765    if (answerVideoCallScreen != null) {
766      answerVideoCallScreen.onVideoScreenStop();
767    }
768  }
769
770  @Override
771  public void onPause() {
772    super.onPause();
773    LogUtil.i("AnswerFragment.onPause", null);
774    inCallScreenDelegate.onInCallScreenPaused();
775  }
776
777  @Override
778  public void onDestroyView() {
779    LogUtil.i("AnswerFragment.onDestroyView", null);
780    if (answerVideoCallScreen != null) {
781      answerVideoCallScreen = null;
782    }
783    super.onDestroyView();
784    inCallScreenDelegate.onInCallScreenUnready();
785    answerScreenDelegate.onAnswerScreenUnready();
786  }
787
788  @Override
789  public void onSaveInstanceState(Bundle bundle) {
790    super.onSaveInstanceState(bundle);
791    bundle.putBoolean(STATE_HAS_ANIMATED_ENTRY, hasAnimatedEntry);
792  }
793
794  private void updateUI() {
795    if (getView() == null) {
796      return;
797    }
798
799    if (primaryInfo != null) {
800      updatePrimaryUI();
801    }
802    if (primaryCallState != null) {
803      contactGridManager.setCallState(primaryCallState);
804    }
805
806    restoreBackgroundMaskColor();
807  }
808
809  @Override
810  public boolean isVideoCall() {
811    return getArguments().getBoolean(ARG_IS_VIDEO_CALL);
812  }
813
814  public boolean isSelfManagedCamera() {
815    return getArguments().getBoolean(ARG_IS_SELF_MANAGED_CAMERA);
816  }
817
818  @Override
819  public void onAnswerProgressUpdate(@FloatRange(from = -1f, to = 1f) float answerProgress) {
820    // Don't fade the window background for call waiting or video upgrades. Fading the background
821    // shows the system wallpaper which looks bad because on reject we switch to another call.
822    if (primaryCallState.state == State.INCOMING && !isVideoCall()) {
823      answerScreenDelegate.updateWindowBackgroundColor(answerProgress);
824    }
825
826    // Fade and scale contact name and video call text
827    float startDelay = .25f;
828    // Header progress is zero over positiveAdjustedProgress = [0, startDelay],
829    // linearly increases over (startDelay, 1] until reaching 1 when positiveAdjustedProgress = 1
830    float headerProgress = Math.max(0, (Math.abs(answerProgress) - 1) / (1 - startDelay) + 1);
831    fadeToward(contactGridManager.getContainerView(), 1 - headerProgress);
832    scaleToward(contactGridManager.getContainerView(), MathUtil.lerp(1f, .75f, headerProgress));
833
834    if (Math.abs(answerProgress) >= .0001) {
835      affordanceHolderLayout.animateHideLeftRightIcon();
836      handler.removeCallbacks(swipeHintRestoreTimer);
837      restoreSwipeHintTexts();
838    }
839  }
840
841  @Override
842  public void answerFromMethod() {
843    acceptCallByUser(false /* answerVideoAsAudio */);
844  }
845
846  @Override
847  public void rejectFromMethod() {
848    rejectCall();
849  }
850
851  @Override
852  public void resetAnswerProgress() {
853    affordanceHolderLayout.reset(true);
854    restoreBackgroundMaskColor();
855  }
856
857  private void animateEntry(@NonNull View rootView) {
858    if (!isAdded()) {
859      LogUtil.i(
860          "AnswerFragment.animateEntry",
861          "Not currently added to Activity. Will not start entry animation.");
862      return;
863    }
864    contactGridManager.getContainerView().setAlpha(0f);
865    Animator alpha =
866        ObjectAnimator.ofFloat(contactGridManager.getContainerView(), View.ALPHA, 0, 1);
867    Animator topRow = createTranslation(rootView.findViewById(R.id.contactgrid_top_row));
868    Animator contactName = createTranslation(rootView.findViewById(R.id.contactgrid_contact_name));
869    Animator bottomRow = createTranslation(rootView.findViewById(R.id.contactgrid_bottom_row));
870    Animator important = createTranslation(importanceBadge);
871    Animator dataContainer = createTranslation(rootView.findViewById(R.id.incall_data_container));
872
873    AnimatorSet animatorSet = new AnimatorSet();
874    AnimatorSet.Builder builder = animatorSet.play(alpha);
875    builder.with(topRow).with(contactName).with(bottomRow).with(important).with(dataContainer);
876    if (isShowingLocationUi()) {
877      builder.with(createTranslation(rootView.findViewById(R.id.incall_location_holder)));
878    }
879    animatorSet.setDuration(
880        rootView.getResources().getInteger(R.integer.answer_animate_entry_millis));
881    animatorSet.addListener(
882        new AnimatorListenerAdapter() {
883          @Override
884          public void onAnimationEnd(Animator animation) {
885            hasAnimatedEntry = true;
886          }
887        });
888    animatorSet.start();
889  }
890
891  private ObjectAnimator createTranslation(View view) {
892    float translationY = view.getTop() * 0.5f;
893    ObjectAnimator animator = ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, translationY, 0);
894    animator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
895    return animator;
896  }
897
898  private void acceptCallByUser(boolean answerVideoAsAudio) {
899    LogUtil.i("AnswerFragment.acceptCallByUser", answerVideoAsAudio ? " answerVideoAsAudio" : "");
900    if (!buttonAcceptClicked) {
901      answerScreenDelegate.onAnswer(answerVideoAsAudio);
902      buttonAcceptClicked = true;
903    }
904  }
905
906  private void rejectCall() {
907    LogUtil.i("AnswerFragment.rejectCall", null);
908    if (!buttonRejectClicked) {
909      Context context = getContext();
910      if (context == null) {
911        LogUtil.w(
912            "AnswerFragment.rejectCall",
913            "Null context when rejecting call. Logger call was skipped");
914      } else {
915        Logger.get(context)
916            .logImpression(DialerImpression.Type.REJECT_INCOMING_CALL_FROM_ANSWER_SCREEN);
917      }
918      buttonRejectClicked = true;
919      answerScreenDelegate.onReject();
920    }
921  }
922
923  private void restoreBackgroundMaskColor() {
924    answerScreenDelegate.updateWindowBackgroundColor(0);
925  }
926
927  private void restoreSwipeHintTexts() {
928    if (getAnswerMethod() != null) {
929      if (allowAnswerAndRelease()) {
930        if (hasCallOnHold()) {
931          getAnswerMethod()
932              .setHintText(getText(R.string.call_incoming_default_label_answer_and_release_third));
933        } else {
934          getAnswerMethod()
935              .setHintText(getText(R.string.call_incoming_default_label_answer_and_release_second));
936        }
937      } else {
938        getAnswerMethod().setHintText(null);
939      }
940    }
941  }
942
943  private void showMessageMenu() {
944    LogUtil.i("AnswerFragment.showMessageMenu", "Show sms menu.");
945    if (getChildFragmentManager().isDestroyed()) {
946      return;
947    }
948
949    textResponsesFragment = SmsBottomSheetFragment.newInstance(textResponses);
950    textResponsesFragment.show(getChildFragmentManager(), null);
951    secondaryButton
952        .animate()
953        .alpha(0)
954        .withEndAction(
955            new Runnable() {
956              @Override
957              public void run() {
958                affordanceHolderLayout.reset(false);
959                secondaryButton.animate().alpha(1);
960              }
961            });
962  }
963
964  @Override
965  public void smsSelected(@Nullable CharSequence text) {
966    LogUtil.i("AnswerFragment.smsSelected", null);
967    textResponsesFragment = null;
968
969    if (text == null) {
970      createCustomSmsDialogFragment = CreateCustomSmsDialogFragment.newInstance();
971      createCustomSmsDialogFragment.show(getChildFragmentManager(), null);
972      return;
973    }
974
975    if (primaryCallState != null && canRejectCallWithSms()) {
976      rejectCall();
977      answerScreenDelegate.onRejectCallWithMessage(text.toString());
978    }
979  }
980
981  @Override
982  public void smsDismissed() {
983    LogUtil.i("AnswerFragment.smsDismissed", null);
984    textResponsesFragment = null;
985    answerScreenDelegate.onDismissDialog();
986  }
987
988  @Override
989  public void customSmsCreated(@NonNull CharSequence text) {
990    LogUtil.i("AnswerFragment.customSmsCreated", null);
991    createCustomSmsDialogFragment = null;
992    if (primaryCallState != null && canRejectCallWithSms()) {
993      rejectCall();
994      answerScreenDelegate.onRejectCallWithMessage(text.toString());
995    }
996  }
997
998  @Override
999  public void customSmsDismissed() {
1000    LogUtil.i("AnswerFragment.customSmsDismissed", null);
1001    createCustomSmsDialogFragment = null;
1002    answerScreenDelegate.onDismissDialog();
1003  }
1004
1005  private boolean canRejectCallWithSms() {
1006    return primaryCallState != null
1007        && !(primaryCallState.state == State.DISCONNECTED
1008            || primaryCallState.state == State.DISCONNECTING
1009            || primaryCallState.state == State.IDLE);
1010  }
1011
1012  private void createInCallScreenDelegate() {
1013    inCallScreenDelegate =
1014        FragmentUtils.getParentUnsafe(this, InCallScreenDelegateFactory.class)
1015            .newInCallScreenDelegate();
1016    Assert.isNotNull(inCallScreenDelegate);
1017    inCallScreenDelegate.onInCallScreenDelegateInit(this);
1018    inCallScreenDelegate.onInCallScreenReady();
1019  }
1020
1021  private void updateImportanceBadgeVisibility() {
1022    if (!isAdded()) {
1023      return;
1024    }
1025
1026    if (!getResources().getBoolean(R.bool.answer_important_call_allowed) || primaryInfo.isSpam) {
1027      importanceBadge.setVisibility(View.GONE);
1028      return;
1029    }
1030
1031    MultimediaData multimediaData = getSessionData();
1032    boolean showImportant = multimediaData != null && multimediaData.isImportant();
1033    TransitionManager.beginDelayedTransition((ViewGroup) importanceBadge.getParent());
1034    // TODO (keyboardr): Change this back to being View.INVISIBLE once mocks are available to
1035    // properly handle smaller screens
1036    importanceBadge.setVisibility(showImportant ? View.VISIBLE : View.GONE);
1037  }
1038
1039  @Nullable
1040  private MultimediaData getSessionData() {
1041    if (primaryInfo == null) {
1042      return null;
1043    }
1044    if (isVideoUpgradeRequest()) {
1045      return null;
1046    }
1047    return primaryInfo.multimediaData;
1048  }
1049
1050  /** Shows the Avatar image if available. */
1051  public static class AvatarFragment extends Fragment implements AvatarPresenter {
1052
1053    private ImageView avatarImageView;
1054
1055    @Nullable
1056    @Override
1057    public View onCreateView(
1058        LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) {
1059      return layoutInflater.inflate(R.layout.fragment_avatar, viewGroup, false);
1060    }
1061
1062    @Override
1063    public void onViewCreated(View view, @Nullable Bundle bundle) {
1064      super.onViewCreated(view, bundle);
1065      avatarImageView = ((ImageView) view.findViewById(R.id.contactgrid_avatar));
1066      FragmentUtils.getParentUnsafe(this, MultimediaFragment.Holder.class).updateAvatar(this);
1067    }
1068
1069    @NonNull
1070    @Override
1071    public ImageView getAvatarImageView() {
1072      return avatarImageView;
1073    }
1074
1075    @Override
1076    public int getAvatarSize() {
1077      return getResources().getDimensionPixelSize(R.dimen.answer_avatar_size);
1078    }
1079
1080    @Override
1081    public boolean shouldShowAnonymousAvatar() {
1082      return false;
1083    }
1084  }
1085}
1086