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