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