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