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 129 private static void fadeToward(View view, float newAlpha) { 130 view.setAlpha(MathUtil.lerp(view.getAlpha(), newAlpha, ANIMATE_LERP_PROGRESS)); 131 } 132 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 { 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 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 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 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 213 public abstract void performAction(AnswerFragment fragment); 214 215 public void applyToView(ImageView view) { 216 view.setImageResource(icon); 217 view.setContentDescription(view.getContext().getText(contentDescription)); 218 } 219 } 220 221 private void performAnswerAndRelease() { 222 restoreAnswerAndReleaseButtonAnimation(); 223 answerScreenDelegate.onAnswerAndReleaseCall(); 224 buttonAcceptClicked = true; 225 } 226 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 339 private void performSecondaryButtonAction() { 340 secondaryBehavior.performAction(this); 341 } 342 343 private void performAnswerAndReleaseButtonAction() { 344 answerAndReleaseBehavior.performAction(this); 345 } 346 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 370 public boolean isActionTimeout() { 371 return (buttonAcceptClicked || buttonRejectClicked) && answerScreenDelegate.isActionTimeout(); 372 } 373 374 @Override 375 @NonNull 376 public String getCallId() { 377 return Assert.isNotNull(getArguments().getString(ARG_CALL_ID)); 378 } 379 380 @Override 381 public boolean isVideoUpgradeRequest() { 382 return getArguments().getBoolean(ARG_IS_VIDEO_UPGRADE_REQUEST); 383 } 384 385 @Override 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 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 451 public boolean allowAnswerAndRelease() { 452 return getArguments().getBoolean(ARG_ALLOW_ANSWER_AND_RELEASE); 453 } 454 455 private boolean hasCallOnHold() { 456 return getArguments().getBoolean(ARG_HAS_CALL_ON_HOLD); 457 } 458 459 @Override 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 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 482 public boolean isShowingLocationUi() { 483 Fragment fragment = getChildFragmentManager().findFragmentById(R.id.incall_location_holder); 484 return fragment != null && fragment.isVisible(); 485 } 486 487 @Override 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 504 public Fragment getAnswerScreenFragment() { 505 return this; 506 } 507 508 private AnswerMethod getAnswerMethod() { 509 return ((AnswerMethod) 510 getChildFragmentManager().findFragmentById(R.id.answer_method_container)); 511 } 512 513 @Override 514 public void setPrimary(PrimaryInfo primaryInfo) { 515 LogUtil.i("AnswerFragment.setPrimary", primaryInfo.toString()); 516 this.primaryInfo = primaryInfo; 517 updatePrimaryUI(); 518 updateImportanceBadgeVisibility(); 519 } 520 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 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 592 private boolean shouldShowAvatar() { 593 return !isVideoCall() && !isVideoUpgradeRequest(); 594 } 595 596 private boolean canShowMap() { 597 return MapsComponent.get(getContext()).getMaps().isAvailable(); 598 } 599 600 @Override 601 public void updateAvatar(AvatarPresenter avatarContainer) { 602 contactGridManager.setAvatarImageView( 603 avatarContainer.getAvatarImageView(), 604 avatarContainer.getAvatarSize(), 605 avatarContainer.shouldShowAnonymousAvatar()); 606 } 607 608 @Override 609 public void setSecondary(@NonNull SecondaryInfo secondaryInfo) {} 610 611 @Override 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 619 public void setEndCallButtonEnabled(boolean enabled, boolean animate) {} 620 621 @Override 622 public void showManageConferenceCallButton(boolean visible) {} 623 624 @Override 625 public boolean isManageConferenceVisible() { 626 return false; 627 } 628 629 @Override 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 641 public void showNoteSentToast() {} 642 643 @Override 644 public void updateInCallScreenColors() {} 645 646 @Override 647 public void onInCallScreenDialpadVisibilityChange(boolean isShowing) {} 648 649 @Override 650 public int getAnswerAndDialpadContainerResourceId() { 651 throw Assert.createUnsupportedOperationFailException(); 652 } 653 654 @Override 655 public Fragment getInCallScreenFragment() { 656 return this; 657 } 658 659 @Override 660 public void onDestroy() { 661 super.onDestroy(); 662 } 663 664 @Override 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 748 public void onAttach(Context context) { 749 super.onAttach(context); 750 FragmentUtils.checkParent(this, InCallScreenDelegateFactory.class); 751 } 752 753 @Override 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 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 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 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 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 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 823 public void onSaveInstanceState(Bundle bundle) { 824 super.onSaveInstanceState(bundle); 825 bundle.putBoolean(STATE_HAS_ANIMATED_ENTRY, hasAnimatedEntry); 826 } 827 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 844 public boolean isRttCall() { 845 return getArguments().getBoolean(ARG_IS_RTT_CALL); 846 } 847 848 @Override 849 public boolean isVideoCall() { 850 return getArguments().getBoolean(ARG_IS_VIDEO_CALL); 851 } 852 853 public boolean isSelfManagedCamera() { 854 return getArguments().getBoolean(ARG_IS_SELF_MANAGED_CAMERA); 855 } 856 857 @Override 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 881 public void answerFromMethod() { 882 acceptCallByUser(false /* answerVideoAsAudio */); 883 } 884 885 @Override 886 public void rejectFromMethod() { 887 rejectCall(); 888 } 889 890 @Override 891 public void resetAnswerProgress() { 892 affordanceHolderLayout.reset(true); 893 restoreBackgroundMaskColor(); 894 } 895 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 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 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 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 962 private void restoreBackgroundMaskColor() { 963 answerScreenDelegate.updateWindowBackgroundColor(0); 964 } 965 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 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 1006 public InCallUiLock acquireInCallUiLock(String tag) { 1007 return answerScreenDelegate.acquireInCallUiLock(tag); 1008 } 1009 1010 @Override 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 1028 public void smsDismissed() { 1029 LogUtil.i("AnswerFragment.smsDismissed", null); 1030 textResponsesFragment = null; 1031 } 1032 1033 @Override 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 1044 public void customSmsDismissed() { 1045 LogUtil.i("AnswerFragment.customSmsDismissed", null); 1046 createCustomSmsDialogFragment = null; 1047 } 1048 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 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 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 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 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 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 1115 public ImageView getAvatarImageView() { 1116 return avatarImageView; 1117 } 1118 1119 @Override 1120 public int getAvatarSize() { 1121 return getResources().getDimensionPixelSize(R.dimen.answer_avatar_size); 1122 } 1123 1124 @Override 1125 public boolean shouldShowAnonymousAvatar() { 1126 return false; 1127 } 1128 } 1129 } 1130