1 /* 2 * Copyright (C) 2013 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; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.AnimatorSet; 22 import android.animation.LayoutTransition; 23 import android.animation.ObjectAnimator; 24 import android.app.Activity; 25 import android.content.Context; 26 import android.content.res.Configuration; 27 import android.graphics.Point; 28 import android.graphics.drawable.AnimationDrawable; 29 import android.graphics.drawable.Drawable; 30 import android.os.Bundle; 31 import android.telecom.DisconnectCause; 32 import android.telecom.VideoProfile; 33 import android.telephony.PhoneNumberUtils; 34 import android.text.TextUtils; 35 import android.text.format.DateUtils; 36 import android.view.Display; 37 import android.view.LayoutInflater; 38 import android.view.View; 39 import android.view.View.OnLayoutChangeListener; 40 import android.view.ViewAnimationUtils; 41 import android.view.ViewGroup; 42 import android.view.ViewPropertyAnimator; 43 import android.view.ViewTreeObserver; 44 import android.view.ViewTreeObserver.OnGlobalLayoutListener; 45 import android.view.accessibility.AccessibilityEvent; 46 import android.view.animation.Animation; 47 import android.view.animation.AnimationUtils; 48 import android.widget.ImageButton; 49 import android.widget.ImageView; 50 import android.widget.TextView; 51 52 import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette; 53 import com.android.contacts.common.widget.FloatingActionButtonController; 54 import com.android.incallui.service.PhoneNumberService; 55 import com.android.phone.common.animation.AnimUtils; 56 57 import java.util.List; 58 59 /** 60 * Fragment for call card. 61 */ 62 public class CallCardFragment extends BaseFragment<CallCardPresenter, CallCardPresenter.CallCardUi> 63 implements CallCardPresenter.CallCardUi { 64 65 private AnimatorSet mAnimatorSet; 66 private int mRevealAnimationDuration; 67 private int mShrinkAnimationDuration; 68 private int mFabNormalDiameter; 69 private int mFabSmallDiameter; 70 private boolean mIsLandscape; 71 private boolean mIsDialpadShowing; 72 73 // Primary caller info 74 private TextView mPhoneNumber; 75 private TextView mNumberLabel; 76 private TextView mPrimaryName; 77 private View mCallStateButton; 78 private ImageView mCallStateIcon; 79 private ImageView mCallStateVideoCallIcon; 80 private TextView mCallStateLabel; 81 private TextView mCallTypeLabel; 82 private View mCallNumberAndLabel; 83 private ImageView mPhoto; 84 private TextView mElapsedTime; 85 private Drawable mPrimaryPhotoDrawable; 86 87 // Container view that houses the entire primary call card, including the call buttons 88 private View mPrimaryCallCardContainer; 89 // Container view that houses the primary call information 90 private ViewGroup mPrimaryCallInfo; 91 private View mCallButtonsContainer; 92 93 // Secondary caller info 94 private View mSecondaryCallInfo; 95 private TextView mSecondaryCallName; 96 private View mSecondaryCallProviderInfo; 97 private TextView mSecondaryCallProviderLabel; 98 private View mSecondaryCallConferenceCallIcon; 99 private View mProgressSpinner; 100 101 private View mManageConferenceCallButton; 102 103 // Dark number info bar 104 private TextView mInCallMessageLabel; 105 106 private FloatingActionButtonController mFloatingActionButtonController; 107 private View mFloatingActionButtonContainer; 108 private ImageButton mFloatingActionButton; 109 private int mFloatingActionButtonVerticalOffset; 110 111 // Cached DisplayMetrics density. 112 private float mDensity; 113 114 private float mTranslationOffset; 115 private Animation mPulseAnimation; 116 117 private int mVideoAnimationDuration; 118 119 private MaterialPalette mCurrentThemeColors; 120 121 @Override getUi()122 CallCardPresenter.CallCardUi getUi() { 123 return this; 124 } 125 126 @Override createPresenter()127 CallCardPresenter createPresenter() { 128 return new CallCardPresenter(); 129 } 130 131 @Override onCreate(Bundle savedInstanceState)132 public void onCreate(Bundle savedInstanceState) { 133 super.onCreate(savedInstanceState); 134 135 mRevealAnimationDuration = getResources().getInteger(R.integer.reveal_animation_duration); 136 mShrinkAnimationDuration = getResources().getInteger(R.integer.shrink_animation_duration); 137 mVideoAnimationDuration = getResources().getInteger(R.integer.video_animation_duration); 138 mFloatingActionButtonVerticalOffset = getResources().getDimensionPixelOffset( 139 R.dimen.floating_action_bar_vertical_offset); 140 mFabNormalDiameter = getResources().getDimensionPixelOffset( 141 R.dimen.end_call_floating_action_button_diameter); 142 mFabSmallDiameter = getResources().getDimensionPixelOffset( 143 R.dimen.end_call_floating_action_button_small_diameter); 144 } 145 146 147 @Override onActivityCreated(Bundle savedInstanceState)148 public void onActivityCreated(Bundle savedInstanceState) { 149 super.onActivityCreated(savedInstanceState); 150 151 final CallList calls = CallList.getInstance(); 152 final Call call = calls.getFirstCall(); 153 getPresenter().init(getActivity(), call); 154 } 155 156 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)157 public View onCreateView(LayoutInflater inflater, ViewGroup container, 158 Bundle savedInstanceState) { 159 super.onCreateView(inflater, container, savedInstanceState); 160 161 mDensity = getResources().getDisplayMetrics().density; 162 mTranslationOffset = 163 getResources().getDimensionPixelSize(R.dimen.call_card_anim_translate_y_offset); 164 165 return inflater.inflate(R.layout.call_card_content, container, false); 166 } 167 168 @Override onViewCreated(View view, Bundle savedInstanceState)169 public void onViewCreated(View view, Bundle savedInstanceState) { 170 super.onViewCreated(view, savedInstanceState); 171 172 mPulseAnimation = 173 AnimationUtils.loadAnimation(view.getContext(), R.anim.call_status_pulse); 174 175 mPhoneNumber = (TextView) view.findViewById(R.id.phoneNumber); 176 mPrimaryName = (TextView) view.findViewById(R.id.name); 177 mNumberLabel = (TextView) view.findViewById(R.id.label); 178 mSecondaryCallInfo = view.findViewById(R.id.secondary_call_info); 179 mSecondaryCallProviderInfo = view.findViewById(R.id.secondary_call_provider_info); 180 mPhoto = (ImageView) view.findViewById(R.id.photo); 181 mCallStateIcon = (ImageView) view.findViewById(R.id.callStateIcon); 182 mCallStateVideoCallIcon = (ImageView) view.findViewById(R.id.videoCallIcon); 183 mCallStateLabel = (TextView) view.findViewById(R.id.callStateLabel); 184 mCallNumberAndLabel = view.findViewById(R.id.labelAndNumber); 185 mCallTypeLabel = (TextView) view.findViewById(R.id.callTypeLabel); 186 mElapsedTime = (TextView) view.findViewById(R.id.elapsedTime); 187 mPrimaryCallCardContainer = view.findViewById(R.id.primary_call_info_container); 188 mPrimaryCallInfo = (ViewGroup) view.findViewById(R.id.primary_call_banner); 189 mCallButtonsContainer = view.findViewById(R.id.callButtonFragment); 190 mInCallMessageLabel = (TextView) view.findViewById(R.id.connectionServiceMessage); 191 mProgressSpinner = view.findViewById(R.id.progressSpinner); 192 193 mFloatingActionButtonContainer = view.findViewById( 194 R.id.floating_end_call_action_button_container); 195 mFloatingActionButton = (ImageButton) view.findViewById( 196 R.id.floating_end_call_action_button); 197 mFloatingActionButton.setOnClickListener(new View.OnClickListener() { 198 @Override 199 public void onClick(View v) { 200 getPresenter().endCallClicked(); 201 } 202 }); 203 mFloatingActionButtonController = new FloatingActionButtonController(getActivity(), 204 mFloatingActionButtonContainer, mFloatingActionButton); 205 206 mSecondaryCallInfo.setOnClickListener(new View.OnClickListener() { 207 @Override 208 public void onClick(View v) { 209 getPresenter().secondaryInfoClicked(); 210 updateFabPositionForSecondaryCallInfo(); 211 } 212 }); 213 214 mCallStateButton = view.findViewById(R.id.callStateButton); 215 mCallStateButton.setOnClickListener(new View.OnClickListener() { 216 @Override 217 public void onClick(View v) { 218 getPresenter().onCallStateButtonTouched(); 219 } 220 }); 221 222 mManageConferenceCallButton = view.findViewById(R.id.manage_conference_call_button); 223 mManageConferenceCallButton.setOnClickListener(new View.OnClickListener() { 224 @Override 225 public void onClick(View v) { 226 InCallActivity activity = (InCallActivity) getActivity(); 227 activity.showConferenceCallManager(true); 228 } 229 }); 230 231 mPrimaryName.setElegantTextHeight(false); 232 mCallStateLabel.setElegantTextHeight(false); 233 } 234 235 @Override setVisible(boolean on)236 public void setVisible(boolean on) { 237 if (on) { 238 getView().setVisibility(View.VISIBLE); 239 } else { 240 getView().setVisibility(View.INVISIBLE); 241 } 242 } 243 244 /** 245 * Hides or shows the progress spinner. 246 * 247 * @param visible {@code True} if the progress spinner should be visible. 248 */ 249 @Override setProgressSpinnerVisible(boolean visible)250 public void setProgressSpinnerVisible(boolean visible) { 251 mProgressSpinner.setVisibility(visible ? View.VISIBLE : View.GONE); 252 } 253 254 /** 255 * Sets the visibility of the primary call card. 256 * Ensures that when the primary call card is hidden, the video surface slides over to fill the 257 * entire screen. 258 * 259 * @param visible {@code True} if the primary call card should be visible. 260 */ 261 @Override setCallCardVisible(final boolean visible)262 public void setCallCardVisible(final boolean visible) { 263 // When animating the hide/show of the views in a landscape layout, we need to take into 264 // account whether we are in a left-to-right locale or a right-to-left locale and adjust 265 // the animations accordingly. 266 final boolean isLayoutRtl = InCallPresenter.isRtl(); 267 268 // Retrieve here since at fragment creation time the incoming video view is not inflated. 269 final View videoView = getView().findViewById(R.id.incomingVideo); 270 271 // Determine how much space there is below or to the side of the call card. 272 final float spaceBesideCallCard = getSpaceBesideCallCard(); 273 274 // We need to translate the video surface, but we need to know its position after the layout 275 // has occurred so use a {@code ViewTreeObserver}. 276 final ViewTreeObserver observer = getView().getViewTreeObserver(); 277 observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 278 @Override 279 public boolean onPreDraw() { 280 // We don't want to continue getting called. 281 if (observer.isAlive()) { 282 observer.removeOnPreDrawListener(this); 283 } 284 285 float videoViewTranslation = 0f; 286 287 // Translate the call card to its pre-animation state. 288 if (mIsLandscape) { 289 float translationX = mPrimaryCallCardContainer.getWidth(); 290 translationX *= isLayoutRtl ? 1 : -1; 291 292 mPrimaryCallCardContainer.setTranslationX(visible ? translationX : 0); 293 294 if (visible) { 295 videoViewTranslation = videoView.getWidth() / 2 - spaceBesideCallCard / 2; 296 videoViewTranslation *= isLayoutRtl ? -1 : 1; 297 } 298 } else { 299 mPrimaryCallCardContainer.setTranslationY(visible ? 300 -mPrimaryCallCardContainer.getHeight() : 0); 301 302 if (visible) { 303 videoViewTranslation = videoView.getHeight() / 2 - spaceBesideCallCard / 2; 304 } 305 } 306 307 // Perform animation of video view. 308 ViewPropertyAnimator videoViewAnimator = videoView.animate() 309 .setInterpolator(AnimUtils.EASE_OUT_EASE_IN) 310 .setDuration(mVideoAnimationDuration); 311 if (mIsLandscape) { 312 videoViewAnimator 313 .translationX(videoViewTranslation) 314 .start(); 315 } else { 316 videoViewAnimator 317 .translationY(videoViewTranslation) 318 .start(); 319 } 320 videoViewAnimator.start(); 321 322 // Animate the call card sliding. 323 ViewPropertyAnimator callCardAnimator = mPrimaryCallCardContainer.animate() 324 .setInterpolator(AnimUtils.EASE_OUT_EASE_IN) 325 .setDuration(mVideoAnimationDuration) 326 .setListener(new AnimatorListenerAdapter() { 327 @Override 328 public void onAnimationEnd(Animator animation) { 329 super.onAnimationEnd(animation); 330 if (!visible) { 331 mPrimaryCallCardContainer.setVisibility(View.GONE); 332 } 333 } 334 335 @Override 336 public void onAnimationStart(Animator animation) { 337 super.onAnimationStart(animation); 338 if (visible) { 339 mPrimaryCallCardContainer.setVisibility(View.VISIBLE); 340 } 341 } 342 }); 343 344 if (mIsLandscape) { 345 float translationX = mPrimaryCallCardContainer.getWidth(); 346 translationX *= isLayoutRtl ? 1 : -1; 347 callCardAnimator 348 .translationX(visible ? 0 : translationX) 349 .start(); 350 } else { 351 callCardAnimator 352 .translationY(visible ? 0 : -mPrimaryCallCardContainer.getHeight()) 353 .start(); 354 } 355 356 return true; 357 } 358 }); 359 } 360 361 /** 362 * Determines the amount of space below the call card for portrait layouts), or beside the 363 * call card for landscape layouts. 364 * 365 * @return The amount of space below or beside the call card. 366 */ getSpaceBesideCallCard()367 public float getSpaceBesideCallCard() { 368 if (mIsLandscape) { 369 return getView().getWidth() - mPrimaryCallCardContainer.getWidth(); 370 } else { 371 return getView().getHeight() - mPrimaryCallCardContainer.getHeight(); 372 } 373 } 374 375 @Override setPrimaryName(String name, boolean nameIsNumber)376 public void setPrimaryName(String name, boolean nameIsNumber) { 377 if (TextUtils.isEmpty(name)) { 378 mPrimaryName.setText(null); 379 } else { 380 mPrimaryName.setText(nameIsNumber 381 ? PhoneNumberUtils.ttsSpanAsPhoneNumber(name) 382 : name); 383 384 // Set direction of the name field 385 int nameDirection = View.TEXT_DIRECTION_INHERIT; 386 if (nameIsNumber) { 387 nameDirection = View.TEXT_DIRECTION_LTR; 388 } 389 mPrimaryName.setTextDirection(nameDirection); 390 } 391 } 392 393 @Override setPrimaryImage(Drawable image)394 public void setPrimaryImage(Drawable image) { 395 if (image != null) { 396 setDrawableToImageView(mPhoto, image); 397 } 398 } 399 400 @Override setPrimaryPhoneNumber(String number)401 public void setPrimaryPhoneNumber(String number) { 402 // Set the number 403 if (TextUtils.isEmpty(number)) { 404 mPhoneNumber.setText(null); 405 mPhoneNumber.setVisibility(View.GONE); 406 } else { 407 mPhoneNumber.setText(PhoneNumberUtils.ttsSpanAsPhoneNumber(number)); 408 mPhoneNumber.setVisibility(View.VISIBLE); 409 mPhoneNumber.setTextDirection(View.TEXT_DIRECTION_LTR); 410 } 411 } 412 413 @Override setPrimaryLabel(String label)414 public void setPrimaryLabel(String label) { 415 if (!TextUtils.isEmpty(label)) { 416 mNumberLabel.setText(label); 417 mNumberLabel.setVisibility(View.VISIBLE); 418 } else { 419 mNumberLabel.setVisibility(View.GONE); 420 } 421 422 } 423 424 @Override setPrimary(String number, String name, boolean nameIsNumber, String label, Drawable photo, boolean isSipCall)425 public void setPrimary(String number, String name, boolean nameIsNumber, String label, 426 Drawable photo, boolean isSipCall) { 427 Log.d(this, "Setting primary call"); 428 429 // set the name field. 430 setPrimaryName(name, nameIsNumber); 431 432 if (TextUtils.isEmpty(number) && TextUtils.isEmpty(label)) { 433 mCallNumberAndLabel.setVisibility(View.GONE); 434 mElapsedTime.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); 435 } else { 436 mCallNumberAndLabel.setVisibility(View.VISIBLE); 437 mElapsedTime.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_END); 438 } 439 440 setPrimaryPhoneNumber(number); 441 442 // Set the label (Mobile, Work, etc) 443 setPrimaryLabel(label); 444 445 showInternetCallLabel(isSipCall); 446 447 setDrawableToImageView(mPhoto, photo); 448 } 449 450 @Override setSecondary(boolean show, String name, boolean nameIsNumber, String label, String providerLabel, boolean isConference)451 public void setSecondary(boolean show, String name, boolean nameIsNumber, String label, 452 String providerLabel, boolean isConference) { 453 454 if (show != mSecondaryCallInfo.isShown()) { 455 updateFabPositionForSecondaryCallInfo(); 456 } 457 458 if (show) { 459 boolean hasProvider = !TextUtils.isEmpty(providerLabel); 460 showAndInitializeSecondaryCallInfo(hasProvider); 461 462 mSecondaryCallConferenceCallIcon.setVisibility(isConference ? View.VISIBLE : View.GONE); 463 464 mSecondaryCallName.setText(nameIsNumber 465 ? PhoneNumberUtils.ttsSpanAsPhoneNumber(name) 466 : name); 467 if (hasProvider) { 468 mSecondaryCallProviderLabel.setText(providerLabel); 469 } 470 471 int nameDirection = View.TEXT_DIRECTION_INHERIT; 472 if (nameIsNumber) { 473 nameDirection = View.TEXT_DIRECTION_LTR; 474 } 475 mSecondaryCallName.setTextDirection(nameDirection); 476 } else { 477 mSecondaryCallInfo.setVisibility(View.GONE); 478 } 479 } 480 481 @Override setCallState( int state, int videoState, int sessionModificationState, DisconnectCause disconnectCause, String connectionLabel, Drawable callStateIcon, String gatewayNumber)482 public void setCallState( 483 int state, 484 int videoState, 485 int sessionModificationState, 486 DisconnectCause disconnectCause, 487 String connectionLabel, 488 Drawable callStateIcon, 489 String gatewayNumber) { 490 boolean isGatewayCall = !TextUtils.isEmpty(gatewayNumber); 491 CharSequence callStateLabel = getCallStateLabelFromState(state, videoState, 492 sessionModificationState, disconnectCause, connectionLabel, isGatewayCall); 493 494 Log.v(this, "setCallState " + callStateLabel); 495 Log.v(this, "DisconnectCause " + disconnectCause.toString()); 496 Log.v(this, "gateway " + connectionLabel + gatewayNumber); 497 498 if (TextUtils.equals(callStateLabel, mCallStateLabel.getText())) { 499 // Nothing to do if the labels are the same 500 return; 501 } 502 503 // Update the call state label and icon. 504 if (!TextUtils.isEmpty(callStateLabel)) { 505 mCallStateLabel.setText(callStateLabel); 506 mCallStateLabel.setAlpha(1); 507 mCallStateLabel.setVisibility(View.VISIBLE); 508 509 if (state == Call.State.ACTIVE || state == Call.State.CONFERENCED) { 510 mCallStateLabel.clearAnimation(); 511 } else { 512 mCallStateLabel.startAnimation(mPulseAnimation); 513 } 514 } else { 515 Animation callStateLabelAnimation = mCallStateLabel.getAnimation(); 516 if (callStateLabelAnimation != null) { 517 callStateLabelAnimation.cancel(); 518 } 519 mCallStateLabel.setText(null); 520 mCallStateLabel.setAlpha(0); 521 mCallStateLabel.setVisibility(View.GONE); 522 } 523 524 if (callStateIcon != null) { 525 mCallStateIcon.setVisibility(View.VISIBLE); 526 // Invoke setAlpha(float) instead of setAlpha(int) to set the view's alpha. This is 527 // needed because the pulse animation operates on the view alpha. 528 mCallStateIcon.setAlpha(1.0f); 529 mCallStateIcon.setImageDrawable(callStateIcon); 530 531 if (state == Call.State.ACTIVE || state == Call.State.CONFERENCED 532 || TextUtils.isEmpty(callStateLabel)) { 533 mCallStateIcon.clearAnimation(); 534 } else { 535 mCallStateIcon.startAnimation(mPulseAnimation); 536 } 537 538 if (callStateIcon instanceof AnimationDrawable) { 539 ((AnimationDrawable) callStateIcon).start(); 540 } 541 } else { 542 Animation callStateIconAnimation = mCallStateIcon.getAnimation(); 543 if (callStateIconAnimation != null) { 544 callStateIconAnimation.cancel(); 545 } 546 547 // Invoke setAlpha(float) instead of setAlpha(int) to set the view's alpha. This is 548 // needed because the pulse animation operates on the view alpha. 549 mCallStateIcon.setAlpha(0.0f); 550 mCallStateIcon.setVisibility(View.GONE); 551 } 552 553 if (VideoProfile.VideoState.isBidirectional(videoState) 554 || (state == Call.State.ACTIVE && sessionModificationState 555 == Call.SessionModificationState.WAITING_FOR_RESPONSE)) { 556 mCallStateVideoCallIcon.setVisibility(View.VISIBLE); 557 } else { 558 mCallStateVideoCallIcon.setVisibility(View.GONE); 559 } 560 561 if (state == Call.State.INCOMING) { 562 if (callStateLabel != null) { 563 getView().announceForAccessibility(callStateLabel); 564 } 565 if (mPrimaryName.getText() != null) { 566 getView().announceForAccessibility(mPrimaryName.getText()); 567 } 568 } 569 } 570 571 @Override setCallbackNumber(String callbackNumber, boolean isEmergencyCall)572 public void setCallbackNumber(String callbackNumber, boolean isEmergencyCall) { 573 if (mInCallMessageLabel == null) { 574 return; 575 } 576 577 if (TextUtils.isEmpty(callbackNumber)) { 578 mInCallMessageLabel.setVisibility(View.GONE); 579 return; 580 } 581 582 // TODO: The new Locale-specific methods don't seem to be working. Revisit this. 583 callbackNumber = PhoneNumberUtils.formatNumber(callbackNumber); 584 585 int stringResourceId = isEmergencyCall ? R.string.card_title_callback_number_emergency 586 : R.string.card_title_callback_number; 587 588 String text = getString(stringResourceId, callbackNumber); 589 mInCallMessageLabel.setText(text); 590 591 mInCallMessageLabel.setVisibility(View.VISIBLE); 592 } 593 showInternetCallLabel(boolean show)594 private void showInternetCallLabel(boolean show) { 595 if (show) { 596 final String label = getView().getContext().getString( 597 R.string.incall_call_type_label_sip); 598 mCallTypeLabel.setVisibility(View.VISIBLE); 599 mCallTypeLabel.setText(label); 600 } else { 601 mCallTypeLabel.setVisibility(View.GONE); 602 } 603 } 604 605 @Override setPrimaryCallElapsedTime(boolean show, long duration)606 public void setPrimaryCallElapsedTime(boolean show, long duration) { 607 if (show) { 608 if (mElapsedTime.getVisibility() != View.VISIBLE) { 609 AnimUtils.fadeIn(mElapsedTime, AnimUtils.DEFAULT_DURATION); 610 } 611 String callTimeElapsed = DateUtils.formatElapsedTime(duration / 1000); 612 String durationDescription = InCallDateUtils.formatDetailedDuration(duration); 613 mElapsedTime.setText(callTimeElapsed); 614 mElapsedTime.setContentDescription(durationDescription); 615 } else { 616 // hide() animation has no effect if it is already hidden. 617 AnimUtils.fadeOut(mElapsedTime, AnimUtils.DEFAULT_DURATION); 618 } 619 } 620 setDrawableToImageView(ImageView view, Drawable photo)621 private void setDrawableToImageView(ImageView view, Drawable photo) { 622 if (photo == null) { 623 photo = ContactInfoCache.getInstance( 624 view.getContext()).getDefaultContactPhotoDrawable(); 625 } 626 627 if (mPrimaryPhotoDrawable == photo) { 628 return; 629 } 630 mPrimaryPhotoDrawable = photo; 631 632 final Drawable current = view.getDrawable(); 633 if (current == null) { 634 view.setImageDrawable(photo); 635 AnimUtils.fadeIn(mElapsedTime, AnimUtils.DEFAULT_DURATION); 636 } else { 637 // Cross fading is buggy and not noticable due to the multiple calls to this method 638 // that switch drawables in the middle of the cross-fade animations. Just set the 639 // photo directly instead. 640 view.setImageDrawable(photo); 641 view.setVisibility(View.VISIBLE); 642 } 643 } 644 645 /** 646 * Gets the call state label based on the state of the call or cause of disconnect. 647 * 648 * Additional labels are applied as follows: 649 * 1. All outgoing calls with display "Calling via [Provider]". 650 * 2. Ongoing calls will display the name of the provider. 651 * 3. Incoming calls will only display "Incoming via..." for accounts. 652 * 4. Video calls, and session modification states (eg. requesting video). 653 */ getCallStateLabelFromState(int state, int videoState, int sessionModificationState, DisconnectCause disconnectCause, String label, boolean isGatewayCall)654 private CharSequence getCallStateLabelFromState(int state, int videoState, 655 int sessionModificationState, DisconnectCause disconnectCause, String label, 656 boolean isGatewayCall) { 657 final Context context = getView().getContext(); 658 CharSequence callStateLabel = null; // Label to display as part of the call banner 659 660 boolean isSpecialCall = label != null; 661 boolean isAccount = isSpecialCall && !isGatewayCall; 662 663 switch (state) { 664 case Call.State.IDLE: 665 // "Call state" is meaningless in this state. 666 break; 667 case Call.State.ACTIVE: 668 // We normally don't show a "call state label" at all in this state 669 // (but we can use the call state label to display the provider name). 670 if (isAccount) { 671 callStateLabel = label; 672 } else if (sessionModificationState 673 == Call.SessionModificationState.REQUEST_FAILED) { 674 callStateLabel = context.getString(R.string.card_title_video_call_error); 675 } else if (sessionModificationState 676 == Call.SessionModificationState.WAITING_FOR_RESPONSE) { 677 callStateLabel = context.getString(R.string.card_title_video_call_requesting); 678 } else if (VideoProfile.VideoState.isBidirectional(videoState)) { 679 callStateLabel = context.getString(R.string.card_title_video_call); 680 } 681 break; 682 case Call.State.ONHOLD: 683 callStateLabel = context.getString(R.string.card_title_on_hold); 684 break; 685 case Call.State.CONNECTING: 686 case Call.State.DIALING: 687 if (isSpecialCall) { 688 callStateLabel = context.getString(R.string.calling_via_template, label); 689 } else { 690 callStateLabel = context.getString(R.string.card_title_dialing); 691 } 692 break; 693 case Call.State.REDIALING: 694 callStateLabel = context.getString(R.string.card_title_redialing); 695 break; 696 case Call.State.INCOMING: 697 case Call.State.CALL_WAITING: 698 if (isAccount) { 699 callStateLabel = context.getString(R.string.incoming_via_template, label); 700 } else if (VideoProfile.VideoState.isBidirectional(videoState)) { 701 callStateLabel = context.getString(R.string.notification_incoming_video_call); 702 } else { 703 callStateLabel = context.getString(R.string.card_title_incoming_call); 704 } 705 break; 706 case Call.State.DISCONNECTING: 707 // While in the DISCONNECTING state we display a "Hanging up" 708 // message in order to make the UI feel more responsive. (In 709 // GSM it's normal to see a delay of a couple of seconds while 710 // negotiating the disconnect with the network, so the "Hanging 711 // up" state at least lets the user know that we're doing 712 // something. This state is currently not used with CDMA.) 713 callStateLabel = context.getString(R.string.card_title_hanging_up); 714 break; 715 case Call.State.DISCONNECTED: 716 callStateLabel = disconnectCause.getLabel(); 717 if (TextUtils.isEmpty(callStateLabel)) { 718 callStateLabel = context.getString(R.string.card_title_call_ended); 719 } 720 break; 721 case Call.State.CONFERENCED: 722 callStateLabel = context.getString(R.string.card_title_conf_call); 723 break; 724 default: 725 Log.wtf(this, "updateCallStateWidgets: unexpected call: " + state); 726 } 727 return callStateLabel; 728 } 729 showAndInitializeSecondaryCallInfo(boolean hasProvider)730 private void showAndInitializeSecondaryCallInfo(boolean hasProvider) { 731 mSecondaryCallInfo.setVisibility(View.VISIBLE); 732 733 // mSecondaryCallName is initialized here (vs. onViewCreated) because it is inaccessible 734 // until mSecondaryCallInfo is inflated in the call above. 735 if (mSecondaryCallName == null) { 736 mSecondaryCallName = (TextView) getView().findViewById(R.id.secondaryCallName); 737 mSecondaryCallConferenceCallIcon = 738 getView().findViewById(R.id.secondaryCallConferenceCallIcon); 739 } 740 741 if (mSecondaryCallProviderLabel == null && hasProvider) { 742 mSecondaryCallProviderInfo.setVisibility(View.VISIBLE); 743 mSecondaryCallProviderLabel = (TextView) getView() 744 .findViewById(R.id.secondaryCallProviderLabel); 745 } 746 } 747 dispatchPopulateAccessibilityEvent(AccessibilityEvent event)748 public void dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 749 if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { 750 dispatchPopulateAccessibilityEvent(event, mCallStateLabel); 751 dispatchPopulateAccessibilityEvent(event, mPrimaryName); 752 dispatchPopulateAccessibilityEvent(event, mPhoneNumber); 753 return; 754 } 755 dispatchPopulateAccessibilityEvent(event, mCallStateLabel); 756 dispatchPopulateAccessibilityEvent(event, mPrimaryName); 757 dispatchPopulateAccessibilityEvent(event, mPhoneNumber); 758 dispatchPopulateAccessibilityEvent(event, mCallTypeLabel); 759 dispatchPopulateAccessibilityEvent(event, mSecondaryCallName); 760 dispatchPopulateAccessibilityEvent(event, mSecondaryCallProviderLabel); 761 762 return; 763 } 764 765 @Override setEndCallButtonEnabled(boolean enabled, boolean animate)766 public void setEndCallButtonEnabled(boolean enabled, boolean animate) { 767 if (enabled != mFloatingActionButton.isEnabled()) { 768 if (animate) { 769 if (enabled) { 770 mFloatingActionButtonController.scaleIn(AnimUtils.NO_DELAY); 771 } else { 772 mFloatingActionButtonController.scaleOut(); 773 } 774 } else { 775 if (enabled) { 776 mFloatingActionButtonContainer.setScaleX(1); 777 mFloatingActionButtonContainer.setScaleY(1); 778 mFloatingActionButtonContainer.setVisibility(View.VISIBLE); 779 } else { 780 mFloatingActionButtonContainer.setVisibility(View.GONE); 781 } 782 } 783 mFloatingActionButton.setEnabled(enabled); 784 updateFabPosition(); 785 } 786 } 787 788 /** 789 * Changes the visibility of the contact photo. 790 * 791 * @param isVisible {@code True} if the UI should show the contact photo. 792 */ 793 @Override setPhotoVisible(boolean isVisible)794 public void setPhotoVisible(boolean isVisible) { 795 mPhoto.setVisibility(isVisible ? View.VISIBLE : View.GONE); 796 } 797 798 /** 799 * Changes the visibility of the "manage conference call" button. 800 * 801 * @param visible Whether to set the button to be visible or not. 802 */ 803 @Override showManageConferenceCallButton(boolean visible)804 public void showManageConferenceCallButton(boolean visible) { 805 mManageConferenceCallButton.setVisibility(visible ? View.VISIBLE : View.GONE); 806 } 807 808 /** 809 * Determines the current visibility of the manage conference button. 810 * 811 * @return {@code true} if the button is visible. 812 */ 813 @Override isManageConferenceVisible()814 public boolean isManageConferenceVisible() { 815 return mManageConferenceCallButton.getVisibility() == View.VISIBLE; 816 } 817 818 /** 819 * Get the overall InCallUI background colors and apply to call card. 820 */ updateColors()821 public void updateColors() { 822 MaterialPalette themeColors = InCallPresenter.getInstance().getThemeColors(); 823 824 if (mCurrentThemeColors != null && mCurrentThemeColors.equals(themeColors)) { 825 return; 826 } 827 828 mPrimaryCallCardContainer.setBackgroundColor(themeColors.mPrimaryColor); 829 mCallButtonsContainer.setBackgroundColor(themeColors.mPrimaryColor); 830 831 mCurrentThemeColors = themeColors; 832 } 833 dispatchPopulateAccessibilityEvent(AccessibilityEvent event, View view)834 private void dispatchPopulateAccessibilityEvent(AccessibilityEvent event, View view) { 835 if (view == null) return; 836 final List<CharSequence> eventText = event.getText(); 837 int size = eventText.size(); 838 view.dispatchPopulateAccessibilityEvent(event); 839 // if no text added write null to keep relative position 840 if (size == eventText.size()) { 841 eventText.add(null); 842 } 843 } 844 animateForNewOutgoingCall(final Point touchPoint, final boolean showCircularReveal)845 public void animateForNewOutgoingCall(final Point touchPoint, 846 final boolean showCircularReveal) { 847 final ViewGroup parent = (ViewGroup) mPrimaryCallCardContainer.getParent(); 848 849 final ViewTreeObserver observer = getView().getViewTreeObserver(); 850 851 mPrimaryCallInfo.getLayoutTransition().disableTransitionType(LayoutTransition.CHANGING); 852 853 observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() { 854 @Override 855 public void onGlobalLayout() { 856 final ViewTreeObserver observer = getView().getViewTreeObserver(); 857 if (!observer.isAlive()) { 858 return; 859 } 860 observer.removeOnGlobalLayoutListener(this); 861 862 final LayoutIgnoringListener listener = new LayoutIgnoringListener(); 863 mPrimaryCallCardContainer.addOnLayoutChangeListener(listener); 864 865 // Prepare the state of views before the circular reveal animation 866 final int originalHeight = mPrimaryCallCardContainer.getHeight(); 867 mPrimaryCallCardContainer.setBottom(parent.getHeight()); 868 869 // Set up FAB. 870 mFloatingActionButtonContainer.setVisibility(View.GONE); 871 mFloatingActionButtonController.setScreenWidth(parent.getWidth()); 872 mCallButtonsContainer.setAlpha(0); 873 mCallStateLabel.setAlpha(0); 874 mPrimaryName.setAlpha(0); 875 mCallTypeLabel.setAlpha(0); 876 mCallNumberAndLabel.setAlpha(0); 877 878 final Animator animator = getOutgoingCallAnimator(touchPoint, 879 parent.getHeight(), originalHeight, showCircularReveal); 880 881 animator.addListener(new AnimatorListenerAdapter() { 882 @Override 883 public void onAnimationEnd(Animator animation) { 884 setViewStatePostAnimation(listener); 885 } 886 }); 887 animator.start(); 888 } 889 }); 890 } 891 onDialpadVisiblityChange(boolean isShown)892 public void onDialpadVisiblityChange(boolean isShown) { 893 mIsDialpadShowing = isShown; 894 updateFabPosition(); 895 } 896 updateFabPosition()897 private void updateFabPosition() { 898 int offsetY = 0; 899 if (!mIsDialpadShowing) { 900 offsetY = mFloatingActionButtonVerticalOffset; 901 if (mSecondaryCallInfo.isShown()) { 902 offsetY -= mSecondaryCallInfo.getHeight(); 903 } 904 } 905 906 mFloatingActionButtonController.align( 907 mIsLandscape ? FloatingActionButtonController.ALIGN_QUARTER_END 908 : FloatingActionButtonController.ALIGN_MIDDLE, 909 0 /* offsetX */, 910 offsetY, 911 true); 912 913 mFloatingActionButtonController.resize( 914 mIsDialpadShowing ? mFabSmallDiameter : mFabNormalDiameter, true); 915 } 916 917 @Override onResume()918 public void onResume() { 919 super.onResume(); 920 // If the previous launch animation is still running, cancel it so that we don't get 921 // stuck in an intermediate animation state. 922 if (mAnimatorSet != null && mAnimatorSet.isRunning()) { 923 mAnimatorSet.cancel(); 924 } 925 926 mIsLandscape = getResources().getConfiguration().orientation 927 == Configuration.ORIENTATION_LANDSCAPE; 928 929 final ViewGroup parent = ((ViewGroup) mPrimaryCallCardContainer.getParent()); 930 final ViewTreeObserver observer = parent.getViewTreeObserver(); 931 parent.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() { 932 @Override 933 public void onGlobalLayout() { 934 ViewTreeObserver viewTreeObserver = observer; 935 if (!viewTreeObserver.isAlive()) { 936 viewTreeObserver = parent.getViewTreeObserver(); 937 } 938 viewTreeObserver.removeOnGlobalLayoutListener(this); 939 mFloatingActionButtonController.setScreenWidth(parent.getWidth()); 940 updateFabPosition(); 941 } 942 }); 943 944 updateColors(); 945 } 946 947 /** 948 * Adds a global layout listener to update the FAB's positioning on the next layout. This allows 949 * us to position the FAB after the secondary call info's height has been calculated. 950 */ updateFabPositionForSecondaryCallInfo()951 private void updateFabPositionForSecondaryCallInfo() { 952 mSecondaryCallInfo.getViewTreeObserver().addOnGlobalLayoutListener( 953 new ViewTreeObserver.OnGlobalLayoutListener() { 954 @Override 955 public void onGlobalLayout() { 956 final ViewTreeObserver observer = mSecondaryCallInfo.getViewTreeObserver(); 957 if (!observer.isAlive()) { 958 return; 959 } 960 observer.removeOnGlobalLayoutListener(this); 961 962 onDialpadVisiblityChange(mIsDialpadShowing); 963 } 964 }); 965 } 966 967 /** 968 * Animator that performs the upwards shrinking animation of the blue call card scrim. 969 * At the start of the animation, each child view is moved downwards by a pre-specified amount 970 * and then translated upwards together with the scrim. 971 */ getShrinkAnimator(int startHeight, int endHeight)972 private Animator getShrinkAnimator(int startHeight, int endHeight) { 973 final Animator shrinkAnimator = 974 ObjectAnimator.ofInt(mPrimaryCallCardContainer, "bottom", startHeight, endHeight); 975 shrinkAnimator.setDuration(mShrinkAnimationDuration); 976 shrinkAnimator.addListener(new AnimatorListenerAdapter() { 977 @Override 978 public void onAnimationStart(Animator animation) { 979 assignTranslateAnimation(mCallStateLabel, 1); 980 assignTranslateAnimation(mCallStateIcon, 1); 981 assignTranslateAnimation(mPrimaryName, 2); 982 assignTranslateAnimation(mCallNumberAndLabel, 3); 983 assignTranslateAnimation(mCallTypeLabel, 4); 984 assignTranslateAnimation(mCallButtonsContainer, 5); 985 986 mFloatingActionButton.setEnabled(true); 987 } 988 }); 989 shrinkAnimator.setInterpolator(AnimUtils.EASE_IN); 990 return shrinkAnimator; 991 } 992 getRevealAnimator(Point touchPoint)993 private Animator getRevealAnimator(Point touchPoint) { 994 final Activity activity = getActivity(); 995 final View view = activity.getWindow().getDecorView(); 996 final Display display = activity.getWindowManager().getDefaultDisplay(); 997 final Point size = new Point(); 998 display.getSize(size); 999 1000 int startX = size.x / 2; 1001 int startY = size.y / 2; 1002 if (touchPoint != null) { 1003 startX = touchPoint.x; 1004 startY = touchPoint.y; 1005 } 1006 1007 final Animator valueAnimator = ViewAnimationUtils.createCircularReveal(view, 1008 startX, startY, 0, Math.max(size.x, size.y)); 1009 valueAnimator.setDuration(mRevealAnimationDuration); 1010 return valueAnimator; 1011 } 1012 getOutgoingCallAnimator(Point touchPoint, int startHeight, int endHeight, boolean showCircularReveal)1013 private Animator getOutgoingCallAnimator(Point touchPoint, int startHeight, int endHeight, 1014 boolean showCircularReveal) { 1015 1016 final Animator shrinkAnimator = getShrinkAnimator(startHeight, endHeight); 1017 1018 if (!showCircularReveal) { 1019 return shrinkAnimator; 1020 } 1021 1022 final Animator revealAnimator = getRevealAnimator(touchPoint); 1023 final AnimatorSet animatorSet = new AnimatorSet(); 1024 animatorSet.playSequentially(revealAnimator, shrinkAnimator); 1025 return animatorSet; 1026 } 1027 assignTranslateAnimation(View view, int offset)1028 private void assignTranslateAnimation(View view, int offset) { 1029 view.setTranslationY(mTranslationOffset * offset); 1030 view.animate().translationY(0).alpha(1).withLayer() 1031 .setDuration(mShrinkAnimationDuration).setInterpolator(AnimUtils.EASE_IN); 1032 } 1033 setViewStatePostAnimation(View view)1034 private void setViewStatePostAnimation(View view) { 1035 view.setTranslationY(0); 1036 view.setAlpha(1); 1037 } 1038 setViewStatePostAnimation(OnLayoutChangeListener layoutChangeListener)1039 private void setViewStatePostAnimation(OnLayoutChangeListener layoutChangeListener) { 1040 setViewStatePostAnimation(mCallButtonsContainer); 1041 setViewStatePostAnimation(mCallStateLabel); 1042 setViewStatePostAnimation(mPrimaryName); 1043 setViewStatePostAnimation(mCallTypeLabel); 1044 setViewStatePostAnimation(mCallNumberAndLabel); 1045 setViewStatePostAnimation(mCallStateIcon); 1046 1047 mPrimaryCallCardContainer.removeOnLayoutChangeListener(layoutChangeListener); 1048 mPrimaryCallInfo.getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING); 1049 mFloatingActionButtonController.scaleIn(AnimUtils.NO_DELAY); 1050 } 1051 1052 private final class LayoutIgnoringListener implements View.OnLayoutChangeListener { 1053 @Override onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom)1054 public void onLayoutChange(View v, 1055 int left, 1056 int top, 1057 int right, 1058 int bottom, 1059 int oldLeft, 1060 int oldTop, 1061 int oldRight, 1062 int oldBottom) { 1063 v.setLeft(oldLeft); 1064 v.setRight(oldRight); 1065 v.setTop(oldTop); 1066 v.setBottom(oldBottom); 1067 } 1068 } 1069 } 1070