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