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