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