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