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