Color extraction on scrims

Consuming color extraction library to add wallpaper
colors to sysui scrims. Handles both lock screen and
notification shade.

Bug: 36856508
Bug: 37013527
Test: runtest --path frameworks/base/packages/SystemUI/tests/src/com/android/systemui/statusbar/ScrimViewTest.java
Change-Id: I8d964c978a766243d90bbfb3432d19eebf1a2056
diff --git a/packages/SystemUI/Android.mk b/packages/SystemUI/Android.mk
index 5ee0c64..c7bae66 100644
--- a/packages/SystemUI/Android.mk
+++ b/packages/SystemUI/Android.mk
@@ -37,7 +37,8 @@
     android-support-v7-mediarouter \
     android-support-v7-palette \
     android-support-v14-preference \
-    android-support-v17-leanback
+    android-support-v17-leanback \
+    colorextraction
 
 LOCAL_STATIC_JAVA_LIBRARIES := \
     SystemUI-tags \
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ScrimView.java b/packages/SystemUI/src/com/android/systemui/statusbar/ScrimView.java
index f9d0cd6..d9ac4d5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ScrimView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ScrimView.java
@@ -19,39 +19,52 @@
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
+import android.annotation.NonNull;
 import android.content.Context;
-import android.content.res.TypedArray;
+import android.content.res.Configuration;
 import android.graphics.Canvas;
 import android.graphics.Color;
-import android.graphics.Paint;
+import android.graphics.Point;
 import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
 import android.graphics.PorterDuffXfermode;
 import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.support.v4.graphics.ColorUtils;
 import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Display;
 import android.view.View;
+import android.view.WindowManager;
 import android.view.animation.Interpolator;
-import com.android.systemui.R;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.Dependency;
+import com.android.systemui.statusbar.policy.ConfigurationController;
+
+import com.google.android.colorextraction.ColorExtractor;
+import com.google.android.colorextraction.drawable.GradientDrawable;
 
 /**
  * A view which can draw a scrim
  */
