CallCardFragment.java revision 45f2a7014de8102479a50886300fecbd704779a4
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 (VideoProfile.VideoState.isVideo(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 String durationDescription = InCallDateUtils.formatDetailedDuration(duration); 624 mElapsedTime.setText(callTimeElapsed); 625 mElapsedTime.setContentDescription(durationDescription); 626 } else { 627 // hide() animation has no effect if it is already hidden. 628 AnimUtils.fadeOut(mElapsedTime, AnimUtils.DEFAULT_DURATION); 629 } 630 } 631 632 private void setDrawableToImageView(ImageView view, Drawable photo) { 633 if (photo == null) { 634 photo = ContactInfoCache.getInstance( 635 view.getContext()).getDefaultContactPhotoDrawable(); 636 } 637 638 if (mPrimaryPhotoDrawable == photo) { 639 return; 640 } 641 mPrimaryPhotoDrawable = photo; 642 643 final Drawable current = view.getDrawable(); 644 if (current == null) { 645 view.setImageDrawable(photo); 646 AnimUtils.fadeIn(mElapsedTime, AnimUtils.DEFAULT_DURATION); 647 } else { 648 // Cross fading is buggy and not noticable due to the multiple calls to this method 649 // that switch drawables in the middle of the cross-fade animations. Just set the 650 // photo directly instead. 651 view.setImageDrawable(photo); 652 view.setVisibility(View.VISIBLE); 653 } 654 } 655 656 /** 657 * Gets the call state label based on the state of the call or cause of disconnect. 658 * 659 * Additional labels are applied as follows: 660 * 1. All outgoing calls with display "Calling via [Provider]". 661 * 2. Ongoing calls will display the name of the provider. 662 * 3. Incoming calls will only display "Incoming via..." for accounts. 663 * 4. Video calls, and session modification states (eg. requesting video). 664 * 5. Incoming and active Wi-Fi calls will show label provided by hint. 665 */ 666 private CharSequence getCallStateLabelFromState(int state, int videoState, 667 int sessionModificationState, DisconnectCause disconnectCause, String label, 668 boolean isGatewayCall, boolean isWifi) { 669 final Context context = getView().getContext(); 670 CharSequence callStateLabel = null; // Label to display as part of the call banner 671 672 boolean isSpecialCall = label != null; 673 boolean isAccount = isSpecialCall && !isGatewayCall; 674 675 switch (state) { 676 case Call.State.IDLE: 677 // "Call state" is meaningless in this state. 678 break; 679 case Call.State.ACTIVE: 680 // We normally don't show a "call state label" at all in this state 681 // (but we can use the call state label to display the provider name). 682 if (isAccount || isWifi) { 683 callStateLabel = label; 684 } else if (sessionModificationState 685 == Call.SessionModificationState.REQUEST_FAILED) { 686 callStateLabel = context.getString(R.string.card_title_video_call_error); 687 } else if (sessionModificationState 688 == Call.SessionModificationState.WAITING_FOR_RESPONSE) { 689 callStateLabel = context.getString(R.string.card_title_video_call_requesting); 690 } else if (VideoProfile.VideoState.isVideo(videoState) && 691 VideoProfile.VideoState.isPaused(videoState)) { 692 callStateLabel = context.getString(R.string.card_title_video_call_paused); 693 } else if (VideoProfile.VideoState.isBidirectional(videoState)) { 694 callStateLabel = context.getString(R.string.card_title_video_call); 695 } 696 break; 697 case Call.State.ONHOLD: 698 callStateLabel = context.getString(R.string.card_title_on_hold); 699 break; 700 case Call.State.CONNECTING: 701 case Call.State.DIALING: 702 if (isSpecialCall && !isWifi) { 703 callStateLabel = context.getString(R.string.calling_via_template, label); 704 } else { 705 callStateLabel = context.getString(R.string.card_title_dialing); 706 } 707 break; 708 case Call.State.REDIALING: 709 callStateLabel = context.getString(R.string.card_title_redialing); 710 break; 711 case Call.State.INCOMING: 712 case Call.State.CALL_WAITING: 713 if (isWifi) { 714 callStateLabel = label; 715 } else if (isAccount) { 716 callStateLabel = context.getString(R.string.incoming_via_template, label); 717 } else if (VideoProfile.VideoState.isBidirectional(videoState)) { 718 callStateLabel = context.getString(R.string.notification_incoming_video_call); 719 } else { 720 callStateLabel = context.getString(R.string.card_title_incoming_call); 721 } 722 break; 723 case Call.State.DISCONNECTING: 724 // While in the DISCONNECTING state we display a "Hanging up" 725 // message in order to make the UI feel more responsive. (In 726 // GSM it's normal to see a delay of a couple of seconds while 727 // negotiating the disconnect with the network, so the "Hanging 728 // up" state at least lets the user know that we're doing 729 // something. This state is currently not used with CDMA.) 730 callStateLabel = context.getString(R.string.card_title_hanging_up); 731 break; 732 case Call.State.DISCONNECTED: 733 callStateLabel = disconnectCause.getLabel(); 734 if (TextUtils.isEmpty(callStateLabel)) { 735 callStateLabel = context.getString(R.string.card_title_call_ended); 736 } 737 break; 738 case Call.State.CONFERENCED: 739 callStateLabel = context.getString(R.string.card_title_conf_call); 740 break; 741 default: 742 Log.wtf(this, "updateCallStateWidgets: unexpected call: " + state); 743 } 744 return callStateLabel; 745 } 746 747 private void showAndInitializeSecondaryCallInfo(boolean hasProvider) { 748 mSecondaryCallInfo.setVisibility(View.VISIBLE); 749 750 // mSecondaryCallName is initialized here (vs. onViewCreated) because it is inaccessible 751 // until mSecondaryCallInfo is inflated in the call above. 752 if (mSecondaryCallName == null) { 753 mSecondaryCallName = (TextView) getView().findViewById(R.id.secondaryCallName); 754 mSecondaryCallConferenceCallIcon = 755 getView().findViewById(R.id.secondaryCallConferenceCallIcon); 756 mSecondaryCallVideoCallIcon = 757 getView().findViewById(R.id.secondaryCallVideoCallIcon); 758 } 759 760 if (mSecondaryCallProviderLabel == null && hasProvider) { 761 mSecondaryCallProviderInfo.setVisibility(View.VISIBLE); 762 mSecondaryCallProviderLabel = (TextView) getView() 763 .findViewById(R.id.secondaryCallProviderLabel); 764 } 765 } 766 767 public void dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 768 if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { 769 dispatchPopulateAccessibilityEvent(event, mCallStateLabel); 770 dispatchPopulateAccessibilityEvent(event, mPrimaryName); 771 dispatchPopulateAccessibilityEvent(event, mPhoneNumber); 772 return; 773 } 774 dispatchPopulateAccessibilityEvent(event, mCallStateLabel); 775 dispatchPopulateAccessibilityEvent(event, mPrimaryName); 776 dispatchPopulateAccessibilityEvent(event, mPhoneNumber); 777 dispatchPopulateAccessibilityEvent(event, mCallTypeLabel); 778 dispatchPopulateAccessibilityEvent(event, mSecondaryCallName); 779 dispatchPopulateAccessibilityEvent(event, mSecondaryCallProviderLabel); 780 781 return; 782 } 783 784 @Override 785 public void setEndCallButtonEnabled(boolean enabled, boolean animate) { 786 if (enabled != mFloatingActionButton.isEnabled()) { 787 if (animate) { 788 if (enabled) { 789 mFloatingActionButtonController.scaleIn(AnimUtils.NO_DELAY); 790 } else { 791 mFloatingActionButtonController.scaleOut(); 792 } 793 } else { 794 if (enabled) { 795 mFloatingActionButtonContainer.setScaleX(1); 796 mFloatingActionButtonContainer.setScaleY(1); 797 mFloatingActionButtonContainer.setVisibility(View.VISIBLE); 798 } else { 799 mFloatingActionButtonContainer.setVisibility(View.GONE); 800 } 801 } 802 mFloatingActionButton.setEnabled(enabled); 803 updateFabPosition(); 804 } 805 } 806 807 /** 808 * Changes the visibility of the HD audio icon. 809 * 810 * @param visible {@code true} if the UI should show the HD audio icon. 811 */ 812 @Override 813 public void showHdAudioIndicator(boolean visible) { 814 mHdAudioIcon.setVisibility(visible ? View.VISIBLE : View.GONE); 815 } 816 817 /** 818 * Changes the visibility of the "manage conference call" button. 819 * 820 * @param visible Whether to set the button to be visible or not. 821 */ 822 @Override 823 public void showManageConferenceCallButton(boolean visible) { 824 mManageConferenceCallButton.setVisibility(visible ? View.VISIBLE : View.GONE); 825 } 826 827 /** 828 * Determines the current visibility of the manage conference button. 829 * 830 * @return {@code true} if the button is visible. 831 */ 832 @Override 833 public boolean isManageConferenceVisible() { 834 return mManageConferenceCallButton.getVisibility() == View.VISIBLE; 835 } 836 837 /** 838 * Get the overall InCallUI background colors and apply to call card. 839 */ 840 public void updateColors() { 841 MaterialPalette themeColors = InCallPresenter.getInstance().getThemeColors(); 842 843 if (mCurrentThemeColors != null && mCurrentThemeColors.equals(themeColors)) { 844 return; 845 } 846 847 if (getResources().getBoolean(R.bool.is_layout_landscape)) { 848 final GradientDrawable drawable = 849 (GradientDrawable) mPrimaryCallCardContainer.getBackground(); 850 drawable.setColor(themeColors.mPrimaryColor); 851 } else { 852 mPrimaryCallCardContainer.setBackgroundColor(themeColors.mPrimaryColor); 853 } 854 mCallButtonsContainer.setBackgroundColor(themeColors.mPrimaryColor); 855 856 mCurrentThemeColors = themeColors; 857 } 858 859 private void dispatchPopulateAccessibilityEvent(AccessibilityEvent event, View view) { 860 if (view == null) return; 861 final List<CharSequence> eventText = event.getText(); 862 int size = eventText.size(); 863 view.dispatchPopulateAccessibilityEvent(event); 864 // if no text added write null to keep relative position 865 if (size == eventText.size()) { 866 eventText.add(null); 867 } 868 } 869 870 @Override 871 public void animateForNewOutgoingCall() { 872 final ViewGroup parent = (ViewGroup) mPrimaryCallCardContainer.getParent(); 873 874 final ViewTreeObserver observer = getView().getViewTreeObserver(); 875 876 final LayoutTransition transition = mPrimaryCallInfo.getLayoutTransition(); 877 transition.disableTransitionType(LayoutTransition.CHANGING); 878 879 mIsAnimating = true; 880 881 observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() { 882 @Override 883 public void onGlobalLayout() { 884 final ViewTreeObserver observer = getView().getViewTreeObserver(); 885 if (!observer.isAlive()) { 886 return; 887 } 888 observer.removeOnGlobalLayoutListener(this); 889 890 final LayoutIgnoringListener listener = new LayoutIgnoringListener(); 891 mPrimaryCallCardContainer.addOnLayoutChangeListener(listener); 892 893 // Prepare the state of views before the slide animation 894 final int originalHeight = mPrimaryCallCardContainer.getHeight(); 895 mPrimaryCallCardContainer.setTag(R.id.view_tag_callcard_actual_height, 896 originalHeight); 897 mPrimaryCallCardContainer.setBottom(parent.getHeight()); 898 899 // Set up FAB. 900 mFloatingActionButtonContainer.setVisibility(View.GONE); 901 mFloatingActionButtonController.setScreenWidth(parent.getWidth()); 902 903 mCallButtonsContainer.setAlpha(0); 904 mCallStateLabel.setAlpha(0); 905 mPrimaryName.setAlpha(0); 906 mCallTypeLabel.setAlpha(0); 907 mCallNumberAndLabel.setAlpha(0); 908 909 assignTranslateAnimation(mCallStateLabel, 1); 910 assignTranslateAnimation(mCallStateIcon, 1); 911 assignTranslateAnimation(mPrimaryName, 2); 912 assignTranslateAnimation(mCallNumberAndLabel, 3); 913 assignTranslateAnimation(mCallTypeLabel, 4); 914 assignTranslateAnimation(mCallButtonsContainer, 5); 915 916 final Animator animator = getShrinkAnimator(parent.getHeight(), originalHeight); 917 918 animator.addListener(new AnimatorListenerAdapter() { 919 @Override 920 public void onAnimationEnd(Animator animation) { 921 mPrimaryCallCardContainer.setTag(R.id.view_tag_callcard_actual_height, 922 null); 923 setViewStatePostAnimation(listener); 924 mIsAnimating = false; 925 InCallPresenter.getInstance().onShrinkAnimationComplete(); 926 } 927 }); 928 animator.start(); 929 } 930 }); 931 } 932 933 public void onDialpadVisibilityChange(boolean isShown) { 934 mIsDialpadShowing = isShown; 935 updateFabPosition(); 936 } 937 938 private void updateFabPosition() { 939 int offsetY = 0; 940 if (!mIsDialpadShowing) { 941 offsetY = mFloatingActionButtonVerticalOffset; 942 if (mSecondaryCallInfo.isShown()) { 943 offsetY -= mSecondaryCallInfo.getHeight(); 944 } 945 } 946 947 mFloatingActionButtonController.align( 948 mIsLandscape ? FloatingActionButtonController.ALIGN_QUARTER_END 949 : FloatingActionButtonController.ALIGN_MIDDLE, 950 0 /* offsetX */, 951 offsetY, 952 true); 953 954 mFloatingActionButtonController.resize( 955 mIsDialpadShowing ? mFabSmallDiameter : mFabNormalDiameter, true); 956 } 957 958 @Override 959 public void onResume() { 960 super.onResume(); 961 // If the previous launch animation is still running, cancel it so that we don't get 962 // stuck in an intermediate animation state. 963 if (mAnimatorSet != null && mAnimatorSet.isRunning()) { 964 mAnimatorSet.cancel(); 965 } 966 967 mIsLandscape = getResources().getBoolean(R.bool.is_layout_landscape); 968 969 final ViewGroup parent = ((ViewGroup) mPrimaryCallCardContainer.getParent()); 970 final ViewTreeObserver observer = parent.getViewTreeObserver(); 971 parent.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() { 972 @Override 973 public void onGlobalLayout() { 974 ViewTreeObserver viewTreeObserver = observer; 975 if (!viewTreeObserver.isAlive()) { 976 viewTreeObserver = parent.getViewTreeObserver(); 977 } 978 viewTreeObserver.removeOnGlobalLayoutListener(this); 979 mFloatingActionButtonController.setScreenWidth(parent.getWidth()); 980 updateFabPosition(); 981 } 982 }); 983 984 updateColors(); 985 } 986 987 /** 988 * Adds a global layout listener to update the FAB's positioning on the next layout. This allows 989 * us to position the FAB after the secondary call info's height has been calculated. 990 */ 991 private void updateFabPositionForSecondaryCallInfo() { 992 mSecondaryCallInfo.getViewTreeObserver().addOnGlobalLayoutListener( 993 new ViewTreeObserver.OnGlobalLayoutListener() { 994 @Override 995 public void onGlobalLayout() { 996 final ViewTreeObserver observer = mSecondaryCallInfo.getViewTreeObserver(); 997 if (!observer.isAlive()) { 998 return; 999 } 1000 observer.removeOnGlobalLayoutListener(this); 1001 1002 onDialpadVisibilityChange(mIsDialpadShowing); 1003 } 1004 }); 1005 } 1006 1007 /** 1008 * Animator that performs the upwards shrinking animation of the blue call card scrim. 1009 * At the start of the animation, each child view is moved downwards by a pre-specified amount 1010 * and then translated upwards together with the scrim. 1011 */ 1012 private Animator getShrinkAnimator(int startHeight, int endHeight) { 1013 final ObjectAnimator shrinkAnimator = 1014 ObjectAnimator.ofInt(mPrimaryCallCardContainer, "bottom", startHeight, endHeight); 1015 shrinkAnimator.setDuration(mShrinkAnimationDuration); 1016 shrinkAnimator.addListener(new AnimatorListenerAdapter() { 1017 @Override 1018 public void onAnimationStart(Animator animation) { 1019 mFloatingActionButton.setEnabled(true); 1020 } 1021 }); 1022 shrinkAnimator.setInterpolator(AnimUtils.EASE_IN); 1023 return shrinkAnimator; 1024 } 1025 1026 private void assignTranslateAnimation(View view, int offset) { 1027 view.setLayerType(View.LAYER_TYPE_HARDWARE, null); 1028 view.buildLayer(); 1029 view.setTranslationY(mTranslationOffset * offset); 1030 view.animate().translationY(0).alpha(1).withLayer() 1031 .setDuration(mShrinkAnimationDuration).setInterpolator(AnimUtils.EASE_IN); 1032 } 1033 1034 private void setViewStatePostAnimation(View view) { 1035 view.setTranslationY(0); 1036 view.setAlpha(1); 1037 } 1038 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 1049 final LayoutTransition transition = mPrimaryCallInfo.getLayoutTransition(); 1050 transition.enableTransitionType(LayoutTransition.CHANGING); 1051 1052 mFloatingActionButtonController.scaleIn(AnimUtils.NO_DELAY); 1053 } 1054 1055 private final class LayoutIgnoringListener implements View.OnLayoutChangeListener { 1056 @Override 1057 public void onLayoutChange(View v, 1058 int left, 1059 int top, 1060 int right, 1061 int bottom, 1062 int oldLeft, 1063 int oldTop, 1064 int oldRight, 1065 int oldBottom) { 1066 v.setLeft(oldLeft); 1067 v.setRight(oldRight); 1068 v.setTop(oldTop); 1069 v.setBottom(oldBottom); 1070 } 1071 } 1072} 1073