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