-public class ScrimView extends View
-{
-    private final Paint mPaint = new Paint();
-    private int mScrimColor;
-    private boolean mIsEmpty = true;
+public class ScrimView extends View implements ConfigurationController.ConfigurationListener {
+    private static final String TAG = "ScrimView";
+    private final ColorExtractor.GradientColors mColors;
     private boolean mDrawAsSrc;
     private float mViewAlpha = 1.0f;
     private ValueAnimator mAlphaAnimator;
     private Rect mExcludedRect = new Rect();
     private boolean mHasExcludedArea;
-    private ValueAnimator.AnimatorUpdateListener mAlphaUpdateListener
-            = new ValueAnimator.AnimatorUpdateListener() {
-        @Override
-        public void onAnimationUpdate(ValueAnimator animation) {
-            mViewAlpha = (float) animation.getAnimatedValue();
-            invalidate();
+    private Drawable mDrawable;
+    private PorterDuffColorFilter mColorFilter;
+    private int mTintColor;
+    private ValueAnimator.AnimatorUpdateListener mAlphaUpdateListener = animation -> {
+        if (mDrawable == null) {
+            Log.w(TAG, "Trying to animate null drawable");
+            return;
         }
+        mDrawable.setAlpha((int) (255 * (float) animation.getAnimatedValue()));
     };
     private AnimatorListenerAdapter mClearAnimatorListener = new AnimatorListenerAdapter() {
         @Override
@@ -76,72 +89,134 @@
     public ScrimView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
 
-        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ScrimView);
+        mDrawable = new GradientDrawable(context);
+        mDrawable.setCallback(this);
+        mColors = new ColorExtractor.GradientColors();
+        updateScreenSize();
 
-        try {
-            mScrimColor = ta.getColor(R.styleable.ScrimView_scrimColor, Color.BLACK);
-        } finally {
-            ta.recycle();
-        }
+        // We need to know about configuration changes to update the gradient size
+        // since it's independent from view bounds.
+        ConfigurationController config = Dependency.get(ConfigurationController.class);
+        config.addCallback(this);
     }
 
     @Override
     protected void onDraw(Canvas canvas) {
-        if (mDrawAsSrc || (!mIsEmpty && mViewAlpha > 0f)) {
-            PorterDuff.Mode mode = mDrawAsSrc ? PorterDuff.Mode.SRC : PorterDuff.Mode.SRC_OVER;
-            int color = getScrimColorWithAlpha();
+        if (mDrawAsSrc || mDrawable.getAlpha() > 0) {
             if (!mHasExcludedArea) {
-                canvas.drawColor(color, mode);
+                mDrawable.draw(canvas);
             } else {
-                mPaint.setColor(color);
                 if (mExcludedRect.top > 0) {
-                    canvas.drawRect(0, 0, getWidth(), mExcludedRect.top, mPaint);
+                    canvas.save();
+                    canvas.clipRect(0, 0, getWidth(), mExcludedRect.top);
+                    mDrawable.draw(canvas);
+                    canvas.restore();
                 }
                 if (mExcludedRect.left > 0) {
-                    canvas.drawRect(0,  mExcludedRect.top, mExcludedRect.left, mExcludedRect.bottom,
-                            mPaint);
+                    canvas.save();
+                    canvas.clipRect(0, mExcludedRect.top, mExcludedRect.left,
+                            mExcludedRect.bottom);
+                    mDrawable.draw(canvas);
+                    canvas.restore();
                 }
                 if (mExcludedRect.right < getWidth()) {
-                    canvas.drawRect(mExcludedRect.right,
-                            mExcludedRect.top,
-                            getWidth(),
-                            mExcludedRect.bottom,
-                            mPaint);
+                    canvas.save();
+                    canvas.clipRect(mExcludedRect.right, mExcludedRect.top, getWidth(),
+                            mExcludedRect.bottom);
+                    mDrawable.draw(canvas);
+                    canvas.restore();
                 }
                 if (mExcludedRect.bottom < getHeight()) {
-                    canvas.drawRect(0,  mExcludedRect.bottom, getWidth(), getHeight(), mPaint);
+                    canvas.save();
+                    canvas.clipRect(0, mExcludedRect.bottom, getWidth(), getHeight());
+                    mDrawable.draw(canvas);
+                    canvas.restore();
                 }
             }
         }
     }
 
-    public int getScrimColorWithAlpha() {
-        int color = mScrimColor;
-        color = Color.argb((int) (Color.alpha(color) * mViewAlpha), Color.red(color),
-                Color.green(color), Color.blue(color));
-        return color;
+    public void setDrawable(Drawable drawable) {
+        mDrawable = drawable;
+        mDrawable.setCallback(this);
+        mDrawable.setBounds(getLeft(), getTop(), getRight(), getBottom());
+        invalidate();
+    }
+
+    @Override
+    public void invalidateDrawable(@NonNull Drawable drawable) {
+        super.invalidateDrawable(drawable);
+        if (drawable == mDrawable) {
+            invalidate();
+        }
     }
 
     public void setDrawAsSrc(boolean asSrc) {
         mDrawAsSrc = asSrc;
-        mPaint.setXfermode(new PorterDuffXfermode(mDrawAsSrc ? PorterDuff.Mode.SRC
-                : PorterDuff.Mode.SRC_OVER));
-        invalidate();
+        PorterDuff.Mode mode = asSrc ? PorterDuff.Mode.SRC : PorterDuff.Mode.SRC_OVER;
+        mDrawable.setXfermode(new PorterDuffXfermode(mode));
     }
 
-    public void setScrimColor(int color) {
-        if (color != mScrimColor) {
-            mIsEmpty = Color.alpha(color) == 0;
-            mScrimColor = color;
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        if (changed) {
+            mDrawable.setBounds(left, top, right, bottom);
             invalidate();
-            if (mChangeRunnable != null) {
-                mChangeRunnable.run();
-            }
         }
     }
 
-    public int getScrimColor() {
-        return mScrimColor;
+    public void setColors(@NonNull ColorExtractor.GradientColors colors) {
+        if (colors == null) {
+            throw new IllegalArgumentException("Colors cannot be null");
+        }
+        if (mColors.equals(colors)) {
+            return;
+        }
+        mColors.set(colors);
+        updateColorWithTint();
+    }
+
+    @VisibleForTesting
+    Drawable getDrawable() {
+        return mDrawable;
+    }
+
+    public void setTint(int color) {
+        if (mTintColor == color) {
+            return;
+        }
+        mTintColor = color;
+        updateColorWithTint();
+    }
+
+    private void updateColorWithTint() {
+        if (mDrawable instanceof GradientDrawable) {
+            // Optimization to blend colors and avoid a color filter
+            GradientDrawable drawable = (GradientDrawable) mDrawable;
+            float tintAmount = Color.alpha(mTintColor) / 255f;
+            int mainTinted = ColorUtils.blendARGB(mColors.getMainColor(), mTintColor,
+                    tintAmount);
+            int secondaryTinted = ColorUtils.blendARGB(mColors.getSecondaryColor(), mTintColor,
+                    tintAmount);
+            drawable.setColors(mainTinted, secondaryTinted, false);
+        } else {
+            if (mColorFilter == null) {
+                mColorFilter = new PorterDuffColorFilter(mTintColor, PorterDuff.Mode.SRC_OVER);
+            } else {
+                mColorFilter.setColor(mTintColor);
+            }
+            mDrawable.setColorFilter(Color.alpha(mTintColor) == 0 ? null : mColorFilter);
+            mDrawable.invalidateSelf();
+        }
+
+        if (mChangeRunnable != null) {
+            mChangeRunnable.run();
+        }
+    }
+
+    public int getTint() {
+        return mTintColor;
     }
 
     @Override
@@ -150,23 +225,30 @@
     }
 
     public void setViewAlpha(float alpha) {
-        if (mAlphaAnimator != null) {
-            mAlphaAnimator.cancel();
-        }
         if (alpha != mViewAlpha) {
             mViewAlpha = alpha;
-            invalidate();
+
+            if (mAlphaAnimator != null) {
+                mAlphaAnimator.cancel();
+            }
+
+            mDrawable.setAlpha((int) (255 * alpha));
             if (mChangeRunnable != null) {
                 mChangeRunnable.run();
             }
         }
     }
 
+    @VisibleForTesting
+    float getViewAlpha() {
+        return mViewAlpha;
+    }
+
     public void animateViewAlpha(float alpha, long durationOut, Interpolator interpolator) {
         if (mAlphaAnimator != null) {
             mAlphaAnimator.cancel();
         }
-        mAlphaAnimator = ValueAnimator.ofFloat(mViewAlpha, alpha);
+        mAlphaAnimator = ValueAnimator.ofFloat(getViewAlpha(), alpha);
         mAlphaAnimator.addUpdateListener(mAlphaUpdateListener);
         mAlphaAnimator.addListener(mClearAnimatorListener);
         mAlphaAnimator.setInterpolator(interpolator);
@@ -193,4 +275,25 @@
     public void setChangeRunnable(Runnable changeRunnable) {
         mChangeRunnable = changeRunnable;
     }
+
+    @Override
+    public void onConfigChanged(Configuration newConfig) {
+        updateScreenSize();
+    }
+
+    private void updateScreenSize() {
+        if (mDrawable instanceof GradientDrawable) {
+            WindowManager wm = mContext.getSystemService(WindowManager.class);
+            if (wm == null) {
+                Log.w(TAG, "Can't resize gradient drawable to fit the screen");
+                return;
+            }
+            Display display = wm.getDefaultDisplay();
+            if (display != null) {
+                Point size = new Point();
+                display.getRealSize(size);
+                ((GradientDrawable) mDrawable).setScreenSize(size.x, size.y);
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index 67d5b6a..2be5b83 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -20,9 +20,12 @@
 import android.animation.AnimatorListenerAdapter;
 import android.animation.PropertyValuesHolder;
 import android.animation.ValueAnimator;
+import android.app.WallpaperManager;
 import android.content.Context;
+import android.graphics.Color;
 import android.graphics.Rect;
 import android.support.v4.graphics.ColorUtils;
+import android.util.Log;
 import android.util.TypedValue;
 import android.view.View;
 import android.view.ViewGroup;
@@ -30,6 +33,7 @@
 import android.view.animation.DecelerateInterpolator;
 import android.view.animation.Interpolator;
 import android.view.animation.PathInterpolator;
+
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.R;
 import com.android.systemui.statusbar.ExpandableNotificationRow;
@@ -38,12 +42,14 @@
 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
 import com.android.systemui.statusbar.stack.ViewState;
 
+import com.google.android.colorextraction.ColorExtractor;
+
 /**
  * Controls both the scrim behind the notifications and in front of the notifications (when a
  * security method gets shown).
  */
 public class ScrimController implements ViewTreeObserver.OnPreDrawListener,
-        OnHeadsUpChangedListener {
+        OnHeadsUpChangedListener, ColorExtractor.OnColorsChangedListener {
     public static final long ANIMATION_DURATION = 220;
     public static final Interpolator KEYGUARD_FADE_OUT_INTERPOLATOR
             = new PathInterpolator(0f, 0, 0.7f, 1f);
@@ -61,11 +67,16 @@
 
     private final LightBarController mLightBarController;
     protected final ScrimView mScrimBehind;
-    private final ScrimView mScrimInFront;
+    protected final ScrimView mScrimInFront;
     private final UnlockMethodCache mUnlockMethodCache;
     private final View mHeadsUpScrim;
     private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
 
+    private final ColorExtractor mColorExtractor;
+    private ColorExtractor.GradientColors mLockColors;
+    private ColorExtractor.GradientColors mSystemColors;
+    private boolean mNeedsDrawableColorUpdate;
+
     protected float mScrimBehindAlpha;
     protected float mScrimBehindAlphaKeyguard = SCRIM_BEHIND_ALPHA_KEYGUARD;
     protected float mScrimBehindAlphaUnlocking = SCRIM_BEHIND_ALPHA_UNLOCKING;
@@ -98,6 +109,7 @@
     private boolean mSkipFirstFrame;
     private boolean mDontAnimateBouncerChanges;
     private boolean mKeyguardFadingOutInProgress;
+    private boolean mAnimatingDozeUnlock;
     private ValueAnimator mKeyguardFadeoutAnimation;
 
     public ScrimController(LightBarController lightBarController, ScrimView scrimBehind,
@@ -111,12 +123,22 @@
         mLightBarController = lightBarController;
         mScrimBehindAlpha = context.getResources().getFloat(R.dimen.scrim_behind_alpha);
 
+        mColorExtractor = new ColorExtractor(context);
+        mColorExtractor.setListener(this);
+        mLockColors = mColorExtractor.getColors(WallpaperManager.FLAG_LOCK);
+        mSystemColors = mColorExtractor.getColors(WallpaperManager.FLAG_SYSTEM);
+        mNeedsDrawableColorUpdate = true;
+
         updateHeadsUpScrim(false);
         updateScrims();
     }
 
     public void setKeyguardShowing(boolean showing) {
         mKeyguardShowing = showing;
+
+        // Showing/hiding the keyguard means that scrim colors
+        // will probably have to be switched
+        mNeedsDrawableColorUpdate = true;
         scheduleUpdate();
     }
 
@@ -157,6 +179,7 @@
 
     public void setWakeAndUnlocking() {
         mWakeAndUnlocking = true;
+        mAnimatingDozeUnlock = true;
         scheduleUpdate();
     }
 
@@ -192,7 +215,7 @@
 
     public void animateKeyguardUnoccluding(long duration) {
         mAnimateChange = false;
-        setScrimBehindColor(0f);
+        setScrimBehindAlpha(0f);
         mAnimateChange = true;
         scheduleUpdate();
         mDurationOverride = duration;
@@ -246,23 +269,35 @@
     }
 
     protected void updateScrims() {
-        if (mAnimateKeyguardFadingOut || mForceHideScrims) {
-            setScrimInFrontColor(0f);
-            setScrimBehindColor(0f);
-        } else if (mWakeAndUnlocking) {
 
+        // Make sure we have the right gradients
+        if (mNeedsDrawableColorUpdate) {
+            mNeedsDrawableColorUpdate = false;
+            if (mKeyguardShowing) {
+                mScrimInFront.setColors(mLockColors);
+                mScrimBehind.setColors(mLockColors);
+            } else {
+                mScrimInFront.setColors(mSystemColors);
+                mScrimBehind.setColors(mSystemColors);
+            }
+        }
+
+        if (mAnimateKeyguardFadingOut || mForceHideScrims) {
+            setScrimInFrontAlpha(0f);
+            setScrimBehindAlpha(0f);
+        } else if (mWakeAndUnlocking) {
             // During wake and unlock, we first hide everything behind a black scrim, which then
             // gets faded out from animateKeyguardFadingOut.
             if (mDozing) {
-                setScrimInFrontColor(0f);
-                setScrimBehindColor(1f);
+                setScrimInFrontAlpha(0f);
+                setScrimBehindAlpha(1f);
             } else {
-                setScrimInFrontColor(1f);
-                setScrimBehindColor(0f);
+                setScrimInFrontAlpha(1f);
+                setScrimBehindAlpha(0f);
             }
         } else if (!mKeyguardShowing && !mBouncerShowing) {
             updateScrimNormal();
-            setScrimInFrontColor(0);
+            setScrimInFrontAlpha(0);
         } else {
             updateScrimKeyguard();
         }
@@ -275,18 +310,18 @@
             float fraction = 1 - behindFraction;
             fraction = (float) Math.pow(fraction, 0.8f);
             behindFraction = (float) Math.pow(behindFraction, 0.8f);
-            setScrimInFrontColor(fraction * getScrimInFrontAlpha());
-            setScrimBehindColor(behindFraction * mScrimBehindAlphaKeyguard);
+            setScrimInFrontAlpha(fraction * getScrimInFrontAlpha());
+            setScrimBehindAlpha(behindFraction * mScrimBehindAlphaKeyguard);
         } else if (mBouncerShowing && !mBouncerIsKeyguard) {
-            setScrimInFrontColor(getScrimInFrontAlpha());
+            setScrimInFrontAlpha(getScrimInFrontAlpha());
             updateScrimNormal();
         } else if (mBouncerShowing) {
-            setScrimInFrontColor(0f);
-            setScrimBehindColor(mScrimBehindAlpha);
+            setScrimInFrontAlpha(0f);
+            setScrimBehindAlpha(mScrimBehindAlpha);
         } else {
             float fraction = Math.max(0, Math.min(mFraction, 1));
-            setScrimInFrontColor(0f);
-            setScrimBehindColor(fraction
+            setScrimInFrontAlpha(0f);
+            setScrimBehindAlpha(fraction
                     * (mScrimBehindAlphaKeyguard - mScrimBehindAlphaUnlocking)
                     + mScrimBehindAlphaUnlocking);
         }
@@ -297,30 +332,29 @@
         // let's start this 20% of the way down the screen
         frac = frac * 1.2f - 0.2f;
         if (frac <= 0) {
-            setScrimBehindColor(0);
+            setScrimBehindAlpha(0);
         } else {
             // woo, special effects
             final float k = (float)(1f-0.5f*(1f-Math.cos(3.14159f * Math.pow(1f-frac, 2f))));
-            setScrimBehindColor(k * mScrimBehindAlpha);
+            setScrimBehindAlpha(k * mScrimBehindAlpha);
         }
     }
 
-    private void setScrimBehindColor(float alpha) {
-        setScrimColor(mScrimBehind, alpha);
+    private void setScrimBehindAlpha(float alpha) {
+        setScrimAlpha(mScrimBehind, alpha);
     }
 
-    private void setScrimInFrontColor(float alpha) {
-        setScrimColor(mScrimInFront, alpha);
+    private void setScrimInFrontAlpha(float alpha) {
+        setScrimAlpha(mScrimInFront, alpha);
         if (alpha == 0f) {
             mScrimInFront.setClickable(false);
         } else {
-
             // Eat touch events (unless dozing).
             mScrimInFront.setClickable(!mDozing);
         }
     }
 
-    private void setScrimColor(View scrim, float alpha) {
+    private void setScrimAlpha(View scrim, float alpha) {
         updateScrim(mAnimateChange, scrim, alpha, getCurrentScrimAlpha(scrim));
     }
 
@@ -346,15 +380,20 @@
         }
     }
 
-    protected void updateScrimColor(View scrim) {
+    private void updateScrimColor(View scrim) {
         float alpha1 = getCurrentScrimAlpha(scrim);
         if (scrim instanceof ScrimView) {
-            float alpha2 = getDozeAlpha(scrim);
-            float alpha = 1 - (1 - alpha1) * (1 - alpha2);
+            ScrimView scrimView = (ScrimView) scrim;
+            float dozeAlpha = getDozeAlpha(scrim);
+            float alpha = 1 - (1 - alpha1) * (1 - dozeAlpha);
             alpha = Math.max(0, Math.min(1.0f, alpha));
-            int baseColor = ((ScrimView) scrim).getScrimColor();
-            ((ScrimView) scrim).setScrimColor(
-                    ColorUtils.setAlphaComponent(baseColor, (int) (alpha * 255)));
+            scrimView.setViewAlpha(alpha);
+
+            int dozeTint = Color.TRANSPARENT;
+            if (mAnimatingDozeUnlock || mDozing) {
+                dozeTint = Color.BLACK;
+            }
+            scrimView.setTint(dozeTint);
         } else {
             scrim.setAlpha(alpha1);
         }
@@ -363,13 +402,10 @@
     private void startScrimAnimation(final View scrim, float target) {
         float current = getCurrentScrimAlpha(scrim);
         ValueAnimator anim = ValueAnimator.ofFloat(current, target);
-        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
-            @Override
-            public void onAnimationUpdate(ValueAnimator animation) {
-                float alpha = (float) animation.getAnimatedValue();
-                setCurrentScrimAlpha(scrim, alpha);
-                updateScrimColor(scrim);
-            }
+        anim.addUpdateListener(animation -> {
+            float alpha = (float) animation.getAnimatedValue();
+            setCurrentScrimAlpha(scrim, alpha);
+            updateScrimColor(scrim);
         });
         anim.setInterpolator(getInterpolator());
         anim.setStartDelay(mAnimationDelay);
@@ -384,6 +420,7 @@
                 if (mKeyguardFadingOutInProgress) {
                     mKeyguardFadeoutAnimation = null;
                     mKeyguardFadingOutInProgress = false;
+                    mAnimatingDozeUnlock = false;
                 }
                 scrim.setTag(TAG_KEY_ANIM, null);
                 scrim.setTag(TAG_KEY_ANIM_TARGET, null);
@@ -436,6 +473,8 @@
                 mOnAnimationFinished = null;
             }
             mKeyguardFadingOutInProgress = false;
+            if (!mWakeAndUnlocking || force)
+                mAnimatingDozeUnlock = false;
         }
     }
 
@@ -559,8 +598,10 @@
         mScrimBehind.setExcludedArea(area);
     }
 
-    public int getScrimBehindColor() {
-        return mScrimBehind.getScrimColorWithAlpha();
+    public int getBackgroundColor() {
+        int color = mLockColors.getMainColor();
+        return Color.argb((int) (mScrimBehind.getAlpha() * Color.alpha(color)),
+                Color.red(color), Color.green(color), Color.blue(color));
     }
 
     public void setScrimBehindChangeRunnable(Runnable changeRunnable) {
@@ -577,4 +618,18 @@
     public void setCurrentUser(int currentUser) {
         // Don't care in the base class.
     }
+
+    @Override
+    public void onColorsChanged(ColorExtractor.GradientColors colors, int which) {
+        if ((which & WallpaperManager.FLAG_LOCK) != 0) {
+            mLockColors = colors;
+            mNeedsDrawableColorUpdate = true;
+            scheduleUpdate();
+        }
+        if ((which & WallpaperManager.FLAG_SYSTEM) != 0) {
+            mSystemColors = colors;
+            mNeedsDrawableColorUpdate = true;
+            scheduleUpdate();
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
index 859c435..63bb4da 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
@@ -58,6 +58,7 @@
 import android.view.animation.Interpolator;
 import android.widget.OverScroller;
 import android.widget.ScrollView;
+
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.systemui.ExpandHelper;
@@ -87,6 +88,8 @@
 import com.android.systemui.statusbar.policy.HeadsUpManager;
 import com.android.systemui.statusbar.policy.ScrollAdapter;
 
+import android.support.v4.graphics.ColorUtils;
+
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -485,16 +488,8 @@
         float alpha = BACKGROUND_ALPHA_DIMMED + (1 - BACKGROUND_ALPHA_DIMMED) * (1.0f - mDimAmount);
         alpha *= mBackgroundFadeAmount;
         // We need to manually blend in the background color
-        int scrimColor = mScrimController.getScrimBehindColor();
-        // SRC_OVER blending Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc
-        float alphaInv = 1 - alpha;
-        int color = Color.argb((int) (alpha * 255 + alphaInv * Color.alpha(scrimColor)),
-                (int) (mBackgroundFadeAmount * Color.red(mBgColor)
-                        + alphaInv * Color.red(scrimColor)),
-                (int) (mBackgroundFadeAmount * Color.green(mBgColor)
-                        + alphaInv * Color.green(scrimColor)),
-                (int) (mBackgroundFadeAmount * Color.blue(mBgColor)
-                        + alphaInv * Color.blue(scrimColor)));
+        int scrimColor = mScrimController.getBackgroundColor();
+        int color = ColorUtils.blendARGB(scrimColor, mBgColor, alpha);
         if (mCachedBackgroundColor != color) {
             mCachedBackgroundColor = color;
             mBackgroundPaint.setColor(color);
@@ -4074,12 +4069,7 @@
 
     public void setScrimController(ScrimController scrimController) {
         mScrimController = scrimController;
-        mScrimController.setScrimBehindChangeRunnable(new Runnable() {
-            @Override
-            public void run() {
-                updateBackgroundDimming();
-            }
-        });
+        mScrimController.setScrimBehindChangeRunnable(this::updateBackgroundDimming);
     }
 
     public void forceNoOverlappingRendering(boolean force) {
diff --git a/packages/SystemUI/tests/Android.mk b/packages/SystemUI/tests/Android.mk
index 8eedf31..fa2d3b5 100644
--- a/packages/SystemUI/tests/Android.mk
+++ b/packages/SystemUI/tests/Android.mk
@@ -43,7 +43,8 @@
     android-support-v7-appcompat \
     android-support-v7-mediarouter \
     android-support-v14-preference \
-    android-support-v17-leanback
+    android-support-v17-leanback \
+    colorextraction
 
 LOCAL_STATIC_JAVA_LIBRARIES := \
     metrics-helper-lib \
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ScrimViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ScrimViewTest.java
new file mode 100644
index 0000000..ea14681
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ScrimViewTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.statusbar;
+
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+
+import android.graphics.drawable.VectorDrawable;
+import android.testing.AndroidTestingRunner;
+import android.view.View;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.statusbar.ScrimView;
+
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.mock;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import static junit.framework.Assert.assertEquals;
+
+@RunWith(AndroidTestingRunner.class)
+public class ScrimViewTest extends SysuiTestCase {
+
+    ScrimView mView;
+
+    @Before
+    public void setUp() {
+        mView = new ScrimView(getContext());
+        mView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
+        mView.layout(0, 0, 1920, 1080);
+    }
+
+    @Test
+    public void testSetDrawable_UpdateDrawable() {
+        Drawable drawable = new ColorDrawable(Color.GREEN);
+        mView.setDrawable(drawable);
+        assertEquals(drawable, mView.getDrawable());
+    }
+
+    @Test
+    public void testSetViewAlpha_propagatesToDrawable() {
+        float alpha = 0.5f;
+        mView.setViewAlpha(alpha);
+        assertEquals(mView.getViewAlpha(), alpha);
+    }
+
+    @Test
+    public void testOnDraw_ExcludeRectDrawable() {
+        mView.setExcludedArea(new Rect(10, 10, 20, 20));
+        Canvas canvas = mock(Canvas.class);
+        mView.onDraw(canvas);
+        // One time for each rect side
+        verify(canvas, times(4)).clipRect(anyInt(), anyInt(), anyInt(), anyInt());
+    }
+
+    @Test
+    public void setTint_set() {
+        int tint = Color.BLUE;
+        mView.setTint(tint);
+        assertEquals(mView.getTint(), tint);
+    }
+}