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