• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.incallui.answer.impl;
18 
19 import android.Manifest.permission;
20 import android.animation.Animator;
21 import android.animation.AnimatorListenerAdapter;
22 import android.animation.AnimatorSet;
23 import android.animation.ObjectAnimator;
24 import android.annotation.SuppressLint;
25 import android.content.Context;
26 import android.content.pm.PackageManager;
27 import android.location.Location;
28 import android.net.Uri;
29 import android.os.Bundle;
30 import android.os.Handler;
31 import android.os.Looper;
32 import android.support.annotation.DrawableRes;
33 import android.support.annotation.FloatRange;
34 import android.support.annotation.NonNull;
35 import android.support.annotation.Nullable;
36 import android.support.annotation.StringRes;
37 import android.support.annotation.VisibleForTesting;
38 import android.support.v4.app.Fragment;
39 import android.text.TextUtils;
40 import android.transition.TransitionManager;
41 import android.view.LayoutInflater;
42 import android.view.View;
43 import android.view.View.AccessibilityDelegate;
44 import android.view.View.OnClickListener;
45 import android.view.ViewGroup;
46 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
47 import android.view.accessibility.AccessibilityEvent;
48 import android.view.accessibility.AccessibilityNodeInfo;
49 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
50 import android.widget.ImageView;
51 import com.android.dialer.common.Assert;
52 import com.android.dialer.common.FragmentUtils;
53 import com.android.dialer.common.LogUtil;
54 import com.android.dialer.common.MathUtil;
55 import com.android.dialer.compat.ActivityCompat;
56 import com.android.dialer.logging.DialerImpression;
57 import com.android.dialer.logging.Logger;
58 import com.android.dialer.multimedia.MultimediaData;
59 import com.android.dialer.util.ViewUtil;
60 import com.android.incallui.answer.impl.CreateCustomSmsDialogFragment.CreateCustomSmsHolder;
61 import com.android.incallui.answer.impl.SmsBottomSheetFragment.SmsSheetHolder;
62 import com.android.incallui.answer.impl.affordance.SwipeButtonHelper.Callback;
63 import com.android.incallui.answer.impl.affordance.SwipeButtonView;
64 import com.android.incallui.answer.impl.answermethod.AnswerMethod;
65 import com.android.incallui.answer.impl.answermethod.AnswerMethodFactory;
66 import com.android.incallui.answer.impl.answermethod.AnswerMethodHolder;
67 import com.android.incallui.answer.impl.utils.Interpolators;
68 import com.android.incallui.answer.protocol.AnswerScreen;
69 import com.android.incallui.answer.protocol.AnswerScreenDelegate;
70 import com.android.incallui.answer.protocol.AnswerScreenDelegateFactory;
71 import com.android.incallui.call.DialerCall.State;
72 import com.android.incallui.contactgrid.ContactGridManager;
73 import com.android.incallui.incall.protocol.ContactPhotoType;
74 import com.android.incallui.incall.protocol.InCallScreen;
75 import com.android.incallui.incall.protocol.InCallScreenDelegate;
76 import com.android.incallui.incall.protocol.InCallScreenDelegateFactory;
77 import com.android.incallui.incall.protocol.PrimaryCallState;
78 import com.android.incallui.incall.protocol.PrimaryInfo;
79 import com.android.incallui.incall.protocol.SecondaryInfo;
80 import com.android.incallui.maps.MapsComponent;
81 import com.android.incallui.sessiondata.AvatarPresenter;
82 import com.android.incallui.sessiondata.MultimediaFragment;
83 import com.android.incallui.util.AccessibilityUtil;
84 import com.android.incallui.video.protocol.VideoCallScreen;
85 import com.android.incallui.videotech.utils.VideoUtils;
86 import java.util.ArrayList;
87 import java.util.List;
88 import java.util.Objects;
89 
90 /** The new version of the incoming call screen. */
91 @SuppressLint("ClickableViewAccessibility")
92 public 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 
fadeToward(View view, float newAlpha)124   private static void fadeToward(View view, float newAlpha) {
125     view.setAlpha(MathUtil.lerp(view.getAlpha(), newAlpha, ANIMATE_LERP_PROGRESS));
126   }
127 
scaleToward(View view, float newScale)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 {
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)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 
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)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 
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)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 
SecondaryBehavior( @rawableRes int icon, @StringRes int contentDescription, @StringRes int accessibilityLabel, @StringRes int hintText)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 
performAction(AnswerFragment fragment)208     public abstract void performAction(AnswerFragment fragment);
209 
applyToView(ImageView view)210     public void applyToView(ImageView view) {
211       view.setImageResource(icon);
212       view.setContentDescription(view.getContext().getText(contentDescription));
213     }
214   }
215 
performAnswerAndRelease()216   private void performAnswerAndRelease() {
217     restoreAnswerAndReleaseButtonAnimation();
218     answerScreenDelegate.onAnswerAndReleaseCall();
219   }
220 
restoreAnswerAndReleaseButtonAnimation()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 
performSecondaryButtonAction()333   private void performSecondaryButtonAction() {
334     secondaryBehavior.performAction(this);
335   }
336 
performAnswerAndReleaseButtonAction()337   private void performAnswerAndReleaseButtonAction() {
338     answerAndReleaseBehavior.performAction(this);
339   }
340 
newInstance( String callId, boolean isVideoCall, boolean isVideoUpgradeRequest, boolean isSelfManagedCamera, boolean allowAnswerAndRelease, boolean hasCallOnHold)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
getCallId()363   public String getCallId() {
364     return Assert.isNotNull(getArguments().getString(ARG_CALL_ID));
365   }
366 
367   @Override
isVideoUpgradeRequest()368   public boolean isVideoUpgradeRequest() {
369     return getArguments().getBoolean(ARG_IS_VIDEO_UPGRADE_REQUEST);
370   }
371 
372   @Override
setTextResponses(List<String> textResponses)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 
initSecondaryButton()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
allowAnswerAndRelease()436   public boolean allowAnswerAndRelease() {
437     return getArguments().getBoolean(ARG_ALLOW_ANSWER_AND_RELEASE);
438   }
439 
hasCallOnHold()440   private boolean hasCallOnHold() {
441     return getArguments().getBoolean(ARG_HAS_CALL_ON_HOLD);
442   }
443 
444   @Override
hasPendingDialogs()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
dismissPendingDialogs()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
isShowingLocationUi()467   public boolean isShowingLocationUi() {
468     Fragment fragment = getChildFragmentManager().findFragmentById(R.id.incall_location_holder);
469     return fragment != null && fragment.isVisible();
470   }
471 
472   @Override
showLocationUi(@ullable Fragment locationUi)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
getAnswerScreenFragment()489   public Fragment getAnswerScreenFragment() {
490     return this;
491   }
492 
getAnswerMethod()493   private AnswerMethod getAnswerMethod() {
494     return ((AnswerMethod)
495         getChildFragmentManager().findFragmentById(R.id.answer_method_container));
496   }
497 
498   @Override
setPrimary(PrimaryInfo primaryInfo)499   public void setPrimary(PrimaryInfo primaryInfo) {
500     LogUtil.i("AnswerFragment.setPrimary", primaryInfo.toString());
501     this.primaryInfo = primaryInfo;
502     updatePrimaryUI();
503     updateImportanceBadgeVisibility();
504   }
505 
updatePrimaryUI()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 
updateDataFragment()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 
shouldShowAvatar()573   private boolean shouldShowAvatar() {
574     return !isVideoCall() && !isVideoUpgradeRequest();
575   }
576 
canShowMap()577   private boolean canShowMap() {
578     return MapsComponent.get(getContext()).getMaps().isAvailable();
579   }
580 
581   @Override
updateAvatar(AvatarPresenter avatarContainer)582   public void updateAvatar(AvatarPresenter avatarContainer) {
583     contactGridManager.setAvatarImageView(
584         avatarContainer.getAvatarImageView(),
585         avatarContainer.getAvatarSize(),
586         avatarContainer.shouldShowAnonymousAvatar());
587   }
588 
589   @Override
setSecondary(@onNull SecondaryInfo secondaryInfo)590   public void setSecondary(@NonNull SecondaryInfo secondaryInfo) {}
591 
592   @Override
setCallState(@onNull PrimaryCallState primaryCallState)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
setEndCallButtonEnabled(boolean enabled, boolean animate)600   public void setEndCallButtonEnabled(boolean enabled, boolean animate) {}
601 
602   @Override
showManageConferenceCallButton(boolean visible)603   public void showManageConferenceCallButton(boolean visible) {}
604 
605   @Override
isManageConferenceVisible()606   public boolean isManageConferenceVisible() {
607     return false;
608   }
609 
610   @Override
dispatchPopulateAccessibilityEvent(AccessibilityEvent event)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
showNoteSentToast()622   public void showNoteSentToast() {}
623 
624   @Override
updateInCallScreenColors()625   public void updateInCallScreenColors() {}
626 
627   @Override
onInCallScreenDialpadVisibilityChange(boolean isShowing)628   public void onInCallScreenDialpadVisibilityChange(boolean isShowing) {}
629 
630   @Override
getAnswerAndDialpadContainerResourceId()631   public int getAnswerAndDialpadContainerResourceId() {
632     throw Assert.createUnsupportedOperationFailException();
633   }
634 
635   @Override
getInCallScreenFragment()636   public Fragment getInCallScreenFragment() {
637     return this;
638   }
639 
640   @Override
onDestroy()641   public void onDestroy() {
642     super.onDestroy();
643   }
644 
645   @Override
onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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
onAttach(Context context)724   public void onAttach(Context context) {
725     super.onAttach(context);
726     FragmentUtils.checkParent(this, InCallScreenDelegateFactory.class);
727   }
728 
729   @Override
onViewCreated(final View view, @Nullable Bundle savedInstanceState)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
onResume()741   public void onResume() {
742     super.onResume();
743     LogUtil.i("AnswerFragment.onResume", null);
744     restoreSwipeHintTexts();
745     inCallScreenDelegate.onInCallScreenResumed();
746   }
747 
748   @Override
onStart()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
onStop()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
onPause()771   public void onPause() {
772     super.onPause();
773     LogUtil.i("AnswerFragment.onPause", null);
774     inCallScreenDelegate.onInCallScreenPaused();
775   }
776 
777   @Override
onDestroyView()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
onSaveInstanceState(Bundle bundle)789   public void onSaveInstanceState(Bundle bundle) {
790     super.onSaveInstanceState(bundle);
791     bundle.putBoolean(STATE_HAS_ANIMATED_ENTRY, hasAnimatedEntry);
792   }
793 
updateUI()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
isVideoCall()810   public boolean isVideoCall() {
811     return getArguments().getBoolean(ARG_IS_VIDEO_CALL);
812   }
813 
isSelfManagedCamera()814   public boolean isSelfManagedCamera() {
815     return getArguments().getBoolean(ARG_IS_SELF_MANAGED_CAMERA);
816   }
817 
818   @Override
onAnswerProgressUpdate(@loatRangefrom = -1f, to = 1f) float answerProgress)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
answerFromMethod()842   public void answerFromMethod() {
843     acceptCallByUser(false /* answerVideoAsAudio */);
844   }
845 
846   @Override
rejectFromMethod()847   public void rejectFromMethod() {
848     rejectCall();
849   }
850 
851   @Override
resetAnswerProgress()852   public void resetAnswerProgress() {
853     affordanceHolderLayout.reset(true);
854     restoreBackgroundMaskColor();
855   }
856 
animateEntry(@onNull View rootView)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 
createTranslation(View view)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 
acceptCallByUser(boolean answerVideoAsAudio)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 
rejectCall()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 
restoreBackgroundMaskColor()923   private void restoreBackgroundMaskColor() {
924     answerScreenDelegate.updateWindowBackgroundColor(0);
925   }
926 
restoreSwipeHintTexts()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 
showMessageMenu()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
smsSelected(@ullable CharSequence text)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
smsDismissed()982   public void smsDismissed() {
983     LogUtil.i("AnswerFragment.smsDismissed", null);
984     textResponsesFragment = null;
985     answerScreenDelegate.onDismissDialog();
986   }
987 
988   @Override
customSmsCreated(@onNull CharSequence text)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
customSmsDismissed()999   public void customSmsDismissed() {
1000     LogUtil.i("AnswerFragment.customSmsDismissed", null);
1001     createCustomSmsDialogFragment = null;
1002     answerScreenDelegate.onDismissDialog();
1003   }
1004 
canRejectCallWithSms()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 
createInCallScreenDelegate()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 
updateImportanceBadgeVisibility()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
getSessionData()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
onCreateView( LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle)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
onViewCreated(View view, @Nullable Bundle bundle)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
getAvatarImageView()1071     public ImageView getAvatarImageView() {
1072       return avatarImageView;
1073     }
1074 
1075     @Override
getAvatarSize()1076     public int getAvatarSize() {
1077       return getResources().getDimensionPixelSize(R.dimen.answer_avatar_size);
1078     }
1079 
1080     @Override
shouldShowAnonymousAvatar()1081     public boolean shouldShowAnonymousAvatar() {
1082       return false;
1083     }
1084   }
1085 }
1086