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