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