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 androidx.annotation.VisibleForTesting;
28 
29 import com.android.internal.policy.SystemBarUtils;
30 import com.android.systemui.R;
31 import com.android.systemui.animation.ShadeInterpolation;
32 import com.android.systemui.statusbar.EmptyShadeView;
33 import com.android.systemui.statusbar.NotificationShelf;
34 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
35 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
36 import com.android.systemui.statusbar.notification.row.ExpandableView;
37 import com.android.systemui.statusbar.notification.row.FooterView;
38 
39 import java.util.ArrayList;
40 import java.util.List;
41 
42 /**
43  * The Algorithm of the
44  * {@link com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout} which can
45  * be queried for {@link StackScrollAlgorithmState}
46  */
47 public class StackScrollAlgorithm {
48 
49     public static final float START_FRACTION = 0.3f;
50 
51     private static final String LOG_TAG = "StackScrollAlgorithm";
52     private final ViewGroup mHostView;
53 
54     private int mPaddingBetweenElements;
55     private int mGapHeight;
56     private int mCollapsedSize;
57 
58     private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState();
59     private boolean mIsExpanded;
60     private boolean mClipNotificationScrollToTop;
61     @VisibleForTesting float mHeadsUpInset;
62     private int mPinnedZTranslationExtra;
63     private float mNotificationScrimPadding;
64     private int mCloseHandleUnderlapHeight;
65 
StackScrollAlgorithm( Context context, ViewGroup hostView)66     public StackScrollAlgorithm(
67             Context context,
68             ViewGroup hostView) {
69         mHostView = hostView;
70         initView(context);
71     }
72 
initView(Context context)73     public void initView(Context context) {
74         initConstants(context);
75     }
76 
initConstants(Context context)77     private void initConstants(Context context) {
78         Resources res = context.getResources();
79         mPaddingBetweenElements = res.getDimensionPixelSize(
80                 R.dimen.notification_divider_height);
81         mCollapsedSize = res.getDimensionPixelSize(R.dimen.notification_min_height);
82         mClipNotificationScrollToTop = res.getBoolean(R.bool.config_clipNotificationScrollToTop);
83         int statusBarHeight = SystemBarUtils.getStatusBarHeight(context);
84         mHeadsUpInset = statusBarHeight + res.getDimensionPixelSize(
85                 R.dimen.heads_up_status_bar_padding);
86         mPinnedZTranslationExtra = res.getDimensionPixelSize(
87                 R.dimen.heads_up_pinned_elevation);
88         mGapHeight = res.getDimensionPixelSize(R.dimen.notification_section_divider_height);
89         mNotificationScrimPadding = res.getDimensionPixelSize(R.dimen.notification_side_paddings);
90         mCloseHandleUnderlapHeight = res.getDimensionPixelSize(R.dimen.close_handle_underlap);
91     }
92 
93     /**
94      * Updates the state of all children in the hostview based on this algorithm.
95      */
resetViewStates(AmbientState ambientState, int speedBumpIndex)96     public void resetViewStates(AmbientState ambientState, int speedBumpIndex) {
97         // The state of the local variables are saved in an algorithmState to easily subdivide it
98         // into multiple phases.
99         StackScrollAlgorithmState algorithmState = mTempAlgorithmState;
100 
101         // First we reset the view states to their default values.
102         resetChildViewStates();
103         initAlgorithmState(algorithmState, ambientState);
104         updatePositionsForState(algorithmState, ambientState);
105         updateZValuesForState(algorithmState, ambientState);
106         updateHeadsUpStates(algorithmState, ambientState);
107         updatePulsingStates(algorithmState, ambientState);
108 
109         updateDimmedActivatedHideSensitive(ambientState, algorithmState);
110         updateClipping(algorithmState, ambientState);
111         updateSpeedBumpState(algorithmState, speedBumpIndex);
112         updateShelfState(algorithmState, ambientState);
113         getNotificationChildrenStates(algorithmState, ambientState);
114     }
115 
116     /**
117      * How expanded or collapsed notifications are when pulling down the shade.
118      * @param ambientState Current ambient state.
119      * @return 0 when fully collapsed, 1 when expanded.
120      */
getNotificationSquishinessFraction(AmbientState ambientState)121     public float getNotificationSquishinessFraction(AmbientState ambientState) {
122         return getExpansionFractionWithoutShelf(mTempAlgorithmState, ambientState);
123     }
124 
resetChildViewStates()125     private void resetChildViewStates() {
126         int numChildren = mHostView.getChildCount();
127         for (int i = 0; i < numChildren; i++) {
128             ExpandableView child = (ExpandableView) mHostView.getChildAt(i);
129             child.resetViewState();
130         }
131     }
132 
getNotificationChildrenStates(StackScrollAlgorithmState algorithmState, AmbientState ambientState)133     private void getNotificationChildrenStates(StackScrollAlgorithmState algorithmState,
134             AmbientState ambientState) {
135         int childCount = algorithmState.visibleChildren.size();
136         for (int i = 0; i < childCount; i++) {
137             ExpandableView v = algorithmState.visibleChildren.get(i);
138             if (v instanceof ExpandableNotificationRow) {
139                 ExpandableNotificationRow row = (ExpandableNotificationRow) v;
140                 row.updateChildrenStates(ambientState);
141             }
142         }
143     }
144 
updateSpeedBumpState(StackScrollAlgorithmState algorithmState, int speedBumpIndex)145     private void updateSpeedBumpState(StackScrollAlgorithmState algorithmState,
146             int speedBumpIndex) {
147         int childCount = algorithmState.visibleChildren.size();
148         int belowSpeedBump = speedBumpIndex;
149         for (int i = 0; i < childCount; i++) {
150             ExpandableView child = algorithmState.visibleChildren.get(i);
151             ExpandableViewState childViewState = child.getViewState();
152 
153             // The speed bump can also be gone, so equality needs to be taken when comparing
154             // indices.
155             childViewState.belowSpeedBump = i >= belowSpeedBump;
156         }
157 
158     }
159 
updateShelfState( StackScrollAlgorithmState algorithmState, AmbientState ambientState)160     private void updateShelfState(
161             StackScrollAlgorithmState algorithmState,
162             AmbientState ambientState) {
163 
164         NotificationShelf shelf = ambientState.getShelf();
165         if (shelf == null) {
166             return;
167         }
168 
169         shelf.updateState(algorithmState, ambientState);
170 
171         // After the shelf has updated its yTranslation, explicitly set alpha=0 for view below shelf
172         // to skip rendering them in the hardware layer. We do not set them invisible because that
173         // runs invalidate & onDraw when these views return onscreen, which is more expensive.
174         if (shelf.getViewState().hidden) {
175             // When the shelf is hidden, it won't clip views, so we don't hide rows
176             return;
177         }
178         final float shelfTop = shelf.getViewState().yTranslation;
179 
180         for (ExpandableView view : algorithmState.visibleChildren) {
181             final float viewTop = view.getViewState().yTranslation;
182             if (viewTop >= shelfTop) {
183                 view.getViewState().alpha = 0;
184             }
185         }
186     }
187 
updateClipping(StackScrollAlgorithmState algorithmState, AmbientState ambientState)188     private void updateClipping(StackScrollAlgorithmState algorithmState,
189             AmbientState ambientState) {
190         float drawStart = ambientState.isOnKeyguard() ? 0
191                 : ambientState.getStackY() - ambientState.getScrollY();
192         float clipStart = 0;
193         int childCount = algorithmState.visibleChildren.size();
194         boolean firstHeadsUp = true;
195         for (int i = 0; i < childCount; i++) {
196             ExpandableView child = algorithmState.visibleChildren.get(i);
197             ExpandableViewState state = child.getViewState();
198             if (!child.mustStayOnScreen() || state.headsUpIsVisible) {
199                 clipStart = Math.max(drawStart, clipStart);
200             }
201             float newYTranslation = state.yTranslation;
202             float newHeight = state.height;
203             float newNotificationEnd = newYTranslation + newHeight;
204             boolean isHeadsUp = (child instanceof ExpandableNotificationRow) && child.isPinned();
205             if (mClipNotificationScrollToTop
206                     && (!state.inShelf || (isHeadsUp && !firstHeadsUp))
207                     && newYTranslation < clipStart
208                     && !ambientState.isShadeOpening()) {
209                 // The previous view is overlapping on top, clip!
210                 float overlapAmount = clipStart - newYTranslation;
211                 state.clipTopAmount = (int) overlapAmount;
212             } else {
213                 state.clipTopAmount = 0;
214             }
215             if (isHeadsUp) {
216                 firstHeadsUp = false;
217             }
218             if (!child.isTransparent()) {
219                 // Only update the previous values if we are not transparent,
220                 // otherwise we would clip to a transparent view.
221                 clipStart = Math.max(clipStart, isHeadsUp ? newYTranslation : newNotificationEnd);
222             }
223         }
224     }
225 
226     /**
227      * Updates the dimmed, activated and hiding sensitive states of the children.
228      */
updateDimmedActivatedHideSensitive(AmbientState ambientState, StackScrollAlgorithmState algorithmState)229     private void updateDimmedActivatedHideSensitive(AmbientState ambientState,
230             StackScrollAlgorithmState algorithmState) {
231         boolean dimmed = ambientState.isDimmed();
232         boolean hideSensitive = ambientState.isHideSensitive();
233         View activatedChild = ambientState.getActivatedChild();
234         int childCount = algorithmState.visibleChildren.size();
235         for (int i = 0; i < childCount; i++) {
236             ExpandableView child = algorithmState.visibleChildren.get(i);
237             ExpandableViewState childViewState = child.getViewState();
238             childViewState.dimmed = dimmed;
239             childViewState.hideSensitive = hideSensitive;
240             boolean isActivatedChild = activatedChild == child;
241             if (dimmed && isActivatedChild) {
242                 childViewState.zTranslation += 2.0f * ambientState.getZDistanceBetweenElements();
243             }
244         }
245     }
246 
247     /**
248      * Initialize the algorithm state like updating the visible children.
249      */
initAlgorithmState(StackScrollAlgorithmState state, AmbientState ambientState)250     private void initAlgorithmState(StackScrollAlgorithmState state, AmbientState ambientState) {
251         state.scrollY = ambientState.getScrollY();
252         state.mCurrentYPosition = -state.scrollY;
253         state.mCurrentExpandedYPosition = -state.scrollY;
254 
255         //now init the visible children and update paddings
256         int childCount = mHostView.getChildCount();
257         state.visibleChildren.clear();
258         state.visibleChildren.ensureCapacity(childCount);
259         int notGoneIndex = 0;
260         for (int i = 0; i < childCount; i++) {
261             ExpandableView v = (ExpandableView) mHostView.getChildAt(i);
262             if (v.getVisibility() != View.GONE) {
263                 if (v == ambientState.getShelf()) {
264                     continue;
265                 }
266                 notGoneIndex = updateNotGoneIndex(state, notGoneIndex, v);
267                 if (v instanceof ExpandableNotificationRow) {
268                     ExpandableNotificationRow row = (ExpandableNotificationRow) v;
269 
270                     // handle the notGoneIndex for the children as well
271                     List<ExpandableNotificationRow> children = row.getAttachedChildren();
272                     if (row.isSummaryWithChildren() && children != null) {
273                         for (ExpandableNotificationRow childRow : children) {
274                             if (childRow.getVisibility() != View.GONE) {
275                                 ExpandableViewState childState = childRow.getViewState();
276                                 childState.notGoneIndex = notGoneIndex;
277                                 notGoneIndex++;
278                             }
279                         }
280                     }
281                 }
282             }
283         }
284 
285         // Save the index of first view in shelf from when shade is fully
286         // expanded. Consider updating these states in updateContentView instead so that we don't
287         // have to recalculate in every frame.
288         float currentY = -ambientState.getScrollY();
289         if (!ambientState.isOnKeyguard()
290                 || (ambientState.isBypassEnabled() && ambientState.isPulseExpanding())) {
291             // add top padding at the start as long as we're not on the lock screen
292             currentY += mNotificationScrimPadding;
293         }
294         state.firstViewInShelf = null;
295         for (int i = 0; i < state.visibleChildren.size(); i++) {
296             final ExpandableView view = state.visibleChildren.get(i);
297 
298             final boolean applyGapHeight = childNeedsGapHeight(
299                     ambientState.getSectionProvider(), i,
300                     view, getPreviousView(i, state));
301             if (applyGapHeight) {
302                 currentY += mGapHeight;
303             }
304 
305             if (ambientState.getShelf() != null) {
306                 final float shelfStart = ambientState.getStackEndHeight()
307                         - ambientState.getShelf().getIntrinsicHeight();
308                 if (currentY >= shelfStart
309                         && !(view instanceof FooterView)
310                         && state.firstViewInShelf == null) {
311                     state.firstViewInShelf = view;
312                 }
313             }
314             currentY = currentY
315                     + getMaxAllowedChildHeight(view)
316                     + mPaddingBetweenElements;
317         }
318     }
319 
updateNotGoneIndex(StackScrollAlgorithmState state, int notGoneIndex, ExpandableView v)320     private int updateNotGoneIndex(StackScrollAlgorithmState state, int notGoneIndex,
321             ExpandableView v) {
322         ExpandableViewState viewState = v.getViewState();
323         viewState.notGoneIndex = notGoneIndex;
324         state.visibleChildren.add(v);
325         notGoneIndex++;
326         return notGoneIndex;
327     }
328 
getPreviousView(int i, StackScrollAlgorithmState algorithmState)329     private ExpandableView getPreviousView(int i, StackScrollAlgorithmState algorithmState) {
330         return i > 0 ? algorithmState.visibleChildren.get(i - 1) : null;
331     }
332 
333     /**
334      * Determine the positions for the views. This is the main part of the algorithm.
335      *
336      * @param algorithmState The state in which the current pass of the algorithm is currently in
337      * @param ambientState   The current ambient state
338      */
updatePositionsForState(StackScrollAlgorithmState algorithmState, AmbientState ambientState)339     private void updatePositionsForState(StackScrollAlgorithmState algorithmState,
340             AmbientState ambientState) {
341         if (!ambientState.isOnKeyguard()
342                 || (ambientState.isBypassEnabled() && ambientState.isPulseExpanding())) {
343             algorithmState.mCurrentYPosition += mNotificationScrimPadding;
344             algorithmState.mCurrentExpandedYPosition += mNotificationScrimPadding;
345         }
346 
347         int childCount = algorithmState.visibleChildren.size();
348         for (int i = 0; i < childCount; i++) {
349             updateChild(i, algorithmState, ambientState);
350         }
351     }
352 
setLocation(ExpandableViewState expandableViewState, float currentYPosition, int i)353     private void setLocation(ExpandableViewState expandableViewState, float currentYPosition,
354             int i) {
355         expandableViewState.location = ExpandableViewState.LOCATION_MAIN_AREA;
356         if (currentYPosition <= 0) {
357             expandableViewState.location = ExpandableViewState.LOCATION_HIDDEN_TOP;
358         }
359     }
360 
361     /**
362      * @return Fraction to apply to view height and gap between views.
363      *         Does not include shelf height even if shelf is showing.
364      */
getExpansionFractionWithoutShelf( StackScrollAlgorithmState algorithmState, AmbientState ambientState)365     private float getExpansionFractionWithoutShelf(
366             StackScrollAlgorithmState algorithmState,
367             AmbientState ambientState) {
368 
369         final boolean showingShelf = ambientState.getShelf() != null
370                 && algorithmState.firstViewInShelf != null;
371 
372         final float shelfHeight = showingShelf ? ambientState.getShelf().getIntrinsicHeight() : 0f;
373         final float scrimPadding = ambientState.isOnKeyguard()
374                 && (!ambientState.isBypassEnabled() || !ambientState.isPulseExpanding())
375                 ? 0 : mNotificationScrimPadding;
376 
377         final float stackHeight = ambientState.getStackHeight()  - shelfHeight - scrimPadding;
378         final float stackEndHeight = ambientState.getStackEndHeight() - shelfHeight - scrimPadding;
379         if (stackEndHeight == 0f) {
380             // This should not happen, since even when the shade is empty we show EmptyShadeView
381             // but check just in case, so we don't return infinity or NaN.
382             return 0f;
383         }
384         return stackHeight / stackEndHeight;
385     }
386 
hasOngoingNotifs(StackScrollAlgorithmState algorithmState)387     public boolean hasOngoingNotifs(StackScrollAlgorithmState algorithmState) {
388         for (int i = 0; i < algorithmState.visibleChildren.size(); i++) {
389             View child = algorithmState.visibleChildren.get(i);
390             if (!(child instanceof ExpandableNotificationRow)) {
391                 continue;
392             }
393             final ExpandableNotificationRow row = (ExpandableNotificationRow) child;
394             if (!row.canViewBeDismissed()) {
395                 return true;
396             }
397         }
398         return false;
399     }
400 
401     // TODO(b/172289889) polish shade open from HUN
402     /**
403      * Populates the {@link ExpandableViewState} for a single child.
404      *
405      * @param i                The index of the child in
406      * {@link StackScrollAlgorithmState#visibleChildren}.
407      * @param algorithmState   The overall output state of the algorithm.
408      * @param ambientState     The input state provided to the algorithm.
409      */
updateChild( int i, StackScrollAlgorithmState algorithmState, AmbientState ambientState)410     protected void updateChild(
411             int i,
412             StackScrollAlgorithmState algorithmState,
413             AmbientState ambientState) {
414 
415         ExpandableView view = algorithmState.visibleChildren.get(i);
416         ExpandableViewState viewState = view.getViewState();
417         viewState.location = ExpandableViewState.LOCATION_UNKNOWN;
418 
419         final boolean isHunGoingToShade = ambientState.isShadeExpanded()
420                 && view == ambientState.getTrackedHeadsUpRow();
421         if (isHunGoingToShade) {
422             // Keep 100% opacity for heads up notification going to shade.
423         } else if (ambientState.isOnKeyguard()) {
424             // Adjust alpha for wakeup to lockscreen.
425             viewState.alpha = 1f - ambientState.getHideAmount();
426         } else if (ambientState.isExpansionChanging()) {
427             // Adjust alpha for shade open & close.
428             float expansion = ambientState.getExpansionFraction();
429             viewState.alpha = ShadeInterpolation.getContentAlpha(expansion);
430         }
431 
432         if (ambientState.isShadeExpanded() && view.mustStayOnScreen()
433                 && viewState.yTranslation >= 0) {
434             // Even if we're not scrolled away we're in view and we're also not in the
435             // shelf. We can relax the constraints and let us scroll off the top!
436             float end = viewState.yTranslation + viewState.height + ambientState.getStackY();
437             viewState.headsUpIsVisible = end < ambientState.getMaxHeadsUpTranslation();
438         }
439 
440         final float expansionFraction = getExpansionFractionWithoutShelf(
441                 algorithmState, ambientState);
442 
443         // Add gap between sections.
444         final boolean applyGapHeight =
445                 childNeedsGapHeight(
446                         ambientState.getSectionProvider(), i,
447                         view, getPreviousView(i, algorithmState));
448         if (applyGapHeight) {
449             algorithmState.mCurrentYPosition += expansionFraction * mGapHeight;
450             algorithmState.mCurrentExpandedYPosition += mGapHeight;
451         }
452 
453         viewState.yTranslation = algorithmState.mCurrentYPosition;
454 
455         if (view instanceof FooterView) {
456             final boolean shadeClosed = !ambientState.isShadeExpanded();
457             final boolean isShelfShowing = algorithmState.firstViewInShelf != null;
458             if (shadeClosed) {
459                 viewState.hidden = true;
460             } else {
461                 final float footerEnd = algorithmState.mCurrentExpandedYPosition
462                         + view.getIntrinsicHeight();
463                 final boolean noSpaceForFooter = footerEnd > ambientState.getStackEndHeight();
464                 ((FooterView.FooterViewState) viewState).hideContent =
465                         isShelfShowing || noSpaceForFooter
466                                 || (ambientState.isDismissAllInProgress()
467                                 && !hasOngoingNotifs(algorithmState));
468             }
469         } else {
470             if (view instanceof EmptyShadeView) {
471                 float fullHeight = ambientState.getLayoutMaxHeight() + mCloseHandleUnderlapHeight
472                         - ambientState.getStackY();
473                 viewState.yTranslation = (fullHeight - getMaxAllowedChildHeight(view)) / 2f;
474             } else if (view != ambientState.getTrackedHeadsUpRow()) {
475                 if (ambientState.isExpansionChanging()) {
476                     // We later update shelf state, then hide views below the shelf.
477                     viewState.hidden = false;
478                     viewState.inShelf = algorithmState.firstViewInShelf != null
479                             && i >= algorithmState.visibleChildren.indexOf(
480                             algorithmState.firstViewInShelf);
481                 } else if (ambientState.getShelf() != null) {
482                     // When pulsing (incoming notification on AOD), innerHeight is 0; clamp all
483                     // to shelf start, thereby hiding all notifications (except the first one, which
484                     // we later unhide in updatePulsingState)
485                     // TODO(b/192348384): merge InnerHeight with StackHeight
486                     // Note: Bypass pulse looks different, but when it is not expanding, we need
487                     //  to use the innerHeight which doesn't update continuously, otherwise we show
488                     //  more notifications than we should during this special transitional states.
489                     boolean bypassPulseNotExpanding = ambientState.isBypassEnabled()
490                             && ambientState.isOnKeyguard() && !ambientState.isPulseExpanding();
491                     final int stackBottom =
492                             !ambientState.isShadeExpanded() || ambientState.isDozing()
493                                     || bypassPulseNotExpanding
494                                     ? ambientState.getInnerHeight()
495                                     : (int) ambientState.getStackHeight();
496                     final int shelfStart =
497                             stackBottom - ambientState.getShelf().getIntrinsicHeight();
498                     viewState.yTranslation = Math.min(viewState.yTranslation, shelfStart);
499                     if (viewState.yTranslation >= shelfStart) {
500                         viewState.hidden = !view.isExpandAnimationRunning()
501                                 && !view.hasExpandingChild();
502                         viewState.inShelf = true;
503                         // Notifications in the shelf cannot be visible HUNs.
504                         viewState.headsUpIsVisible = false;
505                     }
506                 }
507             }
508             // Clip height of view right before shelf.
509             viewState.height = (int) (getMaxAllowedChildHeight(view) * expansionFraction);
510         }
511 
512         algorithmState.mCurrentYPosition += viewState.height
513                 + expansionFraction * mPaddingBetweenElements;
514         algorithmState.mCurrentExpandedYPosition += view.getIntrinsicHeight()
515                 + mPaddingBetweenElements;
516 
517         setLocation(view.getViewState(), algorithmState.mCurrentYPosition, i);
518         viewState.yTranslation += ambientState.getStackY();
519     }
520 
521     /**
522      * Get the gap height needed for before a view
523      *
524      * @param sectionProvider the sectionProvider used to understand the sections
525      * @param visibleIndex the visible index of this view in the list
526      * @param child the child asked about
527      * @param previousChild the child right before it or null if none
528      * @return the size of the gap needed or 0 if none is needed
529      */
getGapHeightForChild( SectionProvider sectionProvider, int visibleIndex, View child, View previousChild)530     public float getGapHeightForChild(
531             SectionProvider sectionProvider,
532             int visibleIndex,
533             View child,
534             View previousChild) {
535 
536         if (childNeedsGapHeight(sectionProvider, visibleIndex, child,
537                 previousChild)) {
538             return mGapHeight;
539         } else {
540             return 0;
541         }
542     }
543 
544     /**
545      * Does a given child need a gap, i.e spacing before a view?
546      *
547      * @param sectionProvider the sectionProvider used to understand the sections
548      * @param visibleIndex the visible index of this view in the list
549      * @param child the child asked about
550      * @param previousChild the child right before it or null if none
551      * @return if the child needs a gap height
552      */
childNeedsGapHeight( SectionProvider sectionProvider, int visibleIndex, View child, View previousChild)553     private boolean childNeedsGapHeight(
554             SectionProvider sectionProvider,
555             int visibleIndex,
556             View child,
557             View previousChild) {
558         return sectionProvider.beginsSection(child, previousChild)
559                 && visibleIndex > 0
560                 && !(previousChild instanceof SectionHeaderView)
561                 && !(child instanceof FooterView);
562     }
563 
updatePulsingStates(StackScrollAlgorithmState algorithmState, AmbientState ambientState)564     private void updatePulsingStates(StackScrollAlgorithmState algorithmState,
565             AmbientState ambientState) {
566         int childCount = algorithmState.visibleChildren.size();
567         for (int i = 0; i < childCount; i++) {
568             View child = algorithmState.visibleChildren.get(i);
569             if (!(child instanceof ExpandableNotificationRow)) {
570                 continue;
571             }
572             ExpandableNotificationRow row = (ExpandableNotificationRow) child;
573             if (!row.showingPulsing() || (i == 0 && ambientState.isPulseExpanding())) {
574                 continue;
575             }
576             ExpandableViewState viewState = row.getViewState();
577             viewState.hidden = false;
578         }
579     }
580 
updateHeadsUpStates(StackScrollAlgorithmState algorithmState, AmbientState ambientState)581     private void updateHeadsUpStates(StackScrollAlgorithmState algorithmState,
582             AmbientState ambientState) {
583         int childCount = algorithmState.visibleChildren.size();
584 
585         // Move the tracked heads up into position during the appear animation, by interpolating
586         // between the HUN inset (where it will appear as a HUN) and the end position in the shade
587         float headsUpTranslation = mHeadsUpInset - ambientState.getStackTopMargin();
588         ExpandableNotificationRow trackedHeadsUpRow = ambientState.getTrackedHeadsUpRow();
589         if (trackedHeadsUpRow != null) {
590             ExpandableViewState childState = trackedHeadsUpRow.getViewState();
591             if (childState != null) {
592                 float endPosition = childState.yTranslation - ambientState.getStackTranslation();
593                 childState.yTranslation = MathUtils.lerp(
594                         headsUpTranslation, endPosition, ambientState.getAppearFraction());
595             }
596         }
597 
598         ExpandableNotificationRow topHeadsUpEntry = null;
599         for (int i = 0; i < childCount; i++) {
600             View child = algorithmState.visibleChildren.get(i);
601             if (!(child instanceof ExpandableNotificationRow)) {
602                 continue;
603             }
604             ExpandableNotificationRow row = (ExpandableNotificationRow) child;
605             if (!(row.isHeadsUp() || row.isHeadsUpAnimatingAway())) {
606                 continue;
607             }
608             ExpandableViewState childState = row.getViewState();
609             if (topHeadsUpEntry == null && row.mustStayOnScreen() && !childState.headsUpIsVisible) {
610                 topHeadsUpEntry = row;
611                 childState.location = ExpandableViewState.LOCATION_FIRST_HUN;
612             }
613             boolean isTopEntry = topHeadsUpEntry == row;
614             float unmodifiedEndLocation = childState.yTranslation + childState.height;
615             if (mIsExpanded) {
616                 if (row.mustStayOnScreen() && !childState.headsUpIsVisible
617                         && !row.showingPulsing()) {
618                     // Ensure that the heads up is always visible even when scrolled off
619                     clampHunToTop(ambientState, row, childState);
620                     if (isTopEntry && row.isAboveShelf()) {
621                         // the first hun can't get off screen.
622                         clampHunToMaxTranslation(ambientState, row, childState);
623                         childState.hidden = false;
624                     }
625                 }
626             }
627             if (row.isPinned()) {
628                 childState.yTranslation = Math.max(childState.yTranslation, headsUpTranslation);
629                 childState.height = Math.max(row.getIntrinsicHeight(), childState.height);
630                 childState.hidden = false;
631                 ExpandableViewState topState =
632                         topHeadsUpEntry == null ? null : topHeadsUpEntry.getViewState();
633                 if (topState != null && !isTopEntry && (!mIsExpanded
634                         || unmodifiedEndLocation > topState.yTranslation + topState.height)) {
635                     // Ensure that a headsUp doesn't vertically extend further than the heads-up at
636                     // the top most z-position
637                     childState.height = row.getIntrinsicHeight();
638                     childState.yTranslation = Math.min(topState.yTranslation + topState.height
639                             - childState.height, childState.yTranslation);
640                 }
641 
642                 // heads up notification show and this row is the top entry of heads up
643                 // notifications. i.e. this row should be the only one row that has input field
644                 // To check if the row need to do translation according to scroll Y
645                 // heads up show full of row's content and any scroll y indicate that the
646                 // translationY need to move up the HUN.
647                 if (!mIsExpanded && isTopEntry && ambientState.getScrollY() > 0) {
648                     childState.yTranslation -= ambientState.getScrollY();
649                 }
650             }
651             if (row.isHeadsUpAnimatingAway()) {
652                 childState.yTranslation = Math.max(childState.yTranslation, mHeadsUpInset);
653                 childState.hidden = false;
654             }
655         }
656     }
657 
clampHunToTop(AmbientState ambientState, ExpandableNotificationRow row, ExpandableViewState childState)658     private void clampHunToTop(AmbientState ambientState, ExpandableNotificationRow row,
659             ExpandableViewState childState) {
660         float newTranslation = Math.max(ambientState.getTopPadding()
661                 + ambientState.getStackTranslation(), childState.yTranslation);
662         childState.height = (int) Math.max(childState.height - (newTranslation
663                 - childState.yTranslation), row.getCollapsedHeight());
664         childState.yTranslation = newTranslation;
665     }
666 
clampHunToMaxTranslation(AmbientState ambientState, ExpandableNotificationRow row, ExpandableViewState childState)667     private void clampHunToMaxTranslation(AmbientState ambientState, ExpandableNotificationRow row,
668             ExpandableViewState childState) {
669         float newTranslation;
670         float maxHeadsUpTranslation = ambientState.getMaxHeadsUpTranslation();
671         float maxShelfPosition = ambientState.getInnerHeight() + ambientState.getTopPadding()
672                 + ambientState.getStackTranslation();
673         maxHeadsUpTranslation = Math.min(maxHeadsUpTranslation, maxShelfPosition);
674         float bottomPosition = maxHeadsUpTranslation - row.getCollapsedHeight();
675         newTranslation = Math.min(childState.yTranslation, bottomPosition);
676         childState.height = (int) Math.min(childState.height, maxHeadsUpTranslation
677                 - newTranslation);
678         childState.yTranslation = newTranslation;
679     }
680 
getMaxAllowedChildHeight(View child)681     protected int getMaxAllowedChildHeight(View child) {
682         if (child instanceof ExpandableView) {
683             ExpandableView expandableView = (ExpandableView) child;
684             return expandableView.getIntrinsicHeight();
685         }
686         return child == null ? mCollapsedSize : child.getHeight();
687     }
688 
689     /**
690      * Calculate the Z positions for all children based on the number of items in both stacks and
691      * save it in the resultState
692      *
693      * @param algorithmState The state in which the current pass of the algorithm is currently in
694      * @param ambientState   The ambient state of the algorithm
695      */
updateZValuesForState(StackScrollAlgorithmState algorithmState, AmbientState ambientState)696     private void updateZValuesForState(StackScrollAlgorithmState algorithmState,
697             AmbientState ambientState) {
698         int childCount = algorithmState.visibleChildren.size();
699         float childrenOnTop = 0.0f;
700 
701         int topHunIndex = -1;
702         for (int i = 0; i < childCount; i++) {
703             ExpandableView child = algorithmState.visibleChildren.get(i);
704             if (child instanceof ActivatableNotificationView
705                     && (child.isAboveShelf() || child.showingPulsing())) {
706                 topHunIndex = i;
707                 break;
708             }
709         }
710 
711         for (int i = childCount - 1; i >= 0; i--) {
712             childrenOnTop = updateChildZValue(i, childrenOnTop,
713                     algorithmState, ambientState, i == topHunIndex);
714         }
715     }
716 
updateChildZValue(int i, float childrenOnTop, StackScrollAlgorithmState algorithmState, AmbientState ambientState, boolean shouldElevateHun)717     protected float updateChildZValue(int i, float childrenOnTop,
718             StackScrollAlgorithmState algorithmState,
719             AmbientState ambientState,
720             boolean shouldElevateHun) {
721         ExpandableView child = algorithmState.visibleChildren.get(i);
722         ExpandableViewState childViewState = child.getViewState();
723         int zDistanceBetweenElements = ambientState.getZDistanceBetweenElements();
724         float baseZ = ambientState.getBaseZHeight();
725         if (child.mustStayOnScreen() && !childViewState.headsUpIsVisible
726                 && !ambientState.isDozingAndNotPulsing(child)
727                 && childViewState.yTranslation < ambientState.getTopPadding()
728                 + ambientState.getStackTranslation()) {
729             if (childrenOnTop != 0.0f) {
730                 childrenOnTop++;
731             } else {
732                 float overlap = ambientState.getTopPadding()
733                         + ambientState.getStackTranslation() - childViewState.yTranslation;
734                 childrenOnTop += Math.min(1.0f, overlap / childViewState.height);
735             }
736             childViewState.zTranslation = baseZ
737                     + childrenOnTop * zDistanceBetweenElements;
738         } else if (shouldElevateHun) {
739             // In case this is a new view that has never been measured before, we don't want to
740             // elevate if we are currently expanded more then the notification
741             int shelfHeight = ambientState.getShelf() == null ? 0 :
742                     ambientState.getShelf().getIntrinsicHeight();
743             float shelfStart = ambientState.getInnerHeight()
744                     - shelfHeight + ambientState.getTopPadding()
745                     + ambientState.getStackTranslation();
746             float notificationEnd = childViewState.yTranslation + child.getIntrinsicHeight()
747                     + mPaddingBetweenElements;
748             if (shelfStart > notificationEnd) {
749                 childViewState.zTranslation = baseZ;
750             } else {
751                 float factor = (notificationEnd - shelfStart) / shelfHeight;
752                 factor = Math.min(factor, 1.0f);
753                 childViewState.zTranslation = baseZ + factor * zDistanceBetweenElements;
754             }
755         } else {
756             childViewState.zTranslation = baseZ;
757         }
758 
759         // We need to scrim the notification more from its surrounding content when we are pinned,
760         // and we therefore elevate it higher.
761         // We can use the headerVisibleAmount for this, since the value nicely goes from 0 to 1 when
762         // expanding after which we have a normal elevation again.
763         childViewState.zTranslation += (1.0f - child.getHeaderVisibleAmount())
764                 * mPinnedZTranslationExtra;
765         return childrenOnTop;
766     }
767 
setIsExpanded(boolean isExpanded)768     public void setIsExpanded(boolean isExpanded) {
769         this.mIsExpanded = isExpanded;
770     }
771 
772     public static class StackScrollAlgorithmState {
773 
774         /**
775          * The scroll position of the algorithm (absolute scrolling).
776          */
777         public int scrollY;
778 
779         /**
780          * First view in shelf.
781          */
782         public ExpandableView firstViewInShelf;
783 
784         /**
785          * The children from the host view which are not gone.
786          */
787         public final ArrayList<ExpandableView> visibleChildren = new ArrayList<>();
788 
789         /**
790          * Y position of the current view during updating children
791          * with expansion factor applied.
792          */
793         private int mCurrentYPosition;
794 
795         /**
796          * Y position of the current view during updating children
797          * without applying the expansion factor.
798          */
799         private int mCurrentExpandedYPosition;
800     }
801 
802     /**
803      * Interface for telling the SSA when a new notification section begins (so it can add in
804      * appropriate margins).
805      */
806     public interface SectionProvider {
807         /**
808          * True if this view starts a new "section" of notifications, such as the gentle
809          * notifications section. False if sections are not enabled.
810          */
beginsSection(@onNull View view, @Nullable View previous)811         boolean beginsSection(@NonNull View view, @Nullable View previous);
812     }
813 
814     /**
815      * Interface for telling the StackScrollAlgorithm information about the bypass state
816      */
817     public interface BypassController {
818         /**
819          * True if bypass is enabled.  Note that this is always false if face auth is not enabled.
820          */
isBypassEnabled()821         boolean isBypassEnabled();
822     }
823 }
824