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