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