Revert "Remove background drawing behind notifications"

This reverts commit 7552019d8bf2033195b1046d9870a1b093574189.

Reason for revert: removing outline breaks lockscreen <=> aod notif animation

DIFF - R.color.notification_shade_background_color is not available
anymore, so get default notif panel background color this way:

Utils.getColorAttr(mContext, android.R.attr.colorBackground).getDefaultColor()

Fixes: 174665900
Change-Id: Ia939e6bc1cb80e8d44b5c00e1534ab36f2c641cc
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 01b55b7..880dd378 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -357,6 +357,9 @@
          the notification is not swiped enough to dismiss it. -->
     <bool name="config_showNotificationGear">true</bool>
 
+    <!-- Whether or not a background should be drawn behind a notification. -->
+    <bool name="config_drawNotificationBackground">true</bool>
+
     <!-- Whether or the notifications can be shown and dismissed with a drag. -->
     <bool name="config_enableNotificationShadeDrag">true</bool>
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
index ac3b6d2..885048d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
@@ -82,6 +82,9 @@
     private ExpandableNotificationRow mTrackedHeadsUpRow;
     private float mAppearFraction;
 
+    /** Tracks the state from AlertingNotificationManager#hasNotifications() */
+    private boolean mHasAlertEntries;
+
     public AmbientState(
             Context context,
             @NonNull SectionProvider sectionProvider) {
@@ -365,10 +368,21 @@
         mPanelTracking = panelTracking;
     }
 
+    public boolean hasPulsingNotifications() {
+        return mPulsing && mHasAlertEntries;
+    }
+
     public void setPulsing(boolean hasPulsing) {
         mPulsing = hasPulsing;
     }
 
+    /**
+     * @return if we're pulsing in general
+     */
+    public boolean isPulsing() {
+        return mPulsing;
+    }
+
     public boolean isPulsing(NotificationEntry entry) {
         return mPulsing && entry.isAlerting();
     }
@@ -527,4 +541,8 @@
     public float getAppearFraction() {
         return mAppearFraction;
     }
