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