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