+
+    public void setHasAlertEntries(boolean hasAlertEntries) {
+        mHasAlertEntries = hasAlertEntries;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSection.java
index ba03a50..1131a65 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSection.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSection.java
@@ -31,22 +31,175 @@
 import com.android.systemui.statusbar.notification.row.ExpandableView;
 
 /**
- * Represents the priority of a notification section and tracks first and last visible children.
+ * Represents the bounds of a section of the notification shade and handles animation when the
+ * bounds change.
  */
 public class NotificationSection {
     private @PriorityBucket int mBucket;
+    private View mOwningView;
+    private Rect mBounds = new Rect();
+    private Rect mCurrentBounds = new Rect(-1, -1, -1, -1);
+    private Rect mStartAnimationRect = new Rect();
+    private Rect mEndAnimationRect = new Rect();
+    private ObjectAnimator mTopAnimator = null;
+    private ObjectAnimator mBottomAnimator = null;
     private ExpandableView mFirstVisibleChild;
     private ExpandableView mLastVisibleChild;
 
-    NotificationSection(@PriorityBucket int bucket) {
+    NotificationSection(View owningView, @PriorityBucket int bucket) {
+        mOwningView = owningView;
         mBucket = bucket;
     }
 
+    public void cancelAnimators() {
+        if (mBottomAnimator != null) {
+            mBottomAnimator.cancel();
+        }
+        if (mTopAnimator != null) {
+            mTopAnimator.cancel();
+        }
+    }
+
+    public Rect getCurrentBounds() {
+        return mCurrentBounds;
+    }
+
+    public Rect getBounds() {
+        return mBounds;
+    }
+
+    public boolean didBoundsChange() {
+        return !mCurrentBounds.equals(mBounds);
+    }
+
+    public boolean areBoundsAnimating() {
+        return mBottomAnimator != null || mTopAnimator != null;
+    }
+
     @PriorityBucket
     public int getBucket() {
         return mBucket;
     }
 
+    public void startBackgroundAnimation(boolean animateTop, boolean animateBottom) {
+        // Left and right bounds are always applied immediately.
+        mCurrentBounds.left = mBounds.left;
+        mCurrentBounds.right = mBounds.right;
+        startBottomAnimation(animateBottom);
+        startTopAnimation(animateTop);
+    }
+
+
+    @ShadeViewRefactor(ShadeViewRefactor.RefactorComponent.STATE_RESOLVER)
+    private void startTopAnimation(boolean animate) {
+        int previousEndValue = mEndAnimationRect.top;
+        int newEndValue = mBounds.top;
+        ObjectAnimator previousAnimator = mTopAnimator;
+        if (previousAnimator != null && previousEndValue == newEndValue) {
+            return;
+        }
+        if (!animate) {
+            // just a local update was performed
+            if (previousAnimator != null) {
+                // we need to increase all animation keyframes of the previous animator by the
+                // relative change to the end value
+                int previousStartValue = mStartAnimationRect.top;
+                PropertyValuesHolder[] values = previousAnimator.getValues();
+                values[0].setIntValues(previousStartValue, newEndValue);
+                mStartAnimationRect.top = previousStartValue;
+                mEndAnimationRect.top = newEndValue;
+                previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
+                return;
+            } else {
+                // no new animation needed, let's just apply the value
+                setBackgroundTop(newEndValue);
+                return;
+            }
+        }
+        if (previousAnimator != null) {
+            previousAnimator.cancel();
+        }
+        ObjectAnimator animator = ObjectAnimator.ofInt(this, "backgroundTop",
+                mCurrentBounds.top, newEndValue);
+        Interpolator interpolator = Interpolators.FAST_OUT_SLOW_IN;
+        animator.setInterpolator(interpolator);
+        animator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
+        // remove the tag when the animation is finished
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mStartAnimationRect.top = -1;
+                mEndAnimationRect.top = -1;
+                mTopAnimator = null;
+            }
+        });
+        animator.start();
+        mStartAnimationRect.top = mCurrentBounds.top;
+        mEndAnimationRect.top = newEndValue;
+        mTopAnimator = animator;
+    }
+
+    @ShadeViewRefactor(ShadeViewRefactor.RefactorComponent.STATE_RESOLVER)
+    private void startBottomAnimation(boolean animate) {
+        int previousStartValue = mStartAnimationRect.bottom;
+        int previousEndValue = mEndAnimationRect.bottom;
+        int newEndValue = mBounds.bottom;
+        ObjectAnimator previousAnimator = mBottomAnimator;
+        if (previousAnimator != null && previousEndValue == newEndValue) {
+            return;
+        }
+        if (!animate) {
+            // just a local update was performed
+            if (previousAnimator != null) {
+                // we need to increase all animation keyframes of the previous animator by the
+                // relative change to the end value
+                PropertyValuesHolder[] values = previousAnimator.getValues();
+                values[0].setIntValues(previousStartValue, newEndValue);
+                mStartAnimationRect.bottom = previousStartValue;
+                mEndAnimationRect.bottom = newEndValue;
+                previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
+                return;
+            } else {
+                // no new animation needed, let's just apply the value
+                setBackgroundBottom(newEndValue);
+                return;
+            }
+        }
+        if (previousAnimator != null) {
+            previousAnimator.cancel();
+        }
+        ObjectAnimator animator = ObjectAnimator.ofInt(this, "backgroundBottom",
+                mCurrentBounds.bottom, newEndValue);
+        Interpolator interpolator = Interpolators.FAST_OUT_SLOW_IN;
+        animator.setInterpolator(interpolator);
+        animator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
+        // remove the tag when the animation is finished
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mStartAnimationRect.bottom = -1;
+                mEndAnimationRect.bottom = -1;
+                mBottomAnimator = null;
+            }
+        });
+        animator.start();
+        mStartAnimationRect.bottom = mCurrentBounds.bottom;
+        mEndAnimationRect.bottom = newEndValue;
+        mBottomAnimator = animator;
+    }
+
+    @ShadeViewRefactor(ShadeViewRefactor.RefactorComponent.SHADE_VIEW)
+    private void setBackgroundTop(int top) {
+        mCurrentBounds.top = top;
+        mOwningView.invalidate();
+    }
+
+    @ShadeViewRefactor(ShadeViewRefactor.RefactorComponent.SHADE_VIEW)
+    private void setBackgroundBottom(int bottom) {
+        mCurrentBounds.bottom = bottom;
+        mOwningView.invalidate();
+    }
+
     public ExpandableView getFirstVisibleChild() {
         return mFirstVisibleChild;
     }
@@ -66,4 +219,93 @@
         mLastVisibleChild = child;
         return changed;
     }
