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