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 
CallStateLabel(CharSequence callStateLabel, boolean isAutoDismissing)81         public CallStateLabel(CharSequence callStateLabel, boolean isAutoDismissing) {
82             mCallStateLabel = callStateLabel;
83             mIsAutoDismissing = isAutoDismissing;
84         }
85 
getCallStateLabel()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          */
isAutoDismissing()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
getUi()196     public CallCardPresenter.CallCardUi getUi() {
197         return this;
198     }
199 
200     @Override
createPresenter()201     public CallCardPresenter createPresenter() {
202         return new CallCardPresenter();
203     }
204 
205     @Override
onCreate(Bundle savedInstanceState)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
onActivityCreated(Bundle savedInstanceState)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
onSaveInstanceState(Bundle outState)234     public void onSaveInstanceState(Bundle outState) {
235         outState.putBoolean(IS_DIALPAD_SHOWING_KEY, mIsDialpadShowing);
236         super.onSaveInstanceState(outState);
237     }
238 
239     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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
onViewCreated(View view, Bundle savedInstanceState)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
setVisible(boolean on)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
setProgressSpinnerVisible(boolean visible)354     public void setProgressSpinnerVisible(boolean visible) {
355         mProgressSpinner.setVisibility(visible ? View.VISIBLE : View.GONE);
356     }
357 
358     @Override
setContactContextTitle(View headerView)359     public void setContactContextTitle(View headerView) {
360         mContactContextListHeaders.removeAllViews();
361         mContactContextListHeaders.addView(headerView);
362     }
363 
364     @Override
setContactContextContent(ListAdapter listAdapter)365     public void setContactContextContent(ListAdapter listAdapter) {
366         mContactContextListView.setAdapter(listAdapter);
367     }
368 
369     @Override
showContactContext(boolean show)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
setCallCardVisible(final boolean visible)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      */
getSpaceBesideCallCard()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
setPrimaryName(String name, boolean nameIsNumber)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
setPrimaryImage(Drawable image, boolean isVisible)523     public void setPrimaryImage(Drawable image, boolean isVisible) {
524         if (image != null) {
525             setDrawableToImageViews(image);
526             showImageView(mPhotoLarge, isVisible);
527         }
528     }
529 
530     @Override
setPrimaryPhoneNumber(String number)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
setPrimaryLabel(String label)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
setPrimary(String number, String name, boolean nameIsNumber, String label, Drawable photo, boolean isSipCall, boolean isContactPhotoShown, boolean isWorkCall)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
setSecondary(boolean show, String name, boolean nameIsNumber, String label, String providerLabel, boolean isConference, boolean isVideoCall, boolean isFullscreen)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
setSecondaryInfoVisible(final boolean visible)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
setCallState( int state, int videoState, int sessionModificationState, DisconnectCause disconnectCause, String connectionLabel, Drawable callStateIcon, String gatewayNumber, boolean isWifi, boolean isConference, boolean isWorkCall)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 
setCallStateLabel(CallStateLabel callStateLabel)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 
changeCallStateLabel(CharSequence callStateLabel)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
setCallbackNumber(String callbackNumber, boolean isEmergencyCall)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
setCallSubject(String callSubject)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 
isAnimating()893     public boolean isAnimating() {
894         return mIsAnimating;
895     }
896 
showInternetCallLabel(boolean show)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
setPrimaryCallElapsedTime(boolean show, long duration)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      */
setDrawableToImageViews(Drawable photo)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      */
showImageView(ImageView view, boolean isVisible)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      */
drawableToBitmap(Drawable drawable)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      */
getCallStateLabelFromState(int state, int videoState, int sessionModificationState, DisconnectCause disconnectCause, String label, boolean isGatewayCall, boolean isWifi, boolean isConference, boolean isWorkCall)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 
initializeSecondaryCallInfo(boolean hasProvider)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 
dispatchPopulateAccessibilityEvent(AccessibilityEvent event)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
sendAccessibilityAnnouncement()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
setEndCallButtonEnabled(boolean enabled, boolean animate)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
showHdAudioIndicator(boolean visible)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
showForwardIndicator(boolean visible)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
showManageConferenceCallButton(boolean visible)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
isManageConferenceVisible()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
isCallSubjectVisible()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      */
updateColors()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 
dispatchPopulateAccessibilityEvent(AccessibilityEvent event, View view)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
animateForNewOutgoingCall()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
showNoteSentToast()1347     public void showNoteSentToast() {
1348         Toast.makeText(getContext(), R.string.note_sent, Toast.LENGTH_LONG).show();
1349     }
1350 
onDialpadVisibilityChange(boolean isShown)1351     public void onDialpadVisibilityChange(boolean isShown) {
1352         mIsDialpadShowing = isShown;
1353         updateFabPosition();
1354     }
1355 
updateFabPosition()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
getContext()1376     public Context getContext() {
1377         return getActivity();
1378     }
1379 
1380     @Override
onResume()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      */
updateFabPositionForSecondaryCallInfo()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      */
getShrinkAnimator(int startHeight, int endHeight)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 
assignTranslateAnimation(View view, int offset)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 
setViewStatePostAnimation(View view)1457     private void setViewStatePostAnimation(View view) {
1458         view.setTranslationY(0);
1459         view.setAlpha(1);
1460     }
1461 
setViewStatePostAnimation(OnLayoutChangeListener layoutChangeListener)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
onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom)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