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.ObjectAnimator; 23 import android.content.Context; 24 import android.graphics.Bitmap; 25 import android.graphics.Canvas; 26 import android.graphics.drawable.AnimationDrawable; 27 import android.graphics.drawable.BitmapDrawable; 28 import android.graphics.drawable.Drawable; 29 import android.graphics.drawable.GradientDrawable; 30 import android.os.Bundle; 31 import android.os.Handler; 32 import android.os.Looper; 33 import android.os.Trace; 34 import android.support.v4.graphics.drawable.RoundedBitmapDrawable; 35 import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory; 36 import android.telecom.DisconnectCause; 37 import android.telephony.PhoneNumberUtils; 38 import android.text.TextUtils; 39 import android.text.format.DateUtils; 40 import android.view.LayoutInflater; 41 import android.view.View; 42 import android.view.View.OnLayoutChangeListener; 43 import android.view.ViewGroup; 44 import android.view.ViewPropertyAnimator; 45 import android.view.ViewTreeObserver; 46 import android.view.ViewTreeObserver.OnGlobalLayoutListener; 47 import android.view.accessibility.AccessibilityEvent; 48 import android.view.accessibility.AccessibilityManager; 49 import android.view.animation.Animation; 50 import android.view.animation.AnimationUtils; 51 import android.widget.ImageButton; 52 import android.widget.ImageView; 53 import android.widget.LinearLayout; 54 import android.widget.ListAdapter; 55 import android.widget.ListView; 56 import android.widget.TextView; 57 import android.widget.Toast; 58 59 import com.android.contacts.common.compat.PhoneNumberUtilsCompat; 60 import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette; 61 import com.android.contacts.common.widget.FloatingActionButtonController; 62 import com.android.dialer.R; 63 import com.android.phone.common.animation.AnimUtils; 64 65 import java.util.List; 66 67 /** 68 * Fragment for call card. 69 */ 70 public class CallCardFragment extends BaseFragment<CallCardPresenter, CallCardPresenter.CallCardUi> 71 implements CallCardPresenter.CallCardUi { 72 private static final String TAG = "CallCardFragment"; 73 74 /** 75 * Internal class which represents the call state label which is to be applied. 76 */ 77 private class CallStateLabel { 78 private CharSequence mCallStateLabel; 79 private boolean mIsAutoDismissing; 80 81 public CallStateLabel(CharSequence callStateLabel, boolean isAutoDismissing) { 82 mCallStateLabel = callStateLabel; 83 mIsAutoDismissing = isAutoDismissing; 84 } 85 86 public CharSequence getCallStateLabel() { 87 return mCallStateLabel; 88 } 89 90 /** 91 * Determines if the call state label should auto-dismiss. 92 * 93 * @return {@code true} if the call state label should auto-dismiss. 94 */ 95 public boolean isAutoDismissing() { 96 return mIsAutoDismissing; 97 } 98 }; 99 100 private static final String IS_DIALPAD_SHOWING_KEY = "is_dialpad_showing"; 101 102 /** 103 * The duration of time (in milliseconds) a call state label should remain visible before 104 * resetting to its previous value. 105 */ 106 private static final long CALL_STATE_LABEL_RESET_DELAY_MS = 3000; 107 /** 108 * Amount of time to wait before sending an announcement via the accessibility manager. 109 * When the call state changes to an outgoing or incoming state for the first time, the 110 * UI can often be changing due to call updates or contact lookup. This allows the UI 111 * to settle to a stable state to ensure that the correct information is announced. 112 */ 113 private static final long ACCESSIBILITY_ANNOUNCEMENT_DELAY_MS = 500; 114 115 private AnimatorSet mAnimatorSet; 116 private int mShrinkAnimationDuration; 117 private int mFabNormalDiameter; 118 private int mFabSmallDiameter; 119 private boolean mIsLandscape; 120 private boolean mHasLargePhoto; 121 private boolean mIsDialpadShowing; 122 123 // Primary caller info 124 private TextView mPhoneNumber; 125 private TextView mNumberLabel; 126 private TextView mPrimaryName; 127 private View mCallStateButton; 128 private ImageView mCallStateIcon; 129 private ImageView mCallStateVideoCallIcon; 130 private TextView mCallStateLabel; 131 private TextView mCallTypeLabel; 132 private ImageView mHdAudioIcon; 133 private ImageView mForwardIcon; 134 private View mCallNumberAndLabel; 135 private TextView mElapsedTime; 136 private Drawable mPrimaryPhotoDrawable; 137 private TextView mCallSubject; 138 private ImageView mWorkProfileIcon; 139 140 // Container view that houses the entire primary call card, including the call buttons 141 private View mPrimaryCallCardContainer; 142 // Container view that houses the primary call information 143 private ViewGroup mPrimaryCallInfo; 144 private View mCallButtonsContainer; 145 private ImageView mPhotoSmall; 146 147 // Secondary caller info 148 private View mSecondaryCallInfo; 149 private TextView mSecondaryCallName; 150 private View mSecondaryCallProviderInfo; 151 private TextView mSecondaryCallProviderLabel; 152 private View mSecondaryCallConferenceCallIcon; 153 private View mSecondaryCallVideoCallIcon; 154 private View mProgressSpinner; 155 156 // Call card content 157 private View mCallCardContent; 158 private ImageView mPhotoLarge; 159 private View mContactContext; 160 private TextView mContactContextTitle; 161 private ListView mContactContextListView; 162 private LinearLayout mContactContextListHeaders; 163 164 private View mManageConferenceCallButton; 165 166 // Dark number info bar 167 private TextView mInCallMessageLabel; 168 169 private FloatingActionButtonController mFloatingActionButtonController; 170 private View mFloatingActionButtonContainer; 171 private ImageButton mFloatingActionButton; 172 private int mFloatingActionButtonVerticalOffset; 173 174 private float mTranslationOffset; 175 private Animation mPulseAnimation; 176 177 private int mVideoAnimationDuration; 178 // Whether or not the call card is currently in the process of an animation 179 private boolean mIsAnimating; 180 181 private MaterialPalette mCurrentThemeColors; 182 183 /** 184 * Call state label to set when an auto-dismissing call state label is dismissed. 185 */ 186 private CharSequence mPostResetCallStateLabel; 187 private boolean mCallStateLabelResetPending = false; 188 private Handler mHandler; 189 190 /** 191 * Determines if secondary call info is populated in the secondary call info UI. 192 */ 193 private boolean mHasSecondaryCallInfo = false; 194 195 @Override 196 public CallCardPresenter.CallCardUi getUi() { 197 return this; 198 } 199 200 @Override 201 public CallCardPresenter createPresenter() { 202 return new CallCardPresenter(); 203 } 204 205 @Override 206 public void onCreate(Bundle savedInstanceState) { 207 super.onCreate(savedInstanceState); 208 209 mHandler = new Handler(Looper.getMainLooper()); 210 mShrinkAnimationDuration = getResources().getInteger(R.integer.shrink_animation_duration); 211 mVideoAnimationDuration = getResources().getInteger(R.integer.video_animation_duration); 212 mFloatingActionButtonVerticalOffset = getResources().getDimensionPixelOffset( 213 R.dimen.floating_action_button_vertical_offset); 214 mFabNormalDiameter = getResources().getDimensionPixelOffset( 215 R.dimen.end_call_floating_action_button_diameter); 216 mFabSmallDiameter = getResources().getDimensionPixelOffset( 217 R.dimen.end_call_floating_action_button_small_diameter); 218 219 if (savedInstanceState != null) { 220 mIsDialpadShowing = savedInstanceState.getBoolean(IS_DIALPAD_SHOWING_KEY, false); 221 } 222 } 223 224 @Override 225 public void onActivityCreated(Bundle savedInstanceState) { 226 super.onActivityCreated(savedInstanceState); 227 228 final CallList calls = CallList.getInstance(); 229 final Call call = calls.getFirstCall(); 230 getPresenter().init(getActivity(), call); 231 } 232 233 @Override 234 public void onSaveInstanceState(Bundle outState) { 235 outState.putBoolean(IS_DIALPAD_SHOWING_KEY, mIsDialpadShowing); 236 super.onSaveInstanceState(outState); 237 } 238 239 @Override 240 public View onCreateView(LayoutInflater inflater, ViewGroup container, 241 Bundle savedInstanceState) { 242 Trace.beginSection(TAG + " onCreate"); 243 mTranslationOffset = 244 getResources().getDimensionPixelSize(R.dimen.call_card_anim_translate_y_offset); 245 final View view = inflater.inflate(R.layout.call_card_fragment, container, false); 246 Trace.endSection(); 247 return view; 248 } 249 250 @Override 251 public void onViewCreated(View view, Bundle savedInstanceState) { 252 super.onViewCreated(view, savedInstanceState); 253 254 mPulseAnimation = 255 AnimationUtils.loadAnimation(view.getContext(), R.anim.call_status_pulse); 256 257 mPhoneNumber = (TextView) view.findViewById(R.id.phoneNumber); 258 mPrimaryName = (TextView) view.findViewById(R.id.name); 259 mNumberLabel = (TextView) view.findViewById(R.id.label); 260 mSecondaryCallInfo = view.findViewById(R.id.secondary_call_info); 261 mSecondaryCallProviderInfo = view.findViewById(R.id.secondary_call_provider_info); 262 mCallCardContent = view.findViewById(R.id.call_card_content); 263 mPhotoLarge = (ImageView) view.findViewById(R.id.photoLarge); 264 mPhotoLarge.setOnClickListener(new View.OnClickListener() { 265 @Override 266 public void onClick(View v) { 267 getPresenter().onContactPhotoClick(); 268 } 269 }); 270 271 mContactContext = view.findViewById(R.id.contact_context); 272 mContactContextTitle = (TextView) view.findViewById(R.id.contactContextTitle); 273 mContactContextListView = (ListView) view.findViewById(R.id.contactContextInfo); 274 // This layout stores all the list header layouts so they can be easily removed. 275 mContactContextListHeaders = new LinearLayout(getView().getContext()); 276 mContactContextListView.addHeaderView(mContactContextListHeaders); 277 278 mCallStateIcon = (ImageView) view.findViewById(R.id.callStateIcon); 279 mCallStateVideoCallIcon = (ImageView) view.findViewById(R.id.videoCallIcon); 280 mWorkProfileIcon = (ImageView) view.findViewById(R.id.workProfileIcon); 281 mCallStateLabel = (TextView) view.findViewById(R.id.callStateLabel); 282 mHdAudioIcon = (ImageView) view.findViewById(R.id.hdAudioIcon); 283 mForwardIcon = (ImageView) view.findViewById(R.id.forwardIcon); 284 mCallNumberAndLabel = view.findViewById(R.id.labelAndNumber); 285 mCallTypeLabel = (TextView) view.findViewById(R.id.callTypeLabel); 286 mElapsedTime = (TextView) view.findViewById(R.id.elapsedTime); 287 mPrimaryCallCardContainer = view.findViewById(R.id.primary_call_info_container); 288 mPrimaryCallInfo = (ViewGroup) view.findViewById(R.id.primary_call_banner); 289 mCallButtonsContainer = view.findViewById(R.id.callButtonFragment); 290 mPhotoSmall = (ImageView) view.findViewById(R.id.photoSmall); 291 mPhotoSmall.setVisibility(View.GONE); 292 mInCallMessageLabel = (TextView) view.findViewById(R.id.connectionServiceMessage); 293 mProgressSpinner = view.findViewById(R.id.progressSpinner); 294 295 mFloatingActionButtonContainer = view.findViewById( 296 R.id.floating_end_call_action_button_container); 297 mFloatingActionButton = (ImageButton) view.findViewById( 298 R.id.floating_end_call_action_button); 299 mFloatingActionButton.setOnClickListener(new View.OnClickListener() { 300 @Override 301 public void onClick(View v) { 302 getPresenter().endCallClicked(); 303 } 304 }); 305 mFloatingActionButtonController = new FloatingActionButtonController(getActivity(), 306 mFloatingActionButtonContainer, mFloatingActionButton); 307 308 mSecondaryCallInfo.setOnClickListener(new View.OnClickListener() { 309 @Override 310 public void onClick(View v) { 311 getPresenter().secondaryInfoClicked(); 312 updateFabPositionForSecondaryCallInfo(); 313 } 314 }); 315 316 mCallStateButton = view.findViewById(R.id.callStateButton); 317 mCallStateButton.setOnLongClickListener(new View.OnLongClickListener() { 318 @Override 319 public boolean onLongClick(View v) { 320 getPresenter().onCallStateButtonTouched(); 321 return false; 322 } 323 }); 324 325 mManageConferenceCallButton = view.findViewById(R.id.manage_conference_call_button); 326 mManageConferenceCallButton.setOnClickListener(new View.OnClickListener() { 327 @Override 328 public void onClick(View v) { 329 InCallActivity activity = (InCallActivity) getActivity(); 330 activity.showConferenceFragment(true); 331 } 332 }); 333 334 mPrimaryName.setElegantTextHeight(false); 335 mCallStateLabel.setElegantTextHeight(false); 336 mCallSubject = (TextView) view.findViewById(R.id.callSubject); 337 } 338 339 @Override 340 public void setVisible(boolean on) { 341 if (on) { 342 getView().setVisibility(View.VISIBLE); 343 } else { 344 getView().setVisibility(View.INVISIBLE); 345 } 346 } 347 348 /** 349 * Hides or shows the progress spinner. 350 * 351 * @param visible {@code True} if the progress spinner should be visible. 352 */ 353 @Override 354 public void setProgressSpinnerVisible(boolean visible) { 355 mProgressSpinner.setVisibility(visible ? View.VISIBLE : View.GONE); 356 } 357 358 @Override 359 public void setContactContextTitle(View headerView) { 360 mContactContextListHeaders.removeAllViews(); 361 mContactContextListHeaders.addView(headerView); 362 } 363 364 @Override 365 public void setContactContextContent(ListAdapter listAdapter) { 366 mContactContextListView.setAdapter(listAdapter); 367 } 368 369 @Override 370 public void showContactContext(boolean show) { 371 showImageView(mPhotoLarge, !show); 372 showImageView(mPhotoSmall, show); 373 mPrimaryCallCardContainer.setElevation( 374 show ? 0 : getResources().getDimension(R.dimen.primary_call_elevation)); 375 mContactContext.setVisibility(show ? View.VISIBLE : View.GONE); 376 } 377 378 /** 379 * Sets the visibility of the primary call card. 380 * Ensures that when the primary call card is hidden, the video surface slides over to fill the 381 * entire screen. 382 * 383 * @param visible {@code True} if the primary call card should be visible. 384 */ 385 @Override 386 public void setCallCardVisible(final boolean visible) { 387 Log.v(this, "setCallCardVisible : isVisible = " + visible); 388 // When animating the hide/show of the views in a landscape layout, we need to take into 389 // account whether we are in a left-to-right locale or a right-to-left locale and adjust 390 // the animations accordingly. 391 final boolean isLayoutRtl = InCallPresenter.isRtl(); 392 393 // Retrieve here since at fragment creation time the incoming video view is not inflated. 394 final View videoView = getView().findViewById(R.id.incomingVideo); 395 if (videoView == null) { 396 return; 397 } 398 399 // Determine how much space there is below or to the side of the call card. 400 final float spaceBesideCallCard = getSpaceBesideCallCard(); 401 402 // We need to translate the video surface, but we need to know its position after the layout 403 // has occurred so use a {@code ViewTreeObserver}. 404 final ViewTreeObserver observer = getView().getViewTreeObserver(); 405 observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 406 @Override 407 public boolean onPreDraw() { 408 // We don't want to continue getting called. 409 getView().getViewTreeObserver().removeOnPreDrawListener(this); 410 411 float videoViewTranslation = 0f; 412 413 // Translate the call card to its pre-animation state. 414 if (!mIsLandscape) { 415 mPrimaryCallCardContainer.setTranslationY(visible ? 416 -mPrimaryCallCardContainer.getHeight() : 0); 417 418 ViewGroup.LayoutParams p = videoView.getLayoutParams(); 419 videoViewTranslation = p.height / 2 - spaceBesideCallCard / 2; 420 } 421 422 // Perform animation of video view. 423 ViewPropertyAnimator videoViewAnimator = videoView.animate() 424 .setInterpolator(AnimUtils.EASE_OUT_EASE_IN) 425 .setDuration(mVideoAnimationDuration); 426 if (mIsLandscape) { 427 videoViewAnimator 428 .translationX(visible ? videoViewTranslation : 0); 429 } else { 430 videoViewAnimator 431 .translationY(visible ? videoViewTranslation : 0); 432 } 433 videoViewAnimator.start(); 434 435 // Animate the call card sliding. 436 ViewPropertyAnimator callCardAnimator = mPrimaryCallCardContainer.animate() 437 .setInterpolator(AnimUtils.EASE_OUT_EASE_IN) 438 .setDuration(mVideoAnimationDuration) 439 .setListener(new AnimatorListenerAdapter() { 440 @Override 441 public void onAnimationEnd(Animator animation) { 442 super.onAnimationEnd(animation); 443 if (!visible) { 444 mPrimaryCallCardContainer.setVisibility(View.GONE); 445 } 446 } 447 448 @Override 449 public void onAnimationStart(Animator animation) { 450 super.onAnimationStart(animation); 451 if (visible) { 452 mPrimaryCallCardContainer.setVisibility(View.VISIBLE); 453 } 454 } 455 }); 456 457 if (mIsLandscape) { 458 float translationX = mPrimaryCallCardContainer.getWidth(); 459 translationX *= isLayoutRtl ? 1 : -1; 460 callCardAnimator 461 .translationX(visible ? 0 : translationX) 462 .start(); 463 } else { 464 callCardAnimator 465 .translationY(visible ? 0 : -mPrimaryCallCardContainer.getHeight()) 466 .start(); 467 } 468 469 return true; 470 } 471 }); 472 } 473 474 /** 475 * Determines the amount of space below the call card for portrait layouts), or beside the 476 * call card for landscape layouts. 477 * 478 * @return The amount of space below or beside the call card. 479 */ 480 public float getSpaceBesideCallCard() { 481 if (mIsLandscape) { 482 return getView().getWidth() - mPrimaryCallCardContainer.getWidth(); 483 } else { 484 final int callCardHeight; 485 // Retrieve the actual height of the call card, independent of whether or not the 486 // outgoing call animation is in progress. The animation does not run in landscape mode 487 // so this only needs to be done for portrait. 488 if (mPrimaryCallCardContainer.getTag(R.id.view_tag_callcard_actual_height) != null) { 489 callCardHeight = (int) mPrimaryCallCardContainer.getTag( 490 R.id.view_tag_callcard_actual_height); 491 } else { 492 callCardHeight = mPrimaryCallCardContainer.getHeight(); 493 } 494 return getView().getHeight() - callCardHeight; 495 } 496 } 497 498 @Override 499 public void setPrimaryName(String name, boolean nameIsNumber) { 500 if (TextUtils.isEmpty(name)) { 501 mPrimaryName.setText(null); 502 } else { 503 mPrimaryName.setText(nameIsNumber 504 ? PhoneNumberUtilsCompat.createTtsSpannable(name) 505 : name); 506 507 // Set direction of the name field 508 int nameDirection = View.TEXT_DIRECTION_INHERIT; 509 if (nameIsNumber) { 510 nameDirection = View.TEXT_DIRECTION_LTR; 511 } 512 mPrimaryName.setTextDirection(nameDirection); 513 } 514 } 515 516 /** 517 * Sets the primary image for the contact photo. 518 * 519 * @param image The drawable to set. 520 * @param isVisible Whether the contact photo should be visible after being set. 521 */ 522 @Override 523 public void setPrimaryImage(Drawable image, boolean isVisible) { 524 if (image != null) { 525 setDrawableToImageViews(image); 526 showImageView(mPhotoLarge, isVisible); 527 } 528 } 529 530 @Override 531 public void setPrimaryPhoneNumber(String number) { 532 // Set the number 533 if (TextUtils.isEmpty(number)) { 534 mPhoneNumber.setText(null); 535 mPhoneNumber.setVisibility(View.GONE); 536 } else { 537 mPhoneNumber.setText(PhoneNumberUtilsCompat.createTtsSpannable(number)); 538 mPhoneNumber.setVisibility(View.VISIBLE); 539 mPhoneNumber.setTextDirection(View.TEXT_DIRECTION_LTR); 540 } 541 } 542 543 @Override 544 public void setPrimaryLabel(String label) { 545 if (!TextUtils.isEmpty(label)) { 546 mNumberLabel.setText(label); 547 mNumberLabel.setVisibility(View.VISIBLE); 548 } else { 549 mNumberLabel.setVisibility(View.GONE); 550 } 551 552 } 553 554 /** 555 * Sets the primary caller information. 556 * 557 * @param number The caller phone number. 558 * @param name The caller name. 559 * @param nameIsNumber {@code true} if the name should be shown in place of the phone number. 560 * @param label The label. 561 * @param photo The contact photo drawable. 562 * @param isSipCall {@code true} if this is a SIP call. 563 * @param isContactPhotoShown {@code true} if the contact photo should be shown (it will be 564 * updated even if it is not shown). 565 * @param isWorkCall Whether the call is placed through a work phone account or caller is a work 566 contact. 567 */ 568 @Override 569 public void setPrimary(String number, String name, boolean nameIsNumber, String label, 570 Drawable photo, boolean isSipCall, boolean isContactPhotoShown, boolean isWorkCall) { 571 Log.d(this, "Setting primary call"); 572 // set the name field. 573 setPrimaryName(name, nameIsNumber); 574 575 if (TextUtils.isEmpty(number) && TextUtils.isEmpty(label)) { 576 mCallNumberAndLabel.setVisibility(View.GONE); 577 mElapsedTime.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); 578 } else { 579 mCallNumberAndLabel.setVisibility(View.VISIBLE); 580 mElapsedTime.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_END); 581 } 582 583 setPrimaryPhoneNumber(number); 584 585 // Set the label (Mobile, Work, etc) 586 setPrimaryLabel(label); 587 588 showInternetCallLabel(isSipCall); 589 590 setDrawableToImageViews(photo); 591 showImageView(mPhotoLarge, isContactPhotoShown); 592 showImageView(mWorkProfileIcon, isWorkCall); 593 } 594 595 @Override 596 public void setSecondary(boolean show, String name, boolean nameIsNumber, String label, 597 String providerLabel, boolean isConference, boolean isVideoCall, boolean isFullscreen) { 598 599 if (show) { 600 mHasSecondaryCallInfo = true; 601 boolean hasProvider = !TextUtils.isEmpty(providerLabel); 602 initializeSecondaryCallInfo(hasProvider); 603 604 // Do not show the secondary caller info in fullscreen mode, but ensure it is populated 605 // in case fullscreen mode is exited in the future. 606 setSecondaryInfoVisible(!isFullscreen); 607 608 mSecondaryCallConferenceCallIcon.setVisibility(isConference ? View.VISIBLE : View.GONE); 609 mSecondaryCallVideoCallIcon.setVisibility(isVideoCall ? View.VISIBLE : View.GONE); 610 611 mSecondaryCallName.setText(nameIsNumber 612 ? PhoneNumberUtilsCompat.createTtsSpannable(name) 613 : name); 614 if (hasProvider) { 615 mSecondaryCallProviderLabel.setText(providerLabel); 616 } 617 618 int nameDirection = View.TEXT_DIRECTION_INHERIT; 619 if (nameIsNumber) { 620 nameDirection = View.TEXT_DIRECTION_LTR; 621 } 622 mSecondaryCallName.setTextDirection(nameDirection); 623 } else { 624 mHasSecondaryCallInfo = false; 625 setSecondaryInfoVisible(false); 626 } 627 } 628 629 /** 630 * Sets the visibility of the secondary caller info box. Note, if the {@code visible} parameter 631 * is passed in {@code true}, and there is no secondary caller info populated (as determined by 632 * {@code mHasSecondaryCallInfo}, the secondary caller info box will not be shown. 633 * 634 * @param visible {@code true} if the secondary caller info should be shown, {@code false} 635 * otherwise. 636 */ 637 @Override 638 public void setSecondaryInfoVisible(final boolean visible) { 639 boolean wasVisible = mSecondaryCallInfo.isShown(); 640 final boolean isVisible = visible && mHasSecondaryCallInfo; 641 Log.v(this, "setSecondaryInfoVisible: wasVisible = " + wasVisible + " isVisible = " 642 + isVisible); 643 644 // If visibility didn't change, nothing to do. 645 if (wasVisible == isVisible) { 646 return; 647 } 648 649 // If we are showing the secondary info, we need to show it before animating so that its 650 // height will be determined on layout. 651 if (isVisible) { 652 mSecondaryCallInfo.setVisibility(View.VISIBLE); 653 } else { 654 mSecondaryCallInfo.setVisibility(View.GONE); 655 } 656 657 updateFabPositionForSecondaryCallInfo(); 658 // We need to translate the secondary caller info, but we need to know its position after 659 // the layout has occurred so use a {@code ViewTreeObserver}. 660 final ViewTreeObserver observer = getView().getViewTreeObserver(); 661 662 observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 663 @Override 664 public boolean onPreDraw() { 665 // We don't want to continue getting called. 666 getView().getViewTreeObserver().removeOnPreDrawListener(this); 667 668 // Get the height of the secondary call info now, and then re-hide the view prior 669 // to doing the actual animation. 670 int secondaryHeight = mSecondaryCallInfo.getHeight(); 671 if (isVisible) { 672 mSecondaryCallInfo.setVisibility(View.GONE); 673 } else { 674 mSecondaryCallInfo.setVisibility(View.VISIBLE); 675 } 676 Log.v(this, "setSecondaryInfoVisible: secondaryHeight = " + secondaryHeight); 677 678 // Set the position of the secondary call info card to its starting location. 679 mSecondaryCallInfo.setTranslationY(visible ? secondaryHeight : 0); 680 681 // Animate the secondary card info slide up/down as it appears and disappears. 682 ViewPropertyAnimator secondaryInfoAnimator = mSecondaryCallInfo.animate() 683 .setInterpolator(AnimUtils.EASE_OUT_EASE_IN) 684 .setDuration(mVideoAnimationDuration) 685 .translationY(isVisible ? 0 : secondaryHeight) 686 .setListener(new AnimatorListenerAdapter() { 687 @Override 688 public void onAnimationEnd(Animator animation) { 689 if (!isVisible) { 690 mSecondaryCallInfo.setVisibility(View.GONE); 691 } 692 } 693 694 @Override 695 public void onAnimationStart(Animator animation) { 696 if (isVisible) { 697 mSecondaryCallInfo.setVisibility(View.VISIBLE); 698 } 699 } 700 }); 701 secondaryInfoAnimator.start(); 702 703 // Notify listeners of a change in the visibility of the secondary info. This is 704 // important when in a video call so that the video call presenter can shift the 705 // video preview up or down to accommodate the secondary caller info. 706 InCallPresenter.getInstance().notifySecondaryCallerInfoVisibilityChanged(visible, 707 secondaryHeight); 708 709 return true; 710 } 711 }); 712 } 713 714 @Override 715 public void setCallState( 716 int state, 717 int videoState, 718 int sessionModificationState, 719 DisconnectCause disconnectCause, 720 String connectionLabel, 721 Drawable callStateIcon, 722 String gatewayNumber, 723 boolean isWifi, 724 boolean isConference, 725 boolean isWorkCall) { 726 boolean isGatewayCall = !TextUtils.isEmpty(gatewayNumber); 727 CallStateLabel callStateLabel = getCallStateLabelFromState(state, videoState, 728 sessionModificationState, disconnectCause, connectionLabel, isGatewayCall, isWifi, 729 isConference, isWorkCall); 730 731 Log.v(this, "setCallState " + callStateLabel.getCallStateLabel()); 732 Log.v(this, "AutoDismiss " + callStateLabel.isAutoDismissing()); 733 Log.v(this, "DisconnectCause " + disconnectCause.toString()); 734 Log.v(this, "gateway " + connectionLabel + gatewayNumber); 735 736 // Check for video state change and update the visibility of the contact photo. The contact 737 // photo is hidden when the incoming video surface is shown. 738 // The contact photo visibility can also change in setPrimary(). 739 boolean showContactPhoto = !VideoCallPresenter.showIncomingVideo(videoState, state); 740 mPhotoLarge.setVisibility(showContactPhoto ? View.VISIBLE : View.GONE); 741 742 // Check if the call subject is showing -- if it is, we want to bypass showing the call 743 // state. 744 boolean isSubjectShowing = mCallSubject.getVisibility() == View.VISIBLE; 745 746 if (TextUtils.equals(callStateLabel.getCallStateLabel(), mCallStateLabel.getText()) && 747 !isSubjectShowing) { 748 // Nothing to do if the labels are the same 749 if (state == Call.State.ACTIVE || state == Call.State.CONFERENCED) { 750 mCallStateLabel.clearAnimation(); 751 mCallStateIcon.clearAnimation(); 752 } 753 return; 754 } 755 756 if (isSubjectShowing) { 757 changeCallStateLabel(null); 758 callStateIcon = null; 759 } else { 760 // Update the call state label and icon. 761 setCallStateLabel(callStateLabel); 762 } 763 764 if (!TextUtils.isEmpty(callStateLabel.getCallStateLabel())) { 765 if (state == Call.State.ACTIVE || state == Call.State.CONFERENCED) { 766 mCallStateLabel.clearAnimation(); 767 } else { 768 mCallStateLabel.startAnimation(mPulseAnimation); 769 } 770 } else { 771 mCallStateLabel.clearAnimation(); 772 } 773 774 if (callStateIcon != null) { 775 mCallStateIcon.setVisibility(View.VISIBLE); 776 // Invoke setAlpha(float) instead of setAlpha(int) to set the view's alpha. This is 777 // needed because the pulse animation operates on the view alpha. 778 mCallStateIcon.setAlpha(1.0f); 779 mCallStateIcon.setImageDrawable(callStateIcon); 780 781 if (state == Call.State.ACTIVE || state == Call.State.CONFERENCED 782 || TextUtils.isEmpty(callStateLabel.getCallStateLabel())) { 783 mCallStateIcon.clearAnimation(); 784 } else { 785 mCallStateIcon.startAnimation(mPulseAnimation); 786 } 787 788 if (callStateIcon instanceof AnimationDrawable) { 789 ((AnimationDrawable) callStateIcon).start(); 790 } 791 } else { 792 mCallStateIcon.clearAnimation(); 793 794 // Invoke setAlpha(float) instead of setAlpha(int) to set the view's alpha. This is 795 // needed because the pulse animation operates on the view alpha. 796 mCallStateIcon.setAlpha(0.0f); 797 mCallStateIcon.setVisibility(View.GONE); 798 } 799 800 if (VideoUtils.isVideoCall(videoState) 801 || (state == Call.State.ACTIVE && sessionModificationState 802 == Call.SessionModificationState.WAITING_FOR_RESPONSE)) { 803 mCallStateVideoCallIcon.setVisibility(View.VISIBLE); 804 } else { 805 mCallStateVideoCallIcon.setVisibility(View.GONE); 806 } 807 } 808 809 private void setCallStateLabel(CallStateLabel callStateLabel) { 810 Log.v(this, "setCallStateLabel : label = " + callStateLabel.getCallStateLabel()); 811 812 if (callStateLabel.isAutoDismissing()) { 813 mCallStateLabelResetPending = true; 814 mHandler.postDelayed(new Runnable() { 815 @Override 816 public void run() { 817 Log.v(this, "restoringCallStateLabel : label = " + 818 mPostResetCallStateLabel); 819 changeCallStateLabel(mPostResetCallStateLabel); 820 mCallStateLabelResetPending = false; 821 } 822 }, CALL_STATE_LABEL_RESET_DELAY_MS); 823 824 changeCallStateLabel(callStateLabel.getCallStateLabel()); 825 } else { 826 // Keep track of the current call state label; used when resetting auto dismissing 827 // call state labels. 828 mPostResetCallStateLabel = callStateLabel.getCallStateLabel(); 829 830 if (!mCallStateLabelResetPending) { 831 changeCallStateLabel(callStateLabel.getCallStateLabel()); 832 } 833 } 834 } 835 836 private void changeCallStateLabel(CharSequence callStateLabel) { 837 Log.v(this, "changeCallStateLabel : label = " + callStateLabel); 838 if (!TextUtils.isEmpty(callStateLabel)) { 839 mCallStateLabel.setText(callStateLabel); 840 mCallStateLabel.setAlpha(1); 841 mCallStateLabel.setVisibility(View.VISIBLE); 842 } else { 843 Animation callStateLabelAnimation = mCallStateLabel.getAnimation(); 844 if (callStateLabelAnimation != null) { 845 callStateLabelAnimation.cancel(); 846 } 847 mCallStateLabel.setText(null); 848 mCallStateLabel.setAlpha(0); 849 mCallStateLabel.setVisibility(View.GONE); 850 } 851 } 852 853 @Override 854 public void setCallbackNumber(String callbackNumber, boolean isEmergencyCall) { 855 if (mInCallMessageLabel == null) { 856 return; 857 } 858 859 if (TextUtils.isEmpty(callbackNumber)) { 860 mInCallMessageLabel.setVisibility(View.GONE); 861 return; 862 } 863 864 // TODO: The new Locale-specific methods don't seem to be working. Revisit this. 865 callbackNumber = PhoneNumberUtils.formatNumber(callbackNumber); 866 867 int stringResourceId = isEmergencyCall ? R.string.card_title_callback_number_emergency 868 : R.string.card_title_callback_number; 869 870 String text = getString(stringResourceId, callbackNumber); 871 mInCallMessageLabel.setText(text); 872 873 mInCallMessageLabel.setVisibility(View.VISIBLE); 874 } 875 876 /** 877 * Sets and shows the call subject if it is not empty. Hides the call subject otherwise. 878 * 879 * @param callSubject The call subject. 880 */ 881 @Override 882 public void setCallSubject(String callSubject) { 883 boolean showSubject = !TextUtils.isEmpty(callSubject); 884 885 mCallSubject.setVisibility(showSubject ? View.VISIBLE : View.GONE); 886 if (showSubject) { 887 mCallSubject.setText(callSubject); 888 } else { 889 mCallSubject.setText(null); 890 } 891 } 892 893 public boolean isAnimating() { 894 return mIsAnimating; 895 } 896 897 private void showInternetCallLabel(boolean show) { 898 if (show) { 899 final String label = getView().getContext().getString( 900 R.string.incall_call_type_label_sip); 901 mCallTypeLabel.setVisibility(View.VISIBLE); 902 mCallTypeLabel.setText(label); 903 } else { 904 mCallTypeLabel.setVisibility(View.GONE); 905 } 906 } 907 908 @Override 909 public void setPrimaryCallElapsedTime(boolean show, long duration) { 910 if (show) { 911 if (mElapsedTime.getVisibility() != View.VISIBLE) { 912 AnimUtils.fadeIn(mElapsedTime, AnimUtils.DEFAULT_DURATION); 913 } 914 String callTimeElapsed = DateUtils.formatElapsedTime(duration / 1000); 915 mElapsedTime.setText(callTimeElapsed); 916 917 String durationDescription = 918 InCallDateUtils.formatDuration(getView().getContext(), duration); 919 mElapsedTime.setContentDescription( 920 !TextUtils.isEmpty(durationDescription) ? durationDescription : null); 921 } else { 922 // hide() animation has no effect if it is already hidden. 923 AnimUtils.fadeOut(mElapsedTime, AnimUtils.DEFAULT_DURATION); 924 } 925 } 926 927 /** 928 * Set all the ImageViews to the same photo. Currently there are 2 photo views: the large one 929 * (which fills about the bottom half of the screen) and the small one, which displays as a 930 * circle next to the primary contact info. This method does not handle whether the ImageView 931 * is shown or not. 932 * 933 * @param photo The photo to set for the image views. 934 */ 935 private void setDrawableToImageViews(Drawable photo) { 936 if (photo == null) { 937 photo = ContactInfoCache.getInstance(getView().getContext()) 938 .getDefaultContactPhotoDrawable(); 939 } 940 941 if (mPrimaryPhotoDrawable == photo){ 942 return; 943 } 944 mPrimaryPhotoDrawable = photo; 945 946 mPhotoLarge.setImageDrawable(photo); 947 948 // Modify the drawable to be round for the smaller ImageView. 949 Bitmap bitmap = drawableToBitmap(photo); 950 if (bitmap != null) { 951 final RoundedBitmapDrawable drawable = 952 RoundedBitmapDrawableFactory.create(getResources(), bitmap); 953 drawable.setAntiAlias(true); 954 drawable.setCornerRadius(bitmap.getHeight() / 2); 955 photo = drawable; 956 } 957 mPhotoSmall.setImageDrawable(photo); 958 } 959 960 /** 961 * Helper method for image view to handle animations. 962 * 963 * @param view The image view to show or hide. 964 * @param isVisible {@code true} if we want to show the image, {@code false} to hide it. 965 */ 966 private void showImageView(ImageView view, boolean isVisible) { 967 if (view.getDrawable() == null) { 968 if (isVisible) { 969 AnimUtils.fadeIn(mElapsedTime, AnimUtils.DEFAULT_DURATION); 970 } 971 } else { 972 // Cross fading is buggy and not noticeable due to the multiple calls to this method 973 // that switch drawables in the middle of the cross-fade animations. Just show the 974 // photo directly instead. 975 view.setVisibility(isVisible ? View.VISIBLE : View.GONE); 976 } 977 } 978 979 /** 980 * Converts a drawable into a bitmap. 981 * 982 * @param drawable the drawable to be converted. 983 */ 984 public static Bitmap drawableToBitmap(Drawable drawable) { 985 Bitmap bitmap; 986 if (drawable instanceof BitmapDrawable) { 987 bitmap = ((BitmapDrawable) drawable).getBitmap(); 988 } else { 989 if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) { 990 // Needed for drawables that are just a colour. 991 bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); 992 } else { 993 bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), 994 drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); 995 } 996 997 Log.i(TAG, "Created bitmap with width " + bitmap.getWidth() + ", height " 998 + bitmap.getHeight()); 999 1000 Canvas canvas = new Canvas(bitmap); 1001 drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); 1002 drawable.draw(canvas); 1003 } 1004 return bitmap; 1005 } 1006 1007 /** 1008 * Gets the call state label based on the state of the call or cause of disconnect. 1009 * 1010 * Additional labels are applied as follows: 1011 * 1. All outgoing calls with display "Calling via [Provider]". 1012 * 2. Ongoing calls will display the name of the provider. 1013 * 3. Incoming calls will only display "Incoming via..." for accounts. 1014 * 4. Video calls, and session modification states (eg. requesting video). 1015 * 5. Incoming and active Wi-Fi calls will show label provided by hint. 1016 * 1017 * TODO: Move this to the CallCardPresenter. 1018 */ 1019 private CallStateLabel getCallStateLabelFromState(int state, int videoState, 1020 int sessionModificationState, DisconnectCause disconnectCause, String label, 1021 boolean isGatewayCall, boolean isWifi, boolean isConference, boolean isWorkCall) { 1022 final Context context = getView().getContext(); 1023 CharSequence callStateLabel = null; // Label to display as part of the call banner 1024 1025 boolean hasSuggestedLabel = label != null; 1026 boolean isAccount = hasSuggestedLabel && !isGatewayCall; 1027 boolean isAutoDismissing = false; 1028 1029 switch (state) { 1030 case Call.State.IDLE: 1031 // "Call state" is meaningless in this state. 1032 break; 1033 case Call.State.ACTIVE: 1034 // We normally don't show a "call state label" at all in this state 1035 // (but we can use the call state label to display the provider name). 1036 if ((isAccount || isWifi || isConference) && hasSuggestedLabel) { 1037 callStateLabel = label; 1038 } else if (sessionModificationState 1039 == Call.SessionModificationState.REQUEST_REJECTED) { 1040 callStateLabel = context.getString(R.string.card_title_video_call_rejected); 1041 isAutoDismissing = true; 1042 } else if (sessionModificationState 1043 == Call.SessionModificationState.REQUEST_FAILED) { 1044 callStateLabel = context.getString(R.string.card_title_video_call_error); 1045 isAutoDismissing = true; 1046 } else if (sessionModificationState 1047 == Call.SessionModificationState.WAITING_FOR_RESPONSE) { 1048 callStateLabel = context.getString(R.string.card_title_video_call_requesting); 1049 } else if (sessionModificationState 1050 == Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) { 1051 callStateLabel = context.getString(R.string.card_title_video_call_requesting); 1052 } else if (VideoUtils.isVideoCall(videoState)) { 1053 callStateLabel = context.getString(R.string.card_title_video_call); 1054 } 1055 break; 1056 case Call.State.ONHOLD: 1057 callStateLabel = context.getString(R.string.card_title_on_hold); 1058 break; 1059 case Call.State.CONNECTING: 1060 case Call.State.DIALING: 1061 if (hasSuggestedLabel && !isWifi) { 1062 callStateLabel = context.getString(R.string.calling_via_template, label); 1063 } else { 1064 callStateLabel = context.getString(R.string.card_title_dialing); 1065 } 1066 break; 1067 case Call.State.REDIALING: 1068 callStateLabel = context.getString(R.string.card_title_redialing); 1069 break; 1070 case Call.State.INCOMING: 1071 case Call.State.CALL_WAITING: 1072 if (isWifi && hasSuggestedLabel) { 1073 callStateLabel = label; 1074 } else if (isAccount) { 1075 callStateLabel = context.getString(R.string.incoming_via_template, label); 1076 } else if (VideoUtils.isVideoCall(videoState)) { 1077 callStateLabel = context.getString(R.string.notification_incoming_video_call); 1078 } else { 1079 callStateLabel = 1080 context.getString(isWorkCall ? R.string.card_title_incoming_work_call 1081 : R.string.card_title_incoming_call); 1082 } 1083 break; 1084 case Call.State.DISCONNECTING: 1085 // While in the DISCONNECTING state we display a "Hanging up" 1086 // message in order to make the UI feel more responsive. (In 1087 // GSM it's normal to see a delay of a couple of seconds while 1088 // negotiating the disconnect with the network, so the "Hanging 1089 // up" state at least lets the user know that we're doing 1090 // something. This state is currently not used with CDMA.) 1091 callStateLabel = context.getString(R.string.card_title_hanging_up); 1092 break; 1093 case Call.State.DISCONNECTED: 1094 callStateLabel = disconnectCause.getLabel(); 1095 if (TextUtils.isEmpty(callStateLabel)) { 1096 callStateLabel = context.getString(R.string.card_title_call_ended); 1097 } 1098 break; 1099 case Call.State.CONFERENCED: 1100 callStateLabel = context.getString(R.string.card_title_conf_call); 1101 break; 1102 default: 1103 Log.wtf(this, "updateCallStateWidgets: unexpected call: " + state); 1104 } 1105 return new CallStateLabel(callStateLabel, isAutoDismissing); 1106 } 1107 1108 private void initializeSecondaryCallInfo(boolean hasProvider) { 1109 // mSecondaryCallName is initialized here (vs. onViewCreated) because it is inaccessible 1110 // until mSecondaryCallInfo is inflated in the call above. 1111 if (mSecondaryCallName == null) { 1112 mSecondaryCallName = (TextView) getView().findViewById(R.id.secondaryCallName); 1113 mSecondaryCallConferenceCallIcon = 1114 getView().findViewById(R.id.secondaryCallConferenceCallIcon); 1115 mSecondaryCallVideoCallIcon = 1116 getView().findViewById(R.id.secondaryCallVideoCallIcon); 1117 } 1118 1119 if (mSecondaryCallProviderLabel == null && hasProvider) { 1120 mSecondaryCallProviderInfo.setVisibility(View.VISIBLE); 1121 mSecondaryCallProviderLabel = (TextView) getView() 1122 .findViewById(R.id.secondaryCallProviderLabel); 1123 } 1124 } 1125 1126 public void dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 1127 if (event.getEventType() == AccessibilityEvent.TYPE_ANNOUNCEMENT) { 1128 // Indicate this call is in active if no label is provided. The label is empty when 1129 // the call is in active, not in other status such as onhold or dialing etc. 1130 if (!mCallStateLabel.isShown() || TextUtils.isEmpty(mCallStateLabel.getText())) { 1131 event.getText().add( 1132 TextUtils.expandTemplate( 1133 getResources().getText(R.string.accessibility_call_is_active), 1134 mPrimaryName.getText())); 1135 } else { 1136 dispatchPopulateAccessibilityEvent(event, mCallStateLabel); 1137 dispatchPopulateAccessibilityEvent(event, mPrimaryName); 1138 dispatchPopulateAccessibilityEvent(event, mCallTypeLabel); 1139 dispatchPopulateAccessibilityEvent(event, mPhoneNumber); 1140 } 1141 return; 1142 } 1143 dispatchPopulateAccessibilityEvent(event, mCallStateLabel); 1144 dispatchPopulateAccessibilityEvent(event, mPrimaryName); 1145 dispatchPopulateAccessibilityEvent(event, mPhoneNumber); 1146 dispatchPopulateAccessibilityEvent(event, mCallTypeLabel); 1147 dispatchPopulateAccessibilityEvent(event, mSecondaryCallName); 1148 dispatchPopulateAccessibilityEvent(event, mSecondaryCallProviderLabel); 1149 1150 return; 1151 } 1152 1153 @Override 1154 public void sendAccessibilityAnnouncement() { 1155 mHandler.postDelayed(new Runnable() { 1156 @Override 1157 public void run() { 1158 if (getView() != null && getView().getParent() != null && 1159 isAccessibilityEnabled(getContext())) { 1160 AccessibilityEvent event = AccessibilityEvent.obtain( 1161 AccessibilityEvent.TYPE_ANNOUNCEMENT); 1162 dispatchPopulateAccessibilityEvent(event); 1163 getView().getParent().requestSendAccessibilityEvent(getView(), event); 1164 } 1165 } 1166 1167 private boolean isAccessibilityEnabled(Context context) { 1168 AccessibilityManager accessibilityManager = 1169 (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); 1170 return accessibilityManager != null && accessibilityManager.isEnabled(); 1171 1172 } 1173 }, ACCESSIBILITY_ANNOUNCEMENT_DELAY_MS); 1174 } 1175 1176 @Override 1177 public void setEndCallButtonEnabled(boolean enabled, boolean animate) { 1178 if (enabled != mFloatingActionButton.isEnabled()) { 1179 if (animate) { 1180 if (enabled) { 1181 mFloatingActionButtonController.scaleIn(AnimUtils.NO_DELAY); 1182 } else { 1183 mFloatingActionButtonController.scaleOut(); 1184 } 1185 } else { 1186 if (enabled) { 1187 mFloatingActionButtonContainer.setScaleX(1); 1188 mFloatingActionButtonContainer.setScaleY(1); 1189 mFloatingActionButtonContainer.setVisibility(View.VISIBLE); 1190 } else { 1191 mFloatingActionButtonContainer.setVisibility(View.GONE); 1192 } 1193 } 1194 mFloatingActionButton.setEnabled(enabled); 1195 updateFabPosition(); 1196 } 1197 } 1198 1199 /** 1200 * Changes the visibility of the HD audio icon. 1201 * 1202 * @param visible {@code true} if the UI should show the HD audio icon. 1203 */ 1204 @Override 1205 public void showHdAudioIndicator(boolean visible) { 1206 mHdAudioIcon.setVisibility(visible ? View.VISIBLE : View.GONE); 1207 } 1208 1209 /** 1210 * Changes the visibility of the forward icon. 1211 * 1212 * @param visible {@code true} if the UI should show the forward icon. 1213 */ 1214 @Override 1215 public void showForwardIndicator(boolean visible) { 1216 mForwardIcon.setVisibility(visible ? View.VISIBLE : View.GONE); 1217 } 1218 1219 1220 /** 1221 * Changes the visibility of the "manage conference call" button. 1222 * 1223 * @param visible Whether to set the button to be visible or not. 1224 */ 1225 @Override 1226 public void showManageConferenceCallButton(boolean visible) { 1227 mManageConferenceCallButton.setVisibility(visible ? View.VISIBLE : View.GONE); 1228 } 1229 1230 /** 1231 * Determines the current visibility of the manage conference button. 1232 * 1233 * @return {@code true} if the button is visible. 1234 */ 1235 @Override 1236 public boolean isManageConferenceVisible() { 1237 return mManageConferenceCallButton.getVisibility() == View.VISIBLE; 1238 } 1239 1240 /** 1241 * Determines the current visibility of the call subject. 1242 * 1243 * @return {@code true} if the subject is visible. 1244 */ 1245 @Override 1246 public boolean isCallSubjectVisible() { 1247 return mCallSubject.getVisibility() == View.VISIBLE; 1248 } 1249 1250 /** 1251 * Get the overall InCallUI background colors and apply to call card. 1252 */ 1253 public void updateColors() { 1254 MaterialPalette themeColors = InCallPresenter.getInstance().getThemeColors(); 1255 1256 if (mCurrentThemeColors != null && mCurrentThemeColors.equals(themeColors)) { 1257 return; 1258 } 1259 1260 if (getResources().getBoolean(R.bool.is_layout_landscape)) { 1261 final GradientDrawable drawable = 1262 (GradientDrawable) mPrimaryCallCardContainer.getBackground(); 1263 drawable.setColor(themeColors.mPrimaryColor); 1264 } else { 1265 mPrimaryCallCardContainer.setBackgroundColor(themeColors.mPrimaryColor); 1266 } 1267 mCallButtonsContainer.setBackgroundColor(themeColors.mPrimaryColor); 1268 mCallSubject.setTextColor(themeColors.mPrimaryColor); 1269 mContactContext.setBackgroundColor(themeColors.mPrimaryColor); 1270 //TODO: set color of message text in call context "recent messages" to be the theme color. 1271 1272 mCurrentThemeColors = themeColors; 1273 } 1274 1275 private void dispatchPopulateAccessibilityEvent(AccessibilityEvent event, View view) { 1276 if (view == null) return; 1277 final List<CharSequence> eventText = event.getText(); 1278 int size = eventText.size(); 1279 view.dispatchPopulateAccessibilityEvent(event); 1280 // if no text added write null to keep relative position 1281 if (size == eventText.size()) { 1282 eventText.add(null); 1283 } 1284 } 1285 1286 @Override 1287 public void animateForNewOutgoingCall() { 1288 final ViewGroup parent = (ViewGroup) mPrimaryCallCardContainer.getParent(); 1289 1290 final ViewTreeObserver observer = getView().getViewTreeObserver(); 1291 1292 mIsAnimating = true; 1293 1294 observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() { 1295 @Override 1296 public void onGlobalLayout() { 1297 final ViewTreeObserver observer = getView().getViewTreeObserver(); 1298 if (!observer.isAlive()) { 1299 return; 1300 } 1301 observer.removeOnGlobalLayoutListener(this); 1302 1303 final LayoutIgnoringListener listener = new LayoutIgnoringListener(); 1304 mPrimaryCallCardContainer.addOnLayoutChangeListener(listener); 1305 1306 // Prepare the state of views before the slide animation 1307 final int originalHeight = mPrimaryCallCardContainer.getHeight(); 1308 mPrimaryCallCardContainer.setTag(R.id.view_tag_callcard_actual_height, 1309 originalHeight); 1310 mPrimaryCallCardContainer.setBottom(parent.getHeight()); 1311 1312 // Set up FAB. 1313 mFloatingActionButtonContainer.setVisibility(View.GONE); 1314 mFloatingActionButtonController.setScreenWidth(parent.getWidth()); 1315 1316 mCallButtonsContainer.setAlpha(0); 1317 mCallStateLabel.setAlpha(0); 1318 mPrimaryName.setAlpha(0); 1319 mCallTypeLabel.setAlpha(0); 1320 mCallNumberAndLabel.setAlpha(0); 1321 1322 assignTranslateAnimation(mCallStateLabel, 1); 1323 assignTranslateAnimation(mCallStateIcon, 1); 1324 assignTranslateAnimation(mPrimaryName, 2); 1325 assignTranslateAnimation(mCallNumberAndLabel, 3); 1326 assignTranslateAnimation(mCallTypeLabel, 4); 1327 assignTranslateAnimation(mCallButtonsContainer, 5); 1328 1329 final Animator animator = getShrinkAnimator(parent.getHeight(), originalHeight); 1330 1331 animator.addListener(new AnimatorListenerAdapter() { 1332 @Override 1333 public void onAnimationEnd(Animator animation) { 1334 mPrimaryCallCardContainer.setTag(R.id.view_tag_callcard_actual_height, 1335 null); 1336 setViewStatePostAnimation(listener); 1337 mIsAnimating = false; 1338 InCallPresenter.getInstance().onShrinkAnimationComplete(); 1339 } 1340 }); 1341 animator.start(); 1342 } 1343 }); 1344 } 1345 1346 @Override 1347 public void showNoteSentToast() { 1348 Toast.makeText(getContext(), R.string.note_sent, Toast.LENGTH_LONG).show(); 1349 } 1350 1351 public void onDialpadVisibilityChange(boolean isShown) { 1352 mIsDialpadShowing = isShown; 1353 updateFabPosition(); 1354 } 1355 1356 private void updateFabPosition() { 1357 int offsetY = 0; 1358 if (!mIsDialpadShowing) { 1359 offsetY = mFloatingActionButtonVerticalOffset; 1360 if (mSecondaryCallInfo.isShown() && mHasLargePhoto) { 1361 offsetY -= mSecondaryCallInfo.getHeight(); 1362 } 1363 } 1364 1365 mFloatingActionButtonController.align( 1366 FloatingActionButtonController.ALIGN_MIDDLE, 1367 0 /* offsetX */, 1368 offsetY, 1369 true); 1370 1371 mFloatingActionButtonController.resize( 1372 mIsDialpadShowing ? mFabSmallDiameter : mFabNormalDiameter, true); 1373 } 1374 1375 @Override 1376 public Context getContext() { 1377 return getActivity(); 1378 } 1379 1380 @Override 1381 public void onResume() { 1382 super.onResume(); 1383 // If the previous launch animation is still running, cancel it so that we don't get 1384 // stuck in an intermediate animation state. 1385 if (mAnimatorSet != null && mAnimatorSet.isRunning()) { 1386 mAnimatorSet.cancel(); 1387 } 1388 1389 mIsLandscape = getResources().getBoolean(R.bool.is_layout_landscape); 1390 mHasLargePhoto = getResources().getBoolean(R.bool.has_large_photo); 1391 1392 final ViewGroup parent = ((ViewGroup) mPrimaryCallCardContainer.getParent()); 1393 final ViewTreeObserver observer = parent.getViewTreeObserver(); 1394 parent.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() { 1395 @Override 1396 public void onGlobalLayout() { 1397 ViewTreeObserver viewTreeObserver = observer; 1398 if (!viewTreeObserver.isAlive()) { 1399 viewTreeObserver = parent.getViewTreeObserver(); 1400 } 1401 viewTreeObserver.removeOnGlobalLayoutListener(this); 1402 mFloatingActionButtonController.setScreenWidth(parent.getWidth()); 1403 updateFabPosition(); 1404 } 1405 }); 1406 1407 updateColors(); 1408 } 1409 1410 /** 1411 * Adds a global layout listener to update the FAB's positioning on the next layout. This allows 1412 * us to position the FAB after the secondary call info's height has been calculated. 1413 */ 1414 private void updateFabPositionForSecondaryCallInfo() { 1415 mSecondaryCallInfo.getViewTreeObserver().addOnGlobalLayoutListener( 1416 new ViewTreeObserver.OnGlobalLayoutListener() { 1417 @Override 1418 public void onGlobalLayout() { 1419 final ViewTreeObserver observer = mSecondaryCallInfo.getViewTreeObserver(); 1420 if (!observer.isAlive()) { 1421 return; 1422 } 1423 observer.removeOnGlobalLayoutListener(this); 1424 1425 onDialpadVisibilityChange(mIsDialpadShowing); 1426 } 1427 }); 1428 } 1429 1430 /** 1431 * Animator that performs the upwards shrinking animation of the blue call card scrim. 1432 * At the start of the animation, each child view is moved downwards by a pre-specified amount 1433 * and then translated upwards together with the scrim. 1434 */ 1435 private Animator getShrinkAnimator(int startHeight, int endHeight) { 1436 final ObjectAnimator shrinkAnimator = 1437 ObjectAnimator.ofInt(mPrimaryCallCardContainer, "bottom", startHeight, endHeight); 1438 shrinkAnimator.setDuration(mShrinkAnimationDuration); 1439 shrinkAnimator.addListener(new AnimatorListenerAdapter() { 1440 @Override 1441 public void onAnimationStart(Animator animation) { 1442 mFloatingActionButton.setEnabled(true); 1443 } 1444 }); 1445 shrinkAnimator.setInterpolator(AnimUtils.EASE_IN); 1446 return shrinkAnimator; 1447 } 1448 1449 private void assignTranslateAnimation(View view, int offset) { 1450 view.setLayerType(View.LAYER_TYPE_HARDWARE, null); 1451 view.buildLayer(); 1452 view.setTranslationY(mTranslationOffset * offset); 1453 view.animate().translationY(0).alpha(1).withLayer() 1454 .setDuration(mShrinkAnimationDuration).setInterpolator(AnimUtils.EASE_IN); 1455 } 1456 1457 private void setViewStatePostAnimation(View view) { 1458 view.setTranslationY(0); 1459 view.setAlpha(1); 1460 } 1461 1462 private void setViewStatePostAnimation(OnLayoutChangeListener layoutChangeListener) { 1463 setViewStatePostAnimation(mCallButtonsContainer); 1464 setViewStatePostAnimation(mCallStateLabel); 1465 setViewStatePostAnimation(mPrimaryName); 1466 setViewStatePostAnimation(mCallTypeLabel); 1467 setViewStatePostAnimation(mCallNumberAndLabel); 1468 setViewStatePostAnimation(mCallStateIcon); 1469 1470 mPrimaryCallCardContainer.removeOnLayoutChangeListener(layoutChangeListener); 1471 1472 mFloatingActionButtonController.scaleIn(AnimUtils.NO_DELAY); 1473 } 1474 1475 private final class LayoutIgnoringListener implements View.OnLayoutChangeListener { 1476 @Override 1477 public void onLayoutChange(View v, 1478 int left, 1479 int top, 1480 int right, 1481 int bottom, 1482 int oldLeft, 1483 int oldTop, 1484 int oldRight, 1485 int oldBottom) { 1486 v.setLeft(oldLeft); 1487 v.setRight(oldRight); 1488 v.setTop(oldTop); 1489 v.setBottom(oldBottom); 1490 } 1491 } 1492 } 1493