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