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