+
+    public void resetCurrentBounds() {
+        mCurrentBounds.set(mBounds);
+    }
+
+    /**
+     * Returns true if {@code top} is equal to the top of this section (if not currently animating)
+     * or where the top of this section will be when animation completes.
+     */
+    public boolean isTargetTop(int top) {
+        return (mTopAnimator == null && mCurrentBounds.top == top)
+                || (mTopAnimator != null && mEndAnimationRect.top == top);
+    }
+
+    /**
+     * Returns true if {@code bottom} is equal to the bottom of this section (if not currently
+     * animating) or where the bottom of this section will be when animation completes.
+     */
+    public boolean isTargetBottom(int bottom) {
+        return (mBottomAnimator == null && mCurrentBounds.bottom == bottom)
+                || (mBottomAnimator != null && mEndAnimationRect.bottom == bottom);
+    }
+
+    /**
+     * Update the bounds of this section based on it's views
+     *
+     * @param minTopPosition the minimum position that the top needs to have
+     * @param minBottomPosition the minimum position that the bottom needs to have
+     * @return the position of the new bottom
+     */
+    public int updateBounds(int minTopPosition, int minBottomPosition,
+            boolean shiftBackgroundWithFirst) {
+        int top = minTopPosition;
+        int bottom = minTopPosition;
+        ExpandableView firstView = getFirstVisibleChild();
+        if (firstView != null) {
+            // Round Y up to avoid seeing the background during animation
+            int finalTranslationY = (int) Math.ceil(ViewState.getFinalTranslationY(firstView));
+            // TODO: look into the already animating part
+            int newTop;
+            if (isTargetTop(finalTranslationY)) {
+                // we're ending up at the same location as we are now, let's just skip the
+                // animation
+                newTop = finalTranslationY;
+            } else {
+                newTop = (int) Math.ceil(firstView.getTranslationY());
+            }
+            top = Math.max(newTop, top);
+            if (firstView.showingPulsing()) {
+                // If we're pulsing, the notification can actually go below!
+                bottom = Math.max(bottom, finalTranslationY
+                        + ExpandableViewState.getFinalActualHeight(firstView));
+                if (shiftBackgroundWithFirst) {
+                    mBounds.left += Math.max(firstView.getTranslation(), 0);
+                    mBounds.right += Math.min(firstView.getTranslation(), 0);
+                }
+            }
+        }
+        top = Math.max(minTopPosition, top);
+        ExpandableView lastView = getLastVisibleChild();
+        if (lastView != null) {
+            float finalTranslationY = ViewState.getFinalTranslationY(lastView);
+            int finalHeight = ExpandableViewState.getFinalActualHeight(lastView);
+            // Round Y down to avoid seeing the background during animation
+            int finalBottom = (int) Math.floor(
+                    finalTranslationY + finalHeight - lastView.getClipBottomAmount());
+            int newBottom;
+            if (isTargetBottom(finalBottom)) {
+                // we're ending up at the same location as we are now, lets just skip the animation
+                newBottom = finalBottom;
+            } else {
+                newBottom = (int) (lastView.getTranslationY() + lastView.getActualHeight()
+                        - lastView.getClipBottomAmount());
+                // The background can never be lower than the end of the last view
+                minBottomPosition = (int) Math.min(
+                        lastView.getTranslationY() + lastView.getActualHeight(),
+                        minBottomPosition);
+            }
+            bottom = Math.max(bottom, Math.max(newBottom, minBottomPosition));
+        }
+        bottom = Math.max(top, bottom);
+        mBounds.top = top;
+        mBounds.bottom = bottom;
+        return bottom;
+    }
+
+    public boolean needsBackground() {
+        return mFirstVisibleChild != null && mBucket != BUCKET_MEDIA_CONTROLS;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt
index 36c5419..4f7e14b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt
@@ -126,7 +126,7 @@
 
     fun createSectionsForBuckets(): Array<NotificationSection> =
             sectionsFeatureManager.getNotificationBuckets()
-                    .map { NotificationSection(it) }
+                    .map { NotificationSection(parent, it) }
                     .toTypedArray()
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 5cc17a0..4487142 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -38,9 +38,12 @@
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Canvas;
+import android.graphics.Color;
 import android.graphics.Outline;
 import android.graphics.Paint;
 import android.graphics.PointF;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
 import android.graphics.Rect;
 import android.os.Bundle;
 import android.os.UserHandle;
@@ -69,6 +72,7 @@
 import android.widget.ScrollView;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.graphics.ColorUtils;
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.keyguard.KeyguardSliceView;
@@ -153,6 +157,8 @@
     private ExpandHelper mExpandHelper;
     private NotificationSwipeHelper mSwipeHelper;
     private int mCurrentStackHeight = Integer.MAX_VALUE;
+    private final Paint mBackgroundPaint = new Paint();
+    private final boolean mShouldDrawNotificationBackground;
     private boolean mHighPriorityBeforeSpeedBump;
     private boolean mDismissRtl;
 
@@ -251,6 +257,7 @@
     protected FooterView mFooterView;
     protected EmptyShadeView mEmptyShadeView;
     private boolean mDismissAllInProgress;
+    private boolean mFadeNotificationsOnDismiss;
     private FooterDismissListener mFooterDismissListener;
     private boolean mFlingAfterUpEvent;
 
@@ -320,6 +327,10 @@
         }
     };
     private NotificationSection[] mSections;
+    private boolean mAnimateNextBackgroundTop;
+    private boolean mAnimateNextBackgroundBottom;
+    private boolean mAnimateNextSectionBoundsChange;
+    private int mBgColor;
     private float mDimAmount;
     private ValueAnimator mDimAnimator;
     private ArrayList<ExpandableView> mTmpSortedChildren = new ArrayList<>();
@@ -330,14 +341,25 @@
         }
     };
     private ValueAnimator.AnimatorUpdateListener mDimUpdateListener
-            = animation -> setDimAmount((Float) animation.getAnimatedValue());
+            = new ValueAnimator.AnimatorUpdateListener() {
+
+        @Override
+        public void onAnimationUpdate(ValueAnimator animation) {
+            setDimAmount((Float) animation.getAnimatedValue());
+        }
+    };
     protected ViewGroup mQsContainer;
     private boolean mContinuousShadowUpdate;
+    private boolean mContinuousBackgroundUpdate;
     private ViewTreeObserver.OnPreDrawListener mShadowUpdater
             = () -> {
                 updateViewShadows();
                 return true;
             };
