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