/* * Copyright (C) 2015 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.notification.stack; import android.app.Notification; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Path; import android.graphics.Path.Direction; import android.graphics.drawable.ColorDrawable; import android.os.Trace; import android.service.notification.StatusBarNotification; import android.util.AttributeSet; import android.util.Log; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.NotificationHeaderView; import android.view.View; import android.view.ViewGroup; import android.widget.RemoteViews; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.widget.NotificationExpandButton; import com.android.systemui.R; import com.android.systemui.statusbar.CrossFadeHelper; import com.android.systemui.statusbar.NotificationGroupingUtil; import com.android.systemui.statusbar.NotificationShelf; import com.android.systemui.statusbar.notification.FeedbackIcon; import com.android.systemui.statusbar.notification.LegacySourceType; import com.android.systemui.statusbar.notification.NotificationFadeAware; import com.android.systemui.statusbar.notification.NotificationUtils; import com.android.systemui.statusbar.notification.Roundable; import com.android.systemui.statusbar.notification.RoundableState; import com.android.systemui.statusbar.notification.SourceType; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; import com.android.systemui.statusbar.notification.row.HybridGroupManager; import com.android.systemui.statusbar.notification.row.HybridNotificationView; import com.android.systemui.statusbar.notification.row.wrapper.NotificationHeaderViewWrapper; import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; import java.util.ArrayList; import java.util.List; /** * A container containing child notifications */ public class NotificationChildrenContainer extends ViewGroup implements NotificationFadeAware, Roundable { private static final String TAG = "NotificationChildrenContainer"; @VisibleForTesting static final int NUMBER_OF_CHILDREN_WHEN_COLLAPSED = 2; @VisibleForTesting static final int NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED = 5; public static final int NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED = 8; private static final AnimationProperties ALPHA_FADE_IN = new AnimationProperties() { private AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha(); @Override public AnimationFilter getAnimationFilter() { return mAnimationFilter; } }.setDuration(200); private static final SourceType FROM_PARENT = SourceType.from("FromParent(NCC)"); private final List mDividers = new ArrayList<>(); private final List mAttachedChildren = new ArrayList<>(); private final HybridGroupManager mHybridGroupManager; private int mChildPadding; private int mDividerHeight; private float mDividerAlpha; private int mNotificationHeaderMargin; private int mNotificationTopPadding; private float mCollapsedBottomPadding; private boolean mChildrenExpanded; private ExpandableNotificationRow mContainingNotification; private TextView mOverflowNumber; private ViewState mGroupOverFlowState; private int mRealHeight; private boolean mUserLocked; private int mActualHeight; private boolean mNeverAppliedGroupState; private int mHeaderHeight; /** * Whether or not individual notifications that are part of this container will have shadows. */ private boolean mEnableShadowOnChildNotifications; private NotificationHeaderView mNotificationHeader; private NotificationHeaderViewWrapper mNotificationHeaderWrapper; private NotificationHeaderView mNotificationHeaderLowPriority; private NotificationHeaderViewWrapper mNotificationHeaderWrapperLowPriority; private NotificationGroupingUtil mGroupingUtil; private ViewState mHeaderViewState; private int mClipBottomAmount; private boolean mIsLowPriority; private OnClickListener mHeaderClickListener; private ViewGroup mCurrentHeader; private boolean mIsConversation; private Path mChildClipPath = null; private final Path mHeaderPath = new Path(); private boolean mShowGroupCountInExpander; private boolean mShowDividersWhenExpanded; private boolean mHideDividersDuringExpand; private int mTranslationForHeader; private int mCurrentHeaderTranslation = 0; private float mHeaderVisibleAmount = 1.0f; private int mUntruncatedChildCount; private boolean mContainingNotificationIsFaded = false; private RoundableState mRoundableState; private boolean mUseRoundnessSourceTypes; public NotificationChildrenContainer(Context context) { this(context, null); } public NotificationChildrenContainer(Context context, AttributeSet attrs) { this(context, attrs, 0); } public NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } public NotificationChildrenContainer( Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); mHybridGroupManager = new HybridGroupManager(getContext()); mRoundableState = new RoundableState(this, this, 0f); initDimens(); setClipChildren(false); } private void initDimens() { Resources res = getResources(); mChildPadding = res.getDimensionPixelOffset(R.dimen.notification_children_padding); mDividerHeight = res.getDimensionPixelOffset( R.dimen.notification_children_container_divider_height); mDividerAlpha = res.getFloat(R.dimen.notification_divider_alpha); mNotificationHeaderMargin = res.getDimensionPixelOffset( R.dimen.notification_children_container_margin_top); mNotificationTopPadding = res.getDimensionPixelOffset( R.dimen.notification_children_container_top_padding); mHeaderHeight = mNotificationHeaderMargin + mNotificationTopPadding; mCollapsedBottomPadding = res.getDimensionPixelOffset( R.dimen.notification_children_collapsed_bottom_padding); mEnableShadowOnChildNotifications = res.getBoolean(R.bool.config_enableShadowOnChildNotifications); mShowGroupCountInExpander = res.getBoolean(R.bool.config_showNotificationGroupCountInExpander); mShowDividersWhenExpanded = res.getBoolean(R.bool.config_showDividersWhenGroupNotificationExpanded); mHideDividersDuringExpand = res.getBoolean(R.bool.config_hideDividersDuringExpand); mTranslationForHeader = res.getDimensionPixelOffset( com.android.internal.R.dimen.notification_content_margin) - mNotificationHeaderMargin; mHybridGroupManager.initDimens(); } @NonNull @Override public RoundableState getRoundableState() { return mRoundableState; } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int childCount = Math.min(mAttachedChildren.size(), NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED); for (int i = 0; i < childCount; i++) { View child = mAttachedChildren.get(i); // We need to layout all children even the GONE ones, such that the heights are // calculated correctly as they are used to calculate how many we can fit on the screen child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight()); mDividers.get(i).layout(0, 0, getWidth(), mDividerHeight); } if (mOverflowNumber != null) { boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL; int left = (isRtl ? 0 : getWidth() - mOverflowNumber.getMeasuredWidth()); int right = left + mOverflowNumber.getMeasuredWidth(); mOverflowNumber.layout(left, 0, right, mOverflowNumber.getMeasuredHeight()); } if (mNotificationHeader != null) { mNotificationHeader.layout(0, 0, mNotificationHeader.getMeasuredWidth(), mNotificationHeader.getMeasuredHeight()); } if (mNotificationHeaderLowPriority != null) { mNotificationHeaderLowPriority.layout(0, 0, mNotificationHeaderLowPriority.getMeasuredWidth(), mNotificationHeaderLowPriority.getMeasuredHeight()); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { Trace.beginSection("NotificationChildrenContainer#onMeasure"); int heightMode = MeasureSpec.getMode(heightMeasureSpec); boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY; boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST; int size = MeasureSpec.getSize(heightMeasureSpec); int newHeightSpec = heightMeasureSpec; if (hasFixedHeight || isHeightLimited) { newHeightSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST); } int width = MeasureSpec.getSize(widthMeasureSpec); if (mOverflowNumber != null) { mOverflowNumber.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), newHeightSpec); } int dividerHeightSpec = MeasureSpec.makeMeasureSpec(mDividerHeight, MeasureSpec.EXACTLY); int height = mNotificationHeaderMargin + mNotificationTopPadding; int childCount = Math.min(mAttachedChildren.size(), NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED); int collapsedChildren = getMaxAllowedVisibleChildren(true /* likeCollapsed */); int overflowIndex = childCount > collapsedChildren ? collapsedChildren - 1 : -1; for (int i = 0; i < childCount; i++) { ExpandableNotificationRow child = mAttachedChildren.get(i); // We need to measure all children even the GONE ones, such that the heights are // calculated correctly as they are used to calculate how many we can fit on the screen. boolean isOverflow = i == overflowIndex; child.setSingleLineWidthIndention(isOverflow && mOverflowNumber != null ? mOverflowNumber.getMeasuredWidth() : 0); child.measure(widthMeasureSpec, newHeightSpec); // layout the divider View divider = mDividers.get(i); divider.measure(widthMeasureSpec, dividerHeightSpec); if (child.getVisibility() != GONE) { height += child.getMeasuredHeight() + mDividerHeight; } } mRealHeight = height; if (heightMode != MeasureSpec.UNSPECIFIED) { height = Math.min(height, size); } int headerHeightSpec = MeasureSpec.makeMeasureSpec(mHeaderHeight, MeasureSpec.EXACTLY); if (mNotificationHeader != null) { mNotificationHeader.measure(widthMeasureSpec, headerHeightSpec); } if (mNotificationHeaderLowPriority != null) { mNotificationHeaderLowPriority.measure(widthMeasureSpec, headerHeightSpec); } setMeasuredDimension(width, height); Trace.endSection(); } @Override public boolean hasOverlappingRendering() { return false; } @Override public boolean pointInView(float localX, float localY, float slop) { return localX >= -slop && localY >= -slop && localX < ((mRight - mLeft) + slop) && localY < (mRealHeight + slop); } /** * Set the untruncated number of children in the group so that the view can update the UI * appropriately. Note that this may differ from the number of views attached as truncated * children will not have views. */ public void setUntruncatedChildCount(int childCount) { mUntruncatedChildCount = childCount; updateGroupOverflow(); } /** * Add a child notification to this view. * * @param row the row to add * @param childIndex the index to add it at, if -1 it will be added at the end */ public void addNotification(ExpandableNotificationRow row, int childIndex) { ensureRemovedFromTransientContainer(row); int newIndex = childIndex < 0 ? mAttachedChildren.size() : childIndex; mAttachedChildren.add(newIndex, row); addView(row); row.setUserLocked(mUserLocked); View divider = inflateDivider(); addView(divider); mDividers.add(newIndex, divider); row.setContentTransformationAmount(0, false /* isLastChild */); row.setNotificationFaded(mContainingNotificationIsFaded); if (!mUseRoundnessSourceTypes) { // This is a workaround, the NotificationShelf should be the owner of `OnScroll` // roundness. // Here we should reset the `OnScroll` roundness only on top-level rows. NotificationShelf.resetLegacyOnScrollRoundness(row); } // It doesn't make sense to keep old animations around, lets cancel them! ExpandableViewState viewState = row.getViewState(); if (viewState != null) { viewState.cancelAnimations(row); row.cancelAppearDrawing(); } if (mUseRoundnessSourceTypes) { applyRoundnessAndInvalidate(); } } private void ensureRemovedFromTransientContainer(View v) { if (v.getParent() != null && v instanceof ExpandableView) { // If the child is animating away, it will still have a parent, so detach it first // TODO: We should really cancel the active animations here. This will // happen automatically when the view's intro animation starts, but // it's a fragile link. ((ExpandableView) v).removeFromTransientContainerForAdditionTo(this); } } public void removeNotification(ExpandableNotificationRow row) { int childIndex = mAttachedChildren.indexOf(row); mAttachedChildren.remove(row); removeView(row); final View divider = mDividers.remove(childIndex); removeView(divider); getOverlay().add(divider); CrossFadeHelper.fadeOut(divider, new Runnable() { @Override public void run() { getOverlay().remove(divider); } }); row.setSystemChildExpanded(false); row.setNotificationFaded(false); row.setUserLocked(false); if (!row.isRemoved()) { mGroupingUtil.restoreChildNotification(row); } if (mUseRoundnessSourceTypes) { row.requestRoundnessReset(FROM_PARENT, /* animate = */ false); applyRoundnessAndInvalidate(); } } /** * @return The number of notification children in the container. */ public int getNotificationChildCount() { return mAttachedChildren.size(); } public void recreateNotificationHeader(OnClickListener listener, boolean isConversation) { mHeaderClickListener = listener; mIsConversation = isConversation; StatusBarNotification notification = mContainingNotification.getEntry().getSbn(); final Notification.Builder builder = Notification.Builder.recoverBuilder(getContext(), notification.getNotification()); RemoteViews header = builder.makeNotificationGroupHeader(); if (mNotificationHeader == null) { mNotificationHeader = (NotificationHeaderView) header.apply(getContext(), this); mNotificationHeader.findViewById(com.android.internal.R.id.expand_button) .setVisibility(VISIBLE); mNotificationHeader.setOnClickListener(mHeaderClickListener); mNotificationHeaderWrapper = (NotificationHeaderViewWrapper) NotificationViewWrapper.wrap( getContext(), mNotificationHeader, mContainingNotification); mNotificationHeaderWrapper.useRoundnessSourceTypes(mUseRoundnessSourceTypes); if (mUseRoundnessSourceTypes) { mNotificationHeaderWrapper.setOnRoundnessChangedListener(this::invalidate); } addView(mNotificationHeader, 0); invalidate(); } else { header.reapply(getContext(), mNotificationHeader); } mNotificationHeaderWrapper.setExpanded(mChildrenExpanded); mNotificationHeaderWrapper.onContentUpdated(mContainingNotification); recreateLowPriorityHeader(builder, isConversation); updateHeaderVisibility(false /* animate */); updateChildrenAppearance(); } /** * Recreate the low-priority header. * * @param builder a builder to reuse. Otherwise the builder will be recovered. */ private void recreateLowPriorityHeader(Notification.Builder builder, boolean isConversation) { RemoteViews header; StatusBarNotification notification = mContainingNotification.getEntry().getSbn(); if (mIsLowPriority) { if (builder == null) { builder = Notification.Builder.recoverBuilder(getContext(), notification.getNotification()); } header = builder.makeLowPriorityContentView(true /* useRegularSubtext */); if (mNotificationHeaderLowPriority == null) { mNotificationHeaderLowPriority = (NotificationHeaderView) header.apply(getContext(), this); mNotificationHeaderLowPriority.findViewById(com.android.internal.R.id.expand_button) .setVisibility(VISIBLE); mNotificationHeaderLowPriority.setOnClickListener(mHeaderClickListener); mNotificationHeaderWrapperLowPriority = (NotificationHeaderViewWrapper) NotificationViewWrapper.wrap( getContext(), mNotificationHeaderLowPriority, mContainingNotification); mNotificationHeaderWrapperLowPriority.useRoundnessSourceTypes( mUseRoundnessSourceTypes ); if (mUseRoundnessSourceTypes) { mNotificationHeaderWrapper.setOnRoundnessChangedListener(this::invalidate); } addView(mNotificationHeaderLowPriority, 0); invalidate(); } else { header.reapply(getContext(), mNotificationHeaderLowPriority); } mNotificationHeaderWrapperLowPriority.onContentUpdated(mContainingNotification); resetHeaderVisibilityIfNeeded(mNotificationHeaderLowPriority, calculateDesiredHeader()); } else { removeView(mNotificationHeaderLowPriority); mNotificationHeaderLowPriority = null; mNotificationHeaderWrapperLowPriority = null; } } /** * Update the appearance of the children to reduce redundancies. */ public void updateChildrenAppearance() { mGroupingUtil.updateChildrenAppearance(); } private void setExpandButtonNumber(NotificationViewWrapper wrapper) { View expandButton = wrapper == null ? null : wrapper.getExpandButton(); if (expandButton instanceof NotificationExpandButton) { ((NotificationExpandButton) expandButton).setNumber(mUntruncatedChildCount); } } public void updateGroupOverflow() { if (mShowGroupCountInExpander) { setExpandButtonNumber(mNotificationHeaderWrapper); setExpandButtonNumber(mNotificationHeaderWrapperLowPriority); return; } int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* likeCollapsed */); if (mUntruncatedChildCount > maxAllowedVisibleChildren) { int number = mUntruncatedChildCount - maxAllowedVisibleChildren; mOverflowNumber = mHybridGroupManager.bindOverflowNumber(mOverflowNumber, number, this); if (mGroupOverFlowState == null) { mGroupOverFlowState = new ViewState(); mNeverAppliedGroupState = true; } } else if (mOverflowNumber != null) { removeView(mOverflowNumber); if (isShown() && isAttachedToWindow()) { final View removedOverflowNumber = mOverflowNumber; addTransientView(removedOverflowNumber, getTransientViewCount()); CrossFadeHelper.fadeOut(removedOverflowNumber, new Runnable() { @Override public void run() { removeTransientView(removedOverflowNumber); } }); } mOverflowNumber = null; mGroupOverFlowState = null; } } @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); updateGroupOverflow(); } private View inflateDivider() { return LayoutInflater.from(mContext).inflate( R.layout.notification_children_divider, this, false); } /** * Get notification children that are attached currently. */ public List getAttachedChildren() { return mAttachedChildren; } /** * Sets the alpha on the content, while leaving the background of the container itself as is. * * @param alpha alpha value to apply to the content */ public void setContentAlpha(float alpha) { for (int i = 0; i < mNotificationHeader.getChildCount(); i++) { mNotificationHeader.getChildAt(i).setAlpha(alpha); } for (ExpandableNotificationRow child : getAttachedChildren()) { child.setContentAlpha(alpha); } } /** * To be called any time the rows have been updated */ public void updateExpansionStates() { if (mChildrenExpanded || mUserLocked) { // we don't modify it the group is expanded or if we are expanding it return; } int size = mAttachedChildren.size(); for (int i = 0; i < size; i++) { ExpandableNotificationRow child = mAttachedChildren.get(i); child.setSystemChildExpanded(i == 0 && size == 1); } } /** * @return the intrinsic size of this children container, i.e the natural fully expanded state */ public int getIntrinsicHeight() { int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(); return getIntrinsicHeight(maxAllowedVisibleChildren); } /** * @return the intrinsic height with a number of children given * in @param maxAllowedVisibleChildren */ private int getIntrinsicHeight(float maxAllowedVisibleChildren) { if (showingAsLowPriority()) { return mNotificationHeaderLowPriority.getHeight(); } int intrinsicHeight = mNotificationHeaderMargin + mCurrentHeaderTranslation; int visibleChildren = 0; int childCount = mAttachedChildren.size(); boolean firstChild = true; float expandFactor = 0; if (mUserLocked) { expandFactor = getGroupExpandFraction(); } boolean childrenExpanded = mChildrenExpanded; for (int i = 0; i < childCount; i++) { if (visibleChildren >= maxAllowedVisibleChildren) { break; } if (!firstChild) { if (mUserLocked) { intrinsicHeight += NotificationUtils.interpolate(mChildPadding, mDividerHeight, expandFactor); } else { intrinsicHeight += childrenExpanded ? mDividerHeight : mChildPadding; } } else { if (mUserLocked) { intrinsicHeight += NotificationUtils.interpolate( 0, mNotificationTopPadding + mDividerHeight, expandFactor); } else { intrinsicHeight += childrenExpanded ? mNotificationTopPadding + mDividerHeight : 0; } firstChild = false; } ExpandableNotificationRow child = mAttachedChildren.get(i); intrinsicHeight += child.getIntrinsicHeight(); visibleChildren++; } if (mUserLocked) { intrinsicHeight += NotificationUtils.interpolate(mCollapsedBottomPadding, 0.0f, expandFactor); } else if (!childrenExpanded) { intrinsicHeight += mCollapsedBottomPadding; } return intrinsicHeight; } /** * Update the state of all its children based on a linear layout algorithm. * * @param parentState the state of the parent */ public void updateState(ExpandableViewState parentState) { int childCount = mAttachedChildren.size(); int yPosition = mNotificationHeaderMargin + mCurrentHeaderTranslation; boolean firstChild = true; int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(); int lastVisibleIndex = maxAllowedVisibleChildren - 1; int firstOverflowIndex = lastVisibleIndex + 1; float expandFactor = 0; boolean expandingToExpandedGroup = mUserLocked && !showingAsLowPriority(); if (mUserLocked) { expandFactor = getGroupExpandFraction(); firstOverflowIndex = getMaxAllowedVisibleChildren(true /* likeCollapsed */); } boolean childrenExpandedAndNotAnimating = mChildrenExpanded && !mContainingNotification.isGroupExpansionChanging(); int launchTransitionCompensation = 0; for (int i = 0; i < childCount; i++) { ExpandableNotificationRow child = mAttachedChildren.get(i); if (!firstChild) { if (expandingToExpandedGroup) { yPosition += NotificationUtils.interpolate(mChildPadding, mDividerHeight, expandFactor); } else { yPosition += mChildrenExpanded ? mDividerHeight : mChildPadding; } } else { if (expandingToExpandedGroup) { yPosition += NotificationUtils.interpolate( 0, mNotificationTopPadding + mDividerHeight, expandFactor); } else { yPosition += mChildrenExpanded ? mNotificationTopPadding + mDividerHeight : 0; } firstChild = false; } ExpandableViewState childState = child.getViewState(); int intrinsicHeight = child.getIntrinsicHeight(); childState.height = intrinsicHeight; childState.setYTranslation(yPosition + launchTransitionCompensation); childState.hidden = false; if (child.isExpandAnimationRunning() || mContainingNotification.hasExpandingChild()) { // Not modifying translationZ during launch animation. The translationZ of the // expanding child is handled inside ExpandableNotificationRow and the translationZ // of the other children inside the group should remain unchanged. In particular, // they should not take over the translationZ of the parent, since the parent has // a positive translationZ set only for the expanding child to be drawn above other // notifications. childState.setZTranslation(child.getTranslationZ()); } else if (childrenExpandedAndNotAnimating && mEnableShadowOnChildNotifications) { // When the group is expanded, the children cast the shadows rather than the parent // so use the parent's elevation here. childState.setZTranslation(parentState.getZTranslation()); } else { childState.setZTranslation(0); } childState.dimmed = parentState.dimmed; childState.hideSensitive = parentState.hideSensitive; childState.belowSpeedBump = parentState.belowSpeedBump; childState.clipTopAmount = 0; childState.setAlpha(0); if (i < firstOverflowIndex) { childState.setAlpha(showingAsLowPriority() ? expandFactor : 1.0f); } else if (expandFactor == 1.0f && i <= lastVisibleIndex) { childState.setAlpha( (mActualHeight - childState.getYTranslation()) / childState.height); childState.setAlpha(Math.max(0.0f, Math.min(1.0f, childState.getAlpha()))); } childState.location = parentState.location; childState.inShelf = parentState.inShelf; yPosition += intrinsicHeight; } if (mOverflowNumber != null) { ExpandableNotificationRow overflowView = mAttachedChildren.get(Math.min( getMaxAllowedVisibleChildren(true /* likeCollapsed */), childCount) - 1); mGroupOverFlowState.copyFrom(overflowView.getViewState()); if (!mChildrenExpanded) { HybridNotificationView alignView = overflowView.getSingleLineView(); if (alignView != null) { View mirrorView = alignView.getTextView(); if (mirrorView.getVisibility() == GONE) { mirrorView = alignView.getTitleView(); } if (mirrorView.getVisibility() == GONE) { mirrorView = alignView; } mGroupOverFlowState.setAlpha(mirrorView.getAlpha()); float yTranslation = mGroupOverFlowState.getYTranslation() + NotificationUtils.getRelativeYOffset( mirrorView, overflowView); mGroupOverFlowState.setYTranslation(yTranslation); } } else { mGroupOverFlowState.setYTranslation( mGroupOverFlowState.getYTranslation() + mNotificationHeaderMargin); mGroupOverFlowState.setAlpha(0.0f); } } if (mNotificationHeader != null) { if (mHeaderViewState == null) { mHeaderViewState = new ViewState(); } mHeaderViewState.initFrom(mNotificationHeader); if (mContainingNotification.hasExpandingChild()) { // Not modifying translationZ during expand animation. mHeaderViewState.setZTranslation(mNotificationHeader.getTranslationZ()); } else if (childrenExpandedAndNotAnimating) { mHeaderViewState.setZTranslation(parentState.getZTranslation()); } else { mHeaderViewState.setZTranslation(0); } mHeaderViewState.setYTranslation(mCurrentHeaderTranslation); mHeaderViewState.setAlpha(mHeaderVisibleAmount); // The hiding is done automatically by the alpha, otherwise we'll pick it up again // in the next frame with the initFrom call above and have an invisible header mHeaderViewState.hidden = false; } } /** * When moving into the bottom stack, the bottom visible child in an expanded group adjusts its * height, children in the group after this are gone. * * @param child the child who's height to adjust. * @param parentHeight the height of the parent. * @param childState the state to update. * @param yPosition the yPosition of the view. * @return true if children after this one should be hidden. */ private boolean updateChildStateForExpandedGroup( ExpandableNotificationRow child, int parentHeight, ExpandableViewState childState, int yPosition) { final int top = yPosition + child.getClipTopAmount(); final int intrinsicHeight = child.getIntrinsicHeight(); final int bottom = top + intrinsicHeight; int newHeight = intrinsicHeight; if (bottom >= parentHeight) { // Child is either clipped or gone newHeight = Math.max((parentHeight - top), 0); } childState.hidden = newHeight == 0; childState.height = newHeight; return childState.height != intrinsicHeight && !childState.hidden; } @VisibleForTesting int getMaxAllowedVisibleChildren() { return getMaxAllowedVisibleChildren(false /* likeCollapsed */); } @VisibleForTesting int getMaxAllowedVisibleChildren(boolean likeCollapsed) { if (!likeCollapsed && (mChildrenExpanded || mContainingNotification.isUserLocked()) && !showingAsLowPriority()) { return NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED; } if (mIsLowPriority || (!mContainingNotification.isOnKeyguard() && mContainingNotification.isExpanded()) || (mContainingNotification.isHeadsUpState() && mContainingNotification.canShowHeadsUp())) { return NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED; } return NUMBER_OF_CHILDREN_WHEN_COLLAPSED; } /** * Applies state to children. */ public void applyState() { int childCount = mAttachedChildren.size(); ViewState tmpState = new ViewState(); float expandFraction = 0.0f; if (mUserLocked) { expandFraction = getGroupExpandFraction(); } final boolean isExpanding = !showingAsLowPriority() && (mUserLocked || mContainingNotification.isGroupExpansionChanging()); final boolean dividersVisible = (mChildrenExpanded && mShowDividersWhenExpanded) || (isExpanding && !mHideDividersDuringExpand); for (int i = 0; i < childCount; i++) { ExpandableNotificationRow child = mAttachedChildren.get(i); ExpandableViewState viewState = child.getViewState(); viewState.applyToView(child); // layout the divider View divider = mDividers.get(i); tmpState.initFrom(divider); tmpState.setYTranslation(viewState.getYTranslation() - mDividerHeight); float alpha = mChildrenExpanded && viewState.getAlpha() != 0 ? mDividerAlpha : 0; if (mUserLocked && !showingAsLowPriority() && viewState.getAlpha() != 0) { alpha = NotificationUtils.interpolate(0, mDividerAlpha, Math.min(viewState.getAlpha(), expandFraction)); } tmpState.hidden = !dividersVisible; tmpState.setAlpha(alpha); tmpState.applyToView(divider); // There is no fake shadow to be drawn on the children child.setFakeShadowIntensity(0.0f, 0.0f, 0, 0); } if (mGroupOverFlowState != null) { mGroupOverFlowState.applyToView(mOverflowNumber); mNeverAppliedGroupState = false; } if (mHeaderViewState != null) { mHeaderViewState.applyToView(mNotificationHeader); } updateChildrenClipping(); } private void updateChildrenClipping() { if (mContainingNotification.hasExpandingChild()) { return; } int childCount = mAttachedChildren.size(); int layoutEnd = mContainingNotification.getActualHeight() - mClipBottomAmount; for (int i = 0; i < childCount; i++) { ExpandableNotificationRow child = mAttachedChildren.get(i); if (child.getVisibility() == GONE) { continue; } float childTop = child.getTranslationY(); float childBottom = childTop + child.getActualHeight(); boolean visible = true; int clipBottomAmount = 0; if (childTop > layoutEnd) { visible = false; } else if (childBottom > layoutEnd) { clipBottomAmount = (int) (childBottom - layoutEnd); } boolean isVisible = child.getVisibility() == VISIBLE; if (visible != isVisible) { child.setVisibility(visible ? VISIBLE : INVISIBLE); } child.setClipBottomAmount(clipBottomAmount); } } @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { boolean isCanvasChanged = false; Path clipPath = mChildClipPath; if (clipPath != null) { final float translation; if (child instanceof ExpandableNotificationRow) { ExpandableNotificationRow notificationRow = (ExpandableNotificationRow) child; translation = notificationRow.getTranslation(); } else { translation = child.getTranslationX(); } isCanvasChanged = true; canvas.save(); if (mUseRoundnessSourceTypes && translation != 0f) { clipPath.offset(translation, 0f); canvas.clipPath(clipPath); clipPath.offset(-translation, 0f); } else { canvas.clipPath(clipPath); } } if (child instanceof NotificationHeaderView && mNotificationHeaderWrapper.hasRoundedCorner()) { float[] radii = mNotificationHeaderWrapper.getUpdatedRadii(); mHeaderPath.reset(); mHeaderPath.addRoundRect( child.getLeft(), child.getTop(), child.getRight(), child.getBottom(), radii, Direction.CW ); if (!isCanvasChanged) { isCanvasChanged = true; canvas.save(); } canvas.clipPath(mHeaderPath); } if (isCanvasChanged) { boolean result = super.drawChild(canvas, child, drawingTime); canvas.restore(); return result; } else { // If there have been no changes to the canvas we can proceed as usual return super.drawChild(canvas, child, drawingTime); } } /** * This is called when the children expansion has changed and positions the children properly * for an appear animation. */ public void prepareExpansionChanged() { // TODO: do something that makes sense, like placing the invisible views correctly return; } /** * Animate to a given state. */ public void startAnimationToState(AnimationProperties properties) { int childCount = mAttachedChildren.size(); ViewState tmpState = new ViewState(); float expandFraction = getGroupExpandFraction(); final boolean isExpanding = !showingAsLowPriority() && (mUserLocked || mContainingNotification.isGroupExpansionChanging()); final boolean dividersVisible = (mChildrenExpanded && mShowDividersWhenExpanded) || (isExpanding && !mHideDividersDuringExpand); for (int i = childCount - 1; i >= 0; i--) { ExpandableNotificationRow child = mAttachedChildren.get(i); ExpandableViewState viewState = child.getViewState(); viewState.animateTo(child, properties); // layout the divider View divider = mDividers.get(i); tmpState.initFrom(divider); tmpState.setYTranslation(viewState.getYTranslation() - mDividerHeight); float alpha = mChildrenExpanded && viewState.getAlpha() != 0 ? mDividerAlpha : 0; if (mUserLocked && !showingAsLowPriority() && viewState.getAlpha() != 0) { alpha = NotificationUtils.interpolate(0, mDividerAlpha, Math.min(viewState.getAlpha(), expandFraction)); } tmpState.hidden = !dividersVisible; tmpState.setAlpha(alpha); tmpState.animateTo(divider, properties); // There is no fake shadow to be drawn on the children child.setFakeShadowIntensity(0.0f, 0.0f, 0, 0); } if (mOverflowNumber != null) { if (mNeverAppliedGroupState) { float alpha = mGroupOverFlowState.getAlpha(); mGroupOverFlowState.setAlpha(0); mGroupOverFlowState.applyToView(mOverflowNumber); mGroupOverFlowState.setAlpha(alpha); mNeverAppliedGroupState = false; } mGroupOverFlowState.animateTo(mOverflowNumber, properties); } if (mNotificationHeader != null) { mHeaderViewState.applyToView(mNotificationHeader); } updateChildrenClipping(); } public ExpandableNotificationRow getViewAtPosition(float y) { // find the view under the pointer, accounting for GONE views final int count = mAttachedChildren.size(); for (int childIdx = 0; childIdx < count; childIdx++) { ExpandableNotificationRow slidingChild = mAttachedChildren.get(childIdx); float childTop = slidingChild.getTranslationY(); float top = childTop + Math.max(0, slidingChild.getClipTopAmount()); float bottom = childTop + slidingChild.getActualHeight(); if (y >= top && y <= bottom) { return slidingChild; } } return null; } public void setChildrenExpanded(boolean childrenExpanded) { mChildrenExpanded = childrenExpanded; updateExpansionStates(); if (mNotificationHeaderWrapper != null) { mNotificationHeaderWrapper.setExpanded(childrenExpanded); } final int count = mAttachedChildren.size(); for (int childIdx = 0; childIdx < count; childIdx++) { ExpandableNotificationRow child = mAttachedChildren.get(childIdx); child.setChildrenExpanded(childrenExpanded, false); } updateHeaderTouchability(); } public void setContainingNotification(ExpandableNotificationRow parent) { mContainingNotification = parent; mGroupingUtil = new NotificationGroupingUtil(mContainingNotification); } public ExpandableNotificationRow getContainingNotification() { return mContainingNotification; } public NotificationViewWrapper getNotificationViewWrapper() { return mNotificationHeaderWrapper; } public NotificationViewWrapper getLowPriorityViewWrapper() { return mNotificationHeaderWrapperLowPriority; } @VisibleForTesting public ViewGroup getCurrentHeaderView() { return mCurrentHeader; } private void updateHeaderVisibility(boolean animate) { ViewGroup desiredHeader; ViewGroup currentHeader = mCurrentHeader; desiredHeader = calculateDesiredHeader(); if (currentHeader == desiredHeader) { return; } if (animate) { if (desiredHeader != null && currentHeader != null) { currentHeader.setVisibility(VISIBLE); desiredHeader.setVisibility(VISIBLE); NotificationViewWrapper visibleWrapper = getWrapperForView(desiredHeader); NotificationViewWrapper hiddenWrapper = getWrapperForView(currentHeader); visibleWrapper.transformFrom(hiddenWrapper); hiddenWrapper.transformTo(visibleWrapper, () -> updateHeaderVisibility(false)); startChildAlphaAnimations(desiredHeader == mNotificationHeader); } else { animate = false; } } if (!animate) { if (desiredHeader != null) { getWrapperForView(desiredHeader).setVisible(true); desiredHeader.setVisibility(VISIBLE); } if (currentHeader != null) { // Wrapper can be null if we were a low priority notification // and just destroyed it by calling setIsLowPriority(false) NotificationViewWrapper wrapper = getWrapperForView(currentHeader); if (wrapper != null) { wrapper.setVisible(false); } currentHeader.setVisibility(INVISIBLE); } } resetHeaderVisibilityIfNeeded(mNotificationHeader, desiredHeader); resetHeaderVisibilityIfNeeded(mNotificationHeaderLowPriority, desiredHeader); mCurrentHeader = desiredHeader; } private void resetHeaderVisibilityIfNeeded(View header, View desiredHeader) { if (header == null) { return; } if (header != mCurrentHeader && header != desiredHeader) { getWrapperForView(header).setVisible(false); header.setVisibility(INVISIBLE); } if (header == desiredHeader && header.getVisibility() != VISIBLE) { getWrapperForView(header).setVisible(true); header.setVisibility(VISIBLE); } } private ViewGroup calculateDesiredHeader() { ViewGroup desiredHeader; if (showingAsLowPriority()) { desiredHeader = mNotificationHeaderLowPriority; } else { desiredHeader = mNotificationHeader; } return desiredHeader; } private void startChildAlphaAnimations(boolean toVisible) { float target = toVisible ? 1.0f : 0.0f; float start = 1.0f - target; int childCount = mAttachedChildren.size(); for (int i = 0; i < childCount; i++) { if (i >= NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED) { break; } ExpandableNotificationRow child = mAttachedChildren.get(i); child.setAlpha(start); ViewState viewState = new ViewState(); viewState.initFrom(child); viewState.setAlpha(target); ALPHA_FADE_IN.setDelay(i * 50); viewState.animateTo(child, ALPHA_FADE_IN); } } private void updateHeaderTransformation() { if (mUserLocked && showingAsLowPriority()) { float fraction = getGroupExpandFraction(); mNotificationHeaderWrapper.transformFrom(mNotificationHeaderWrapperLowPriority, fraction); mNotificationHeader.setVisibility(VISIBLE); mNotificationHeaderWrapperLowPriority.transformTo(mNotificationHeaderWrapper, fraction); } } private NotificationViewWrapper getWrapperForView(View visibleHeader) { if (visibleHeader == mNotificationHeader) { return mNotificationHeaderWrapper; } return mNotificationHeaderWrapperLowPriority; } /** * Called when a groups expansion changes to adjust the background of the header view. * * @param expanded whether the group is expanded. */ public void updateHeaderForExpansion(boolean expanded) { if (mNotificationHeader != null) { if (expanded) { ColorDrawable cd = new ColorDrawable(); cd.setColor(mContainingNotification.calculateBgColor()); mNotificationHeader.setHeaderBackgroundDrawable(cd); } else { mNotificationHeader.setHeaderBackgroundDrawable(null); } } } public int getMaxContentHeight() { if (showingAsLowPriority()) { return getMinHeight(NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED, true /* likeHighPriority */); } int maxContentHeight = mNotificationHeaderMargin + mCurrentHeaderTranslation + mNotificationTopPadding; int visibleChildren = 0; int childCount = mAttachedChildren.size(); for (int i = 0; i < childCount; i++) { if (visibleChildren >= NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED) { break; } ExpandableNotificationRow child = mAttachedChildren.get(i); float childHeight = child.isExpanded(true /* allowOnKeyguard */) ? child.getMaxExpandHeight() : child.getShowingLayout().getMinHeight(true /* likeGroupExpanded */); maxContentHeight += childHeight; visibleChildren++; } if (visibleChildren > 0) { maxContentHeight += visibleChildren * mDividerHeight; } return maxContentHeight; } public void setActualHeight(int actualHeight) { if (!mUserLocked) { return; } mActualHeight = actualHeight; float fraction = getGroupExpandFraction(); boolean showingLowPriority = showingAsLowPriority(); updateHeaderTransformation(); int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* forceCollapsed */); int childCount = mAttachedChildren.size(); for (int i = 0; i < childCount; i++) { ExpandableNotificationRow child = mAttachedChildren.get(i); float childHeight; if (showingLowPriority) { childHeight = child.getShowingLayout().getMinHeight(false /* likeGroupExpanded */); } else if (child.isExpanded(true /* allowOnKeyguard */)) { childHeight = child.getMaxExpandHeight(); } else { childHeight = child.getShowingLayout().getMinHeight( true /* likeGroupExpanded */); } if (i < maxAllowedVisibleChildren) { float singleLineHeight = child.getShowingLayout().getMinHeight( false /* likeGroupExpanded */); child.setActualHeight((int) NotificationUtils.interpolate(singleLineHeight, childHeight, fraction), false); } else { child.setActualHeight((int) childHeight, false); } } } public float getGroupExpandFraction() { int visibleChildrenExpandedHeight = showingAsLowPriority() ? getMaxContentHeight() : getVisibleChildrenExpandHeight(); int minExpandHeight = getCollapsedHeight(); float factor = (mActualHeight - minExpandHeight) / (float) (visibleChildrenExpandedHeight - minExpandHeight); return Math.max(0.0f, Math.min(1.0f, factor)); } private int getVisibleChildrenExpandHeight() { int intrinsicHeight = mNotificationHeaderMargin + mCurrentHeaderTranslation + mNotificationTopPadding + mDividerHeight; int visibleChildren = 0; int childCount = mAttachedChildren.size(); int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* forceCollapsed */); for (int i = 0; i < childCount; i++) { if (visibleChildren >= maxAllowedVisibleChildren) { break; } ExpandableNotificationRow child = mAttachedChildren.get(i); float childHeight = child.isExpanded(true /* allowOnKeyguard */) ? child.getMaxExpandHeight() : child.getShowingLayout().getMinHeight(true /* likeGroupExpanded */); intrinsicHeight += childHeight; visibleChildren++; } return intrinsicHeight; } public int getMinHeight() { return getMinHeight(NUMBER_OF_CHILDREN_WHEN_COLLAPSED, false /* likeHighPriority */); } public int getCollapsedHeight() { return getMinHeight(getMaxAllowedVisibleChildren(true /* forceCollapsed */), false /* likeHighPriority */); } public int getCollapsedHeightWithoutHeader() { return getMinHeight(getMaxAllowedVisibleChildren(true /* forceCollapsed */), false /* likeHighPriority */, 0); } /** * Get the minimum Height for this group. * * @param maxAllowedVisibleChildren the number of children that should be visible * @param likeHighPriority if the height should be calculated as if it were not low * priority */ private int getMinHeight(int maxAllowedVisibleChildren, boolean likeHighPriority) { return getMinHeight(maxAllowedVisibleChildren, likeHighPriority, mCurrentHeaderTranslation); } /** * Get the minimum Height for this group. * * @param maxAllowedVisibleChildren the number of children that should be visible * @param likeHighPriority if the height should be calculated as if it were not low * priority * @param headerTranslation the translation amount of the header */ private int getMinHeight( int maxAllowedVisibleChildren, boolean likeHighPriority, int headerTranslation) { if (!likeHighPriority && showingAsLowPriority()) { if (mNotificationHeaderLowPriority == null) { Log.e(TAG, "getMinHeight: low priority header is null", new Exception()); return 0; } return mNotificationHeaderLowPriority.getHeight(); } int minExpandHeight = mNotificationHeaderMargin + headerTranslation; int visibleChildren = 0; boolean firstChild = true; int childCount = mAttachedChildren.size(); for (int i = 0; i < childCount; i++) { if (visibleChildren >= maxAllowedVisibleChildren) { break; } if (!firstChild) { minExpandHeight += mChildPadding; } else { firstChild = false; } ExpandableNotificationRow child = mAttachedChildren.get(i); View singleLineView = child.getSingleLineView(); if (singleLineView != null) { minExpandHeight += singleLineView.getHeight(); } else { Log.e(TAG, "getMinHeight: child " + child + " single line view is null", new Exception()); } visibleChildren++; } minExpandHeight += mCollapsedBottomPadding; return minExpandHeight; } public boolean showingAsLowPriority() { return mIsLowPriority && !mContainingNotification.isExpanded(); } public void reInflateViews(OnClickListener listener, StatusBarNotification notification) { if (mNotificationHeader != null) { removeView(mNotificationHeader); mNotificationHeader = null; } if (mNotificationHeaderLowPriority != null) { removeView(mNotificationHeaderLowPriority); mNotificationHeaderLowPriority = null; } recreateNotificationHeader(listener, mIsConversation); initDimens(); for (int i = 0; i < mDividers.size(); i++) { View prevDivider = mDividers.get(i); int index = indexOfChild(prevDivider); removeView(prevDivider); View divider = inflateDivider(); addView(divider, index); mDividers.set(i, divider); } removeView(mOverflowNumber); mOverflowNumber = null; mGroupOverFlowState = null; updateGroupOverflow(); } public void setUserLocked(boolean userLocked) { mUserLocked = userLocked; if (!mUserLocked) { updateHeaderVisibility(false /* animate */); } int childCount = mAttachedChildren.size(); for (int i = 0; i < childCount; i++) { ExpandableNotificationRow child = mAttachedChildren.get(i); child.setUserLocked(userLocked && !showingAsLowPriority()); } updateHeaderTouchability(); } private void updateHeaderTouchability() { if (mNotificationHeader != null) { mNotificationHeader.setAcceptAllTouches(mChildrenExpanded || mUserLocked); } } public void onNotificationUpdated() { if (mShowGroupCountInExpander) { // The overflow number is not used, so its color is irrelevant; skip this return; } int color = mContainingNotification.getNotificationColor(); Resources.Theme theme = new ContextThemeWrapper(mContext, com.android.internal.R.style.Theme_DeviceDefault_DayNight).getTheme(); try (TypedArray ta = theme.obtainStyledAttributes( new int[]{com.android.internal.R.attr.colorAccent})) { color = ta.getColor(0, color); } mHybridGroupManager.setOverflowNumberColor(mOverflowNumber, color); } public int getPositionInLinearLayout(View childInGroup) { int position = mNotificationHeaderMargin + mCurrentHeaderTranslation + mNotificationTopPadding; for (int i = 0; i < mAttachedChildren.size(); i++) { ExpandableNotificationRow child = mAttachedChildren.get(i); boolean notGone = child.getVisibility() != View.GONE; if (notGone) { position += mDividerHeight; } if (child == childInGroup) { return position; } if (notGone) { position += child.getIntrinsicHeight(); } } return 0; } public void setClipBottomAmount(int clipBottomAmount) { mClipBottomAmount = clipBottomAmount; updateChildrenClipping(); } public void setIsLowPriority(boolean isLowPriority) { mIsLowPriority = isLowPriority; if (mContainingNotification != null) { /* we're not yet set up yet otherwise */ recreateLowPriorityHeader(null /* existingBuilder */, mIsConversation); updateHeaderVisibility(false /* animate */); } if (mUserLocked) { setUserLocked(mUserLocked); } } /** * @return the view wrapper for the currently showing priority. */ public NotificationViewWrapper getVisibleWrapper() { if (showingAsLowPriority()) { return mNotificationHeaderWrapperLowPriority; } return mNotificationHeaderWrapper; } public void onExpansionChanged() { if (mIsLowPriority) { if (mUserLocked) { setUserLocked(mUserLocked); } updateHeaderVisibility(true /* animate */); } } @VisibleForTesting public boolean isUserLocked() { return mUserLocked; } @Override public void applyRoundnessAndInvalidate() { boolean last = true; if (mUseRoundnessSourceTypes) { if (mNotificationHeaderWrapper != null) { mNotificationHeaderWrapper.requestTopRoundness( /* value = */ getTopRoundness(), /* sourceType = */ FROM_PARENT, /* animate = */ false ); } if (mNotificationHeaderWrapperLowPriority != null) { mNotificationHeaderWrapperLowPriority.requestTopRoundness( /* value = */ getTopRoundness(), /* sourceType = */ FROM_PARENT, /* animate = */ false ); } } for (int i = mAttachedChildren.size() - 1; i >= 0; i--) { ExpandableNotificationRow child = mAttachedChildren.get(i); if (child.getVisibility() == View.GONE) { continue; } if (mUseRoundnessSourceTypes) { child.requestRoundness( /* top = */ 0f, /* bottom = */ last ? getBottomRoundness() : 0f, /* sourceType = */ FROM_PARENT, /* animate = */ false); } else { child.requestRoundness( /* top = */ 0f, /* bottom = */ last ? getBottomRoundness() : 0f, LegacySourceType.DefaultValue, /* animate = */ isShown()); } last = false; } Roundable.super.applyRoundnessAndInvalidate(); } public void setHeaderVisibleAmount(float headerVisibleAmount) { mHeaderVisibleAmount = headerVisibleAmount; mCurrentHeaderTranslation = (int) ((1.0f - headerVisibleAmount) * mTranslationForHeader); } /** * Shows the given feedback icon, or hides the icon if null. */ public void setFeedbackIcon(@Nullable FeedbackIcon icon) { if (mNotificationHeaderWrapper != null) { mNotificationHeaderWrapper.setFeedbackIcon(icon); } if (mNotificationHeaderWrapperLowPriority != null) { mNotificationHeaderWrapperLowPriority.setFeedbackIcon(icon); } } public void setRecentlyAudiblyAlerted(boolean audiblyAlertedRecently) { if (mNotificationHeaderWrapper != null) { mNotificationHeaderWrapper.setRecentlyAudiblyAlerted(audiblyAlertedRecently); } if (mNotificationHeaderWrapperLowPriority != null) { mNotificationHeaderWrapperLowPriority.setRecentlyAudiblyAlerted(audiblyAlertedRecently); } } @Override public void setNotificationFaded(boolean faded) { mContainingNotificationIsFaded = faded; if (mNotificationHeaderWrapper != null) { mNotificationHeaderWrapper.setNotificationFaded(faded); } if (mNotificationHeaderWrapperLowPriority != null) { mNotificationHeaderWrapperLowPriority.setNotificationFaded(faded); } for (ExpandableNotificationRow child : mAttachedChildren) { child.setNotificationFaded(faded); } } /** * Allow to define a path the clip the children in #drawChild() * * @param childClipPath path used to clip the children */ public void setChildClipPath(@Nullable Path childClipPath) { mChildClipPath = childClipPath; invalidate(); } public NotificationHeaderViewWrapper getNotificationHeaderWrapper() { return mNotificationHeaderWrapper; } /** * Enable the support for rounded corner based on the SourceType * * @param enabled true if is supported */ public void useRoundnessSourceTypes(boolean enabled) { mUseRoundnessSourceTypes = enabled; } @Override public String toString() { String roundableStateDebug = "RoundableState = " + getRoundableState().debugString(); return "NotificationChildrenContainer:" + hashCode() + " { " + roundableStateDebug + " }"; } }