+    private ViewTreeObserver.OnPreDrawListener mBackgroundUpdater = () -> {
+                updateBackground();
+                return true;
+            };
     private Comparator<ExpandableView> mViewPositionComparator = (view, otherView) -> {
         float endY = view.getTranslationY() + view.getActualHeight();
         float otherEndY = otherView.getTranslationY() + otherView.getActualHeight();
@@ -356,7 +378,7 @@
             if (mAmbientState.isHiddenAtAll()) {
                 float xProgress = mHideXInterpolator.getInterpolation(
                         (1 - mLinearHideAmount) * mBackgroundXFactor);
-                outline.setRoundRect(mOutlineAnimationRect,
+                outline.setRoundRect(mBackgroundAnimationRect,
                         MathUtils.lerp(mCornerRadius / 2.0f, mCornerRadius,
                                 xProgress));
                 outline.setAlpha(1.0f - mAmbientState.getHideAmount());
@@ -365,6 +387,7 @@
             }
         }
     };
+    private PorterDuffXfermode mSrcMode = new PorterDuffXfermode(PorterDuff.Mode.SRC);
     private boolean mPulsing;
     private boolean mScrollable;
     private View mForcedScroll;
@@ -397,6 +420,7 @@
     private boolean mInHeadsUpPinnedMode;
     private boolean mHeadsUpAnimatingAway;
     private int mStatusBarState;
