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