blob: c2022d18c3751451b28f2c1e64cd4d7c613b19d7 [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.incallui;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.AnimationDrawable;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Trace;
import android.support.v4.graphics.drawable.RoundedBitmapDrawable;
import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory;
import android.telecom.DisconnectCause;
import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnLayoutChangeListener;
import android.view.ViewGroup;
import android.view.ViewPropertyAnimator;
import android.view.ViewTreeObserver;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette;
import com.android.contacts.common.widget.FloatingActionButtonController;
import com.android.dialer.R;
import com.android.phone.common.animation.AnimUtils;
import java.util.List;
/**
* Fragment for call card.
*/
public class CallCardFragment extends BaseFragment<CallCardPresenter, CallCardPresenter.CallCardUi>
implements CallCardPresenter.CallCardUi {
private static final String TAG = "CallCardFragment";
/**
* Internal class which represents the call state label which is to be applied.
*/
private class CallStateLabel {
private CharSequence mCallStateLabel;
private boolean mIsAutoDismissing;
public CallStateLabel(CharSequence callStateLabel, boolean isAutoDismissing) {
mCallStateLabel = callStateLabel;
mIsAutoDismissing = isAutoDismissing;
}
public CharSequence getCallStateLabel() {
return mCallStateLabel;
}
/**
* Determines if the call state label should auto-dismiss.
*
* @return {@code true} if the call state label should auto-dismiss.
*/
public boolean isAutoDismissing() {
return mIsAutoDismissing;
}
};
private static final String IS_DIALPAD_SHOWING_KEY = "is_dialpad_showing";
/**
* The duration of time (in milliseconds) a call state label should remain visible before
* resetting to its previous value.
*/
private static final long CALL_STATE_LABEL_RESET_DELAY_MS = 3000;
/**
* Amount of time to wait before sending an announcement via the accessibility manager.
* When the call state changes to an outgoing or incoming state for the first time, the
* UI can often be changing due to call updates or contact lookup. This allows the UI
* to settle to a stable state to ensure that the correct information is announced.
*/
private static final long ACCESSIBILITY_ANNOUNCEMENT_DELAY_MS = 500;
private AnimatorSet mAnimatorSet;
private int mShrinkAnimationDuration;
private int mFabNormalDiameter;
private int mFabSmallDiameter;
private boolean mIsLandscape;
private boolean mHasLargePhoto;
private boolean mIsDialpadShowing;
// Primary caller info
private TextView mPhoneNumber;
private TextView mNumberLabel;
private TextView mPrimaryName;
private View mCallStateButton;
private ImageView mCallStateIcon;
private ImageView mCallStateVideoCallIcon;
private TextView mCallStateLabel;
private TextView mCallTypeLabel;
private ImageView mHdAudioIcon;
private ImageView mForwardIcon;
private ImageView mSpamIcon;
private View mCallNumberAndLabel;
private TextView mElapsedTime;
private Drawable mPrimaryPhotoDrawable;
private TextView mCallSubject;
private ImageView mWorkProfileIcon;
// Container view that houses the entire primary call card, including the call buttons
private View mPrimaryCallCardContainer;
// Container view that houses the primary call information
private ViewGroup mPrimaryCallInfo;
private View mCallButtonsContainer;
private ImageView mPhotoSmall;
// Secondary caller info
private View mSecondaryCallInfo;
private TextView mSecondaryCallName;
private View mSecondaryCallProviderInfo;
private TextView mSecondaryCallProviderLabel;
private View mSecondaryCallConferenceCallIcon;
private View mSecondaryCallVideoCallIcon;
private View mProgressSpinner;
// Call card content
private View mCallCardContent;
private ImageView mPhotoLarge;
private View mContactContext;
private TextView mContactContextTitle;
private ListView mContactContextListView;
private LinearLayout mContactContextListHeaders;
private View mManageConferenceCallButton;
// Dark number info bar
private TextView mInCallMessageLabel;
private FloatingActionButtonController mFloatingActionButtonController;
private View mFloatingActionButtonContainer;
private ImageButton mFloatingActionButton;
private int mFloatingActionButtonVerticalOffset;
private float mTranslationOffset;
private Animation mPulseAnimation;
private int mVideoAnimationDuration;
// Whether or not the call card is currently in the process of an animation
private boolean mIsAnimating;
private MaterialPalette mCurrentThemeColors;
/**
* Call state label to set when an auto-dismissing call state label is dismissed.
*/
private CharSequence mPostResetCallStateLabel;
private boolean mCallStateLabelResetPending = false;
private Handler mHandler;
/**
* Determines if secondary call info is populated in the secondary call info UI.
*/
private boolean mHasSecondaryCallInfo = false;
@Override
public CallCardPresenter.CallCardUi getUi() {
return this;
}
@Override
public CallCardPresenter createPresenter() {
return new CallCardPresenter();
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mHandler = new Handler(Looper.getMainLooper());
mShrinkAnimationDuration = getResources().getInteger(R.integer.shrink_animation_duration);
mVideoAnimationDuration = getResources().getInteger(R.integer.video_animation_duration);
mFloatingActionButtonVerticalOffset = getResources().getDimensionPixelOffset(
R.dimen.floating_action_button_vertical_offset);
mFabNormalDiameter = getResources().getDimensionPixelOffset(
R.dimen.end_call_floating_action_button_diameter);
mFabSmallDiameter = getResources().getDimensionPixelOffset(
R.dimen.end_call_floating_action_button_small_diameter);
if (savedInstanceState != null) {
mIsDialpadShowing = savedInstanceState.getBoolean(IS_DIALPAD_SHOWING_KEY, false);
}
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
final CallList calls = CallList.getInstance();
final Call call = calls.getFirstCall();
getPresenter().init(getActivity(), call);
}
@Override
public void onSaveInstanceState(Bundle outState) {
outState.putBoolean(IS_DIALPAD_SHOWING_KEY, mIsDialpadShowing);
super.onSaveInstanceState(outState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Trace.beginSection(TAG + " onCreate");
mTranslationOffset =
getResources().getDimensionPixelSize(R.dimen.call_card_anim_translate_y_offset);
final View view = inflater.inflate(R.layout.call_card_fragment, container, false);
Trace.endSection();
return view;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mPulseAnimation =
AnimationUtils.loadAnimation(view.getContext(), R.anim.call_status_pulse);
mPhoneNumber = (TextView) view.findViewById(R.id.phoneNumber);
mPrimaryName = (TextView) view.findViewById(R.id.name);
mNumberLabel = (TextView) view.findViewById(R.id.label);
mSecondaryCallInfo = view.findViewById(R.id.secondary_call_info);
mSecondaryCallProviderInfo = view.findViewById(R.id.secondary_call_provider_info);
mCallCardContent = view.findViewById(R.id.call_card_content);
mPhotoLarge = (ImageView) view.findViewById(R.id.photoLarge);
mPhotoLarge.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
getPresenter().onContactPhotoClick();
}
});
mContactContext = view.findViewById(R.id.contact_context);
mContactContextTitle = (TextView) view.findViewById(R.id.contactContextTitle);
mContactContextListView = (ListView) view.findViewById(R.id.contactContextInfo);
// This layout stores all the list header layouts so they can be easily removed.
mContactContextListHeaders = new LinearLayout(getView().getContext());
mContactContextListView.addHeaderView(mContactContextListHeaders);
mCallStateIcon = (ImageView) view.findViewById(R.id.callStateIcon);
mCallStateVideoCallIcon = (ImageView) view.findViewById(R.id.videoCallIcon);
mWorkProfileIcon = (ImageView) view.findViewById(R.id.workProfileIcon);
mCallStateLabel = (TextView) view.findViewById(R.id.callStateLabel);
mHdAudioIcon = (ImageView) view.findViewById(R.id.hdAudioIcon);
mForwardIcon = (ImageView) view.findViewById(R.id.forwardIcon);
mSpamIcon = (ImageView) view.findViewById(R.id.spamIcon);
mCallNumberAndLabel = view.findViewById(R.id.labelAndNumber);
mCallTypeLabel = (TextView) view.findViewById(R.id.callTypeLabel);
mElapsedTime = (TextView) view.findViewById(R.id.elapsedTime);
mPrimaryCallCardContainer = view.findViewById(R.id.primary_call_info_container);
mPrimaryCallInfo = (ViewGroup) view.findViewById(R.id.primary_call_banner);
mCallButtonsContainer = view.findViewById(R.id.callButtonFragment);
mPhotoSmall = (ImageView) view.findViewById(R.id.photoSmall);
mPhotoSmall.setVisibility(View.GONE);
mInCallMessageLabel = (TextView) view.findViewById(R.id.connectionServiceMessage);
mProgressSpinner = view.findViewById(R.id.progressSpinner);
mFloatingActionButtonContainer = view.findViewById(
R.id.floating_end_call_action_button_container);
mFloatingActionButton = (ImageButton) view.findViewById(
R.id.floating_end_call_action_button);
mFloatingActionButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
getPresenter().endCallClicked();
}
});
mFloatingActionButtonController = new FloatingActionButtonController(getActivity(),
mFloatingActionButtonContainer, mFloatingActionButton);
mSecondaryCallInfo.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
getPresenter().secondaryInfoClicked();
updateFabPositionForSecondaryCallInfo();
}
});
mCallStateButton = view.findViewById(R.id.callStateButton);
mCallStateButton.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
getPresenter().onCallStateButtonTouched();
return false;
}
});
mManageConferenceCallButton = view.findViewById(R.id.manage_conference_call_button);
mManageConferenceCallButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
InCallActivity activity = (InCallActivity) getActivity();
activity.showConferenceFragment(true);
}
});
mPrimaryName.setElegantTextHeight(false);
mCallStateLabel.setElegantTextHeight(false);
mCallSubject = (TextView) view.findViewById(R.id.callSubject);
}
@Override
public void setVisible(boolean on) {
if (on) {
getView().setVisibility(View.VISIBLE);
} else {
getView().setVisibility(View.INVISIBLE);
}
}
/**
* Hides or shows the progress spinner.
*
* @param visible {@code True} if the progress spinner should be visible.
*/
@Override
public void setProgressSpinnerVisible(boolean visible) {
mProgressSpinner.setVisibility(visible ? View.VISIBLE : View.GONE);
}
@Override
public void setContactContextTitle(View headerView) {
mContactContextListHeaders.removeAllViews();
mContactContextListHeaders.addView(headerView);
}
@Override
public void setContactContextContent(ListAdapter listAdapter) {
mContactContextListView.setAdapter(listAdapter);
}
@Override
public void showContactContext(boolean show) {
showImageView(mPhotoLarge, !show);
showImageView(mPhotoSmall, show);
mPrimaryCallCardContainer.setElevation(
show ? 0 : getResources().getDimension(R.dimen.primary_call_elevation));
mContactContext.setVisibility(show ? View.VISIBLE : View.GONE);
}
/**
* Sets the visibility of the primary call card.
* Ensures that when the primary call card is hidden, the video surface slides over to fill the
* entire screen.
*
* @param visible {@code True} if the primary call card should be visible.
*/
@Override
public void setCallCardVisible(final boolean visible) {
Log.v(this, "setCallCardVisible : isVisible = " + visible);
// When animating the hide/show of the views in a landscape layout, we need to take into
// account whether we are in a left-to-right locale or a right-to-left locale and adjust
// the animations accordingly.
final boolean isLayoutRtl = InCallPresenter.isRtl();
// Retrieve here since at fragment creation time the incoming video view is not inflated.
final View videoView = getView().findViewById(R.id.incomingVideo);
if (videoView == null) {
return;
}
// Determine how much space there is below or to the side of the call card.
final float spaceBesideCallCard = getSpaceBesideCallCard();
// We need to translate the video surface, but we need to know its position after the layout
// has occurred so use a {@code ViewTreeObserver}.
final ViewTreeObserver observer = getView().getViewTreeObserver();
observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
// We don't want to continue getting called.
getView().getViewTreeObserver().removeOnPreDrawListener(this);
float videoViewTranslation = 0f;
// Translate the call card to its pre-animation state.
if (!mIsLandscape) {
mPrimaryCallCardContainer.setTranslationY(visible ?
-mPrimaryCallCardContainer.getHeight() : 0);
ViewGroup.LayoutParams p = videoView.getLayoutParams();
videoViewTranslation = p.height / 2 - spaceBesideCallCard / 2;
}
// Perform animation of video view.
ViewPropertyAnimator videoViewAnimator = videoView.animate()
.setInterpolator(AnimUtils.EASE_OUT_EASE_IN)
.setDuration(mVideoAnimationDuration);
if (mIsLandscape) {
videoViewAnimator
.translationX(visible ? videoViewTranslation : 0);
} else {
videoViewAnimator
.translationY(visible ? videoViewTranslation : 0);
}
videoViewAnimator.start();
// Animate the call card sliding.
ViewPropertyAnimator callCardAnimator = mPrimaryCallCardContainer.animate()
.setInterpolator(AnimUtils.EASE_OUT_EASE_IN)
.setDuration(mVideoAnimationDuration)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
if (!visible) {
mPrimaryCallCardContainer.setVisibility(View.GONE);
}
}
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
if (visible) {
mPrimaryCallCardContainer.setVisibility(View.VISIBLE);
}
}
});
if (mIsLandscape) {
float translationX = mPrimaryCallCardContainer.getWidth();
translationX *= isLayoutRtl ? 1 : -1;
callCardAnimator
.translationX(visible ? 0 : translationX)
.start();
} else {
callCardAnimator
.translationY(visible ? 0 : -mPrimaryCallCardContainer.getHeight())
.start();
}
return true;
}
});
}
/**
* Determines the amount of space below the call card for portrait layouts), or beside the
* call card for landscape layouts.
*
* @return The amount of space below or beside the call card.
*/
public float getSpaceBesideCallCard() {
if (mIsLandscape) {
return getView().getWidth() - mPrimaryCallCardContainer.getWidth();
} else {
final int callCardHeight;
// Retrieve the actual height of the call card, independent of whether or not the
// outgoing call animation is in progress. The animation does not run in landscape mode
// so this only needs to be done for portrait.
if (mPrimaryCallCardContainer.getTag(R.id.view_tag_callcard_actual_height) != null) {
callCardHeight = (int) mPrimaryCallCardContainer.getTag(
R.id.view_tag_callcard_actual_height);
} else {
callCardHeight = mPrimaryCallCardContainer.getHeight();
}
return getView().getHeight() - callCardHeight;
}
}
@Override
public void setPrimaryName(String name, boolean nameIsNumber) {
if (TextUtils.isEmpty(name)) {
mPrimaryName.setText(null);
} else {
mPrimaryName.setText(nameIsNumber
? PhoneNumberUtilsCompat.createTtsSpannable(name)
: name);
// Set direction of the name field
int nameDirection = View.TEXT_DIRECTION_INHERIT;
if (nameIsNumber) {
nameDirection = View.TEXT_DIRECTION_LTR;
}
mPrimaryName.setTextDirection(nameDirection);
}
}
/**
* Sets the primary image for the contact photo.
*
* @param image The drawable to set.
* @param isVisible Whether the contact photo should be visible after being set.
*/
@Override
public void setPrimaryImage(Drawable image, boolean isVisible) {
if (image != null) {
setDrawableToImageViews(image);
showImageView(mPhotoLarge, isVisible);
}
}
@Override
public void setPrimaryPhoneNumber(String number) {
// Set the number
if (TextUtils.isEmpty(number)) {
mPhoneNumber.setText(null);
mPhoneNumber.setVisibility(View.GONE);
} else {
mPhoneNumber.setText(PhoneNumberUtilsCompat.createTtsSpannable(number));
mPhoneNumber.setVisibility(View.VISIBLE);
mPhoneNumber.setTextDirection(View.TEXT_DIRECTION_LTR);
}
}
@Override
public void setPrimaryLabel(String label) {
if (!TextUtils.isEmpty(label)) {
mNumberLabel.setText(label);
mNumberLabel.setVisibility(View.VISIBLE);
} else {
mNumberLabel.setVisibility(View.GONE);
}
}
/**
* Sets the primary caller information.
*
* @param number The caller phone number.
* @param name The caller name.
* @param nameIsNumber {@code true} if the name should be shown in place of the phone number.
* @param label The label.
* @param photo The contact photo drawable.
* @param isSipCall {@code true} if this is a SIP call.
* @param isContactPhotoShown {@code true} if the contact photo should be shown (it will be
* updated even if it is not shown).
* @param isWorkCall Whether the call is placed through a work phone account or caller is a work
contact.
*/
@Override
public void setPrimary(String number, String name, boolean nameIsNumber, String label,
Drawable photo, boolean isSipCall, boolean isContactPhotoShown, boolean isWorkCall) {
Log.d(this, "Setting primary call");
// set the name field.
setPrimaryName(name, nameIsNumber);
if (TextUtils.isEmpty(number) && TextUtils.isEmpty(label)) {
mCallNumberAndLabel.setVisibility(View.GONE);
mElapsedTime.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
} else {
mCallNumberAndLabel.setVisibility(View.VISIBLE);
mElapsedTime.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_END);
}
setPrimaryPhoneNumber(number);
// Set the label (Mobile, Work, etc)
setPrimaryLabel(label);
showInternetCallLabel(isSipCall);
setDrawableToImageViews(photo);
showImageView(mPhotoLarge, isContactPhotoShown);
showImageView(mWorkProfileIcon, isWorkCall);
}
@Override
public void setSecondary(boolean show, String name, boolean nameIsNumber, String label,
String providerLabel, boolean isConference, boolean isVideoCall, boolean isFullscreen) {
if (show) {
mHasSecondaryCallInfo = true;
boolean hasProvider = !TextUtils.isEmpty(providerLabel);
initializeSecondaryCallInfo(hasProvider);
// Do not show the secondary caller info in fullscreen mode, but ensure it is populated
// in case fullscreen mode is exited in the future.
setSecondaryInfoVisible(!isFullscreen);
mSecondaryCallConferenceCallIcon.setVisibility(isConference ? View.VISIBLE : View.GONE);
mSecondaryCallVideoCallIcon.setVisibility(isVideoCall ? View.VISIBLE : View.GONE);
mSecondaryCallName.setText(nameIsNumber
? PhoneNumberUtilsCompat.createTtsSpannable(name)
: name);
if (hasProvider) {
mSecondaryCallProviderLabel.setText(providerLabel);
}
int nameDirection = View.TEXT_DIRECTION_INHERIT;
if (nameIsNumber) {
nameDirection = View.TEXT_DIRECTION_LTR;
}
mSecondaryCallName.setTextDirection(nameDirection);
} else {
mHasSecondaryCallInfo = false;
setSecondaryInfoVisible(false);
}
}
/**
* Sets the visibility of the secondary caller info box. Note, if the {@code visible} parameter
* is passed in {@code true}, and there is no secondary caller info populated (as determined by
* {@code mHasSecondaryCallInfo}, the secondary caller info box will not be shown.
*
* @param visible {@code true} if the secondary caller info should be shown, {@code false}
* otherwise.
*/
@Override
public void setSecondaryInfoVisible(final boolean visible) {
boolean wasVisible = mSecondaryCallInfo.isShown();
final boolean isVisible = visible && mHasSecondaryCallInfo;
Log.v(this, "setSecondaryInfoVisible: wasVisible = " + wasVisible + " isVisible = "
+ isVisible);
// If visibility didn't change, nothing to do.
if (wasVisible == isVisible) {
return;
}
// If we are showing the secondary info, we need to show it before animating so that its
// height will be determined on layout.
if (isVisible) {
mSecondaryCallInfo.setVisibility(View.VISIBLE);
} else {
mSecondaryCallInfo.setVisibility(View.GONE);
}
updateFabPositionForSecondaryCallInfo();
// We need to translate the secondary caller info, but we need to know its position after
// the layout has occurred so use a {@code ViewTreeObserver}.
final ViewTreeObserver observer = getView().getViewTreeObserver();
observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
// We don't want to continue getting called.
getView().getViewTreeObserver().removeOnPreDrawListener(this);
// Get the height of the secondary call info now, and then re-hide the view prior
// to doing the actual animation.
int secondaryHeight = mSecondaryCallInfo.getHeight();
if (isVisible) {
mSecondaryCallInfo.setVisibility(View.GONE);
} else {
mSecondaryCallInfo.setVisibility(View.VISIBLE);
}
Log.v(this, "setSecondaryInfoVisible: secondaryHeight = " + secondaryHeight);
// Set the position of the secondary call info card to its starting location.
mSecondaryCallInfo.setTranslationY(visible ? secondaryHeight : 0);
// Animate the secondary card info slide up/down as it appears and disappears.
ViewPropertyAnimator secondaryInfoAnimator = mSecondaryCallInfo.animate()
.setInterpolator(AnimUtils.EASE_OUT_EASE_IN)
.setDuration(mVideoAnimationDuration)
.translationY(isVisible ? 0 : secondaryHeight)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (!isVisible) {
mSecondaryCallInfo.setVisibility(View.GONE);
}
}
@Override
public void onAnimationStart(Animator animation) {
if (isVisible) {
mSecondaryCallInfo.setVisibility(View.VISIBLE);
}
}
});
secondaryInfoAnimator.start();
// Notify listeners of a change in the visibility of the secondary info. This is
// important when in a video call so that the video call presenter can shift the
// video preview up or down to accommodate the secondary caller info.
InCallPresenter.getInstance().notifySecondaryCallerInfoVisibilityChanged(visible,
secondaryHeight);
return true;
}
});
}
@Override
public void setCallState(
int state,
int videoState,
int sessionModificationState,
DisconnectCause disconnectCause,
String connectionLabel,
Drawable callStateIcon,
String gatewayNumber,
boolean isWifi,
boolean isConference,
boolean isWorkCall) {
boolean isGatewayCall = !TextUtils.isEmpty(gatewayNumber);
CallStateLabel callStateLabel = getCallStateLabelFromState(state, videoState,
sessionModificationState, disconnectCause, connectionLabel, isGatewayCall, isWifi,
isConference, isWorkCall);
Log.v(this, "setCallState " + callStateLabel.getCallStateLabel());
Log.v(this, "AutoDismiss " + callStateLabel.isAutoDismissing());
Log.v(this, "DisconnectCause " + disconnectCause.toString());
Log.v(this, "gateway " + connectionLabel + gatewayNumber);
// Check for video state change and update the visibility of the contact photo. The contact
// photo is hidden when the incoming video surface is shown.
// The contact photo visibility can also change in setPrimary().
boolean showContactPhoto = !VideoCallPresenter.showIncomingVideo(videoState, state);
mPhotoLarge.setVisibility(showContactPhoto ? View.VISIBLE : View.GONE);
// Check if the call subject is showing -- if it is, we want to bypass showing the call
// state.
boolean isSubjectShowing = mCallSubject.getVisibility() == View.VISIBLE;
if (TextUtils.equals(callStateLabel.getCallStateLabel(), mCallStateLabel.getText()) &&
!isSubjectShowing) {
// Nothing to do if the labels are the same
if (state == Call.State.ACTIVE || state == Call.State.CONFERENCED) {
mCallStateLabel.clearAnimation();
mCallStateIcon.clearAnimation();
}
return;
}
if (isSubjectShowing) {
changeCallStateLabel(null);
callStateIcon = null;
} else {
// Update the call state label and icon.
setCallStateLabel(callStateLabel);
}
if (!TextUtils.isEmpty(callStateLabel.getCallStateLabel())) {
if (state == Call.State.ACTIVE || state == Call.State.CONFERENCED) {
mCallStateLabel.clearAnimation();
} else {
mCallStateLabel.startAnimation(mPulseAnimation);
}
} else {
mCallStateLabel.clearAnimation();
}
if (callStateIcon != null) {
mCallStateIcon.setVisibility(View.VISIBLE);
// Invoke setAlpha(float) instead of setAlpha(int) to set the view's alpha. This is
// needed because the pulse animation operates on the view alpha.
mCallStateIcon.setAlpha(1.0f);
mCallStateIcon.setImageDrawable(callStateIcon);
if (state == Call.State.ACTIVE || state == Call.State.CONFERENCED
|| TextUtils.isEmpty(callStateLabel.getCallStateLabel())) {
mCallStateIcon.clearAnimation();
} else {
mCallStateIcon.startAnimation(mPulseAnimation);
}
if (callStateIcon instanceof AnimationDrawable) {
((AnimationDrawable) callStateIcon).start();
}
} else {
mCallStateIcon.clearAnimation();
// Invoke setAlpha(float) instead of setAlpha(int) to set the view's alpha. This is
// needed because the pulse animation operates on the view alpha.
mCallStateIcon.setAlpha(0.0f);
mCallStateIcon.setVisibility(View.GONE);
}
if (VideoUtils.isVideoCall(videoState)
|| (state == Call.State.ACTIVE && sessionModificationState
== Call.SessionModificationState.WAITING_FOR_RESPONSE)) {
mCallStateVideoCallIcon.setVisibility(View.VISIBLE);
} else {
mCallStateVideoCallIcon.setVisibility(View.GONE);
}
}
private void setCallStateLabel(CallStateLabel callStateLabel) {
Log.v(this, "setCallStateLabel : label = " + callStateLabel.getCallStateLabel());
if (callStateLabel.isAutoDismissing()) {
mCallStateLabelResetPending = true;
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
Log.v(this, "restoringCallStateLabel : label = " +
mPostResetCallStateLabel);
changeCallStateLabel(mPostResetCallStateLabel);
mCallStateLabelResetPending = false;
}
}, CALL_STATE_LABEL_RESET_DELAY_MS);
changeCallStateLabel(callStateLabel.getCallStateLabel());
} else {
// Keep track of the current call state label; used when resetting auto dismissing
// call state labels.
mPostResetCallStateLabel = callStateLabel.getCallStateLabel();
if (!mCallStateLabelResetPending) {
changeCallStateLabel(callStateLabel.getCallStateLabel());
}
}
}
private void changeCallStateLabel(CharSequence callStateLabel) {
Log.v(this, "changeCallStateLabel : label = " + callStateLabel);
if (!TextUtils.isEmpty(callStateLabel)) {
mCallStateLabel.setText(callStateLabel);
mCallStateLabel.setAlpha(1);
mCallStateLabel.setVisibility(View.VISIBLE);
} else {
Animation callStateLabelAnimation = mCallStateLabel.getAnimation();
if (callStateLabelAnimation != null) {
callStateLabelAnimation.cancel();
}
mCallStateLabel.setText(null);
mCallStateLabel.setAlpha(0);
mCallStateLabel.setVisibility(View.GONE);
}
}
@Override
public void setCallbackNumber(String callbackNumber, boolean isEmergencyCall) {
if (mInCallMessageLabel == null) {
return;
}
if (TextUtils.isEmpty(callbackNumber)) {
mInCallMessageLabel.setVisibility(View.GONE);
return;
}
// TODO: The new Locale-specific methods don't seem to be working. Revisit this.
callbackNumber = PhoneNumberUtils.formatNumber(callbackNumber);
int stringResourceId = isEmergencyCall ? R.string.card_title_callback_number_emergency
: R.string.card_title_callback_number;
String text = getString(stringResourceId, callbackNumber);
mInCallMessageLabel.setText(text);
mInCallMessageLabel.setVisibility(View.VISIBLE);
}
/**
* Sets and shows the call subject if it is not empty. Hides the call subject otherwise.
*
* @param callSubject The call subject.
*/
@Override
public void setCallSubject(String callSubject) {
boolean showSubject = !TextUtils.isEmpty(callSubject);
mCallSubject.setVisibility(showSubject ? View.VISIBLE : View.GONE);
if (showSubject) {
mCallSubject.setText(callSubject);
} else {
mCallSubject.setText(null);
}
}
public boolean isAnimating() {
return mIsAnimating;
}
private void showInternetCallLabel(boolean show) {
if (show) {
final String label = getView().getContext().getString(
R.string.incall_call_type_label_sip);
mCallTypeLabel.setVisibility(View.VISIBLE);
mCallTypeLabel.setText(label);
} else {
mCallTypeLabel.setVisibility(View.GONE);
}
}
@Override
public void setPrimaryCallElapsedTime(boolean show, long duration) {
if (show) {
if (mElapsedTime.getVisibility() != View.VISIBLE) {
AnimUtils.fadeIn(mElapsedTime, AnimUtils.DEFAULT_DURATION);
}
String callTimeElapsed = DateUtils.formatElapsedTime(duration / 1000);
mElapsedTime.setText(callTimeElapsed);
String durationDescription =
InCallDateUtils.formatDuration(duration);
mElapsedTime.setContentDescription(
!TextUtils.isEmpty(durationDescription) ? durationDescription : null);
} else {
// hide() animation has no effect if it is already hidden.
AnimUtils.fadeOut(mElapsedTime, AnimUtils.DEFAULT_DURATION);
}
}
/**
* Set all the ImageViews to the same photo. Currently there are 2 photo views: the large one
* (which fills about the bottom half of the screen) and the small one, which displays as a
* circle next to the primary contact info. This method does not handle whether the ImageView
* is shown or not.
*
* @param photo The photo to set for the image views.
*/
private void setDrawableToImageViews(Drawable photo) {
if (photo == null) {
photo = ContactInfoCache.getInstance(getView().getContext())
.getDefaultContactPhotoDrawable();
}
if (mPrimaryPhotoDrawable == photo){
return;
}
mPrimaryPhotoDrawable = photo;
mPhotoLarge.setImageDrawable(photo);
// Modify the drawable to be round for the smaller ImageView.
Bitmap bitmap = drawableToBitmap(photo);
if (bitmap != null) {
final RoundedBitmapDrawable drawable =
RoundedBitmapDrawableFactory.create(getResources(), bitmap);
drawable.setAntiAlias(true);
drawable.setCornerRadius(bitmap.getHeight() / 2);
photo = drawable;
}
mPhotoSmall.setImageDrawable(photo);
}
/**
* Helper method for image view to handle animations.
*
* @param view The image view to show or hide.
* @param isVisible {@code true} if we want to show the image, {@code false} to hide it.
*/
private void showImageView(ImageView view, boolean isVisible) {
if (view.getDrawable() == null) {
if (isVisible) {
AnimUtils.fadeIn(mElapsedTime, AnimUtils.DEFAULT_DURATION);
}
} else {
// Cross fading is buggy and not noticeable due to the multiple calls to this method
// that switch drawables in the middle of the cross-fade animations. Just show the
// photo directly instead.
view.setVisibility(isVisible ? View.VISIBLE : View.GONE);
}
}
/**
* Converts a drawable into a bitmap.
*
* @param drawable the drawable to be converted.
*/
public static Bitmap drawableToBitmap(Drawable drawable) {
Bitmap bitmap;
if (drawable instanceof BitmapDrawable) {
bitmap = ((BitmapDrawable) drawable).getBitmap();
} else {
if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) {
// Needed for drawables that are just a colour.
bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
} else {
bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
}
Log.i(TAG, "Created bitmap with width " + bitmap.getWidth() + ", height "
+ bitmap.getHeight());
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
}
return bitmap;
}
/**
* Gets the call state label based on the state of the call or cause of disconnect.
*
* Additional labels are applied as follows:
* 1. All outgoing calls with display "Calling via [Provider]".
* 2. Ongoing calls will display the name of the provider.
* 3. Incoming calls will only display "Incoming via..." for accounts.
* 4. Video calls, and session modification states (eg. requesting video).
* 5. Incoming and active Wi-Fi calls will show label provided by hint.
*
* TODO: Move this to the CallCardPresenter.
*/
private CallStateLabel getCallStateLabelFromState(int state, int videoState,
int sessionModificationState, DisconnectCause disconnectCause, String label,
boolean isGatewayCall, boolean isWifi, boolean isConference, boolean isWorkCall) {
final Context context = getView().getContext();
CharSequence callStateLabel = null; // Label to display as part of the call banner
boolean hasSuggestedLabel = label != null;
boolean isAccount = hasSuggestedLabel && !isGatewayCall;
boolean isAutoDismissing = false;
switch (state) {
case Call.State.IDLE:
// "Call state" is meaningless in this state.
break;
case Call.State.ACTIVE:
// We normally don't show a "call state label" at all in this state
// (but we can use the call state label to display the provider name).
if ((isAccount || isWifi || isConference) && hasSuggestedLabel) {
callStateLabel = label;
} else if (sessionModificationState
== Call.SessionModificationState.REQUEST_REJECTED) {
callStateLabel = context.getString(R.string.card_title_video_call_rejected);
isAutoDismissing = true;
} else if (sessionModificationState
== Call.SessionModificationState.REQUEST_FAILED) {
callStateLabel = context.getString(R.string.card_title_video_call_error);
isAutoDismissing = true;
} else if (sessionModificationState
== Call.SessionModificationState.WAITING_FOR_RESPONSE) {
callStateLabel = context.getString(R.string.card_title_video_call_requesting);
} else if (sessionModificationState
== Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
callStateLabel = context.getString(R.string.card_title_video_call_requesting);
} else if (VideoUtils.isVideoCall(videoState)) {
callStateLabel = context.getString(R.string.card_title_video_call);
}
break;
case Call.State.ONHOLD:
callStateLabel = context.getString(R.string.card_title_on_hold);
break;
case Call.State.CONNECTING:
case Call.State.DIALING:
if (hasSuggestedLabel && !isWifi) {
callStateLabel = context.getString(R.string.calling_via_template, label);
} else {
callStateLabel = context.getString(R.string.card_title_dialing);
}
break;
case Call.State.REDIALING:
callStateLabel = context.getString(R.string.card_title_redialing);
break;
case Call.State.INCOMING:
case Call.State.CALL_WAITING:
if (isWifi && hasSuggestedLabel) {
callStateLabel = label;
} else if (isAccount) {
callStateLabel = context.getString(R.string.incoming_via_template, label);
} else if (VideoUtils.isVideoCall(videoState)) {
callStateLabel = context.getString(R.string.notification_incoming_video_call);
} else {
callStateLabel =
context.getString(isWorkCall ? R.string.card_title_incoming_work_call
: R.string.card_title_incoming_call);
}
break;
case Call.State.DISCONNECTING:
// While in the DISCONNECTING state we display a "Hanging up"
// message in order to make the UI feel more responsive. (In
// GSM it's normal to see a delay of a couple of seconds while
// negotiating the disconnect with the network, so the "Hanging
// up" state at least lets the user know that we're doing
// something. This state is currently not used with CDMA.)
callStateLabel = context.getString(R.string.card_title_hanging_up);
break;
case Call.State.DISCONNECTED:
callStateLabel = disconnectCause.getLabel();
if (TextUtils.isEmpty(callStateLabel)) {
callStateLabel = context.getString(R.string.card_title_call_ended);
}
break;
case Call.State.CONFERENCED:
callStateLabel = context.getString(R.string.card_title_conf_call);
break;
default:
Log.wtf(this, "updateCallStateWidgets: unexpected call: " + state);
}
return new CallStateLabel(callStateLabel, isAutoDismissing);
}
private void initializeSecondaryCallInfo(boolean hasProvider) {
// mSecondaryCallName is initialized here (vs. onViewCreated) because it is inaccessible
// until mSecondaryCallInfo is inflated in the call above.
if (mSecondaryCallName == null) {
mSecondaryCallName = (TextView) getView().findViewById(R.id.secondaryCallName);
mSecondaryCallConferenceCallIcon =
getView().findViewById(R.id.secondaryCallConferenceCallIcon);
mSecondaryCallVideoCallIcon =
getView().findViewById(R.id.secondaryCallVideoCallIcon);
}
if (mSecondaryCallProviderLabel == null && hasProvider) {
mSecondaryCallProviderInfo.setVisibility(View.VISIBLE);
mSecondaryCallProviderLabel = (TextView) getView()
.findViewById(R.id.secondaryCallProviderLabel);
}
}
public void dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
if (event.getEventType() == AccessibilityEvent.TYPE_ANNOUNCEMENT) {
// Indicate this call is in active if no label is provided. The label is empty when
// the call is in active, not in other status such as onhold or dialing etc.
if (!mCallStateLabel.isShown() || TextUtils.isEmpty(mCallStateLabel.getText())) {
event.getText().add(
TextUtils.expandTemplate(
getResources().getText(R.string.accessibility_call_is_active),
mPrimaryName.getText()));
} else {
dispatchPopulateAccessibilityEvent(event, mCallStateLabel);
dispatchPopulateAccessibilityEvent(event, mPrimaryName);
dispatchPopulateAccessibilityEvent(event, mCallTypeLabel);
dispatchPopulateAccessibilityEvent(event, mPhoneNumber);
}
return;
}
dispatchPopulateAccessibilityEvent(event, mCallStateLabel);
dispatchPopulateAccessibilityEvent(event, mPrimaryName);
dispatchPopulateAccessibilityEvent(event, mPhoneNumber);
dispatchPopulateAccessibilityEvent(event, mCallTypeLabel);
dispatchPopulateAccessibilityEvent(event, mSecondaryCallName);
dispatchPopulateAccessibilityEvent(event, mSecondaryCallProviderLabel);
return;
}
@Override
public void sendAccessibilityAnnouncement() {
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
if (getView() != null && getView().getParent() != null &&
isAccessibilityEnabled(getContext())) {
AccessibilityEvent event = AccessibilityEvent.obtain(
AccessibilityEvent.TYPE_ANNOUNCEMENT);
dispatchPopulateAccessibilityEvent(event);
getView().getParent().requestSendAccessibilityEvent(getView(), event);
}
}
private boolean isAccessibilityEnabled(Context context) {
AccessibilityManager accessibilityManager =
(AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
return accessibilityManager != null && accessibilityManager.isEnabled();
}
}, ACCESSIBILITY_ANNOUNCEMENT_DELAY_MS);
}
@Override
public void setEndCallButtonEnabled(boolean enabled, boolean animate) {
if (enabled != mFloatingActionButton.isEnabled()) {
if (animate) {
if (enabled) {
mFloatingActionButtonController.scaleIn(AnimUtils.NO_DELAY);
} else {
mFloatingActionButtonController.scaleOut();
}
} else {
if (enabled) {
mFloatingActionButtonContainer.setScaleX(1);
mFloatingActionButtonContainer.setScaleY(1);
mFloatingActionButtonContainer.setVisibility(View.VISIBLE);
} else {
mFloatingActionButtonContainer.setVisibility(View.GONE);
}
}
mFloatingActionButton.setEnabled(enabled);
updateFabPosition();
}
}
/**
* Changes the visibility of the HD audio icon.
*
* @param visible {@code true} if the UI should show the HD audio icon.
*/
@Override
public void showHdAudioIndicator(boolean visible) {
mHdAudioIcon.setVisibility(visible ? View.VISIBLE : View.GONE);
}
/**
* Changes the visibility of the forward icon.
*
* @param visible {@code true} if the UI should show the forward icon.
*/
@Override
public void showForwardIndicator(boolean visible) {
mForwardIcon.setVisibility(visible ? View.VISIBLE : View.GONE);
}
/**
* Changes the visibility of the spam icon.
*
* @param visible {@code true} if the UI should show the spam icon.
*/
@Override
public void showSpamIndicator(boolean visible) {
if (visible) {
mSpamIcon.setVisibility(View.VISIBLE);
mNumberLabel.setText(R.string.label_spam_caller);
mPhoneNumber.setVisibility(View.GONE);
}
}
/**
* Changes the visibility of the "manage conference call" button.
*
* @param visible Whether to set the button to be visible or not.
*/
@Override
public void showManageConferenceCallButton(boolean visible) {
mManageConferenceCallButton.setVisibility(visible ? View.VISIBLE : View.GONE);
}
/**
* Determines the current visibility of the manage conference button.
*
* @return {@code true} if the button is visible.
*/
@Override
public boolean isManageConferenceVisible() {
return mManageConferenceCallButton.getVisibility() == View.VISIBLE;
}
/**
* Determines the current visibility of the call subject.
*
* @return {@code true} if the subject is visible.
*/
@Override
public boolean isCallSubjectVisible() {
return mCallSubject.getVisibility() == View.VISIBLE;
}
/**
* Get the overall InCallUI background colors and apply to call card.
*/
public void updateColors() {
MaterialPalette themeColors = InCallPresenter.getInstance().getThemeColors();
if (mCurrentThemeColors != null && mCurrentThemeColors.equals(themeColors)) {
return;
}
if (getResources().getBoolean(R.bool.is_layout_landscape)) {
final GradientDrawable drawable =
(GradientDrawable) mPrimaryCallCardContainer.getBackground();
drawable.setColor(themeColors.mPrimaryColor);
} else {
mPrimaryCallCardContainer.setBackgroundColor(themeColors.mPrimaryColor);
}
mCallButtonsContainer.setBackgroundColor(themeColors.mPrimaryColor);
mCallSubject.setTextColor(themeColors.mPrimaryColor);
mContactContext.setBackgroundColor(themeColors.mPrimaryColor);
//TODO: set color of message text in call context "recent messages" to be the theme color.
mCurrentThemeColors = themeColors;
}
private void dispatchPopulateAccessibilityEvent(AccessibilityEvent event, View view) {
if (view == null) return;
final List<CharSequence> eventText = event.getText();
int size = eventText.size();
view.dispatchPopulateAccessibilityEvent(event);
// if no text added write null to keep relative position
if (size == eventText.size()) {
eventText.add(null);
}
}
@Override
public void animateForNewOutgoingCall() {
final ViewGroup parent = (ViewGroup) mPrimaryCallCardContainer.getParent();
final ViewTreeObserver observer = getView().getViewTreeObserver();
mIsAnimating = true;
observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
final ViewTreeObserver observer = getView().getViewTreeObserver();
if (!observer.isAlive()) {
return;
}
observer.removeOnGlobalLayoutListener(this);
final LayoutIgnoringListener listener = new LayoutIgnoringListener();
mPrimaryCallCardContainer.addOnLayoutChangeListener(listener);
// Prepare the state of views before the slide animation
final int originalHeight = mPrimaryCallCardContainer.getHeight();
mPrimaryCallCardContainer.setTag(R.id.view_tag_callcard_actual_height,
originalHeight);
mPrimaryCallCardContainer.setBottom(parent.getHeight());
// Set up FAB.
mFloatingActionButtonContainer.setVisibility(View.GONE);
mFloatingActionButtonController.setScreenWidth(parent.getWidth());
mCallButtonsContainer.setAlpha(0);
mCallStateLabel.setAlpha(0);
mPrimaryName.setAlpha(0);
mCallTypeLabel.setAlpha(0);
mCallNumberAndLabel.setAlpha(0);
assignTranslateAnimation(mCallStateLabel, 1);
assignTranslateAnimation(mCallStateIcon, 1);
assignTranslateAnimation(mPrimaryName, 2);
assignTranslateAnimation(mCallNumberAndLabel, 3);
assignTranslateAnimation(mCallTypeLabel, 4);
assignTranslateAnimation(mCallButtonsContainer, 5);
final Animator animator = getShrinkAnimator(parent.getHeight(), originalHeight);
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mPrimaryCallCardContainer.setTag(R.id.view_tag_callcard_actual_height,
null);
setViewStatePostAnimation(listener);
mIsAnimating = false;
InCallPresenter.getInstance().onShrinkAnimationComplete();
if (animator != null) {
animator.removeListener(this);
}
}
});
animator.start();
}
});
}
@Override
public void showNoteSentToast() {
Toast.makeText(getContext(), R.string.note_sent, Toast.LENGTH_LONG).show();
}
public void onDialpadVisibilityChange(boolean isShown) {
mIsDialpadShowing = isShown;
updateFabPosition();
}
private void updateFabPosition() {
int offsetY = 0;
if (!mIsDialpadShowing) {
offsetY = mFloatingActionButtonVerticalOffset;
if (mSecondaryCallInfo.isShown() && mHasLargePhoto) {
offsetY -= mSecondaryCallInfo.getHeight();
}
}
mFloatingActionButtonController.align(
FloatingActionButtonController.ALIGN_MIDDLE,
0 /* offsetX */,
offsetY,
true);
mFloatingActionButtonController.resize(
mIsDialpadShowing ? mFabSmallDiameter : mFabNormalDiameter, true);
}
@Override
public Context getContext() {
return getActivity();
}
@Override
public void onResume() {
super.onResume();
// If the previous launch animation is still running, cancel it so that we don't get
// stuck in an intermediate animation state.
if (mAnimatorSet != null && mAnimatorSet.isRunning()) {
mAnimatorSet.cancel();
}
mIsLandscape = getResources().getBoolean(R.bool.is_layout_landscape);
mHasLargePhoto = getResources().getBoolean(R.bool.has_large_photo);
final ViewGroup parent = ((ViewGroup) mPrimaryCallCardContainer.getParent());
final ViewTreeObserver observer = parent.getViewTreeObserver();
parent.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
ViewTreeObserver viewTreeObserver = observer;
if (!viewTreeObserver.isAlive()) {
viewTreeObserver = parent.getViewTreeObserver();
}
viewTreeObserver.removeOnGlobalLayoutListener(this);
mFloatingActionButtonController.setScreenWidth(parent.getWidth());
updateFabPosition();
}
});
updateColors();
}
/**
* Adds a global layout listener to update the FAB's positioning on the next layout. This allows
* us to position the FAB after the secondary call info's height has been calculated.
*/
private void updateFabPositionForSecondaryCallInfo() {
mSecondaryCallInfo.getViewTreeObserver().addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
final ViewTreeObserver observer = mSecondaryCallInfo.getViewTreeObserver();
if (!observer.isAlive()) {
return;
}
observer.removeOnGlobalLayoutListener(this);
onDialpadVisibilityChange(mIsDialpadShowing);
}
});
}
/**
* Animator that performs the upwards shrinking animation of the blue call card scrim.
* At the start of the animation, each child view is moved downwards by a pre-specified amount
* and then translated upwards together with the scrim.
*/
private Animator getShrinkAnimator(int startHeight, int endHeight) {
final ObjectAnimator shrinkAnimator =
ObjectAnimator.ofInt(mPrimaryCallCardContainer, "bottom", startHeight, endHeight);
shrinkAnimator.setDuration(mShrinkAnimationDuration);
shrinkAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
mFloatingActionButton.setEnabled(true);
}
});
shrinkAnimator.setInterpolator(AnimUtils.EASE_IN);
return shrinkAnimator;
}
private void assignTranslateAnimation(View view, int offset) {
view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
view.buildLayer();
view.setTranslationY(mTranslationOffset * offset);
view.animate().translationY(0).alpha(1).withLayer()
.setDuration(mShrinkAnimationDuration).setInterpolator(AnimUtils.EASE_IN);
}
private void setViewStatePostAnimation(View view) {
view.setTranslationY(0);
view.setAlpha(1);
}
private void setViewStatePostAnimation(OnLayoutChangeListener layoutChangeListener) {
setViewStatePostAnimation(mCallButtonsContainer);
setViewStatePostAnimation(mCallStateLabel);
setViewStatePostAnimation(mPrimaryName);
setViewStatePostAnimation(mCallTypeLabel);
setViewStatePostAnimation(mCallNumberAndLabel);
setViewStatePostAnimation(mCallStateIcon);
mPrimaryCallCardContainer.removeOnLayoutChangeListener(layoutChangeListener);
mFloatingActionButtonController.scaleIn(AnimUtils.NO_DELAY);
}
private final class LayoutIgnoringListener implements View.OnLayoutChangeListener {
@Override
public void onLayoutChange(View v,
int left,
int top,
int right,
int bottom,
int oldLeft,
int oldTop,
int oldRight,
int oldBottom) {
v.setLeft(oldLeft);
v.setRight(oldRight);
v.setTop(oldTop);
v.setBottom(oldBottom);
}
}
}