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