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