1 /*
2  * Copyright (C) 2014 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.notification.stack;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.content.Context;
22 import android.content.res.Resources;
23 import android.util.MathUtils;
24 import android.view.View;
25 import android.view.ViewGroup;
26 
27 import com.android.internal.annotations.VisibleForTesting;
28 import com.android.internal.policy.SystemBarUtils;
29 import com.android.keyguard.BouncerPanelExpansionCalculator;
30 import com.android.systemui.R;
31 import com.android.systemui.animation.ShadeInterpolation;
32 import com.android.systemui.shade.transition.LargeScreenShadeInterpolator;
33 import com.android.systemui.statusbar.EmptyShadeView;
34 import com.android.systemui.statusbar.NotificationShelf;
35 import com.android.systemui.statusbar.notification.SourceType;
36 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
37 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
38 import com.android.systemui.statusbar.notification.row.ExpandableView;
39 import com.android.systemui.statusbar.notification.row.FooterView;
40 
41 import java.util.ArrayList;
42 import java.util.List;
43 
44 /**
45  * The Algorithm of the
46  * {@link com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout} which can
47  * be queried for {@link StackScrollAlgorithmState}
48  */
49 public class StackScrollAlgorithm {
50 
51     public static final float START_FRACTION = 0.5f;
52 
53     private static final String TAG = "StackScrollAlgorithm";
54     private static final Boolean DEBUG = false;
55     private static final SourceType STACK_SCROLL_ALGO = SourceType.from("StackScrollAlgorithm");
56 
57     private final ViewGroup mHostView;
58     private float mPaddingBetweenElements;
59     private float mGapHeight;
60     private float mGapHeightOnLockscreen;
61     private int mCollapsedSize;
62     private boolean mEnableNotificationClipping;
63 
64     private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState();
65     private boolean mIsExpanded;
66     private boolean mClipNotificationScrollToTop;
67     @VisibleForTesting
68     float mHeadsUpInset;
69     private int mPinnedZTranslationExtra;
70     private float mNotificationScrimPadding;
71     private int mMarginBottom;
72     private float mQuickQsOffsetHeight;
73     private float mSmallCornerRadius;
74     private float mLargeCornerRadius;
75 
StackScrollAlgorithm( Context context, ViewGroup hostView)76     public StackScrollAlgorithm(
77             Context context,
78             ViewGroup hostView) {
79         mHostView = hostView;
80         initView(context);
81     }
82 
initView(Context context)83     public void initView(Context context) {
84         updateResources(context);
85     }
86 
updateResources(Context context)87     private void updateResources(Context context) {
88         Resources res = context.getResources();
89         mPaddingBetweenElements = res.getDimensionPixelSize(
90                 R.dimen.notification_divider_height);
91         mCollapsedSize = res.getDimensionPixelSize(R.dimen.notification_min_height);
92         mEnableNotificationClipping = res.getBoolean(R.bool.notification_enable_clipping);
93         mClipNotificationScrollToTop = res.getBoolean(R.bool.config_clipNotificationScrollToTop);
94         int statusBarHeight = SystemBarUtils.getStatusBarHeight(context);
95         mHeadsUpInset = statusBarHeight + res.getDimensionPixelSize(
96                 R.dimen.heads_up_status_bar_padding);
97         mPinnedZTranslationExtra = res.getDimensionPixelSize(
98                 R.dimen.heads_up_pinned_elevation);
99         mGapHeight = res.getDimensionPixelSize(R.dimen.notification_section_divider_height);
100         mGapHeightOnLockscreen = res.getDimensionPixelSize(
101                 R.dimen.notification_section_divider_height_lockscreen);
102         mNotificationScrimPadding = res.getDimensionPixelSize(R.dimen.notification_side_paddings);
103         mMarginBottom = res.getDimensionPixelSize(R.dimen.notification_panel_margin_bottom);
104         mQuickQsOffsetHeight = SystemBarUtils.getQuickQsOffsetHeight(context);
105         mSmallCornerRadius = res.getDimension(R.dimen.notification_corner_radius_small);
106         mLargeCornerRadius = res.getDimension(R.dimen.notification_corner_radius);
107     }
108 
109     /**
110      * Updates the state of all children in the hostview based on this algorithm.
111      */
resetViewStates(AmbientState ambientState, int speedBumpIndex)112     public void resetViewStates(AmbientState ambientState, int speedBumpIndex) {
113         // The state of the local variables are saved in an algorithmState to easily subdivide it
114         // into multiple phases.
115         StackScrollAlgorithmState algorithmState = mTempAlgorithmState;
116 
117         // First we reset the view states to their default values.
118         resetChildViewStates();
119         initAlgorithmState(algorithmState, ambientState);
120         updatePositionsForState(algorithmState, ambientState);
121         updateZValuesForState(algorithmState, ambientState);
122         updateHeadsUpStates(algorithmState, ambientState);
123         updatePulsingStates(algorithmState, ambientState);
124 
125         updateDimmedAndHideSensitive(ambientState, algorithmState);
126         updateClipping(algorithmState, ambientState);
127         updateSpeedBumpState(algorithmState, speedBumpIndex);
128         updateShelfState(algorithmState, ambientState);
129         updateAlphaState(algorithmState, ambientState);
130         getNotificationChildrenStates(algorithmState);
131     }
132 
updateAlphaState(StackScrollAlgorithmState algorithmState, AmbientState ambientState)133     private void updateAlphaState(StackScrollAlgorithmState algorithmState,
134                                   AmbientState ambientState) {
135         for (ExpandableView view : algorithmState.visibleChildren) {
136             final ViewState viewState = view.getViewState();
137             final boolean isHunGoingToShade = ambientState.isShadeExpanded()
138                     && view == ambientState.getTrackedHeadsUpRow();
139 
140             if (isHunGoingToShade) {
141                 // Keep 100% opacity for heads up notification going to shade.
142                 viewState.setAlpha(1f);
143             } else if (ambientState.isOnKeyguard()) {
144                 // Adjust alpha for wakeup to lockscreen.
145                 viewState.setAlpha(1f - ambientState.getHideAmount());
146             } else if (ambientState.isExpansionChanging()) {
147                 // Adjust alpha for shade open & close.
148                 float expansion = ambientState.getExpansionFraction();
149                 if (ambientState.isBouncerInTransit()) {
150                     viewState.setAlpha(
151                             BouncerPanelExpansionCalculator.aboutToShowBouncerProgress(expansion));
152                 } else if (view instanceof FooterView) {
153                     viewState.setAlpha(interpolateFooterAlpha(ambientState));
154                 } else {
155                     viewState.setAlpha(interpolateNotificationContentAlpha(ambientState));
156                 }
157             }
158 
159             // For EmptyShadeView if on keyguard, we need to control the alpha to create
160             // a nice transition when the user is dragging down the notification panel.
161             if (view instanceof EmptyShadeView && ambientState.isOnKeyguard()) {
162                 final float fractionToShade = ambientState.getFractionToShade();
163                 viewState.setAlpha(ShadeInterpolation.getContentAlpha(fractionToShade));
164             }
165 
166             NotificationShelf shelf = ambientState.getShelf();
167             if (shelf != null) {
168                 final ViewState shelfState = shelf.getViewState();
169 
170                 // After the shelf has updated its yTranslation, explicitly set alpha=0 for view
171                 // below shelf to skip rendering them in the hardware layer. We do not set them
172                 // invisible because that runs invalidate & onDraw when these views return onscreen,
173                 // which is more expensive.
174                 if (shelfState.hidden) {
175                     // When the shelf is hidden, it won't clip views, so we don't hide rows
176                     continue;
177                 }
178 
179                 final float shelfTop = shelfState.getYTranslation();
180                 final float viewTop = viewState.getYTranslation();
181                 if (viewTop >= shelfTop) {
182                     viewState.setAlpha(0);
183                 }
184             }
185         }
186     }
187 
interpolateFooterAlpha(AmbientState ambientState)188     private float interpolateFooterAlpha(AmbientState ambientState) {
189         float expansion = ambientState.getExpansionFraction();
190         if (ambientState.isSmallScreen()) {
191             return ShadeInterpolation.getContentAlpha(expansion);
192         }
193         LargeScreenShadeInterpolator interpolator = ambientState.getLargeScreenShadeInterpolator();
194         return interpolator.getNotificationFooterAlpha(expansion);
195     }
196 
interpolateNotificationContentAlpha(AmbientState ambientState)197     private float interpolateNotificationContentAlpha(AmbientState ambientState) {
198         float expansion = ambientState.getExpansionFraction();
199         if (ambientState.isSmallScreen()) {
200             return ShadeInterpolation.getContentAlpha(expansion);
201         }
202         LargeScreenShadeInterpolator interpolator = ambientState.getLargeScreenShadeInterpolator();
203         return interpolator.getNotificationContentAlpha(expansion);
204     }
205 
206     /**
207      * How expanded or collapsed notifications are when pulling down the shade.
208      *
209      * @param ambientState Current ambient state.
210      * @return 0 when fully collapsed, 1 when expanded.
211      */
getNotificationSquishinessFraction(AmbientState ambientState)212     public float getNotificationSquishinessFraction(AmbientState ambientState) {
213         return getExpansionFractionWithoutShelf(mTempAlgorithmState, ambientState);
214     }
215 
log(String s)216     public static void log(String s) {
217         if (DEBUG) {
218             android.util.Log.i(TAG, s);
219         }
220     }
221 
logView(View view, String s)222     public static void logView(View view, String s) {
223         String viewString = "";
224         if (view instanceof ExpandableNotificationRow) {
225             ExpandableNotificationRow row = ((ExpandableNotificationRow) view);
226             if (row.getEntry() == null) {
227                 viewString = "ExpandableNotificationRow has null NotificationEntry";
228             } else {
229                 viewString = row.getEntry().getSbn().getId() + "";
230             }
231         } else if (view == null) {
232             viewString = "View is null";
233         } else if (view instanceof SectionHeaderView) {
234             viewString = "SectionHeaderView";
235         } else if (view instanceof FooterView) {
236             viewString = "FooterView";
237         } else if (view instanceof MediaContainerView) {
238             viewString = "MediaContainerView";
239         } else if (view instanceof EmptyShadeView) {
240             viewString = "EmptyShadeView";
241         } else {
242             viewString = view.toString();
243         }
244         log(viewString + " " + s);
245     }
246 
resetChildViewStates()247     private void resetChildViewStates() {
248         int numChildren = mHostView.getChildCount();
249         for (int i = 0; i < numChildren; i++) {
250             ExpandableView child = (ExpandableView) mHostView.getChildAt(i);
251             child.resetViewState();
252         }
253     }
254 
getNotificationChildrenStates(StackScrollAlgorithmState algorithmState)255     private void getNotificationChildrenStates(StackScrollAlgorithmState algorithmState) {
256         int childCount = algorithmState.visibleChildren.size();
257         for (int i = 0; i < childCount; i++) {
258             ExpandableView v = algorithmState.visibleChildren.get(i);
259             if (v instanceof ExpandableNotificationRow) {
260                 ExpandableNotificationRow row = (ExpandableNotificationRow) v;
261                 row.updateChildrenStates();
262             }
263         }
264     }
265 
updateSpeedBumpState(StackScrollAlgorithmState algorithmState, int speedBumpIndex)266     private void updateSpeedBumpState(StackScrollAlgorithmState algorithmState,
267                                       int speedBumpIndex) {
268         int childCount = algorithmState.visibleChildren.size();
269         int belowSpeedBump = speedBumpIndex;
270         for (int i = 0; i < childCount; i++) {
271             ExpandableView child = algorithmState.visibleChildren.get(i);
272             ExpandableViewState childViewState = child.getViewState();
273 
274             // The speed bump can also be gone, so equality needs to be taken when comparing
275             // indices.
276             childViewState.belowSpeedBump = i >= belowSpeedBump;
277         }
278 
279     }
280 
updateShelfState( StackScrollAlgorithmState algorithmState, AmbientState ambientState)281     private void updateShelfState(
282             StackScrollAlgorithmState algorithmState,
283             AmbientState ambientState) {
284 
285         NotificationShelf shelf = ambientState.getShelf();
286         if (shelf == null) {
287             return;
288         }
289 
290         shelf.updateState(algorithmState, ambientState);
291     }
292 
updateClipping(StackScrollAlgorithmState algorithmState, AmbientState ambientState)293     private void updateClipping(StackScrollAlgorithmState algorithmState,
294                                 AmbientState ambientState) {
295         float drawStart = ambientState.isOnKeyguard() ? 0
296                 : ambientState.getStackY() - ambientState.getScrollY();
297         float clipStart = 0;
298         int childCount = algorithmState.visibleChildren.size();
299         boolean firstHeadsUp = true;
300         float firstHeadsUpEnd = 0;
301         for (int i = 0; i < childCount; i++) {
302             ExpandableView child = algorithmState.visibleChildren.get(i);
303             ExpandableViewState state = child.getViewState();
304             if (!child.mustStayOnScreen() || state.headsUpIsVisible) {
305                 clipStart = Math.max(drawStart, clipStart);
306             }
307             float newYTranslation = state.getYTranslation();
308             float newHeight = state.height;
309             float newNotificationEnd = newYTranslation + newHeight;
310             boolean isHeadsUp = (child instanceof ExpandableNotificationRow) && child.isPinned();
311             if (mClipNotificationScrollToTop
312                     && !firstHeadsUp
313                     && (isHeadsUp || child.isHeadsUpAnimatingAway())
314                     && newNotificationEnd > firstHeadsUpEnd
315                     && !ambientState.isShadeExpanded()) {
316                 // The bottom of this view is peeking out from under the previous view.
317                 // Clip the part that is peeking out.
318                 float overlapAmount = newNotificationEnd - firstHeadsUpEnd;
319                 state.clipBottomAmount = mEnableNotificationClipping ? (int) overlapAmount : 0;
320             } else {
321                 state.clipBottomAmount = 0;
322             }
323             if (firstHeadsUp) {
324                 firstHeadsUpEnd = newNotificationEnd;
325             }
326             if (isHeadsUp) {
327                 firstHeadsUp = false;
328             }
329             if (!child.isTransparent()) {
330                 // Only update the previous values if we are not transparent,
331                 // otherwise we would clip to a transparent view.
332                 clipStart = Math.max(clipStart, isHeadsUp ? newYTranslation : newNotificationEnd);
333             }
334         }
335     }
336 
337     /** Updates the dimmed and hiding sensitive states of the children. */
updateDimmedAndHideSensitive(AmbientState ambientState, StackScrollAlgorithmState algorithmState)338     private void updateDimmedAndHideSensitive(AmbientState ambientState,
339             StackScrollAlgorithmState algorithmState) {
340         boolean dimmed = ambientState.isDimmed();
341         boolean hideSensitive = ambientState.isHideSensitive();
342         int childCount = algorithmState.visibleChildren.size();
343         for (int i = 0; i < childCount; i++) {
344             ExpandableView child = algorithmState.visibleChildren.get(i);
345             ExpandableViewState childViewState = child.getViewState();
346             childViewState.dimmed = dimmed;
347             childViewState.hideSensitive = hideSensitive;
348         }
349     }
350 
351     /**
352      * Initialize the algorithm state like updating the visible children.
353      */
initAlgorithmState(StackScrollAlgorithmState state, AmbientState ambientState)354     private void initAlgorithmState(StackScrollAlgorithmState state, AmbientState ambientState) {
355         state.scrollY = ambientState.getScrollY();
356         state.mCurrentYPosition = -state.scrollY;
357         state.mCurrentExpandedYPosition = -state.scrollY;
358 
359         //now init the visible children and update paddings
360         int childCount = mHostView.getChildCount();
361         state.visibleChildren.clear();
362         state.visibleChildren.ensureCapacity(childCount);
363         int notGoneIndex = 0;
364         for (int i = 0; i < childCount; i++) {
365             ExpandableView v = (ExpandableView) mHostView.getChildAt(i);
366             if (v.getVisibility() != View.GONE) {
367                 if (v == ambientState.getShelf()) {
368                     continue;
369                 }
370                 notGoneIndex = updateNotGoneIndex(state, notGoneIndex, v);
371                 if (v instanceof ExpandableNotificationRow) {
372                     ExpandableNotificationRow row = (ExpandableNotificationRow) v;
373 
374                     // handle the notGoneIndex for the children as well
375                     List<ExpandableNotificationRow> children = row.getAttachedChildren();
376                     if (row.isSummaryWithChildren() && children != null) {
377                         for (ExpandableNotificationRow childRow : children) {
378                             if (childRow.getVisibility() != View.GONE) {
379                                 ExpandableViewState childState = childRow.getViewState();
380                                 childState.notGoneIndex = notGoneIndex;
381                                 notGoneIndex++;
382                             }
383                         }
384                     }
385                 }
386             }
387         }
388 
389         // Save the index of first view in shelf from when shade is fully
390         // expanded. Consider updating these states in updateContentView instead so that we don't
391         // have to recalculate in every frame.
392         float currentY = -ambientState.getScrollY();
393         if (!ambientState.isOnKeyguard()
394                 || (ambientState.isBypassEnabled() && ambientState.isPulseExpanding())) {
395             // add top padding at the start as long as we're not on the lock screen
396             currentY += mNotificationScrimPadding;
397         }
398         state.firstViewInShelf = null;
399         for (int i = 0; i < state.visibleChildren.size(); i++) {
400             final ExpandableView view = state.visibleChildren.get(i);
401 
402             final boolean applyGapHeight = childNeedsGapHeight(
403                     ambientState.getSectionProvider(), i,
404                     view, getPreviousView(i, state));
405             if (applyGapHeight) {
406                 currentY += getGapForLocation(
407                         ambientState.getFractionToShade(), ambientState.isOnKeyguard());
408             }
409 
410             if (ambientState.getShelf() != null) {
411                 final float shelfStart = ambientState.getStackEndHeight()
412                         - ambientState.getShelf().getIntrinsicHeight()
413                         - mPaddingBetweenElements;
414                 if (currentY >= shelfStart
415                         && !(view instanceof FooterView)
416                         && state.firstViewInShelf == null) {
417                     state.firstViewInShelf = view;
418                 }
419             }
420             currentY = currentY
421                     + getMaxAllowedChildHeight(view)
422                     + mPaddingBetweenElements;
423         }
424     }
425 
updateNotGoneIndex(StackScrollAlgorithmState state, int notGoneIndex, ExpandableView v)426     private int updateNotGoneIndex(StackScrollAlgorithmState state, int notGoneIndex,
427                                    ExpandableView v) {
428         ExpandableViewState viewState = v.getViewState();
429         viewState.notGoneIndex = notGoneIndex;
430         state.visibleChildren.add(v);
431         notGoneIndex++;
432         return notGoneIndex;
433     }
434 
getPreviousView(int i, StackScrollAlgorithmState algorithmState)435     private ExpandableView getPreviousView(int i, StackScrollAlgorithmState algorithmState) {
436         return i > 0 ? algorithmState.visibleChildren.get(i - 1) : null;
437     }
438 
439     /**
440      * Update the position of QS Frame.
441      */
updateQSFrameTop(int qsHeight)442     public void updateQSFrameTop(int qsHeight) {
443         // Intentionally empty for sub-classes in other device form factors to override
444     }
445 
446     /**
447      * Determine the positions for the views. This is the main part of the algorithm.
448      *
449      * @param algorithmState The state in which the current pass of the algorithm is currently in
450      * @param ambientState   The current ambient state
451      */
updatePositionsForState(StackScrollAlgorithmState algorithmState, AmbientState ambientState)452     protected void updatePositionsForState(StackScrollAlgorithmState algorithmState,
453                                            AmbientState ambientState) {
454         if (!ambientState.isOnKeyguard()
455                 || (ambientState.isBypassEnabled() && ambientState.isPulseExpanding())) {
456             algorithmState.mCurrentYPosition += mNotificationScrimPadding;
457             algorithmState.mCurrentExpandedYPosition += mNotificationScrimPadding;
458         }
459 
460         int childCount = algorithmState.visibleChildren.size();
461         for (int i = 0; i < childCount; i++) {
462             updateChild(i, algorithmState, ambientState);
463         }
464     }
465 
setLocation(ExpandableViewState expandableViewState, float currentYPosition, int i)466     private void setLocation(ExpandableViewState expandableViewState, float currentYPosition,
467                              int i) {
468         expandableViewState.location = ExpandableViewState.LOCATION_MAIN_AREA;
469         if (currentYPosition <= 0) {
470             expandableViewState.location = ExpandableViewState.LOCATION_HIDDEN_TOP;
471         }
472     }
473 
474     /**
475      * @return Fraction to apply to view height and gap between views.
476      * Does not include shelf height even if shelf is showing.
477      */
getExpansionFractionWithoutShelf( StackScrollAlgorithmState algorithmState, AmbientState ambientState)478     protected float getExpansionFractionWithoutShelf(
479             StackScrollAlgorithmState algorithmState,
480             AmbientState ambientState) {
481 
482         final boolean showingShelf = ambientState.getShelf() != null
483                 && algorithmState.firstViewInShelf != null;
484 
485         final float shelfHeight = showingShelf ? ambientState.getShelf().getIntrinsicHeight() : 0f;
486         final float scrimPadding = ambientState.isOnKeyguard()
487                 && (!ambientState.isBypassEnabled() || !ambientState.isPulseExpanding())
488                 ? 0 : mNotificationScrimPadding;
489 
490         final float stackHeight = ambientState.getStackHeight() - shelfHeight - scrimPadding;
491         final float stackEndHeight = ambientState.getStackEndHeight() - shelfHeight - scrimPadding;
492         if (stackEndHeight == 0f) {
493             // This should not happen, since even when the shade is empty we show EmptyShadeView
494             // but check just in case, so we don't return infinity or NaN.
495             return 0f;
496         }
497         return stackHeight / stackEndHeight;
498     }
499 
hasNonClearableNotifs(StackScrollAlgorithmState algorithmState)500     private boolean hasNonClearableNotifs(StackScrollAlgorithmState algorithmState) {
501         for (int i = 0; i < algorithmState.visibleChildren.size(); i++) {
502             View child = algorithmState.visibleChildren.get(i);
503             if (!(child instanceof ExpandableNotificationRow)) {
504                 continue;
505             }
506             final ExpandableNotificationRow row = (ExpandableNotificationRow) child;
507             if (!row.canViewBeCleared()) {
508                 return true;
509             }
510         }
511         return false;
512     }
513 
514     @VisibleForTesting
maybeUpdateHeadsUpIsVisible( ExpandableViewState viewState, boolean isShadeExpanded, boolean mustStayOnScreen, boolean topVisible, float viewEnd, float hunMax)515     void maybeUpdateHeadsUpIsVisible(
516             ExpandableViewState viewState,
517             boolean isShadeExpanded,
518             boolean mustStayOnScreen,
519             boolean topVisible,
520             float viewEnd,
521             float hunMax) {
522         if (isShadeExpanded && mustStayOnScreen && topVisible) {
523             viewState.headsUpIsVisible = viewEnd < hunMax;
524         }
525     }
526 
527     // TODO(b/172289889) polish shade open from HUN
528 
529     /**
530      * Populates the {@link ExpandableViewState} for a single child.
531      *
532      * @param i              The index of the child in
533      *                       {@link StackScrollAlgorithmState#visibleChildren}.
534      * @param algorithmState The overall output state of the algorithm.
535      * @param ambientState   The input state provided to the algorithm.
536      */
537     protected void updateChild(
538             int i,
539             StackScrollAlgorithmState algorithmState,
540             AmbientState ambientState) {
541 
542         ExpandableView view = algorithmState.visibleChildren.get(i);
543         ExpandableViewState viewState = view.getViewState();
544         viewState.location = ExpandableViewState.LOCATION_UNKNOWN;
545 
546         float expansionFraction = getExpansionFractionWithoutShelf(
547                 algorithmState, ambientState);
548 
549         // Add gap between sections.
550         final boolean applyGapHeight =
551                 childNeedsGapHeight(
552                         ambientState.getSectionProvider(), i,
553                         view, getPreviousView(i, algorithmState));
554         if (applyGapHeight) {
555             final float gap = getGapForLocation(
556                     ambientState.getFractionToShade(), ambientState.isOnKeyguard());
557             algorithmState.mCurrentYPosition += expansionFraction * gap;
558             algorithmState.mCurrentExpandedYPosition += gap;
559         }
560 
561         // Must set viewState.yTranslation _before_ use.
562         // Incoming views have yTranslation=0 by default.
563         viewState.setYTranslation(algorithmState.mCurrentYPosition);
564 
565         float viewEnd = viewState.getYTranslation() + viewState.height + ambientState.getStackY();
566         maybeUpdateHeadsUpIsVisible(viewState, ambientState.isShadeExpanded(),
567                 view.mustStayOnScreen(), /* topVisible */ viewState.getYTranslation() >= 0,
568                 viewEnd, /* hunMax */ ambientState.getMaxHeadsUpTranslation()
569         );
570         if (view instanceof FooterView) {
571             final boolean shadeClosed = !ambientState.isShadeExpanded();
572             final boolean isShelfShowing = algorithmState.firstViewInShelf != null;
573             if (shadeClosed) {
574                 viewState.hidden = true;
575             } else {
576                 final float footerEnd = algorithmState.mCurrentExpandedYPosition
577                         + view.getIntrinsicHeight();
578                 final boolean noSpaceForFooter = footerEnd > ambientState.getStackEndHeight();
579                 ((FooterView.FooterViewState) viewState).hideContent =
580                         isShelfShowing || noSpaceForFooter
581                                 || (ambientState.isClearAllInProgress()
582                                 && !hasNonClearableNotifs(algorithmState));
583             }
584         } else {
585             if (view instanceof EmptyShadeView) {
586                 float fullHeight = ambientState.getLayoutMaxHeight() + mMarginBottom
587                         - ambientState.getStackY();
588                 viewState.setYTranslation((fullHeight - getMaxAllowedChildHeight(view)) / 2f);
589             } else if (view != ambientState.getTrackedHeadsUpRow()) {
590                 if (ambientState.isExpansionChanging()) {
591                     // We later update shelf state, then hide views below the shelf.
592                     viewState.hidden = false;
593                     viewState.inShelf = algorithmState.firstViewInShelf != null
594                             && i >= algorithmState.visibleChildren.indexOf(
595                             algorithmState.firstViewInShelf);
596                 } else if (ambientState.getShelf() != null) {
597                     // When pulsing (incoming notification on AOD), innerHeight is 0; clamp all
598                     // to shelf start, thereby hiding all notifications (except the first one, which
599                     // we later unhide in updatePulsingState)
600                     // TODO(b/192348384): merge InnerHeight with StackHeight
601                     // Note: Bypass pulse looks different, but when it is not expanding, we need
602                     //  to use the innerHeight which doesn't update continuously, otherwise we show
603                     //  more notifications than we should during this special transitional states.
604                     boolean bypassPulseNotExpanding = ambientState.isBypassEnabled()
605                             && ambientState.isOnKeyguard() && !ambientState.isPulseExpanding();
606                     final float stackBottom = !ambientState.isShadeExpanded()
607                             || ambientState.getDozeAmount() == 1f
608                             || bypassPulseNotExpanding
609                             ? ambientState.getInnerHeight()
610                             : ambientState.getStackHeight();
611                     final float shelfStart = stackBottom
612                             - ambientState.getShelf().getIntrinsicHeight()
613                             - mPaddingBetweenElements;
614                     updateViewWithShelf(view, viewState, shelfStart);
615                 }
616             }
617             viewState.height = getMaxAllowedChildHeight(view);
618             if (!view.isPinned() && !view.isHeadsUpAnimatingAway()
619                     && !ambientState.isPulsingRow(view)) {
620                 // The expansion fraction should not affect HUNs or pulsing notifications.
621                 viewState.height *= expansionFraction;
622             }
623         }
624 
625         algorithmState.mCurrentYPosition +=
626                 expansionFraction * (getMaxAllowedChildHeight(view) + mPaddingBetweenElements);
627         algorithmState.mCurrentExpandedYPosition += view.getIntrinsicHeight()
628                 + mPaddingBetweenElements;
629 
630         setLocation(view.getViewState(), algorithmState.mCurrentYPosition, i);
631         viewState.setYTranslation(viewState.getYTranslation() + ambientState.getStackY());
632     }
633 
634     @VisibleForTesting
updateViewWithShelf(ExpandableView view, ExpandableViewState viewState, float shelfStart)635     void updateViewWithShelf(ExpandableView view, ExpandableViewState viewState, float shelfStart) {
636         viewState.setYTranslation(Math.min(viewState.getYTranslation(), shelfStart));
637         if (viewState.getYTranslation() >= shelfStart) {
638             viewState.hidden = !view.isExpandAnimationRunning()
639                     && !view.hasExpandingChild();
640             viewState.inShelf = true;
641             // Notifications in the shelf cannot be visible HUNs.
642             viewState.headsUpIsVisible = false;
643         }
644     }
645 
646     /**
647      * Get the gap height needed for before a view
648      *
649      * @param sectionProvider the sectionProvider used to understand the sections
650      * @param visibleIndex    the visible index of this view in the list
651      * @param child           the child asked about
652      * @param previousChild   the child right before it or null if none
653      * @return the size of the gap needed or 0 if none is needed
654      */
getGapHeightForChild( SectionProvider sectionProvider, int visibleIndex, View child, View previousChild, float fractionToShade, boolean onKeyguard)655     public float getGapHeightForChild(
656             SectionProvider sectionProvider,
657             int visibleIndex,
658             View child,
659             View previousChild,
660             float fractionToShade,
661             boolean onKeyguard) {
662 
663         if (childNeedsGapHeight(sectionProvider, visibleIndex, child,
664                 previousChild)) {
665             return getGapForLocation(fractionToShade, onKeyguard);
666         } else {
667             return 0;
668         }
669     }
670 
671     @VisibleForTesting
getGapForLocation(float fractionToShade, boolean onKeyguard)672     float getGapForLocation(float fractionToShade, boolean onKeyguard) {
673         if (fractionToShade > 0f) {
674             return MathUtils.lerp(mGapHeightOnLockscreen, mGapHeight, fractionToShade);
675         }
676         if (onKeyguard) {
677             return mGapHeightOnLockscreen;
678         }
679         return mGapHeight;
680     }
681 
682     /**
683      * Does a given child need a gap, i.e spacing before a view?
684      *
685      * @param sectionProvider the sectionProvider used to understand the sections
686      * @param visibleIndex    the visible index of this view in the list
687      * @param child           the child asked about
688      * @param previousChild   the child right before it or null if none
689      * @return if the child needs a gap height
690      */
childNeedsGapHeight( SectionProvider sectionProvider, int visibleIndex, View child, View previousChild)691     private boolean childNeedsGapHeight(
692             SectionProvider sectionProvider,
693             int visibleIndex,
694             View child,
695             View previousChild) {
696         return sectionProvider.beginsSection(child, previousChild)
697                 && visibleIndex > 0
698                 && !(previousChild instanceof SectionHeaderView)
699                 && !(child instanceof FooterView);
700     }
701 
702     @VisibleForTesting
updatePulsingStates(StackScrollAlgorithmState algorithmState, AmbientState ambientState)703     void updatePulsingStates(StackScrollAlgorithmState algorithmState,
704                                      AmbientState ambientState) {
705         int childCount = algorithmState.visibleChildren.size();
706         ExpandableNotificationRow pulsingRow = null;
707         for (int i = 0; i < childCount; i++) {
708             View child = algorithmState.visibleChildren.get(i);
709             if (!(child instanceof ExpandableNotificationRow)) {
710                 continue;
711             }
712             ExpandableNotificationRow row = (ExpandableNotificationRow) child;
713             if (!row.showingPulsing() || (i == 0 && ambientState.isPulseExpanding())) {
714                 continue;
715             }
716             ExpandableViewState viewState = row.getViewState();
717             viewState.hidden = false;
718             pulsingRow = row;
719         }
720 
721         // Set AmbientState#pulsingRow to the current pulsing row when on AOD.
722         // Set AmbientState#pulsingRow=null when on lockscreen, since AmbientState#pulsingRow
723         // is only used for skipping the unfurl animation for (the notification that was already
724         // showing at full height on AOD) during the AOD=>lockscreen transition, where
725         // dozeAmount=[1f, 0f). We also need to reset the pulsingRow once it is no longer used
726         // because it will interfere with future unfurling animations - for example, during the
727         // LS=>AOD animation, the pulsingRow may stay at full height when it should squish with the
728         // rest of the stack.
729         if (ambientState.getDozeAmount() == 0.0f || ambientState.getDozeAmount() == 1.0f) {
730             ambientState.setPulsingRow(pulsingRow);
731         }
732     }
733 
updateHeadsUpStates(StackScrollAlgorithmState algorithmState, AmbientState ambientState)734     private void updateHeadsUpStates(StackScrollAlgorithmState algorithmState,
735                                      AmbientState ambientState) {
736         int childCount = algorithmState.visibleChildren.size();
737 
738         // Move the tracked heads up into position during the appear animation, by interpolating
739         // between the HUN inset (where it will appear as a HUN) and the end position in the shade
740         float headsUpTranslation = mHeadsUpInset - ambientState.getStackTopMargin();
741         ExpandableNotificationRow trackedHeadsUpRow = ambientState.getTrackedHeadsUpRow();
742         if (trackedHeadsUpRow != null) {
743             ExpandableViewState childState = trackedHeadsUpRow.getViewState();
744             if (childState != null) {
745                 float endPos = childState.getYTranslation() - ambientState.getStackTranslation();
746                 childState.setYTranslation(MathUtils.lerp(
747                         headsUpTranslation, endPos, ambientState.getAppearFraction()));
748             }
749         }
750 
751         ExpandableNotificationRow topHeadsUpEntry = null;
752         for (int i = 0; i < childCount; i++) {
753             View child = algorithmState.visibleChildren.get(i);
754             if (!(child instanceof ExpandableNotificationRow)) {
755                 continue;
756             }
757             ExpandableNotificationRow row = (ExpandableNotificationRow) child;
758             if (!(row.isHeadsUp() || row.isHeadsUpAnimatingAway())) {
759                 continue;
760             }
761             ExpandableViewState childState = row.getViewState();
762             if (topHeadsUpEntry == null && row.mustStayOnScreen() && !childState.headsUpIsVisible) {
763                 topHeadsUpEntry = row;
764                 childState.location = ExpandableViewState.LOCATION_FIRST_HUN;
765             }
766             boolean isTopEntry = topHeadsUpEntry == row;
767             float unmodifiedEndLocation = childState.getYTranslation() + childState.height;
768             if (mIsExpanded) {
769                 if (shouldHunBeVisibleWhenScrolled(row.mustStayOnScreen(),
770                         childState.headsUpIsVisible, row.showingPulsing(),
771                         ambientState.isOnKeyguard(), row.getEntry().isStickyAndNotDemoted())) {
772                     // Ensure that the heads up is always visible even when scrolled off
773                     clampHunToTop(mQuickQsOffsetHeight, ambientState.getStackTranslation(),
774                             row.getCollapsedHeight(), childState);
775                     if (isTopEntry && row.isAboveShelf()) {
776                         // the first hun can't get off screen.
777                         clampHunToMaxTranslation(ambientState, row, childState);
778                         childState.hidden = false;
779                     }
780                 }
781             }
782             if (row.isPinned()) {
783                 // Make sure row yTranslation is at maximum the HUN yTranslation,
784                 // which accounts for AmbientState.stackTopMargin in split-shade.
785                 childState.setYTranslation(
786                         Math.max(childState.getYTranslation(), headsUpTranslation));
787                 childState.height = Math.max(row.getIntrinsicHeight(), childState.height);
788                 childState.hidden = false;
789                 ExpandableViewState topState =
790                         topHeadsUpEntry == null ? null : topHeadsUpEntry.getViewState();
791                 if (topState != null && !isTopEntry && (!mIsExpanded
792                         || unmodifiedEndLocation > topState.getYTranslation() + topState.height)) {
793                     // Ensure that a headsUp doesn't vertically extend further than the heads-up at
794                     // the top most z-position
795                     childState.height = row.getIntrinsicHeight();
796                 }
797 
798                 // heads up notification show and this row is the top entry of heads up
799                 // notifications. i.e. this row should be the only one row that has input field
800                 // To check if the row need to do translation according to scroll Y
801                 // heads up show full of row's content and any scroll y indicate that the
802                 // translationY need to move up the HUN.
803                 if (!mIsExpanded && isTopEntry && ambientState.getScrollY() > 0) {
804                     childState.setYTranslation(
805                             childState.getYTranslation() - ambientState.getScrollY());
806                 }
807             }
808             if (row.isHeadsUpAnimatingAway()) {
809                 // Make sure row yTranslation is at maximum the HUN yTranslation,
810                 // which accounts for AmbientState.stackTopMargin in split-shade.
811                 childState.setYTranslation(
812                         Math.max(childState.getYTranslation(), headsUpTranslation));
813                 // keep it visible for the animation
814                 childState.hidden = false;
815             }
816         }
817     }
818 
819     @VisibleForTesting
shouldHunBeVisibleWhenScrolled(boolean mustStayOnScreen, boolean headsUpIsVisible, boolean showingPulsing, boolean isOnKeyguard, boolean headsUpOnKeyguard)820     boolean shouldHunBeVisibleWhenScrolled(boolean mustStayOnScreen, boolean headsUpIsVisible,
821             boolean showingPulsing, boolean isOnKeyguard, boolean headsUpOnKeyguard) {
822         return mustStayOnScreen && !headsUpIsVisible
823                         && !showingPulsing
824                         && (!isOnKeyguard || headsUpOnKeyguard);
825     }
826 
827      /**
828      * When shade is open and we are scrolled to the bottom of notifications,
829      * clamp incoming HUN in its collapsed form, right below qs offset.
830      * Transition pinned collapsed HUN to full height when scrolling back up.
831      */
832     @VisibleForTesting
clampHunToTop(float quickQsOffsetHeight, float stackTranslation, float collapsedHeight, ExpandableViewState viewState)833     void clampHunToTop(float quickQsOffsetHeight, float stackTranslation, float collapsedHeight,
834                        ExpandableViewState viewState) {
835 
836         final float newTranslation = Math.max(quickQsOffsetHeight + stackTranslation,
837                 viewState.getYTranslation());
838 
839         // Transition from collapsed pinned state to fully expanded state
840         // when the pinned HUN approaches its actual location (when scrolling back to top).
841         final float distToRealY = newTranslation - viewState.getYTranslation();
842         viewState.height = (int) Math.max(viewState.height - distToRealY, collapsedHeight);
843         viewState.setYTranslation(newTranslation);
844     }
845 
846     // Pin HUN to bottom of expanded QS
847     // while the rest of notifications are scrolled offscreen.
clampHunToMaxTranslation(AmbientState ambientState, ExpandableNotificationRow row, ExpandableViewState childState)848     private void clampHunToMaxTranslation(AmbientState ambientState, ExpandableNotificationRow row,
849                                           ExpandableViewState childState) {
850         float maxHeadsUpTranslation = ambientState.getMaxHeadsUpTranslation();
851         final float maxShelfPosition = ambientState.getInnerHeight() + ambientState.getTopPadding()
852                 + ambientState.getStackTranslation();
853         maxHeadsUpTranslation = Math.min(maxHeadsUpTranslation, maxShelfPosition);
854 
855         final float bottomPosition = maxHeadsUpTranslation - row.getCollapsedHeight();
856         final float newTranslation = Math.min(childState.getYTranslation(), bottomPosition);
857         childState.height = (int) Math.min(childState.height, maxHeadsUpTranslation
858                 - newTranslation);
859         childState.setYTranslation(newTranslation);
860 
861         // Animate pinned HUN bottom corners to and from original roundness.
862         final float originalCornerRadius =
863                 row.isLastInSection() ? 1f : (mSmallCornerRadius / mLargeCornerRadius);
864         final float bottomValue = computeCornerRoundnessForPinnedHun(mHostView.getHeight(),
865                 ambientState.getStackY(), getMaxAllowedChildHeight(row), originalCornerRadius);
866         row.requestBottomRoundness(bottomValue, STACK_SCROLL_ALGO);
867         row.addOnDetachResetRoundness(STACK_SCROLL_ALGO);
868     }
869 
870     @VisibleForTesting
computeCornerRoundnessForPinnedHun(float hostViewHeight, float stackY, float viewMaxHeight, float originalCornerRadius)871     float computeCornerRoundnessForPinnedHun(float hostViewHeight, float stackY,
872                                              float viewMaxHeight, float originalCornerRadius) {
873 
874         // Compute y where corner roundness should be in its original unpinned state.
875         // We use view max height because the pinned collapsed HUN expands to max height
876         // when it becomes unpinned.
877         final float originalRoundnessY = hostViewHeight - viewMaxHeight;
878 
879         final float distToOriginalRoundness = Math.max(0f, stackY - originalRoundnessY);
880         final float progressToPinnedRoundness = Math.min(1f,
881                 distToOriginalRoundness / viewMaxHeight);
882 
883         return MathUtils.lerp(originalCornerRadius, 1f, progressToPinnedRoundness);
884     }
885 
getMaxAllowedChildHeight(View child)886     protected int getMaxAllowedChildHeight(View child) {
887         if (child instanceof ExpandableView) {
888             ExpandableView expandableView = (ExpandableView) child;
889             return expandableView.getIntrinsicHeight();
890         }
891         return child == null ? mCollapsedSize : child.getHeight();
892     }
893 
894     /**
895      * Calculate the Z positions for all children based on the number of items in both stacks and
896      * save it in the resultState
897      *
898      * @param algorithmState The state in which the current pass of the algorithm is currently in
899      * @param ambientState   The ambient state of the algorithm
900      */
updateZValuesForState(StackScrollAlgorithmState algorithmState, AmbientState ambientState)901     private void updateZValuesForState(StackScrollAlgorithmState algorithmState,
902                                        AmbientState ambientState) {
903         int childCount = algorithmState.visibleChildren.size();
904         float childrenOnTop = 0.0f;
905 
906         int topHunIndex = -1;
907         for (int i = 0; i < childCount; i++) {
908             ExpandableView child = algorithmState.visibleChildren.get(i);
909             if (child instanceof ActivatableNotificationView
910                     && (child.isAboveShelf() || child.showingPulsing())) {
911                 topHunIndex = i;
912                 break;
913             }
914         }
915 
916         for (int i = childCount - 1; i >= 0; i--) {
917             childrenOnTop = updateChildZValue(i, childrenOnTop,
918                     algorithmState, ambientState, i == topHunIndex);
919         }
920     }
921 
922     /**
923      * Calculate and update the Z positions for a given child. We currently only give shadows to
924      * HUNs to distinguish a HUN from its surroundings.
925      *
926      * @param isTopHun      Whether the child is a top HUN. A top HUN means a HUN that shows on the
927      *                      vertically top of screen. Top HUNs should have drop shadows
928      * @param childrenOnTop It is greater than 0 when there's an existing HUN that is elevated
929      * @return childrenOnTop The decimal part represents the fraction of the elevated HUN's height
930      *                      that overlaps with QQS Panel. The integer part represents the count of
931      *                      previous HUNs whose Z positions are greater than 0.
932      */
updateChildZValue(int i, float childrenOnTop, StackScrollAlgorithmState algorithmState, AmbientState ambientState, boolean isTopHun)933     protected float updateChildZValue(int i, float childrenOnTop,
934                                       StackScrollAlgorithmState algorithmState,
935                                       AmbientState ambientState,
936                                       boolean isTopHun) {
937         ExpandableView child = algorithmState.visibleChildren.get(i);
938         ExpandableViewState childViewState = child.getViewState();
939         float baseZ = ambientState.getBaseZHeight();
940 
941         if (child.mustStayOnScreen() && !childViewState.headsUpIsVisible
942                 && !ambientState.isDozingAndNotPulsing(child)
943                 && childViewState.getYTranslation() < ambientState.getTopPadding()
944                 + ambientState.getStackTranslation()) {
945 
946             if (childrenOnTop != 0.0f) {
947                 // To elevate the later HUN over previous HUN when multiple HUNs exist
948                 childrenOnTop++;
949             } else {
950                 // Handles HUN shadow when Shade is opened, and AmbientState.mScrollY > 0
951                 // Calculate the HUN's z-value based on its overlapping fraction with QQS Panel.
952                 // When scrolling down shade to make HUN back to in-position in Notification Panel,
953                 // The overlapping fraction goes to 0, and shadows hides gradually.
954                 float overlap = ambientState.getTopPadding()
955                         + ambientState.getStackTranslation() - childViewState.getYTranslation();
956                 // To prevent over-shadow during HUN entry
957                 childrenOnTop += Math.min(
958                         1.0f,
959                         overlap / childViewState.height
960                 );
961             }
962             childViewState.setZTranslation(baseZ
963                     + childrenOnTop * mPinnedZTranslationExtra);
964         } else if (isTopHun) {
965             // In case this is a new view that has never been measured before, we don't want to
966             // elevate if we are currently expanded more than the notification
967             int shelfHeight = ambientState.getShelf() == null ? 0 :
968                     ambientState.getShelf().getIntrinsicHeight();
969             float shelfStart = ambientState.getInnerHeight()
970                     - shelfHeight + ambientState.getTopPadding()
971                     + ambientState.getStackTranslation();
972             float notificationEnd = childViewState.getYTranslation() + child.getIntrinsicHeight()
973                     + mPaddingBetweenElements;
974             if (shelfStart > notificationEnd) {
975                 // When the notification doesn't overlap with Notification Shelf, there's no shadow
976                 childViewState.setZTranslation(baseZ);
977             } else {
978                 // Give shadow to the notification if it overlaps with Notification Shelf
979                 float factor = (notificationEnd - shelfStart) / shelfHeight;
980                 if (Float.isNaN(factor)) { // Avoid problems when the above is 0/0.
981                     factor = 1.0f;
982                 }
983                 factor = Math.min(factor, 1.0f);
984                 childViewState.setZTranslation(baseZ + factor * mPinnedZTranslationExtra);
985             }
986         } else {
987             childViewState.setZTranslation(baseZ);
988         }
989 
990         // While HUN is showing and Shade is closed: headerVisibleAmount stays 0, shadow stays.
991         // During HUN-to-Shade (eg. dragging down HUN to open Shade): headerVisibleAmount goes
992         // gradually from 0 to 1, shadow hides gradually.
993         // Header visibility is a deprecated concept, we are using headerVisibleAmount only because
994         // this value nicely goes from 0 to 1 during the HUN-to-Shade process.
995 
996         childViewState.setZTranslation(childViewState.getZTranslation()
997                 + (1.0f - child.getHeaderVisibleAmount()) * mPinnedZTranslationExtra);
998         return childrenOnTop;
999     }
1000 
setIsExpanded(boolean isExpanded)1001     public void setIsExpanded(boolean isExpanded) {
1002         this.mIsExpanded = isExpanded;
1003     }
1004 
1005     public static class StackScrollAlgorithmState {
1006 
1007         /**
1008          * The scroll position of the algorithm (absolute scrolling).
1009          */
1010         public int scrollY;
1011 
1012         /**
1013          * First view in shelf.
1014          */
1015         public ExpandableView firstViewInShelf;
1016 
1017         /**
1018          * The children from the host view which are not gone.
1019          */
1020         public final ArrayList<ExpandableView> visibleChildren = new ArrayList<>();
1021 
1022         /**
1023          * Y position of the current view during updating children
1024          * with expansion factor applied.
1025          */
1026         private float mCurrentYPosition;
1027 
1028         /**
1029          * Y position of the current view during updating children
1030          * without applying the expansion factor.
1031          */
1032         private float mCurrentExpandedYPosition;
1033     }
1034 
1035     /**
1036      * Interface for telling the SSA when a new notification section begins (so it can add in
1037      * appropriate margins).
1038      */
1039     public interface SectionProvider {
1040         /**
1041          * True if this view starts a new "section" of notifications, such as the gentle
1042          * notifications section. False if sections are not enabled.
1043          */
1044         boolean beginsSection(@NonNull View view, @Nullable View previous);
1045     }
1046 
1047     /**
1048      * Interface for telling the StackScrollAlgorithm information about the bypass state
1049      */
1050     public interface BypassController {
1051         /**
1052          * True if bypass is enabled.  Note that this is always false if face auth is not enabled.
1053          */
1054         boolean isBypassEnabled();
1055     }
1056 }
1057