• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 static android.app.Flags.notificationsRedesignTemplates;
20 import static android.os.Trace.TRACE_TAG_APP;
21 import static android.view.MotionEvent.ACTION_CANCEL;
22 import static android.view.MotionEvent.ACTION_UP;
23 
24 import static com.android.app.tracing.TrackGroupUtils.trackGroup;
25 import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_SCROLL_FLING;
26 import static com.android.internal.jank.InteractionJankMonitor.CUJ_SHADE_CLEAR_ALL;
27 import static com.android.systemui.Flags.magneticNotificationSwipes;
28 import static com.android.systemui.Flags.notificationOverExpansionClippingFix;
29 import static com.android.systemui.Flags.physicalNotificationMovement;
30 import static com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt.BUCKET_SILENT;
31 import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_SWIPE;
32 import static com.android.systemui.statusbar.notification.stack.shared.model.AccessibilityScrollEvent.SCROLL_DOWN;
33 import static com.android.systemui.statusbar.notification.stack.shared.model.AccessibilityScrollEvent.SCROLL_UP;
34 import static com.android.systemui.util.DumpUtilsKt.println;
35 import static com.android.systemui.util.DumpUtilsKt.visibilityString;
36 
37 import static java.lang.annotation.RetentionPolicy.SOURCE;
38 
39 import android.annotation.ColorInt;
40 import android.annotation.DrawableRes;
41 import android.annotation.FloatRange;
42 import android.annotation.IntDef;
43 import android.annotation.NonNull;
44 import android.annotation.Nullable;
45 import android.annotation.StringRes;
46 import android.content.Context;
47 import android.content.Intent;
48 import android.content.res.Configuration;
49 import android.content.res.Resources;
50 import android.graphics.Canvas;
51 import android.graphics.Color;
52 import android.graphics.Outline;
53 import android.graphics.Paint;
54 import android.graphics.Path;
55 import android.graphics.Rect;
56 import android.graphics.RenderEffect;
57 import android.graphics.RenderNode;
58 import android.graphics.Shader;
59 import android.os.Bundle;
60 import android.os.SystemClock;
61 import android.os.Trace;
62 import android.provider.Settings;
63 import android.util.AttributeSet;
64 import android.util.IndentingPrintWriter;
65 import android.util.Log;
66 import android.util.MathUtils;
67 import android.view.DisplayCutout;
68 import android.view.InputDevice;
69 import android.view.LayoutInflater;
70 import android.view.MotionEvent;
71 import android.view.NotificationHeaderView;
72 import android.view.VelocityTracker;
73 import android.view.View;
74 import android.view.ViewConfiguration;
75 import android.view.ViewGroup;
76 import android.view.ViewOutlineProvider;
77 import android.view.ViewTreeObserver;
78 import android.view.WindowInsets;
79 import android.view.WindowInsetsAnimation;
80 import android.view.accessibility.AccessibilityEvent;
81 import android.view.accessibility.AccessibilityNodeInfo;
82 import android.view.animation.AnimationUtils;
83 import android.view.animation.Interpolator;
84 import android.widget.OverScroller;
85 import android.widget.ScrollView;
86 
87 import com.android.app.animation.Interpolators;
88 import com.android.internal.annotations.VisibleForTesting;
89 import com.android.internal.jank.InteractionJankMonitor;
90 import com.android.internal.policy.SystemBarUtils;
91 import com.android.keyguard.BouncerPanelExpansionCalculator;
92 import com.android.keyguard.KeyguardSliceView;
93 import com.android.systemui.Dependency;
94 import com.android.systemui.Dumpable;
95 import com.android.systemui.ExpandHelper;
96 import com.android.systemui.flags.FeatureFlags;
97 import com.android.systemui.flags.Flags;
98 import com.android.systemui.plugins.ActivityStarter;
99 import com.android.systemui.qs.flags.QSComposeFragment;
100 import com.android.systemui.res.R;
101 import com.android.systemui.scene.shared.flag.SceneContainerFlag;
102 import com.android.systemui.shade.QSHeaderBoundsProvider;
103 import com.android.systemui.shade.TouchLogger;
104 import com.android.systemui.statusbar.NotificationShelf;
105 import com.android.systemui.statusbar.StatusBarState;
106 import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips;
107 import com.android.systemui.statusbar.headsup.shared.StatusBarNoHunBehavior;
108 import com.android.systemui.statusbar.notification.ColorUpdateLogger;
109 import com.android.systemui.statusbar.notification.FakeShadowView;
110 import com.android.systemui.statusbar.notification.LaunchAnimationParameters;
111 import com.android.systemui.statusbar.notification.NotificationTransitionAnimatorController;
112 import com.android.systemui.statusbar.notification.NotificationUtils;
113 import com.android.systemui.statusbar.notification.PhysicsPropertyAnimator;
114 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
115 import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
116 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
117 import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix;
118 import com.android.systemui.statusbar.notification.emptyshade.ui.view.EmptyShadeView;
119 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView;
120 import com.android.systemui.statusbar.notification.headsup.HeadsUpAnimationEvent;
121 import com.android.systemui.statusbar.notification.headsup.HeadsUpAnimator;
122 import com.android.systemui.statusbar.notification.headsup.HeadsUpTouchHelper;
123 import com.android.systemui.statusbar.notification.headsup.HeadsUpUtil;
124 import com.android.systemui.statusbar.notification.headsup.NotificationsHunSharedAnimationValues;
125 import com.android.systemui.statusbar.notification.logging.NotificationLogger;
126 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
127 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
128 import com.android.systemui.statusbar.notification.row.ExpandableView;
129 import com.android.systemui.statusbar.notification.row.StackScrollerDecorView;
130 import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
131 import com.android.systemui.statusbar.notification.shared.NotificationContentAlphaOptimization;
132 import com.android.systemui.statusbar.notification.shared.NotificationHeadsUpCycling;
133 import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun;
134 import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor;
135 import com.android.systemui.statusbar.notification.stack.shared.model.AccessibilityScrollEvent;
136 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimBounds;
137 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape;
138 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrollState;
139 import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView;
140 import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
141 import com.android.systemui.statusbar.policy.ScrollAdapter;
142 import com.android.systemui.statusbar.policy.SplitShadeStateController;
143 import com.android.systemui.util.Assert;
144 import com.android.systemui.util.ColorUtilKt;
145 import com.android.systemui.util.DumpUtilsKt;
146 import com.android.systemui.util.ListenerSet;
147 import com.android.systemui.wallpapers.domain.interactor.WallpaperInteractor;
148 
149 import com.google.errorprone.annotations.CompileTimeConstant;
150 
151 import java.io.PrintWriter;
152 import java.lang.annotation.Retention;
153 import java.util.ArrayList;
154 import java.util.Collections;
155 import java.util.Comparator;
156 import java.util.HashMap;
157 import java.util.HashSet;
158 import java.util.List;
159 import java.util.Map;
160 import java.util.Objects;
161 import java.util.Set;
162 import java.util.concurrent.Callable;
163 import java.util.function.BiConsumer;
164 import java.util.function.Consumer;
165 
166 /**
167  * A layout which handles a dynamic amount of notifications and presents them in a scrollable stack.
168  */
169 public class NotificationStackScrollLayout
170         extends ViewGroup
171         implements Dumpable, NotificationScrollView {
172     public static final float BACKGROUND_ALPHA_DIMMED = 0.7f;
173     private static final String TAG = "StackScroller";
174     private static final boolean SPEW = Log.isLoggable(TAG, Log.VERBOSE);
175 
176     private boolean mShadeNeedsToClose = false;
177 
178     @VisibleForTesting
179     static final float RUBBER_BAND_FACTOR_NORMAL = 0.1f;
180     private static final float RUBBER_BAND_FACTOR_AFTER_EXPAND = 0.15f;
181     private static final float RUBBER_BAND_FACTOR_ON_PANEL_EXPAND = 0.21f;
182     /**
183      * Sentinel value for no current active pointer. Used by {@link #mActivePointerId}.
184      */
185     private static final int INVALID_POINTER = -1;
186     private boolean mKeyguardBypassEnabled;
187 
188     private final ExpandHelper mExpandHelper;
189     private NotificationSwipeHelper mSwipeHelper;
190     private int mCurrentStackHeight = Integer.MAX_VALUE;
191     private boolean mHighPriorityBeforeSpeedBump;
192 
193     private float mExpandedHeight;
194     private int mOwnScrollY;
195     private int mMaxLayoutHeight;
196 
197     private VelocityTracker mVelocityTracker;
198     private OverScroller mScroller;
199 
200     private Runnable mFinishScrollingCallback;
201     private int mTouchSlop;
202     private float mSlopMultiplier;
203     private int mMinimumVelocity;
204     private int mMaximumVelocity;
205     private int mOverflingDistance;
206     private float mMaxOverScroll;
207     private boolean mIsBeingDragged;
208     private boolean mSendingTouchesToSceneFramework;
209     private int mLastMotionY;
210     private int mDownX;
211     private int mActivePointerId = INVALID_POINTER;
212     private boolean mTouchIsClick;
213     private float mInitialTouchX;
214     private float mInitialTouchY;
215 
216     private final boolean mDebugLines;
217     private Paint mDebugPaint;
218     /**
219      * Used to track the Y positions that were already used to draw debug text labels.
220      */
221     private Set<Integer> mDebugTextUsedYPositions;
222     private final boolean mDebugRemoveAnimation;
223     private int mContentHeight;
224     private float mIntrinsicContentHeight;
225     private int mPaddingBetweenElements;
226     private int mMaxTopPadding;
227     private boolean mAnimateNextTopPaddingChange;
228     private int mBottomPadding;
229     @VisibleForTesting
230     // mImeInset=0 when IME is hidden
231     int mImeInset = 0;
232     private float mQsExpansionFraction;
233     private final int mSplitShadeMinContentHeight;
234     private String mLastUpdateSidePaddingDumpString;
235     private long mLastUpdateSidePaddingElapsedRealtime;
236     private String mLastInitViewDumpString;
237     private long mLastInitViewElapsedRealtime;
238 
239     @Nullable
240     private final HeadsUpAnimator mHeadsUpAnimator;
241     /**
242      * The algorithm which calculates the properties for our children
243      */
244     private final StackScrollAlgorithm mStackScrollAlgorithm;
245     private final AmbientState mAmbientState;
246     private final ScrollViewFields mScrollViewFields = new ScrollViewFields();
247 
248     private final GroupMembershipManager mGroupMembershipManager;
249     private final GroupExpansionManager mGroupExpansionManager;
250     private final HashSet<ExpandableView> mChildrenToAddAnimated = new HashSet<>();
251     private final ArrayList<View> mAddedHeadsUpChildren = new ArrayList<>();
252     private final ArrayList<ExpandableView> mChildrenToRemoveAnimated = new ArrayList<>();
253     private final ArrayList<ExpandableView> mChildrenChangingPositions = new ArrayList<>();
254     private final HashSet<View> mFromMoreCardAdditions = new HashSet<>();
255     private final ArrayList<AnimationEvent> mAnimationEvents = new ArrayList<>();
256     private final ArrayList<View> mSwipedOutViews = new ArrayList<>();
257     private NotificationStackSizeCalculator mNotificationStackSizeCalculator;
258     private final StackStateAnimator mStateAnimator;
259     // TODO(b/332732878): call setAnimationsEnabled with scene container enabled, then remove this
260     private boolean mAnimationsEnabled = SceneContainerFlag.isEnabled();
261     private boolean mChangePositionInProgress;
262     private boolean mChildTransferInProgress;
263 
264     private int mSpeedBumpIndex = -1;
265     private boolean mSpeedBumpIndexDirty = true;
266 
267     /**
268      * The raw amount of the overScroll on the top, which is not rubber-banded.
269      */
270     private float mOverScrolledTopPixels;
271 
272     /**
273      * The raw amount of the overScroll on the bottom, which is not rubber-banded.
274      */
275     private float mOverScrolledBottomPixels;
276     private final ListenerSet<Runnable> mStackHeightChangedListeners = new ListenerSet<>();
277     private final ListenerSet<Runnable> mHeadsUpHeightChangedListeners = new ListenerSet<>();
278     private NotificationLogger.OnChildLocationsChangedListener mLegacyLocationsChangedListener;
279     private OnNotificationLocationsChangedListener mLocationsChangedListener;
280     private OnOverscrollTopChangedListener mOverscrollTopChangedListener;
281     private ExpandableView.OnHeightChangedListener mOnHeightChangedListener;
282     private Runnable mOnHeightChangedRunnable;
283     private OnEmptySpaceClickListener mOnEmptySpaceClickListener;
284     private boolean mNeedsAnimation;
285     private boolean mTopPaddingNeedsAnimation;
286     private boolean mHideSensitiveNeedsAnimation;
287     private boolean mActivateNeedsAnimation;
288     private boolean mGoToFullShadeNeedsAnimation;
289     private boolean mIsExpanded = true;
290     private boolean mChildrenUpdateRequested;
291     private boolean mIsExpansionChanging;
292     private boolean mPanelTracking;
293     private boolean mExpandingNotification;
294     private boolean mExpandedInThisMotion;
295     private boolean mShouldShowShelfOnly;
296     protected boolean mScrollingEnabled;
297     protected FooterView mFooterView;
298     protected EmptyShadeView mEmptyShadeView;
299     private boolean mClearAllInProgress;
300     private boolean mFlingAfterUpEvent;
301     /**
302      * Was the scroller scrolled to the top when the down motion was observed?
303      */
304     private boolean mScrolledToTopOnFirstDown;
305     /**
306      * The minimal amount of over scroll which is needed in order to switch to the quick settings
307      * when over scrolling on a expanded card.
308      */
309     private float mMinTopOverScrollToEscape;
310     private int mIntrinsicPadding;
311     private float mTopPaddingOverflow;
312     private boolean mDontReportNextOverScroll;
313     private boolean mDontClampNextScroll;
314     private boolean mNeedViewResizeAnimation;
315     private ExpandableView mExpandedGroupView;
316     private boolean mEverythingNeedsAnimation;
317 
318     /**
319      * The maximum scrollPosition which we are allowed to reach when a notification was expanded.
320      * This is needed to avoid scrolling too far after the notification was collapsed in the same
321      * motion.
322      */
323     private int mMaxScrollAfterExpand;
324     boolean mCheckForLeavebehind;
325 
326     /**
327      * Should in this touch motion only be scrolling allowed? It's true when the scroller was
328      * animating.
329      */
330     private boolean mOnlyScrollingInThisMotion;
331     private boolean mDisallowDismissInThisMotion;
332     private boolean mDisallowScrollingInThisMotion;
333     private long mGoToFullShadeDelay;
334     private final ViewTreeObserver.OnPreDrawListener mChildrenUpdater
335             = new ViewTreeObserver.OnPreDrawListener() {
336         @Override
337         public boolean onPreDraw() {
338             if (SceneContainerFlag.isEnabled()) {
339                 getViewTreeObserver().removeOnPreDrawListener(this);
340                 return true;
341             }
342             updateForcedScroll();
343             updateChildren();
344             mChildrenUpdateRequested = false;
345             getViewTreeObserver().removeOnPreDrawListener(this);
346             return true;
347         }
348     };
349     private NotificationStackScrollLogger mLogger;
350     private Runnable mResetUserExpandedStatesRunnable;
351     private ActivityStarter mActivityStarter;
352     private final int[] mTempInt2 = new int[2];
353     private final HashSet<Runnable> mAnimationFinishedRunnables = new HashSet<>();
354     private final HashSet<ExpandableView> mClearTransientViewsWhenFinished = new HashSet<>();
355     private final Map<ExpandableNotificationRow, HeadsUpAnimationEvent> mHeadsUpChangeAnimations
356             = new HashMap<>();
357     private boolean mForceNoOverlappingRendering;
358     private final ArrayList<ExpandableNotificationRow> mTmpHeadsUpChangeAnimations =
359             new ArrayList<>();
360     private boolean mAnimationRunning;
361     private final ViewTreeObserver.OnPreDrawListener mRunningAnimationUpdater
362             = new ViewTreeObserver.OnPreDrawListener() {
363         @Override
364         public boolean onPreDraw() {
365             onPreDrawDuringAnimation();
366             return true;
367         }
368     };
369     private final NotificationSection[] mSections;
370     private final ArrayList<ExpandableView> mTmpSortedChildren = new ArrayList<>();
371     protected ViewGroup mQsHeader;
372 
373     @Nullable
374     private QSHeaderBoundsProvider mQSHeaderBoundsProvider;
375 
376     // Rect of QsHeader. Kept as a field just to avoid creating a new one each time.
377     private final Rect mQsHeaderBound = new Rect();
378     private boolean mContinuousShadowUpdate;
379     private final ViewTreeObserver.OnPreDrawListener mShadowUpdater = () -> {
380         updateViewShadows();
381         return true;
382     };
383     private final Comparator<ExpandableView> mViewPositionComparator = (view, otherView) -> {
384         float endY = view.getTranslationY() + view.getActualHeight();
385         float otherEndY = otherView.getTranslationY() + otherView.getActualHeight();
386         // Return zero when the two notifications end at the same location
387         return Float.compare(endY, otherEndY);
388     };
389     private final ViewOutlineProvider mOutlineProvider = new ViewOutlineProvider() {
390         @Override
391         public void getOutline(View view, Outline outline) {
392             if (mAmbientState.isHiddenAtAll()) {
393                 float xProgress = mHideXInterpolator.getInterpolation(
394                         (1 - mLinearHideAmount) * mBackgroundXFactor);
395                 outline.setRoundRect(mBackgroundAnimationRect,
396                         MathUtils.lerp(mCornerRadius / 2.0f, mCornerRadius,
397                                 xProgress));
398                 outline.setAlpha(1.0f - mAmbientState.getHideAmount());
399             } else {
400                 ViewOutlineProvider.BACKGROUND.getOutline(view, outline);
401             }
402         }
403     };
404 
405     private final Callable<Map<String, Integer>> collectVisibleLocationsCallable =
406             new Callable<>() {
407                 @Override
408                 public Map<String, Integer> call() {
409                     return collectVisibleNotificationLocations();
410                 }
411             };
412 
413     private boolean mPulsing;
414     private boolean mScrollable;
415     private View mForcedScroll;
416     private boolean mIsInsetAnimationRunning;
417 
418     private final WindowInsetsAnimation.Callback mInsetsCallback =
419             new WindowInsetsAnimation.Callback(
420                     WindowInsetsAnimation.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
421 
422                 @Override
423                 public void onPrepare(WindowInsetsAnimation animation) {
424                     mIsInsetAnimationRunning = true;
425                 }
426 
427                 @Override
428                 public WindowInsets onProgress(WindowInsets windowInsets,
429                         List<WindowInsetsAnimation> list) {
430                     updateImeInset(windowInsets);
431                     return windowInsets;
432                 }
433 
434                 @Override
435                 public void onEnd(WindowInsetsAnimation animation) {
436                     mIsInsetAnimationRunning = false;
437                 }
438             };
439 
440     /**
441      * @see #setHideAmount(float, float)
442      */
443     private float mInterpolatedHideAmount = 0f;
444 
445     /**
446      * @see #setHideAmount(float, float)
447      */
448     private float mLinearHideAmount = 0f;
449 
450     /**
451      * How fast the background scales in the X direction as a factor of the Y expansion.
452      */
453     private float mBackgroundXFactor = 1f;
454 
455     /**
456      * Indicates QS are full screen and pushing notifications out of the screen.
457      * It's different from QS just being expanded as in split shade QS can be expanded and
458      * still don't take full screen nor influence notifications.
459      */
460     private boolean mQsFullScreen;
461     private boolean mForwardScrollable;
462     private boolean mBackwardScrollable;
463     private NotificationShelf mShelf;
464     /**
465      * Limits the number of visible notifications. The remaining are collapsed in the notification
466      * shelf. -1 when there is no limit.
467      */
468     private int mMaxDisplayedNotifications = -1;
469     private float mKeyguardBottomPadding = -1;
470     @VisibleForTesting
471     int mStatusBarHeight;
472     private int mMinInteractionHeight;
473     private final Rect mClipRect = new Rect();
474     private boolean mIsClipped;
475     private Rect mRequestedClipBounds;
476     private boolean mInHeadsUpPinnedMode;
477     @VisibleForTesting
478     boolean mHeadsUpAnimatingAway;
479     private Consumer<Boolean> mHeadsUpAnimatingAwayListener;
480     private int mStatusBarState;
481     private boolean mHeadsUpGoingAwayAnimationsAllowed = true;
482     private final Runnable mReflingAndAnimateScroll = this::animateScroll;
483     private int mCornerRadius;
484     private int mMinimumPaddings;
485     private int mQsTilePadding;
486     private boolean mSkinnyNotifsInLandscape;
487     private int mSidePaddings;
488     private final Rect mBackgroundAnimationRect = new Rect();
489     private final ArrayList<BiConsumer<Float, Float>> mExpandedHeightListeners = new ArrayList<>();
490     private int mHeadsUpInset;
491 
492     /**
493      * The position of the scroll boundary relative to this view. This is where the notifications
494      * stop scrolling and will start to clip instead.
495      */
496     private int mQsScrollBoundaryPosition;
497     private HeadsUpAppearanceController mHeadsUpAppearanceController;
498     private final Rect mTmpRect = new Rect();
499     private ClearAllListener mClearAllListener;
500     private ClearAllAnimationListener mClearAllAnimationListener;
501     private Runnable mClearAllFinishedWhilePanelExpandedRunnable;
502     private Consumer<Boolean> mOnStackYChanged;
503 
504     private Interpolator mHideXInterpolator = Interpolators.FAST_OUT_SLOW_IN;
505 
506     private final NotificationSectionsManager mSectionsManager;
507     private float mLastSentAppear;
508     private float mLastSentExpandedHeight;
509     private boolean mWillExpand;
510     private int mGapHeight;
511 
512     /**
513      * The extra inset during the full shade transition
514      */
515     private float mExtraTopInsetForFullShadeTransition;
516 
517     private int mWaterfallTopInset;
518     private NotificationStackScrollLayoutController mController;
519 
520     /**
521      * The clip path used to clip the view in a rounded way.
522      */
523     private final Path mRoundedClipPath = new Path();
524 
525     /** The clip path defining where we are NOT allowed to draw. */
526     private final Path mNegativeRoundedClipPath = new Path();
527 
528     /** RenderNode to blur notifications which will be reused (redrawn) whenever NSSL is drawn. */
529     private final RenderNode mBlurNode = new RenderNode("BlurNode");
530 
531     /** Radius of the blur effect applied to the content of the NSSL. */
532     private float mBlurRadius = 0f;
533     @Nullable private RenderEffect mBlurEffect = null;
534 
535     /**
536      * The clip Path used to clip the launching notification. This may be different
537      * from the normal path, as the views launch animation could start clipped.
538      */
539     private final Path mLaunchedNotificationClipPath = new Path();
540 
541     /** Should we use rounded rect clipping right now */
542     private boolean mShouldUseRoundedRectClipping = false;
543 
544     /** Should we set an out path for the drawing canvas */
545     private boolean mShouldUseNegativeRoundedRectClipping = false;
546 
547     private int mRoundedRectClippingLeft;
548     private int mRoundedRectClippingTop;
549     private int mRoundedRectClippingBottom;
550     private int mRoundedRectClippingRight;
551     private int mRoundedRectClippingYTranslation;
552     private final float[] mRoundedClipCornerRadii = new float[8];
553 
554     /**
555      * Whether stackY should be animated in case the view is getting shorter than the scroll
556      * position and this scrolling will lead to the top scroll inset getting smaller.
557      */
558     private boolean mAnimateStackYForContentHeightChange = false;
559 
560     /**
561      * Are we launching a notification right now
562      */
563     private boolean mLaunchingNotification;
564 
565     /**
566      * Does the launching notification need to be clipped
567      */
568     private boolean mLaunchingNotificationNeedsToBeClipped;
569 
570     /**
571      * The current launch animation params when launching a notification
572      */
573     private LaunchAnimationParameters mLaunchAnimationParams;
574 
575     /**
576      * Corner radii of the launched notification if it's clipped
577      */
578     private final float[] mLaunchedNotificationRadii = new float[8];
579 
580     /**
581      * The notification that is being launched currently.
582      */
583     private ExpandableNotificationRow mExpandingNotificationRow;
584 
585     /**
586      * Do notifications dismiss with normal transitioning
587      */
588     private boolean mDismissUsingRowTranslationX = true;
589     private ExpandableNotificationRow mTopHeadsUpRow;
590     private NotificationStackScrollLayoutController.TouchHandler mTouchHandler;
591     private boolean mShouldUseSplitNotificationShade;
592     private boolean mShouldSkipTopPaddingAnimationAfterFold = false;
593     @Nullable private SplitShadeStateController mSplitShadeStateController = null;
594     private boolean mIsSmallLandscapeLockscreenEnabled = false;
595     private boolean mSuppressHeightUpdates;
596     private boolean mIsOnLockscreen;
597 
598     /** Pass splitShadeStateController to view and update split shade */
passSplitShadeStateController(SplitShadeStateController splitShadeStateController)599     public void passSplitShadeStateController(SplitShadeStateController splitShadeStateController) {
600         mSplitShadeStateController = splitShadeStateController;
601         updateSplitNotificationShade();
602     }
603     private final FeatureFlags mFeatureFlags;
604 
605     private final ExpandableView.OnHeightChangedListener mOnChildHeightChangedListener =
606             new ExpandableView.OnHeightChangedListener() {
607                 @Override
608                 public void onHeightChanged(ExpandableView view, boolean needsAnimation) {
609                     onChildHeightChanged(view, needsAnimation);
610                 }
611 
612                 @Override
613                 public void onReset(ExpandableView view) {
614                     onChildHeightReset(view);
615                 }
616             };
617 
618     private final NotificationEntry.OnSensitivityChangedListener
619             mOnChildSensitivityChangedListener =
620             new NotificationEntry.OnSensitivityChangedListener() {
621                 @Override
622                 public void onSensitivityChanged(NotificationEntry entry) {
623                     if (mAnimationsEnabled) {
624                         mHideSensitiveNeedsAnimation = true;
625                         requestChildrenUpdate();
626                     }
627                 }
628             };
629 
630     private Consumer<Integer> mScrollListener;
631     private final ScrollAdapter mScrollAdapter = new ScrollAdapter() {
632         @Override
633         public boolean isScrolledToTop() {
634             if (SceneContainerFlag.isEnabled()) {
635                 return mScrollViewFields.getScrollState().isScrolledToTop();
636             } else {
637                 return getOwnScrollY() == 0;
638             }
639         }
640 
641         @Override
642         public boolean isScrolledToBottom() {
643             return getOwnScrollY() >= getScrollRange();
644         }
645 
646         @Override
647         public View getHostView() {
648             return NotificationStackScrollLayout.this;
649         }
650     };
651 
652     @Nullable
653     private WallpaperInteractor mWallpaperInteractor;
654 
NotificationStackScrollLayout(Context context, AttributeSet attrs)655     public NotificationStackScrollLayout(Context context, AttributeSet attrs) {
656         super(context, attrs, 0, 0);
657         Resources res = getResources();
658         mFeatureFlags = Dependency.get(FeatureFlags.class);
659         mIsSmallLandscapeLockscreenEnabled = mFeatureFlags.isEnabled(
660                 Flags.LOCKSCREEN_ENABLE_LANDSCAPE);
661         mDebugLines = mFeatureFlags.isEnabled(Flags.NSSL_DEBUG_LINES);
662         mDebugRemoveAnimation = mFeatureFlags.isEnabled(Flags.NSSL_DEBUG_REMOVE_ANIMATION);
663         mSectionsManager = Dependency.get(NotificationSectionsManager.class);
664         mSectionsManager.initialize(this);
665         mSections = mSectionsManager.createSectionsForBuckets();
666 
667         mAmbientState = Dependency.get(AmbientState.class);
668         int minHeight = res.getDimensionPixelSize(R.dimen.notification_min_height);
669         int maxHeight = res.getDimensionPixelSize(R.dimen.notification_max_height);
670         mSplitShadeMinContentHeight = res.getDimensionPixelSize(
671                 R.dimen.nssl_split_shade_min_content_height);
672         mExpandHelper = new ExpandHelper(getContext(), mExpandHelperCallback,
673                 minHeight, maxHeight);
674         mExpandHelper.setEventSource(this);
675         mExpandHelper.setScrollAdapter(mScrollAdapter);
676 
677         if (NotificationsHunSharedAnimationValues.isEnabled()) {
678             mHeadsUpAnimator = new HeadsUpAnimator(context, /* systemBarUtilsProxy= */ null);
679         } else {
680             mHeadsUpAnimator = null;
681         }
682         mStackScrollAlgorithm =  new StackScrollAlgorithm(context, this, mHeadsUpAnimator);
683         mStateAnimator = new StackStateAnimator(context, this, mHeadsUpAnimator);
684         setOutlineProvider(mOutlineProvider);
685 
686         // We could set this whenever we 'requestChildUpdate' much like the viewTreeObserver, but
687         // that adds a bunch of complexity, and drawing nothing isn't *that* expensive.
688         boolean willDraw = SceneContainerFlag.isEnabled() || mDebugLines;
689         setWillNotDraw(!willDraw);
690         if (mDebugLines) {
691             mDebugPaint = new Paint();
692             mDebugPaint.setColor(0xffff0000);
693             mDebugPaint.setStrokeWidth(2);
694             mDebugPaint.setStyle(Paint.Style.STROKE);
695             mDebugPaint.setTextSize(25f);
696         }
697         mGroupMembershipManager = Dependency.get(GroupMembershipManager.class);
698         mGroupExpansionManager = Dependency.get(GroupExpansionManager.class);
699         setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
700         if (!SceneContainerFlag.isEnabled()) {
701             setWindowInsetsAnimationCallback(mInsetsCallback);
702         }
703     }
704 
705     /**
706      * Set the overexpansion of the panel to be applied to the view.
707      */
setOverExpansion(float margin)708     void setOverExpansion(float margin) {
709         mAmbientState.setOverExpansion(margin);
710         if (notificationOverExpansionClippingFix() && !SceneContainerFlag.isEnabled()) {
711             setRoundingClippingYTranslation(mShouldUseSplitNotificationShade ? (int) margin : 0);
712         }
713         updateStackPosition();
714         requestChildrenUpdate();
715     }
716 
717     @Override
onFinishInflate()718     protected void onFinishInflate() {
719         super.onFinishInflate();
720 
721         if (!ModesEmptyShadeFix.isEnabled()) {
722             inflateEmptyShadeView();
723         }
724     }
725 
726     /**
727      * Sets whether keyguard bypass is enabled. If true, this layout will be rendered in bypass
728      * mode when it is on the keyguard.
729      */
setKeyguardBypassEnabled(boolean isEnabled)730     public void setKeyguardBypassEnabled(boolean isEnabled) {
731         mKeyguardBypassEnabled = isEnabled;
732     }
733 
734     /**
735      * @return the height at which we will wake up when pulsing
736      */
getWakeUpHeight()737     public float getWakeUpHeight() {
738         ExpandableView firstChild = getFirstChildWithBackground();
739         if (firstChild != null) {
740             if (mKeyguardBypassEnabled) {
741                 return firstChild.getHeadsUpHeightWithoutHeader();
742             } else {
743                 return firstChild.getCollapsedHeight();
744             }
745         }
746         return 0f;
747     }
748 
setLogger(NotificationStackScrollLogger logger)749     protected void setLogger(NotificationStackScrollLogger logger) {
750         mLogger = logger;
751     }
752 
getNotificationSquishinessFraction()753     public float getNotificationSquishinessFraction() {
754         SceneContainerFlag.assertInLegacyMode();
755         return mStackScrollAlgorithm.getNotificationSquishinessFraction(mAmbientState);
756     }
757 
reinflateViews()758     void reinflateViews() {
759         if (!ModesEmptyShadeFix.isEnabled()) {
760             inflateEmptyShadeView();
761         }
762         mSectionsManager.reinflateViews();
763     }
764 
sendRemoteInputRowBottomBound(Float bottom)765     void sendRemoteInputRowBottomBound(Float bottom) {
766         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
767         if (bottom != null) {
768             bottom += getResources().getDimensionPixelSize(
769                     com.android.internal.R.dimen.notification_content_margin);
770         }
771         mScrollViewFields.sendRemoteInputRowBottomBound(bottom);
772     }
773 
updateBgColor()774     void updateBgColor() {
775         for (int i = 0; i < getChildCount(); i++) {
776             View child = getChildAt(i);
777             if (child instanceof ActivatableNotificationView activatableView) {
778                 activatableView.updateBackgroundColors();
779             }
780         }
781     }
782 
onJustBeforeDraw()783     private void onJustBeforeDraw() {
784         if (SceneContainerFlag.isEnabled()) {
785             if (mChildrenUpdateRequested) {
786                 updateChildren();
787                 mChildrenUpdateRequested = false;
788             }
789         }
790     }
791 
onDraw(Canvas canvas)792     protected void onDraw(Canvas canvas) {
793         onJustBeforeDraw();
794 
795         if (mDebugLines) {
796             onDrawDebug(canvas);
797         }
798     }
799 
logHunSkippedForUnexpectedState(ExpandableNotificationRow enr, boolean expected, boolean actual)800     private void logHunSkippedForUnexpectedState(ExpandableNotificationRow enr,
801                                                  boolean expected, boolean actual) {
802         if (mLogger == null) return;
803         mLogger.hunSkippedForUnexpectedState(enr.getLoggingKey(), expected, actual);
804     }
805 
logHunAnimationSkipped(ExpandableNotificationRow enr, String reason)806     private void logHunAnimationSkipped(ExpandableNotificationRow enr, String reason) {
807         if (mLogger == null) return;
808         mLogger.hunAnimationSkipped(enr.getLoggingKey(), reason);
809     }
810 
logHunAnimationEventAdded(ExpandableNotificationRow enr, int type)811     private void logHunAnimationEventAdded(ExpandableNotificationRow enr, int type) {
812         if (mLogger == null) return;
813         mLogger.hunAnimationEventAdded(enr.getLoggingKey(), type);
814     }
815 
onDrawDebug(Canvas canvas)816     private void onDrawDebug(Canvas canvas) {
817         if (mDebugTextUsedYPositions == null) {
818             mDebugTextUsedYPositions = new HashSet<>();
819         } else {
820             mDebugTextUsedYPositions.clear();
821         }
822 
823         mDebugPaint.setColor(Color.DKGRAY);
824         canvas.drawPath(mRoundedClipPath, mDebugPaint);
825 
826         int y = 0;
827         drawDebugInfo(canvas, y, Color.RED, /* label= */ "y = " + y);
828 
829         if (SceneContainerFlag.isEnabled()) {
830             y = (int) mAmbientState.getStackTop();
831             drawDebugInfo(canvas, y, Color.RED, /* label= */ "getStackTop() = " + y);
832 
833             y = (int) mAmbientState.getStackCutoff();
834             drawDebugInfo(canvas, y, Color.MAGENTA, /* label= */ "getStackCutoff() = " + y);
835 
836             y = (int) mAmbientState.getHeadsUpTop();
837             drawDebugInfo(canvas, y, Color.GREEN, /* label= */ "getHeadsUpTop() = " + y);
838 
839             y = (int) (mAmbientState.getStackTop() + mScrollViewFields.getIntrinsicStackHeight());
840             drawDebugInfo(canvas, y, Color.BLUE,
841                     /* label= */ "getStackTop() + getIntrinsicStackHeight() = " + y);
842 
843             return; // the rest of the fields are not important in Flexiglass
844         }
845 
846         y = mAmbientState.getTopPadding();
847         drawDebugInfo(canvas, y, Color.RED, /* label= */ "getTopPadding() = " + y);
848 
849         y = getLayoutHeight();
850         drawDebugInfo(canvas, y, Color.YELLOW, /* label= */ "getLayoutHeight() = " + y);
851 
852         y = mMaxLayoutHeight;
853         drawDebugInfo(canvas, y, Color.MAGENTA, /* label= */ "mMaxLayoutHeight = " + y);
854 
855         // The space between mTopPadding and mKeyguardBottomPadding determines the available space
856         // for notifications on keyguard.
857         if (mKeyguardBottomPadding >= 0) {
858             y = getHeight() - (int) mKeyguardBottomPadding;
859             drawDebugInfo(canvas, y, Color.RED,
860                     /* label= */ "getHeight() - mKeyguardBottomPadding = " + y);
861         }
862 
863         y = getHeight() - getEmptyBottomMarginInternal();
864         drawDebugInfo(canvas, y, Color.GREEN,
865                 /* label= */ "getHeight() - getEmptyBottomMargin() = " + y);
866 
867         y = (int) (mAmbientState.getStackY());
868         drawDebugInfo(canvas, y, Color.CYAN, /* label= */ "mAmbientState.getStackY() = " + y);
869 
870         y = (int) (mAmbientState.getStackY() + mAmbientState.getInterpolatedStackHeight());
871         drawDebugInfo(canvas, y, Color.LTGRAY,
872                 /* label= */ "mAmbientState.getStackY() + mAmbientState.getStackHeight() = " + y);
873 
874         y = (int) (mAmbientState.getStackY() + getIntrinsicContentHeight());
875         drawDebugInfo(canvas, y, Color.YELLOW,
876                 /* label= */ "mAmbientState.getStackY() + mIntrinsicContentHeight = " + y);
877 
878         y = getContentHeight();
879         drawDebugInfo(canvas, y, Color.MAGENTA,
880                 /* label= */ "mContentHeight = " + y);
881 
882         y = mRoundedRectClippingBottom;
883         drawDebugInfo(canvas, y, Color.DKGRAY,
884                 /* label= */ "mRoundedRectClippingBottom) = " + y);
885     }
886 
drawDebugInfo(Canvas canvas, int y, int color, String label)887     private void drawDebugInfo(Canvas canvas, int y, int color, String label) {
888         mDebugPaint.setColor(color);
889         canvas.drawLine(/* startX= */ 0, /* startY= */ y, /* stopX= */ getWidth(), /* stopY= */ y,
890                 mDebugPaint);
891         canvas.drawText(label, /* x= */ 0, /* y= */ computeDebugYTextPosition(y), mDebugPaint);
892     }
893 
computeDebugYTextPosition(int lineY)894     private int computeDebugYTextPosition(int lineY) {
895         int textY = lineY;
896         while (mDebugTextUsedYPositions.contains(textY)) {
897             textY = (int) (textY + mDebugPaint.getTextSize());
898         }
899         mDebugTextUsedYPositions.add(textY);
900         return textY;
901     }
902 
reinitView()903     private void reinitView() {
904         initView(getContext(), mSwipeHelper, mNotificationStackSizeCalculator);
905     }
906 
initView(Context context, NotificationSwipeHelper swipeHelper, NotificationStackSizeCalculator notificationStackSizeCalculator)907     void initView(Context context, NotificationSwipeHelper swipeHelper,
908                   NotificationStackSizeCalculator notificationStackSizeCalculator) {
909         mScroller = new OverScroller(getContext());
910         mSwipeHelper = swipeHelper;
911         mNotificationStackSizeCalculator = notificationStackSizeCalculator;
912 
913         setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
914         setClipChildren(false);
915         final ViewConfiguration configuration = ViewConfiguration.get(context);
916         mTouchSlop = configuration.getScaledTouchSlop();
917         mSlopMultiplier = configuration.getScaledAmbiguousGestureMultiplier();
918         mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
919         mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
920         mOverflingDistance = configuration.getScaledOverflingDistance();
921 
922         Resources res = context.getResources();
923         mSwipeHelper.updateResourceProperties(res);
924         final boolean isSmallScreenLandscape = res.getBoolean(R.bool.is_small_screen_landscape);
925         boolean useSmallLandscapeLockscreenResources = mIsSmallLandscapeLockscreenEnabled
926                 && isSmallScreenLandscape;
927         // TODO (b/293252410) remove condition here when flag is launched
928         //  Instead update the config_skinnyNotifsInLandscape to be false whenever
929         //  is_small_screen_landscape is true. Then, only use the config_skinnyNotifsInLandscape.
930         final boolean configSkinnyNotifsInLandscape = res.getBoolean(
931                 R.bool.config_skinnyNotifsInLandscape);
932         if (useSmallLandscapeLockscreenResources) {
933             mSkinnyNotifsInLandscape = false;
934         } else {
935             mSkinnyNotifsInLandscape = configSkinnyNotifsInLandscape;
936         }
937 
938         mLastInitViewDumpString =
939                 "mIsSmallLandscapeLockscreenEnabled=" + mIsSmallLandscapeLockscreenEnabled
940                         + " isSmallScreenLandscape=" + isSmallScreenLandscape
941                         + " useSmallLandscapeLockscreenResources="
942                         + useSmallLandscapeLockscreenResources
943                         + " skinnyNotifsInLandscape=" + configSkinnyNotifsInLandscape
944                         + " mSkinnyNotifsInLandscape=" + mSkinnyNotifsInLandscape;
945         mLastInitViewElapsedRealtime = SystemClock.elapsedRealtime();
946 
947         mGapHeight = res.getDimensionPixelSize(R.dimen.notification_section_divider_height);
948         mStackScrollAlgorithm.initView(context);
949         mStateAnimator.initView(context);
950         mAmbientState.reload(context);
951         mPaddingBetweenElements = Math.max(1,
952                 res.getDimensionPixelSize(R.dimen.notification_divider_height));
953         mMinTopOverScrollToEscape = res.getDimensionPixelSize(
954                 R.dimen.min_top_overscroll_to_qs);
955         mStatusBarHeight = SystemBarUtils.getStatusBarHeight(mContext);
956         mBottomPadding = res.getDimensionPixelSize(R.dimen.notification_panel_padding_bottom);
957         mMinimumPaddings = res.getDimensionPixelSize(R.dimen.notification_side_paddings);
958         mQsTilePadding = res.getDimensionPixelOffset(R.dimen.qs_tile_margin_horizontal);
959         mSidePaddings = mMinimumPaddings;  // Updated in onMeasure by updateSidePadding()
960         mMinInteractionHeight = res.getDimensionPixelSize(
961                 R.dimen.notification_min_interaction_height);
962         mCornerRadius = res.getDimensionPixelSize(R.dimen.notification_corner_radius);
963         mHeadsUpInset = mStatusBarHeight + res.getDimensionPixelSize(
964                 R.dimen.heads_up_status_bar_padding);
965         mQsScrollBoundaryPosition = SystemBarUtils.getQuickQsOffsetHeight(mContext);
966     }
967 
updateSidePadding(int viewWidth)968     void updateSidePadding(int viewWidth) {
969         final int orientation = getResources().getConfiguration().orientation;
970 
971         mLastUpdateSidePaddingDumpString = "viewWidth=" + viewWidth
972                 + " skinnyNotifsInLandscape=" + mSkinnyNotifsInLandscape
973                 + " orientation=" + orientation;
974         mLastUpdateSidePaddingElapsedRealtime = SystemClock.elapsedRealtime();
975 
976         if (viewWidth == 0) {
977             Log.e(TAG, "updateSidePadding: viewWidth is zero");
978             mSidePaddings = mMinimumPaddings;
979             return;
980         }
981 
982         if (orientation == Configuration.ORIENTATION_PORTRAIT) {
983             mSidePaddings = mMinimumPaddings;
984             return;
985         }
986 
987         if (mShouldUseSplitNotificationShade) {
988             if (mSkinnyNotifsInLandscape) {
989                 Log.e(TAG, "updateSidePadding: mSkinnyNotifsInLandscape has betrayed us!");
990             }
991             mSidePaddings = mMinimumPaddings;
992             return;
993         }
994 
995         final int innerWidth = viewWidth - mMinimumPaddings * 2;
996         final int qsTileWidth = (innerWidth - mQsTilePadding * 3) / 4;
997         mSidePaddings = mMinimumPaddings + qsTileWidth + mQsTilePadding;
998     }
999 
updateCornerRadius()1000     void updateCornerRadius() {
1001         int newRadius = getResources().getDimensionPixelSize(R.dimen.notification_corner_radius);
1002         if (mCornerRadius != newRadius) {
1003             mCornerRadius = newRadius;
1004             invalidate();
1005         }
1006     }
1007 
notifyHeightChangeListener(ExpandableView view)1008     private void notifyHeightChangeListener(ExpandableView view) {
1009         notifyHeightChangeListener(view, false /* needsAnimation */);
1010     }
1011 
notifyHeightChangeListener(ExpandableView view, boolean needsAnimation)1012     private void notifyHeightChangeListener(ExpandableView view, boolean needsAnimation) {
1013         if (mOnHeightChangedListener != null) {
1014             mOnHeightChangedListener.onHeightChanged(view, needsAnimation);
1015         }
1016 
1017         if (mOnHeightChangedRunnable != null) {
1018             mOnHeightChangedRunnable.run();
1019         }
1020     }
1021 
isPulseExpanding()1022     public boolean isPulseExpanding() {
1023         return mAmbientState.isPulseExpanding();
1024     }
1025 
getSpeedBumpIndex()1026     public int getSpeedBumpIndex() {
1027         if (mSpeedBumpIndexDirty) {
1028             mSpeedBumpIndexDirty = false;
1029             int speedBumpIndex = 0;
1030             int currentIndex = 0;
1031             final int n = getChildCount();
1032             for (int i = 0; i < n; i++) {
1033                 View view = getChildAt(i);
1034                 if (view.getVisibility() == View.GONE
1035                         || !(view instanceof ExpandableNotificationRow row)) {
1036                     continue;
1037                 }
1038                 int bucket = NotificationBundleUi.isEnabled()
1039                         ? row.getEntryAdapter().getSectionBucket()
1040                         : row.getEntryLegacy().getBucket();
1041                 boolean isAmbient = NotificationBundleUi.isEnabled()
1042                         ? row.getEntryAdapter().isAmbient()
1043                         : row.getEntryLegacy().isAmbient();
1044                 currentIndex++;
1045                 boolean beforeSpeedBump;
1046                 if (mHighPriorityBeforeSpeedBump) {
1047                     beforeSpeedBump = bucket < BUCKET_SILENT;
1048                 } else {
1049                     beforeSpeedBump = !isAmbient;
1050                 }
1051                 if (beforeSpeedBump) {
1052                     speedBumpIndex = currentIndex;
1053                 }
1054             }
1055 
1056             mSpeedBumpIndex = speedBumpIndex;
1057         }
1058         return mSpeedBumpIndex;
1059     }
1060 
1061     private boolean mSuppressChildrenMeasureAndLayout = false;
1062 
1063     /**
1064      * Similar to {@link ViewGroup#suppressLayout} but still performs layout of
1065      * the container itself and suppresses only measure and layout calls to children.
1066      */
1067     public void suppressChildrenMeasureAndLayout(boolean suppress) {
1068         mSuppressChildrenMeasureAndLayout = suppress;
1069 
1070         if (!suppress) {
1071             requestLayout();
1072         }
1073     }
1074 
1075     @Override
1076     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1077         Trace.beginSection("NotificationStackScrollLayout#onMeasure");
1078         if (SPEW) {
1079             Log.d(TAG, "onMeasure("
1080                     + "widthMeasureSpec=" + MeasureSpec.toString(widthMeasureSpec) + ", "
1081                     + "heightMeasureSpec=" + MeasureSpec.toString(heightMeasureSpec) + ")");
1082         }
1083         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1084 
1085         int width = MeasureSpec.getSize(widthMeasureSpec);
1086         updateSidePadding(width);
1087 
1088         if (mSuppressChildrenMeasureAndLayout) {
1089             Trace.endSection();
1090             return;
1091         }
1092 
1093         int childWidthSpec = MeasureSpec.makeMeasureSpec(width - mSidePaddings * 2,
1094                 MeasureSpec.getMode(widthMeasureSpec));
1095         // Don't constrain the height of the children so we know how big they'd like to be
1096         int childHeightSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec),
1097                 MeasureSpec.UNSPECIFIED);
1098 
1099         // We need to measure all children even the GONE ones, such that the heights are calculated
1100         // correctly as they are used to calculate how many we can fit on the screen.
1101         final int size = getChildCount();
1102         for (int i = 0; i < size; i++) {
1103             measureChild(getChildAt(i), childWidthSpec, childHeightSpec);
1104         }
1105         if (SceneContainerFlag.isEnabled()) {
1106             setMaxLayoutHeight(getMeasuredHeight());
1107             updateContentHeight();
1108         }
1109         Trace.endSection();
1110     }
1111 
1112     @Override
1113     public void requestLayout() {
1114         Trace.instant(TRACE_TAG_APP, "NotificationStackScrollLayout#requestLayout");
1115         super.requestLayout();
1116     }
1117 
1118     private void notifyStackHeightChangedListeners() {
1119         for (Runnable listener : mStackHeightChangedListeners) {
1120             listener.run();
1121         }
1122     }
1123 
1124     @Override
1125     public void addStackHeightChangedListener(@NonNull Runnable runnable) {
1126         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
1127         mStackHeightChangedListeners.addIfAbsent(runnable);
1128     }
1129 
1130     @Override
1131     public void removeStackHeightChangedListener(@NonNull Runnable runnable) {
1132         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
1133         mStackHeightChangedListeners.remove(runnable);
1134     }
1135 
1136     private void notifyHeadsUpHeightChangedForView(View view) {
1137         if (mTopHeadsUpRow == view) {
1138             notifyHeadsUpHeightChangedListeners();
1139         }
1140     }
1141 
1142     private void notifyHeadsUpHeightChangedListeners() {
1143         for (Runnable listener : mHeadsUpHeightChangedListeners) {
1144             listener.run();
1145         }
1146     }
1147 
1148     @Override
1149     public void addHeadsUpHeightChangedListener(@NonNull Runnable runnable) {
1150         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
1151         mHeadsUpHeightChangedListeners.addIfAbsent(runnable);
1152     }
1153 
1154     @Override
1155     public void removeHeadsUpHeightChangedListener(@NonNull Runnable runnable) {
1156         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
1157         mHeadsUpHeightChangedListeners.remove(runnable);
1158     }
1159 
1160     @Override
1161     protected void onLayout(boolean changed, int l, int t, int r, int b) {
1162         if (!mSuppressChildrenMeasureAndLayout) {
1163             // we layout all our children centered on the top
1164             float centerX = getWidth() / 2.0f;
1165             for (int i = 0; i < getChildCount(); i++) {
1166                 View child = getChildAt(i);
1167                 // We need to layout all children even the GONE ones, such that the heights are
1168                 // calculated correctly as they are used to calculate how many we can fit on
1169                 // the screen
1170                 float width = child.getMeasuredWidth();
1171                 float height = child.getMeasuredHeight();
1172                 child.layout((int) (centerX - width / 2.0f),
1173                         0,
1174                         (int) (centerX + width / 2.0f),
1175                         (int) height);
1176             }
1177         }
1178         if (!SceneContainerFlag.isEnabled()) {
1179             setMaxLayoutHeight(getHeight());
1180             updateContentHeight();
1181         }
1182         clampScrollPosition();
1183         requestChildrenUpdate();
1184         updateFirstAndLastBackgroundViews();
1185         updateAlgorithmLayoutMinHeight();
1186         updateOwnTranslationZ();
1187 
1188         if (!SceneContainerFlag.isEnabled()) {
1189             // Give The Algorithm information regarding the QS height so it can layout notifications
1190             // properly. Needed for some devices that grows notifications down-to-top
1191             int height;
1192             if (QSComposeFragment.isEnabled()) {
1193                 if (mQSHeaderBoundsProvider != null) {
1194                     height = mQSHeaderBoundsProvider.getHeightProvider().invoke();
1195                 } else {
1196                     height = 0;
1197                 }
1198             } else {
1199                 if (mQsHeader != null) {
1200                     height = mQsHeader.getHeight();
1201                 } else {
1202                     height = 0;
1203                 }
1204             }
1205             mStackScrollAlgorithm.updateQSFrameTop(height);
1206         }
1207 
1208         // Once the layout has finished, we don't need to animate any scrolling clampings anymore.
1209         mAnimateStackYForContentHeightChange = false;
1210     }
1211 
1212     private void requestAnimationOnViewResize(ExpandableNotificationRow row) {
1213         if (mAnimationsEnabled && (mIsExpanded || row != null && row.isPinned())) {
1214             mNeedViewResizeAnimation = true;
1215             mNeedsAnimation = true;
1216         }
1217     }
1218 
1219     @NonNull
1220     @Override
1221     public View asView() {
1222         return this;
1223     }
1224 
1225     @Override
1226     public void setMaxAlpha(float alpha) {
1227         mController.setMaxAlphaFromView(alpha);
1228     }
1229 
1230     @Override
1231     public void setOccluded(boolean isOccluded) {
1232         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) {
1233             return;
1234         }
1235         this.setVisibility(isOccluded ? View.INVISIBLE : View.VISIBLE);
1236     }
1237 
1238     @Override
1239     public void setScrollState(@NonNull ShadeScrollState scrollState) {
1240         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) {
1241             return;
1242         }
1243 
1244         boolean forwardScrollable =
1245                 scrollState.getScrollPosition() < scrollState.getMaxScrollPosition();
1246         boolean backwardScrollable = scrollState.getScrollPosition() > 0;
1247         mScrollable = forwardScrollable || backwardScrollable;
1248         mForwardScrollable = forwardScrollable;
1249         mBackwardScrollable = backwardScrollable;
1250 
1251         boolean scrollPositionChanged = mScrollViewFields.getScrollState().getScrollPosition()
1252                 != scrollState.getScrollPosition();
1253         mScrollViewFields.setScrollState(scrollState);
1254 
1255         if (scrollPositionChanged) {
1256             sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SCROLLED);
1257         }
1258     }
1259 
1260     @Override
1261     public void setStackTop(float stackTop) {
1262         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
1263         if (mAmbientState.getStackTop() != stackTop) {
1264             mAmbientState.setStackTop(stackTop);
1265             onTopPaddingChanged(/* animate = */ isAddOrRemoveAnimationPending());
1266         }
1267     }
1268 
1269     @Override
1270     public void setStackCutoff(float stackCutoff) {
1271         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
1272         if (mAmbientState.getStackCutoff() != stackCutoff) {
1273             mAmbientState.setStackCutoff(stackCutoff);
1274             updateStackEndHeightAndStackHeight(mAmbientState.getExpansionFraction());
1275             requestChildrenUpdate();
1276         }
1277     }
1278 
1279     @Override
1280     public void setHeadsUpTop(float headsUpTop) {
1281         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
1282         if (mAmbientState.getHeadsUpTop() != headsUpTop) {
1283             mAmbientState.setHeadsUpTop(headsUpTop);
1284             requestChildrenUpdate();
1285         }
1286     }
1287 
1288     @Override
1289     public void setHeadsUpBottom(float headsUpBottom) {
1290         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
1291         if (NotificationsHunSharedAnimationValues.isEnabled()) {
1292             mHeadsUpAnimator.setHeadsUpAppearHeightBottom(Math.round(headsUpBottom));
1293         } else if (mAmbientState.getHeadsUpBottom() != headsUpBottom) {
1294             mAmbientState.setHeadsUpBottom(headsUpBottom);
1295             mStateAnimator.setHeadsUpAppearHeightBottom(Math.round(headsUpBottom));
1296         }
1297     }
1298 
1299     @Override
1300     public void closeGutsOnSceneTouch() {
1301         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
1302         mController.closeControlsDueToOutsideTouch();
1303     }
1304 
1305     @Override
1306     public void setSyntheticScrollConsumer(@Nullable Consumer<Float> consumer) {
1307         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
1308         mScrollViewFields.setSyntheticScrollConsumer(consumer);
1309     }
1310 
1311     @Override
1312     public void setAccessibilityScrollEventConsumer(
1313             @Nullable Consumer<AccessibilityScrollEvent> consumer) {
1314         mScrollViewFields.setAccessibilityScrollEventConsumer(consumer);
1315     }
1316 
1317     @Override
1318     public void setCurrentGestureOverscrollConsumer(@Nullable Consumer<Boolean> consumer) {
1319         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
1320         mScrollViewFields.setCurrentGestureOverscrollConsumer(consumer);
1321     }
1322 
1323     @Override
1324     public void setCurrentGestureInGutsConsumer(@Nullable Consumer<Boolean> consumer) {
1325         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
1326         mScrollViewFields.setCurrentGestureInGutsConsumer(consumer);
1327     }
1328 
1329     @Override
1330     public void setRemoteInputRowBottomBoundConsumer(@Nullable Consumer<Float> consumer) {
1331         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
1332         mScrollViewFields.setRemoteInputRowBottomBoundConsumer(consumer);
1333     }
1334 
1335     /**
1336      * @param listener to be notified after the location of Notification children might have
1337      *                 changed.
1338      */
1339     public void setNotificationLocationsChangedListener(
1340             @Nullable OnNotificationLocationsChangedListener listener) {
1341         if (NotificationsLiveDataStoreRefactor.isUnexpectedlyInLegacyMode()) {
1342             return;
1343         }
1344         mLocationsChangedListener = listener;
1345     }
1346 
1347     public void setChildLocationsChangedListener(
1348             NotificationLogger.OnChildLocationsChangedListener listener) {
1349         NotificationsLiveDataStoreRefactor.assertInLegacyMode();
1350         mLegacyLocationsChangedListener = listener;
1351     }
1352 
1353     private void setMaxLayoutHeight(int maxLayoutHeight) {
1354         mMaxLayoutHeight = maxLayoutHeight;
1355         updateAlgorithmHeightAndPadding();
1356     }
1357 
1358     private void updateAlgorithmHeightAndPadding() {
1359         mAmbientState.setLayoutHeight(getLayoutHeight());
1360         mAmbientState.setLayoutMaxHeight(mMaxLayoutHeight);
1361         updateAlgorithmLayoutMinHeight();
1362     }
1363 
1364     private void updateAlgorithmLayoutMinHeight() {
1365         if (!SceneContainerFlag.isEnabled()) {
1366             mAmbientState.setLayoutMinHeight(mQsFullScreen || isHeadsUpTransition()
1367                     ? getLayoutMinHeightInternal() : 0);
1368         }
1369     }
1370 
1371     /**
1372      * Updates the children views according to the stack scroll algorithm. Call this whenever
1373      * modifications to {@link #getOwnScrollY()} are performed to reflect it in the view layout.
1374      */
1375     private void updateChildren() {
1376         Trace.beginSection("NSSL#updateChildren");
1377         updateScrollStateForAddedChildren();
1378         mAmbientState.setCurrentScrollVelocity(mScroller.isFinished()
1379                 ? 0
1380                 : mScroller.getCurrVelocity());
1381         mStackScrollAlgorithm.resetViewStates(mAmbientState, getSpeedBumpIndex());
1382         if (!isCurrentlyAnimating() && !mNeedsAnimation) {
1383             applyCurrentState();
1384         } else {
1385             startAnimationToState();
1386         }
1387         Trace.endSection();
1388     }
1389 
1390     private void onPreDrawDuringAnimation() {
1391         mShelf.updateAppearance();
1392     }
1393 
1394     private void updateScrollStateForAddedChildren() {
1395         if (mChildrenToAddAnimated.isEmpty()) {
1396             return;
1397         }
1398         for (int i = 0; i < getChildCount(); i++) {
1399             ExpandableView child = getChildAtIndex(i);
1400             if (mChildrenToAddAnimated.contains(child)) {
1401                 final int startingPosition = getPositionInLinearLayout(child);
1402                 final int childHeight = getIntrinsicHeight(child) + mPaddingBetweenElements;
1403                 if (startingPosition < getOwnScrollY()) {
1404                     // This child starts off screen, so let's keep it offscreen to keep the
1405                     // others visible
1406 
1407                     setOwnScrollY(getOwnScrollY() + childHeight);
1408                 }
1409             }
1410         }
1411         clampScrollPosition();
1412     }
1413 
1414     private void updateForcedScroll() {
1415         if (mForcedScroll != null && (!mForcedScroll.hasFocus()
1416                 || !mForcedScroll.isAttachedToWindow())) {
1417             mForcedScroll = null;
1418         }
1419         if (mForcedScroll != null) {
1420             ExpandableView expandableView = (ExpandableView) mForcedScroll;
1421             int positionInLinearLayout = getPositionInLinearLayout(expandableView);
1422             int targetScroll = targetScrollForView(expandableView, positionInLinearLayout);
1423             int outOfViewScroll = positionInLinearLayout + expandableView.getIntrinsicHeight();
1424             targetScroll = Math.max(0, Math.min(targetScroll, getScrollRange()));
1425             // Only apply the scroll if we're scrolling the view upwards, or the view is so
1426             // far up that it is not visible anymore.
1427             if (getOwnScrollY() < targetScroll || outOfViewScroll < getOwnScrollY()) {
1428                 setOwnScrollY(targetScroll);
1429             }
1430         }
1431     }
1432 
1433     void requestChildrenUpdate() {
1434         if (!mChildrenUpdateRequested) {
1435             getViewTreeObserver().addOnPreDrawListener(mChildrenUpdater);
1436             mChildrenUpdateRequested = true;
1437             invalidate();
1438         }
1439     }
1440 
1441     @Override
1442     public void setAlpha(float alpha) {
1443         super.setAlpha(alpha);
1444         if (Trace.isEnabled()) {
1445             Trace.setCounter(
1446                     trackGroup(/* groupName= */ "shade", /* trackName= */ "NSSLResultingAlpha"),
1447                     (int) (alpha * 100));
1448         }
1449     }
1450 
1451     private boolean isCurrentlyAnimating() {
1452         return mStateAnimator.isRunning();
1453     }
1454 
1455     private void clampScrollPosition() {
1456         // NSSL doesn't control scrolling with SceneContainer enabled
1457         if (SceneContainerFlag.isEnabled()) {
1458             return;
1459         }
1460         int scrollRange = getScrollRange();
1461         if (scrollRange < getOwnScrollY() && !mAmbientState.isClearAllInProgress()) {
1462             // if the scroll boundary updates the position of the stack,
1463             boolean animateStackY = scrollRange < getScrollAmountToScrollBoundary()
1464                     && mAnimateStackYForContentHeightChange;
1465             setOwnScrollY(scrollRange, animateStackY);
1466         }
1467     }
1468 
1469     public int getTopPadding() {
1470         SceneContainerFlag.assertInLegacyMode();
1471         return mAmbientState.getTopPadding();
1472     }
1473 
1474     private void onTopPaddingChanged(boolean animate) {
1475         boolean shouldAnimate = animate || mAnimateNextTopPaddingChange;
1476         updateAlgorithmHeightAndPadding();
1477         updateContentHeight();
1478         if (mAmbientState.isOnKeyguard()
1479                 && !mShouldUseSplitNotificationShade
1480                 && mShouldSkipTopPaddingAnimationAfterFold) {
1481             mShouldSkipTopPaddingAnimationAfterFold = false;
1482         } else if (shouldAnimate && mAnimationsEnabled && mIsExpanded) {
1483             mTopPaddingNeedsAnimation = true;
1484             mNeedsAnimation = true;
1485         }
1486         updateStackPosition();
1487         requestChildrenUpdate();
1488         notifyHeightChangeListener(null, shouldAnimate);
1489         mAnimateNextTopPaddingChange = false;
1490     }
1491 
1492     /**
1493      * Apply expansion fraction to the y position and height of the notifications panel.
1494      */
1495     private void updateStackPosition() {
1496         updateStackPosition(false /* listenerNeedsAnimation */);
1497     }
1498 
1499     /**
1500      * @return Whether we should skip stack height updates.
1501      * True when
1502      * 1) Unlock hint is running
1503      * 2) Swiping up on lockscreen or flinging down after swipe up
1504      */
1505     private boolean shouldSkipHeightUpdate() {
1506         if (SceneContainerFlag.isEnabled()) {
1507             return mSuppressHeightUpdates;
1508         } else {
1509             return mAmbientState.isOnKeyguard()
1510                     && (mAmbientState.isSwipingUp()
1511                     || mAmbientState.isFlingingAfterSwipeUpOnLockscreen());
1512         }
1513     }
1514 
1515     /**
1516      * Apply expansion fraction to the y position and height of the notifications panel.
1517      *
1518      * @param listenerNeedsAnimation does the listener need to animate?
1519      */
1520     private void updateStackPosition(boolean listenerNeedsAnimation) {
1521         // When scene container is active, we only want to recalculate stack heights.
1522         if (SceneContainerFlag.isEnabled()) {
1523             updateStackEndHeightAndStackHeight(mAmbientState.getExpansionFraction());
1524             return;
1525         }
1526         float topOverscrollAmount = mShouldUseSplitNotificationShade
1527                 ? getCurrentOverScrollAmount(true /* top */) : 0f;
1528         final float endTopPosition = getTopPadding() + mExtraTopInsetForFullShadeTransition
1529                 + mAmbientState.getOverExpansion()
1530                 + topOverscrollAmount
1531                 - getCurrentOverScrollAmount(false /* top */);
1532         float fraction = mAmbientState.getExpansionFraction();
1533         // If we are on quick settings, we need to quickly hide it to show the bouncer to avoid an
1534         // overlap. Otherwise, we maintain the normal fraction for smoothness.
1535         if (mAmbientState.isBouncerInTransit() && getQsExpansionFraction() > 0f) {
1536             fraction = BouncerPanelExpansionCalculator.aboutToShowBouncerProgress(fraction);
1537         }
1538         final float stackY = MathUtils.lerp(0, endTopPosition, fraction);
1539         mAmbientState.setStackY(stackY);
1540 
1541         if (mOnStackYChanged != null) {
1542             mOnStackYChanged.accept(listenerNeedsAnimation);
1543         }
1544         updateStackEndHeightAndStackHeight(fraction);
1545     }
1546 
1547     @VisibleForTesting
1548     public void updateStackEndHeightAndStackHeight(float fraction) {
1549         final float oldStackHeight = mAmbientState.getInterpolatedStackHeight();
1550         if (SceneContainerFlag.isEnabled()) {
1551             final float endHeight;
1552             if (!shouldSkipHeightUpdate()) {
1553                 endHeight = updateStackEndHeight();
1554             } else {
1555                 endHeight = mAmbientState.getStackEndHeight();
1556             }
1557             updateInterpolatedStackHeight(endHeight, fraction);
1558         } else {
1559             if (getQsExpansionFraction() <= 0 && !shouldSkipHeightUpdate()) {
1560                 final float endHeight = updateStackEndHeight(
1561                         getHeight(), getEmptyBottomMarginInternal(), getTopPadding());
1562                 updateInterpolatedStackHeight(endHeight, fraction);
1563             } else {
1564                 // Always updateStackHeight to prevent jumps in the stack height when this fraction
1565                 // suddenly reapplies after a freeze.
1566                 final float endHeight = mAmbientState.getStackEndHeight();
1567                 updateInterpolatedStackHeight(endHeight, fraction);
1568             }
1569         }
1570         if (oldStackHeight != mAmbientState.getInterpolatedStackHeight()) {
1571             requestChildrenUpdate();
1572         }
1573     }
1574 
1575     private float updateStackEndHeight() {
1576         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return 0f;
1577         final float height;
1578         if (mMaxDisplayedNotifications != -1) {
1579             // The stack intrinsic height already contains the correct value when there is a limit
1580             // in the max number of notifications (e.g. as in keyguard).
1581             height = mScrollViewFields.getIntrinsicStackHeight();
1582         } else {
1583             height = Math.max(0f, mAmbientState.getStackCutoff() - mAmbientState.getStackTop());
1584         }
1585         mAmbientState.setStackEndHeight(height);
1586         return height;
1587     }
1588 
1589     private float updateStackEndHeight(float height, float bottomMargin, float topPadding) {
1590         SceneContainerFlag.assertInLegacyMode();
1591         final float stackEndHeight;
1592         if (mMaxDisplayedNotifications != -1) {
1593             // The stack intrinsic height already contains the correct value when there is a limit
1594             // in the max number of notifications (e.g. as in keyguard).
1595             stackEndHeight = getIntrinsicContentHeight();
1596         } else {
1597             stackEndHeight = Math.max(0f, height - bottomMargin - topPadding);
1598         }
1599         mAmbientState.setStackEndHeight(stackEndHeight);
1600         return stackEndHeight;
1601     }
1602 
1603     @VisibleForTesting
1604     public void updateInterpolatedStackHeight(float endHeight, float fraction) {
1605         mAmbientState.setInterpolatedStackHeight(
1606                 MathUtils.lerp(endHeight * StackScrollAlgorithm.START_FRACTION,
1607                         endHeight, fraction));
1608     }
1609 
1610     /**
1611      * Add a listener when the StackY changes. The argument signifies whether an animation is
1612      * needed.
1613      */
1614     void setOnStackYChanged(Consumer<Boolean> onStackYChanged) {
1615         SceneContainerFlag.assertInLegacyMode();
1616         mOnStackYChanged = onStackYChanged;
1617     }
1618 
1619     @Override
1620     public void setExpandFraction(float expandFraction) {
1621         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
1622         final float oldFraction = mAmbientState.getExpansionFraction();
1623         final boolean wasExpanding = oldFraction != 0f && oldFraction != 1f;
1624         final boolean nowExpanding = expandFraction != 0f && expandFraction != 1f;
1625 
1626         // need to enter 'expanding' state before handling the new expand fraction, and then
1627         if (nowExpanding && !wasExpanding) {
1628             onExpansionStarted();
1629             mController.checkSnoozeLeavebehind();
1630         }
1631 
1632         // Update the expand progress between started/stopped events
1633         mAmbientState.setExpansionFraction(expandFraction);
1634 
1635         if (!shouldSkipHeightUpdate()) {
1636             updateStackEndHeightAndStackHeight(expandFraction);
1637             updateExpandedHeight(expandFraction);
1638         }
1639 
1640         // expansion stopped event requires that the expandFraction has already been updated
1641         if (!nowExpanding && wasExpanding) {
1642             setCheckForLeaveBehind(false);
1643             onExpansionStopped();
1644         }
1645     }
1646 
1647     private void updateExpandedHeight(float expandFraction) {
1648         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
1649         float expandedHeight = expandFraction * getHeight();
1650         setIsExpanded(expandedHeight > 0);
1651 
1652         if (mExpandedHeight != expandedHeight) {
1653             mExpandedHeight = expandedHeight;
1654             updateAlgorithmHeightAndPadding();
1655             requestChildrenUpdate();
1656             notifyAppearChangedListeners();
1657         }
1658     }
1659 
1660     @Override
1661     public void setQsExpandFraction(float expandFraction) {
1662         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
1663         mAmbientState.setQsExpansionFraction(expandFraction);
1664     }
1665 
1666     /**
1667      * Update the height of the panel.
1668      *
1669      * @param height the expanded height of the panel
1670      */
1671     public void setExpandedHeight(float height) {
1672         SceneContainerFlag.assertInLegacyMode();
1673         final boolean skipHeightUpdate = shouldSkipHeightUpdate();
1674 
1675         updateStackPosition();
1676 
1677         if (!skipHeightUpdate) {
1678             mExpandedHeight = height;
1679             setIsExpanded(height > 0);
1680             int minExpansionHeight = getMinExpansionHeight();
1681             if (height < minExpansionHeight && !mShouldUseSplitNotificationShade) {
1682                 mClipRect.left = 0;
1683                 mClipRect.right = getWidth();
1684                 mClipRect.top = 0;
1685                 mClipRect.bottom = (int) height;
1686                 height = minExpansionHeight;
1687                 setRequestedClipBounds(mClipRect);
1688             } else {
1689                 setRequestedClipBounds(null);
1690             }
1691         }
1692         int stackHeight;
1693         float translationY;
1694         float appearFraction = 1.0f;
1695         boolean appearing = calculateAppearFraction(height) < 1;
1696         if (!appearing) {
1697             translationY = 0;
1698             if (mShouldShowShelfOnly) {
1699                 stackHeight = getTopPadding() + mShelf.getIntrinsicHeight();
1700             } else if (mQsFullScreen) {
1701                 int stackStartPosition =
1702                         getContentHeight() - getTopPadding() + getIntrinsicPadding();
1703                 int stackEndPosition = getMaxTopPadding() + mShelf.getIntrinsicHeight();
1704                 if (stackStartPosition <= stackEndPosition) {
1705                     stackHeight = stackEndPosition;
1706                 } else {
1707                     if (mShouldUseSplitNotificationShade) {
1708                         // This prevents notifications from being collapsed when QS is expanded.
1709                         stackHeight = (int) height;
1710                     } else {
1711                         stackHeight = (int) NotificationUtils.interpolate(stackStartPosition,
1712                                 stackEndPosition, getQsExpansionFraction());
1713                     }
1714                 }
1715             } else {
1716                 stackHeight = (int) (skipHeightUpdate ? mExpandedHeight : height);
1717             }
1718         } else {
1719             appearFraction = calculateAppearFraction(height);
1720             if (appearFraction >= 0) {
1721                 translationY = NotificationUtils.interpolate(getExpandTranslationStart(), 0,
1722                         appearFraction);
1723             } else {
1724                 // This may happen when pushing up a heads up. We linearly push it up from the
1725                 // start
1726                 translationY = height - getAppearStartPosition() + getExpandTranslationStart();
1727             }
1728             stackHeight = (int) (height - translationY);
1729             if (isHeadsUpTransition() && appearFraction >= 0) {
1730                 int topSpacing = mShouldUseSplitNotificationShade
1731                         ? mAmbientState.getStackTopMargin() : getTopPadding();
1732                 float startPos = mHeadsUpInset - topSpacing;
1733                 translationY = MathUtils.lerp(startPos, 0, appearFraction);
1734             }
1735         }
1736         mAmbientState.setAppearFraction(appearFraction);
1737         if (stackHeight != mCurrentStackHeight && !skipHeightUpdate) {
1738             mCurrentStackHeight = stackHeight;
1739             updateAlgorithmHeightAndPadding();
1740             requestChildrenUpdate();
1741         }
1742         setStackTranslation(translationY);
1743         notifyAppearChangedListeners();
1744     }
1745 
1746     private void notifyAppearChangedListeners() {
1747         float appear;
1748         float expandAmount;
1749         if (mKeyguardBypassEnabled && onKeyguard()) {
1750             appear = calculateAppearFractionBypassInternal();
1751             expandAmount = getPulseHeight();
1752         } else {
1753             appear = MathUtils.saturate(calculateAppearFraction(mExpandedHeight));
1754             expandAmount = mExpandedHeight;
1755         }
1756         if (appear != mLastSentAppear || expandAmount != mLastSentExpandedHeight) {
1757             mLastSentAppear = appear;
1758             mLastSentExpandedHeight = expandAmount;
1759             for (int i = 0; i < mExpandedHeightListeners.size(); i++) {
1760                 BiConsumer<Float, Float> listener = mExpandedHeightListeners.get(i);
1761                 listener.accept(expandAmount, appear);
1762             }
1763         }
1764     }
1765 
1766     private void setRequestedClipBounds(Rect clipRect) {
1767         mRequestedClipBounds = clipRect;
1768         updateClipping();
1769     }
1770 
1771     public void updateClipping() {
1772         boolean clipped = mRequestedClipBounds != null && !mInHeadsUpPinnedMode
1773                 && !mHeadsUpAnimatingAway;
1774         if (mIsClipped != clipped) {
1775             mIsClipped = clipped;
1776         }
1777 
1778         if (mAmbientState.isHiddenAtAll()) {
1779             invalidateOutline();
1780             if (isFullyHidden()) {
1781                 setClipBounds(null);
1782             }
1783         } else if (clipped) {
1784             setClipBounds(mRequestedClipBounds);
1785         } else {
1786             setClipBounds(null);
1787         }
1788 
1789         setClipToOutline(false);
1790     }
1791 
1792     /**
1793      * @return The translation at the beginning when expanding.
1794      * Measured relative to the resting position.
1795      */
1796     private float getExpandTranslationStart() {
1797         SceneContainerFlag.assertInLegacyMode();
1798         return -getTopPadding() + getMinExpansionHeight() - mShelf.getIntrinsicHeight();
1799     }
1800 
1801     /**
1802      * @return the position from where the appear transition starts when expanding.
1803      * Measured in absolute height.
1804      */
1805     private float getAppearStartPosition() {
1806         SceneContainerFlag.assertInLegacyMode();
1807         if (isHeadsUpTransition()) {
1808             final NotificationSection firstVisibleSection = getFirstVisibleSection();
1809             final int pinnedHeight = firstVisibleSection != null
1810                     ? firstVisibleSection.getFirstVisibleChild().getPinnedHeadsUpHeight()
1811                     : 0;
1812             return mHeadsUpInset - mAmbientState.getStackTopMargin() + pinnedHeight;
1813         }
1814         return getMinExpansionHeight();
1815     }
1816 
1817     /**
1818      * @return the height of the top heads up notification when pinned. This is different from the
1819      * intrinsic height, which also includes whether the notification is system expanded and
1820      * is mainly used when dragging down from a heads up notification.
1821      */
1822     private int getTopHeadsUpPinnedHeight() {
1823         if (mTopHeadsUpRow == null) {
1824             return 0;
1825         }
1826         ExpandableNotificationRow row = getTopHeadsUpRow();
1827         return row.getPinnedHeadsUpHeight();
1828     }
1829 
1830     private int getTopHeadsUpIntrinsicHeight() {
1831         if (mTopHeadsUpRow == null) {
1832             return 0;
1833         }
1834         ExpandableNotificationRow row = getTopHeadsUpRow();
1835         return row.getIntrinsicHeight();
1836     }
1837 
1838     private ExpandableNotificationRow getTopHeadsUpRow() {
1839         ExpandableNotificationRow row = mTopHeadsUpRow;
1840         if (NotificationBundleUi.isEnabled()) {
1841             if (mGroupMembershipManager.isChildInGroup(row.getEntryAdapter())
1842                     && row.isChildInGroup()) {
1843                 row = row.getNotificationParent();
1844             }
1845         } else {
1846             if (row.isChildInGroup()) {
1847                 final NotificationEntry groupSummary =
1848                         mGroupMembershipManager.getGroupSummary(row.getEntryLegacy());
1849                 if (groupSummary != null) {
1850                     row = groupSummary.getRow();
1851                 }
1852             }
1853         }
1854         return row;
1855     }
1856     /**
1857      * @return the position from where the appear transition ends when expanding.
1858      * Measured in absolute height.
1859      *
1860      * TODO(b/308591475): This entire logic can probably be improved as part of the empty shade
1861      *  refactor, but for now:
1862      *  - if the empty shade is visible, we can assume that the visible notif count is not 0;
1863      *  - if the shelf is visible, we can assume there are at least 2 notifications present (this
1864      *    is not true for AOD, but this logic refers to the expansion of the shade, where we never
1865      *    have the shelf on its own)
1866      */
1867     private float getAppearEndPosition() {
1868         SceneContainerFlag.assertInLegacyMode();
1869 
1870         int appearPosition = mAmbientState.getStackTopMargin();
1871         if (mEmptyShadeView.getVisibility() == GONE) {
1872             if (isHeadsUpTransition()
1873                     || (mInHeadsUpPinnedMode && !mAmbientState.isDozing())) {
1874                 if (mShelf.getVisibility() != GONE) {
1875                     appearPosition += mShelf.getIntrinsicHeight() + mPaddingBetweenElements;
1876                 }
1877                 appearPosition += getTopHeadsUpPinnedHeight()
1878                         + getPositionInLinearLayout(mAmbientState.getTrackedHeadsUpRow());
1879             } else if (mShelf.getVisibility() != GONE) {
1880                 appearPosition += mShelf.getIntrinsicHeight();
1881             }
1882         } else {
1883             appearPosition = mEmptyShadeView.getHeight();
1884         }
1885         return appearPosition + (onKeyguard() ? getTopPadding() : getIntrinsicPadding());
1886     }
1887 
1888     private boolean isHeadsUpTransition() {
1889         return mAmbientState.getTrackedHeadsUpRow() != null;
1890     }
1891 
1892     /**
1893      * @param height the height of the panel
1894      * @return Fraction of the appear animation that has been performed. Normally follows expansion
1895      * fraction so goes from 0 to 1, the only exception is HUN where it can go negative, down to -1,
1896      * when HUN is swiped up.
1897      */
1898     @FloatRange(from = -1.0, to = 1.0)
1899     public float calculateAppearFraction(float height) {
1900         if (isHeadsUpTransition() && !SceneContainerFlag.isEnabled()) {
1901             // HUN is a special case because fraction can go negative if swiping up. And for now
1902             // it must go negative as other pieces responsible for proper translation up assume
1903             // negative value for HUN going up.
1904             // This can't use expansion fraction as that goes only from 0 to 1. Also when
1905             // appear fraction for HUN is 0, expansion fraction will be already around 0.2-0.3
1906             // and that makes translation jump immediately.
1907             float appearEndPosition = getAppearEndPosition();
1908             float appearStartPosition = getAppearStartPosition();
1909             float hunAppearFraction = (height - appearStartPosition)
1910                     / (appearEndPosition - appearStartPosition);
1911             return MathUtils.constrain(hunAppearFraction, -1, 1);
1912         } else {
1913             return mAmbientState.getExpansionFraction();
1914         }
1915     }
1916 
1917     public float getStackTranslation() {
1918         return mAmbientState.getStackTranslation();
1919     }
1920 
1921     private void setStackTranslation(float stackTranslation) {
1922         if (stackTranslation != getStackTranslation()) {
1923             mAmbientState.setStackTranslation(stackTranslation);
1924             requestChildrenUpdate();
1925         }
1926     }
1927 
1928     /**
1929      * Get the current height of the view. This is at most the msize of the view given by a the
1930      * layout but it can also be made smaller by setting {@link #mCurrentStackHeight}
1931      *
1932      * @return either the layout height or the externally defined height, whichever is smaller
1933      */
1934     private int getLayoutHeight() {
1935         return Math.min(mMaxLayoutHeight, mCurrentStackHeight);
1936     }
1937 
1938     public void setQsHeader(ViewGroup qsHeader) {
1939         QSComposeFragment.assertInLegacyMode();
1940         mQsHeader = qsHeader;
1941     }
1942 
1943     public void setQsHeaderBoundsProvider(QSHeaderBoundsProvider qsHeaderBoundsProvider) {
1944         SceneContainerFlag.assertInLegacyMode();
1945         QSComposeFragment.isUnexpectedlyInLegacyMode();
1946         mQSHeaderBoundsProvider = qsHeaderBoundsProvider;
1947     }
1948 
1949     public static boolean isPinnedHeadsUp(View v) {
1950         if (v instanceof ExpandableNotificationRow row) {
1951             return row.isHeadsUp() && row.isPinned();
1952         }
1953         return false;
1954     }
1955 
1956     private boolean isHeadsUp(View v) {
1957         if (v instanceof ExpandableNotificationRow row) {
1958             return row.isHeadsUp();
1959         }
1960         return false;
1961     }
1962 
1963     private ExpandableView getChildAtPosition(float touchX, float touchY) {
1964         return getChildAtPosition(touchX, touchY, true /* requireMinHeight */,
1965                 true /* ignoreDecors */, true /* ignoreWidth */);
1966     }
1967 
1968     /**
1969      * Get the child at a certain screen location.
1970      *
1971      * @param touchX           the x coordinate
1972      * @param touchY           the y coordinate
1973      * @param requireMinHeight Whether a minimum height is required for a child to be returned.
1974      * @param ignoreDecors     Whether decors can be returned
1975      * @param ignoreWidth      Whether we should ignore the width of the child
1976      * @return the child at the given location.
1977      */
1978     ExpandableView getChildAtPosition(float touchX, float touchY, boolean requireMinHeight,
1979             boolean ignoreDecors, boolean ignoreWidth) {
1980         // find the view under the pointer, accounting for GONE views
1981         final int count = getChildCount();
1982         for (int childIdx = 0; childIdx < count; childIdx++) {
1983             ExpandableView slidingChild = getChildAtIndex(childIdx);
1984             if (slidingChild.getVisibility() != VISIBLE
1985                     || (ignoreDecors && slidingChild instanceof StackScrollerDecorView)) {
1986                 continue;
1987             }
1988             float childTop = slidingChild.getTranslationY();
1989             float top = childTop + Math.max(0, slidingChild.getClipTopAmount());
1990             float bottom = childTop + slidingChild.getActualHeight()
1991                     - slidingChild.getClipBottomAmount();
1992 
1993             // Allow the full width of this view to prevent gesture conflict on Keyguard (phone and
1994             // camera affordance).
1995             int left = ignoreWidth ? 0 : slidingChild.getLeft();
1996             int right = ignoreWidth ? getWidth() : slidingChild.getRight();
1997 
1998             if ((bottom - top >= mMinInteractionHeight || !requireMinHeight)
1999                     && touchY >= top && touchY <= bottom && touchX >= left && touchX <= right) {
2000                 if (slidingChild instanceof ExpandableNotificationRow row) {
2001                     boolean isEntrySummaryForTopHun;
2002                     if (NotificationBundleUi.isEnabled()) {
2003                         isEntrySummaryForTopHun = Objects.equals(
2004                                 ((ExpandableNotificationRow) slidingChild).getNotificationParent(),
2005                                 mTopHeadsUpRow);
2006                     } else {
2007                         NotificationEntry entry = row.getEntryLegacy();
2008                         isEntrySummaryForTopHun = mTopHeadsUpRow != null &&
2009                                 mGroupMembershipManager.getGroupSummary(
2010                                         mTopHeadsUpRow.getEntryLegacy()) == entry;
2011                     }
2012                     if (!mIsExpanded && row.isHeadsUp() && row.isPinned()
2013                             && mTopHeadsUpRow != row
2014                             && !isEntrySummaryForTopHun) {
2015                         continue;
2016                     }
2017                     return row.getViewAtPosition(touchY - childTop);
2018                 }
2019                 return slidingChild;
2020             }
2021         }
2022         return null;
2023     }
2024 
2025     private ExpandableView getChildAtIndex(int index) {
2026         return (ExpandableView) getChildAt(index);
2027     }
2028 
2029     public ExpandableView getChildAtRawPosition(float touchX, float touchY) {
2030         getLocationOnScreen(mTempInt2);
2031         return getChildAtPosition(touchX - mTempInt2[0], touchY - mTempInt2[1]);
2032     }
2033 
2034     public void setScrollingEnabled(boolean enable) {
2035         mScrollingEnabled = enable;
2036     }
2037 
2038     public void lockScrollTo(View v) {
2039         // NSSL shouldn't handle scrolling with SceneContainer enabled.
2040         if (mForcedScroll == v || SceneContainerFlag.isEnabled()) {
2041             return;
2042         }
2043         mForcedScroll = v;
2044         updateForcedScroll();
2045     }
2046 
2047     public boolean scrollTo(View v) {
2048         // NSSL shouldn't handle scrolling with SceneContainer enabled.
2049         if (SceneContainerFlag.isEnabled()) {
2050             return false;
2051         }
2052         ExpandableView expandableView = (ExpandableView) v;
2053         int positionInLinearLayout = getPositionInLinearLayout(v);
2054         int targetScroll = targetScrollForView(expandableView, positionInLinearLayout);
2055         int outOfViewScroll = positionInLinearLayout + expandableView.getIntrinsicHeight();
2056 
2057         // Only apply the scroll if we're scrolling the view upwards, or the view is so far up
2058         // that it is not visible anymore.
2059         if (getOwnScrollY() < targetScroll || outOfViewScroll < getOwnScrollY()) {
2060             mScroller.startScroll(mScrollX, getOwnScrollY(), 0, targetScroll - getOwnScrollY());
2061             mDontReportNextOverScroll = true;
2062             animateScroll();
2063             return true;
2064         }
2065         return false;
2066     }
2067 
2068     /**
2069      * @return the scroll necessary to make the bottom edge of {@param v} align with the top of
2070      * the IME.
2071      */
2072     private int targetScrollForView(ExpandableView v, int positionInLinearLayout) {
2073         SceneContainerFlag.assertInLegacyMode();
2074         return positionInLinearLayout + v.getIntrinsicHeight() +
2075                 getImeInset() - getHeight()
2076                 + ((!isExpanded() && isPinnedHeadsUp(v)) ? mHeadsUpInset : getTopPadding());
2077     }
2078 
2079     private void updateImeInset(WindowInsets windowInsets) {
2080         SceneContainerFlag.assertInLegacyMode();
2081         mImeInset = windowInsets.getInsets(WindowInsets.Type.ime()).bottom;
2082 
2083         if (mFooterView != null && mFooterView.getViewState() != null) {
2084             // Do not animate footer Y when showing IME so that after IME hides, the footer
2085             // appears at the correct Y. Once resetY is true, it remains true (even when IME
2086             // hides, where mImeInset=0) until reset in FooterViewState#animateTo.
2087             ((FooterView.FooterViewState) mFooterView.getViewState()).resetY |= mImeInset > 0;
2088         }
2089 
2090         if (mForcedScroll != null) {
2091             updateForcedScroll();
2092         }
2093 
2094         int range = getScrollRange();
2095         if (getOwnScrollY() > range) {
2096             setOwnScrollY(range);
2097         }
2098     }
2099 
2100     @Override
2101     public WindowInsets onApplyWindowInsets(WindowInsets insets) {
2102         mWaterfallTopInset = 0;
2103         final DisplayCutout cutout = insets.getDisplayCutout();
2104         if (cutout != null) {
2105             mWaterfallTopInset = cutout.getWaterfallInsets().top;
2106         }
2107         if (!SceneContainerFlag.isEnabled() && !mIsInsetAnimationRunning) {
2108             // update bottom inset e.g. after rotation
2109             updateImeInset(insets);
2110         }
2111         return insets;
2112     }
2113 
2114     public void setExpandingEnabled(boolean enable) {
2115         mExpandHelper.setEnabled(enable);
2116     }
2117 
2118     private boolean isScrollingEnabled() {
2119         return mScrollingEnabled;
2120     }
2121 
2122     boolean onKeyguard() {
2123         return mStatusBarState == StatusBarState.KEYGUARD;
2124     }
2125 
2126     @Override
2127     protected void onConfigurationChanged(Configuration newConfig) {
2128         super.onConfigurationChanged(newConfig);
2129         Resources res = getResources();
2130         updateSplitNotificationShade();
2131         mStatusBarHeight = SystemBarUtils.getStatusBarHeight(mContext);
2132         float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop();
2133         mSwipeHelper.setPagingTouchSlop(pagingTouchSlop);
2134         reinitView();
2135     }
2136 
2137     public void dismissViewAnimated(
2138             View child, Consumer<Boolean> endRunnable, int delay, long duration) {
2139         if (child instanceof SectionHeaderView) {
2140             ((StackScrollerDecorView) child).setContentVisible(
2141                     false /* visible */, true /* animate */, endRunnable);
2142             return;
2143         }
2144         mSwipeHelper.dismissChild(
2145                 child,
2146                 0 /* velocity */,
2147                 endRunnable,
2148                 delay,
2149                 true /* useAccelerateInterpolator */,
2150                 duration,
2151                 true /* isClearAll */);
2152     }
2153 
2154     private void snapViewIfNeeded(NotificationEntry entry) {
2155         ExpandableNotificationRow child = entry.getRow();
2156         boolean animate = mIsExpanded || isPinnedHeadsUp(child);
2157         // If the child is showing the notification menu snap to that
2158         if (child.getProvider() != null) {
2159             float targetLeft = child.getProvider().isMenuVisible() ? child.getTranslation() : 0;
2160             mSwipeHelper.snapChildIfNeeded(child, animate, targetLeft);
2161         }
2162     }
2163 
2164     public ViewGroup getViewParentForNotification() {
2165         return this;
2166     }
2167 
2168     /**
2169      * Perform a scroll upwards and adapt the overscroll amounts accordingly
2170      *
2171      * @param deltaY The amount to scroll upwards, has to be positive.
2172      * @return The amount of scrolling to be performed by the scroller,
2173      * not handled by the overScroll amount.
2174      */
2175     private float overScrollUp(int deltaY, int range) {
2176         deltaY = Math.max(deltaY, 0);
2177         float currentTopAmount = getCurrentOverScrollAmount(true);
2178         float newTopAmount = currentTopAmount - deltaY;
2179         if (currentTopAmount > 0) {
2180             setOverScrollAmount(newTopAmount, true /* onTop */,
2181                     false /* animate */);
2182         }
2183         // Top overScroll might not grab all scrolling motion,
2184         // we have to scroll as well.
2185         float scrollAmount = newTopAmount < 0 ? -newTopAmount : 0.0f;
2186         float newScrollY = getOwnScrollY() + scrollAmount;
2187         if (newScrollY > range) {
2188             if (!mExpandedInThisMotion) {
2189                 float currentBottomPixels = getCurrentOverScrolledPixels(false);
2190                 // We overScroll on the bottom
2191                 setOverScrolledPixels(currentBottomPixels + newScrollY - range,
2192                         false /* onTop */,
2193                         false /* animate */);
2194             }
2195             setOwnScrollY(range);
2196             scrollAmount = 0.0f;
2197         }
2198         return scrollAmount;
2199     }
2200 
2201     /**
2202      * Perform a scroll downward and adapt the overscroll amounts accordingly
2203      *
2204      * @param deltaY The amount to scroll downwards, has to be negative.
2205      * @return The amount of scrolling to be performed by the scroller,
2206      * not handled by the overScroll amount.
2207      */
2208     private float overScrollDown(int deltaY) {
2209         deltaY = Math.min(deltaY, 0);
2210         float currentBottomAmount = getCurrentOverScrollAmount(false);
2211         float newBottomAmount = currentBottomAmount + deltaY;
2212         if (currentBottomAmount > 0) {
2213             setOverScrollAmount(newBottomAmount, false /* onTop */,
2214                     false /* animate */);
2215         }
2216         // Bottom overScroll might not grab all scrolling motion,
2217         // we have to scroll as well.
2218         float scrollAmount = newBottomAmount < 0 ? newBottomAmount : 0.0f;
2219         float newScrollY = getOwnScrollY() + scrollAmount;
2220         if (newScrollY < 0) {
2221             float currentTopPixels = getCurrentOverScrolledPixels(true);
2222             // We overScroll on the top
2223             setOverScrolledPixels(currentTopPixels - newScrollY,
2224                     true /* onTop */,
2225                     false /* animate */);
2226             setOwnScrollY(0);
2227             scrollAmount = 0.0f;
2228         }
2229         return scrollAmount;
2230     }
2231 
2232     private void initVelocityTrackerIfNotExists() {
2233         if (mVelocityTracker == null) {
2234             mVelocityTracker = VelocityTracker.obtain();
2235         }
2236     }
2237 
2238     private void recycleVelocityTracker() {
2239         if (mVelocityTracker != null) {
2240             mVelocityTracker.recycle();
2241             mVelocityTracker = null;
2242         }
2243     }
2244 
2245     private void initOrResetVelocityTracker() {
2246         if (mVelocityTracker == null) {
2247             mVelocityTracker = VelocityTracker.obtain();
2248         } else {
2249             mVelocityTracker.clear();
2250         }
2251     }
2252 
2253     public void setFinishScrollingCallback(Runnable runnable) {
2254         SceneContainerFlag.assertInLegacyMode();
2255         mFinishScrollingCallback = runnable;
2256     }
2257 
2258     private void animateScroll() {
2259         if (mScroller.computeScrollOffset()) {
2260             int oldY = getOwnScrollY();
2261             int y = mScroller.getCurrY();
2262 
2263             if (oldY != y) {
2264                 int range = getScrollRange();
2265                 if (y < 0 && oldY >= 0 || y > range && oldY <= range) {
2266                     // This frame takes us into overscroll, so set the max overscroll based on
2267                     // the current velocity
2268                     setMaxOverScrollFromCurrentVelocity();
2269                 }
2270 
2271                 if (mDontClampNextScroll) {
2272                     range = Math.max(range, oldY);
2273                 }
2274                 customOverScrollBy(y - oldY, oldY, range,
2275                         (int) (mMaxOverScroll));
2276             }
2277             postOnAnimation(mReflingAndAnimateScroll);
2278         } else {
2279             mDontClampNextScroll = false;
2280             if (mFinishScrollingCallback != null) {
2281                 mFinishScrollingCallback.run();
2282             }
2283         }
2284     }
2285 
2286     private void setMaxOverScrollFromCurrentVelocity() {
2287         float currVelocity = mScroller.getCurrVelocity();
2288         if (currVelocity >= mMinimumVelocity) {
2289             mMaxOverScroll = Math.abs(currVelocity) / 1000 * mOverflingDistance;
2290         }
2291     }
2292 
2293     /**
2294      * Scrolls by the given delta, overscrolling if needed.  If called during a fling and the delta
2295      * would cause us to exceed the provided maximum overscroll, springs back instead.
2296      * <p>
2297      * This method performs the determination of whether we're exceeding the overscroll and clamps
2298      * the scroll amount if so.  The actual scrolling/overscrolling happens in
2299      * {@link #onCustomOverScrolled(int, boolean)}
2300      *
2301      * @param deltaY         The (signed) number of pixels to scroll.
2302      * @param scrollY        The current scroll position (absolute scrolling only).
2303      * @param scrollRangeY   The maximum allowable scroll position (absolute scrolling only).
2304      * @param maxOverScrollY The current (unsigned) limit on number of pixels to overscroll by.
2305      */
2306     private void customOverScrollBy(int deltaY, int scrollY, int scrollRangeY, int maxOverScrollY) {
2307         int newScrollY = scrollY + deltaY;
2308         final int top = -maxOverScrollY;
2309         final int bottom = maxOverScrollY + scrollRangeY;
2310 
2311         boolean clampedY = false;
2312         if (newScrollY > bottom) {
2313             newScrollY = bottom;
2314             clampedY = true;
2315         } else if (newScrollY < top) {
2316             newScrollY = top;
2317             clampedY = true;
2318         }
2319 
2320         onCustomOverScrolled(newScrollY, clampedY);
2321     }
2322 
2323     /**
2324      * Set the amount of overScrolled pixels which will force the view to apply a rubber-banded
2325      * overscroll effect based on numPixels. By default this will also cancel animations on the
2326      * same overScroll edge.
2327      *
2328      * @param numPixels The amount of pixels to overScroll by. These will be scaled according to
2329      *                  the rubber-banding logic.
2330      * @param onTop     Should the effect be applied on top of the scroller.
2331      * @param animate   Should an animation be performed.
2332      */
2333     public void setOverScrolledPixels(float numPixels, boolean onTop, boolean animate) {
2334         setOverScrollAmount(numPixels * getRubberBandFactor(onTop), onTop, animate, true);
2335     }
2336 
2337     /**
2338      * Set the effective overScroll amount which will be directly reflected in the layout.
2339      * By default this will also cancel animations on the same overScroll edge.
2340      *
2341      * @param amount  The amount to overScroll by.
2342      * @param onTop   Should the effect be applied on top of the scroller.
2343      * @param animate Should an animation be performed.
2344      */
2345 
2346     public void setOverScrollAmount(float amount, boolean onTop, boolean animate) {
2347         setOverScrollAmount(amount, onTop, animate, true);
2348     }
2349 
2350     /**
2351      * Set the effective overScroll amount which will be directly reflected in the layout.
2352      *
2353      * @param amount          The amount to overScroll by.
2354      * @param onTop           Should the effect be applied on top of the scroller.
2355      * @param animate         Should an animation be performed.
2356      * @param cancelAnimators Should running animations be cancelled.
2357      */
2358     public void setOverScrollAmount(float amount, boolean onTop, boolean animate,
2359                                     boolean cancelAnimators) {
2360         setOverScrollAmount(amount, onTop, animate, cancelAnimators, isRubberbanded(onTop));
2361     }
2362 
2363     /**
2364      * Set the effective overScroll amount which will be directly reflected in the layout.
2365      *
2366      * @param amount          The amount to overScroll by.
2367      * @param onTop           Should the effect be applied on top of the scroller.
2368      * @param animate         Should an animation be performed.
2369      * @param cancelAnimators Should running animations be cancelled.
2370      * @param isRubberbanded  The value which will be passed to
2371      *                        {@link OnOverscrollTopChangedListener#onOverscrollTopChanged}
2372      */
2373     public void setOverScrollAmount(float amount, boolean onTop, boolean animate,
2374                                     boolean cancelAnimators, boolean isRubberbanded) {
2375         if (cancelAnimators) {
2376             mStateAnimator.cancelOverScrollAnimators(onTop);
2377         }
2378         setOverScrollAmountInternal(amount, onTop, animate, isRubberbanded);
2379     }
2380 
2381     private void setOverScrollAmountInternal(float amount, boolean onTop, boolean animate,
2382                                              boolean isRubberbanded) {
2383         SceneContainerFlag.assertInLegacyMode();
2384         amount = Math.max(0, amount);
2385         if (animate) {
2386             mStateAnimator.animateOverScrollToAmount(amount, onTop, isRubberbanded);
2387         } else {
2388             setOverScrolledPixels(amount / getRubberBandFactor(onTop), onTop);
2389             mAmbientState.setOverScrollAmount(amount, onTop);
2390             if (onTop) {
2391                 notifyOverscrollTopListener(amount, isRubberbanded);
2392             }
2393             updateStackPosition();
2394             requestChildrenUpdate();
2395         }
2396     }
2397 
2398     private void notifyOverscrollTopListener(float amount, boolean isRubberbanded) {
2399         mExpandHelper.onlyObserveMovements(amount > 1.0f);
2400         if (mDontReportNextOverScroll) {
2401             mDontReportNextOverScroll = false;
2402             return;
2403         }
2404         if (mOverscrollTopChangedListener != null) {
2405             mOverscrollTopChangedListener.onOverscrollTopChanged(amount, isRubberbanded);
2406         }
2407     }
2408 
2409     public void setOverscrollTopChangedListener(
2410             OnOverscrollTopChangedListener overscrollTopChangedListener) {
2411         SceneContainerFlag.assertInLegacyMode();
2412         mOverscrollTopChangedListener = overscrollTopChangedListener;
2413     }
2414 
2415     public float getCurrentOverScrollAmount(boolean top) {
2416         return mAmbientState.getOverScrollAmount(top);
2417     }
2418 
2419     public float getCurrentOverScrolledPixels(boolean top) {
2420         return top ? mOverScrolledTopPixels : mOverScrolledBottomPixels;
2421     }
2422 
2423     private void setOverScrolledPixels(float amount, boolean onTop) {
2424         if (onTop) {
2425             mOverScrolledTopPixels = amount;
2426         } else {
2427             mOverScrolledBottomPixels = amount;
2428         }
2429     }
2430 
2431     /**
2432      * Scrolls to the given position, overscrolling if needed.  If called during a fling and the
2433      * position exceeds the provided maximum overscroll, springs back instead.
2434      *
2435      * @param scrollY  The target scroll position.
2436      * @param clampedY Whether this value was clamped by the calling method, meaning we've reached
2437      *                 the overscroll limit.
2438      */
2439     private void onCustomOverScrolled(int scrollY, boolean clampedY) {
2440         // Treat animating scrolls differently; see #computeScroll() for why.
2441         if (!mScroller.isFinished()) {
2442             setOwnScrollY(scrollY);
2443             if (clampedY) {
2444                 springBack();
2445             } else {
2446                 float overScrollTop = getCurrentOverScrollAmount(true);
2447                 if (getOwnScrollY() < 0) {
2448                     notifyOverscrollTopListener(-getOwnScrollY(), isRubberbanded(true));
2449                 } else {
2450                     notifyOverscrollTopListener(overScrollTop, isRubberbanded(true));
2451                 }
2452             }
2453         } else {
2454             setOwnScrollY(scrollY);
2455         }
2456     }
2457 
2458     /**
2459      * Springs back from an overscroll by stopping the {@link #mScroller} and animating the
2460      * overscroll amount back to zero.
2461      */
2462     private void springBack() {
2463         int scrollRange = getScrollRange();
2464         boolean overScrolledTop = getOwnScrollY() <= 0;
2465         boolean overScrolledBottom = getOwnScrollY() >= scrollRange;
2466         if (overScrolledTop || overScrolledBottom) {
2467             boolean onTop;
2468             float newAmount;
2469             if (overScrolledTop) {
2470                 onTop = true;
2471                 newAmount = -getOwnScrollY();
2472                 setOwnScrollY(0);
2473                 mDontReportNextOverScroll = true;
2474             } else {
2475                 onTop = false;
2476                 newAmount = getOwnScrollY() - scrollRange;
2477                 setOwnScrollY(scrollRange);
2478             }
2479             setOverScrollAmount(newAmount, onTop, false);
2480             setOverScrollAmount(0.0f, onTop, true);
2481             mScroller.forceFinished(true);
2482         }
2483     }
2484 
2485     private int getScrollRange() {
2486         // TODO(b/360091533) Disable the usages of #getScrollRange() with SceneContainer enabled,
2487         // because NSSL shouldn't control its own scrolling.
2488         if (SceneContainerFlag.isEnabled()) {
2489             return 0;
2490         }
2491         // In current design, it only use the top HUN to treat all of HUNs
2492         // although there are more than one HUNs
2493         int contentHeight = getContentHeight();
2494         if (!isExpanded() && mInHeadsUpPinnedMode) {
2495             contentHeight = mHeadsUpInset + getTopHeadsUpPinnedHeight();
2496         }
2497         int scrollRange = Math.max(0, contentHeight - mMaxLayoutHeight);
2498         int imeInset = getImeInset();
2499         scrollRange += Math.min(imeInset, Math.max(0, contentHeight - (getHeight() - imeInset)));
2500         if (scrollRange > 0) {
2501             scrollRange = Math.max(getScrollAmountToScrollBoundary(), scrollRange);
2502         }
2503         return scrollRange;
2504     }
2505 
2506     private int getImeInset() {
2507         SceneContainerFlag.assertInLegacyMode();
2508         // The NotificationStackScrollLayout does not extend all the way to the bottom of the
2509         // display. Therefore, subtract that space from the mImeInset, in order to only include
2510         // the portion of the bottom inset that actually overlaps the NotificationStackScrollLayout.
2511         return Math.max(0, mImeInset
2512                 - (getRootView().getHeight() - getHeight() - getLocationOnScreen()[1]));
2513     }
2514 
2515     /**
2516      * @return the first child which has visibility unequal to GONE
2517      */
2518     public ExpandableView getFirstChildNotGone() {
2519         SceneContainerFlag.assertInLegacyMode();
2520         return getFirstChildNotGoneInternal();
2521     }
2522 
2523     private ExpandableView getFirstChildNotGoneInternal() {
2524         int childCount = getChildCount();
2525         for (int i = 0; i < childCount; i++) {
2526             View child = getChildAt(i);
2527             if (child.getVisibility() != View.GONE && child != mShelf) {
2528                 return (ExpandableView) child;
2529             }
2530         }
2531         return null;
2532     }
2533 
2534     /**
2535      * @return The first child which has visibility unequal to GONE which is currently below the
2536      * given translationY or equal to it.
2537      */
2538     private View getFirstChildBelowTranlsationY(float translationY, boolean ignoreChildren) {
2539         int childCount = getChildCount();
2540         for (int i = 0; i < childCount; i++) {
2541             View child = getChildAt(i);
2542             if (child.getVisibility() == View.GONE) {
2543                 continue;
2544             }
2545             float rowTranslation = child.getTranslationY();
2546             if (rowTranslation >= translationY) {
2547                 return child;
2548             } else if (!ignoreChildren && child instanceof ExpandableNotificationRow row) {
2549                 if (row.isSummaryWithChildren() && row.areChildrenExpanded()) {
2550                     List<ExpandableNotificationRow> notificationChildren =
2551                             row.getAttachedChildren();
2552                     int childrenSize = notificationChildren.size();
2553                     for (int childIndex = 0; childIndex < childrenSize; childIndex++) {
2554                         ExpandableNotificationRow rowChild = notificationChildren.get(childIndex);
2555                         if (rowChild.getTranslationY() + rowTranslation >= translationY) {
2556                             return rowChild;
2557                         }
2558                     }
2559                 }
2560             }
2561         }
2562         return null;
2563     }
2564 
2565     /**
2566      * @return the last child which has visibility unequal to GONE
2567      */
2568     public ExpandableView getLastChildNotGone() {
2569         int childCount = getChildCount();
2570         for (int i = childCount - 1; i >= 0; i--) {
2571             View child = getChildAt(i);
2572             if (child.getVisibility() != View.GONE && child != mShelf) {
2573                 return (ExpandableView) child;
2574             }
2575         }
2576         return null;
2577     }
2578 
2579     /**
2580      * @return the number of children which have visibility unequal to GONE
2581      */
2582     public int getNotGoneChildCount() {
2583         SceneContainerFlag.assertInLegacyMode();
2584         int childCount = getChildCount();
2585         int count = 0;
2586         for (int i = 0; i < childCount; i++) {
2587             ExpandableView child = getChildAtIndex(i);
2588             if (child.getVisibility() != View.GONE && !child.willBeGone() && child != mShelf) {
2589                 count++;
2590             }
2591         }
2592         return count;
2593     }
2594 
2595     @VisibleForTesting
2596     void updateIntrinsicStackHeight() {
2597         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
2598 
2599         final int shelfIntrinsicHeight = mShelf != null ? mShelf.getIntrinsicHeight() : 0;
2600         final int notificationsHeight = (int) mNotificationStackSizeCalculator.computeHeight(
2601                 /* notificationStackScrollLayout= */ this,
2602                 mMaxDisplayedNotifications,
2603                 shelfIntrinsicHeight
2604         );
2605         if (mScrollViewFields.getIntrinsicStackHeight() != notificationsHeight) {
2606             mScrollViewFields.setIntrinsicStackHeight(notificationsHeight);
2607             notifyStackHeightChangedListeners();
2608         }
2609     }
2610 
2611     private void updateContentHeight() {
2612         if (SceneContainerFlag.isEnabled()) {
2613             updateIntrinsicStackHeight();
2614             updateStackEndHeightAndStackHeight(mAmbientState.getExpansionFraction());
2615             return;
2616         }
2617 
2618         final float scrimTopPadding = getScrimTopPaddingOrZero();
2619         final int shelfIntrinsicHeight = mShelf != null ? mShelf.getIntrinsicHeight() : 0;
2620         final float height =
2621                 (int) scrimTopPadding + (int) mNotificationStackSizeCalculator.computeHeight(
2622                         /* notificationStackScrollLayout= */ this, mMaxDisplayedNotifications,
2623                         shelfIntrinsicHeight);
2624         setIntrinsicContentHeight(height);
2625 
2626         // The topPadding can be bigger than the regular padding when qs is expanded, in that
2627         // state the maxPanelHeight and the contentHeight should be bigger
2628         setContentHeight(
2629                 (int) (height + Math.max(getIntrinsicPadding(), getTopPadding()) + mBottomPadding));
2630         updateScrollability();
2631         clampScrollPosition();
2632         updateStackPosition();
2633         mAmbientState.setContentHeight(getContentHeight());
2634     }
2635 
2636     @Override
2637     public int getIntrinsicStackHeight() {
2638         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return 0;
2639         return mScrollViewFields.getIntrinsicStackHeight();
2640     }
2641 
2642     @Override
2643     public int getTopHeadsUpHeight() {
2644         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return 0;
2645         return getTopHeadsUpIntrinsicHeight();
2646     }
2647 
2648     @Override
2649     public int getStackBottomInset() {
2650         return mPaddingBetweenElements + mShelf.getIntrinsicHeight();
2651     }
2652 
2653     /**
2654      * Calculate the gap height between two different views
2655      *
2656      * @param previous     the previousView
2657      * @param current      the currentView
2658      * @param visibleIndex the visible index in the list
2659      * @return the gap height needed before the current view
2660      */
2661     public float calculateGapHeight(
2662             ExpandableView previous,
2663             ExpandableView current,
2664             int visibleIndex
2665     ) {
2666         return mStackScrollAlgorithm.getGapHeightForChild(mSectionsManager, visibleIndex, current,
2667                 previous, mAmbientState.getFractionToShade(), mAmbientState.isOnKeyguard());
2668     }
2669 
2670     public boolean hasPulsingNotifications() {
2671         return mPulsing;
2672     }
2673 
2674     private void updateScrollability() {
2675         if (SceneContainerFlag.isEnabled()) {
2676             return;
2677         }
2678         boolean scrollable = !mQsFullScreen && getScrollRange() > 0;
2679         if (scrollable != mScrollable) {
2680             mScrollable = scrollable;
2681             setFocusable(scrollable);
2682             updateForwardAndBackwardScrollability();
2683         }
2684     }
2685 
2686     private void updateForwardAndBackwardScrollability() {
2687         SceneContainerFlag.assertInLegacyMode();
2688         boolean forwardScrollable = mScrollable && !mScrollAdapter.isScrolledToBottom();
2689         boolean backwardsScrollable = mScrollable && !mScrollAdapter.isScrolledToTop();
2690         boolean changed = forwardScrollable != mForwardScrollable
2691                 || backwardsScrollable != mBackwardScrollable;
2692         mForwardScrollable = forwardScrollable;
2693         mBackwardScrollable = backwardsScrollable;
2694         if (changed) {
2695             sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
2696         }
2697     }
2698 
2699     private NotificationSection getFirstVisibleSection() {
2700         for (NotificationSection section : mSections) {
2701             if (section.getFirstVisibleChild() != null) {
2702                 return section;
2703             }
2704         }
2705         return null;
2706     }
2707 
2708     private NotificationSection getLastVisibleSection() {
2709         for (int i = mSections.length - 1; i >= 0; i--) {
2710             NotificationSection section = mSections[i];
2711             if (section.getLastVisibleChild() != null) {
2712                 return section;
2713             }
2714         }
2715         return null;
2716     }
2717 
2718     private ExpandableView getLastChildWithBackground() {
2719         int childCount = getChildCount();
2720         for (int i = childCount - 1; i >= 0; i--) {
2721             ExpandableView child = getChildAtIndex(i);
2722             if (child.getVisibility() != View.GONE && !(child instanceof StackScrollerDecorView)
2723                     && child != mShelf) {
2724                 return child;
2725             }
2726         }
2727         return null;
2728     }
2729 
2730     private ExpandableView getFirstChildWithBackground() {
2731         int childCount = getChildCount();
2732         for (int i = 0; i < childCount; i++) {
2733             ExpandableView child = getChildAtIndex(i);
2734             if (child.getVisibility() != View.GONE && !(child instanceof StackScrollerDecorView)
2735                     && child != mShelf) {
2736                 return child;
2737             }
2738         }
2739         return null;
2740     }
2741 
2742     //TODO: We shouldn't have to generate this list every time
2743     private List<ExpandableView> getChildrenWithBackground() {
2744         ArrayList<ExpandableView> children = new ArrayList<>();
2745         int childCount = getChildCount();
2746         for (int i = 0; i < childCount; i++) {
2747             ExpandableView child = getChildAtIndex(i);
2748             if (child.getVisibility() != View.GONE
2749                     && !(child instanceof StackScrollerDecorView)
2750                     && child != mShelf) {
2751                 children.add(child);
2752             }
2753         }
2754         return children;
2755     }
2756 
2757     /**
2758      * Fling the scroll view
2759      *
2760      * @param velocityY The initial velocity in the Y direction. Positive
2761      *                  numbers mean that the finger/cursor is moving down the screen,
2762      *                  which means we want to scroll towards the top.
2763      */
2764     protected void fling(int velocityY) {
2765         // Scrolls and flings are handled by the Composables with SceneContainer enabled
2766         SceneContainerFlag.assertInLegacyMode();
2767         if (getChildCount() > 0) {
2768             float topAmount = getCurrentOverScrollAmount(true);
2769             float bottomAmount = getCurrentOverScrollAmount(false);
2770             if (velocityY < 0 && topAmount > 0) {
2771                 setOwnScrollY(getOwnScrollY() - (int) topAmount);
2772                 if (!mShouldUseSplitNotificationShade) {
2773                     mDontReportNextOverScroll = true;
2774                     setOverScrollAmount(0, true, false);
2775                 }
2776                 mMaxOverScroll = Math.abs(velocityY) / 1000f * getRubberBandFactor(true /* onTop */)
2777                         * mOverflingDistance + topAmount;
2778             } else if (velocityY > 0 && bottomAmount > 0) {
2779                 setOwnScrollY((int) (getOwnScrollY() + bottomAmount));
2780                 setOverScrollAmount(0, false, false);
2781                 mMaxOverScroll = Math.abs(velocityY) / 1000f
2782                         * getRubberBandFactor(false /* onTop */) * mOverflingDistance
2783                         + bottomAmount;
2784             } else {
2785                 // it will be set once we reach the boundary
2786                 mMaxOverScroll = 0.0f;
2787             }
2788             int scrollRange = getScrollRange();
2789             int minScrollY = Math.max(0, scrollRange);
2790             if (mExpandedInThisMotion) {
2791                 minScrollY = Math.min(minScrollY, mMaxScrollAfterExpand);
2792             }
2793             mScroller.fling(mScrollX, getOwnScrollY(), 1, velocityY, 0, 0, 0, minScrollY, 0,
2794                     mExpandedInThisMotion && getOwnScrollY() >= 0 ? 0 : Integer.MAX_VALUE / 2);
2795 
2796             animateScroll();
2797         }
2798     }
2799 
2800     /**
2801      * @return Whether a fling performed on the top overscroll edge lead to the expanded
2802      * overScroll view (i.e QS).
2803      */
2804     private boolean shouldOverScrollFling(int initialVelocity) {
2805         float topOverScroll = getCurrentOverScrollAmount(true);
2806         return mScrolledToTopOnFirstDown
2807                 && !mExpandedInThisMotion
2808                 && !mShouldUseSplitNotificationShade
2809                 && (initialVelocity > mMinimumVelocity
2810                 || (topOverScroll > mMinTopOverScrollToEscape && initialVelocity > 0));
2811     }
2812 
2813     /**
2814      * Updates the top padding of the notifications, taking {@link #getIntrinsicPadding()} into
2815      * account.
2816      *
2817      * @param qsHeight the top padding imposed by the quick settings panel
2818      * @param animate  whether to animate the change
2819      */
updateTopPadding(float qsHeight, boolean animate)2820     public void updateTopPadding(float qsHeight, boolean animate) {
2821         SceneContainerFlag.assertInLegacyMode();
2822         int topPadding = (int) qsHeight;
2823         int minStackHeight = getLayoutMinHeightInternal();
2824         if (topPadding + minStackHeight > getHeight()) {
2825             mTopPaddingOverflow = topPadding + minStackHeight - getHeight();
2826         } else {
2827             mTopPaddingOverflow = 0;
2828         }
2829         if (mAmbientState.getTopPadding() != topPadding) {
2830             mAmbientState.setTopPadding(topPadding);
2831             onTopPaddingChanged(/* animate = */ animate && !mKeyguardBypassEnabled);
2832         }
2833         setExpandedHeight(mExpandedHeight);
2834     }
2835 
getLayoutMinHeight()2836     public int getLayoutMinHeight() {
2837         SceneContainerFlag.assertInLegacyMode();
2838         return getLayoutMinHeightInternal();
2839     }
2840 
getLayoutMinHeightInternal()2841     private int getLayoutMinHeightInternal() {
2842         SceneContainerFlag.assertInLegacyMode();
2843         if (isHeadsUpTransition()) {
2844             ExpandableNotificationRow trackedHeadsUpRow = mAmbientState.getTrackedHeadsUpRow();
2845             if (trackedHeadsUpRow.isAboveShelf()) {
2846                 int hunDistance = (int) MathUtils.lerp(
2847                         0,
2848                         getPositionInLinearLayout(trackedHeadsUpRow),
2849                         mAmbientState.getAppearFraction());
2850                 return getTopHeadsUpPinnedHeight() + hunDistance;
2851             } else {
2852                 return getTopHeadsUpPinnedHeight();
2853             }
2854         }
2855         return mShelf.getVisibility() == GONE ? 0 : mShelf.getIntrinsicHeight();
2856     }
2857 
getTopPaddingOverflow()2858     public float getTopPaddingOverflow() {
2859         SceneContainerFlag.assertInLegacyMode();
2860         return mTopPaddingOverflow;
2861     }
2862 
getRubberBandFactor(boolean onTop)2863     private float getRubberBandFactor(boolean onTop) {
2864         if (!onTop) {
2865             return RUBBER_BAND_FACTOR_NORMAL;
2866         }
2867         if (mExpandedInThisMotion) {
2868             return RUBBER_BAND_FACTOR_AFTER_EXPAND;
2869         } else if (mIsExpansionChanging || mPanelTracking) {
2870             return RUBBER_BAND_FACTOR_ON_PANEL_EXPAND;
2871         } else if (mScrolledToTopOnFirstDown && !mShouldUseSplitNotificationShade) {
2872             return 1.0f;
2873         }
2874         return RUBBER_BAND_FACTOR_NORMAL;
2875     }
2876 
2877     /**
2878      * Accompanying function for {@link #getRubberBandFactor}: Returns true if the overscroll is
2879      * rubberbanded, false if it is technically an overscroll but rather a motion to expand the
2880      * overscroll view (e.g. expand QS).
2881      */
isRubberbanded(boolean onTop)2882     private boolean isRubberbanded(boolean onTop) {
2883         return !onTop || mExpandedInThisMotion || mIsExpansionChanging || mPanelTracking
2884                 || !mScrolledToTopOnFirstDown;
2885     }
2886 
2887 
setChildTransferInProgress(boolean childTransferInProgress)2888     public void setChildTransferInProgress(boolean childTransferInProgress) {
2889         Assert.isMainThread();
2890         mChildTransferInProgress = childTransferInProgress;
2891     }
2892 
2893     @Override
onViewRemoved(View child)2894     public void onViewRemoved(View child) {
2895         super.onViewRemoved(child);
2896         // we only call our internal methods if this is actually a removal and not just a
2897         // notification which becomes a child notification
2898         ExpandableView expandableView = (ExpandableView) child;
2899         if (!mChildTransferInProgress) {
2900             onViewRemovedInternal(expandableView, this);
2901         }
2902         mShelf.requestRoundnessResetFor(expandableView);
2903     }
2904 
cleanUpViewStateForEntry(NotificationEntry entry)2905     public void cleanUpViewStateForEntry(NotificationEntry entry) {
2906         View child = entry.getRow();
2907         if (child == mSwipeHelper.getTranslatingParentView()) {
2908             mSwipeHelper.clearTranslatingParentView();
2909         }
2910     }
2911 
onViewRemovedInternal(ExpandableView child, ViewGroup container)2912     private void onViewRemovedInternal(ExpandableView child, ViewGroup container) {
2913         if (mChangePositionInProgress) {
2914             // This is only a position change, don't do anything special
2915             return;
2916         }
2917         child.setOnHeightChangedListener(null);
2918         if (child instanceof ExpandableNotificationRow) {
2919             NotificationEntry entry = ((ExpandableNotificationRow) child).getEntry();
2920             entry.removeOnSensitivityChangedListener(mOnChildSensitivityChangedListener);
2921         }
2922         if (!SceneContainerFlag.isEnabled()) {
2923             updateScrollStateForRemovedChild(child);
2924         }
2925         boolean animationGenerated = container != null && generateRemoveAnimation(child);
2926         if (animationGenerated) {
2927             if (!mSwipedOutViews.contains(child) || !isFullySwipedOut(child)) {
2928                 logAddTransientChild(child, container);
2929                 child.addToTransientContainer(container, 0);
2930             }
2931         } else {
2932             mSwipedOutViews.remove(child);
2933 
2934             if (child instanceof ExpandableNotificationRow) {
2935                 ((ExpandableNotificationRow) child).removeChildrenWithKeepInParent();
2936             }
2937         }
2938         updateAnimationState(false, child);
2939 
2940         focusNextViewIfFocused(child);
2941     }
2942 
logAddTransientChild(ExpandableView child, ViewGroup container)2943     private void logAddTransientChild(ExpandableView child, ViewGroup container) {
2944         if (mLogger == null) {
2945             return;
2946         }
2947         if (child instanceof ExpandableNotificationRow) {
2948             if (container instanceof NotificationChildrenContainer) {
2949                 mLogger.addTransientChildNotificationToChildContainer(
2950                         ((ExpandableNotificationRow) child).getLoggingKey(),
2951                         ((NotificationChildrenContainer) container)
2952                                 .getContainingNotification().getLoggingKey()
2953                 );
2954             } else if (container instanceof NotificationStackScrollLayout) {
2955                 mLogger.addTransientChildNotificationToNssl(
2956                         ((ExpandableNotificationRow) child).getLoggingKey()
2957                 );
2958             } else {
2959                 mLogger.addTransientChildNotificationToViewGroup(
2960                         ((ExpandableNotificationRow) child).getLoggingKey(),
2961                         container
2962                 );
2963             }
2964         }
2965     }
2966 
2967     @Override
addTransientView(View view, int index)2968     public void addTransientView(View view, int index) {
2969         if (mLogger != null && view instanceof ExpandableNotificationRow) {
2970             mLogger.addTransientRow(((ExpandableNotificationRow) view).getLoggingKey(), index);
2971         }
2972         super.addTransientView(view, index);
2973     }
2974 
2975     @Override
removeTransientView(View view)2976     public void removeTransientView(View view) {
2977         if (mLogger != null && view instanceof ExpandableNotificationRow) {
2978             mLogger.removeTransientRow(((ExpandableNotificationRow) view).getLoggingKey());
2979         }
2980         super.removeTransientView(view);
2981     }
2982 
2983     /**
2984      * Has this view been fully swiped out such that it's not visible anymore.
2985      */
isFullySwipedOut(ExpandableView child)2986     public boolean isFullySwipedOut(ExpandableView child) {
2987         return Math.abs(child.getTranslation()) >= Math.abs(getTotalTranslationLength(child));
2988     }
2989 
focusNextViewIfFocused(View view)2990     private void focusNextViewIfFocused(View view) {
2991         if (view instanceof ExpandableNotificationRow row) {
2992             if (row.shouldRefocusOnDismiss()) {
2993                 View nextView = row.getChildAfterViewWhenDismissed();
2994                 if (nextView == null) {
2995                     View groupParentWhenDismissed = row.getGroupParentWhenDismissed();
2996                     nextView = getFirstChildBelowTranlsationY(groupParentWhenDismissed != null
2997                             ? groupParentWhenDismissed.getTranslationY()
2998                             : view.getTranslationY(), true /* ignoreChildren */);
2999                 }
3000                 if (nextView != null) {
3001                     nextView.requestAccessibilityFocus();
3002                 }
3003             }
3004         }
3005 
3006     }
3007 
isChildInGroup(View child)3008     private boolean isChildInGroup(View child) {
3009         if (child instanceof ExpandableNotificationRow) {
3010             ExpandableNotificationRow childRow = (ExpandableNotificationRow) child;
3011             return NotificationBundleUi.isEnabled()
3012                     ? mGroupMembershipManager.isChildInGroup(childRow.getEntryAdapter())
3013                     : mGroupMembershipManager.isChildInGroup(childRow.getEntryLegacy());
3014         }
3015         return false;
3016     }
3017 
3018     /**
3019      * Generate a remove animation for a child view.
3020      *
3021      * @param child The view to generate the remove animation for.
3022      * @return Whether a new animation was generated or an existing animation was detected by this
3023      * method. We need this to determine if a transient view is needed.
3024      */
generateRemoveAnimation(ExpandableView child)3025     boolean generateRemoveAnimation(ExpandableView child) {
3026         String key = "";
3027         if (mDebugRemoveAnimation) {
3028             if (child instanceof ExpandableNotificationRow) {
3029                 key = ((ExpandableNotificationRow) child).getKey();
3030             }
3031             Log.d(TAG, "generateRemoveAnimation " + key);
3032         }
3033         if (removeRemovedChildFromHeadsUpChangeAnimations(child)) {
3034             if (mDebugRemoveAnimation) {
3035                 Log.d(TAG, "removedBecauseOfHeadsUp " + key);
3036             }
3037             mAddedHeadsUpChildren.remove(child);
3038             return false;
3039         }
3040         // Skip adding animation for clicked heads up notifications when the
3041         // Shade is closed, because the animation event is generated in
3042         // generateHeadsUpAnimationEvents. Only report that an animation was
3043         // actually generated (thus requesting the transient view be added)
3044         // if a removal animation is in progress.
3045         if (!isExpanded() && isClickedHeadsUp(child)) {
3046             // An animation is already running, add it transiently
3047             mClearTransientViewsWhenFinished.add(child);
3048             return child.inRemovalAnimation();
3049         }
3050         if (mDebugRemoveAnimation) {
3051             Log.d(TAG, "generateRemove " + key
3052                     + "\nmIsExpanded " + mIsExpanded
3053                     + "\nmAnimationsEnabled " + mAnimationsEnabled);
3054         }
3055         if (mIsExpanded && mAnimationsEnabled) {
3056             if (!mChildrenToAddAnimated.contains(child)) {
3057                 if (mDebugRemoveAnimation) {
3058                     Log.d(TAG, "needsAnimation = true " + key);
3059                 }
3060                 // Generate Animations
3061                 mChildrenToRemoveAnimated.add(child);
3062                 mNeedsAnimation = true;
3063                 return true;
3064             } else {
3065                 mChildrenToAddAnimated.remove(child);
3066                 mFromMoreCardAdditions.remove(child);
3067                 return false;
3068             }
3069         }
3070         return false;
3071     }
3072 
isClickedHeadsUp(View child)3073     private boolean isClickedHeadsUp(View child) {
3074         return HeadsUpUtil.isClickedHeadsUpNotification(child);
3075     }
3076 
3077     /**
3078      * Remove a removed child view from the heads up animations if it was just added there
3079      *
3080      * @return whether any child was removed from the list to animate and the view was just added
3081      */
removeRemovedChildFromHeadsUpChangeAnimations(View child)3082     private boolean removeRemovedChildFromHeadsUpChangeAnimations(View child) {
3083         boolean hasAddEvent = false;
3084         for (HeadsUpAnimationEvent event : mHeadsUpChangeAnimations.values()) {
3085             ExpandableNotificationRow row = event.getRow();
3086             boolean isHeadsUp = event.isHeadsUpAppearance();
3087             if (child == row) {
3088                 mTmpHeadsUpChangeAnimations.add(event.getRow());
3089                 hasAddEvent |= isHeadsUp;
3090             }
3091         }
3092         if (hasAddEvent) {
3093             // This child was just added lets remove all events.
3094             mTmpHeadsUpChangeAnimations.forEach((row) -> mHeadsUpChangeAnimations.remove(row));
3095             ((ExpandableNotificationRow) child).setHeadsUpAnimatingAway(false);
3096         }
3097         mTmpHeadsUpChangeAnimations.clear();
3098         return hasAddEvent && mAddedHeadsUpChildren.contains(child);
3099     }
3100 
3101     /**
3102      * Updates the scroll position when a child was removed
3103      *
3104      * @param removedChild the removed child
3105      */
updateScrollStateForRemovedChild(ExpandableView removedChild)3106     private void updateScrollStateForRemovedChild(ExpandableView removedChild) {
3107         SceneContainerFlag.assertInLegacyMode();
3108         final int startingPosition = getPositionInLinearLayout(removedChild);
3109         final int childHeight = getIntrinsicHeight(removedChild) + mPaddingBetweenElements;
3110         final int endPosition = startingPosition + childHeight;
3111         final int scrollBoundaryStart = getScrollAmountToScrollBoundary();
3112         mAnimateStackYForContentHeightChange = true;
3113         // This is reset onLayout
3114         if (endPosition <= getOwnScrollY() - scrollBoundaryStart) {
3115             // This child is fully scrolled of the top, so we have to deduct its height from the
3116             // scrollPosition
3117             setOwnScrollY(getOwnScrollY() - childHeight);
3118         } else if (startingPosition < getOwnScrollY() - scrollBoundaryStart) {
3119             // This child is currently being scrolled into, set the scroll position to the
3120             // start of this child
3121             setOwnScrollY(startingPosition + scrollBoundaryStart);
3122         }
3123     }
3124 
3125     /**
3126      * @return the amount of scrolling needed to start clipping notifications.
3127      */
getScrollAmountToScrollBoundary()3128     private int getScrollAmountToScrollBoundary() {
3129         SceneContainerFlag.assertInLegacyMode();
3130         if (mShouldUseSplitNotificationShade) {
3131             return mSidePaddings;
3132         }
3133         return getTopPadding() - mQsScrollBoundaryPosition;
3134     }
3135 
getIntrinsicHeight(View view)3136     private int getIntrinsicHeight(View view) {
3137         if (view instanceof ExpandableView expandableView) {
3138             return expandableView.getIntrinsicHeight();
3139         }
3140         return view.getHeight();
3141     }
3142 
getPositionInLinearLayout(View requestedView)3143     private int getPositionInLinearLayout(View requestedView) {
3144         ExpandableNotificationRow childInGroup = null;
3145         ExpandableNotificationRow requestedRow = null;
3146         if (isChildInGroup(requestedView)) {
3147             // We're asking for a child in a group. Calculate the position of the parent first,
3148             // then within the parent.
3149             childInGroup = (ExpandableNotificationRow) requestedView;
3150             requestedView = requestedRow = childInGroup.getNotificationParent();
3151         }
3152         final float scrimTopPadding = getScrimTopPaddingOrZero();
3153         int position = (int) scrimTopPadding;
3154         int visibleIndex = -1;
3155         ExpandableView lastVisibleChild = null;
3156         for (int i = 0; i < getChildCount(); i++) {
3157             ExpandableView child = getChildAtIndex(i);
3158             boolean notGone = child.getVisibility() != View.GONE;
3159             if (notGone) visibleIndex++;
3160             if (notGone && !child.hasNoContentHeight()) {
3161                 if (position != scrimTopPadding) {
3162                     if (lastVisibleChild != null) {
3163                         position += calculateGapHeight(lastVisibleChild, child, visibleIndex);
3164                     }
3165                     position += mPaddingBetweenElements;
3166                 }
3167             }
3168             if (child == requestedView) {
3169                 if (requestedRow != null) {
3170                     position += requestedRow.getPositionOfChild(childInGroup);
3171                 }
3172                 return position;
3173             }
3174             if (notGone) {
3175                 position += getIntrinsicHeight(child);
3176                 lastVisibleChild = child;
3177             }
3178         }
3179         return 0;
3180     }
3181 
3182     /**
3183      * Returns the top scrim padding, or zero if the SceneContainer flag is enabled.
3184      */
getScrimTopPaddingOrZero()3185     private int getScrimTopPaddingOrZero() {
3186         if (SceneContainerFlag.isEnabled()) {
3187             // the scrim padding is set on the notification placeholder
3188             return 0;
3189         }
3190         return mAmbientState.isOnKeyguard() ? 0 : mMinimumPaddings;
3191     }
3192 
3193     @Override
onViewAdded(View child)3194     public void onViewAdded(View child) {
3195         super.onViewAdded(child);
3196         if (child instanceof ExpandableView) {
3197             onViewAddedInternal((ExpandableView) child);
3198         }
3199     }
3200 
updateFirstAndLastBackgroundViews()3201     private void updateFirstAndLastBackgroundViews() {
3202         ExpandableView lastChild = getLastChildWithBackground();
3203         mSectionsManager.updateFirstAndLastViewsForAllSections(
3204                 mSections, getChildrenWithBackground());
3205 
3206         mAmbientState.setLastVisibleBackgroundChild(lastChild);
3207         invalidate();
3208     }
3209 
onViewAddedInternal(ExpandableView child)3210     private void onViewAddedInternal(ExpandableView child) {
3211         updateHideSensitiveForChild(child);
3212         child.setOnHeightChangedListener(mOnChildHeightChangedListener);
3213         if (child instanceof ExpandableNotificationRow row) {
3214             NotificationEntry entry = ((ExpandableNotificationRow) child).getEntry();
3215             entry.addOnSensitivityChangedListener(mOnChildSensitivityChangedListener);
3216             if (SceneContainerFlag.isEnabled()) {
3217                 row.setOnKeyguard(mIsOnLockscreen);
3218             }
3219         }
3220         generateAddAnimation(child, false /* fromMoreCard */);
3221         updateAnimationState(child);
3222         updateChronometerForChild(child);
3223         if (child instanceof ExpandableNotificationRow row) {
3224             row.setDismissUsingRowTranslationX(mDismissUsingRowTranslationX, /* force= */ true);
3225         }
3226     }
3227 
updateHideSensitiveForChild(ExpandableView child)3228     private void updateHideSensitiveForChild(ExpandableView child) {
3229         child.setHideSensitiveForIntrinsicHeight(mAmbientState.isHideSensitive());
3230     }
3231 
notifyGroupChildRemoved(ExpandableView row, ViewGroup childrenContainer)3232     public void notifyGroupChildRemoved(ExpandableView row, ViewGroup childrenContainer) {
3233         onViewRemovedInternal(row, childrenContainer);
3234     }
3235 
notifyGroupChildAdded(ExpandableView row)3236     public void notifyGroupChildAdded(ExpandableView row) {
3237         onViewAddedInternal(row);
3238     }
3239 
setAnimationsEnabled(boolean animationsEnabled)3240     public void setAnimationsEnabled(boolean animationsEnabled) {
3241         // TODO(b/332732878): remove the initial value of this field once the setter is called
3242         mAnimationsEnabled = animationsEnabled;
3243         updateNotificationAnimationStates();
3244         if (!animationsEnabled) {
3245             mSwipedOutViews.clear();
3246             mChildrenToRemoveAnimated.clear();
3247             clearTemporaryViewsInGroup(
3248                     /* viewGroup = */ this,
3249                     /* reason = */ "setAnimationsEnabled");
3250         }
3251     }
3252 
updateNotificationAnimationStates()3253     private void updateNotificationAnimationStates() {
3254         boolean running = mAnimationsEnabled || hasPulsingNotifications();
3255         mShelf.setAnimationsEnabled(running);
3256         int childCount = getChildCount();
3257         for (int i = 0; i < childCount; i++) {
3258             View child = getChildAt(i);
3259             running &= mIsExpanded || isPinnedHeadsUp(child);
3260             updateAnimationState(running, child);
3261         }
3262     }
3263 
updateAnimationState(View child)3264     void updateAnimationState(View child) {
3265         updateAnimationState((mAnimationsEnabled || hasPulsingNotifications())
3266                 && (mIsExpanded || isPinnedHeadsUp(child)), child);
3267     }
3268 
setExpandingNotification(ExpandableNotificationRow row)3269     void setExpandingNotification(ExpandableNotificationRow row) {
3270         if (mExpandingNotificationRow != null && row == null) {
3271             // Let's unset the clip path being set during launch
3272             mExpandingNotificationRow.setExpandingClipPath(null);
3273             ExpandableNotificationRow parent = mExpandingNotificationRow.getNotificationParent();
3274             if (parent != null) {
3275                 parent.setExpandingClipPath(null);
3276             }
3277         }
3278         mExpandingNotificationRow = row;
3279         updateLaunchedNotificationClipPath();
3280         requestChildrenUpdate();
3281     }
3282 
applyLaunchAnimationParams(LaunchAnimationParameters params)3283     public void applyLaunchAnimationParams(LaunchAnimationParameters params) {
3284         // Modify the clipping for launching notifications
3285         mLaunchAnimationParams = params;
3286         setLaunchingNotification(params != null);
3287         updateLaunchedNotificationClipPath();
3288         requestChildrenUpdate();
3289     }
3290 
updateAnimationState(boolean running, View child)3291     private void updateAnimationState(boolean running, View child) {
3292         if (child instanceof ExpandableNotificationRow row) {
3293             row.setAnimationRunning(running);
3294         }
3295     }
3296 
isAddOrRemoveAnimationPending()3297     boolean isAddOrRemoveAnimationPending() {
3298         return mNeedsAnimation
3299                 && (!mChildrenToAddAnimated.isEmpty() || !mChildrenToRemoveAnimated.isEmpty());
3300     }
3301 
generateAddAnimation(ExpandableView child, boolean fromMoreCard)3302     public void generateAddAnimation(ExpandableView child, boolean fromMoreCard) {
3303         if (mIsExpanded && mAnimationsEnabled && !mChangePositionInProgress && !isFullyHidden()) {
3304             // Generate Animations
3305             mChildrenToAddAnimated.add(child);
3306             if (fromMoreCard) {
3307                 mFromMoreCardAdditions.add(child);
3308             }
3309             mNeedsAnimation = true;
3310         }
3311         if (isHeadsUp(child) && mAnimationsEnabled && !mChangePositionInProgress
3312                 && !isFullyHidden()) {
3313             mAddedHeadsUpChildren.add(child);
3314             mChildrenToAddAnimated.remove(child);
3315         }
3316     }
3317 
changeViewPosition(ExpandableView child, int newIndex)3318     public void changeViewPosition(ExpandableView child, int newIndex) {
3319         Assert.isMainThread();
3320         if (mChangePositionInProgress) {
3321             throw new IllegalStateException("Reentrant call to changeViewPosition");
3322         }
3323 
3324         int currentIndex = indexOfChild(child);
3325 
3326         if (currentIndex == -1) {
3327             boolean isTransient = child instanceof ExpandableNotificationRow
3328                     && child.getTransientContainer() != null;
3329             Log.e(TAG, "Attempting to re-position "
3330                     + (isTransient ? "transient" : "")
3331                     + " view {"
3332                     + child
3333                     + "}");
3334             return;
3335         }
3336 
3337         if (child != null && child.getParent() == this && currentIndex != newIndex) {
3338             mChangePositionInProgress = true;
3339             child.setChangingPosition(true);
3340             removeView(child);
3341             addView(child, newIndex);
3342             child.setChangingPosition(false);
3343             mChangePositionInProgress = false;
3344             if (mIsExpanded && mAnimationsEnabled && child.getVisibility() != View.GONE) {
3345                 mChildrenChangingPositions.add(child);
3346                 mNeedsAnimation = true;
3347             }
3348         }
3349     }
3350 
startAnimationToState()3351     private void startAnimationToState() {
3352         if (mNeedsAnimation) {
3353             generateAllAnimationEvents();
3354             mNeedsAnimation = false;
3355         }
3356         if (!mAnimationEvents.isEmpty() || isCurrentlyAnimating()) {
3357             setAnimationRunning(true);
3358             mStateAnimator.startAnimationForEvents(mAnimationEvents, mGoToFullShadeDelay);
3359             mAnimationEvents.clear();
3360             updateViewShadows();
3361         } else {
3362             applyCurrentState();
3363         }
3364         mGoToFullShadeDelay = 0;
3365     }
3366 
generateAllAnimationEvents()3367     private void generateAllAnimationEvents() {
3368         generateHeadsUpAnimationEvents();
3369         generateChildRemovalEvents();
3370         generateChildAdditionEvents();
3371         generatePositionChangeEvents();
3372         generateTopPaddingEvent();
3373         generateActivateEvent();
3374         generateHideSensitiveEvent();
3375         generateGoToFullShadeEvent();
3376         generateViewResizeEvent();
3377         generateGroupExpansionEvent();
3378         generateAnimateEverythingEvent();
3379     }
3380 
generateHeadsUpAnimationEvents()3381     private void generateHeadsUpAnimationEvents() {
3382         for (HeadsUpAnimationEvent headsUpEvent : mHeadsUpChangeAnimations.values()) {
3383             ExpandableNotificationRow row = headsUpEvent.getRow();
3384             boolean isHeadsUp = headsUpEvent.isHeadsUpAppearance();
3385             if (isHeadsUp != row.isHeadsUp()) {
3386                 // For cases where we have a heads up showing and appearing again we shouldn't
3387                 // do the animations at all.
3388                 logHunSkippedForUnexpectedState(row, isHeadsUp, row.isHeadsUp());
3389                 continue;
3390             }
3391             int type = AnimationEvent.ANIMATION_TYPE_HEADS_UP_OTHER;
3392             boolean onBottom = false;
3393             boolean pinnedAndClosed = row.isPinned() && !mIsExpanded;
3394             boolean performDisappearAnimation = !mIsExpanded
3395                     // Only animate if we still have pinned heads up, otherwise we just have the
3396                     // regular collapse animation of the lock screen
3397                     || (mKeyguardBypassEnabled && onKeyguard()
3398                     && mInHeadsUpPinnedMode);
3399             if (performDisappearAnimation && !isHeadsUp) {
3400                 type = row.wasJustClicked()
3401                         ? AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK
3402                         : AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR;
3403                 if (NotificationHeadsUpCycling.isEnabled()) {
3404                     if (mStackScrollAlgorithm.isCyclingOut(row, mAmbientState)) {
3405                         type = AnimationEvent.ANIMATION_TYPE_HEADS_UP_CYCLING_OUT;
3406                     }
3407                 }
3408                 if (row.isChildInGroup()) {
3409                     // We can otherwise get stuck in there if it was just isolated
3410                     row.setHeadsUpAnimatingAway(false);
3411                     logHunAnimationSkipped(row, "row is child in group");
3412                     continue;
3413                 }
3414             } else {
3415                 ExpandableViewState viewState = row.getViewState();
3416                 if (viewState == null) {
3417                     // A view state was never generated for this view, so we don't need to animate
3418                     // this. This may happen with notification children.
3419                     logHunAnimationSkipped(row, "row has no viewState");
3420                     continue;
3421                 }
3422                 boolean shouldHunAppearFromTheBottom =
3423                         mStackScrollAlgorithm.shouldHunAppearFromBottom(mAmbientState, viewState);
3424                 if (isHeadsUp && (mAddedHeadsUpChildren.contains(row) || pinnedAndClosed)) {
3425                     if (pinnedAndClosed || shouldHunAppearFromTheBottom) {
3426                         // Our custom add animation
3427                         type = AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR;
3428                         if (NotificationHeadsUpCycling.isEnabled()) {
3429                             if (mStackScrollAlgorithm.isCyclingIn(row, mAmbientState)) {
3430                                 type = AnimationEvent.ANIMATION_TYPE_HEADS_UP_CYCLING_IN;
3431                             }
3432                         }
3433                     } else {
3434                         // Normal add animation
3435                         type = AnimationEvent.ANIMATION_TYPE_ADD;
3436                     }
3437                     onBottom = !pinnedAndClosed;
3438                 }
3439             }
3440             AnimationEvent event = new AnimationEvent(row, type);
3441             event.headsUpFromBottom = onBottom;
3442 
3443             boolean hasStatusBarChip =
3444                     StatusBarNotifChips.isEnabled() && headsUpEvent.getHasStatusBarChip();
3445             event.headsUpHasStatusBarChip = hasStatusBarChip;
3446             // TODO(b/283084712) remove this and update the HUN filters at creation
3447             event.filter.animateHeight = false;
3448             mAnimationEvents.add(event);
3449             if (SPEW) {
3450                 Log.v(TAG, "Generating HUN animation event: "
3451                         + " isHeadsUp=" + isHeadsUp
3452                         + " type=" + type
3453                         + " onBottom=" + onBottom
3454                         + " row=" + row.getKey());
3455             }
3456             logHunAnimationEventAdded(row, type);
3457         }
3458         mHeadsUpChangeAnimations.clear();
3459         mAddedHeadsUpChildren.clear();
3460     }
3461 
generateGroupExpansionEvent()3462     private void generateGroupExpansionEvent() {
3463         // Generate a group expansion/collapsing event if there is such a group at all
3464         if (mExpandedGroupView != null) {
3465             mAnimationEvents.add(new AnimationEvent(mExpandedGroupView,
3466                     AnimationEvent.ANIMATION_TYPE_GROUP_EXPANSION_CHANGED));
3467             mExpandedGroupView = null;
3468         }
3469     }
3470 
generateViewResizeEvent()3471     private void generateViewResizeEvent() {
3472         if (mNeedViewResizeAnimation) {
3473             boolean hasDisappearAnimation = false;
3474             for (AnimationEvent animationEvent : mAnimationEvents) {
3475                 final int type = animationEvent.animationType;
3476                 if (type == AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK
3477                         || type == AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR) {
3478                     hasDisappearAnimation = true;
3479                     break;
3480                 }
3481             }
3482 
3483             if (!hasDisappearAnimation) {
3484                 mAnimationEvents.add(
3485                         new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_VIEW_RESIZE));
3486             }
3487         }
3488         mNeedViewResizeAnimation = false;
3489     }
3490 
generateChildRemovalEvents()3491     private void generateChildRemovalEvents() {
3492         for (ExpandableView child : mChildrenToRemoveAnimated) {
3493             boolean childWasSwipedOut = mSwipedOutViews.contains(child);
3494 
3495             // we need to know the view after this one
3496             float removedTranslation = child.getTranslationY();
3497             boolean ignoreChildren = true;
3498             if (child instanceof ExpandableNotificationRow row) {
3499                 if (row.isRemoved() && row.wasChildInGroupWhenRemoved()) {
3500                     removedTranslation = row.getTranslationWhenRemoved();
3501                     ignoreChildren = false;
3502                 }
3503                 childWasSwipedOut |= isFullySwipedOut(row);
3504             } else if (child instanceof MediaContainerView) {
3505                 childWasSwipedOut = true;
3506             }
3507             if (!childWasSwipedOut) {
3508                 Rect clipBounds = child.getClipBounds();
3509                 childWasSwipedOut = clipBounds != null && clipBounds.height() == 0;
3510 
3511                 if (childWasSwipedOut) {
3512                     // Clean up any potential transient views if the child has already been swiped
3513                     // out, as we won't be animating it further (due to its height already being
3514                     // clipped to 0.
3515                     child.removeFromTransientContainer();
3516                 }
3517             }
3518             int animationType = childWasSwipedOut
3519                     ? AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT
3520                     : AnimationEvent.ANIMATION_TYPE_REMOVE;
3521             AnimationEvent event = new AnimationEvent(child, animationType);
3522             event.viewAfterChangingView = getFirstChildBelowTranlsationY(removedTranslation,
3523                     ignoreChildren);
3524             mAnimationEvents.add(event);
3525             mSwipedOutViews.remove(child);
3526             if (mDebugRemoveAnimation) {
3527                 String key = "";
3528                 if (child instanceof ExpandableNotificationRow) {
3529                     key = ((ExpandableNotificationRow) child).getKey();
3530                 }
3531                 Log.d(TAG, "created Remove Event - SwipedOut: " + childWasSwipedOut + " " + key);
3532             }
3533         }
3534         mChildrenToRemoveAnimated.clear();
3535     }
3536 
generatePositionChangeEvents()3537     private void generatePositionChangeEvents() {
3538         for (ExpandableView child : mChildrenChangingPositions) {
3539             Integer duration = null;
3540             if (child instanceof ExpandableNotificationRow row) {
3541                 if (row.getEntry().isMarkedForUserTriggeredMovement()) {
3542                     duration = StackStateAnimator.ANIMATION_DURATION_PRIORITY_CHANGE;
3543                     row.getEntry().markForUserTriggeredMovement(false);
3544                 }
3545             }
3546             AnimationEvent animEvent = duration == null
3547                     ? new AnimationEvent(child, AnimationEvent.ANIMATION_TYPE_CHANGE_POSITION)
3548                     : new AnimationEvent(
3549                     child, AnimationEvent.ANIMATION_TYPE_CHANGE_POSITION, duration);
3550             mAnimationEvents.add(animEvent);
3551         }
3552         mChildrenChangingPositions.clear();
3553     }
3554 
generateChildAdditionEvents()3555     private void generateChildAdditionEvents() {
3556         for (ExpandableView child : mChildrenToAddAnimated) {
3557             if (mFromMoreCardAdditions.contains(child)) {
3558                 mAnimationEvents.add(new AnimationEvent(child,
3559                         AnimationEvent.ANIMATION_TYPE_ADD,
3560                         StackStateAnimator.ANIMATION_DURATION_STANDARD));
3561             } else {
3562                 mAnimationEvents.add(new AnimationEvent(child,
3563                         AnimationEvent.ANIMATION_TYPE_ADD));
3564             }
3565         }
3566         mChildrenToAddAnimated.clear();
3567         mFromMoreCardAdditions.clear();
3568     }
3569 
generateTopPaddingEvent()3570     private void generateTopPaddingEvent() {
3571         if (mTopPaddingNeedsAnimation) {
3572             AnimationEvent event;
3573             if (mAmbientState.isDozing()) {
3574                 event = new AnimationEvent(null /* view */,
3575                         AnimationEvent.ANIMATION_TYPE_TOP_PADDING_CHANGED,
3576                         KeyguardSliceView.DEFAULT_ANIM_DURATION);
3577             } else {
3578                 event = new AnimationEvent(null /* view */,
3579                         AnimationEvent.ANIMATION_TYPE_TOP_PADDING_CHANGED);
3580             }
3581             mAnimationEvents.add(event);
3582         }
3583         mTopPaddingNeedsAnimation = false;
3584     }
3585 
generateActivateEvent()3586     private void generateActivateEvent() {
3587         if (mActivateNeedsAnimation) {
3588             mAnimationEvents.add(
3589                     new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_ACTIVATED_CHILD));
3590         }
3591         mActivateNeedsAnimation = false;
3592     }
3593 
generateAnimateEverythingEvent()3594     private void generateAnimateEverythingEvent() {
3595         if (mEverythingNeedsAnimation) {
3596             mAnimationEvents.add(
3597                     new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_EVERYTHING));
3598         }
3599         mEverythingNeedsAnimation = false;
3600     }
3601 
generateHideSensitiveEvent()3602     private void generateHideSensitiveEvent() {
3603         if (mHideSensitiveNeedsAnimation) {
3604             mAnimationEvents.add(
3605                     new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_HIDE_SENSITIVE));
3606         }
3607         mHideSensitiveNeedsAnimation = false;
3608     }
3609 
generateGoToFullShadeEvent()3610     private void generateGoToFullShadeEvent() {
3611         if (mGoToFullShadeNeedsAnimation) {
3612             mAnimationEvents.add(
3613                     new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_GO_TO_FULL_SHADE));
3614         }
3615         mGoToFullShadeNeedsAnimation = false;
3616     }
3617 
3618     /**
3619      * @return Whether a y coordinate is inside the content.
3620      */
isInContentBounds(float y)3621     public boolean isInContentBounds(float y) {
3622         if (SceneContainerFlag.isEnabled()) {
3623             return y < mAmbientState.getStackCutoff();
3624         } else {
3625             return y < getHeight() - getEmptyBottomMarginInternal();
3626         }
3627     }
3628 
getTouchSlop(MotionEvent event)3629     private float getTouchSlop(MotionEvent event) {
3630         // Adjust the touch slop if another gesture may be being performed.
3631         return event.getClassification() == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE
3632                 ? mTouchSlop * mSlopMultiplier
3633                 : mTouchSlop;
3634     }
3635 
3636     @Override
onTouchEvent(MotionEvent ev)3637     public boolean onTouchEvent(MotionEvent ev) {
3638         if (mTouchHandler != null) {
3639             boolean touchHandled = mTouchHandler.onTouchEvent(ev);
3640             if (SceneContainerFlag.isEnabled()) {
3641                 if (getChildAtPosition(
3642                         mInitialTouchX, mInitialTouchY, true, true, false) == null) {
3643                     // If scene container is enabled, any touch that we are handling that is not on
3644                     // a child view should be handled by scene container instead.
3645                     return false;
3646                 } else {
3647                     // If scene container is enabled, any touch that we are handling that is not on
3648                     // a child view should be handled by scene container instead.
3649                     return touchHandled;
3650                 }
3651             } else if (touchHandled) {
3652                 return true;
3653             }
3654         }
3655 
3656         return super.onTouchEvent(ev);
3657     }
3658 
3659     @Override
dispatchTouchEvent(MotionEvent ev)3660     public boolean dispatchTouchEvent(MotionEvent ev) {
3661         if (SceneContainerFlag.isEnabled()) {
3662             int action = ev.getActionMasked();
3663             boolean isTouchInGuts = mController.isTouchInGutsView(ev);
3664             if (action == MotionEvent.ACTION_DOWN && !isTouchInGuts) {
3665                 mController.closeControlsDueToOutsideTouch();
3666             }
3667             if (mIsBeingDragged) {
3668                 boolean isUpOrCancel = action == ACTION_UP || action == ACTION_CANCEL;
3669                 if (mSendingTouchesToSceneFramework) {
3670                     MotionEvent adjustedEvent = MotionEvent.obtain(ev);
3671                     adjustedEvent.setLocation(ev.getRawX(), ev.getRawY());
3672                     mScrollViewFields.sendCurrentGestureOverscroll(
3673                             getExpandedInThisMotion() && !isUpOrCancel);
3674                     mController.sendTouchToSceneFramework(adjustedEvent);
3675                     adjustedEvent.recycle();
3676                 } else if (!isUpOrCancel) {
3677                     // if this is the first touch being sent to the scene framework,
3678                     // convert it into a synthetic DOWN event.
3679                     mSendingTouchesToSceneFramework = true;
3680                     MotionEvent downEvent = MotionEvent.obtain(ev);
3681                     downEvent.setAction(MotionEvent.ACTION_DOWN);
3682                     downEvent.setLocation(ev.getRawX(), ev.getRawY());
3683                     mScrollViewFields.sendCurrentGestureInGuts(isTouchInGuts);
3684                     mScrollViewFields.sendCurrentGestureOverscroll(getExpandedInThisMotion());
3685                     mController.sendTouchToSceneFramework(downEvent);
3686                     downEvent.recycle();
3687                 }
3688 
3689                 if (isUpOrCancel) {
3690                     mScrollViewFields.sendCurrentGestureInGuts(false);
3691                     mScrollViewFields.sendCurrentGestureOverscroll(false);
3692                     setIsBeingDragged(false);
3693                     // dispatch to touchHandlers, so they can still finalize a previously started
3694                     // motion, while the shade is being dragged
3695                     return super.dispatchTouchEvent(ev);
3696                 }
3697                 return false;
3698             }
3699         }
3700         return TouchLogger.logDispatchTouch(TAG, ev, super.dispatchTouchEvent(ev));
3701     }
3702 
dispatchDownEventToScroller(MotionEvent ev)3703     void dispatchDownEventToScroller(MotionEvent ev) {
3704         MotionEvent downEvent = MotionEvent.obtain(ev);
3705         downEvent.setAction(MotionEvent.ACTION_DOWN);
3706         onScrollTouch(downEvent);
3707         downEvent.recycle();
3708     }
3709 
3710     // Only when scene container is enabled, mark that we are being dragged so that we start
3711     // dispatching the rest of the gesture to scene container.
startDraggingOnHun()3712     void startDraggingOnHun() {
3713         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
3714         setIsBeingDragged(true);
3715     }
3716 
3717     @Override
onGenericMotionEvent(MotionEvent event)3718     public boolean onGenericMotionEvent(MotionEvent event) {
3719         if (!isScrollingEnabled()
3720                 || !mIsExpanded
3721                 || mSwipeHelper.isSwiping()
3722                 || mExpandingNotification
3723                 || mDisallowScrollingInThisMotion) {
3724             return false;
3725         }
3726         if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
3727             switch (event.getAction()) {
3728                 case MotionEvent.ACTION_SCROLL: {
3729                     // If scene container is active, NSSL should not control its own scrolling.
3730                     if (SceneContainerFlag.isEnabled()) {
3731                         return false;
3732                     }
3733                     if (!mIsBeingDragged) {
3734                         final float vscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
3735                         if (vscroll != 0) {
3736                             final int delta = (int) (vscroll * getVerticalScrollFactor());
3737                             final int range = getScrollRange();
3738                             int oldScrollY = getOwnScrollY();
3739                             int newScrollY = oldScrollY - delta;
3740                             if (newScrollY < 0) {
3741                                 newScrollY = 0;
3742                             } else if (newScrollY > range) {
3743                                 newScrollY = range;
3744                             }
3745                             if (newScrollY != oldScrollY) {
3746                                 setOwnScrollY(newScrollY);
3747                                 return true;
3748                             }
3749                         }
3750                     }
3751                 }
3752             }
3753         }
3754         return super.onGenericMotionEvent(event);
3755     }
3756 
onScrollTouch(MotionEvent ev)3757     boolean onScrollTouch(MotionEvent ev) {
3758         if (!isScrollingEnabled()) {
3759             return false;
3760         }
3761         if (!isInScrollableRegion(ev) && !mIsBeingDragged) {
3762             return false;
3763         }
3764         mForcedScroll = null;
3765         initVelocityTrackerIfNotExists();
3766         mVelocityTracker.addMovement(ev);
3767 
3768         final int action = ev.getActionMasked();
3769         if (ev.findPointerIndex(mActivePointerId) == -1 && action != MotionEvent.ACTION_DOWN) {
3770             // Incomplete gesture, possibly due to window swap mid-gesture. Ignore until a new
3771             // one starts.
3772             Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent "
3773                     + MotionEvent.actionToString(ev.getActionMasked()));
3774             return true;
3775         }
3776 
3777         // If the scene framework is enabled, ignore all non-move gestures if we are currently
3778         // dragging - they should be dispatched to the scene framework. Move gestures should be let
3779         // through to determine if we are still dragging or not.
3780         if (
3781                 SceneContainerFlag.isEnabled()
3782                 && mIsBeingDragged
3783                 && action != MotionEvent.ACTION_MOVE
3784         ) {
3785             setIsBeingDragged(false);
3786             return false;
3787         }
3788 
3789         switch (action) {
3790             case MotionEvent.ACTION_DOWN: {
3791                 if (getChildCount() == 0 || !isInContentBounds(ev)) {
3792                     return false;
3793                 }
3794                 boolean isBeingDragged = !mScroller.isFinished();
3795                 setIsBeingDragged(isBeingDragged);
3796                 /*
3797                  * If being flinged and user touches, stop the fling. isFinished
3798                  * will be false if being flinged.
3799                  */
3800                 if (!mScroller.isFinished()) {
3801                     mScroller.forceFinished(true);
3802                 }
3803 
3804                 // Remember where the motion event started
3805                 mLastMotionY = (int) ev.getY();
3806                 mDownX = (int) ev.getX();
3807                 mActivePointerId = ev.getPointerId(0);
3808                 break;
3809             }
3810             case MotionEvent.ACTION_MOVE:
3811                 final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
3812                 if (activePointerIndex == -1) {
3813                     Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
3814                     break;
3815                 }
3816 
3817                 final int y = (int) ev.getY(activePointerIndex);
3818                 final int x = (int) ev.getX(activePointerIndex);
3819                 int deltaY = mLastMotionY - y;
3820                 final int xDiff = Math.abs(x - mDownX);
3821                 final int yDiff = Math.abs(deltaY);
3822                 final float touchSlop = getTouchSlop(ev);
3823                 if (!mIsBeingDragged && yDiff > touchSlop && yDiff > xDiff) {
3824                     setIsBeingDragged(true);
3825                     if (deltaY > 0) {
3826                         deltaY -= touchSlop;
3827                     } else {
3828                         deltaY += touchSlop;
3829                     }
3830                 }
3831                 if (mIsBeingDragged) {
3832                     // Defer actual scrolling to the scene framework if enabled
3833                     if (SceneContainerFlag.isEnabled()) {
3834                         return false;
3835                     }
3836                     // Scroll to follow the motion event
3837                     mLastMotionY = y;
3838                     float scrollAmount;
3839                     int range;
3840                     range = getScrollRange();
3841                     if (mExpandedInThisMotion) {
3842                         range = Math.min(range, mMaxScrollAfterExpand);
3843                     }
3844                     if (deltaY < 0) {
3845                         scrollAmount = overScrollDown(deltaY);
3846                     } else {
3847                         scrollAmount = overScrollUp(deltaY, range);
3848                     }
3849 
3850                     // Calling customOverScrollBy will call onCustomOverScrolled, which
3851                     // sets the scrolling if applicable.
3852                     if (scrollAmount != 0.0f) {
3853                         // The scrolling motion could not be compensated with the
3854                         // existing overScroll, we have to scroll the view
3855                         customOverScrollBy((int) scrollAmount, getOwnScrollY(),
3856                                 range, getHeight() / 2);
3857                     }
3858                 }
3859                 break;
3860             case ACTION_UP:
3861                 if (SceneContainerFlag.isEnabled() && mIsBeingDragged) {
3862                     mActivePointerId = INVALID_POINTER;
3863                     endDrag();
3864                 } else if (mIsBeingDragged) {
3865                     final VelocityTracker velocityTracker = mVelocityTracker;
3866                     velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
3867                     int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
3868 
3869                     if (shouldOverScrollFling(initialVelocity)) {
3870                         onOverScrollFling(true, initialVelocity);
3871                     } else {
3872                         if (getChildCount() > 0) {
3873                             if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
3874                                 float currentOverScrollTop = getCurrentOverScrollAmount(true);
3875                                 if (currentOverScrollTop == 0.0f || initialVelocity > 0) {
3876                                     mFlingAfterUpEvent = true;
3877                                     setFinishScrollingCallback(() -> {
3878                                         mFlingAfterUpEvent = false;
3879                                         InteractionJankMonitor.getInstance()
3880                                                 .end(CUJ_NOTIFICATION_SHADE_SCROLL_FLING);
3881                                         setFinishScrollingCallback(null);
3882                                     });
3883                                     fling(-initialVelocity);
3884                                 } else {
3885                                     onOverScrollFling(false, initialVelocity);
3886                                 }
3887                             } else {
3888                                 if (mScroller.springBack(mScrollX, getOwnScrollY(), 0, 0, 0,
3889                                         getScrollRange())) {
3890                                     animateScroll();
3891                                 }
3892                             }
3893                         }
3894                     }
3895                     mActivePointerId = INVALID_POINTER;
3896                     endDrag();
3897                 }
3898 
3899                 break;
3900             case ACTION_CANCEL:
3901                 if (mIsBeingDragged && getChildCount() > 0) {
3902                     if (mScroller.springBack(mScrollX, getOwnScrollY(), 0, 0, 0,
3903                             getScrollRange())) {
3904                         animateScroll();
3905                     }
3906                     mActivePointerId = INVALID_POINTER;
3907                     endDrag();
3908                 }
3909                 break;
3910             case MotionEvent.ACTION_POINTER_DOWN: {
3911                 final int index = ev.getActionIndex();
3912                 mLastMotionY = (int) ev.getY(index);
3913                 mDownX = (int) ev.getX(index);
3914                 mActivePointerId = ev.getPointerId(index);
3915                 break;
3916             }
3917             case MotionEvent.ACTION_POINTER_UP:
3918                 onSecondaryPointerUp(ev);
3919                 mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
3920                 mDownX = (int) ev.getX(ev.findPointerIndex(mActivePointerId));
3921                 break;
3922         }
3923         return true;
3924     }
3925 
isFlingAfterUpEvent()3926     boolean isFlingAfterUpEvent() {
3927         SceneContainerFlag.assertInLegacyMode();
3928         return mFlingAfterUpEvent;
3929     }
3930 
3931     /** Is this touch event inside the scrollable region? */
3932     @VisibleForTesting
isInScrollableRegion(MotionEvent ev)3933     boolean isInScrollableRegion(MotionEvent ev) {
3934         if (!SceneContainerFlag.isEnabled()) {
3935             return !isInsideQsHeader(ev);
3936         }
3937         ShadeScrimShape shape = mScrollViewFields.getClippingShape();
3938         if (shape == null) {
3939             return true; // When there is no scrim, consider this event scrollable.
3940         }
3941 
3942         ShadeScrimBounds bounds = shape.getBounds();
3943         return ev.getX() >= bounds.getLeft()
3944                 && ev.getX() <= bounds.getRight()
3945                 && ev.getY() >= bounds.getTop()
3946                 && ev.getY() <= bounds.getBottom();
3947     }
3948 
isInsideQsHeader(MotionEvent ev)3949     protected boolean isInsideQsHeader(MotionEvent ev) {
3950         SceneContainerFlag.assertInLegacyMode();
3951         if (QSComposeFragment.isEnabled()) {
3952             if (mQSHeaderBoundsProvider == null) {
3953                 return false;
3954             } else {
3955                 mQSHeaderBoundsProvider.getBoundsOnScreenProvider().invoke(mQsHeaderBound);
3956             }
3957         } else {
3958             if (mQsHeader == null) {
3959                 return false;
3960             } else {
3961                 mQsHeader.getBoundsOnScreen(mQsHeaderBound);
3962             }
3963         }
3964 
3965         /**
3966          * One-handed mode defines a feature FEATURE_ONE_HANDED of DisplayArea {@link DisplayArea}
3967          * that will translate down the Y-coordinate whole window screen type except for
3968          * TYPE_NAVIGATION_BAR and TYPE_NAVIGATION_BAR_PANEL .{@link DisplayAreaPolicy}.
3969          *
3970          * So, to consider triggered One-handed mode would translate down the absolute Y-coordinate
3971          * of DisplayArea into relative coordinates for all windows, we need to correct the
3972          * QS Head bounds here.
3973          */
3974         int left =
3975                 QSComposeFragment.isEnabled() ? mQSHeaderBoundsProvider.getLeftProvider().invoke()
3976                         : mQsHeader.getLeft();
3977         final int xOffset = Math.round(ev.getRawX() - ev.getX() + left);
3978         final int yOffset = Math.round(ev.getRawY() - ev.getY());
3979         mQsHeaderBound.offsetTo(xOffset, yOffset);
3980         return mQsHeaderBound.contains((int) ev.getRawX(), (int) ev.getRawY());
3981     }
3982 
onOverScrollFling(boolean open, int initialVelocity)3983     private void onOverScrollFling(boolean open, int initialVelocity) {
3984         if (mOverscrollTopChangedListener != null) {
3985             mOverscrollTopChangedListener.flingTopOverscroll(initialVelocity, open);
3986         }
3987         mDontReportNextOverScroll = true;
3988         setOverScrollAmount(0.0f, true, false);
3989     }
3990 
3991 
onSecondaryPointerUp(MotionEvent ev)3992     private void onSecondaryPointerUp(MotionEvent ev) {
3993         final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
3994                 MotionEvent.ACTION_POINTER_INDEX_SHIFT;
3995         final int pointerId = ev.getPointerId(pointerIndex);
3996         if (pointerId == mActivePointerId) {
3997             // This was our active pointer going up. Choose a new
3998             // active pointer and adjust accordingly.
3999             // TODO: Make this decision more intelligent.
4000             final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
4001             mLastMotionY = (int) ev.getY(newPointerIndex);
4002             mActivePointerId = ev.getPointerId(newPointerIndex);
4003             if (mVelocityTracker != null) {
4004                 mVelocityTracker.clear();
4005             }
4006         }
4007     }
4008 
endDrag()4009     private void endDrag() {
4010         setIsBeingDragged(false);
4011 
4012         recycleVelocityTracker();
4013 
4014         if (getCurrentOverScrollAmount(true /* onTop */) > 0) {
4015             setOverScrollAmount(0, true /* onTop */, true /* animate */);
4016         }
4017         if (getCurrentOverScrollAmount(false /* onTop */) > 0) {
4018             setOverScrollAmount(0, false /* onTop */, true /* animate */);
4019         }
4020     }
4021 
4022     @Override
onInterceptTouchEvent(MotionEvent ev)4023     public boolean onInterceptTouchEvent(MotionEvent ev) {
4024         if (mTouchHandler != null && mTouchHandler.onInterceptTouchEvent(ev)) {
4025             return true;
4026         }
4027         return super.onInterceptTouchEvent(ev);
4028     }
4029 
handleEmptySpaceClick(MotionEvent ev)4030     void handleEmptySpaceClick(MotionEvent ev) {
4031         if (SceneContainerFlag.isEnabled()) return;
4032         logEmptySpaceClick(ev, isBelowLastNotification(mInitialTouchX, mInitialTouchY),
4033                 mStatusBarState, mTouchIsClick);
4034         switch (ev.getActionMasked()) {
4035             case MotionEvent.ACTION_MOVE:
4036                 final float touchSlop = getTouchSlop(ev);
4037                 if (mTouchIsClick && (Math.abs(ev.getY() - mInitialTouchY) > touchSlop
4038                         || Math.abs(ev.getX() - mInitialTouchX) > touchSlop)) {
4039                     mTouchIsClick = false;
4040                 }
4041                 break;
4042             case ACTION_UP:
4043                 if (mStatusBarState != StatusBarState.KEYGUARD && mTouchIsClick &&
4044                         isBelowLastNotification(mInitialTouchX, mInitialTouchY)) {
4045                     debugShadeLog("handleEmptySpaceClick: touch event propagated further");
4046                     mOnEmptySpaceClickListener.onEmptySpaceClicked(mInitialTouchX, mInitialTouchY);
4047                 }
4048                 break;
4049             default:
4050                 debugShadeLog("handleEmptySpaceClick: MotionEvent ignored");
4051         }
4052     }
4053 
debugShadeLog(@ompileTimeConstant final String s)4054     private void debugShadeLog(@CompileTimeConstant final String s) {
4055         if (mLogger == null) {
4056             return;
4057         }
4058         mLogger.logShadeDebugEvent(s);
4059     }
4060 
logEmptySpaceClick(MotionEvent ev, boolean isTouchBelowLastNotification, int statusBarState, boolean touchIsClick)4061     private void logEmptySpaceClick(MotionEvent ev, boolean isTouchBelowLastNotification,
4062                                     int statusBarState, boolean touchIsClick) {
4063         if (mLogger == null) {
4064             return;
4065         }
4066         mLogger.logEmptySpaceClick(
4067                 isTouchBelowLastNotification,
4068                 statusBarState,
4069                 touchIsClick,
4070                 MotionEvent.actionToString(ev.getActionMasked()));
4071     }
4072 
initDownStates(MotionEvent ev)4073     void initDownStates(MotionEvent ev) {
4074         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
4075             mExpandedInThisMotion = false;
4076             mOnlyScrollingInThisMotion = !mScroller.isFinished();
4077             mDisallowScrollingInThisMotion = false;
4078             mDisallowDismissInThisMotion = false;
4079             mTouchIsClick = true;
4080             mInitialTouchX = ev.getX();
4081             mInitialTouchY = ev.getY();
4082         }
4083     }
4084 
4085     @Override
requestDisallowInterceptTouchEvent(boolean disallowIntercept)4086     public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
4087         super.requestDisallowInterceptTouchEvent(disallowIntercept);
4088         if (disallowIntercept) {
4089             cancelLongPress();
4090         }
4091     }
4092 
onInterceptTouchEventScroll(MotionEvent ev)4093     boolean onInterceptTouchEventScroll(MotionEvent ev) {
4094         if (!isScrollingEnabled()) {
4095             return false;
4096         }
4097         /*
4098          * This method JUST determines whether we want to intercept the motion.
4099          * If we return true, onMotionEvent will be called and we do the actual
4100          * scrolling there.
4101          */
4102 
4103         /*
4104          * Shortcut the most recurring case: the user is in the dragging
4105          * state and is moving their finger.  We want to intercept this
4106          * motion.
4107          */
4108         final int action = ev.getAction();
4109         if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
4110             return true;
4111         }
4112 
4113         switch (action & MotionEvent.ACTION_MASK) {
4114             case MotionEvent.ACTION_MOVE: {
4115                 /*
4116                  * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
4117                  * whether the user has moved far enough from the original down touch.
4118                  */
4119 
4120                 /*
4121                  * Locally do absolute value. mLastMotionY is set to the y value
4122                  * of the down event.
4123                  */
4124                 final int activePointerId = mActivePointerId;
4125                 if (activePointerId == INVALID_POINTER) {
4126                     // If we don't have a valid id, the touch down wasn't on content.
4127                     break;
4128                 }
4129 
4130                 final int pointerIndex = ev.findPointerIndex(activePointerId);
4131                 if (pointerIndex == -1) {
4132                     Log.e(TAG, "Invalid pointerId=" + activePointerId
4133                             + " in onInterceptTouchEvent");
4134                     break;
4135                 }
4136 
4137                 final int y = (int) ev.getY(pointerIndex);
4138                 final int x = (int) ev.getX(pointerIndex);
4139                 final int yDiff = Math.abs(y - mLastMotionY);
4140                 final int xDiff = Math.abs(x - mDownX);
4141                 if (yDiff > getTouchSlop(ev) && yDiff > xDiff) {
4142                     setIsBeingDragged(true);
4143                     mLastMotionY = y;
4144                     mDownX = x;
4145                     initVelocityTrackerIfNotExists();
4146                     mVelocityTracker.addMovement(ev);
4147                 }
4148                 break;
4149             }
4150 
4151             case MotionEvent.ACTION_DOWN: {
4152                 final int y = (int) ev.getY();
4153                 mScrolledToTopOnFirstDown = mScrollAdapter.isScrolledToTop();
4154                 final ExpandableView childAtTouchPos = getChildAtPosition(
4155                         ev.getX(), y, false /* requireMinHeight */,
4156                         false /* ignoreDecors */, true /* ignoreWidth */);
4157                 if (childAtTouchPos == null) {
4158                     setIsBeingDragged(false);
4159                     recycleVelocityTracker();
4160                     break;
4161                 }
4162 
4163                 /*
4164                  * Remember location of down touch.
4165                  * ACTION_DOWN always refers to pointer index 0.
4166                  */
4167                 mLastMotionY = y;
4168                 mDownX = (int) ev.getX();
4169                 mActivePointerId = ev.getPointerId(0);
4170 
4171                 initOrResetVelocityTracker();
4172                 mVelocityTracker.addMovement(ev);
4173                 /*
4174                  * If being flinged and user touches the screen, initiate drag;
4175                  * otherwise don't.  mScroller.isFinished should be false when
4176                  * being flinged.
4177                  */
4178                 boolean isBeingDragged = !mScroller.isFinished();
4179                 setIsBeingDragged(isBeingDragged);
4180                 break;
4181             }
4182 
4183             case ACTION_CANCEL:
4184             case ACTION_UP:
4185                 /* Release the drag */
4186                 setIsBeingDragged(false);
4187                 mActivePointerId = INVALID_POINTER;
4188                 recycleVelocityTracker();
4189                 if (mScroller.springBack(mScrollX, getOwnScrollY(), 0, 0, 0, getScrollRange())) {
4190                     animateScroll();
4191                 }
4192                 break;
4193             case MotionEvent.ACTION_POINTER_UP:
4194                 onSecondaryPointerUp(ev);
4195                 break;
4196         }
4197 
4198         /*
4199          * The only time we want to intercept motion events is if we are in the
4200          * drag mode.
4201          */
4202         return mIsBeingDragged;
4203     }
4204 
4205     /**
4206      * @return Whether the specified motion event is actually happening over the content.
4207      */
isInContentBounds(MotionEvent event)4208     private boolean isInContentBounds(MotionEvent event) {
4209         return isInContentBounds(event.getY());
4210     }
4211 
4212 
4213     @VisibleForTesting
setIsBeingDragged(boolean isDragged)4214     void setIsBeingDragged(boolean isDragged) {
4215         mIsBeingDragged = isDragged;
4216         if (isDragged) {
4217             requestDisallowInterceptTouchEvent(true);
4218             cancelLongPress();
4219             resetExposedMenuView(true /* animate */, true /* force */);
4220         } else {
4221             mSendingTouchesToSceneFramework = false;
4222         }
4223     }
4224 
4225     @VisibleForTesting
getIsBeingDragged()4226     boolean getIsBeingDragged() {
4227         return mIsBeingDragged;
4228     }
4229 
requestDisallowLongPress()4230     public void requestDisallowLongPress() {
4231         cancelLongPress();
4232     }
4233 
requestDisallowDismiss()4234     public void requestDisallowDismiss() {
4235         mDisallowDismissInThisMotion = true;
4236     }
4237 
cancelLongPress()4238     public void cancelLongPress() {
4239         mSwipeHelper.cancelLongPress();
4240     }
4241 
setOnEmptySpaceClickListener(OnEmptySpaceClickListener listener)4242     public void setOnEmptySpaceClickListener(OnEmptySpaceClickListener listener) {
4243         SceneContainerFlag.assertInLegacyMode();
4244         mOnEmptySpaceClickListener = listener;
4245     }
4246 
4247     /**
4248      * @hide
4249      */
4250     @Override
performAccessibilityActionInternal(int action, Bundle arguments)4251     public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
4252         if (super.performAccessibilityActionInternal(action, arguments)) {
4253             return true;
4254         }
4255         if (!isEnabled()) {
4256             return false;
4257         }
4258 
4259         if (SceneContainerFlag.isEnabled()) {
4260             switch (action) {
4261                 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
4262                 case android.R.id.accessibilityActionScrollDown:
4263                     mScrollViewFields.sendAccessibilityScrollEvent(SCROLL_DOWN);
4264                     return true;
4265                 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
4266                 case android.R.id.accessibilityActionScrollUp:
4267                     mScrollViewFields.sendAccessibilityScrollEvent(SCROLL_UP);
4268                     return true;
4269             }
4270             return false;
4271         }
4272 
4273         int direction = -1;
4274         switch (action) {
4275             case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
4276                 // fall through
4277             case android.R.id.accessibilityActionScrollDown:
4278                 direction = 1;
4279                 // fall through
4280             case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
4281                 // fall through
4282             case android.R.id.accessibilityActionScrollUp:
4283                 final int viewportHeight =
4284                         getHeight() - mPaddingBottom - getTopPadding() - mPaddingTop
4285                                 - mShelf.getIntrinsicHeight();
4286                 final int targetScrollY = Math.max(0,
4287                         Math.min(getOwnScrollY() + direction * viewportHeight, getScrollRange()));
4288                 if (targetScrollY != getOwnScrollY()) {
4289                     mScroller.startScroll(mScrollX, getOwnScrollY(), 0,
4290                             targetScrollY - getOwnScrollY());
4291                     animateScroll();
4292                     return true;
4293                 }
4294                 break;
4295         }
4296         return false;
4297     }
4298 
4299     @Override
onWindowFocusChanged(boolean hasWindowFocus)4300     public void onWindowFocusChanged(boolean hasWindowFocus) {
4301         super.onWindowFocusChanged(hasWindowFocus);
4302         if (!hasWindowFocus) {
4303             cancelLongPress();
4304         }
4305     }
4306 
4307     @Override
clearChildFocus(View child)4308     public void clearChildFocus(View child) {
4309         super.clearChildFocus(child);
4310         if (mForcedScroll == child) {
4311             mForcedScroll = null;
4312         }
4313     }
4314 
isScrolledToBottom()4315     boolean isScrolledToBottom() {
4316         SceneContainerFlag.assertInLegacyMode();
4317         return mScrollAdapter.isScrolledToBottom();
4318     }
4319 
getEmptyBottomMargin()4320     int getEmptyBottomMargin() {
4321         SceneContainerFlag.assertInLegacyMode();
4322         return getEmptyBottomMarginInternal();
4323     }
4324 
getEmptyBottomMarginInternal()4325     private int getEmptyBottomMarginInternal() {
4326         int contentHeight;
4327         if (mShouldUseSplitNotificationShade) {
4328             // When in split shade and there are no notifications, the height can be too low, as
4329             // it is based on notifications bottom, which is lower on split shade.
4330             // Here we prefer to use at least a minimum height defined for split shade.
4331             // Otherwise the expansion motion is too fast.
4332             contentHeight = Math.max(mSplitShadeMinContentHeight, getContentHeight());
4333         } else {
4334             contentHeight = getContentHeight();
4335         }
4336         return Math.max(mMaxLayoutHeight - contentHeight, 0);
4337     }
4338 
onExpansionStarted()4339     void onExpansionStarted() {
4340         mIsExpansionChanging = true;
4341         mAmbientState.setExpansionChanging(true);
4342     }
4343 
onExpansionStopped()4344     void onExpansionStopped() {
4345         mIsExpansionChanging = false;
4346         mAmbientState.setExpansionChanging(false);
4347         if (!mIsExpanded) {
4348             resetScrollPosition();
4349             mResetUserExpandedStatesRunnable.run();
4350             clearTemporaryViews();
4351             clearUserLockedViews();
4352             resetAllSwipeState();
4353         }
4354     }
4355 
clearUserLockedViews()4356     private void clearUserLockedViews() {
4357         for (int i = 0; i < getChildCount(); i++) {
4358             ExpandableView child = getChildAtIndex(i);
4359             if (child instanceof ExpandableNotificationRow row) {
4360                 row.setUserLocked(false);
4361             }
4362         }
4363     }
4364 
clearTemporaryViews()4365     private void clearTemporaryViews() {
4366         // lets make sure nothing is transient anymore
4367         clearTemporaryViewsInGroup(
4368                 /* viewGroup = */ this,
4369                 /* reason = */ "clearTemporaryViews"
4370         );
4371         for (int i = 0; i < getChildCount(); i++) {
4372             ExpandableView child = getChildAtIndex(i);
4373             if (child instanceof ExpandableNotificationRow row) {
4374                 clearTemporaryViewsInGroup(
4375                         /* viewGroup = */ row.getChildrenContainer(),
4376                         /* reason = */ "clearTemporaryViewsInGroup(row.getChildrenContainer())"
4377                 );
4378             }
4379         }
4380     }
4381 
clearTemporaryViewsInGroup(ViewGroup viewGroup, String reason)4382     private void clearTemporaryViewsInGroup(ViewGroup viewGroup, String reason) {
4383         while (viewGroup != null && viewGroup.getTransientViewCount() != 0) {
4384             final View transientView = viewGroup.getTransientView(0);
4385             viewGroup.removeTransientView(transientView);
4386             if (transientView instanceof ExpandableView) {
4387                 ((ExpandableView) transientView).setTransientContainer(null);
4388                 if (transientView instanceof ExpandableNotificationRow) {
4389                     logTransientNotificationRowTraversalCleaned(
4390                             (ExpandableNotificationRow) transientView,
4391                             reason
4392                     );
4393                 }
4394             }
4395         }
4396     }
4397 
resetChildAlpha()4398     private void resetChildAlpha() {
4399         for (int i = 0; i < getChildCount(); i++) {
4400             ExpandableView child = getChildAtIndex(i);
4401             if (child instanceof ExpandableNotificationRow row) {
4402                 if (row.isExpandAnimationRunning()) continue;
4403                 row.resetAllContentAlphas();
4404             }
4405         }
4406     }
4407 
logTransientNotificationRowTraversalCleaned( ExpandableNotificationRow transientView, String reason )4408     private void logTransientNotificationRowTraversalCleaned(
4409             ExpandableNotificationRow transientView,
4410             String reason
4411     ) {
4412         if (mLogger == null) {
4413             return;
4414         }
4415         mLogger.transientNotificationRowTraversalCleaned(transientView.getLoggingKey(), reason);
4416     }
4417 
onPanelTrackingStarted()4418     void onPanelTrackingStarted() {
4419         mPanelTracking = true;
4420         mAmbientState.setPanelTracking(true);
4421         resetExposedMenuView(true /* animate */, true /* force */);
4422     }
4423 
onPanelTrackingStopped()4424     void onPanelTrackingStopped() {
4425         mPanelTracking = false;
4426         mAmbientState.setPanelTracking(false);
4427     }
4428 
resetScrollPosition()4429     void resetScrollPosition() {
4430         mScroller.abortAnimation();
4431         setOwnScrollY(0);
4432     }
4433 
4434     @VisibleForTesting
setIsExpanded(boolean isExpanded)4435     void setIsExpanded(boolean isExpanded) {
4436         boolean changed = isExpanded != mIsExpanded;
4437         mIsExpanded = isExpanded;
4438         mStackScrollAlgorithm.setIsExpanded(isExpanded);
4439         mAmbientState.setShadeExpanded(isExpanded);
4440         mStateAnimator.setShadeExpanded(isExpanded);
4441         mSwipeHelper.setIsExpanded(isExpanded);
4442         if (changed) {
4443             mWillExpand = false;
4444             if (mIsExpanded) {
4445                 // Resetting headsUpAnimatingAway on Shade expansion avoids delays caused by
4446                 // waiting for all child animations to finish.
4447                 // TODO(b/328390331) Do we need to reset this on QS expanded as well?
4448                 if (SceneContainerFlag.isEnabled()) {
4449                     setHeadsUpAnimatingAway(false);
4450                 }
4451                 if (NotificationContentAlphaOptimization.isEnabled()) {
4452                     resetChildAlpha();
4453                 }
4454             } else {
4455                 mGroupExpansionManager.collapseGroups();
4456                 mExpandHelper.cancelImmediately();
4457                 if (!mIsExpansionChanging) {
4458                     resetAllSwipeState();
4459                 }
4460                 finalizeClearAllAnimation();
4461             }
4462             updateNotificationAnimationStates();
4463             updateChronometers();
4464             requestChildrenUpdate();
4465             updateUseRoundedRectClipping();
4466             updateDismissBehavior();
4467         }
4468     }
4469 
updateChronometers()4470     private void updateChronometers() {
4471         int childCount = getChildCount();
4472         for (int i = 0; i < childCount; i++) {
4473             updateChronometerForChild(getChildAt(i));
4474         }
4475     }
4476 
updateChronometerForChild(View child)4477     void updateChronometerForChild(View child) {
4478         if (child instanceof ExpandableNotificationRow row) {
4479             row.setChronometerRunning(mIsExpanded);
4480         }
4481     }
4482 
onChildHeightChanged(ExpandableView view, boolean needsAnimation)4483     void onChildHeightChanged(ExpandableView view, boolean needsAnimation) {
4484         boolean previouslyNeededAnimation = mAnimateStackYForContentHeightChange;
4485         if (needsAnimation) {
4486             mAnimateStackYForContentHeightChange = true;
4487         }
4488         updateContentHeight();
4489         updateScrollPositionOnExpandInBottom(view);
4490         clampScrollPosition();
4491         notifyHeightChangeListener(view, needsAnimation);
4492         ExpandableNotificationRow row = view instanceof ExpandableNotificationRow
4493                 ? (ExpandableNotificationRow) view
4494                 : null;
4495         NotificationSection firstSection = getFirstVisibleSection();
4496         ExpandableView firstVisibleChild =
4497                 firstSection == null ? null : firstSection.getFirstVisibleChild();
4498         if (row != null) {
4499             if (row == firstVisibleChild
4500                     || row.getNotificationParent() == firstVisibleChild) {
4501                 updateAlgorithmLayoutMinHeight();
4502             }
4503         }
4504         if (needsAnimation) {
4505             requestAnimationOnViewResize(row);
4506         }
4507         requestChildrenUpdate();
4508         notifyHeadsUpHeightChangedForView(view);
4509         mAnimateStackYForContentHeightChange = previouslyNeededAnimation;
4510     }
4511 
onChildHeightReset(ExpandableView view)4512     void onChildHeightReset(ExpandableView view) {
4513         updateAnimationState(view);
4514         updateChronometerForChild(view);
4515         notifyHeadsUpHeightChangedForView(view);
4516     }
4517 
updateScrollPositionOnExpandInBottom(ExpandableView view)4518     private void updateScrollPositionOnExpandInBottom(ExpandableView view) {
4519         if (view instanceof ExpandableNotificationRow row && !onKeyguard()) {
4520             // TODO: once we're recycling this will need to check the adapter position of the child
4521             if (row.isUserLocked() && row != getFirstChildNotGoneInternal()) {
4522                 if (row.isSummaryWithChildren()) {
4523                     return;
4524                 }
4525                 // We are actually expanding this view
4526                 float endPosition = row.getTranslationY() + row.getActualHeight();
4527                 if (row.isChildInGroup()) {
4528                     endPosition += row.getNotificationParent().getTranslationY();
4529                 }
4530                 int layoutEnd = mMaxLayoutHeight + (int) getStackTranslation();
4531                 NotificationSection lastSection = getLastVisibleSection();
4532                 ExpandableView lastVisibleChild =
4533                         lastSection == null ? null : lastSection.getLastVisibleChild();
4534                 if (row != lastVisibleChild && mShelf.getVisibility() != GONE) {
4535                     layoutEnd -= mShelf.getIntrinsicHeight() + mPaddingBetweenElements;
4536                 }
4537                 if (endPosition > layoutEnd) {
4538                     // if Scene Container is active, send bottom notification expansion delta
4539                     // to it so that it can scroll the stack and scrim accordingly.
4540                     if (SceneContainerFlag.isEnabled()) {
4541                         float diff = endPosition - layoutEnd;
4542                         mScrollViewFields.sendSyntheticScroll(diff);
4543                     }
4544                     setOwnScrollY((int) (getOwnScrollY() + endPosition - layoutEnd));
4545                     mDisallowScrollingInThisMotion = true;
4546                 }
4547             }
4548         }
4549     }
4550 
setOnHeightChangedListener( ExpandableView.OnHeightChangedListener onHeightChangedListener)4551     void setOnHeightChangedListener(
4552             ExpandableView.OnHeightChangedListener onHeightChangedListener) {
4553         this.mOnHeightChangedListener = onHeightChangedListener;
4554     }
4555 
setOnHeightChangedRunnable(Runnable r)4556     void setOnHeightChangedRunnable(Runnable r) {
4557         this.mOnHeightChangedRunnable = r;
4558     }
4559 
onChildAnimationFinished()4560     void onChildAnimationFinished() {
4561         setAnimationRunning(false);
4562         if (SceneContainerFlag.isEnabled()) {
4563             setHeadsUpAnimatingAway(false);
4564         }
4565         requestChildrenUpdate();
4566         runAnimationFinishedRunnables();
4567         clearTransient();
4568         clearHeadsUpDisappearRunning();
4569         finalizeClearAllAnimation();
4570     }
4571 
finalizeClearAllAnimation()4572     private void finalizeClearAllAnimation() {
4573         if (mAmbientState.isClearAllInProgress()) {
4574             setClearAllInProgress(false);
4575             if (mShadeNeedsToClose) {
4576                 mShadeNeedsToClose = false;
4577                 if (mIsExpanded) {
4578                     mClearAllFinishedWhilePanelExpandedRunnable.run();
4579                 }
4580             }
4581         }
4582     }
4583 
clearHeadsUpDisappearRunning()4584     private void clearHeadsUpDisappearRunning() {
4585         for (int i = 0; i < getChildCount(); i++) {
4586             View view = getChildAt(i);
4587             if (view instanceof ExpandableNotificationRow row) {
4588                 row.setHeadsUpAnimatingAway(false);
4589                 if (row.isSummaryWithChildren()) {
4590                     for (ExpandableNotificationRow child : row.getAttachedChildren()) {
4591                         child.setHeadsUpAnimatingAway(false);
4592                     }
4593                 }
4594             }
4595         }
4596     }
4597 
clearTransient()4598     private void clearTransient() {
4599         for (ExpandableView view : mClearTransientViewsWhenFinished) {
4600             view.removeFromTransientContainer();
4601         }
4602         mClearTransientViewsWhenFinished.clear();
4603     }
4604 
runAnimationFinishedRunnables()4605     private void runAnimationFinishedRunnables() {
4606         for (Runnable runnable : mAnimationFinishedRunnables) {
4607             runnable.run();
4608         }
4609         mAnimationFinishedRunnables.clear();
4610     }
4611 
updateSensitiveness(boolean animate, boolean hideSensitive)4612     void updateSensitiveness(boolean animate, boolean hideSensitive) {
4613         if (hideSensitive != mAmbientState.isHideSensitive()) {
4614             int childCount = getChildCount();
4615             for (int i = 0; i < childCount; i++) {
4616                 ExpandableView v = getChildAtIndex(i);
4617                 v.setHideSensitiveForIntrinsicHeight(hideSensitive);
4618             }
4619             mAmbientState.setHideSensitive(hideSensitive);
4620             if (animate && mAnimationsEnabled) {
4621                 mHideSensitiveNeedsAnimation = true;
4622                 mNeedsAnimation = true;
4623             }
4624             updateContentHeight();
4625             requestChildrenUpdate();
4626         }
4627     }
4628 
applyCurrentState()4629     private void applyCurrentState() {
4630         int numChildren = getChildCount();
4631         for (int i = 0; i < numChildren; i++) {
4632             ExpandableView child = getChildAtIndex(i);
4633             child.applyViewState();
4634         }
4635 
4636         if (NotificationsLiveDataStoreRefactor.isEnabled()) {
4637             if (mLocationsChangedListener != null) {
4638                 mLocationsChangedListener.onChildLocationsChanged(collectVisibleLocationsCallable);
4639             }
4640         } else {
4641             if (mLegacyLocationsChangedListener != null) {
4642                 mLegacyLocationsChangedListener.onChildLocationsChanged();
4643             }
4644         }
4645 
4646         runAnimationFinishedRunnables();
4647         setAnimationRunning(false);
4648         updateViewShadows();
4649     }
4650 
4651     /**
4652      * Retrieves a map of visible [{@link ExpandableViewState#location}]s of the actively displayed
4653      * Notification children associated by their Notification keys.
4654      * Locations are collected recursively including locations from the child views of Notification
4655      * Groups, that are visible.
4656      */
collectVisibleNotificationLocations()4657     private Map<String, Integer> collectVisibleNotificationLocations() {
4658         Map<String, Integer> visibilities = new HashMap<>();
4659         int numChildren = getChildCount();
4660         for (int i = 0; i < numChildren; i++) {
4661             ExpandableView child = getChildAtIndex(i);
4662             if (child instanceof ExpandableNotificationRow row) {
4663                 row.collectVisibleLocations(visibilities);
4664             }
4665         }
4666         return visibilities;
4667     }
4668 
updateViewShadows()4669     private void updateViewShadows() {
4670         // we need to work around an issue where the shadow would not cast between siblings when
4671         // their z difference is between 0 and 0.1
4672 
4673         // Lefts first sort by Z difference
4674         for (int i = 0; i < getChildCount(); i++) {
4675             ExpandableView child = getChildAtIndex(i);
4676             if (child.getVisibility() != GONE) {
4677                 mTmpSortedChildren.add(child);
4678             }
4679         }
4680         Collections.sort(mTmpSortedChildren, mViewPositionComparator);
4681 
4682         // Now lets update the shadow for the views
4683         ExpandableView previous = null;
4684         for (int i = 0; i < mTmpSortedChildren.size(); i++) {
4685             ExpandableView expandableView = mTmpSortedChildren.get(i);
4686             float translationZ = expandableView.getTranslationZ();
4687             float otherZ = previous == null ? translationZ : previous.getTranslationZ();
4688             float diff = otherZ - translationZ;
4689             if (diff <= 0.0f || diff >= FakeShadowView.SHADOW_SIBLING_TRESHOLD) {
4690                 // There is no fake shadow to be drawn
4691                 expandableView.setFakeShadowIntensity(0.0f, 0.0f, 0, 0);
4692             } else {
4693                 float yLocation = previous.getTranslationY() + previous.getActualHeight() -
4694                         expandableView.getTranslationY();
4695                 expandableView.setFakeShadowIntensity(
4696                         diff / FakeShadowView.SHADOW_SIBLING_TRESHOLD,
4697                         previous.getOutlineAlpha(), (int) yLocation,
4698                         (int) (previous.getOutlineTranslation() + previous.getTranslation()));
4699             }
4700             previous = expandableView;
4701         }
4702 
4703         mTmpSortedChildren.clear();
4704     }
4705 
4706     /**
4707      * Update colors of section headers, shade footer, and empty shade views.
4708      */
updateDecorViews()4709     void updateDecorViews() {
4710         final @ColorInt int onSurface = mContext.getColor(
4711                 com.android.internal.R.color.materialColorOnSurface);
4712         final @ColorInt int onSurfaceVariant = mContext.getColor(
4713                 com.android.internal.R.color.materialColorOnSurfaceVariant);
4714 
4715         ColorUpdateLogger colorUpdateLogger = ColorUpdateLogger.getInstance();
4716         if (colorUpdateLogger != null) {
4717             colorUpdateLogger.logEvent("NSSL.updateDecorViews()",
4718                     "onSurface=" + ColorUtilKt.hexColorString(onSurface)
4719                             + " onSurfaceVariant=" + ColorUtilKt.hexColorString(onSurfaceVariant));
4720         }
4721 
4722         mSectionsManager.setHeaderForegroundColors(onSurface, onSurfaceVariant);
4723 
4724         if (mFooterView != null) {
4725             mFooterView.updateColors();
4726         }
4727 
4728         if (mEmptyShadeView != null) {
4729             mEmptyShadeView.setTextColors(onSurface, onSurfaceVariant);
4730         }
4731     }
4732 
4733     /**
4734      * Requests an animation for the next stack height update, to animate from the constrained stack
4735      * displayed on the lock screen, to the scrollable stack displayed in the expanded shade.
4736      */
animateGoToFullShade(long delay)4737     public void animateGoToFullShade(long delay) {
4738         mGoToFullShadeNeedsAnimation = true;
4739         mGoToFullShadeDelay = delay;
4740         mNeedsAnimation = true;
4741         requestChildrenUpdate();
4742     }
4743 
cancelExpandHelper()4744     public void cancelExpandHelper() {
4745         mExpandHelper.cancel();
4746     }
4747 
setIntrinsicPadding(int intrinsicPadding)4748     void setIntrinsicPadding(int intrinsicPadding) {
4749         SceneContainerFlag.assertInLegacyMode();
4750         mIntrinsicPadding = intrinsicPadding;
4751     }
4752 
4753     /**
4754      * Distance from the top of the screen in, where notifications should start when fully expanded
4755      * or in the LS.
4756      *
4757      * Always 0 with SceneContainer enabled.
4758      */
getIntrinsicPadding()4759     int getIntrinsicPadding() {
4760         if (SceneContainerFlag.isEnabled()) {
4761             return 0;
4762         }
4763         return mIntrinsicPadding;
4764     }
4765 
4766     @Override
shouldDelayChildPressedState()4767     public boolean shouldDelayChildPressedState() {
4768         return true;
4769     }
4770 
4771     /**
4772      * See {@link AmbientState#setDozing}.
4773      */
4774     @Override
setDozing(boolean dozing)4775     public void setDozing(boolean dozing) {
4776         if (mAmbientState.isDozing() == dozing) {
4777             return;
4778         }
4779         mAmbientState.setDozing(dozing);
4780         requestChildrenUpdate();
4781         notifyHeightChangeListener(mShelf);
4782     }
4783 
4784     /**
4785      * Sets the current hide amount.
4786      *
4787      * @param linearHideAmount       The hide amount that follows linear interpoloation in the
4788      *                               animation,
4789      *                               i.e. animates from 0 to 1 or vice-versa in a linear manner.
4790      * @param interpolatedHideAmount The hide amount that follows the actual interpolation of the
4791      *                               animation curve.
4792      */
setHideAmount(float linearHideAmount, float interpolatedHideAmount)4793     void setHideAmount(float linearHideAmount, float interpolatedHideAmount) {
4794         mLinearHideAmount = linearHideAmount;
4795         mInterpolatedHideAmount = interpolatedHideAmount;
4796         boolean wasFullyHidden = mAmbientState.isFullyHidden();
4797         boolean wasHiddenAtAll = mAmbientState.isHiddenAtAll();
4798         mAmbientState.setHideAmount(interpolatedHideAmount);
4799         boolean nowFullyHidden = mAmbientState.isFullyHidden();
4800         boolean nowHiddenAtAll = mAmbientState.isHiddenAtAll();
4801         if (nowFullyHidden != wasFullyHidden) {
4802             updateVisibility();
4803             resetAllSwipeState();
4804         }
4805         if (!wasHiddenAtAll && nowHiddenAtAll) {
4806             resetExposedMenuView(true /* animate */, true /* animate */);
4807         }
4808         if (nowFullyHidden != wasFullyHidden || wasHiddenAtAll != nowHiddenAtAll) {
4809             invalidateOutline();
4810         }
4811         updateAlgorithmHeightAndPadding();
4812         requestChildrenUpdate();
4813         updateOwnTranslationZ();
4814     }
4815 
updateOwnTranslationZ()4816     private void updateOwnTranslationZ() {
4817         // Since we are clipping to the outline we need to make sure that the shadows aren't
4818         // clipped when pulsing
4819         float ownTranslationZ = 0;
4820         if (mKeyguardBypassEnabled && mAmbientState.isHiddenAtAll()) {
4821             ExpandableView firstChildNotGone = getFirstChildNotGoneInternal();
4822             if (firstChildNotGone != null && firstChildNotGone.showingPulsing()) {
4823                 ownTranslationZ = firstChildNotGone.getTranslationZ();
4824             }
4825         }
4826         setTranslationZ(ownTranslationZ);
4827     }
4828 
updateVisibility()4829     private void updateVisibility() {
4830         mController.updateVisibility(!mAmbientState.isFullyHidden() || !onKeyguard());
4831     }
4832 
notifyHideAnimationStart(boolean hide)4833     void notifyHideAnimationStart(boolean hide) {
4834         // We only swap the scaling factor if we're fully hidden or fully awake to avoid
4835         // interpolation issues when playing with the power button.
4836         if (mInterpolatedHideAmount == 0 || mInterpolatedHideAmount == 1) {
4837             mBackgroundXFactor = hide ? 1.8f : 1.5f;
4838             mHideXInterpolator = hide
4839                     ? Interpolators.FAST_OUT_SLOW_IN_REVERSE
4840                     : Interpolators.FAST_OUT_SLOW_IN;
4841         }
4842     }
4843 
4844     /** Bind the {@link FooterView} to the NSSL. */
setFooterView(@onNull FooterView footerView)4845     public void setFooterView(@NonNull FooterView footerView) {
4846         int index = -1;
4847         if (mFooterView != null) {
4848             index = indexOfChild(mFooterView);
4849             removeView(mFooterView);
4850         }
4851         mFooterView = footerView;
4852         addView(mFooterView, index);
4853     }
4854 
setEmptyShadeView(EmptyShadeView emptyShadeView)4855     public void setEmptyShadeView(EmptyShadeView emptyShadeView) {
4856         int index = -1;
4857         if (mEmptyShadeView != null) {
4858             index = indexOfChild(mEmptyShadeView);
4859             removeView(mEmptyShadeView);
4860         }
4861         mEmptyShadeView = emptyShadeView;
4862         addView(mEmptyShadeView, index);
4863     }
4864 
4865     /** Trigger an update for the empty shade resources and visibility. */
updateEmptyShadeView(boolean visible, boolean areNotificationsHiddenInShade, boolean hasFilteredOutSeenNotifications)4866     public void updateEmptyShadeView(boolean visible, boolean areNotificationsHiddenInShade,
4867             boolean hasFilteredOutSeenNotifications) {
4868         ModesEmptyShadeFix.assertInLegacyMode();
4869 
4870         mEmptyShadeView.setVisible(visible, mIsExpanded && mAnimationsEnabled);
4871 
4872         if (areNotificationsHiddenInShade) {
4873             updateEmptyShadeViewResources(R.string.dnd_suppressing_shade_text, 0, 0);
4874         } else if (hasFilteredOutSeenNotifications) {
4875             updateEmptyShadeViewResources(
4876                     R.string.no_unseen_notif_text,
4877                     R.string.unlock_to_see_notif_text,
4878                     R.drawable.ic_friction_lock_closed);
4879         } else {
4880             updateEmptyShadeViewResources(R.string.empty_shade_text, 0, 0);
4881         }
4882     }
4883 
updateEmptyShadeViewResources( @tringRes int newTextRes, @StringRes int newFooterTextRes, @DrawableRes int newFooterIconRes)4884     private void updateEmptyShadeViewResources(
4885             @StringRes int newTextRes,
4886             @StringRes int newFooterTextRes,
4887             @DrawableRes int newFooterIconRes) {
4888         ModesEmptyShadeFix.assertInLegacyMode();
4889 
4890         int oldTextRes = mEmptyShadeView.getTextResource();
4891         if (oldTextRes != newTextRes) {
4892             mEmptyShadeView.setText(newTextRes);
4893         }
4894         int oldFooterTextRes = mEmptyShadeView.getFooterTextResource();
4895         if (oldFooterTextRes != newFooterTextRes) {
4896             mEmptyShadeView.setFooterText(newFooterTextRes);
4897         }
4898         int oldFooterIconRes = mEmptyShadeView.getFooterIconResource();
4899         if (oldFooterIconRes != newFooterIconRes) {
4900             mEmptyShadeView.setFooterIcon(newFooterIconRes);
4901         }
4902         if (newFooterIconRes != 0 || newFooterTextRes != 0) {
4903             mEmptyShadeView.setFooterVisibility(View.VISIBLE);
4904         } else {
4905             mEmptyShadeView.setFooterVisibility(View.GONE);
4906         }
4907     }
4908 
isEmptyShadeViewVisible()4909     public boolean isEmptyShadeViewVisible() {
4910         SceneContainerFlag.assertInLegacyMode();
4911         if (mEmptyShadeView == null) {
4912             return false;
4913         }
4914         return mEmptyShadeView.isVisible();
4915     }
4916 
4917     @VisibleForTesting
setClearAllInProgress(boolean clearAllInProgress)4918     public void setClearAllInProgress(boolean clearAllInProgress) {
4919         mClearAllInProgress = clearAllInProgress;
4920         mAmbientState.setClearAllInProgress(clearAllInProgress);
4921         mController.getNotificationRoundnessManager().setClearAllInProgress(clearAllInProgress);
4922     }
4923 
getClearAllInProgress()4924     boolean getClearAllInProgress() {
4925         return mClearAllInProgress;
4926     }
4927 
4928     /**
4929      * @return the padding after the media header on the lockscreen
4930      */
getPaddingAfterMedia()4931     public int getPaddingAfterMedia() {
4932         return mGapHeight + mPaddingBetweenElements;
4933     }
4934 
getEmptyShadeViewHeight()4935     public int getEmptyShadeViewHeight() {
4936         SceneContainerFlag.assertInLegacyMode();
4937         return mEmptyShadeView.getHeight();
4938     }
4939 
getBottomMostNotificationBottom()4940     public float getBottomMostNotificationBottom() {
4941         SceneContainerFlag.assertInLegacyMode();
4942         final int count = getChildCount();
4943         float max = 0;
4944         for (int childIdx = 0; childIdx < count; childIdx++) {
4945             ExpandableView child = getChildAtIndex(childIdx);
4946             if (child.getVisibility() == GONE) {
4947                 continue;
4948             }
4949             float bottom = child.getTranslationY() + child.getActualHeight()
4950                     - child.getClipBottomAmount();
4951             if (bottom > max) {
4952                 max = bottom;
4953             }
4954         }
4955         return max + getStackTranslation();
4956     }
4957 
setResetUserExpandedStatesRunnable(Runnable runnable)4958     public void setResetUserExpandedStatesRunnable(Runnable runnable) {
4959         this.mResetUserExpandedStatesRunnable = runnable;
4960     }
4961 
setActivityStarter(ActivityStarter activityStarter)4962     public void setActivityStarter(ActivityStarter activityStarter) {
4963         mActivityStarter = activityStarter;
4964     }
4965 
requestAnimateEverything()4966     void requestAnimateEverything() {
4967         if (mIsExpanded && mAnimationsEnabled) {
4968             mEverythingNeedsAnimation = true;
4969             mNeedsAnimation = true;
4970             requestChildrenUpdate();
4971         }
4972     }
4973 
isBelowLastNotification(float touchX, float touchY)4974     public boolean isBelowLastNotification(float touchX, float touchY) {
4975         SceneContainerFlag.assertInLegacyMode();
4976         int childCount = getChildCount();
4977         for (int i = childCount - 1; i >= 0; i--) {
4978             ExpandableView child = getChildAtIndex(i);
4979             if (child.getVisibility() != View.GONE) {
4980                 float childTop = child.getY();
4981                 if (childTop > touchY) {
4982                     // we are above a notification entirely let's abort
4983                     return false;
4984                 }
4985                 boolean belowChild = touchY > childTop + child.getActualHeight()
4986                         - child.getClipBottomAmount();
4987                 if (child == mFooterView) {
4988                     if (!belowChild && !mFooterView.isOnEmptySpace(touchX - mFooterView.getX(),
4989                             touchY - childTop)) {
4990                         // We clicked on the dismiss button
4991                         return false;
4992                     }
4993                 } else if (child == mEmptyShadeView) {
4994                     // We arrived at the empty shade view, for which we accept all clicks
4995                     return true;
4996                 } else if (!belowChild) {
4997                     // We are on a child
4998                     return false;
4999                 }
5000             }
5001         }
5002         return touchY > getTopPadding() + getStackTranslation();
5003     }
5004 
5005     /**
5006      * @hide
5007      */
5008     @Override
onInitializeAccessibilityEventInternal(AccessibilityEvent event)5009     public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
5010         super.onInitializeAccessibilityEventInternal(event);
5011         event.setScrollable(mScrollable);
5012         event.setMaxScrollX(mScrollX);
5013 
5014         if (SceneContainerFlag.isEnabled()) {
5015             event.setScrollY(mScrollViewFields.getScrollState().getScrollPosition());
5016             event.setMaxScrollY(mScrollViewFields.getScrollState().getMaxScrollPosition());
5017         } else {
5018             event.setScrollY(getOwnScrollY());
5019             event.setMaxScrollY(getScrollRange());
5020         }
5021     }
5022 
5023     @Override
onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info)5024     public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
5025         super.onInitializeAccessibilityNodeInfoInternal(info);
5026         if (mScrollable) {
5027             info.setScrollable(true);
5028             if (mBackwardScrollable) {
5029                 info.addAction(
5030                         AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD);
5031                 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_UP);
5032             }
5033             if (mForwardScrollable) {
5034                 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD);
5035                 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_DOWN);
5036             }
5037         }
5038         // Talkback only listenes to scroll events of certain classes, let's make us a scrollview
5039         info.setClassName(ScrollView.class.getName());
5040     }
5041 
getContainerChildCount()5042     public int getContainerChildCount() {
5043         return getChildCount();
5044     }
5045 
getContainerChildAt(int i)5046     public View getContainerChildAt(int i) {
5047         return getChildAt(i);
5048     }
5049 
removeContainerView(View v)5050     public void removeContainerView(View v) {
5051         Assert.isMainThread();
5052         removeView(v);
5053         updateSpeedBumpIndex();
5054     }
5055 
addContainerView(View v)5056     public void addContainerView(View v) {
5057         Assert.isMainThread();
5058         addView(v);
5059         updateSpeedBumpIndex();
5060     }
5061 
addContainerViewAt(View v, int index)5062     public void addContainerViewAt(View v, int index) {
5063         Assert.isMainThread();
5064         ensureRemovedFromTransientContainer(v);
5065         addView(v, index);
5066         updateSpeedBumpIndex();
5067     }
5068 
ensureRemovedFromTransientContainer(View v)5069     private void ensureRemovedFromTransientContainer(View v) {
5070         if (v.getParent() != null && v instanceof ExpandableView) {
5071             // If the child is animating away, it will still have a parent, so detach it first
5072             // TODO: We should really cancel the active animations here. This will
5073             //  happen automatically when the view's intro animation starts, but
5074             //  it's a fragile link.
5075             ((ExpandableView) v).removeFromTransientContainerForAdditionTo(this);
5076         }
5077     }
5078 
runAfterAnimationFinished(Runnable runnable)5079     public void runAfterAnimationFinished(Runnable runnable) {
5080         mAnimationFinishedRunnables.add(runnable);
5081     }
5082 
generateHeadsUpAnimation( NotificationEntry entry, boolean isHeadsUp, boolean hasStatusBarChip)5083     public void generateHeadsUpAnimation(
5084             NotificationEntry entry, boolean isHeadsUp, boolean hasStatusBarChip) {
5085         SceneContainerFlag.assertInLegacyMode();
5086         ExpandableNotificationRow row = entry.getHeadsUpAnimationView();
5087         generateHeadsUpAnimation(row, isHeadsUp, hasStatusBarChip);
5088     }
5089 
5090     /**
5091      * Notifies the NSSL, that the given view would need a HeadsUp animation, when it is being
5092      * added to this container.
5093      *
5094      * @param row to animate
5095      * @param isHeadsUp true for appear, false for disappear animations
5096      * @param hasStatusBarChip true if the status bar is currently displaying a chip for the given
5097      *                         notification
5098      */
generateHeadsUpAnimation( ExpandableNotificationRow row, boolean isHeadsUp, boolean hasStatusBarChip)5099     public void generateHeadsUpAnimation(
5100             ExpandableNotificationRow row, boolean isHeadsUp, boolean hasStatusBarChip) {
5101         boolean addAnimation =
5102                 mAnimationsEnabled && (isHeadsUp || mHeadsUpGoingAwayAnimationsAllowed);
5103         if (NotificationThrottleHun.isEnabled()) {
5104             final boolean closedAndSeenInShade = !mIsExpanded && row.getEntry() != null
5105                     && row.getEntry().isSeenInShade();
5106             addAnimation = addAnimation && !closedAndSeenInShade;
5107         }
5108         if (SPEW) {
5109             Log.v(TAG, "generateHeadsUpAnimation:"
5110                     + " addAnimation=" + addAnimation
5111                     + (row.getEntry() == null ? " entry NULL "
5112                             : " isSeenInShade=" + row.getEntry().isSeenInShade()
5113                                     + " row=" + row.getKey())
5114                     + " mIsExpanded=" + mIsExpanded
5115                     + " isHeadsUp=" + isHeadsUp
5116                     + " hasStatusBarChip=" + hasStatusBarChip);
5117         }
5118 
5119         if (addAnimation) {
5120             // If we're hiding a HUN we just started showing THIS FRAME, then remove that event,
5121             // and do not add the disappear event either.
5122             boolean showingHunThisFrame =
5123                     mHeadsUpChangeAnimations.containsKey(row)
5124                             && mHeadsUpChangeAnimations.get(row).isHeadsUpAppearance();
5125             if (!isHeadsUp && showingHunThisFrame) {
5126                 mHeadsUpChangeAnimations.remove(row);
5127                 if (SPEW) {
5128                     Log.v(TAG, "generateHeadsUpAnimation: previous hun appear animation cancelled");
5129                 }
5130                 logHunAnimationSkipped(row, "previous hun appear animation cancelled");
5131                 return;
5132             }
5133             mHeadsUpChangeAnimations.put(
5134                     row, new HeadsUpAnimationEvent(row, isHeadsUp, hasStatusBarChip));
5135             mNeedsAnimation = true;
5136             if (!mIsExpanded && !mWillExpand && !isHeadsUp) {
5137                 row.setHeadsUpAnimatingAway(true);
5138                 if (SceneContainerFlag.isEnabled()) {
5139                     setHeadsUpAnimatingAway(true);
5140                 }
5141             }
5142             if (StatusBarNotifChips.isEnabled()) {
5143                 row.setHasStatusBarChipDuringHeadsUpAnimation(hasStatusBarChip);
5144             }
5145             requestChildrenUpdate();
5146         }
5147     }
5148 
5149     /**
5150      * Set the boundary for the bottom heads up position. The heads up will always be above this
5151      * position.
5152      *
5153      * @param height          the height of the screen
5154      * @param bottomBarHeight the height of the bar on the bottom
5155      */
setHeadsUpBoundaries(int height, int bottomBarHeight)5156     public void setHeadsUpBoundaries(int height, int bottomBarHeight) {
5157         SceneContainerFlag.assertInLegacyMode();
5158         mAmbientState.setMaxHeadsUpTranslation(height - bottomBarHeight);
5159 
5160         if (NotificationsHunSharedAnimationValues.isEnabled()) {
5161             mHeadsUpAnimator.setHeadsUpAppearHeightBottom(height);
5162             mHeadsUpAnimator.setStackTopMargin(mAmbientState.getStackTopMargin());
5163         } else {
5164             mStackScrollAlgorithm.setHeadsUpAppearHeightBottom(height);
5165             mStateAnimator.setHeadsUpAppearHeightBottom(height);
5166             mStateAnimator.setStackTopMargin(mAmbientState.getStackTopMargin());
5167         }
5168 
5169         requestChildrenUpdate();
5170     }
5171 
setWillExpand(boolean willExpand)5172     public void setWillExpand(boolean willExpand) {
5173         mWillExpand = willExpand;
5174     }
5175 
setTrackingHeadsUp(ExpandableNotificationRow row)5176     public void setTrackingHeadsUp(ExpandableNotificationRow row) {
5177         mAmbientState.setTrackedHeadsUpRow(row);
5178     }
5179 
forceNoOverlappingRendering(boolean force)5180     public void forceNoOverlappingRendering(boolean force) {
5181         mForceNoOverlappingRendering = force;
5182     }
5183 
5184     @Override
hasOverlappingRendering()5185     public boolean hasOverlappingRendering() {
5186         return !mForceNoOverlappingRendering && super.hasOverlappingRendering();
5187     }
5188 
setAnimationRunning(boolean animationRunning)5189     public void setAnimationRunning(boolean animationRunning) {
5190         if (animationRunning != mAnimationRunning) {
5191             if (animationRunning) {
5192                 getViewTreeObserver().addOnPreDrawListener(mRunningAnimationUpdater);
5193             } else {
5194                 getViewTreeObserver().removeOnPreDrawListener(mRunningAnimationUpdater);
5195             }
5196             mAnimationRunning = animationRunning;
5197             updateContinuousShadowDrawing();
5198         }
5199     }
5200 
isExpanded()5201     public boolean isExpanded() {
5202         return mIsExpanded;
5203     }
5204 
setPulsing(boolean pulsing, boolean animated)5205     public void setPulsing(boolean pulsing, boolean animated) {
5206         if (!mPulsing && !pulsing) {
5207             return;
5208         }
5209         mPulsing = pulsing;
5210         mAmbientState.setPulsing(pulsing);
5211         mSwipeHelper.setPulsing(pulsing);
5212         updateNotificationAnimationStates();
5213         updateAlgorithmHeightAndPadding();
5214         updateContentHeight();
5215         requestChildrenUpdate();
5216         notifyHeightChangeListener(null, animated);
5217     }
5218 
setQsFullScreen(boolean qsFullScreen)5219     public void setQsFullScreen(boolean qsFullScreen) {
5220         SceneContainerFlag.assertInLegacyMode();
5221         if (qsFullScreen == mQsFullScreen) {
5222             return;  // no change
5223         }
5224         mQsFullScreen = qsFullScreen;
5225         updateAlgorithmLayoutMinHeight();
5226         updateScrollability();
5227     }
5228 
isQsFullScreen()5229     boolean isQsFullScreen() {
5230         SceneContainerFlag.assertInLegacyMode();
5231         return mQsFullScreen;
5232     }
5233 
getQsExpansionFraction()5234     private float getQsExpansionFraction() {
5235         SceneContainerFlag.assertInLegacyMode();
5236         return mQsExpansionFraction;
5237     }
5238 
setQsExpansionFraction(float qsExpansionFraction)5239     public void setQsExpansionFraction(float qsExpansionFraction) {
5240         SceneContainerFlag.assertInLegacyMode();
5241         mQsExpansionFraction = qsExpansionFraction;
5242         updateUseRoundedRectClipping();
5243 
5244         // If notifications are scrolled,
5245         // clear out scrollY by the time we push notifications offscreen
5246         if (getOwnScrollY() > 0) {
5247             setOwnScrollY((int) MathUtils.lerp(getOwnScrollY(), 0, getQsExpansionFraction()));
5248         }
5249     }
5250 
5251     @VisibleForTesting
getOwnScrollY()5252     int getOwnScrollY() {
5253         if (SceneContainerFlag.isEnabled()) {
5254             return 0;
5255         } else {
5256             return mOwnScrollY;
5257         }
5258     }
5259 
5260     @VisibleForTesting
setOwnScrollY(int ownScrollY)5261     void setOwnScrollY(int ownScrollY) {
5262         setOwnScrollY(ownScrollY, false /* animateScrollChangeListener */);
5263     }
5264 
setOwnScrollY(int ownScrollY, boolean animateStackYChangeListener)5265     private void setOwnScrollY(int ownScrollY, boolean animateStackYChangeListener) {
5266         // If scene container is active, NSSL should not control its own scrolling.
5267         if (SceneContainerFlag.isEnabled()) {
5268             return;
5269         }
5270         // Avoid Flicking during clear all
5271         // when the shade finishes closing, onExpansionStopped will call
5272         // resetScrollPosition to setOwnScrollY to 0
5273         if (mAmbientState.isClosing() || mAmbientState.isClearAllInProgress()) {
5274             return;
5275         }
5276 
5277         if (ownScrollY != getOwnScrollY()) {
5278             // We still want to call the normal scrolled changed for accessibility reasons
5279             onScrollChanged(mScrollX, ownScrollY, mScrollX, getOwnScrollY());
5280             mOwnScrollY = ownScrollY;
5281             mAmbientState.setScrollY(getOwnScrollY());
5282             updateOnScrollChange();
5283             updateStackPosition(animateStackYChangeListener);
5284         }
5285     }
5286 
updateOnScrollChange()5287     private void updateOnScrollChange() {
5288         SceneContainerFlag.assertInLegacyMode();
5289         if (mScrollListener != null) {
5290             mScrollListener.accept(getOwnScrollY());
5291         }
5292         updateForwardAndBackwardScrollability();
5293         requestChildrenUpdate();
5294     }
5295 
5296     @Nullable
getShelf()5297     public ExpandableView getShelf() {
5298         return mShelf;
5299     }
5300 
setShelf(NotificationShelf shelf)5301     public void setShelf(NotificationShelf shelf) {
5302         int index = -1;
5303         if (mShelf != null) {
5304             index = indexOfChild(mShelf);
5305             removeView(mShelf);
5306         }
5307         mShelf = shelf;
5308         addView(mShelf, index);
5309         mAmbientState.setShelf(mShelf);
5310         mStateAnimator.setShelf(mShelf);
5311         shelf.bind(mAmbientState, this, mController.getNotificationRoundnessManager());
5312     }
5313 
5314     /**
5315      * Whether the notifications are displayed over the unoccluded lockscreen. Returns false on the
5316      * locked shade.
5317      */
isOnLockscreen()5318     public boolean isOnLockscreen() {
5319         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return false;
5320         return mIsOnLockscreen;
5321     }
5322 
5323     /** @see #isOnLockscreen() */
setOnLockscreen(boolean isOnLockscreen)5324     public void setOnLockscreen(boolean isOnLockscreen) {
5325         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
5326         if (mIsOnLockscreen != isOnLockscreen) {
5327             mIsOnLockscreen = isOnLockscreen;
5328             for (int i = 0; i < getChildCount(); i++) {
5329                 View child = getChildAt(i);
5330                 if (child instanceof ExpandableNotificationRow childRow) {
5331                     childRow.setOnKeyguard(isOnLockscreen);
5332                 }
5333             }
5334         }
5335     }
5336 
setMaxDisplayedNotifications(int maxDisplayedNotifications)5337     public void setMaxDisplayedNotifications(int maxDisplayedNotifications) {
5338         if (mMaxDisplayedNotifications != maxDisplayedNotifications) {
5339             mMaxDisplayedNotifications = maxDisplayedNotifications;
5340             if (SceneContainerFlag.isEnabled()) {
5341                 updateIntrinsicStackHeight();
5342                 updateStackEndHeightAndStackHeight(mAmbientState.getExpansionFraction());
5343                 if (maxDisplayedNotifications == -1) {
5344                     animateGoToFullShade(0);
5345                 }
5346             } else {
5347                 updateContentHeight();
5348             }
5349             notifyHeightChangeListener(mShelf);
5350         }
5351     }
5352 
5353     /**
5354      * This is used for debugging only; it will be used to draw the otherwise invisible line which
5355      * NotificationPanelViewController treats as the bottom when calculating how many notifications
5356      * appear on the keyguard.
5357      * Setting a negative number will disable rendering this line.
5358      */
setKeyguardBottomPadding(float keyguardBottomPadding)5359     public void setKeyguardBottomPadding(float keyguardBottomPadding) {
5360         mKeyguardBottomPadding = keyguardBottomPadding;
5361     }
5362 
setShouldShowShelfOnly(boolean shouldShowShelfOnly)5363     public void setShouldShowShelfOnly(boolean shouldShowShelfOnly) {
5364         mShouldShowShelfOnly = shouldShowShelfOnly;
5365         updateAlgorithmLayoutMinHeight();
5366     }
5367 
getMinExpansionHeight()5368     public int getMinExpansionHeight() {
5369         // shelf height is defined in dp but status bar height can be defined in px, that makes
5370         // relation between them variable - sometimes one might be bigger than the other when
5371         // changing density. That’s why we need to ensure we’re not subtracting negative value below
5372         return mShelf.getIntrinsicHeight()
5373                 - Math.max(0,
5374                 (mShelf.getIntrinsicHeight() - mStatusBarHeight + mWaterfallTopInset) / 2)
5375                 + mWaterfallTopInset;
5376     }
5377 
setInHeadsUpPinnedMode(boolean inHeadsUpPinnedMode)5378     public void setInHeadsUpPinnedMode(boolean inHeadsUpPinnedMode) {
5379         mInHeadsUpPinnedMode = inHeadsUpPinnedMode;
5380         updateClipping();
5381     }
5382 
5383     /** TODO(b/328390331) make this private, when {@link SceneContainerFlag} is removed */
setHeadsUpAnimatingAway(boolean headsUpAnimatingAway)5384     public void setHeadsUpAnimatingAway(boolean headsUpAnimatingAway) {
5385         if (mHeadsUpAnimatingAway != headsUpAnimatingAway) {
5386             mHeadsUpAnimatingAway = headsUpAnimatingAway;
5387             if (mHeadsUpAnimatingAwayListener != null) {
5388                 mHeadsUpAnimatingAwayListener.accept(headsUpAnimatingAway);
5389             }
5390         }
5391         updateClipping();
5392     }
5393 
5394     /**
5395      * Sets a listener to be notified about the heads up disappear animation state changes. If there
5396      * are overlapping animations, it will receive updates when the first disappar animation has
5397      * started, and when the last has finished.
5398      *
5399      * @param headsUpAnimatingAwayListener to be notified about disappear animation state changes.
5400      */
setHeadsUpAnimatingAwayListener( Consumer<Boolean> headsUpAnimatingAwayListener)5401     public void setHeadsUpAnimatingAwayListener(
5402             Consumer<Boolean> headsUpAnimatingAwayListener) {
5403         mHeadsUpAnimatingAwayListener = headsUpAnimatingAwayListener;
5404     }
5405     @VisibleForTesting
setStatusBarState(int statusBarState)5406     public void setStatusBarState(int statusBarState) {
5407         mStatusBarState = statusBarState;
5408         mAmbientState.setStatusBarState(statusBarState);
5409         updateSpeedBumpIndex();
5410         updateDismissBehavior();
5411     }
5412 
5413     @Override
setShowingStackOnLockscreen(boolean showingStackOnLockscreen)5414     public void setShowingStackOnLockscreen(boolean showingStackOnLockscreen) {
5415         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
5416         mAmbientState.setShowingStackOnLockscreen(showingStackOnLockscreen);
5417     }
5418 
5419     @Override
setAlphaForLockscreenFadeIn(float alphaForLockscreenFadeIn)5420     public void setAlphaForLockscreenFadeIn(float alphaForLockscreenFadeIn) {
5421         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
5422         mAmbientState.setLockscreenStackFadeInProgress(alphaForLockscreenFadeIn);
5423         requestChildrenUpdate();
5424     }
5425 
onStatePostChange(boolean fromShadeLocked)5426     void onStatePostChange(boolean fromShadeLocked) {
5427         boolean onKeyguard = onKeyguard();
5428 
5429         if (mHeadsUpAppearanceController != null && !StatusBarNoHunBehavior.isEnabled()) {
5430             mHeadsUpAppearanceController.onStateChanged();
5431         }
5432 
5433         setExpandingEnabled(!onKeyguard);
5434         requestChildrenUpdate();
5435         onUpdateRowStates();
5436         updateVisibility();
5437     }
5438 
setExpandingVelocity(float expandingVelocity)5439     public void setExpandingVelocity(float expandingVelocity) {
5440         SceneContainerFlag.assertInLegacyMode();
5441         mAmbientState.setExpandingVelocity(expandingVelocity);
5442     }
5443 
getOpeningHeight()5444     public float getOpeningHeight() {
5445         SceneContainerFlag.assertInLegacyMode();
5446         if (mEmptyShadeView == null || mEmptyShadeView.getVisibility() == GONE) {
5447             return getMinExpansionHeight();
5448         } else {
5449             return getAppearEndPosition();
5450         }
5451     }
5452 
setIsFullWidth(boolean isFullWidth)5453     public void setIsFullWidth(boolean isFullWidth) {
5454         mAmbientState.setSmallScreen(isFullWidth);
5455     }
5456 
setPanelFlinging(boolean flinging)5457     public void setPanelFlinging(boolean flinging) {
5458         SceneContainerFlag.assertInLegacyMode();
5459         mAmbientState.setFlinging(flinging);
5460         if (!flinging) {
5461             // re-calculate the stack height which was frozen while flinging
5462             updateStackPosition();
5463         }
5464     }
5465 
5466     @Override
suppressHeightUpdates(boolean suppress)5467     public void suppressHeightUpdates(boolean suppress) {
5468         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
5469         mSuppressHeightUpdates = suppress;
5470     }
5471 
setHeadsUpGoingAwayAnimationsAllowed(boolean headsUpGoingAwayAnimationsAllowed)5472     public void setHeadsUpGoingAwayAnimationsAllowed(boolean headsUpGoingAwayAnimationsAllowed) {
5473         mHeadsUpGoingAwayAnimationsAllowed = headsUpGoingAwayAnimationsAllowed;
5474     }
5475 
dump(PrintWriter pwOriginal, String[] args)5476     public void dump(PrintWriter pwOriginal, String[] args) {
5477         IndentingPrintWriter pw = DumpUtilsKt.asIndenting(pwOriginal);
5478         final long elapsedRealtime = SystemClock.elapsedRealtime();
5479         pw.println("Internal state:");
5480         DumpUtilsKt.withIncreasedIndent(pw, () -> {
5481             println(pw, "pulsing", mPulsing);
5482             println(pw, "expanded", mIsExpanded);
5483             println(pw, "headsUpPinned", mInHeadsUpPinnedMode);
5484             println(pw, "roundedRectClipping", mShouldUseRoundedRectClipping);
5485             println(pw, "negativeRoundedRectClipping", mShouldUseNegativeRoundedRectClipping);
5486             println(pw, "qsClipDismiss", mDismissUsingRowTranslationX);
5487             println(pw, "visibility", visibilityString(getVisibility()));
5488             println(pw, "alpha", getAlpha());
5489             println(pw, "suppressChildrenMeasureLayout", mSuppressChildrenMeasureAndLayout);
5490             println(pw, "scrollY", mAmbientState.getScrollY());
5491             println(pw, "showShelfOnly", mShouldShowShelfOnly);
5492             println(pw, "hideAmount", mAmbientState.getHideAmount());
5493             println(pw, "ambientStateSwipingUp", mAmbientState.isSwipingUp());
5494             println(pw, "maxDisplayedNotifications", mMaxDisplayedNotifications);
5495             println(pw, "intrinsicPadding", mIntrinsicPadding);
5496             println(pw, "bottomPadding", mBottomPadding);
5497             dumpRoundedRectClipping(pw);
5498             println(pw, "requestedClipBounds", mRequestedClipBounds);
5499             println(pw, "isClipped", mIsClipped);
5500             println(pw, "translationX", getTranslationX());
5501             println(pw, "translationY", getTranslationY());
5502             println(pw, "translationZ", getTranslationZ());
5503             println(pw, "skinnyNotifsInLandscape", mSkinnyNotifsInLandscape);
5504             println(pw, "minimumPaddings", mMinimumPaddings);
5505             println(pw, "qsTilePadding", mQsTilePadding);
5506             println(pw, "sidePaddings", mSidePaddings);
5507             println(pw, "elapsedRealtime", elapsedRealtime);
5508             println(pw, "lastInitView", mLastInitViewDumpString);
5509             println(pw, "lastInitViewElapsedRealtime", mLastInitViewElapsedRealtime);
5510             println(pw, "lastInitViewMillisAgo", elapsedRealtime - mLastInitViewElapsedRealtime);
5511             println(pw, "shouldUseSplitNotificationShade", mShouldUseSplitNotificationShade);
5512             println(pw, "lastUpdateSidePadding", mLastUpdateSidePaddingDumpString);
5513             println(pw, "lastUpdateSidePaddingElapsedRealtime",
5514                     mLastUpdateSidePaddingElapsedRealtime);
5515             println(pw, "lastUpdateSidePaddingMillisAgo",
5516                     elapsedRealtime - mLastUpdateSidePaddingElapsedRealtime);
5517             println(pw, "isSmallLandscapeLockscreenEnabled", mIsSmallLandscapeLockscreenEnabled);
5518             mNotificationStackSizeCalculator.dump(pw, args);
5519             mScrollViewFields.dump(pw);
5520             if (!SceneContainerFlag.isEnabled()) {
5521                 // fields which will be removed with SceneContainer
5522                 println(pw, "intrinsicContentHeight", getIntrinsicContentHeight());
5523                 println(pw, "contentHeight", getContentHeight());
5524                 println(pw, "topPadding", getTopPadding());
5525                 println(pw, "maxTopPadding", getMaxTopPadding());
5526                 println(pw, "qsExpandFraction", getQsExpansionFraction());
5527             }
5528         });
5529         pw.println();
5530         pw.println("Contents:");
5531         DumpUtilsKt.withIncreasedIndent(
5532                 pw,
5533                 () -> {
5534                     int childCount = getChildCount();
5535                     pw.println("Number of children: " + childCount);
5536                     pw.println();
5537 
5538                     for (int i = 0; i < childCount; i++) {
5539                         ExpandableView child = getChildAtIndex(i);
5540                         child.dump(pw, args);
5541                         pw.println();
5542                     }
5543                     int transientViewCount = getTransientViewCount();
5544                     pw.println("Transient Views: " + transientViewCount);
5545                     for (int i = 0; i < transientViewCount; i++) {
5546                         ExpandableView child = (ExpandableView) getTransientView(i);
5547                         child.dump(pw, args);
5548                     }
5549                     View swipedView = mSwipeHelper.getSwipedView();
5550                     pw.println("Swiped view: " + swipedView);
5551                     if (swipedView instanceof ExpandableView expandableView) {
5552                         expandableView.dump(pw, args);
5553                     }
5554                 });
5555     }
5556 
dumpRoundedRectClipping(IndentingPrintWriter pw)5557     private void dumpRoundedRectClipping(IndentingPrintWriter pw) {
5558         pw.append("roundedRectClipping{l=").print(mRoundedRectClippingLeft);
5559         pw.append(" t=").print(mRoundedRectClippingTop);
5560         pw.append(" r=").print(mRoundedRectClippingRight);
5561         pw.append(" b=").print(mRoundedRectClippingBottom);
5562         pw.append(" +y=").print(mRoundedRectClippingYTranslation);
5563         pw.append("} topRadius=").print(mRoundedClipCornerRadii[0]);
5564         pw.append(" bottomRadius=").println(mRoundedClipCornerRadii[4]);
5565     }
5566 
isFullyHidden()5567     public boolean isFullyHidden() {
5568         return mAmbientState.isFullyHidden();
5569     }
5570 
5571     /**
5572      * Add a listener whenever the expanded height changes. The first value passed as an
5573      * argument is the expanded height and the second one is the appearFraction.
5574      *
5575      * @param listener the listener to notify.
5576      */
addOnExpandedHeightChangedListener(BiConsumer<Float, Float> listener)5577     public void addOnExpandedHeightChangedListener(BiConsumer<Float, Float> listener) {
5578         mExpandedHeightListeners.add(listener);
5579     }
5580 
5581     /**
5582      * Stop a listener from listening to the expandedHeight.
5583      */
removeOnExpandedHeightChangedListener(BiConsumer<Float, Float> listener)5584     public void removeOnExpandedHeightChangedListener(BiConsumer<Float, Float> listener) {
5585         mExpandedHeightListeners.remove(listener);
5586     }
5587 
setHeadsUpAppearanceController( HeadsUpAppearanceController headsUpAppearanceController)5588     void setHeadsUpAppearanceController(
5589             HeadsUpAppearanceController headsUpAppearanceController) {
5590         mHeadsUpAppearanceController = headsUpAppearanceController;
5591     }
5592 
5593     @VisibleForTesting
isVisible(View child)5594     public boolean isVisible(View child) {
5595         boolean hasClipBounds = child.getClipBounds(mTmpRect);
5596         return child.getVisibility() == View.VISIBLE
5597                 && (!hasClipBounds || mTmpRect.height() > 0);
5598     }
5599 
5600     /** Whether the group is expanded to show the child notifications, and they are visible. */
areChildrenVisible(ExpandableNotificationRow parent)5601     private boolean areChildrenVisible(ExpandableNotificationRow parent) {
5602         List<ExpandableNotificationRow> children = parent.getAttachedChildren();
5603         return isVisible(parent)
5604                 && children != null
5605                 && parent.areChildrenExpanded();
5606     }
5607 
5608     // Similar to #getRowsToDismissInBackend, but filters for visible views.
getVisibleViewsToAnimateAway(@electedRows int selection, boolean hideSilentSection)5609     private ArrayList<View> getVisibleViewsToAnimateAway(@SelectedRows int selection,
5610             boolean hideSilentSection) {
5611         final int viewCount = getChildCount();
5612         final ArrayList<View> viewsToHide = new ArrayList<>(viewCount);
5613 
5614         for (int i = 0; i < viewCount; i++) {
5615             final View view = getChildAt(i);
5616 
5617             if (view instanceof SectionHeaderView) {
5618                 // The only SectionHeaderView we have is the silent section header.
5619                 if (hideSilentSection) {
5620                     viewsToHide.add(view);
5621                 }
5622             }
5623 
5624             if (view instanceof ExpandableNotificationRow parent) {
5625                 if (isVisible(parent) && includeChildInClearAll(parent, selection)) {
5626                     viewsToHide.add(parent);
5627                 }
5628 
5629                 if (areChildrenVisible(parent)) {
5630                     for (ExpandableNotificationRow child : parent.getAttachedChildren()) {
5631                         if (isVisible(child) && includeChildInClearAll(child, selection)) {
5632                             viewsToHide.add(child);
5633                         }
5634                     }
5635                 }
5636             }
5637         }
5638         return viewsToHide;
5639     }
5640 
getRowsToDismissInBackend( @electedRows int selection)5641     private ArrayList<ExpandableNotificationRow> getRowsToDismissInBackend(
5642             @SelectedRows int selection) {
5643         final int childCount = getChildCount();
5644         final ArrayList<ExpandableNotificationRow> viewsToRemove = new ArrayList<>(childCount);
5645 
5646         for (int i = 0; i < childCount; i++) {
5647             final View view = getChildAt(i);
5648             if (!(view instanceof ExpandableNotificationRow parent)) {
5649                 continue;
5650             }
5651             if (includeChildInClearAll(parent, selection)) {
5652                 viewsToRemove.add(parent);
5653             }
5654             List<ExpandableNotificationRow> children = parent.getAttachedChildren();
5655             if (isVisible(parent) && children != null) {
5656                 for (ExpandableNotificationRow child : children) {
5657                     if (includeChildInClearAll(parent, selection)) {
5658                         viewsToRemove.add(child);
5659                     }
5660                 }
5661             }
5662         }
5663         return viewsToRemove;
5664     }
5665 
5666     /** Clear all clearable notifications when the user requests it. */
clearAllNotifications(boolean hideSilentSection)5667     public void clearAllNotifications(boolean hideSilentSection) {
5668         clearNotifications(ROWS_ALL, /* closeShade = */ true, hideSilentSection);
5669     }
5670 
5671     /** Clear all clearable silent notifications when the user requests it. */
clearSilentNotifications(boolean closeShade, boolean hideSilentSection)5672     public void clearSilentNotifications(boolean closeShade,
5673             boolean hideSilentSection) {
5674         clearNotifications(ROWS_GENTLE, closeShade, hideSilentSection);
5675     }
5676 
5677     /**
5678      * Collects a list of visible rows, and animates them away in a staggered fashion as if they
5679      * were dismissed. Notifications are dismissed in the backend via onClearAllAnimationsEnd.
5680      */
clearNotifications(@electedRows int selection, boolean closeShade, boolean hideSilentSection)5681     void clearNotifications(@SelectedRows int selection, boolean closeShade,
5682             boolean hideSilentSection) {
5683         // Animate-swipe all dismissable notifications, then animate the shade closed
5684         final ArrayList<View> viewsToAnimateAway = getVisibleViewsToAnimateAway(selection,
5685                 hideSilentSection);
5686         final ArrayList<ExpandableNotificationRow> rowsToDismissInBackend =
5687                 getRowsToDismissInBackend(selection);
5688         if (mClearAllListener != null) {
5689             mClearAllListener.onClearAll(selection);
5690         }
5691         final Consumer<Boolean> dismissInBackend = (cancelled) -> {
5692             if (cancelled) {
5693                 post(() -> onClearAllAnimationsEnd(rowsToDismissInBackend, selection));
5694             } else {
5695                 onClearAllAnimationsEnd(rowsToDismissInBackend, selection);
5696             }
5697         };
5698         if (viewsToAnimateAway.isEmpty()) {
5699             dismissInBackend.accept(true);
5700             return;
5701         }
5702         // Disable normal animations
5703         setClearAllInProgress(true);
5704         mShadeNeedsToClose = closeShade;
5705 
5706         InteractionJankMonitor.getInstance().begin(this, CUJ_SHADE_CLEAR_ALL);
5707         // Decrease the delay for every row we animate to give the sense of
5708         // accelerating the swipes
5709         final int rowDelayDecrement = 5;
5710         int currentDelay = 60;
5711         int totalDelay = 0;
5712         final int numItems = viewsToAnimateAway.size();
5713         for (int i = numItems - 1; i >= 0; i--) {
5714             View view = viewsToAnimateAway.get(i);
5715             Consumer<Boolean> endRunnable = null;
5716             if (i == 0) {
5717                 endRunnable = dismissInBackend;
5718             }
5719             dismissViewAnimated(view, endRunnable, totalDelay, ANIMATION_DURATION_SWIPE);
5720             currentDelay = Math.max(30, currentDelay - rowDelayDecrement);
5721             totalDelay += currentDelay;
5722         }
5723     }
5724 
includeChildInClearAll( ExpandableNotificationRow row, @SelectedRows int selection)5725     private boolean includeChildInClearAll(
5726             ExpandableNotificationRow row,
5727             @SelectedRows int selection) {
5728         return canChildBeCleared(row) && matchesSelection(row, selection);
5729     }
5730 
inflateEmptyShadeView()5731     private void inflateEmptyShadeView() {
5732         ModesEmptyShadeFix.assertInLegacyMode();
5733 
5734         EmptyShadeView oldView = mEmptyShadeView;
5735         EmptyShadeView view = (EmptyShadeView) LayoutInflater.from(mContext).inflate(
5736                 R.layout.status_bar_no_notifications, this, false);
5737         view.setOnClickListener(v -> {
5738             final boolean showHistory = mController.isHistoryEnabled();
5739             Intent intent = showHistory
5740                     ? new Intent(Settings.ACTION_NOTIFICATION_HISTORY)
5741                     : new Intent(Settings.ACTION_NOTIFICATION_SETTINGS);
5742             mActivityStarter.startActivity(intent, true, true, Intent.FLAG_ACTIVITY_SINGLE_TOP);
5743         });
5744         setEmptyShadeView(view);
5745         view.setVisible(oldView != null && oldView.isVisible(), /* animate = */ false);
5746         updateEmptyShadeViewResources(
5747                 oldView == null ? R.string.empty_shade_text : oldView.getTextResource(),
5748                 oldView == null ? 0 : oldView.getFooterTextResource(),
5749                 oldView == null ? 0 : oldView.getFooterIconResource());
5750     }
5751 
5752     /**
5753      * Updates expanded, dimmed and locked states of notification rows.
5754      */
onUpdateRowStates()5755     public void onUpdateRowStates() {
5756 
5757         // The following views will be moved to the end of mStackScroller. This counter represents
5758         // the offset from the last child. Initialized to 1 for the very last position. It is post-
5759         // incremented in the following "changeViewPosition" calls so that its value is correct for
5760         // subsequent calls.
5761         int offsetFromEnd = 1;
5762         changeViewPosition(mFooterView, getChildCount() - offsetFromEnd++);
5763         changeViewPosition(mEmptyShadeView, getChildCount() - offsetFromEnd++);
5764 
5765         // No post-increment for this call because it is the last one. Make sure to add one if
5766         // another "changeViewPosition" call is ever added.
5767         changeViewPosition(mShelf,
5768                 getChildCount() - offsetFromEnd);
5769     }
5770 
5771     /**
5772      * Set how far the wake up is when waking up from pulsing. This is a height and will adjust the
5773      * notification positions accordingly.
5774      *
5775      * @param height the new wake up height
5776      * @return the overflow how much the height is further than he lowest notification
5777      */
setPulseHeight(float height)5778     public float setPulseHeight(float height) {
5779         float overflow;
5780         mAmbientState.setPulseHeight(height);
5781         if (mKeyguardBypassEnabled) {
5782             notifyAppearChangedListeners();
5783             overflow = Math.max(0, height - getIntrinsicPadding());
5784         } else {
5785             overflow = Math.max(0, height
5786                     - mAmbientState.getInnerHeight(true /* ignorePulseHeight */));
5787         }
5788         requestChildrenUpdate();
5789         return overflow;
5790     }
5791 
getPulseHeight()5792     public float getPulseHeight() {
5793         return mAmbientState.getPulseHeight();
5794     }
5795 
5796     /**
5797      * Set the amount how much we're dozing. This is different from how hidden the shade is, when
5798      * the notification is pulsing.
5799      */
setDozeAmount(float dozeAmount)5800     public void setDozeAmount(float dozeAmount) {
5801         mAmbientState.setDozeAmount(dozeAmount);
5802         updateStackPosition();
5803         requestChildrenUpdate();
5804     }
5805 
isFullyAwake()5806     public boolean isFullyAwake() {
5807         return mAmbientState.isFullyAwake();
5808     }
5809 
wakeUpFromPulse()5810     public void wakeUpFromPulse() {
5811         setPulseHeight(getWakeUpHeight());
5812         // Let's place the hidden views at the end of the pulsing notification to make sure we have
5813         // a smooth animation
5814         boolean firstVisibleView = true;
5815         float wakeUplocation = -1f;
5816         int childCount = getChildCount();
5817         for (int i = 0; i < childCount; i++) {
5818             ExpandableView view = getChildAtIndex(i);
5819             if (view.getVisibility() == View.GONE) {
5820                 continue;
5821             }
5822             boolean isShelf = view == mShelf;
5823             if (!(view instanceof ExpandableNotificationRow) && !isShelf) {
5824                 continue;
5825             }
5826             if (view.getVisibility() == View.VISIBLE && !isShelf) {
5827                 if (firstVisibleView) {
5828                     firstVisibleView = false;
5829                     wakeUplocation = view.getTranslationY()
5830                             + view.getActualHeight() - mShelf.getIntrinsicHeight();
5831                 }
5832             } else if (!firstVisibleView) {
5833                 if (physicalNotificationMovement()) {
5834                     PhysicsPropertyAnimator.setProperty(view, PhysicsPropertyAnimator.Y_TRANSLATION,
5835                             wakeUplocation);
5836                 } else {
5837                     view.setTranslationY(wakeUplocation);
5838                 }
5839             }
5840         }
5841     }
5842 
setOnPulseHeightChangedListener(Runnable listener)5843     public void setOnPulseHeightChangedListener(Runnable listener) {
5844         mAmbientState.setOnPulseHeightChangedListener(listener);
5845     }
5846 
calculateAppearFractionBypass()5847     public float calculateAppearFractionBypass() {
5848         SceneContainerFlag.assertInLegacyMode();
5849         return calculateAppearFractionBypassInternal();
5850     }
5851 
calculateAppearFractionBypassInternal()5852     private float calculateAppearFractionBypassInternal() {
5853         float pulseHeight = getPulseHeight();
5854         // The total distance required to fully reveal the header
5855         float totalDistance = getIntrinsicPadding();
5856         return MathUtils.smoothStep(0, totalDistance, pulseHeight);
5857     }
5858 
setController( NotificationStackScrollLayoutController notificationStackScrollLayoutController)5859     public void setController(
5860             NotificationStackScrollLayoutController notificationStackScrollLayoutController) {
5861         mController = notificationStackScrollLayoutController;
5862         mController.getNotificationRoundnessManager().setAnimatedChildren(mChildrenToAddAnimated);
5863     }
5864 
setWallpaperInteractor(WallpaperInteractor wallpaperInteractor)5865     public void setWallpaperInteractor(WallpaperInteractor wallpaperInteractor) {
5866         mWallpaperInteractor = wallpaperInteractor;
5867     }
5868 
addSwipedOutView(View v)5869     void addSwipedOutView(View v) {
5870         mSwipedOutViews.add(v);
5871     }
5872 
onSwipeBegin(View viewSwiped)5873     void onSwipeBegin(View viewSwiped) {
5874         if (!(viewSwiped instanceof ExpandableNotificationRow)) {
5875             return;
5876         }
5877         mSectionsManager.updateFirstAndLastViewsForAllSections(
5878                 mSections,
5879                 getChildrenWithBackground()
5880         );
5881 
5882         if (!magneticNotificationSwipes()) {
5883             RoundableTargets targets = mController
5884                     .getNotificationTargetsHelper()
5885                     .findRoundableTargets(
5886                             (ExpandableNotificationRow) viewSwiped,
5887                             this,
5888                             mSectionsManager);
5889 
5890             mController.getNotificationRoundnessManager()
5891                     .setViewsAffectedBySwipe(
5892                             targets.getBefore(),
5893                             targets.getSwiped(),
5894                             targets.getAfter());
5895             mController.getNotificationRoundnessManager()
5896                     .setRoundnessForAffectedViews(/* roundness */ 1f);
5897         }
5898 
5899         updateFirstAndLastBackgroundViews();
5900         requestDisallowInterceptTouchEvent(true);
5901         updateContinuousShadowDrawing();
5902         requestChildrenUpdate();
5903     }
5904 
onSwipeEnd()5905     void onSwipeEnd() {
5906         updateFirstAndLastBackgroundViews();
5907         if (!magneticNotificationSwipes()) {
5908             mController.getNotificationRoundnessManager()
5909                     .setViewsAffectedBySwipe(null, null, null);
5910         }
5911         // Round bottom corners for notification right before shelf.
5912         mShelf.updateAppearance();
5913     }
5914 
5915     /**
5916      * @param topHeadsUpRow the first headsUp row in z-order.
5917      */
setTopHeadsUpRow(@ullable ExpandableNotificationRow topHeadsUpRow)5918     public void setTopHeadsUpRow(@Nullable ExpandableNotificationRow topHeadsUpRow) {
5919         mTopHeadsUpRow = topHeadsUpRow;
5920         notifyHeadsUpHeightChangedListeners();
5921     }
5922 
getIsExpanded()5923     public boolean getIsExpanded() {
5924         return mIsExpanded;
5925     }
5926 
getOnlyScrollingInThisMotion()5927     boolean getOnlyScrollingInThisMotion() {
5928         return mOnlyScrollingInThisMotion;
5929     }
5930 
getExpandHelper()5931     ExpandHelper getExpandHelper() {
5932         return mExpandHelper;
5933     }
5934 
isExpandingNotification()5935     boolean isExpandingNotification() {
5936         return mExpandingNotification;
5937     }
5938 
5939     @VisibleForTesting
setExpandingNotification(boolean isExpanding)5940     void setExpandingNotification(boolean isExpanding) {
5941         mExpandingNotification = isExpanding;
5942     }
5943 
getDisallowScrollingInThisMotion()5944     boolean getDisallowScrollingInThisMotion() {
5945         return mDisallowScrollingInThisMotion;
5946     }
5947 
isBeingDragged()5948     public boolean isBeingDragged() {
5949         return mIsBeingDragged;
5950     }
5951 
getExpandedInThisMotion()5952     boolean getExpandedInThisMotion() {
5953         return mExpandedInThisMotion;
5954     }
5955 
5956     @VisibleForTesting
setExpandedInThisMotion(boolean expandedInThisMotion)5957     void setExpandedInThisMotion(boolean expandedInThisMotion) {
5958         mExpandedInThisMotion = expandedInThisMotion;
5959     }
5960 
getDisallowDismissInThisMotion()5961     boolean getDisallowDismissInThisMotion() {
5962         return mDisallowDismissInThisMotion;
5963     }
5964 
setCheckForLeaveBehind(boolean checkForLeaveBehind)5965     void setCheckForLeaveBehind(boolean checkForLeaveBehind) {
5966         mCheckForLeavebehind = checkForLeaveBehind;
5967     }
5968 
setTouchHandler(NotificationStackScrollLayoutController.TouchHandler touchHandler)5969     void setTouchHandler(NotificationStackScrollLayoutController.TouchHandler touchHandler) {
5970         mTouchHandler = touchHandler;
5971     }
5972 
getCheckSnoozeLeaveBehind()5973     boolean getCheckSnoozeLeaveBehind() {
5974         return mCheckForLeavebehind;
5975     }
5976 
setClearAllListener(ClearAllListener listener)5977     void setClearAllListener(ClearAllListener listener) {
5978         mClearAllListener = listener;
5979     }
5980 
setClearAllAnimationListener(ClearAllAnimationListener clearAllAnimationListener)5981     void setClearAllAnimationListener(ClearAllAnimationListener clearAllAnimationListener) {
5982         mClearAllAnimationListener = clearAllAnimationListener;
5983     }
5984 
setHighPriorityBeforeSpeedBump(boolean highPriorityBeforeSpeedBump)5985     public void setHighPriorityBeforeSpeedBump(boolean highPriorityBeforeSpeedBump) {
5986         mHighPriorityBeforeSpeedBump = highPriorityBeforeSpeedBump;
5987     }
5988 
setClearAllFinishedWhilePanelExpandedRunnable(Runnable runnable)5989     void setClearAllFinishedWhilePanelExpandedRunnable(Runnable runnable) {
5990         mClearAllFinishedWhilePanelExpandedRunnable = runnable;
5991     }
5992 
5993     /**
5994      * Sets the extra top inset for the full shade transition. This moves notifications down
5995      * during the drag down.
5996      */
setExtraTopInsetForFullShadeTransition(float inset)5997     public void setExtraTopInsetForFullShadeTransition(float inset) {
5998         mExtraTopInsetForFullShadeTransition = inset;
5999         updateStackPosition();
6000         requestChildrenUpdate();
6001     }
6002 
6003     /**
6004      * @param fraction Fraction of the lockscreen to shade transition. 0f for all other states.
6005      *                 Once the lockscreen to shade transition completes and the shade is 100% open
6006      *                 LockscreenShadeTransitionController resets fraction to 0
6007      *                 where it remains until the next lockscreen-to-shade transition.
6008      */
setFractionToShade(float fraction)6009     public void setFractionToShade(float fraction) {
6010         mAmbientState.setFractionToShade(fraction);
6011         updateContentHeight();  // Recompute stack height with different section gap.
6012         requestChildrenUpdate();
6013     }
6014 
6015     /**
6016      * Set a listener to when scrolling changes.
6017      */
setOnScrollListener(Consumer<Integer> listener)6018     public void setOnScrollListener(Consumer<Integer> listener) {
6019         SceneContainerFlag.assertInLegacyMode();
6020         mScrollListener = listener;
6021     }
6022 
6023     /** Sets rounded rect where the view is allowed to draw. */
6024     @Override
setClippingShape(@ullable ShadeScrimShape shape)6025     public void setClippingShape(@Nullable ShadeScrimShape shape) {
6026         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
6027         if (Objects.equals(mScrollViewFields.getClippingShape(), shape)) return;
6028         mScrollViewFields.setClippingShape(shape);
6029         mShouldUseRoundedRectClipping = shape != null;
6030         mRoundedClipPath.reset();
6031         if (shape != null) {
6032             ShadeScrimBounds bounds = shape.getBounds();
6033             mRoundedRectClippingLeft = (int) bounds.getLeft();
6034             mRoundedRectClippingTop = (int) bounds.getTop();
6035             mRoundedRectClippingRight = (int) bounds.getRight();
6036             mRoundedRectClippingBottom = (int) bounds.getBottom();
6037             mRoundedClipCornerRadii[0] = shape.getTopRadius();
6038             mRoundedClipCornerRadii[1] = shape.getTopRadius();
6039             mRoundedClipCornerRadii[2] = shape.getTopRadius();
6040             mRoundedClipCornerRadii[3] = shape.getTopRadius();
6041             mRoundedClipCornerRadii[4] = shape.getBottomRadius();
6042             mRoundedClipCornerRadii[5] = shape.getBottomRadius();
6043             mRoundedClipCornerRadii[6] = shape.getBottomRadius();
6044             mRoundedClipCornerRadii[7] = shape.getBottomRadius();
6045             mRoundedClipPath.addRoundRect(
6046                     bounds.getLeft(), bounds.getTop(), bounds.getRight(), bounds.getBottom(),
6047                     mRoundedClipCornerRadii, Path.Direction.CW);
6048         }
6049         invalidate();
6050     }
6051 
6052     @Override
setNegativeClippingShape(@ullable ShadeScrimShape shape)6053     public void setNegativeClippingShape(@Nullable ShadeScrimShape shape) {
6054         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
6055         if (Objects.equals(mScrollViewFields.getNegativeClippingShape(), shape)) return;
6056 
6057         mScrollViewFields.setNegativeClippingShape(shape);
6058         mShouldUseNegativeRoundedRectClipping = shape != null;
6059         mNegativeRoundedClipPath.reset();
6060         if (shape != null) {
6061             ShadeScrimBounds bounds = shape.getBounds();
6062             float bottomRadius = shape.getBottomRadius();
6063             mNegativeRoundedClipPath.addRoundRect(
6064                     bounds.getLeft(), bounds.getTop(), bounds.getRight(), bounds.getBottom(),
6065                     new float[]{0, 0, 0, 0, bottomRadius, bottomRadius, bottomRadius, bottomRadius},
6066                     Path.Direction.CW);
6067         }
6068         invalidate();
6069     }
6070 
6071     @Override
setBlurRadius(float blurRadius)6072     public void setBlurRadius(float blurRadius) {
6073         if (mBlurRadius != blurRadius) {
6074             mBlurRadius = blurRadius;
6075             updateBlurEffect();
6076             invalidate();
6077         }
6078     }
6079 
updateBlurEffect()6080     private void updateBlurEffect() {
6081         if (mBlurRadius > 0) {
6082             mBlurEffect =
6083                     RenderEffect.createBlurEffect(mBlurRadius, mBlurRadius, Shader.TileMode.CLAMP);
6084             spewLog("Setting up blur RenderEffect for NotificationStackScrollLayout");
6085         } else {
6086             spewLog("Clearing the blur RenderEffect setup for NotificationStackScrollLayout");
6087             mBlurEffect = null;
6088         }
6089     }
6090 
6091     /**
6092      * Set rounded rect clipping bounds on this view.
6093      */
setRoundedClippingBounds(int left, int top, int right, int bottom, int topRadius, int bottomRadius)6094     public void setRoundedClippingBounds(int left, int top, int right, int bottom, int topRadius,
6095                                          int bottomRadius) {
6096         SceneContainerFlag.assertInLegacyMode();
6097         if (mRoundedRectClippingLeft == left && mRoundedRectClippingRight == right
6098                 && mRoundedRectClippingBottom == bottom && mRoundedRectClippingTop == top
6099                 && mRoundedClipCornerRadii[0] == topRadius
6100                 && mRoundedClipCornerRadii[5] == bottomRadius) {
6101             return;
6102         }
6103         mRoundedRectClippingLeft = left;
6104         mRoundedRectClippingTop = top;
6105         mRoundedRectClippingBottom = bottom;
6106         mRoundedRectClippingRight = right;
6107         mRoundedClipCornerRadii[0] = topRadius;
6108         mRoundedClipCornerRadii[1] = topRadius;
6109         mRoundedClipCornerRadii[2] = topRadius;
6110         mRoundedClipCornerRadii[3] = topRadius;
6111         mRoundedClipCornerRadii[4] = bottomRadius;
6112         mRoundedClipCornerRadii[5] = bottomRadius;
6113         mRoundedClipCornerRadii[6] = bottomRadius;
6114         mRoundedClipCornerRadii[7] = bottomRadius;
6115         updateRoundedClipPath();
6116     }
6117 
6118     // see b/288553572
setRoundingClippingYTranslation(int yTranslation)6119     private void setRoundingClippingYTranslation(int yTranslation) {
6120         SceneContainerFlag.assertInLegacyMode();
6121         if (mRoundedRectClippingYTranslation == yTranslation) {
6122             return;
6123         }
6124         mRoundedRectClippingYTranslation = yTranslation;
6125         updateRoundedClipPath();
6126     }
6127 
updateRoundedClipPath()6128     private void updateRoundedClipPath() {
6129         SceneContainerFlag.assertInLegacyMode();
6130         mRoundedClipPath.reset();
6131         mRoundedClipPath.addRoundRect(
6132                 mRoundedRectClippingLeft,
6133                 mRoundedRectClippingTop + mRoundedRectClippingYTranslation,
6134                 mRoundedRectClippingRight,
6135                 mRoundedRectClippingBottom + mRoundedRectClippingYTranslation,
6136                 mRoundedClipCornerRadii, Path.Direction.CW);
6137         if (mShouldUseRoundedRectClipping) {
6138             invalidate();
6139         }
6140     }
6141 
6142     @VisibleForTesting
updateSplitNotificationShade()6143     void updateSplitNotificationShade() {
6144         boolean split = mSplitShadeStateController.shouldUseSplitNotificationShade(getResources());
6145         if (split != mShouldUseSplitNotificationShade) {
6146             mShouldUseSplitNotificationShade = split;
6147             mShouldSkipTopPaddingAnimationAfterFold = true;
6148             mAmbientState.setUseSplitShade(split);
6149             updateDismissBehavior();
6150             updateUseRoundedRectClipping();
6151             requestLayout();
6152         }
6153     }
6154 
updateDismissBehavior()6155     private void updateDismissBehavior() {
6156         // On the split keyguard, dismissing with clipping without a visual boundary looks odd,
6157         // so let's use the content dismiss behavior instead.
6158         boolean dismissUsingRowTranslationX = !mShouldUseSplitNotificationShade
6159                 || (mStatusBarState != StatusBarState.KEYGUARD && mIsExpanded);
6160         if (mDismissUsingRowTranslationX != dismissUsingRowTranslationX) {
6161             mDismissUsingRowTranslationX = dismissUsingRowTranslationX;
6162             for (int i = 0; i < getChildCount(); i++) {
6163                 View child = getChildAt(i);
6164                 if (child instanceof ExpandableNotificationRow) {
6165                     ((ExpandableNotificationRow) child).setDismissUsingRowTranslationX(
6166                             dismissUsingRowTranslationX, /* force= */ false);
6167                 }
6168             }
6169         }
6170     }
6171 
6172     /**
6173      * Set if we're launching a notification right now.
6174      */
setLaunchingNotification(boolean launching)6175     private void setLaunchingNotification(boolean launching) {
6176         if (launching == mLaunchingNotification) {
6177             return;
6178         }
6179         mLaunchingNotification = launching;
6180         mLaunchingNotificationNeedsToBeClipped = mLaunchAnimationParams != null
6181                 && (mLaunchAnimationParams.getStartRoundedTopClipping() > 0
6182                 || mLaunchAnimationParams.getParentStartRoundedTopClipping() > 0);
6183         if (!mLaunchingNotificationNeedsToBeClipped || !mLaunchingNotification) {
6184             mLaunchedNotificationClipPath.reset();
6185         }
6186         // When launching notifications, we're clipping the children individually instead of in
6187         // dispatchDraw
6188         invalidate();
6189     }
6190 
6191     /**
6192      * Should we use rounded rect clipping
6193      */
updateUseRoundedRectClipping()6194     private void updateUseRoundedRectClipping() {
6195         if (SceneContainerFlag.isEnabled()) return;
6196         // We don't want to clip notifications when QS is expanded, because incoming heads up on
6197         // the bottom would be clipped otherwise
6198         boolean qsAllowsClipping =
6199                 getQsExpansionFraction() < 0.5f || mShouldUseSplitNotificationShade;
6200         boolean clip = mIsExpanded && qsAllowsClipping;
6201         if (clip != mShouldUseRoundedRectClipping) {
6202             mShouldUseRoundedRectClipping = clip;
6203             invalidate();
6204         }
6205     }
6206 
6207     /**
6208      * Update the clip path for launched notifications in case they were originally clipped
6209      */
6210     private void updateLaunchedNotificationClipPath() {
6211         if (!mLaunchingNotificationNeedsToBeClipped || !mLaunchingNotification
6212                 || mExpandingNotificationRow == null) {
6213             return;
6214         }
6215         int[] absoluteCoords = new int[2];
6216         getLocationOnScreen(absoluteCoords);
6217 
6218         int left = Math.min(mLaunchAnimationParams.getLeft() - absoluteCoords[0],
6219                 mRoundedRectClippingLeft);
6220         int right = Math.max(mLaunchAnimationParams.getRight() - absoluteCoords[0],
6221                 mRoundedRectClippingRight);
6222         int bottom = Math.max(mLaunchAnimationParams.getBottom() - absoluteCoords[1],
6223                 mRoundedRectClippingBottom);
6224         float expandProgress = Interpolators.FAST_OUT_SLOW_IN.getInterpolation(
6225                 mLaunchAnimationParams.getProgress(0,
6226                         NotificationTransitionAnimatorController.ANIMATION_DURATION_TOP_ROUNDING));
6227         int top = (int) Math.min(MathUtils.lerp(mRoundedRectClippingTop,
6228                         mLaunchAnimationParams.getTop() - absoluteCoords[1], expandProgress),
6229                 mRoundedRectClippingTop);
6230         float topRadius = mLaunchAnimationParams.getTopCornerRadius();
6231         float bottomRadius = mLaunchAnimationParams.getBottomCornerRadius();
6232         mLaunchedNotificationRadii[0] = topRadius;
6233         mLaunchedNotificationRadii[1] = topRadius;
6234         mLaunchedNotificationRadii[2] = topRadius;
6235         mLaunchedNotificationRadii[3] = topRadius;
6236         mLaunchedNotificationRadii[4] = bottomRadius;
6237         mLaunchedNotificationRadii[5] = bottomRadius;
6238         mLaunchedNotificationRadii[6] = bottomRadius;
6239         mLaunchedNotificationRadii[7] = bottomRadius;
6240         mLaunchedNotificationClipPath.reset();
6241         mLaunchedNotificationClipPath.addRoundRect(left, top, right, bottom,
6242                 mLaunchedNotificationRadii, Path.Direction.CW);
6243         // Offset into notification clip coordinates instead of parent ones.
6244         // This is needed since the notification changes in translationZ, where clipping via
6245         // canvas dispatching won't work.
6246         ExpandableNotificationRow expandingRow = mExpandingNotificationRow;
6247         if (expandingRow.getNotificationParent() != null) {
6248             expandingRow = expandingRow.getNotificationParent();
6249         }
6250         mLaunchedNotificationClipPath.offset(
6251                 -expandingRow.getLeft() - expandingRow.getTranslationX(),
6252                 -expandingRow.getTop() - expandingRow.getTranslationY());
6253         expandingRow.setExpandingClipPath(mLaunchedNotificationClipPath);
6254         if (mShouldUseRoundedRectClipping) {
6255             invalidate();
6256         }
6257     }
6258 
6259     @Override
6260     protected void dispatchDraw(@NonNull Canvas canvas) {
6261         if (mBlurEffect != null) {
6262             spewLog("Applying blur RenderEffect to NotificationStackScrollLayout");
6263             // reuse the cached RenderNode to blur
6264             mBlurNode.setPosition(0, 0, canvas.getWidth(), canvas.getHeight());
6265             mBlurNode.setRenderEffect(mBlurEffect);
6266             Canvas blurCanvas = mBlurNode.beginRecording();
6267             // draw all the children (except HUNs) on the blurred canvas
6268             super.dispatchDraw(blurCanvas);
6269             mBlurNode.endRecording();
6270             // apply clipping to the canvas
6271             int saveCount = canvas.save();
6272             applyClipToCanvas(canvas);
6273             // draw the blurred content to the clipped canvas
6274             canvas.drawRenderNode(mBlurNode);
6275             // restore the canvas, so it doesn't clip anymore
6276             canvas.restoreToCount(saveCount);
6277             // draw the children that were left out during the dispatchDraw phase
6278             for (int i = 0; i < getChildCount(); i++) {
6279                 // TODO(b/388469101) draw these children in z-order
6280                 ExpandableView child = getChildAtIndex(i);
6281                 if (shouldSkipBlurForChild(child)) {
6282                     super.drawChild(canvas, child, getDrawingTime());
6283                 }
6284             }
6285         } else {
6286             if (!mLaunchingNotification) {
6287                 // When launching notifications, we're clipping the children individually instead
6288                 //  of in dispatchDraw
6289                 applyClipToCanvas(canvas);
6290             }
6291             super.dispatchDraw(canvas);
6292         }
6293     }
6294 
6295     private void applyClipToCanvas(Canvas canvas) {
6296         if (mShouldUseRoundedRectClipping) {
6297             // clip by the positive path if it is defined
6298             canvas.clipPath(mRoundedClipPath);
6299         }
6300         if (mShouldUseNegativeRoundedRectClipping) {
6301             // subtract the negative path if it is defined
6302             canvas.clipOutPath(mNegativeRoundedClipPath);
6303         }
6304     }
6305 
6306     @Override
6307     protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
6308         boolean shouldUseClipping =
6309                 mShouldUseRoundedRectClipping || mShouldUseNegativeRoundedRectClipping;
6310         if (mBlurEffect != null) {
6311             if (shouldSkipBlurForChild(child)) {
6312                 // skip drawing this child during the regular dispatchDraw pass
6313                 return false;
6314             } else {
6315                 // draw the child as if nothing happened, non-blurred elements shouldn't be
6316                 // affected by clipping either
6317                 return super.drawChild(canvas, child, drawingTime);
6318             }
6319         } else if (mLaunchingNotification && shouldUseClipping) {
6320             // Let's clip children individually during notification launch
6321             canvas.save();
6322             ExpandableView expandableView = (ExpandableView) child;
6323             Path clipPath;
6324             Path clipOutPath;
6325             if (expandableView.isExpandAnimationRunning()
6326                     || ((ExpandableView) child).hasExpandingChild()) {
6327                 // When launching the notification, it is not clipped by this layout, but by the
6328                 // view itself. This is because the view is Translating in Z, where this clipPath
6329                 // wouldn't apply.
6330                 clipPath = null;
6331                 clipOutPath = null;
6332             } else {
6333                 clipPath = mRoundedClipPath;
6334                 clipOutPath = mNegativeRoundedClipPath;
6335             }
6336             if (mShouldUseRoundedRectClipping && clipPath != null) {
6337                 canvas.clipPath(clipPath);
6338             }
6339             if (mShouldUseNegativeRoundedRectClipping && clipOutPath != null) {
6340                 canvas.clipOutPath(clipOutPath);
6341             }
6342             boolean result = super.drawChild(canvas, child, drawingTime);
6343             canvas.restore();
6344             return result;
6345         } else {
6346             return super.drawChild(canvas, child, drawingTime);
6347         }
6348     }
6349 
6350     private boolean shouldSkipBlurForChild(View child) {
6351         if (child instanceof ExpandableView row) {
6352             return row.isHeadsUpState();
6353         } else {
6354             return false;
6355         }
6356     }
6357 
6358     /**
6359      * Calculate the total translation needed when dismissing.
6360      */
6361     public float getTotalTranslationLength(View animView) {
6362         if (!mDismissUsingRowTranslationX) {
6363             return animView.getMeasuredWidth();
6364         }
6365         float notificationWidth = animView.getMeasuredWidth();
6366         int containerWidth = getMeasuredWidth();
6367         float padding = (containerWidth - notificationWidth) / 2.0f;
6368         return containerWidth - padding;
6369     }
6370 
6371     /**
6372      * @return the start location where we start clipping notifications.
6373      */
6374     public int getTopClippingStartLocation() {
6375         return mIsExpanded ? mQsScrollBoundaryPosition : 0;
6376     }
6377 
6378     /**
6379      * Request an animation whenever the toppadding changes next
6380      */
6381     public void animateNextTopPaddingChange() {
6382         mAnimateNextTopPaddingChange = true;
6383     }
6384 
6385     /**
6386      * Sets a {@link StackStateLogger} which is notified as the {@link StackStateAnimator} updates
6387      * the views.
6388      */
6389     protected void setStackStateLogger(StackStateLogger logger) {
6390         mStateAnimator.setLogger(logger);
6391     }
6392 
6393     /**
6394      * A listener that is notified when the empty space below the notifications is clicked on
6395      */
6396     public interface OnEmptySpaceClickListener {
6397         void onEmptySpaceClicked(float x, float y);
6398     }
6399 
6400     /**
6401      * A listener that gets notified when the overscroll at the top has changed.
6402      */
6403     public interface OnOverscrollTopChangedListener {
6404 
6405         /**
6406          * Notifies a listener that the overscroll has changed.
6407          *
6408          * @param amount         the amount of overscroll, in pixels
6409          * @param isRubberbanded if true, this is a rubberbanded overscroll; if false, this is an
6410          *                       unrubberbanded motion to directly expand overscroll view (e.g
6411          *                       expand
6412          *                       QS)
6413          */
6414         void onOverscrollTopChanged(float amount, boolean isRubberbanded);
6415 
6416         /**
6417          * Notify a listener that the scroller wants to escape from the scrolling motion and
6418          * start a fling animation to the expanded or collapsed overscroll view (e.g expand the QS)
6419          *
6420          * @param velocity The velocity that the Scroller had when over flinging
6421          * @param open     Should the fling open or close the overscroll view.
6422          */
6423         void flingTopOverscroll(float velocity, boolean open);
6424     }
6425 
6426     /**
6427      * A listener that is notified when some ExpandableNotificationRow locations might have changed.
6428      */
6429     public interface OnNotificationLocationsChangedListener {
6430         /**
6431          * Called when the location of ExpandableNotificationRows might have changed.
6432          *
6433          * @param locations mapping of Notification keys to locations.
6434          */
6435         void onChildLocationsChanged(Callable<Map<String, Integer>> locations);
6436     }
6437 
6438     private void updateSpeedBumpIndex() {
6439         mSpeedBumpIndexDirty = true;
6440     }
6441 
6442     private void resetAllSwipeState() {
6443         Trace.beginSection("NSSL.resetAllSwipeState()");
6444         mSwipeHelper.resetTouchState();
6445         for (int i = 0; i < getChildCount(); i++) {
6446             View child = getChildAt(i);
6447             mSwipeHelper.forceResetSwipeState(child);
6448             if (child instanceof ExpandableNotificationRow childRow) {
6449                 List<ExpandableNotificationRow> grandchildren = childRow.getAttachedChildren();
6450                 if (grandchildren != null) {
6451                     for (ExpandableNotificationRow grandchild : grandchildren) {
6452                         mSwipeHelper.forceResetSwipeState(grandchild);
6453                     }
6454                 }
6455             }
6456         }
6457         updateContinuousShadowDrawing();
6458         Trace.endSection();
6459     }
6460 
6461     void updateContinuousShadowDrawing() {
6462         boolean continuousShadowUpdate = mAnimationRunning
6463                 || mSwipeHelper.isSwiping();
6464         if (continuousShadowUpdate != mContinuousShadowUpdate) {
6465             if (continuousShadowUpdate) {
6466                 getViewTreeObserver().addOnPreDrawListener(mShadowUpdater);
6467             } else {
6468                 getViewTreeObserver().removeOnPreDrawListener(mShadowUpdater);
6469             }
6470             mContinuousShadowUpdate = continuousShadowUpdate;
6471         }
6472     }
6473 
6474     private void resetExposedMenuView(boolean animate, boolean force) {
6475         mSwipeHelper.resetExposedMenuView(animate, force);
6476     }
6477 
6478     static boolean matchesSelection(
6479             ExpandableNotificationRow row,
6480             @SelectedRows int selection) {
6481         int bucket = NotificationBundleUi.isEnabled()
6482                 ? row.getEntryAdapter().getSectionBucket()
6483                 : row.getEntryLegacy().getBucket();
6484         switch (selection) {
6485             case ROWS_ALL:
6486                 return true;
6487             case ROWS_HIGH_PRIORITY:
6488                 return bucket < BUCKET_SILENT;
6489             case ROWS_GENTLE:
6490                 return bucket == BUCKET_SILENT;
6491             default:
6492                 throw new IllegalArgumentException("Unknown selection: " + selection);
6493         }
6494     }
6495 
6496     static class AnimationEvent {
6497 
6498         static AnimationFilter[] FILTERS = new AnimationFilter[]{
6499 
6500                 // ANIMATION_TYPE_ADD
6501                 physicalNotificationMovement()
6502                         ? new AnimationFilter()
6503                                 .animateAlpha()
6504                                 .animateHeight()
6505                                 .animateTopInset()
6506                                 .animateY()
6507                                 .animateZ()
6508                         : new AnimationFilter()
6509                                 .animateAlpha()
6510                                 .animateHeight()
6511                                 .animateTopInset()
6512                                 .animateY()
6513                                 .animateZ()
6514                                 .hasDelays(),
6515 
6516                 // ANIMATION_TYPE_REMOVE
6517                 physicalNotificationMovement()
6518                         ? new AnimationFilter()
6519                                 .animateAlpha()
6520                                 .animateHeight()
6521                                 .animateTopInset()
6522                                 .animateY()
6523                                 .animateZ()
6524                         : new AnimationFilter()
6525                                 .animateAlpha()
6526                                 .animateHeight()
6527                                 .animateTopInset()
6528                                 .animateY()
6529                                 .animateZ()
6530                                 .hasDelays(),
6531 
6532                 // ANIMATION_TYPE_REMOVE_SWIPED_OUT
6533                 physicalNotificationMovement()
6534                         ? new AnimationFilter()
6535                                 .animateHeight()
6536                                 .animateTopInset()
6537                                 .animateY()
6538                                 .animateZ()
6539                         : new AnimationFilter()
6540                                 .animateHeight()
6541                                 .animateTopInset()
6542                                 .animateY()
6543                                 .animateZ()
6544                                 .hasDelays(),
6545 
6546                 // ANIMATION_TYPE_TOP_PADDING_CHANGED
6547                 new AnimationFilter()
6548                         .animateHeight()
6549                         .animateTopInset()
6550                         .animateY()
6551                         .animateZ(),
6552 
6553                 // ANIMATION_TYPE_ACTIVATED_CHILD
6554                 new AnimationFilter()
6555                         .animateZ(),
6556 
6557                 // ANIMATION_TYPE_DIMMED
6558                 new AnimationFilter(),
6559 
6560                 // ANIMATION_TYPE_CHANGE_POSITION
6561                 new AnimationFilter()
6562                         .animateAlpha() // maybe the children change positions
6563                         .animateHeight()
6564                         .animateTopInset()
6565                         .animateY()
6566                         .animateZ(),
6567 
6568                 // ANIMATION_TYPE_GO_TO_FULL_SHADE
6569                 new AnimationFilter()
6570                         .animateHeight()
6571                         .animateTopInset()
6572                         .animateY()
6573                         .animateZ()
6574                         .hasDelays(),
6575 
6576                 // ANIMATION_TYPE_HIDE_SENSITIVE
6577                 new AnimationFilter()
6578                         .animateHideSensitive(),
6579 
6580                 // ANIMATION_TYPE_VIEW_RESIZE
6581                 new AnimationFilter()
6582                         .animateHeight()
6583                         .animateTopInset()
6584                         .animateY()
6585                         .animateZ(),
6586 
6587                 // ANIMATION_TYPE_GROUP_EXPANSION_CHANGED
6588                 new AnimationFilter()
6589                         .animateAlpha()
6590                         .animateHeight()
6591                         .animateTopInset()
6592                         .animateY()
6593                         .animateZ(),
6594 
6595                 // ANIMATION_TYPE_HEADS_UP_APPEAR
6596                 new AnimationFilter()
6597                         .animateHeight()
6598                         .animateTopInset()
6599                         .animateY()
6600                         .animateZ(),
6601 
6602                 // ANIMATION_TYPE_HEADS_UP_DISAPPEAR
6603                 new AnimationFilter()
6604                         .animateHeight()
6605                         .animateTopInset()
6606                         .animateY()
6607                         .animateZ()
6608                         .hasDelays(),
6609 
6610                 // ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK
6611                 new AnimationFilter()
6612                         .animateHeight()
6613                         .animateTopInset()
6614                         .animateY()
6615                         .animateZ()
6616                         .hasDelays(),
6617 
6618                 // ANIMATION_TYPE_HEADS_UP_OTHER
6619                 new AnimationFilter()
6620                         .animateHeight()
6621                         .animateTopInset()
6622                         .animateY()
6623                         .animateZ(),
6624 
6625                 // ANIMATION_TYPE_EVERYTHING
6626                 new AnimationFilter()
6627                         .animateAlpha()
6628                         .animateHideSensitive()
6629                         .animateHeight()
6630                         .animateTopInset()
6631                         .animateY()
6632                         .animateZ(),
6633 
6634                 // ANIMATION_TYPE_HEADS_UP_CYCLING_OUT
6635                 new AnimationFilter()
6636                         .animateHeight()
6637                         .animateTopInset()
6638                         .animateY()
6639                         .animateZ()
6640                         .hasDelays(),
6641 
6642                 // ANIMATION_TYPE_HEADS_UP_CYCLING_IN
6643                 new AnimationFilter()
6644                         .animateHeight()
6645                         .animateTopInset()
6646                         .animateY()
6647                         .animateZ()
6648                         .hasDelays(),
6649         };
6650 
6651         static int[] LENGTHS = new int[]{
6652 
6653                 // ANIMATION_TYPE_ADD
6654                 StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR,
6655 
6656                 // ANIMATION_TYPE_REMOVE
6657                 StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR,
6658 
6659                 // ANIMATION_TYPE_REMOVE_SWIPED_OUT
6660                 StackStateAnimator.ANIMATION_DURATION_STANDARD,
6661 
6662                 // ANIMATION_TYPE_TOP_PADDING_CHANGED
6663                 StackStateAnimator.ANIMATION_DURATION_STANDARD,
6664 
6665                 // ANIMATION_TYPE_ACTIVATED_CHILD
6666                 StackStateAnimator.ANIMATION_DURATION_DIMMED_ACTIVATED,
6667 
6668                 // ANIMATION_TYPE_DIMMED
6669                 StackStateAnimator.ANIMATION_DURATION_DIMMED_ACTIVATED,
6670 
6671                 // ANIMATION_TYPE_CHANGE_POSITION
6672                 StackStateAnimator.ANIMATION_DURATION_STANDARD,
6673 
6674                 // ANIMATION_TYPE_GO_TO_FULL_SHADE
6675                 StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE,
6676 
6677                 // ANIMATION_TYPE_HIDE_SENSITIVE
6678                 StackStateAnimator.ANIMATION_DURATION_STANDARD,
6679 
6680                 // ANIMATION_TYPE_VIEW_RESIZE
6681                 StackStateAnimator.ANIMATION_DURATION_STANDARD,
6682 
6683                 // ANIMATION_TYPE_GROUP_EXPANSION_CHANGED
6684                 StackStateAnimator.ANIMATION_DURATION_STANDARD,
6685 
6686                 // ANIMATION_TYPE_HEADS_UP_APPEAR
6687                 StackStateAnimator.ANIMATION_DURATION_HEADS_UP_APPEAR,
6688 
6689                 // ANIMATION_TYPE_HEADS_UP_DISAPPEAR
6690                 StackStateAnimator.ANIMATION_DURATION_HEADS_UP_DISAPPEAR,
6691 
6692                 // ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK
6693                 StackStateAnimator.ANIMATION_DURATION_HEADS_UP_DISAPPEAR,
6694 
6695                 // ANIMATION_TYPE_HEADS_UP_OTHER
6696                 StackStateAnimator.ANIMATION_DURATION_STANDARD,
6697 
6698                 // ANIMATION_TYPE_EVERYTHING
6699                 StackStateAnimator.ANIMATION_DURATION_STANDARD,
6700 
6701                 // ANIMATION_TYPE_HEADS_UP_CYCLING_OUT
6702                 StackStateAnimator.ANIMATION_DURATION_HEADS_UP_CYCLING,
6703 
6704                 // ANIMATION_TYPE_HEADS_UP_CYCLING_IN
6705                 StackStateAnimator.ANIMATION_DURATION_HEADS_UP_CYCLING,
6706         };
6707 
6708         static final int ANIMATION_TYPE_ADD = 0;
6709         static final int ANIMATION_TYPE_REMOVE = 1;
6710         static final int ANIMATION_TYPE_REMOVE_SWIPED_OUT = 2;
6711         static final int ANIMATION_TYPE_TOP_PADDING_CHANGED = 3;
6712         static final int ANIMATION_TYPE_ACTIVATED_CHILD = 4;
6713         static final int ANIMATION_TYPE_DIMMED = 5;
6714         static final int ANIMATION_TYPE_CHANGE_POSITION = 6;
6715         static final int ANIMATION_TYPE_GO_TO_FULL_SHADE = 7;
6716         static final int ANIMATION_TYPE_HIDE_SENSITIVE = 8;
6717         static final int ANIMATION_TYPE_VIEW_RESIZE = 9;
6718         static final int ANIMATION_TYPE_GROUP_EXPANSION_CHANGED = 10;
6719         static final int ANIMATION_TYPE_HEADS_UP_APPEAR = 11;
6720         static final int ANIMATION_TYPE_HEADS_UP_DISAPPEAR = 12;
6721         static final int ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK = 13;
6722         static final int ANIMATION_TYPE_HEADS_UP_OTHER = 14;
6723         static final int ANIMATION_TYPE_EVERYTHING = 15;
6724         static final int ANIMATION_TYPE_HEADS_UP_CYCLING_OUT = 16;
6725         static final int ANIMATION_TYPE_HEADS_UP_CYCLING_IN = 17;
6726 
6727         final long eventStartTime;
6728         final ExpandableView mChangingView;
6729         final int animationType;
6730         final AnimationFilter filter;
6731         final long length;
6732         View viewAfterChangingView;
6733         boolean headsUpFromBottom;
6734         boolean headsUpHasStatusBarChip;
6735 
6736         AnimationEvent(ExpandableView view, int type) {
6737             this(view, type, LENGTHS[type]);
6738         }
6739 
6740         AnimationEvent(ExpandableView view, int type, AnimationFilter filter) {
6741             this(view, type, LENGTHS[type], filter);
6742         }
6743 
6744         AnimationEvent(ExpandableView view, int type, long length) {
6745             this(view, type, length, FILTERS[type]);
6746         }
6747 
6748         AnimationEvent(ExpandableView view, int type, long length, AnimationFilter filter) {
6749             eventStartTime = AnimationUtils.currentAnimationTimeMillis();
6750             mChangingView = view;
6751             animationType = type;
6752             this.length = length;
6753             this.filter = filter;
6754         }
6755 
6756         /**
6757          * Combines the length of several animation events into a single value.
6758          *
6759          * @param events The events of the lengths to combine.
6760          * @return The combined length. Depending on the event types, this might be the maximum of
6761          * all events or the length of a specific event.
6762          */
6763         static long combineLength(ArrayList<AnimationEvent> events) {
6764             long length = 0;
6765             int size = events.size();
6766             for (int i = 0; i < size; i++) {
6767                 AnimationEvent event = events.get(i);
6768                 length = Math.max(length, event.length);
6769                 if (event.animationType == ANIMATION_TYPE_GO_TO_FULL_SHADE) {
6770                     return event.length;
6771                 }
6772             }
6773             return length;
6774         }
6775     }
6776 
6777     static boolean canChildBeDismissed(View v) {
6778         if (v instanceof ExpandableNotificationRow row) {
6779             return row.canExpandableViewBeDismissed();
6780         }
6781         return false;
6782     }
6783 
6784     static boolean canChildBeCleared(View v) {
6785         if (v instanceof ExpandableNotificationRow row) {
6786             if (row.areGutsExposed() || !row.hasFinishedInitialization()) {
6787                 return false;
6788             }
6789             return row.canViewBeCleared();
6790         }
6791         return false;
6792     }
6793 
6794     // --------------------- NotificationEntryManager/NotifPipeline methods ------------------------
6795 
6796     void onEntryUpdated(NotificationEntry entry) {
6797         // If the row already exists, the user may have performed a dismiss action on the
6798         // notification. Since it's not clearable we should snap it back.
6799         if (entry.rowExists() && !entry.getSbn().isClearable()) {
6800             snapViewIfNeeded(entry);
6801         }
6802     }
6803 
6804     /**
6805      * Called after the animations for a "clear all notifications" action has ended.
6806      */
6807     private void onClearAllAnimationsEnd(
6808             List<ExpandableNotificationRow> viewsToRemove,
6809             @SelectedRows int selectedRows) {
6810         InteractionJankMonitor.getInstance().end(CUJ_SHADE_CLEAR_ALL);
6811         if (mClearAllAnimationListener != null) {
6812             mClearAllAnimationListener.onAnimationEnd(viewsToRemove, selectedRows);
6813         }
6814     }
6815 
6816     void resetCheckSnoozeLeavebehind() {
6817         setCheckForLeaveBehind(true);
6818     }
6819 
6820     private final HeadsUpTouchHelper.Callback mHeadsUpCallback = new HeadsUpTouchHelper.Callback() {
6821         @Override
6822         public ExpandableView getChildAtRawPosition(float touchX, float touchY) {
6823             return NotificationStackScrollLayout.this.getChildAtRawPosition(touchX, touchY);
6824         }
6825 
6826         @Override
6827         public boolean isExpanded() {
6828             return mIsExpanded;
6829         }
6830 
6831         @Override
6832         public Context getContext() {
6833             return mContext;
6834         }
6835     };
6836 
6837     public HeadsUpTouchHelper.Callback getHeadsUpCallback() {
6838         return mHeadsUpCallback;
6839     }
6840 
6841     void onGroupExpandChanged(ExpandableNotificationRow changedRow, boolean expanded) {
6842         boolean animated = mAnimationsEnabled && (mIsExpanded || changedRow.isPinned());
6843         if (animated) {
6844             mExpandedGroupView = changedRow;
6845             mNeedsAnimation = true;
6846         }
6847 
6848         changedRow.setChildrenExpanded(expanded);
6849         onChildHeightChanged(changedRow, false /* needsAnimation */);
6850         updateGroupHeaderAlignment(changedRow, expanded);
6851 
6852         runAfterAnimationFinished(changedRow::onFinishedExpansionChange);
6853     }
6854 
6855     private void updateGroupHeaderAlignment(ExpandableNotificationRow row, boolean expanded) {
6856         if (!notificationsRedesignTemplates()) {
6857             return;
6858         }
6859 
6860         NotificationChildrenContainer childrenContainer = row.getChildrenContainer();
6861         if (childrenContainer == null) {
6862             Log.wtf(TAG, "Tried to update group header alignment for something that's "
6863                     + "not a group; key = " + row.getKey());
6864             return;
6865         }
6866         NotificationHeaderView header = childrenContainer.getGroupHeader();
6867         if (header != null) {
6868             resetYTranslation(header.getTopLineView());
6869             resetYTranslation(header.getExpandButton());
6870             header.centerTopLine(expanded);
6871         }
6872     }
6873 
6874     /**
6875      * Reset the y translation of the {@code view} via the {@link ViewState}, to ensure that the
6876      * animation state is updated correctly.
6877      */
6878     private static void resetYTranslation(View view) {
6879         ViewState viewState = new ViewState();
6880         viewState.initFrom(view);
6881         viewState.setYTranslation(0);
6882         viewState.applyToView(view);
6883     }
6884 
6885     private final ExpandHelper.Callback mExpandHelperCallback = new ExpandHelper.Callback() {
6886         @Override
6887         public ExpandableView getChildAtPosition(float touchX, float touchY) {
6888             return NotificationStackScrollLayout.this.getChildAtPosition(touchX, touchY);
6889         }
6890 
6891         @Override
6892         public ExpandableView getChildAtRawPosition(float touchX, float touchY) {
6893             return NotificationStackScrollLayout.this.getChildAtRawPosition(touchX, touchY);
6894         }
6895 
6896         @Override
6897         public boolean canChildBeExpanded(View v) {
6898             return v instanceof ExpandableNotificationRow
6899                     && ((ExpandableNotificationRow) v).isExpandable()
6900                     && !((ExpandableNotificationRow) v).areGutsExposed()
6901                     && (mIsExpanded || !((ExpandableNotificationRow) v).isPinned());
6902         }
6903 
6904         /* Only ever called as a consequence of an expansion gesture in the shade. */
6905         @Override
6906         public void setUserExpandedChild(View v, boolean userExpanded) {
6907             if (v instanceof ExpandableNotificationRow row) {
6908                 if (userExpanded && onKeyguard()) {
6909                     // Due to a race when locking the screen while touching, a notification may be
6910                     // expanded even after we went back to keyguard. An example of this happens if
6911                     // you click in the empty space while expanding a group.
6912 
6913                     // We also need to un-user lock it here, since otherwise the content height
6914                     // calculated might be wrong. We also can't invert the two calls since
6915                     // un-userlocking it will trigger a layout switch in the content view.
6916                     row.setUserLocked(false);
6917                     updateContentHeight();
6918                     notifyHeightChangeListener(row);
6919                     return;
6920                 }
6921                 row.setUserExpanded(userExpanded, true /* allowChildrenExpansion */);
6922                 row.onExpandedByGesture(userExpanded);
6923             }
6924         }
6925 
6926         @Override
6927         public void setExpansionCancelled(View v) {
6928             if (v instanceof ExpandableNotificationRow) {
6929                 ((ExpandableNotificationRow) v).setGroupExpansionChanging(false);
6930             }
6931         }
6932 
6933         @Override
6934         public void setUserLockedChild(View v, boolean userLocked) {
6935             if (v instanceof ExpandableNotificationRow) {
6936                 ((ExpandableNotificationRow) v).setUserLocked(userLocked);
6937             }
6938             cancelLongPress();
6939             requestDisallowInterceptTouchEvent(true);
6940         }
6941 
6942         @Override
6943         public void expansionStateChanged(boolean isExpanding) {
6944             mExpandingNotification = isExpanding;
6945             if (!mExpandedInThisMotion) {
6946                 mMaxScrollAfterExpand = getOwnScrollY();
6947                 mExpandedInThisMotion = true;
6948             }
6949         }
6950 
6951         @Override
6952         public int getMaxExpandHeight(ExpandableView view) {
6953             return view.getMaxContentHeight();
6954         }
6955     };
6956 
6957     public ExpandHelper.Callback getExpandHelperCallback() {
6958         return mExpandHelperCallback;
6959     }
6960 
6961     float getAppearFraction() {
6962         return mLastSentAppear;
6963     }
6964 
6965     float getExpandedHeight() {
6966         return mLastSentExpandedHeight;
6967     }
6968 
6969     /**
6970      * Enum for selecting some or all notification rows (does not included non-notif views).
6971      */
6972     @Retention(SOURCE)
6973     @IntDef({ROWS_ALL, ROWS_HIGH_PRIORITY, ROWS_GENTLE})
6974     @interface SelectedRows {
6975     }
6976 
6977     /**
6978      * All rows representing notifs.
6979      */
6980     public static final int ROWS_ALL = 0;
6981     /**
6982      * Only rows where entry.isHighPriority() is true.
6983      */
6984     public static final int ROWS_HIGH_PRIORITY = 1;
6985     /**
6986      * Only rows where entry.isHighPriority() is false.
6987      */
6988     public static final int ROWS_GENTLE = 2;
6989 
6990     interface ClearAllListener {
6991         void onClearAll(@SelectedRows int selectedRows);
6992     }
6993 
6994     interface ClearAllAnimationListener {
6995         void onAnimationEnd(
6996                 List<ExpandableNotificationRow> viewsToRemove, @SelectedRows int selectedRows);
6997     }
6998 
6999     // -------------------- Getters / Setters for the SceneContainer refactor ----------------------
7000 
7001     /** Use {@link ScrollViewFields#intrinsicStackHeight}, when SceneContainerFlag is enabled. */
7002     private int getContentHeight() {
7003         SceneContainerFlag.assertInLegacyMode();
7004         return mContentHeight;
7005     }
7006 
7007     private void setContentHeight(int contentHeight) {
7008         SceneContainerFlag.assertInLegacyMode();
7009         mContentHeight = contentHeight;
7010     }
7011 
7012     /**
7013      * Use {@link ScrollViewFields#intrinsicStackHeight}, when SceneContainerFlag is enabled.
7014      * @return the height of the content ignoring the footer.
7015      */
7016     public float getIntrinsicContentHeight() {
7017         SceneContainerFlag.assertInLegacyMode();
7018         return mIntrinsicContentHeight;
7019     }
7020 
7021     private void setIntrinsicContentHeight(float intrinsicContentHeight) {
7022         SceneContainerFlag.assertInLegacyMode();
7023         mIntrinsicContentHeight = intrinsicContentHeight;
7024     }
7025 
7026     private int getMaxTopPadding() {
7027         SceneContainerFlag.assertInLegacyMode();
7028         return mMaxTopPadding;
7029     }
7030 
7031     /** Not used with SceneContainerFlag, because we rely on the placeholder for placement. */
7032     public void setMaxTopPadding(int maxTopPadding) {
7033         SceneContainerFlag.assertInLegacyMode();
7034         mMaxTopPadding = maxTopPadding;
7035     }
7036 
7037     private void spewLog(String logMsg) {
7038         if (SPEW) {
7039             Log.v(TAG, logMsg);
7040         }
7041     }
7042 }
7043