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