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