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