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