+    private int mCachedBackgroundColor;
     private boolean mHeadsUpGoingAwayAnimationsAllowed = true;
     private Runnable mReflingAndAnimateScroll = () -> {
         if (ANCHOR_SCROLLING) {
@@ -406,7 +430,7 @@
     };
     private int mCornerRadius;
     private int mSidePaddings;
-    private final Rect mOutlineAnimationRect = new Rect();
+    private final Rect mBackgroundAnimationRect = new Rect();
     private ArrayList<BiConsumer<Float, Float>> mExpandedHeightListeners = new ArrayList<>();
     private int mHeadsUpInset;
     private HeadsUpAppearanceController mHeadsUpAppearanceController;
@@ -426,6 +450,7 @@
 
     private final NotificationSectionsManager mSectionsManager;
     private ForegroundServiceDungeonView mFgsSectionView;
+    private boolean mAnimateBottomOnLayout;
     private float mLastSentAppear;
     private float mLastSentExpandedHeight;
     private boolean mWillExpand;
@@ -436,6 +461,7 @@
 
     private boolean mKeyguardMediaControllorVisible;
     private NotificationEntry mTopHeadsUpEntry;
+    private long mNumHeadsUp;
     private NotificationStackScrollLayoutController.TouchHandler mTouchHandler;
 
     private final ExpandableView.OnHeightChangedListener mOnChildHeightChangedListener =
@@ -502,6 +528,7 @@
         mSections = mSectionsManager.createSectionsForBuckets();
 
         mAmbientState = new AmbientState(context, mSectionsManager);
+        mBgColor = Utils.getColorAttr(mContext, android.R.attr.colorBackground).getDefaultColor();
         int minHeight = res.getDimensionPixelSize(R.dimen.notification_min_height);
         int maxHeight = res.getDimensionPixelSize(R.dimen.notification_max_height);
         mExpandHelper = new ExpandHelper(getContext(), mExpandHelperCallback,
@@ -510,9 +537,13 @@
         mExpandHelper.setScrollAdapter(mScrollAdapter);
 
         mStackScrollAlgorithm = createStackScrollAlgorithm(context);
+        mShouldDrawNotificationBackground =
+                res.getBoolean(R.bool.config_drawNotificationBackground);
         setOutlineProvider(mOutlineProvider);
 
-        setWillNotDraw(!DEBUG);
+        boolean willDraw = mShouldDrawNotificationBackground || DEBUG;
+        setWillNotDraw(!willDraw);
+        mBackgroundPaint.setAntiAlias(true);
         if (DEBUG) {
             mDebugPaint = new Paint();
             mDebugPaint.setColor(0xffff0000);
@@ -557,7 +588,7 @@
      * @return the height at which we will wake up when pulsing
      */
     public float getWakeUpHeight() {
-        ExpandableView firstChild = getFirstExpandableView();
+        ExpandableView firstChild = getFirstChildWithBackground();
         if (firstChild != null) {
             if (mKeyguardBypassEnabledProvider.getBypassEnabled()) {
                 return firstChild.getHeadsUpHeightWithoutHeader();
@@ -608,11 +639,22 @@
     }
 
     void updateBgColor() {
+        mBgColor = Utils.getColorAttr(mContext, android.R.attr.colorBackground).getDefaultColor();
+        updateBackgroundDimming();
         mShelf.onUiModeChanged();
     }
 
     @ShadeViewRefactor(RefactorComponent.DECORATOR)
     protected void onDraw(Canvas canvas) {
+        if (mShouldDrawNotificationBackground
+                && (mSections[0].getCurrentBounds().top
+                < mSections[mSections.length - 1].getCurrentBounds().bottom
+                || mAmbientState.isDozing())) {
+            drawBackground(canvas);
+        } else if (mInHeadsUpPinnedMode || mHeadsUpAnimatingAway) {
+            drawHeadsUpBackground(canvas);
+        }
+
         if (DEBUG) {
             int y = mTopPadding;
             canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
@@ -647,6 +689,160 @@
         }
     }
 
+    @ShadeViewRefactor(RefactorComponent.DECORATOR)
+    private void drawBackground(Canvas canvas) {
+        int lockScreenLeft = mSidePaddings;
+        int lockScreenRight = getWidth() - mSidePaddings;
+        int lockScreenTop = mSections[0].getCurrentBounds().top;
+        int lockScreenBottom = mSections[mSections.length - 1].getCurrentBounds().bottom;
+        int hiddenLeft = getWidth() / 2;
+        int hiddenTop = mTopPadding;
+
+        float yProgress = 1 - mInterpolatedHideAmount;
+        float xProgress = mHideXInterpolator.getInterpolation(
+                (1 - mLinearHideAmount) * mBackgroundXFactor);
+
+        int left = (int) MathUtils.lerp(hiddenLeft, lockScreenLeft, xProgress);
+        int right = (int) MathUtils.lerp(hiddenLeft, lockScreenRight, xProgress);
+        int top = (int) MathUtils.lerp(hiddenTop, lockScreenTop, yProgress);
+        int bottom = (int) MathUtils.lerp(hiddenTop, lockScreenBottom, yProgress);
+        mBackgroundAnimationRect.set(
+                left,
+                top,
+                right,
+                bottom);
+
+        int backgroundTopAnimationOffset = top - lockScreenTop;
+        // TODO(kprevas): this may not be necessary any more since we don't display the shelf in AOD
+        boolean anySectionHasVisibleChild = false;
+        for (NotificationSection section : mSections) {
+            if (section.needsBackground()) {
+                anySectionHasVisibleChild = true;
+                break;
+            }
+        }
+        boolean shouldDrawBackground;
+        if (mKeyguardBypassEnabledProvider.getBypassEnabled() && onKeyguard()) {
+            shouldDrawBackground = isPulseExpanding();
+        } else {
+            shouldDrawBackground = !mAmbientState.isDozing() || anySectionHasVisibleChild;
+        }
+        if (shouldDrawBackground) {
+            drawBackgroundRects(canvas, left, right, top, backgroundTopAnimationOffset);
+        }
+
+        updateClipping();
+    }
+
+    /**
+     * Draws round rects for each background section.
+     *
+     * We want to draw a round rect for each background section as defined by {@link #mSections}.
+     * However, if two sections are directly adjacent with no gap between them (e.g. on the
+     * lockscreen where the shelf can appear directly below the high priority section, or while
+     * scrolling the shade so that the top of the shelf is right at the bottom of the high priority
+     * section), we don't want to round the adjacent corners.
+     *
+     * Since {@link Canvas} doesn't provide a way to draw a half-rounded rect, this means that we
+     * need to coalesce the backgrounds for adjacent sections and draw them as a single round rect.
+     * This method tracks the top of each rect we need to draw, then iterates through the visible
+     * sections.  If a section is not adjacent to the previous section, we draw the previous rect
+     * behind the sections we've accumulated up to that point, then start a new rect at the top of
+     * the current section.  When we're done iterating we will always have one rect left to draw.
+     */
+    private void drawBackgroundRects(Canvas canvas, int left, int right, int top,
+            int animationYOffset) {
+        int backgroundRectTop = top;
+        int lastSectionBottom =
+                mSections[0].getCurrentBounds().bottom + animationYOffset;
+        int currentLeft = left;
+        int currentRight = right;
+        boolean first = true;
+        for (NotificationSection section : mSections) {
+            if (!section.needsBackground()) {
+                continue;
+            }
+            int sectionTop = section.getCurrentBounds().top + animationYOffset;
+            int ownLeft = Math.min(Math.max(left, section.getCurrentBounds().left), right);
+            int ownRight = Math.max(Math.min(right, section.getCurrentBounds().right), ownLeft);
+            // If sections are directly adjacent to each other, we don't want to draw them
+            // as separate roundrects, as the rounded corners right next to each other look
+            // bad.
+            if (sectionTop - lastSectionBottom > DISTANCE_BETWEEN_ADJACENT_SECTIONS_PX
+                    || ((currentLeft != ownLeft || currentRight != ownRight) && !first)) {
+                canvas.drawRoundRect(currentLeft,
+                        backgroundRectTop,
+                        currentRight,
+                        lastSectionBottom,
+                        mCornerRadius, mCornerRadius, mBackgroundPaint);
+                backgroundRectTop = sectionTop;
+            }
+            currentLeft = ownLeft;
+            currentRight = ownRight;
+            lastSectionBottom =
+                    section.getCurrentBounds().bottom + animationYOffset;
+            first = false;
+        }
+        canvas.drawRoundRect(currentLeft,
+                backgroundRectTop,
+                currentRight,
+                lastSectionBottom,
+                mCornerRadius, mCornerRadius, mBackgroundPaint);
+    }
+
+    private void drawHeadsUpBackground(Canvas canvas) {
+        int left = mSidePaddings;
+        int right = getWidth() - mSidePaddings;
+
+        float top = getHeight();
+        float bottom = 0;
+        int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View child = getChildAt(i);
+            if (child.getVisibility() != View.GONE
+                    && child instanceof ExpandableNotificationRow) {
+                ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+                if ((row.isPinned() || row.isHeadsUpAnimatingAway()) && row.getTranslation() < 0
+                        && row.getProvider().shouldShowGutsOnSnapOpen()) {
+                    top = Math.min(top, row.getTranslationY());
+                    bottom = Math.max(bottom, row.getTranslationY() + row.getActualHeight());
+                }
+            }
+        }
+
+        if (top < bottom) {
+            canvas.drawRoundRect(
+                    left, top, right, bottom,
+                    mCornerRadius, mCornerRadius, mBackgroundPaint);
+        }
+    }
+
+    @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
+    void updateBackgroundDimming() {
+        // No need to update the background color if it's not being drawn.
+        if (!mShouldDrawNotificationBackground) {
+            return;
+        }
+        final boolean newFlowHideShelf = Settings.Global.getInt(mContext.getContentResolver(),
+                Settings.Global.SHOW_NEW_NOTIF_DISMISS, 1 /* on by default */) == 1;
+        if (newFlowHideShelf) {
+            mBackgroundPaint.setColor(Color.TRANSPARENT);
+            invalidate();
+            return;
+        }
+        // Interpolate between semi-transparent notification panel background color
+        // and white AOD separator.
+        float colorInterpolation = MathUtils.smoothStep(0.4f /* start */, 1f /* end */,
+                mLinearHideAmount);
+        int color = ColorUtils.blendARGB(mBgColor, Color.WHITE, colorInterpolation);
+
+        if (mCachedBackgroundColor != color) {
+            mCachedBackgroundColor = color;
+            mBackgroundPaint.setColor(color);
+            invalidate();
+        }
+    }
+
     private void reinitView() {
         initView(getContext(), mKeyguardBypassEnabledProvider, mSwipeHelper);
     }
@@ -781,7 +977,7 @@
         updateContentHeight();
         clampScrollPosition();
         requestChildrenUpdate();
-        updateFirstAndLastExpandableView();
+        updateFirstAndLastBackgroundViews();
         updateAlgorithmLayoutMinHeight();
         updateOwnTranslationZ();
     }
@@ -852,6 +1048,9 @@
     private void onPreDrawDuringAnimation() {
         mShelf.updateAppearance();
         updateClippingToTopRoundedCorner();
+        if (!mNeedsAnimation && !mChildrenUpdateRequested) {
+            updateBackground();
+        }
     }
 
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
@@ -2168,6 +2367,131 @@
         }
     }
 
+    @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
+    private void updateBackground() {
+        // No need to update the background color if it's not being drawn.
+        if (!mShouldDrawNotificationBackground) {
+            return;
+        }
+
+        updateBackgroundBounds();
+        if (didSectionBoundsChange()) {
+            boolean animate = mAnimateNextSectionBoundsChange || mAnimateNextBackgroundTop
+                    || mAnimateNextBackgroundBottom || areSectionBoundsAnimating();
+            if (!isExpanded()) {
+                abortBackgroundAnimators();
+                animate = false;
+            }
+            if (animate) {
+                startBackgroundAnimation();
+            } else {
+                for (NotificationSection section : mSections) {
+                    section.resetCurrentBounds();
+                }
+                invalidate();
+            }
+        } else {
+            abortBackgroundAnimators();
+        }
+        mAnimateNextBackgroundTop = false;
+        mAnimateNextBackgroundBottom = false;
+        mAnimateNextSectionBoundsChange = false;
+    }
+
+    @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
+    private void abortBackgroundAnimators() {
+        for (NotificationSection section : mSections) {
+            section.cancelAnimators();
+        }
+    }
+
+    private boolean didSectionBoundsChange() {
+        for (NotificationSection section : mSections) {
+            if (section.didBoundsChange()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
+    private boolean areSectionBoundsAnimating() {
+        for (NotificationSection section : mSections) {
+            if (section.areBoundsAnimating()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
+    private void startBackgroundAnimation() {
+        // TODO(kprevas): do we still need separate fields for top/bottom?
+        // or can each section manage its own animation state?
+        NotificationSection firstVisibleSection = getFirstVisibleSection();
+        NotificationSection lastVisibleSection = getLastVisibleSection();
+        for (NotificationSection section : mSections) {
+            section.startBackgroundAnimation(
+                    section == firstVisibleSection
+                            ? mAnimateNextBackgroundTop
+                            : mAnimateNextSectionBoundsChange,
+                    section == lastVisibleSection
+                            ? mAnimateNextBackgroundBottom
+                            : mAnimateNextSectionBoundsChange);
+        }
+    }
+
+    /**
+     * Update the background bounds to the new desired bounds
+     */
+    @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
+    private void updateBackgroundBounds() {
+        int left = mSidePaddings;
+        int right = getWidth() - mSidePaddings;
+        for (NotificationSection section : mSections) {
+            section.getBounds().left = left;
+            section.getBounds().right = right;
+        }
+
+        if (!mIsExpanded) {
+            for (NotificationSection section : mSections) {
+                section.getBounds().top = 0;
+                section.getBounds().bottom = 0;
+            }
+            return;
+        }
+        int minTopPosition;
+        NotificationSection lastSection = getLastVisibleSection();
+        boolean onKeyguard = mStatusBarState == StatusBarState.KEYGUARD;
+        if (!onKeyguard) {
+            minTopPosition = (int) (mTopPadding + mStackTranslation);
+        } else if (lastSection == null) {
+            minTopPosition = mTopPadding;
+        } else {
+            // The first sections could be empty while there could still be elements in later
+            // sections. The position of these first few sections is determined by the position of
+            // the first visible section.
+            NotificationSection firstVisibleSection = getFirstVisibleSection();
+            firstVisibleSection.updateBounds(0 /* minTopPosition*/, 0 /* minBottomPosition */,
+                    false /* shiftPulsingWithFirst */);
+            minTopPosition = firstVisibleSection.getBounds().top;
+        }
+        boolean shiftPulsingWithFirst = mNumHeadsUp <= 1
+                && (mAmbientState.isDozing()
+                        || (mKeyguardBypassEnabledProvider.getBypassEnabled() && onKeyguard));
+        for (NotificationSection section : mSections) {
+            int minBottomPosition = minTopPosition;
+            if (section == lastSection) {
+                // We need to make sure the section goes all the way to the shelf
+                minBottomPosition = (int) (ViewState.getFinalTranslationY(mShelf)
+                        + mShelf.getIntrinsicHeight());
+            }
+            minTopPosition = section.updateBounds(minTopPosition, minBottomPosition,
+                    shiftPulsingWithFirst);
+            shiftPulsingWithFirst = false;
+        }
+    }
+
     private NotificationSection getFirstVisibleSection() {
         for (NotificationSection section : mSections) {
             if (section.getFirstVisibleChild() != null) {
@@ -2188,7 +2512,7 @@
     }
 
     @ShadeViewRefactor(RefactorComponent.COORDINATOR)
-    private ExpandableView getLastExpandableView() {
+    private ExpandableView getLastChildWithBackground() {
         int childCount = getChildCount();
         for (int i = childCount - 1; i >= 0; i--) {
             ExpandableView child = (ExpandableView) getChildAt(i);
@@ -2201,7 +2525,7 @@
     }
 
     @ShadeViewRefactor(RefactorComponent.COORDINATOR)
-    private ExpandableView getFirstExpandableView() {
+    private ExpandableView getFirstChildWithBackground() {
         int childCount = getChildCount();
         for (int i = 0; i < childCount; i++) {
             ExpandableView child = (ExpandableView) getChildAt(i);
@@ -2214,7 +2538,7 @@
     }
 
     //TODO: We shouldn't have to generate this list every time
-    private List<ExpandableView> getExpandableViewList() {
+    private List<ExpandableView> getChildrenWithBackground() {
         ArrayList<ExpandableView> children = new ArrayList<>();
         int childCount = getChildCount();
         for (int i = 0; i < childCount; i++) {
@@ -2752,13 +3076,32 @@
     }
 
     @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
-    private void updateFirstAndLastExpandableView() {
-        ExpandableView lastChild = getLastExpandableView();
-        mSectionsManager.updateFirstAndLastViewsForAllSections(
-                mSections, getExpandableViewList());
+    private void updateFirstAndLastBackgroundViews() {
+        NotificationSection firstSection = getFirstVisibleSection();
+        NotificationSection lastSection = getLastVisibleSection();
+        ExpandableView previousFirstChild =
+                firstSection == null ? null : firstSection.getFirstVisibleChild();
+        ExpandableView previousLastChild =
+                lastSection == null ? null : lastSection.getLastVisibleChild();
+
+        ExpandableView firstChild = getFirstChildWithBackground();
+        ExpandableView lastChild = getLastChildWithBackground();
+        boolean sectionViewsChanged = mSectionsManager.updateFirstAndLastViewsForAllSections(
+                mSections, getChildrenWithBackground());
+
+        if (mAnimationsEnabled && mIsExpanded) {
+            mAnimateNextBackgroundTop = firstChild != previousFirstChild;
+            mAnimateNextBackgroundBottom = lastChild != previousLastChild || mAnimateBottomOnLayout;
+            mAnimateNextSectionBoundsChange = sectionViewsChanged;
+        } else {
+            mAnimateNextBackgroundTop = false;
+            mAnimateNextBackgroundBottom = false;
+            mAnimateNextSectionBoundsChange = false;
+        }
         mAmbientState.setLastVisibleBackgroundChild(lastChild);
         // TODO: Refactor SectionManager and put the RoundnessManager there.
         mController.getNoticationRoundessManager().updateRoundedChildren(mSections);
+        mAnimateBottomOnLayout = false;
         invalidate();
     }
 
@@ -2920,6 +3263,7 @@
             setAnimationRunning(true);
             mStateAnimator.startAnimationForEvents(mAnimationEvents, mGoToFullShadeDelay);
             mAnimationEvents.clear();
+            updateBackground();
             updateViewShadows();
             updateClippingToTopRoundedCorner();
         } else {
@@ -4004,6 +4348,7 @@
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     private void setDimAmount(float dimAmount) {
         mDimAmount = dimAmount;
+        updateBackgroundDimming();
     }
 
     @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
@@ -4072,6 +4417,7 @@
         }
         runAnimationFinishedRunnables();
         setAnimationRunning(false);
+        updateBackground();
         updateViewShadows();
         updateClippingToTopRoundedCorner();
     }
@@ -4200,6 +4546,7 @@
             invalidateOutline();
         }
         updateAlgorithmHeightAndPadding();
+        updateBackgroundDimming();
         requestChildrenUpdate();
         updateOwnTranslationZ();
     }
@@ -5080,6 +5427,7 @@
      */
     public void setDozeAmount(float dozeAmount) {
         mAmbientState.setDozeAmount(dozeAmount);
+        updateContinuousBackgroundDrawing();
         requestChildrenUpdate();
     }
 
@@ -5117,6 +5465,7 @@
     }
 
     void setAnimateBottomOnLayout(boolean animateBottomOnLayout) {
+        mAnimateBottomOnLayout = animateBottomOnLayout;
     }
 
     public void setOnPulseHeightChangedListener(Runnable listener) {
@@ -5149,14 +5498,25 @@
 
     void onSwipeBegin() {
         requestDisallowInterceptTouchEvent(true);
+        updateFirstAndLastBackgroundViews();
         updateContinuousShadowDrawing();
+        updateContinuousBackgroundDrawing();
         requestChildrenUpdate();
     }
 
+    void onSwipeEnd() {
+        updateFirstAndLastBackgroundViews();
+    }
+
     void setTopHeadsUpEntry(NotificationEntry topEntry) {
         mTopHeadsUpEntry = topEntry;
     }
 
+    void setNumHeadsUp(long numHeadsUp) {
+        mNumHeadsUp = numHeadsUp;
+        mAmbientState.setHasAlertEntries(numHeadsUp > 0);
+    }
+
     public boolean getIsExpanded() {
         return mIsExpanded;
     }
@@ -5271,6 +5631,27 @@
         mSectionsManager.updateSectionBoundaries(reason);
     }
 
+    boolean isSilkDismissEnabled() {
+        return Settings.Global.getInt(mContext.getContentResolver(),
+                Settings.Global.SHOW_NEW_NOTIF_DISMISS, 1 /* enabled by default */) == 1;
+    }
+
+    void updateContinuousBackgroundDrawing() {
+        if (isSilkDismissEnabled()) {
+            return;
+        }
+        boolean continuousBackground = !mAmbientState.isFullyAwake()
+                && mSwipeHelper.isSwiping();
+        if (continuousBackground != mContinuousBackgroundUpdate) {
+            mContinuousBackgroundUpdate = continuousBackground;
+            if (continuousBackground) {
+                getViewTreeObserver().addOnPreDrawListener(mBackgroundUpdater);
+            } else {
+                getViewTreeObserver().removeOnPreDrawListener(mBackgroundUpdater);
+            }
+        }
+    }
+
     @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
     void updateContinuousShadowDrawing() {
         boolean continuousShadowUpdate = mAnimationRunning
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 8a03309..c2d030b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -395,6 +395,7 @@
                     if (mView.getDismissAllInProgress()) {
                         return;
                     }
+                    mView.onSwipeEnd();
                     if (view instanceof ExpandableNotificationRow) {
                         ExpandableNotificationRow row = (ExpandableNotificationRow) view;
                         if (row.isHeadsUp()) {
@@ -454,6 +455,7 @@
 
                 @Override
                 public void onChildSnappedBack(View animView, float targetLeft) {
+                    mView.onSwipeEnd();
                     if (animView instanceof ExpandableNotificationRow) {
                         ExpandableNotificationRow row = (ExpandableNotificationRow) animView;
                         if (row.isPinned() && !canChildBeDismissed(row)
@@ -517,7 +519,9 @@
 
                 @Override
                 public void onHeadsUpStateChanged(NotificationEntry entry, boolean isHeadsUp) {
+                    long numEntries = mHeadsUpManager.getAllEntries().count();
                     NotificationEntry topEntry = mHeadsUpManager.getTopEntry();
+                    mView.setNumHeadsUp(numEntries);
                     mView.setTopHeadsUpEntry(topEntry);
                     mNotificationRoundnessManager.updateView(entry.getRow(), false /* animate */);
                 }
@@ -661,6 +665,8 @@
         mHeadsUpManager.setAnimationStateHandler(mView::setHeadsUpGoingAwayAnimationsAllowed);
         mDynamicPrivacyController.addListener(mDynamicPrivacyControllerListener);
 
+        mScrimController.setScrimBehindChangeRunnable(mView::updateBackgroundDimming);
+
         mLockscreenUserManager.addUserChangedListener(mLockscreenUserChangeListener);
 
         mFadeNotificationsOnDismiss =  // TODO: this should probably be injected directly