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