• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 package com.android.systemui.statusbar;
18 
19 import static com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerProgress;
20 import static com.android.systemui.util.ColorUtilKt.hexColorString;
21 
22 import android.content.Context;
23 import android.content.res.Configuration;
24 import android.content.res.Resources;
25 import android.graphics.Rect;
26 import android.os.Bundle;
27 import android.util.AttributeSet;
28 import android.util.IndentingPrintWriter;
29 import android.util.MathUtils;
30 import android.view.View;
31 import android.view.ViewGroup;
32 import android.view.ViewTreeObserver;
33 import android.view.accessibility.AccessibilityNodeInfo;
34 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
35 import android.view.animation.Interpolator;
36 import android.view.animation.PathInterpolator;
37 
38 import androidx.annotation.NonNull;
39 
40 import com.android.app.animation.Interpolators;
41 import com.android.internal.annotations.VisibleForTesting;
42 import com.android.internal.policy.SystemBarUtils;
43 import com.android.systemui.animation.ShadeInterpolation;
44 import com.android.systemui.res.R;
45 import com.android.systemui.scene.shared.flag.SceneContainerFlag;
46 import com.android.systemui.shade.transition.LargeScreenShadeInterpolator;
47 import com.android.systemui.statusbar.notification.ColorUpdateLogger;
48 import com.android.systemui.statusbar.notification.NotificationUtils;
49 import com.android.systemui.statusbar.notification.SourceType;
50 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
51 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
52 import com.android.systemui.statusbar.notification.row.ExpandableView;
53 import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
54 import com.android.systemui.statusbar.notification.shared.NotificationMinimalism;
55 import com.android.systemui.statusbar.notification.shelf.NotificationShelfBackgroundView;
56 import com.android.systemui.statusbar.notification.shelf.NotificationShelfIconContainer;
57 import com.android.systemui.statusbar.notification.stack.AmbientState;
58 import com.android.systemui.statusbar.notification.stack.AnimationProperties;
59 import com.android.systemui.statusbar.notification.stack.ExpandableViewState;
60 import com.android.systemui.statusbar.notification.stack.NotificationRoundnessManager;
61 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
62 import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm;
63 import com.android.systemui.statusbar.notification.stack.ViewState;
64 import com.android.systemui.statusbar.phone.NotificationIconContainer;
65 import com.android.systemui.util.DumpUtilsKt;
66 
67 import java.io.PrintWriter;
68 
69 /**
70  * A notification shelf view that is placed inside the notification scroller. It manages the
71  * overflow icons that don't fit into the regular list anymore.
72  */
73 public class NotificationShelf extends ActivatableNotificationView {
74 
75     private static final int TAG_CONTINUOUS_CLIPPING = R.id.continuous_clipping_tag;
76     private static final String TAG = "NotificationShelf";
77 
78     // More extreme version of SLOW_OUT_LINEAR_IN which keeps the icon nearly invisible until after
79     // the next icon has translated out of the way, to avoid overlapping.
80     private static final Interpolator ICON_ALPHA_INTERPOLATOR =
81             new PathInterpolator(0.6f, 0f, 0.6f, 0f);
82     private static final SourceType BASE_VALUE = SourceType.from("BaseValue");
83     private static final SourceType SHELF_SCROLL = SourceType.from("ShelfScroll");
84 
85     @VisibleForTesting
86     public NotificationShelfIconContainer mShelfIcons;
87     // This field hides mBackgroundNormal from super class for short-shelf alignment
88     @VisibleForTesting
89     public NotificationShelfBackgroundView mBackgroundNormal;
90     private boolean mHideBackground;
91     private int mStatusBarHeight;
92     private boolean mEnableNotificationClipping;
93     private AmbientState mAmbientState;
94     private int mPaddingBetweenElements;
95     private int mNotGoneIndex;
96     private boolean mHasItemsInStableShelf;
97     private boolean mAlignedToEnd;
98     private int mScrollFastThreshold;
99     private boolean mInteractive;
100     private boolean mAnimationsEnabled = true;
101     private boolean mShowNotificationShelf;
102     private final Rect mClipRect = new Rect();
103     private int mIndexOfFirstViewInShelf = -1;
104     private float mCornerAnimationDistance;
105     private float mActualWidth = -1;
106     private int mMaxIconsOnLockscreen;
107     private boolean mCanModifyColorOfNotifications;
108     private boolean mCanInteract;
109     private NotificationStackScrollLayout mHostLayout;
110     private NotificationRoundnessManager mRoundnessManager;
111 
NotificationShelf(Context context, AttributeSet attrs)112     public NotificationShelf(Context context, AttributeSet attrs) {
113         super(context, attrs);
114     }
115 
116     @VisibleForTesting
NotificationShelf(Context context, AttributeSet attrs, boolean showNotificationShelf)117     public NotificationShelf(Context context, AttributeSet attrs, boolean showNotificationShelf) {
118         super(context, attrs);
119         mShowNotificationShelf = showNotificationShelf;
120     }
121 
122     @Override
123     @VisibleForTesting
onFinishInflate()124     public void onFinishInflate() {
125         super.onFinishInflate();
126         mShelfIcons = findViewById(R.id.content);
127         mShelfIcons.setClipChildren(false);
128         mShelfIcons.setClipToPadding(false);
129 
130         mBackgroundNormal = (NotificationShelfBackgroundView) super.mBackgroundNormal;
131 
132         setClipToActualHeight(false);
133         setClipChildren(false);
134         setClipToPadding(false);
135         mShelfIcons.setIsStaticLayout(false);
136         requestRoundness(/* top = */ 1f, /* bottom = */ 1f, BASE_VALUE, /* animate = */ false);
137         updateResources();
138     }
139 
bind(AmbientState ambientState, NotificationStackScrollLayout hostLayout, NotificationRoundnessManager roundnessManager)140     public void bind(AmbientState ambientState, NotificationStackScrollLayout hostLayout,
141             NotificationRoundnessManager roundnessManager) {
142         mAmbientState = ambientState;
143         mHostLayout = hostLayout;
144         mRoundnessManager = roundnessManager;
145     }
146 
updateResources()147     private void updateResources() {
148         Resources res = getResources();
149         mStatusBarHeight = SystemBarUtils.getStatusBarHeight(mContext);
150         mPaddingBetweenElements = res.getDimensionPixelSize(R.dimen.notification_divider_height);
151         mMaxIconsOnLockscreen = res.getInteger(R.integer.max_notif_icons_on_lockscreen);
152 
153         ViewGroup.LayoutParams layoutParams = getLayoutParams();
154         final int newShelfHeight = res.getDimensionPixelOffset(R.dimen.notification_shelf_height);
155         if (newShelfHeight != layoutParams.height) {
156             layoutParams.height = newShelfHeight;
157             setLayoutParams(layoutParams);
158         }
159 
160         final int padding = res.getDimensionPixelOffset(R.dimen.shelf_icon_container_padding);
161         mShelfIcons.setPadding(padding, 0, padding, 0);
162         mScrollFastThreshold = res.getDimensionPixelOffset(R.dimen.scroll_fast_threshold);
163         mShowNotificationShelf = res.getBoolean(R.bool.config_showNotificationShelf);
164         mCornerAnimationDistance = res.getDimensionPixelSize(
165                 R.dimen.notification_corner_animation_distance);
166         mEnableNotificationClipping = res.getBoolean(R.bool.notification_enable_clipping);
167 
168         mShelfIcons.setOverrideIconColor(true);
169         if (!mShowNotificationShelf) {
170             setVisibility(GONE);
171         }
172     }
173 
174     @Override
onConfigurationChanged(Configuration newConfig)175     protected void onConfigurationChanged(Configuration newConfig) {
176         super.onConfigurationChanged(newConfig);
177         updateResources();
178     }
179 
180     @Override
getContentView()181     protected View getContentView() {
182         return mShelfIcons;
183     }
184 
getShelfIcons()185     public NotificationIconContainer getShelfIcons() {
186         return mShelfIcons;
187     }
188 
189     @Override
190     @NonNull
createExpandableViewState()191     public ExpandableViewState createExpandableViewState() {
192         return new ShelfState();
193     }
194 
195     @Override
toString()196     public String toString() {
197         return super.toString()
198                 + " (hideBackground=" + mHideBackground
199                 + " notGoneIndex=" + mNotGoneIndex
200                 + " hasItemsInStableShelf=" + mHasItemsInStableShelf
201                 + " interactive=" + mInteractive
202                 + " animationsEnabled=" + mAnimationsEnabled
203                 + " showNotificationShelf=" + mShowNotificationShelf
204                 + " indexOfFirstViewInShelf=" + mIndexOfFirstViewInShelf
205                 + ')';
206     }
207 
208     /**
209      * Update the state of the shelf.
210      */
updateState(StackScrollAlgorithm.StackScrollAlgorithmState algorithmState, AmbientState ambientState)211     public void updateState(StackScrollAlgorithm.StackScrollAlgorithmState algorithmState,
212                             AmbientState ambientState) {
213         ExpandableView lastView = ambientState.getLastVisibleBackgroundChild();
214         ShelfState viewState = (ShelfState) getViewState();
215         if (mShowNotificationShelf && lastView != null) {
216             ExpandableViewState lastViewState = lastView.getViewState();
217             viewState.copyFrom(lastViewState);
218 
219             viewState.height = getIntrinsicHeight();
220             viewState.setZTranslation(ambientState.getBaseZHeight());
221             viewState.clipTopAmount = 0;
222 
223             if (ambientState.isExpansionChanging() && !ambientState.isOnKeyguard()) {
224                 float expansion = ambientState.getExpansionFraction();
225                 if (ambientState.isBouncerInTransit()) {
226                     viewState.setAlpha(aboutToShowBouncerProgress(expansion));
227                 } else {
228                     if (ambientState.isSmallScreen()) {
229                         viewState.setAlpha(ShadeInterpolation.getContentAlpha(expansion));
230                     } else {
231                         LargeScreenShadeInterpolator interpolator =
232                                 ambientState.getLargeScreenShadeInterpolator();
233                         viewState.setAlpha(interpolator.getNotificationContentAlpha(expansion));
234                     }
235                 }
236             } else {
237                 viewState.setAlpha(1f - ambientState.getHideAmount());
238             }
239             viewState.hideSensitive = false;
240             viewState.setXTranslation(getTranslationX());
241             viewState.hasItemsInStableShelf = lastViewState.inShelf;
242             viewState.firstViewInShelf = algorithmState.firstViewInShelf;
243             if (mNotGoneIndex != -1) {
244                 viewState.notGoneIndex = Math.min(viewState.notGoneIndex, mNotGoneIndex);
245             }
246 
247             viewState.hidden = !mAmbientState.isShadeExpanded()
248                     || algorithmState.firstViewInShelf == null;
249 
250             final int indexOfFirstViewInShelf = algorithmState.visibleChildren.indexOf(
251                     algorithmState.firstViewInShelf);
252 
253             if (mAmbientState.isExpansionChanging()
254                     && algorithmState.firstViewInShelf != null
255                     && indexOfFirstViewInShelf > 0) {
256 
257                 // Show shelf if section before it is showing.
258                 final ExpandableView viewBeforeShelf = algorithmState.visibleChildren.get(
259                         indexOfFirstViewInShelf - 1);
260                 if (viewBeforeShelf.getViewState().hidden) {
261                     viewState.hidden = true;
262                 }
263             }
264         } else {
265             viewState.hidden = true;
266             viewState.location = ExpandableViewState.LOCATION_GONE;
267             viewState.hasItemsInStableShelf = false;
268         }
269 
270         final float stackBottom = SceneContainerFlag.isEnabled()
271                 ? ambientState.getStackTop() + ambientState.getInterpolatedStackHeight()
272                 : ambientState.getStackY() + ambientState.getInterpolatedStackHeight();
273 
274         if (viewState.hidden) {
275             // if the shelf is hidden, position it at the end of the stack (plus the clip
276             // padding), such that when it appears animated, it will smoothly move in from the
277             // bottom, without jump cutting any notifications
278             viewState.setYTranslation(stackBottom + mPaddingBetweenElements);
279         } else {
280             viewState.setYTranslation(stackBottom - viewState.height);
281         }
282     }
283 
284     /**
285      * Set the actual width of the shelf, this will only differ from width for short shelves.
286      */
287     @VisibleForTesting
setActualWidth(float actualWidth)288     public void setActualWidth(float actualWidth) {
289         setBackgroundWidth((int) actualWidth);
290         if (mShelfIcons != null) {
291             mShelfIcons.setAlignToEnd(isAlignedToEnd());
292             mShelfIcons.setActualLayoutWidth((int) actualWidth);
293         }
294         mActualWidth = actualWidth;
295     }
296 
297     @Override
setBackgroundWidth(int width)298     public void setBackgroundWidth(int width) {
299         super.setBackgroundWidth(width);
300         if (!NotificationMinimalism.isEnabled()) {
301             return;
302         }
303         if (mBackgroundNormal != null) {
304             mBackgroundNormal.setAlignToEnd(isAlignedToEnd());
305         }
306     }
307 
308     @Override
getBoundsOnScreen(Rect outRect, boolean clipToParent)309     public void getBoundsOnScreen(Rect outRect, boolean clipToParent) {
310         super.getBoundsOnScreen(outRect, clipToParent);
311         final int actualWidth = getActualWidth();
312         final boolean alignedToRight = NotificationMinimalism.isEnabled() ? isAlignedToRight() :
313                 isLayoutRtl();
314         if (alignedToRight) {
315             outRect.left = outRect.right - actualWidth;
316         } else {
317             outRect.right = outRect.left + actualWidth;
318         }
319     }
320 
321     /**
322      * @return Actual width of shelf, accounting for possible ongoing width animation
323      */
getActualWidth()324     public int getActualWidth() {
325         return mActualWidth > -1 ? (int) mActualWidth : getWidth();
326     }
327 
328     /**
329      * @param localX Click x from left of screen
330      * @param slop   Margin of error within which we count x for valid click
331      * @param left   Left of shelf, from left of screen
332      * @param right  Right of shelf, from left of screen
333      * @return Whether click x was in view
334      */
335     @VisibleForTesting
isXInView(float localX, float slop, float left, float right)336     public boolean isXInView(float localX, float slop, float left, float right) {
337         return (left - slop) <= localX && localX < (right + slop);
338     }
339 
340     /**
341      * @param localY Click y from top of shelf
342      * @param slop   Margin of error within which we count y for valid click
343      * @param top    Top of shelf
344      * @param bottom Height of shelf
345      * @return Whether click y was in view
346      */
347     @VisibleForTesting
isYInView(float localY, float slop, float top, float bottom)348     public boolean isYInView(float localY, float slop, float top, float bottom) {
349         return (top - slop) <= localY && localY < (bottom + slop);
350     }
351 
352     /**
353      * @param localX Click x
354      * @param localY Click y
355      * @param slop   Margin of error for valid click
356      * @return Whether this click was on the visible (non-clipped) part of the shelf
357      */
358     @Override
pointInView(float localX, float localY, float slop)359     public boolean pointInView(float localX, float localY, float slop) {
360         final float left, right;
361 
362         if (NotificationMinimalism.isEnabled()) {
363             left = getShelfLeftBound();
364             right = getShelfRightBound();
365         } else {
366             final float containerWidth = getWidth();
367             final float shelfWidth = getActualWidth();
368             left = isLayoutRtl() ? containerWidth - shelfWidth : 0;
369             right = isLayoutRtl() ? containerWidth : shelfWidth;
370         }
371 
372         final float top = mClipTopAmount;
373         final float bottom = getActualHeight();
374 
375         return isXInView(localX, slop, left, right)
376                 && isYInView(localY, slop, top, bottom);
377     }
378 
379     /**
380      * @return The left boundary of the shelf.
381      */
382     @VisibleForTesting
getShelfLeftBound()383     public float getShelfLeftBound() {
384         if (isAlignedToRight()) {
385             return getWidth() - getActualWidth();
386         } else {
387             return 0;
388         }
389     }
390 
391     /**
392      * @return The right boundary of the shelf.
393      */
394     @VisibleForTesting
getShelfRightBound()395     public float getShelfRightBound() {
396         if (isAlignedToRight()) {
397             return getWidth();
398         } else {
399             return getActualWidth();
400         }
401     }
402 
403     @VisibleForTesting
isAlignedToRight()404     public boolean isAlignedToRight() {
405         return isAlignedToEnd() ^ isLayoutRtl();
406     }
407 
408     /**
409      * When notification minimalism is on, on split shade, we want the notification shelf to align
410      * to the layout end (right for LTR; left for RTL).
411      * @return whether to align with the minimalism split shade style
412      */
413     @VisibleForTesting
isAlignedToEnd()414     public boolean isAlignedToEnd() {
415         if (!NotificationMinimalism.isEnabled()) {
416             return false;
417         } else if (SceneContainerFlag.isEnabled()) {
418             return mAlignedToEnd;
419         } else {
420             return mAmbientState.getUseSplitShade();
421         }
422     }
423 
424     /** @see #isAlignedToEnd() */
setAlignedToEnd(boolean alignedToEnd)425     public void setAlignedToEnd(boolean alignedToEnd) {
426         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) {
427             return;
428         }
429         if (mAlignedToEnd != alignedToEnd) {
430             mAlignedToEnd = alignedToEnd;
431             requestLayout();
432         }
433     }
434 
435     @Override
updateBackgroundColors()436     public void updateBackgroundColors() {
437         super.updateBackgroundColors();
438         ColorUpdateLogger colorUpdateLogger = ColorUpdateLogger.getInstance();
439 
440         if (colorUpdateLogger != null) {
441             colorUpdateLogger.logEvent("Shelf.updateBackgroundColors()",
442                     "normalBgColor=" + hexColorString(getNormalBgColor())
443                             + " background=" + mBackgroundNormal.toDumpString());
444         }
445     }
446 
447     /**
448      * Update the shelf appearance based on the other notifications around it. This transforms
449      * the icons from the notification area into the shelf.
450      */
updateAppearance()451     public void updateAppearance() {
452         // If the shelf should not be shown, then there is no need to update anything.
453         if (!mShowNotificationShelf) {
454             return;
455         }
456         mShelfIcons.resetViewStates();
457         float shelfStart = getTranslationY();
458         float numViewsInShelf = 0.0f;
459         View lastChild = mAmbientState.getLastVisibleBackgroundChild();
460         mNotGoneIndex = -1;
461         //  find the first view that doesn't overlap with the shelf
462         int notGoneIndex = 0;
463         int colorOfViewBeforeLast = NO_COLOR;
464         int colorTwoBefore = NO_COLOR;
465         int previousColor = NO_COLOR;
466         float transitionAmount = 0.0f;
467         float currentScrollVelocity = mAmbientState.getCurrentScrollVelocity();
468         boolean scrollingFast = currentScrollVelocity > mScrollFastThreshold
469                 || (mAmbientState.isExpansionChanging()
470                 && Math.abs(mAmbientState.getExpandingVelocity()) > mScrollFastThreshold);
471         boolean expandingAnimated = mAmbientState.isExpansionChanging()
472                 && !mAmbientState.isPanelTracking();
473         int baseZHeight = mAmbientState.getBaseZHeight();
474         int clipTopAmount = 0;
475 
476         for (int i = 0; i < getHostLayoutChildCount(); i++) {
477             ExpandableView child = getHostLayoutChildAt(i);
478             if (!child.needsClippingToShelf() || child.getVisibility() == GONE) {
479                 continue;
480             }
481             float notificationClipEnd;
482             boolean aboveShelf = ViewState.getFinalTranslationZ(child) > baseZHeight
483                     || child.isPinned();
484             boolean isLastChild = child == lastChild;
485             final float viewStart = child.getTranslationY();
486             final float shelfClipStart = getTranslationY() - mPaddingBetweenElements;
487             final float inShelfAmount = getAmountInShelf(i, child, scrollingFast,
488                     expandingAnimated, isLastChild, shelfClipStart);
489 
490             // TODO(b/172289889) scale mPaddingBetweenElements with expansion amount
491             if (aboveShelf) {
492                 notificationClipEnd = shelfStart + getIntrinsicHeight();
493             } else {
494                 notificationClipEnd = shelfStart - mPaddingBetweenElements;
495             }
496             int clipTop = updateNotificationClipHeight(child, notificationClipEnd, notGoneIndex);
497             clipTopAmount = Math.max(clipTop, clipTopAmount);
498 
499             // If the current row is an ExpandableNotificationRow, update its color, roundedness,
500             // and icon state.
501             if (child instanceof ExpandableNotificationRow expandableRow) {
502                 numViewsInShelf += inShelfAmount;
503                 int ownColorUntinted = expandableRow.getBackgroundColorWithoutTint();
504                 if (viewStart >= shelfStart && mNotGoneIndex == -1) {
505                     mNotGoneIndex = notGoneIndex;
506                     setTintColor(previousColor);
507                     setOverrideTintColor(colorTwoBefore, transitionAmount);
508 
509                 } else if (mNotGoneIndex == -1) {
510                     colorTwoBefore = previousColor;
511                     transitionAmount = inShelfAmount;
512                 }
513                 // We don't want to modify the color if the notification is hun'd
514                 if (isLastChild && canModifyColorOfNotifications()) {
515                     if (colorOfViewBeforeLast == NO_COLOR) {
516                         colorOfViewBeforeLast = ownColorUntinted;
517                     }
518                     expandableRow.setOverrideTintColor(colorOfViewBeforeLast, inShelfAmount);
519                 } else {
520                     colorOfViewBeforeLast = ownColorUntinted;
521                     expandableRow.setOverrideTintColor(NO_COLOR, 0 /* overrideAmount */);
522                 }
523                 if (notGoneIndex != 0 || !aboveShelf) {
524                     expandableRow.setAboveShelf(false);
525                 }
526 
527                 previousColor = ownColorUntinted;
528                 notGoneIndex++;
529             }
530 
531             if (child instanceof ActivatableNotificationView anv) {
532                 updateCornerRoundnessOnScroll(anv, viewStart, shelfStart);
533             }
534         }
535 
536         clipTransientViews();
537 
538         setClipTopAmount(clipTopAmount);
539 
540         boolean isHidden = getViewState().hidden
541                 || clipTopAmount >= getIntrinsicHeight()
542                 || !mShowNotificationShelf
543                 || numViewsInShelf < 1f;
544 
545         final float fractionToShade = Interpolators.STANDARD.getInterpolation(
546                 mAmbientState.getFractionToShade());
547 
548         if (mAmbientState.isOnKeyguard()) {
549             float numViews = MathUtils.min(numViewsInShelf, mMaxIconsOnLockscreen + 1);
550             float shortestWidth = mShelfIcons.calculateWidthFor(numViews);
551             float actualWidth = MathUtils.lerp(shortestWidth, getWidth(), fractionToShade);
552             setActualWidth(actualWidth);
553         } else {
554             setActualWidth(getWidth());
555         }
556 
557         // TODO(b/172289889) transition last icon in shelf to notification icon and vice versa.
558         setVisibility(isHidden ? View.INVISIBLE : View.VISIBLE);
559         mShelfIcons.calculateIconXTranslations();
560         mShelfIcons.applyIconStates();
561         for (int i = 0; i < getHostLayoutChildCount(); i++) {
562             View child = getHostLayoutChildAt(i);
563             if (!(child instanceof ExpandableNotificationRow row)
564                     || child.getVisibility() == GONE) {
565                 continue;
566             }
567             updateContinuousClipping(row);
568         }
569         boolean hideBackground = isHidden;
570         setHideBackground(hideBackground);
571         if (mNotGoneIndex == -1) {
572             mNotGoneIndex = notGoneIndex;
573         }
574     }
575 
576     private ExpandableView getHostLayoutChildAt(int index) {
577         return (ExpandableView) mHostLayout.getChildAt(index);
578     }
579 
580     private int getHostLayoutChildCount() {
581         return mHostLayout.getChildCount();
582     }
583 
584     private boolean canModifyColorOfNotifications() {
585         return mCanModifyColorOfNotifications && mAmbientState.isShadeExpanded();
586     }
587 
588     private void updateCornerRoundnessOnScroll(
589             ActivatableNotificationView anv,
590             float viewStart,
591             float shelfStart) {
592 
593         final boolean isUnlockedHeadsUp = !mAmbientState.isOnKeyguard()
594                 && !mAmbientState.isShadeExpanded()
595                 && anv instanceof ExpandableNotificationRow
596                 && anv.isHeadsUp();
597 
598         final boolean isHunGoingToShade = mAmbientState.isShadeExpanded()
599                 && anv == mAmbientState.getTrackedHeadsUpRow();
600 
601         final boolean shouldUpdateCornerRoundness = viewStart < shelfStart
602                 && !isViewAffectedBySwipe(anv)
603                 && !isUnlockedHeadsUp
604                 && !isHunGoingToShade
605                 && !anv.isAboveShelf()
606                 && !mAmbientState.isPulsing()
607                 && !mAmbientState.isDozing();
608 
609         if (!shouldUpdateCornerRoundness) {
610             return;
611         }
612 
613         final float viewEnd = viewStart + anv.getActualHeight();
614         final float cornerAnimationDistance = mCornerAnimationDistance
615                 * mAmbientState.getExpansionFraction();
616         final float cornerAnimationTop = shelfStart - cornerAnimationDistance;
617 
618         final float topValue;
619         if (viewStart >= cornerAnimationTop) {
620             // Round top corners within animation bounds
621             topValue = MathUtils.saturate(
622                     (viewStart - cornerAnimationTop) / cornerAnimationDistance);
623         } else {
624             // Fast scroll skips frames and leaves corners with unfinished rounding.
625             // Reset top and bottom corners outside of animation bounds.
626             topValue = 0f;
627         }
628         anv.requestTopRoundness(topValue, SHELF_SCROLL, /* animate = */ false);
629 
630         final float bottomValue;
631         if (viewEnd >= cornerAnimationTop) {
632             // Round bottom corners within animation bounds
633             bottomValue = MathUtils.saturate(
634                     (viewEnd - cornerAnimationTop) / cornerAnimationDistance);
635         } else {
636             // Fast scroll skips frames and leaves corners with unfinished rounding.
637             // Reset top and bottom corners outside of animation bounds.
638             bottomValue = 0f;
639         }
640         anv.requestBottomRoundness(bottomValue, SHELF_SCROLL, /* animate = */ false);
641     }
642 
643     private boolean isViewAffectedBySwipe(ExpandableView expandableView) {
644         return mRoundnessManager.isViewAffectedBySwipe(expandableView);
645     }
646 
647     /**
648      * Clips transient views to the top of the shelf - Transient views are only used for
649      * disappearing views/animations and need to be clipped correctly by the shelf to ensure they
650      * don't show underneath the notification stack when something is animating and the user
651      * swipes quickly.
652      */
653     private void clipTransientViews() {
654         for (int i = 0; i < getHostLayoutTransientViewCount(); i++) {
655             View transientView = getHostLayoutTransientView(i);
656             if (transientView instanceof ExpandableView transientExpandableView) {
657                 updateNotificationClipHeight(transientExpandableView, getTranslationY(), -1);
658             }
659         }
660     }
661 
662     private View getHostLayoutTransientView(int index) {
663         return mHostLayout.getTransientView(index);
664     }
665 
666     private int getHostLayoutTransientViewCount() {
667         return mHostLayout.getTransientViewCount();
668     }
669 
670     private void updateIconClipAmount(ExpandableNotificationRow row) {
671         float maxTop = row.getTranslationY();
672         if (getClipTopAmount() != 0) {
673             // if the shelf is clipped, lets make sure we also clip the icon
674             maxTop = Math.max(maxTop, getTranslationY() + getClipTopAmount());
675         }
676         StatusBarIconView icon = NotificationBundleUi.isEnabled()
677                 ? row.getEntryAdapter().getIcons().getShelfIcon()
678                 : row.getEntryLegacy().getIcons().getShelfIcon();
679         float shelfIconPosition = getTranslationY() + icon.getTop() + icon.getTranslationY();
680         if (shelfIconPosition < maxTop && !mAmbientState.isFullyHidden()) {
681             int top = (int) (maxTop - shelfIconPosition);
682             Rect clipRect = new Rect(0, top, icon.getWidth(), Math.max(top, icon.getHeight()));
683             icon.setClipBounds(clipRect);
684         } else {
685             icon.setClipBounds(null);
686         }
687     }
688 
689     private void updateContinuousClipping(final ExpandableNotificationRow row) {
690         StatusBarIconView icon = NotificationBundleUi.isEnabled()
691                 ? row.getEntryAdapter().getIcons().getShelfIcon()
692                 : row.getEntryLegacy().getIcons().getShelfIcon();
693         boolean needsContinuousClipping = ViewState.isAnimatingY(icon) && !mAmbientState.isDozing();
694         boolean isContinuousClipping = icon.getTag(TAG_CONTINUOUS_CLIPPING) != null;
695         if (needsContinuousClipping && !isContinuousClipping) {
696             final ViewTreeObserver observer = icon.getViewTreeObserver();
697             ViewTreeObserver.OnPreDrawListener predrawListener =
698                     new ViewTreeObserver.OnPreDrawListener() {
699                         @Override
700                         public boolean onPreDraw() {
701                             boolean animatingY = ViewState.isAnimatingY(icon);
702                             if (!animatingY) {
703                                 if (observer.isAlive()) {
704                                     observer.removeOnPreDrawListener(this);
705                                 }
706                                 icon.setTag(TAG_CONTINUOUS_CLIPPING, null);
707                                 return true;
708                             }
709                             updateIconClipAmount(row);
710                             return true;
711                         }
712                     };
713             observer.addOnPreDrawListener(predrawListener);
714             icon.addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
715                 @Override
716                 public void onViewAttachedToWindow(View v) {
717                 }
718 
719                 @Override
720                 public void onViewDetachedFromWindow(View v) {
721                     if (v == icon) {
722                         if (observer.isAlive()) {
723                             observer.removeOnPreDrawListener(predrawListener);
724                         }
725                         icon.setTag(TAG_CONTINUOUS_CLIPPING, null);
726                     }
727                 }
728             });
729             icon.setTag(TAG_CONTINUOUS_CLIPPING, predrawListener);
730         }
731     }
732 
733     /**
734      * Update the clipping of this view.
735      *
736      * @return the amount that our own top should be clipped
737      */
738     private int updateNotificationClipHeight(ExpandableView view,
739                                              float notificationClipEnd, int childIndex) {
740         float viewEnd = view.getTranslationY() + view.getActualHeight();
741         boolean isPinned = (view.isPinned() || view.isHeadsUpAnimatingAway())
742                 && !mAmbientState.isDozingAndNotPulsing(view);
743         boolean shouldClipOwnTop;
744         if (mAmbientState.isPulseExpanding()) {
745             shouldClipOwnTop = childIndex == 0;
746         } else {
747             shouldClipOwnTop = view.showingPulsing();
748         }
749         if (!isPinned) {
750             if (viewEnd > notificationClipEnd && !shouldClipOwnTop) {
751                 int clipBottomAmount =
752                         mEnableNotificationClipping ? (int) (viewEnd - notificationClipEnd) : 0;
753                 view.setClipBottomAmount(clipBottomAmount);
754             } else {
755                 view.setClipBottomAmount(0);
756             }
757         }
758         if (shouldClipOwnTop) {
759             return (int) (viewEnd - getTranslationY());
760         } else {
761             return 0;
762         }
763     }
764 
765     @Override
766     public void setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd,
767                                        int outlineTranslation) {
768         if (!mHasItemsInStableShelf) {
769             shadowIntensity = 0.0f;
770         }
771         super.setFakeShadowIntensity(shadowIntensity, outlineAlpha, shadowYEnd, outlineTranslation);
772     }
773 
774     /**
775      * @param i                 Index of the view in the host layout.
776      * @param view              The current ExpandableView.
777      * @param scrollingFast     Whether we are scrolling fast.
778      * @param expandingAnimated Whether we are expanding a notification.
779      * @param isLastChild       Whether this is the last view.
780      * @param shelfClipStart    The point at which notifications start getting clipped by the shelf.
781      * @return The amount how much this notification is in the shelf.
782      * 0f is not in shelf. 1f is completely in shelf.
783      */
784     @VisibleForTesting
785     public float getAmountInShelf(
786             int i,
787             ExpandableView view,
788             boolean scrollingFast,
789             boolean expandingAnimated,
790             boolean isLastChild,
791             float shelfClipStart
792     ) {
793 
794         // Let's calculate how much the view is in the shelf
795         float viewStart = view.getTranslationY();
796         int fullHeight = view.getActualHeight() + mPaddingBetweenElements;
797         float iconTransformStart = calculateIconTransformationStart(view);
798 
799         // Let's make sure the transform distance is
800         // at most to the icon (relevant for conversations)
801         float transformDistance = Math.min(
802                 viewStart + fullHeight - iconTransformStart,
803                 getIntrinsicHeight());
804 
805         if (isLastChild) {
806             fullHeight = Math.min(fullHeight, view.getMinHeight() - getIntrinsicHeight());
807             transformDistance = Math.min(
808                     transformDistance,
809                     view.getMinHeight() - getIntrinsicHeight());
810         }
811 
812         float viewEnd = viewStart + fullHeight;
813         float fullTransitionAmount = 0.0f;
814         float iconTransitionAmount = 0.0f;
815 
816         // Don't animate shelf icons during shade expansion.
817         if (mAmbientState.isExpansionChanging() && !mAmbientState.isOnKeyguard()) {
818             // TODO(b/172289889) handle icon placement for notification that is clipped by the shelf
819             if (mIndexOfFirstViewInShelf != -1 && i >= mIndexOfFirstViewInShelf) {
820                 fullTransitionAmount = 1f;
821                 iconTransitionAmount = 1f;
822             }
823 
824         } else if (viewEnd >= shelfClipStart
825                 && (mAmbientState.isShadeExpanded()
826                 || (!view.isPinned() && !view.isHeadsUpAnimatingAway()))) {
827 
828             if (viewStart < shelfClipStart && Math.abs(viewStart - shelfClipStart) > 0.001f) {
829                 // Partially clipped by shelf.
830                 float fullAmount = (shelfClipStart - viewStart) / fullHeight;
831                 fullAmount = Math.min(1.0f, fullAmount);
832                 fullTransitionAmount = 1.0f - fullAmount;
833                 if (isLastChild) {
834                     // Reduce icon transform distance to completely fade in shelf icon
835                     // by the time the notification icon fades out, and vice versa
836                     iconTransitionAmount = (shelfClipStart - viewStart)
837                             / (iconTransformStart - viewStart);
838                 } else {
839                     iconTransitionAmount = (shelfClipStart - iconTransformStart)
840                             / transformDistance;
841                 }
842                 iconTransitionAmount = MathUtils.constrain(iconTransitionAmount, 0.0f, 1.0f);
843                 iconTransitionAmount = 1.0f - iconTransitionAmount;
844             } else {
845                 // Fully in shelf.
846                 fullTransitionAmount = 1.0f;
847                 iconTransitionAmount = 1.0f;
848             }
849         }
850         updateIconPositioning(view, iconTransitionAmount,
851                 scrollingFast, expandingAnimated, isLastChild);
852         return fullTransitionAmount;
853     }
854 
855     /**
856      * @return the location where the transformation into the shelf should start.
857      */
calculateIconTransformationStart(ExpandableView view)858     private float calculateIconTransformationStart(ExpandableView view) {
859         View target = view.getShelfTransformationTarget();
860         if (target == null) {
861             return view.getTranslationY();
862         }
863         float start = view.getTranslationY() + view.getRelativeTopPadding(target);
864 
865         // Let's not start the transformation right at the icon but by the padding before it.
866         start -= view.getShelfIcon().getTop();
867         return start;
868     }
869 
updateIconPositioning( ExpandableView view, float iconTransitionAmount, boolean scrollingFast, boolean expandingAnimated, boolean isLastChild )870     private void updateIconPositioning(
871             ExpandableView view,
872             float iconTransitionAmount,
873             boolean scrollingFast,
874             boolean expandingAnimated,
875             boolean isLastChild
876     ) {
877         StatusBarIconView icon = view.getShelfIcon();
878         NotificationIconContainer.IconState iconState = getIconState(icon);
879         if (iconState == null) {
880             return;
881         }
882         boolean clampInShelf = iconTransitionAmount > 0.5f || isTargetClipped(view);
883         float clampedAmount = clampInShelf ? 1.0f : 0.0f;
884         if (iconTransitionAmount == clampedAmount) {
885             iconState.noAnimations = (scrollingFast || expandingAnimated) && !isLastChild;
886         }
887         if (!isLastChild
888                 && (scrollingFast || (expandingAnimated && !ViewState.isAnimatingY(icon)))) {
889             iconState.cancelAnimations(icon);
890             iconState.noAnimations = true;
891         }
892         float transitionAmount;
893         if (mAmbientState.isHiddenAtAll() && !view.isInShelf()) {
894             transitionAmount = mAmbientState.isFullyHidden() ? 1 : 0;
895         } else {
896             transitionAmount = iconTransitionAmount;
897             iconState.needsCannedAnimation = iconState.clampedAppearAmount != clampedAmount;
898         }
899         iconState.clampedAppearAmount = clampedAmount;
900         setIconTransformationAmount(view, transitionAmount);
901     }
902 
isTargetClipped(ExpandableView view)903     private boolean isTargetClipped(ExpandableView view) {
904         View target = view.getShelfTransformationTarget();
905         if (target == null) {
906             return false;
907         }
908         // We should never clip the target, let's instead put it into the shelf!
909         float endOfTarget = view.getTranslationY()
910                 + view.getContentTranslation()
911                 + view.getRelativeTopPadding(target)
912                 + target.getHeight();
913         return endOfTarget >= getTranslationY() - mPaddingBetweenElements;
914     }
915 
setIconTransformationAmount(ExpandableView view, float transitionAmount)916     private void setIconTransformationAmount(ExpandableView view, float transitionAmount) {
917         if (!(view instanceof ExpandableNotificationRow row)) {
918             return;
919         }
920         StatusBarIconView icon = row.getShelfIcon();
921         NotificationIconContainer.IconState iconState = getIconState(icon);
922         if (iconState == null) {
923             return;
924         }
925         iconState.setAlpha(ICON_ALPHA_INTERPOLATOR.getInterpolation(transitionAmount));
926         boolean isAppearing = row.isDrawingAppearAnimation() && !row.isInShelf();
927         iconState.hidden = isAppearing
928                 || (view instanceof ExpandableNotificationRow
929                 && ((ExpandableNotificationRow) view).isMinimized()
930                 && mShelfIcons.areIconsOverflowing())
931                 || (transitionAmount == 0.0f && !iconState.isAnimating(icon))
932                 || row.isAboveShelf()
933                 || row.showingPulsing()
934                 || row.getTranslationZ() > mAmbientState.getBaseZHeight();
935 
936         iconState.iconAppearAmount = iconState.hidden ? 0f : transitionAmount;
937 
938         // Fade in icons at shelf start
939         // This is important for conversation icons, which are badged and need x reset
940         iconState.setXTranslation(mShelfIcons.getActualPaddingStart());
941 
942         final boolean stayingInShelf = row.isInShelf() && !row.isTransformingIntoShelf();
943         if (stayingInShelf) {
944             iconState.iconAppearAmount = 1.0f;
945             iconState.setAlpha(1.0f);
946             iconState.hidden = false;
947         }
948         int backgroundColor = getBackgroundColorWithoutTint();
949         int shelfColor = icon.getContrastedStaticDrawableColor(backgroundColor);
950         if (row.isShowingIcon() && shelfColor != StatusBarIconView.NO_COLOR) {
951             int iconColor = row.getOriginalIconColor();
952             shelfColor = NotificationUtils.interpolateColors(iconColor, shelfColor,
953                     iconState.iconAppearAmount);
954         }
955         iconState.iconColor = shelfColor;
956     }
957 
getIconState(StatusBarIconView icon)958     private NotificationIconContainer.IconState getIconState(StatusBarIconView icon) {
959         if (mShelfIcons == null) {
960             return null;
961         }
962         return mShelfIcons.getIconState(icon);
963     }
964 
965     @Override
hasNoContentHeight()966     public boolean hasNoContentHeight() {
967         return true;
968     }
969 
setHideBackground(boolean hideBackground)970     private void setHideBackground(boolean hideBackground) {
971         if (mHideBackground != hideBackground) {
972             mHideBackground = hideBackground;
973             updateOutline();
974         }
975     }
976 
977     @Override
needsOutline()978     protected boolean needsOutline() {
979         return !mHideBackground && super.needsOutline();
980     }
981 
982 
983     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)984     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
985         super.onLayout(changed, left, top, right, bottom);
986 
987         // we always want to clip to our sides, such that nothing can draw outside of these bounds
988         int height = getResources().getDisplayMetrics().heightPixels;
989         mClipRect.set(0, -height, getWidth(), height);
990         if (mShelfIcons != null) {
991             mShelfIcons.setClipBounds(mClipRect);
992         }
993     }
994 
995     /**
996      * @return the index of the notification at which the shelf visually resides
997      */
getNotGoneIndex()998     public int getNotGoneIndex() {
999         return mNotGoneIndex;
1000     }
1001 
setHasItemsInStableShelf(boolean hasItemsInStableShelf)1002     private void setHasItemsInStableShelf(boolean hasItemsInStableShelf) {
1003         if (mHasItemsInStableShelf != hasItemsInStableShelf) {
1004             mHasItemsInStableShelf = hasItemsInStableShelf;
1005             updateInteractiveness();
1006         }
1007     }
1008 
updateInteractiveness()1009     private void updateInteractiveness() {
1010         mInteractive = mCanInteract && mHasItemsInStableShelf;
1011         setClickable(mInteractive);
1012         setFocusable(mInteractive);
1013         setImportantForAccessibility(mInteractive ? View.IMPORTANT_FOR_ACCESSIBILITY_YES
1014                 : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
1015     }
1016 
1017     @Override
isInteractive()1018     protected boolean isInteractive() {
1019         return mInteractive;
1020     }
1021 
setAnimationsEnabled(boolean enabled)1022     public void setAnimationsEnabled(boolean enabled) {
1023         mAnimationsEnabled = enabled;
1024         if (!enabled) {
1025             // we need to wait with enabling the animations until the first frame has passed
1026             mShelfIcons.setAnimationsEnabled(false);
1027         }
1028     }
1029 
1030     @Override
hasOverlappingRendering()1031     public boolean hasOverlappingRendering() {
1032         return false;  // Shelf only uses alpha for transitions where the difference can't be seen.
1033     }
1034 
1035     @Override
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)1036     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
1037         super.onInitializeAccessibilityNodeInfo(info);
1038         if (mInteractive) {
1039             // Add two accessibility actions that both performs expanding the notification shade
1040             info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND);
1041 
1042             AccessibilityAction seeAll = new AccessibilityAction(
1043                     AccessibilityNodeInfo.ACTION_CLICK,
1044                     getContext().getString(R.string.accessibility_overflow_action)
1045             );
1046             info.addAction(seeAll);
1047         }
1048     }
1049 
1050     @Override
performAccessibilityAction(int action, Bundle args)1051     public boolean performAccessibilityAction(int action, Bundle args) {
1052         // override ACTION_EXPAND with ACTION_CLICK
1053         if (action == AccessibilityNodeInfo.ACTION_EXPAND) {
1054             return super.performAccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, args);
1055         } else {
1056             return super.performAccessibilityAction(action, args);
1057         }
1058     }
1059 
1060     @Override
needsClippingToShelf()1061     public boolean needsClippingToShelf() {
1062         return false;
1063     }
1064 
setCanModifyColorOfNotifications(boolean canModifyColorOfNotifications)1065     public void setCanModifyColorOfNotifications(boolean canModifyColorOfNotifications) {
1066         mCanModifyColorOfNotifications = canModifyColorOfNotifications;
1067     }
1068 
setCanInteract(boolean canInteract)1069     public void setCanInteract(boolean canInteract) {
1070         mCanInteract = canInteract;
1071         updateInteractiveness();
1072     }
1073 
setIndexOfFirstViewInShelf(ExpandableView firstViewInShelf)1074     public void setIndexOfFirstViewInShelf(ExpandableView firstViewInShelf) {
1075         mIndexOfFirstViewInShelf = getIndexOfViewInHostLayout(firstViewInShelf);
1076     }
1077 
getIndexOfViewInHostLayout(ExpandableView child)1078     private int getIndexOfViewInHostLayout(ExpandableView child) {
1079         return mHostLayout.indexOfChild(child);
1080     }
1081 
requestRoundnessResetFor(ExpandableView child)1082     public void requestRoundnessResetFor(ExpandableView child) {
1083         child.requestRoundnessReset(SHELF_SCROLL);
1084     }
1085 
1086     @Override
dump(PrintWriter pwOriginal, String[] args)1087     public void dump(PrintWriter pwOriginal, String[] args) {
1088         IndentingPrintWriter pw = DumpUtilsKt.asIndenting(pwOriginal);
1089         super.dump(pw, args);
1090         if (DUMP_VERBOSE) {
1091             DumpUtilsKt.withIncreasedIndent(pw, () -> {
1092                 pw.println("mActualWidth: " + mActualWidth);
1093                 pw.println("mStatusBarHeight: " + mStatusBarHeight);
1094             });
1095         }
1096     }
1097 
1098     public class ShelfState extends ExpandableViewState {
1099         private boolean hasItemsInStableShelf;
1100         private ExpandableView firstViewInShelf;
1101 
1102         @Override
applyToView(View view)1103         public void applyToView(View view) {
1104             if (!mShowNotificationShelf) {
1105                 return;
1106             }
1107             super.applyToView(view);
1108             setIndexOfFirstViewInShelf(firstViewInShelf);
1109             updateAppearance();
1110             setHasItemsInStableShelf(hasItemsInStableShelf);
1111             mShelfIcons.setAnimationsEnabled(mAnimationsEnabled);
1112         }
1113 
1114         @Override
animateTo(View view, AnimationProperties properties)1115         public void animateTo(View view, AnimationProperties properties) {
1116             if (!mShowNotificationShelf) {
1117                 return;
1118             }
1119             super.animateTo(view, properties);
1120             setIndexOfFirstViewInShelf(firstViewInShelf);
1121             updateAppearance();
1122             setHasItemsInStableShelf(hasItemsInStableShelf);
1123             mShelfIcons.setAnimationsEnabled(mAnimationsEnabled);
1124         }
1125     }
1126 }
1127