/* * Copyright (C) 2014 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.systemui.statusbar; import android.app.Notification; import android.app.PendingIntent; import android.app.RemoteInput; import android.content.Context; import android.graphics.Rect; import android.os.Build; import android.service.notification.StatusBarNotification; import android.util.ArraySet; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.NotificationHeaderView; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.NotificationColorUtil; import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.statusbar.notification.HybridGroupManager; import com.android.systemui.statusbar.notification.HybridNotificationView; import com.android.systemui.statusbar.notification.NotificationCustomViewWrapper; import com.android.systemui.statusbar.notification.NotificationUtils; import com.android.systemui.statusbar.notification.NotificationViewWrapper; import com.android.systemui.statusbar.phone.NotificationGroupManager; import com.android.systemui.statusbar.policy.RemoteInputView; import com.android.systemui.statusbar.policy.SmartReplyConstants; import com.android.systemui.statusbar.policy.SmartReplyView; /** * A frame layout containing the actual payload of the notification, including the contracted, * expanded and heads up layout. This class is responsible for clipping the content and and * switching between the expanded, contracted and the heads up view depending on its clipped size. */ public class NotificationContentView extends FrameLayout { private static final String TAG = "NotificationContentView"; public static final int VISIBLE_TYPE_CONTRACTED = 0; public static final int VISIBLE_TYPE_EXPANDED = 1; public static final int VISIBLE_TYPE_HEADSUP = 2; private static final int VISIBLE_TYPE_SINGLELINE = 3; public static final int VISIBLE_TYPE_AMBIENT = 4; private static final int VISIBLE_TYPE_AMBIENT_SINGLELINE = 5; public static final int UNDEFINED = -1; private final Rect mClipBounds = new Rect(); private int mMinContractedHeight; private int mNotificationContentMarginEnd; private View mContractedChild; private View mExpandedChild; private View mHeadsUpChild; private HybridNotificationView mSingleLineView; private View mAmbientChild; private HybridNotificationView mAmbientSingleLineChild; private RemoteInputView mExpandedRemoteInput; private RemoteInputView mHeadsUpRemoteInput; private SmartReplyConstants mSmartReplyConstants; private SmartReplyView mExpandedSmartReplyView; private SmartReplyController mSmartReplyController; private NotificationViewWrapper mContractedWrapper; private NotificationViewWrapper mExpandedWrapper; private NotificationViewWrapper mHeadsUpWrapper; private NotificationViewWrapper mAmbientWrapper; private HybridGroupManager mHybridGroupManager; private int mClipTopAmount; private int mContentHeight; private int mVisibleType = VISIBLE_TYPE_CONTRACTED; private boolean mDark; private boolean mAnimate; private boolean mIsHeadsUp; private boolean mLegacy; private boolean mIsChildInGroup; private int mSmallHeight; private int mHeadsUpHeight; private int mNotificationMaxHeight; private int mNotificationAmbientHeight; private StatusBarNotification mStatusBarNotification; private NotificationGroupManager mGroupManager; private RemoteInputController mRemoteInputController; private Runnable mExpandedVisibleListener; private final ViewTreeObserver.OnPreDrawListener mEnableAnimationPredrawListener = new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { // We need to post since we don't want the notification to animate on the very first // frame post(new Runnable() { @Override public void run() { mAnimate = true; } }); getViewTreeObserver().removeOnPreDrawListener(this); return true; } }; private OnClickListener mExpandClickListener; private boolean mBeforeN; private boolean mExpandable; private boolean mClipToActualHeight = true; private ExpandableNotificationRow mContainingNotification; /** The visible type at the start of a touch driven transformation */ private int mTransformationStartVisibleType; /** The visible type at the start of an animation driven transformation */ private int mAnimationStartVisibleType = UNDEFINED; private boolean mUserExpanding; private int mSingleLineWidthIndention; private boolean mForceSelectNextLayout = true; private PendingIntent mPreviousExpandedRemoteInputIntent; private PendingIntent mPreviousHeadsUpRemoteInputIntent; private RemoteInputView mCachedExpandedRemoteInput; private RemoteInputView mCachedHeadsUpRemoteInput; private int mContentHeightAtAnimationStart = UNDEFINED; private boolean mFocusOnVisibilityChange; private boolean mHeadsUpAnimatingAway; private boolean mIconsVisible; private int mClipBottomAmount; private boolean mIsLowPriority; private boolean mIsContentExpandable; private boolean mRemoteInputVisible; private int mUnrestrictedContentHeight; public NotificationContentView(Context context, AttributeSet attrs) { super(context, attrs); mHybridGroupManager = new HybridGroupManager(getContext(), this); mSmartReplyConstants = Dependency.get(SmartReplyConstants.class); mSmartReplyController = Dependency.get(SmartReplyController.class); initView(); } public void initView() { mMinContractedHeight = getResources().getDimensionPixelSize( R.dimen.min_notification_layout_height); mNotificationContentMarginEnd = getResources().getDimensionPixelSize( com.android.internal.R.dimen.notification_content_margin_end); } public void setHeights(int smallHeight, int headsUpMaxHeight, int maxHeight, int ambientHeight) { mSmallHeight = smallHeight; mHeadsUpHeight = headsUpMaxHeight; mNotificationMaxHeight = maxHeight; mNotificationAmbientHeight = ambientHeight; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int heightMode = MeasureSpec.getMode(heightMeasureSpec); boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY; boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST; int maxSize = Integer.MAX_VALUE / 2; int width = MeasureSpec.getSize(widthMeasureSpec); if (hasFixedHeight || isHeightLimited) { maxSize = MeasureSpec.getSize(heightMeasureSpec); } int maxChildHeight = 0; if (mExpandedChild != null) { int notificationMaxHeight = mNotificationMaxHeight; if (mExpandedSmartReplyView != null) { notificationMaxHeight += mExpandedSmartReplyView.getHeightUpperLimit(); } notificationMaxHeight += mExpandedWrapper.getExtraMeasureHeight(); int size = notificationMaxHeight; ViewGroup.LayoutParams layoutParams = mExpandedChild.getLayoutParams(); boolean useExactly = false; if (layoutParams.height >= 0) { // An actual height is set size = Math.min(size, layoutParams.height); useExactly = true; } int spec = MeasureSpec.makeMeasureSpec(size, useExactly ? MeasureSpec.EXACTLY : MeasureSpec.AT_MOST); measureChildWithMargins(mExpandedChild, widthMeasureSpec, 0, spec, 0); maxChildHeight = Math.max(maxChildHeight, mExpandedChild.getMeasuredHeight()); } if (mContractedChild != null) { int heightSpec; int size = mSmallHeight; ViewGroup.LayoutParams layoutParams = mContractedChild.getLayoutParams(); boolean useExactly = false; if (layoutParams.height >= 0) { // An actual height is set size = Math.min(size, layoutParams.height); useExactly = true; } if (shouldContractedBeFixedSize() || useExactly) { heightSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY); } else { heightSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST); } measureChildWithMargins(mContractedChild, widthMeasureSpec, 0, heightSpec, 0); int measuredHeight = mContractedChild.getMeasuredHeight(); if (measuredHeight < mMinContractedHeight) { heightSpec = MeasureSpec.makeMeasureSpec(mMinContractedHeight, MeasureSpec.EXACTLY); measureChildWithMargins(mContractedChild, widthMeasureSpec, 0, heightSpec, 0); } maxChildHeight = Math.max(maxChildHeight, measuredHeight); if (updateContractedHeaderWidth()) { measureChildWithMargins(mContractedChild, widthMeasureSpec, 0, heightSpec, 0); } if (mExpandedChild != null && mContractedChild.getMeasuredHeight() > mExpandedChild.getMeasuredHeight()) { // the Expanded child is smaller then the collapsed. Let's remeasure it. heightSpec = MeasureSpec.makeMeasureSpec(mContractedChild.getMeasuredHeight(), MeasureSpec.EXACTLY); measureChildWithMargins(mExpandedChild, widthMeasureSpec, 0, heightSpec, 0); } } if (mHeadsUpChild != null) { int maxHeight = mHeadsUpHeight; maxHeight += mHeadsUpWrapper.getExtraMeasureHeight(); int size = maxHeight; ViewGroup.LayoutParams layoutParams = mHeadsUpChild.getLayoutParams(); boolean useExactly = false; if (layoutParams.height >= 0) { // An actual height is set size = Math.min(size, layoutParams.height); useExactly = true; } measureChildWithMargins(mHeadsUpChild, widthMeasureSpec, 0, MeasureSpec.makeMeasureSpec(size, useExactly ? MeasureSpec.EXACTLY : MeasureSpec.AT_MOST), 0); maxChildHeight = Math.max(maxChildHeight, mHeadsUpChild.getMeasuredHeight()); } if (mSingleLineView != null) { int singleLineWidthSpec = widthMeasureSpec; if (mSingleLineWidthIndention != 0 && MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) { singleLineWidthSpec = MeasureSpec.makeMeasureSpec( width - mSingleLineWidthIndention + mSingleLineView.getPaddingEnd(), MeasureSpec.EXACTLY); } mSingleLineView.measure(singleLineWidthSpec, MeasureSpec.makeMeasureSpec(mNotificationMaxHeight, MeasureSpec.AT_MOST)); maxChildHeight = Math.max(maxChildHeight, mSingleLineView.getMeasuredHeight()); } if (mAmbientChild != null) { int size = mNotificationAmbientHeight; ViewGroup.LayoutParams layoutParams = mAmbientChild.getLayoutParams(); boolean useExactly = false; if (layoutParams.height >= 0) { // An actual height is set size = Math.min(size, layoutParams.height); useExactly = true; } mAmbientChild.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(size, useExactly ? MeasureSpec.EXACTLY : MeasureSpec.AT_MOST)); maxChildHeight = Math.max(maxChildHeight, mAmbientChild.getMeasuredHeight()); } if (mAmbientSingleLineChild != null) { int size = mNotificationAmbientHeight; ViewGroup.LayoutParams layoutParams = mAmbientSingleLineChild.getLayoutParams(); boolean useExactly = false; if (layoutParams.height >= 0) { // An actual height is set size = Math.min(size, layoutParams.height); useExactly = true; } int ambientSingleLineWidthSpec = widthMeasureSpec; if (mSingleLineWidthIndention != 0 && MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) { ambientSingleLineWidthSpec = MeasureSpec.makeMeasureSpec( width - mSingleLineWidthIndention + mAmbientSingleLineChild.getPaddingEnd(), MeasureSpec.EXACTLY); } mAmbientSingleLineChild.measure(ambientSingleLineWidthSpec, MeasureSpec.makeMeasureSpec(size, useExactly ? MeasureSpec.EXACTLY : MeasureSpec.AT_MOST)); maxChildHeight = Math.max(maxChildHeight, mAmbientSingleLineChild.getMeasuredHeight()); } int ownHeight = Math.min(maxChildHeight, maxSize); setMeasuredDimension(width, ownHeight); } /** * Get the extra height that needs to be added to the notification height for a given * {@link RemoteInputView}. * This is needed when the user is inline replying in order to ensure that the reply bar has * enough padding. * * @param remoteInput The remote input to check. * @return The extra height needed. */ private int getExtraRemoteInputHeight(RemoteInputView remoteInput) { if (remoteInput != null && (remoteInput.isActive() || remoteInput.isSending())) { return getResources().getDimensionPixelSize( com.android.internal.R.dimen.notification_content_margin); } return 0; } private boolean updateContractedHeaderWidth() { // We need to update the expanded and the collapsed header to have exactly the same with to // have the expand buttons laid out at the same location. NotificationHeaderView contractedHeader = mContractedWrapper.getNotificationHeader(); if (contractedHeader != null) { if (mExpandedChild != null && mExpandedWrapper.getNotificationHeader() != null) { NotificationHeaderView expandedHeader = mExpandedWrapper.getNotificationHeader(); int expandedSize = expandedHeader.getMeasuredWidth() - expandedHeader.getPaddingEnd(); int collapsedSize = contractedHeader.getMeasuredWidth() - expandedHeader.getPaddingEnd(); if (expandedSize != collapsedSize) { int paddingEnd = contractedHeader.getMeasuredWidth() - expandedSize; contractedHeader.setPadding( contractedHeader.isLayoutRtl() ? paddingEnd : contractedHeader.getPaddingLeft(), contractedHeader.getPaddingTop(), contractedHeader.isLayoutRtl() ? contractedHeader.getPaddingLeft() : paddingEnd, contractedHeader.getPaddingBottom()); contractedHeader.setShowWorkBadgeAtEnd(true); return true; } } else { int paddingEnd = mNotificationContentMarginEnd; if (contractedHeader.getPaddingEnd() != paddingEnd) { contractedHeader.setPadding( contractedHeader.isLayoutRtl() ? paddingEnd : contractedHeader.getPaddingLeft(), contractedHeader.getPaddingTop(), contractedHeader.isLayoutRtl() ? contractedHeader.getPaddingLeft() : paddingEnd, contractedHeader.getPaddingBottom()); contractedHeader.setShowWorkBadgeAtEnd(false); return true; } } } return false; } private boolean shouldContractedBeFixedSize() { return mBeforeN && mContractedWrapper instanceof NotificationCustomViewWrapper; } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { int previousHeight = 0; if (mExpandedChild != null) { previousHeight = mExpandedChild.getHeight(); } super.onLayout(changed, left, top, right, bottom); if (previousHeight != 0 && mExpandedChild.getHeight() != previousHeight) { mContentHeightAtAnimationStart = previousHeight; } updateClipping(); invalidateOutline(); selectLayout(false /* animate */, mForceSelectNextLayout /* force */); mForceSelectNextLayout = false; updateExpandButtons(mExpandable); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); updateVisibility(); } public View getContractedChild() { return mContractedChild; } public View getExpandedChild() { return mExpandedChild; } public View getHeadsUpChild() { return mHeadsUpChild; } public View getAmbientChild() { return mAmbientChild; } public HybridNotificationView getAmbientSingleLineChild() { return mAmbientSingleLineChild; } public void setContractedChild(View child) { if (mContractedChild != null) { mContractedChild.animate().cancel(); removeView(mContractedChild); } addView(child); mContractedChild = child; mContractedWrapper = NotificationViewWrapper.wrap(getContext(), child, mContainingNotification); } private NotificationViewWrapper getWrapperForView(View child) { if (child == mContractedChild) { return mContractedWrapper; } if (child == mExpandedChild) { return mExpandedWrapper; } if (child == mHeadsUpChild) { return mHeadsUpWrapper; } if (child == mAmbientChild) { return mAmbientWrapper; } return null; } public void setExpandedChild(View child) { if (mExpandedChild != null) { mPreviousExpandedRemoteInputIntent = null; if (mExpandedRemoteInput != null) { mExpandedRemoteInput.onNotificationUpdateOrReset(); if (mExpandedRemoteInput.isActive()) { mPreviousExpandedRemoteInputIntent = mExpandedRemoteInput.getPendingIntent(); mCachedExpandedRemoteInput = mExpandedRemoteInput; mExpandedRemoteInput.dispatchStartTemporaryDetach(); ((ViewGroup)mExpandedRemoteInput.getParent()).removeView(mExpandedRemoteInput); } } mExpandedChild.animate().cancel(); removeView(mExpandedChild); mExpandedRemoteInput = null; } if (child == null) { mExpandedChild = null; mExpandedWrapper = null; if (mVisibleType == VISIBLE_TYPE_EXPANDED) { mVisibleType = VISIBLE_TYPE_CONTRACTED; } if (mTransformationStartVisibleType == VISIBLE_TYPE_EXPANDED) { mTransformationStartVisibleType = UNDEFINED; } return; } addView(child); mExpandedChild = child; mExpandedWrapper = NotificationViewWrapper.wrap(getContext(), child, mContainingNotification); } public void setHeadsUpChild(View child) { if (mHeadsUpChild != null) { mPreviousHeadsUpRemoteInputIntent = null; if (mHeadsUpRemoteInput != null) { mHeadsUpRemoteInput.onNotificationUpdateOrReset(); if (mHeadsUpRemoteInput.isActive()) { mPreviousHeadsUpRemoteInputIntent = mHeadsUpRemoteInput.getPendingIntent(); mCachedHeadsUpRemoteInput = mHeadsUpRemoteInput; mHeadsUpRemoteInput.dispatchStartTemporaryDetach(); ((ViewGroup)mHeadsUpRemoteInput.getParent()).removeView(mHeadsUpRemoteInput); } } mHeadsUpChild.animate().cancel(); removeView(mHeadsUpChild); mHeadsUpRemoteInput = null; } if (child == null) { mHeadsUpChild = null; mHeadsUpWrapper = null; if (mVisibleType == VISIBLE_TYPE_HEADSUP) { mVisibleType = VISIBLE_TYPE_CONTRACTED; } if (mTransformationStartVisibleType == VISIBLE_TYPE_HEADSUP) { mTransformationStartVisibleType = UNDEFINED; } return; } addView(child); mHeadsUpChild = child; mHeadsUpWrapper = NotificationViewWrapper.wrap(getContext(), child, mContainingNotification); } public void setAmbientChild(View child) { if (mAmbientChild != null) { mAmbientChild.animate().cancel(); removeView(mAmbientChild); } if (child == null) { return; } addView(child); mAmbientChild = child; mAmbientWrapper = NotificationViewWrapper.wrap(getContext(), child, mContainingNotification); } @Override protected void onVisibilityChanged(View changedView, int visibility) { super.onVisibilityChanged(changedView, visibility); updateVisibility(); } private void updateVisibility() { setVisible(isShown()); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); getViewTreeObserver().removeOnPreDrawListener(mEnableAnimationPredrawListener); } private void setVisible(final boolean isVisible) { if (isVisible) { // This call can happen multiple times, but removing only removes a single one. // We therefore need to remove the old one. getViewTreeObserver().removeOnPreDrawListener(mEnableAnimationPredrawListener); // We only animate if we are drawn at least once, otherwise the view might animate when // it's shown the first time getViewTreeObserver().addOnPreDrawListener(mEnableAnimationPredrawListener); } else { getViewTreeObserver().removeOnPreDrawListener(mEnableAnimationPredrawListener); mAnimate = false; } } private void focusExpandButtonIfNecessary() { if (mFocusOnVisibilityChange) { NotificationHeaderView header = getVisibleNotificationHeader(); if (header != null) { ImageView expandButton = header.getExpandButton(); if (expandButton != null) { expandButton.requestAccessibilityFocus(); } } mFocusOnVisibilityChange = false; } } public void setContentHeight(int contentHeight) { mUnrestrictedContentHeight = Math.max(contentHeight, getMinHeight()); int maxContentHeight = mContainingNotification.getIntrinsicHeight() - getExtraRemoteInputHeight(mExpandedRemoteInput) - getExtraRemoteInputHeight(mHeadsUpRemoteInput); mContentHeight = Math.min(mUnrestrictedContentHeight, maxContentHeight); selectLayout(mAnimate /* animate */, false /* force */); int minHeightHint = getMinContentHeightHint(); NotificationViewWrapper wrapper = getVisibleWrapper(mVisibleType); if (wrapper != null) { wrapper.setContentHeight(mUnrestrictedContentHeight, minHeightHint); } wrapper = getVisibleWrapper(mTransformationStartVisibleType); if (wrapper != null) { wrapper.setContentHeight(mUnrestrictedContentHeight, minHeightHint); } updateClipping(); invalidateOutline(); } /** * @return the minimum apparent height that the wrapper should allow for the purpose * of aligning elements at the bottom edge. If this is larger than the content * height, the notification is clipped instead of being further shrunk. */ private int getMinContentHeightHint() { if (mIsChildInGroup && isVisibleOrTransitioning(VISIBLE_TYPE_SINGLELINE)) { return mContext.getResources().getDimensionPixelSize( com.android.internal.R.dimen.notification_action_list_height); } // Transition between heads-up & expanded, or pinned. if (mHeadsUpChild != null && mExpandedChild != null) { boolean transitioningBetweenHunAndExpanded = isTransitioningFromTo(VISIBLE_TYPE_HEADSUP, VISIBLE_TYPE_EXPANDED) || isTransitioningFromTo(VISIBLE_TYPE_EXPANDED, VISIBLE_TYPE_HEADSUP); boolean pinned = !isVisibleOrTransitioning(VISIBLE_TYPE_CONTRACTED) && (mIsHeadsUp || mHeadsUpAnimatingAway) && !mContainingNotification.isOnKeyguard(); if (transitioningBetweenHunAndExpanded || pinned) { return Math.min(getViewHeight(VISIBLE_TYPE_HEADSUP), getViewHeight(VISIBLE_TYPE_EXPANDED)); } } // Size change of the expanded version if ((mVisibleType == VISIBLE_TYPE_EXPANDED) && mContentHeightAtAnimationStart >= 0 && mExpandedChild != null) { return Math.min(mContentHeightAtAnimationStart, getViewHeight(VISIBLE_TYPE_EXPANDED)); } int hint; if (mAmbientChild != null && isVisibleOrTransitioning(VISIBLE_TYPE_AMBIENT)) { hint = mAmbientChild.getHeight(); } else if (mAmbientSingleLineChild != null && isVisibleOrTransitioning( VISIBLE_TYPE_AMBIENT_SINGLELINE)) { hint = mAmbientSingleLineChild.getHeight(); } else if (mHeadsUpChild != null && isVisibleOrTransitioning(VISIBLE_TYPE_HEADSUP)) { hint = getViewHeight(VISIBLE_TYPE_HEADSUP); } else if (mExpandedChild != null) { hint = getViewHeight(VISIBLE_TYPE_EXPANDED); } else { hint = getViewHeight(VISIBLE_TYPE_CONTRACTED) + mContext.getResources().getDimensionPixelSize( com.android.internal.R.dimen.notification_action_list_height); } if (mExpandedChild != null && isVisibleOrTransitioning(VISIBLE_TYPE_EXPANDED)) { hint = Math.min(hint, getViewHeight(VISIBLE_TYPE_EXPANDED)); } return hint; } private boolean isTransitioningFromTo(int from, int to) { return (mTransformationStartVisibleType == from || mAnimationStartVisibleType == from) && mVisibleType == to; } private boolean isVisibleOrTransitioning(int type) { return mVisibleType == type || mTransformationStartVisibleType == type || mAnimationStartVisibleType == type; } private void updateContentTransformation() { int visibleType = calculateVisibleType(); if (visibleType != mVisibleType) { // A new transformation starts mTransformationStartVisibleType = mVisibleType; final TransformableView shownView = getTransformableViewForVisibleType(visibleType); final TransformableView hiddenView = getTransformableViewForVisibleType( mTransformationStartVisibleType); shownView.transformFrom(hiddenView, 0.0f); getViewForVisibleType(visibleType).setVisibility(View.VISIBLE); hiddenView.transformTo(shownView, 0.0f); mVisibleType = visibleType; updateBackgroundColor(true /* animate */); } if (mForceSelectNextLayout) { forceUpdateVisibilities(); } if (mTransformationStartVisibleType != UNDEFINED && mVisibleType != mTransformationStartVisibleType && getViewForVisibleType(mTransformationStartVisibleType) != null) { final TransformableView shownView = getTransformableViewForVisibleType(mVisibleType); final TransformableView hiddenView = getTransformableViewForVisibleType( mTransformationStartVisibleType); float transformationAmount = calculateTransformationAmount(); shownView.transformFrom(hiddenView, transformationAmount); hiddenView.transformTo(shownView, transformationAmount); updateBackgroundTransformation(transformationAmount); } else { updateViewVisibilities(visibleType); updateBackgroundColor(false); } } private void updateBackgroundTransformation(float transformationAmount) { int endColor = getBackgroundColor(mVisibleType); int startColor = getBackgroundColor(mTransformationStartVisibleType); if (endColor != startColor) { if (startColor == 0) { startColor = mContainingNotification.getBackgroundColorWithoutTint(); } if (endColor == 0) { endColor = mContainingNotification.getBackgroundColorWithoutTint(); } endColor = NotificationUtils.interpolateColors(startColor, endColor, transformationAmount); } mContainingNotification.updateBackgroundAlpha(transformationAmount); mContainingNotification.setContentBackground(endColor, false, this); } private float calculateTransformationAmount() { int startHeight = getViewHeight(mTransformationStartVisibleType); int endHeight = getViewHeight(mVisibleType); int progress = Math.abs(mContentHeight - startHeight); int totalDistance = Math.abs(endHeight - startHeight); if (totalDistance == 0) { Log.wtf(TAG, "the total transformation distance is 0" + "\n StartType: " + mTransformationStartVisibleType + " height: " + startHeight + "\n VisibleType: " + mVisibleType + " height: " + endHeight + "\n mContentHeight: " + mContentHeight); return 1.0f; } float amount = (float) progress / (float) totalDistance; return Math.min(1.0f, amount); } public int getContentHeight() { return mContentHeight; } public int getMaxHeight() { if (mContainingNotification.isShowingAmbient()) { return getShowingAmbientView().getHeight(); } else if (mExpandedChild != null) { return getViewHeight(VISIBLE_TYPE_EXPANDED) + getExtraRemoteInputHeight(mExpandedRemoteInput); } else if (mIsHeadsUp && mHeadsUpChild != null && !mContainingNotification.isOnKeyguard()) { return getViewHeight(VISIBLE_TYPE_HEADSUP) + getExtraRemoteInputHeight(mHeadsUpRemoteInput); } return getViewHeight(VISIBLE_TYPE_CONTRACTED); } private int getViewHeight(int visibleType) { View view = getViewForVisibleType(visibleType); int height = view.getHeight(); NotificationViewWrapper viewWrapper = getWrapperForView(view); if (viewWrapper != null) { height += viewWrapper.getHeaderTranslation(); } return height; } public int getMinHeight() { return getMinHeight(false /* likeGroupExpanded */); } public int getMinHeight(boolean likeGroupExpanded) { if (mContainingNotification.isShowingAmbient()) { return getShowingAmbientView().getHeight(); } else if (likeGroupExpanded || !mIsChildInGroup || isGroupExpanded()) { return getViewHeight(VISIBLE_TYPE_CONTRACTED); } else { return mSingleLineView.getHeight(); } } public View getShowingAmbientView() { View v = mIsChildInGroup ? mAmbientSingleLineChild : mAmbientChild; if (v != null) { return v; } else { return mContractedChild; } } private boolean isGroupExpanded() { return mGroupManager.isGroupExpanded(mStatusBarNotification); } public void setClipTopAmount(int clipTopAmount) { mClipTopAmount = clipTopAmount; updateClipping(); } public void setClipBottomAmount(int clipBottomAmount) { mClipBottomAmount = clipBottomAmount; updateClipping(); } @Override public void setTranslationY(float translationY) { super.setTranslationY(translationY); updateClipping(); } private void updateClipping() { if (mClipToActualHeight) { int top = (int) (mClipTopAmount - getTranslationY()); int bottom = (int) (mUnrestrictedContentHeight - mClipBottomAmount - getTranslationY()); bottom = Math.max(top, bottom); mClipBounds.set(0, top, getWidth(), bottom); setClipBounds(mClipBounds); } else { setClipBounds(null); } } public void setClipToActualHeight(boolean clipToActualHeight) { mClipToActualHeight = clipToActualHeight; updateClipping(); } private void selectLayout(boolean animate, boolean force) { if (mContractedChild == null) { return; } if (mUserExpanding) { updateContentTransformation(); } else { int visibleType = calculateVisibleType(); boolean changedType = visibleType != mVisibleType; if (changedType || force) { View visibleView = getViewForVisibleType(visibleType); if (visibleView != null) { visibleView.setVisibility(VISIBLE); transferRemoteInputFocus(visibleType); } if (animate && ((visibleType == VISIBLE_TYPE_EXPANDED && mExpandedChild != null) || (visibleType == VISIBLE_TYPE_HEADSUP && mHeadsUpChild != null) || (visibleType == VISIBLE_TYPE_SINGLELINE && mSingleLineView != null) || visibleType == VISIBLE_TYPE_CONTRACTED)) { animateToVisibleType(visibleType); } else { updateViewVisibilities(visibleType); } mVisibleType = visibleType; if (changedType) { focusExpandButtonIfNecessary(); } NotificationViewWrapper visibleWrapper = getVisibleWrapper(visibleType); if (visibleWrapper != null) { visibleWrapper.setContentHeight(mUnrestrictedContentHeight, getMinContentHeightHint()); } updateBackgroundColor(animate); } } } private void forceUpdateVisibilities() { forceUpdateVisibility(VISIBLE_TYPE_CONTRACTED, mContractedChild, mContractedWrapper); forceUpdateVisibility(VISIBLE_TYPE_EXPANDED, mExpandedChild, mExpandedWrapper); forceUpdateVisibility(VISIBLE_TYPE_HEADSUP, mHeadsUpChild, mHeadsUpWrapper); forceUpdateVisibility(VISIBLE_TYPE_SINGLELINE, mSingleLineView, mSingleLineView); forceUpdateVisibility(VISIBLE_TYPE_AMBIENT, mAmbientChild, mAmbientWrapper); forceUpdateVisibility(VISIBLE_TYPE_AMBIENT_SINGLELINE, mAmbientSingleLineChild, mAmbientSingleLineChild); fireExpandedVisibleListenerIfVisible(); // forceUpdateVisibilities cancels outstanding animations without updating the // mAnimationStartVisibleType. Do so here instead. mAnimationStartVisibleType = UNDEFINED; } private void fireExpandedVisibleListenerIfVisible() { if (mExpandedVisibleListener != null && mExpandedChild != null && isShown() && mExpandedChild.getVisibility() == VISIBLE) { Runnable listener = mExpandedVisibleListener; mExpandedVisibleListener = null; listener.run(); } } private void forceUpdateVisibility(int type, View view, TransformableView wrapper) { if (view == null) { return; } boolean visible = mVisibleType == type || mTransformationStartVisibleType == type; if (!visible) { view.setVisibility(INVISIBLE); } else { wrapper.setVisible(true); } } public void updateBackgroundColor(boolean animate) { int customBackgroundColor = getBackgroundColor(mVisibleType); mContainingNotification.resetBackgroundAlpha(); mContainingNotification.setContentBackground(customBackgroundColor, animate, this); } public void setBackgroundTintColor(int color) { if (mExpandedSmartReplyView != null) { mExpandedSmartReplyView.setBackgroundTintColor(color); } } public int getVisibleType() { return mVisibleType; } public int getBackgroundColorForExpansionState() { // When expanding or user locked we want the new type, when collapsing we want // the original type final int visibleType = (mContainingNotification.isGroupExpanded() || mContainingNotification.isUserLocked()) ? calculateVisibleType() : getVisibleType(); return getBackgroundColor(visibleType); } public int getBackgroundColor(int visibleType) { NotificationViewWrapper currentVisibleWrapper = getVisibleWrapper(visibleType); int customBackgroundColor = 0; if (currentVisibleWrapper != null) { customBackgroundColor = currentVisibleWrapper.getCustomBackgroundColor(); } return customBackgroundColor; } private void updateViewVisibilities(int visibleType) { updateViewVisibility(visibleType, VISIBLE_TYPE_CONTRACTED, mContractedChild, mContractedWrapper); updateViewVisibility(visibleType, VISIBLE_TYPE_EXPANDED, mExpandedChild, mExpandedWrapper); updateViewVisibility(visibleType, VISIBLE_TYPE_HEADSUP, mHeadsUpChild, mHeadsUpWrapper); updateViewVisibility(visibleType, VISIBLE_TYPE_SINGLELINE, mSingleLineView, mSingleLineView); updateViewVisibility(visibleType, VISIBLE_TYPE_AMBIENT, mAmbientChild, mAmbientWrapper); updateViewVisibility(visibleType, VISIBLE_TYPE_AMBIENT_SINGLELINE, mAmbientSingleLineChild, mAmbientSingleLineChild); fireExpandedVisibleListenerIfVisible(); // updateViewVisibilities cancels outstanding animations without updating the // mAnimationStartVisibleType. Do so here instead. mAnimationStartVisibleType = UNDEFINED; } private void updateViewVisibility(int visibleType, int type, View view, TransformableView wrapper) { if (view != null) { wrapper.setVisible(visibleType == type); } } private void animateToVisibleType(int visibleType) { final TransformableView shownView = getTransformableViewForVisibleType(visibleType); final TransformableView hiddenView = getTransformableViewForVisibleType(mVisibleType); if (shownView == hiddenView || hiddenView == null) { shownView.setVisible(true); return; } mAnimationStartVisibleType = mVisibleType; shownView.transformFrom(hiddenView); getViewForVisibleType(visibleType).setVisibility(View.VISIBLE); hiddenView.transformTo(shownView, new Runnable() { @Override public void run() { if (hiddenView != getTransformableViewForVisibleType(mVisibleType)) { hiddenView.setVisible(false); } mAnimationStartVisibleType = UNDEFINED; } }); fireExpandedVisibleListenerIfVisible(); } private void transferRemoteInputFocus(int visibleType) { if (visibleType == VISIBLE_TYPE_HEADSUP && mHeadsUpRemoteInput != null && (mExpandedRemoteInput != null && mExpandedRemoteInput.isActive())) { mHeadsUpRemoteInput.stealFocusFrom(mExpandedRemoteInput); } if (visibleType == VISIBLE_TYPE_EXPANDED && mExpandedRemoteInput != null && (mHeadsUpRemoteInput != null && mHeadsUpRemoteInput.isActive())) { mExpandedRemoteInput.stealFocusFrom(mHeadsUpRemoteInput); } } /** * @param visibleType one of the static enum types in this view * @return the corresponding transformable view according to the given visible type */ private TransformableView getTransformableViewForVisibleType(int visibleType) { switch (visibleType) { case VISIBLE_TYPE_EXPANDED: return mExpandedWrapper; case VISIBLE_TYPE_HEADSUP: return mHeadsUpWrapper; case VISIBLE_TYPE_SINGLELINE: return mSingleLineView; case VISIBLE_TYPE_AMBIENT: return mAmbientWrapper; case VISIBLE_TYPE_AMBIENT_SINGLELINE: return mAmbientSingleLineChild; default: return mContractedWrapper; } } /** * @param visibleType one of the static enum types in this view * @return the corresponding view according to the given visible type */ private View getViewForVisibleType(int visibleType) { switch (visibleType) { case VISIBLE_TYPE_EXPANDED: return mExpandedChild; case VISIBLE_TYPE_HEADSUP: return mHeadsUpChild; case VISIBLE_TYPE_SINGLELINE: return mSingleLineView; case VISIBLE_TYPE_AMBIENT: return mAmbientChild; case VISIBLE_TYPE_AMBIENT_SINGLELINE: return mAmbientSingleLineChild; default: return mContractedChild; } } public NotificationViewWrapper getVisibleWrapper(int visibleType) { switch (visibleType) { case VISIBLE_TYPE_EXPANDED: return mExpandedWrapper; case VISIBLE_TYPE_HEADSUP: return mHeadsUpWrapper; case VISIBLE_TYPE_CONTRACTED: return mContractedWrapper; case VISIBLE_TYPE_AMBIENT: return mAmbientWrapper; default: return null; } } /** * @return one of the static enum types in this view, calculated form the current state */ public int calculateVisibleType() { if (mContainingNotification.isShowingAmbient()) { if (mIsChildInGroup && mAmbientSingleLineChild != null) { return VISIBLE_TYPE_AMBIENT_SINGLELINE; } else if (mAmbientChild != null) { return VISIBLE_TYPE_AMBIENT; } else { return VISIBLE_TYPE_CONTRACTED; } } if (mUserExpanding) { int height = !mIsChildInGroup || isGroupExpanded() || mContainingNotification.isExpanded(true /* allowOnKeyguard */) ? mContainingNotification.getMaxContentHeight() : mContainingNotification.getShowingLayout().getMinHeight(); if (height == 0) { height = mContentHeight; } int expandedVisualType = getVisualTypeForHeight(height); int collapsedVisualType = mIsChildInGroup && !isGroupExpanded() ? VISIBLE_TYPE_SINGLELINE : getVisualTypeForHeight(mContainingNotification.getCollapsedHeight()); return mTransformationStartVisibleType == collapsedVisualType ? expandedVisualType : collapsedVisualType; } int intrinsicHeight = mContainingNotification.getIntrinsicHeight(); int viewHeight = mContentHeight; if (intrinsicHeight != 0) { // the intrinsicHeight might be 0 because it was just reset. viewHeight = Math.min(mContentHeight, intrinsicHeight); } return getVisualTypeForHeight(viewHeight); } private int getVisualTypeForHeight(float viewHeight) { boolean noExpandedChild = mExpandedChild == null; if (!noExpandedChild && viewHeight == getViewHeight(VISIBLE_TYPE_EXPANDED)) { return VISIBLE_TYPE_EXPANDED; } if (!mUserExpanding && mIsChildInGroup && !isGroupExpanded()) { return VISIBLE_TYPE_SINGLELINE; } if ((mIsHeadsUp || mHeadsUpAnimatingAway) && mHeadsUpChild != null && !mContainingNotification.isOnKeyguard()) { if (viewHeight <= getViewHeight(VISIBLE_TYPE_HEADSUP) || noExpandedChild) { return VISIBLE_TYPE_HEADSUP; } else { return VISIBLE_TYPE_EXPANDED; } } else { if (noExpandedChild || (viewHeight <= getViewHeight(VISIBLE_TYPE_CONTRACTED) && (!mIsChildInGroup || isGroupExpanded() || !mContainingNotification.isExpanded(true /* allowOnKeyguard */)))) { return VISIBLE_TYPE_CONTRACTED; } else { return VISIBLE_TYPE_EXPANDED; } } } public boolean isContentExpandable() { return mIsContentExpandable; } public void setDark(boolean dark, boolean fade, long delay) { if (mContractedChild == null) { return; } mDark = dark; selectLayout(!dark && fade /* animate */, false /* force */); } public void setHeadsUp(boolean headsUp) { mIsHeadsUp = headsUp; selectLayout(false /* animate */, true /* force */); updateExpandButtons(mExpandable); } @Override public boolean hasOverlappingRendering() { // This is not really true, but good enough when fading from the contracted to the expanded // layout, and saves us some layers. return false; } public void setLegacy(boolean legacy) { mLegacy = legacy; updateLegacy(); } private void updateLegacy() { if (mContractedChild != null) { mContractedWrapper.setLegacy(mLegacy); } if (mExpandedChild != null) { mExpandedWrapper.setLegacy(mLegacy); } if (mHeadsUpChild != null) { mHeadsUpWrapper.setLegacy(mLegacy); } } public void setIsChildInGroup(boolean isChildInGroup) { mIsChildInGroup = isChildInGroup; if (mContractedChild != null) { mContractedWrapper.setIsChildInGroup(mIsChildInGroup); } if (mExpandedChild != null) { mExpandedWrapper.setIsChildInGroup(mIsChildInGroup); } if (mHeadsUpChild != null) { mHeadsUpWrapper.setIsChildInGroup(mIsChildInGroup); } if (mAmbientChild != null) { mAmbientWrapper.setIsChildInGroup(mIsChildInGroup); } updateAllSingleLineViews(); } public void onNotificationUpdated(NotificationData.Entry entry) { mStatusBarNotification = entry.notification; mBeforeN = entry.targetSdk < Build.VERSION_CODES.N; updateAllSingleLineViews(); if (mContractedChild != null) { mContractedWrapper.onContentUpdated(entry.row); } if (mExpandedChild != null) { mExpandedWrapper.onContentUpdated(entry.row); } if (mHeadsUpChild != null) { mHeadsUpWrapper.onContentUpdated(entry.row); } if (mAmbientChild != null) { mAmbientWrapper.onContentUpdated(entry.row); } applyRemoteInputAndSmartReply(entry); updateLegacy(); mForceSelectNextLayout = true; setDark(mDark, false /* animate */, 0 /* delay */); mPreviousExpandedRemoteInputIntent = null; mPreviousHeadsUpRemoteInputIntent = null; } private void updateAllSingleLineViews() { updateSingleLineView(); updateAmbientSingleLineView(); } private void updateSingleLineView() { if (mIsChildInGroup) { boolean isNewView = mSingleLineView == null; mSingleLineView = mHybridGroupManager.bindFromNotification( mSingleLineView, mStatusBarNotification.getNotification()); if (isNewView) { updateViewVisibility(mVisibleType, VISIBLE_TYPE_SINGLELINE, mSingleLineView, mSingleLineView); } } else if (mSingleLineView != null) { removeView(mSingleLineView); mSingleLineView = null; } } private void updateAmbientSingleLineView() { if (mIsChildInGroup) { boolean isNewView = mAmbientSingleLineChild == null; mAmbientSingleLineChild = mHybridGroupManager.bindAmbientFromNotification( mAmbientSingleLineChild, mStatusBarNotification.getNotification()); if (isNewView) { updateViewVisibility(mVisibleType, VISIBLE_TYPE_AMBIENT_SINGLELINE, mAmbientSingleLineChild, mAmbientSingleLineChild); } } else if (mAmbientSingleLineChild != null) { removeView(mAmbientSingleLineChild); mAmbientSingleLineChild = null; } } private void applyRemoteInputAndSmartReply(final NotificationData.Entry entry) { if (mRemoteInputController == null) { return; } boolean enableSmartReplies = (mSmartReplyConstants.isEnabled() && (!mSmartReplyConstants.requiresTargetingP() || entry.targetSdk >= Build.VERSION_CODES.P)); boolean hasRemoteInput = false; RemoteInput remoteInputWithChoices = null; PendingIntent pendingIntentWithChoices = null; Notification.Action[] actions = entry.notification.getNotification().actions; if (actions != null) { for (Notification.Action a : actions) { if (a.getRemoteInputs() != null) { for (RemoteInput ri : a.getRemoteInputs()) { boolean showRemoteInputView = ri.getAllowFreeFormInput(); boolean showSmartReplyView = enableSmartReplies && ri.getChoices() != null && ri.getChoices().length > 0; if (showRemoteInputView) { hasRemoteInput = true; } if (showSmartReplyView) { remoteInputWithChoices = ri; pendingIntentWithChoices = a.actionIntent; } if (showRemoteInputView || showSmartReplyView) { break; } } } } } applyRemoteInput(entry, hasRemoteInput); applySmartReplyView(remoteInputWithChoices, pendingIntentWithChoices, entry); } private void applyRemoteInput(NotificationData.Entry entry, boolean hasRemoteInput) { View bigContentView = mExpandedChild; if (bigContentView != null) { mExpandedRemoteInput = applyRemoteInput(bigContentView, entry, hasRemoteInput, mPreviousExpandedRemoteInputIntent, mCachedExpandedRemoteInput, mExpandedWrapper); } else { mExpandedRemoteInput = null; } if (mCachedExpandedRemoteInput != null && mCachedExpandedRemoteInput != mExpandedRemoteInput) { // We had a cached remote input but didn't reuse it. Clean up required. mCachedExpandedRemoteInput.dispatchFinishTemporaryDetach(); } mCachedExpandedRemoteInput = null; View headsUpContentView = mHeadsUpChild; if (headsUpContentView != null) { mHeadsUpRemoteInput = applyRemoteInput(headsUpContentView, entry, hasRemoteInput, mPreviousHeadsUpRemoteInputIntent, mCachedHeadsUpRemoteInput, mHeadsUpWrapper); } else { mHeadsUpRemoteInput = null; } if (mCachedHeadsUpRemoteInput != null && mCachedHeadsUpRemoteInput != mHeadsUpRemoteInput) { // We had a cached remote input but didn't reuse it. Clean up required. mCachedHeadsUpRemoteInput.dispatchFinishTemporaryDetach(); } mCachedHeadsUpRemoteInput = null; } private RemoteInputView applyRemoteInput(View view, NotificationData.Entry entry, boolean hasRemoteInput, PendingIntent existingPendingIntent, RemoteInputView cachedView, NotificationViewWrapper wrapper) { View actionContainerCandidate = view.findViewById( com.android.internal.R.id.actions_container); if (actionContainerCandidate instanceof FrameLayout) { RemoteInputView existing = (RemoteInputView) view.findViewWithTag(RemoteInputView.VIEW_TAG); if (existing != null) { existing.onNotificationUpdateOrReset(); } if (existing == null && hasRemoteInput) { ViewGroup actionContainer = (FrameLayout) actionContainerCandidate; if (cachedView == null) { RemoteInputView riv = RemoteInputView.inflate( mContext, actionContainer, entry, mRemoteInputController); riv.setVisibility(View.INVISIBLE); actionContainer.addView(riv, new LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) ); existing = riv; } else { actionContainer.addView(cachedView); cachedView.dispatchFinishTemporaryDetach(); cachedView.requestFocus(); existing = cachedView; } } if (hasRemoteInput) { int color = entry.notification.getNotification().color; if (color == Notification.COLOR_DEFAULT) { color = mContext.getColor(R.color.default_remote_input_background); } existing.setBackgroundColor(NotificationColorUtil.ensureTextBackgroundColor(color, mContext.getColor(R.color.remote_input_text_enabled), mContext.getColor(R.color.remote_input_hint))); existing.setWrapper(wrapper); existing.setOnVisibilityChangedListener(this::setRemoteInputVisible); if (existingPendingIntent != null || existing.isActive()) { // The current action could be gone, or the pending intent no longer valid. // If we find a matching action in the new notification, focus, otherwise close. Notification.Action[] actions = entry.notification.getNotification().actions; if (existingPendingIntent != null) { existing.setPendingIntent(existingPendingIntent); } if (existing.updatePendingIntentFromActions(actions)) { if (!existing.isActive()) { existing.focus(); } } else { if (existing.isActive()) { existing.close(); } } } } return existing; } return null; } private void applySmartReplyView(RemoteInput remoteInput, PendingIntent pendingIntent, NotificationData.Entry entry) { if (mExpandedChild != null) { mExpandedSmartReplyView = applySmartReplyView(mExpandedChild, remoteInput, pendingIntent, entry); if (mExpandedSmartReplyView != null && remoteInput != null && remoteInput.getChoices() != null && remoteInput.getChoices().length > 0) { mSmartReplyController.smartRepliesAdded(entry, remoteInput.getChoices().length); } } } private SmartReplyView applySmartReplyView( View view, RemoteInput remoteInput, PendingIntent pendingIntent, NotificationData.Entry entry) { View smartReplyContainerCandidate = view.findViewById( com.android.internal.R.id.smart_reply_container); if (!(smartReplyContainerCandidate instanceof LinearLayout)) { return null; } LinearLayout smartReplyContainer = (LinearLayout) smartReplyContainerCandidate; if (remoteInput == null || pendingIntent == null) { smartReplyContainer.setVisibility(View.GONE); return null; } // If we are showing the spinner we don't want to add the buttons. boolean showingSpinner = entry.notification.getNotification() .extras.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false); if (showingSpinner) { smartReplyContainer.setVisibility(View.GONE); return null; } // If we are keeping the notification around while sending we don't want to add the buttons. boolean hideSmartReplies = entry.notification.getNotification() .extras.getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false); if (hideSmartReplies) { smartReplyContainer.setVisibility(View.GONE); return null; } SmartReplyView smartReplyView = null; if (smartReplyContainer.getChildCount() == 0) { smartReplyView = SmartReplyView.inflate(mContext, smartReplyContainer); smartReplyContainer.addView(smartReplyView); } else if (smartReplyContainer.getChildCount() == 1) { View child = smartReplyContainer.getChildAt(0); if (child instanceof SmartReplyView) { smartReplyView = (SmartReplyView) child; } } if (smartReplyView != null) { smartReplyView.setRepliesFromRemoteInput(remoteInput, pendingIntent, mSmartReplyController, entry, smartReplyContainer); smartReplyContainer.setVisibility(View.VISIBLE); } return smartReplyView; } public void closeRemoteInput() { if (mHeadsUpRemoteInput != null) { mHeadsUpRemoteInput.close(); } if (mExpandedRemoteInput != null) { mExpandedRemoteInput.close(); } } public void setGroupManager(NotificationGroupManager groupManager) { mGroupManager = groupManager; } public void setRemoteInputController(RemoteInputController r) { mRemoteInputController = r; } public void setExpandClickListener(OnClickListener expandClickListener) { mExpandClickListener = expandClickListener; } public void updateExpandButtons(boolean expandable) { mExpandable = expandable; // if the expanded child has the same height as the collapsed one we hide it. if (mExpandedChild != null && mExpandedChild.getHeight() != 0) { if ((!mIsHeadsUp && !mHeadsUpAnimatingAway) || mHeadsUpChild == null || mContainingNotification.isOnKeyguard()) { if (mExpandedChild.getHeight() <= mContractedChild.getHeight()) { expandable = false; } } else if (mExpandedChild.getHeight() <= mHeadsUpChild.getHeight()) { expandable = false; } } if (mExpandedChild != null) { mExpandedWrapper.updateExpandability(expandable, mExpandClickListener); } if (mContractedChild != null) { mContractedWrapper.updateExpandability(expandable, mExpandClickListener); } if (mHeadsUpChild != null) { mHeadsUpWrapper.updateExpandability(expandable, mExpandClickListener); } mIsContentExpandable = expandable; } public NotificationHeaderView getNotificationHeader() { NotificationHeaderView header = null; if (mContractedChild != null) { header = mContractedWrapper.getNotificationHeader(); } if (header == null && mExpandedChild != null) { header = mExpandedWrapper.getNotificationHeader(); } if (header == null && mHeadsUpChild != null) { header = mHeadsUpWrapper.getNotificationHeader(); } if (header == null && mAmbientChild != null) { header = mAmbientWrapper.getNotificationHeader(); } return header; } public void showAppOpsIcons(ArraySet activeOps) { if (mContractedChild != null && mContractedWrapper.getNotificationHeader() != null) { mContractedWrapper.getNotificationHeader().showAppOpsIcons(activeOps); } if (mExpandedChild != null && mExpandedWrapper.getNotificationHeader() != null) { mExpandedWrapper.getNotificationHeader().showAppOpsIcons(activeOps); } if (mHeadsUpChild != null && mHeadsUpWrapper.getNotificationHeader() != null) { mHeadsUpWrapper.getNotificationHeader().showAppOpsIcons(activeOps); } } public NotificationHeaderView getContractedNotificationHeader() { if (mContractedChild != null) { return mContractedWrapper.getNotificationHeader(); } return null; } public NotificationHeaderView getVisibleNotificationHeader() { NotificationViewWrapper wrapper = getVisibleWrapper(mVisibleType); return wrapper == null ? null : wrapper.getNotificationHeader(); } public void setContainingNotification(ExpandableNotificationRow containingNotification) { mContainingNotification = containingNotification; } public void requestSelectLayout(boolean needsAnimation) { selectLayout(needsAnimation, false); } public void reInflateViews() { if (mIsChildInGroup && mSingleLineView != null) { removeView(mSingleLineView); mSingleLineView = null; updateAllSingleLineViews(); } } public void setUserExpanding(boolean userExpanding) { mUserExpanding = userExpanding; if (userExpanding) { mTransformationStartVisibleType = mVisibleType; } else { mTransformationStartVisibleType = UNDEFINED; mVisibleType = calculateVisibleType(); updateViewVisibilities(mVisibleType); updateBackgroundColor(false); } } /** * Set by how much the single line view should be indented. Used when a overflow indicator is * present and only during measuring */ public void setSingleLineWidthIndention(int singleLineWidthIndention) { if (singleLineWidthIndention != mSingleLineWidthIndention) { mSingleLineWidthIndention = singleLineWidthIndention; mContainingNotification.forceLayout(); forceLayout(); } } public HybridNotificationView getSingleLineView() { return mSingleLineView; } public void setRemoved() { if (mExpandedRemoteInput != null) { mExpandedRemoteInput.setRemoved(); } if (mHeadsUpRemoteInput != null) { mHeadsUpRemoteInput.setRemoved(); } } public void setContentHeightAnimating(boolean animating) { if (!animating) { mContentHeightAtAnimationStart = UNDEFINED; } } @VisibleForTesting boolean isAnimatingVisibleType() { return mAnimationStartVisibleType != UNDEFINED; } public void setHeadsUpAnimatingAway(boolean headsUpAnimatingAway) { mHeadsUpAnimatingAway = headsUpAnimatingAway; selectLayout(false /* animate */, true /* force */); } public void setFocusOnVisibilityChange() { mFocusOnVisibilityChange = true; } public void setIconsVisible(boolean iconsVisible) { mIconsVisible = iconsVisible; updateIconVisibilities(); } private void updateIconVisibilities() { if (mContractedWrapper != null) { NotificationHeaderView header = mContractedWrapper.getNotificationHeader(); if (header != null) { header.getIcon().setForceHidden(!mIconsVisible); } } if (mHeadsUpWrapper != null) { NotificationHeaderView header = mHeadsUpWrapper.getNotificationHeader(); if (header != null) { header.getIcon().setForceHidden(!mIconsVisible); } } if (mExpandedWrapper != null) { NotificationHeaderView header = mExpandedWrapper.getNotificationHeader(); if (header != null) { header.getIcon().setForceHidden(!mIconsVisible); } } } @Override public void onVisibilityAggregated(boolean isVisible) { super.onVisibilityAggregated(isVisible); if (isVisible) { fireExpandedVisibleListenerIfVisible(); } } /** * Sets a one-shot listener for when the expanded view becomes visible. * * This will fire the listener immediately if the expanded view is already visible. */ public void setOnExpandedVisibleListener(Runnable r) { mExpandedVisibleListener = r; fireExpandedVisibleListenerIfVisible(); } public void setIsLowPriority(boolean isLowPriority) { mIsLowPriority = isLowPriority; } public boolean isDimmable() { if (!mContractedWrapper.isDimmable()) { return false; } return true; } /** * Should a single click be disallowed on this view when on the keyguard? */ public boolean disallowSingleClick(float x, float y) { NotificationViewWrapper visibleWrapper = getVisibleWrapper(getVisibleType()); if (visibleWrapper != null) { return visibleWrapper.disallowSingleClick(x, y); } return false; } public boolean shouldClipToRounding(boolean topRounded, boolean bottomRounded) { boolean needsPaddings = shouldClipToRounding(getVisibleType(), topRounded, bottomRounded); if (mUserExpanding) { needsPaddings |= shouldClipToRounding(mTransformationStartVisibleType, topRounded, bottomRounded); } return needsPaddings; } private boolean shouldClipToRounding(int visibleType, boolean topRounded, boolean bottomRounded) { NotificationViewWrapper visibleWrapper = getVisibleWrapper(visibleType); if (visibleWrapper == null) { return false; } return visibleWrapper.shouldClipToRounding(topRounded, bottomRounded); } public CharSequence getActiveRemoteInputText() { if (mExpandedRemoteInput != null && mExpandedRemoteInput.isActive()) { return mExpandedRemoteInput.getText(); } if (mHeadsUpRemoteInput != null && mHeadsUpRemoteInput.isActive()) { return mHeadsUpRemoteInput.getText(); } return null; } @Override public boolean dispatchTouchEvent(MotionEvent ev) { float y = ev.getY(); // We still want to distribute touch events to the remote input even if it's outside the // view boundary. We're therefore manually dispatching these events to the remote view RemoteInputView riv = getRemoteInputForView(getViewForVisibleType(mVisibleType)); if (riv != null && riv.getVisibility() == VISIBLE) { int inputStart = mUnrestrictedContentHeight - riv.getHeight(); if (y <= mUnrestrictedContentHeight && y >= inputStart) { ev.offsetLocation(0, -inputStart); return riv.dispatchTouchEvent(ev); } } return super.dispatchTouchEvent(ev); } /** * Overridden to make sure touches to the reply action bar actually go through to this view */ @Override public boolean pointInView(float localX, float localY, float slop) { float top = mClipTopAmount; float bottom = mUnrestrictedContentHeight; return localX >= -slop && localY >= top - slop && localX < ((mRight - mLeft) + slop) && localY < (bottom + slop); } private RemoteInputView getRemoteInputForView(View child) { if (child == mExpandedChild) { return mExpandedRemoteInput; } else if (child == mHeadsUpChild) { return mHeadsUpRemoteInput; } return null; } public int getExpandHeight() { int viewType = VISIBLE_TYPE_EXPANDED; if (mExpandedChild == null) { viewType = VISIBLE_TYPE_CONTRACTED; } return getViewHeight(viewType) + getExtraRemoteInputHeight(mExpandedRemoteInput); } public int getHeadsUpHeight() { int viewType = VISIBLE_TYPE_HEADSUP; if (mHeadsUpChild == null) { viewType = VISIBLE_TYPE_CONTRACTED; } // The headsUp remote input quickly switches to the expanded one, so lets also include that // one return getViewHeight(viewType) + getExtraRemoteInputHeight(mHeadsUpRemoteInput) + getExtraRemoteInputHeight(mExpandedRemoteInput); } public void setRemoteInputVisible(boolean remoteInputVisible) { mRemoteInputVisible = remoteInputVisible; setClipChildren(!remoteInputVisible); } @Override public void setClipChildren(boolean clipChildren) { clipChildren = clipChildren && !mRemoteInputVisible; super.setClipChildren(clipChildren); } public void setHeaderVisibleAmount(float headerVisibleAmount) { if (mContractedWrapper != null) { mContractedWrapper.setHeaderVisibleAmount(headerVisibleAmount); } if (mHeadsUpWrapper != null) { mHeadsUpWrapper.setHeaderVisibleAmount(headerVisibleAmount); } if (mExpandedWrapper != null) { mExpandedWrapper.setHeaderVisibleAmount(headerVisibleAmount); } } }