Create a basic doze dream in SystemUI.

The doze dream is not configured by default.

When configured, the doze dream does not show anything by default.
It teases a dark version of the keyguard (showing only the time
and notifications) when a notification arrives or significant motion
is detected.

Bug:15863249
Change-Id: Icfceb054d35d6fd4d9178eda7480e2464873ca4b
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
new file mode 100644
index 0000000..cc0d4a7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2014 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.doze;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.hardware.Sensor;
+import android.hardware.SensorManager;
+import android.hardware.TriggerEvent;
+import android.hardware.TriggerEventListener;
+import android.os.PowerManager;
+import android.os.Vibrator;
+import android.service.dreams.DozeHardware;
+import android.service.dreams.DreamService;
+import android.util.Log;
+
+import com.android.systemui.SystemUIApplication;
+
+public class DozeService extends DreamService {
+    private static final boolean DEBUG = false;
+
+    private static final String TEASE_ACTION = "com.android.systemui.doze.tease";
+
+    private final String mTag = String.format("DozeService.%08x", hashCode());
+    private final Context mContext = this;
+
+    private Host mHost;
+    private DozeHardware mDozeHardware;
+    private SensorManager mSensors;
+    private Sensor mSigMotionSensor;
+    private PowerManager mPowerManager;
+    private PowerManager.WakeLock mWakeLock;
+    private boolean mDreaming;
+    private boolean mTeaseReceiverRegistered;
+
+    public DozeService() {
+        if (DEBUG) Log.d(mTag, "new DozeService()");
+        setDebug(DEBUG);
+    }
+
+    @Override
+    public void onCreate() {
+        if (DEBUG) Log.d(mTag, "onCreate");
+        super.onCreate();
+
+        if (getApplication() instanceof SystemUIApplication) {
+            final SystemUIApplication app = (SystemUIApplication) getApplication();
+            mHost = app.getComponent(Host.class);
+        }
+
+        setWindowless(true);
+
+        mSensors = (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE);
+        mSigMotionSensor = mSensors.getDefaultSensor(Sensor.TYPE_SIGNIFICANT_MOTION);
+        mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
+        mWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, mTag);
+    }
+
+    @Override
+    public void onAttachedToWindow() {
+        if (DEBUG) Log.d(mTag, "onAttachedToWindow");
+        super.onAttachedToWindow();
+    }
+
+    @Override
+    public void onDreamingStarted() {
+        super.onDreamingStarted();
+        mDozeHardware = getDozeHardware();
+        if (DEBUG) Log.d(mTag, "onDreamingStarted canDoze=" + canDoze()
+                + " dozeHardware=" + mDozeHardware);
+        mDreaming = true;
+        listenForTeaseSignals(true);
+        requestDoze();
+    }
+
+    public void stayAwake(long millis) {
+        if (mDreaming && millis > 0) {
+            mWakeLock.acquire(millis);
+        }
+    }
+
+    public void startDozing() {
+        if (DEBUG) Log.d(mTag, "startDozing mDreaming=" + mDreaming);
+        if (!mDreaming) {
+            Log.w(mTag, "Not dozing, no longer dreaming");
+            return;
+        }
+
+        super.startDozing();
+    }
+
+    @Override
+    public void onDreamingStopped() {
+        if (DEBUG) Log.d(mTag, "onDreamingStopped isDozing=" + isDozing());
+        super.onDreamingStopped();
+
+        mDreaming = false;
+        mDozeHardware = null;
+        if (mWakeLock.isHeld()) {
+            mWakeLock.release();
+        }
+        listenForTeaseSignals(false);
+        stopDozing();
+        dozingStopped();
+    }
+
+    @Override
+    public void onDetachedFromWindow() {
+        if (DEBUG) Log.d(mTag, "onDetachedFromWindow");
+        super.onDetachedFromWindow();
+
+        dozingStopped();
+    }
+
+    @Override
+    public void onDestroy() {
+        if (DEBUG) Log.d(mTag, "onDestroy");
+        super.onDestroy();
+
+        dozingStopped();
+    }
+
+    private void requestDoze() {
+        if (mHost != null) {
+            mHost.requestDoze(this);
+        }
+    }
+
+    private void requestTease() {
+        if (mHost != null) {
+            mHost.requestTease(this);
+        }
+    }
+
+    private void dozingStopped() {
+        if (mHost != null) {
+            mHost.dozingStopped(this);
+        }
+    }
+
+    private void listenForTeaseSignals(boolean listen) {
+        if (DEBUG) Log.d(mTag, "listenForTeaseSignals: " + listen);
+        if (mHost == null) return;
+        listenForSignificantMotion(listen);
+        if (listen) {
+            mContext.registerReceiver(mTeaseReceiver, new IntentFilter(TEASE_ACTION));
+            mTeaseReceiverRegistered = true;
+            mHost.addCallback(mHostCallback);
+        } else {
+            if (mTeaseReceiverRegistered) {
+                mContext.unregisterReceiver(mTeaseReceiver);
+            }
+            mTeaseReceiverRegistered = false;
+            mHost.removeCallback(mHostCallback);
+        }
+    }
+
+    private void listenForSignificantMotion(boolean listen) {
+        if (mSigMotionSensor == null) return;
+        if (listen) {
+            mSensors.requestTriggerSensor(mSigMotionListener, mSigMotionSensor);
+        } else {
+            mSensors.cancelTriggerSensor(mSigMotionListener, mSigMotionSensor);
+        }
+    }
+
+    private static String triggerEventToString(TriggerEvent event) {
+        if (event == null) return null;
+        final StringBuilder sb = new StringBuilder("TriggerEvent[")
+                .append(event.timestamp).append(',')
+                .append(event.sensor.getName());
+        if (event.values != null) {
+            for (int i = 0; i < event.values.length; i++) {
+                sb.append(',').append(event.values[i]);
+            }
+        }
+        return sb.append(']').toString();
+    }
+
+    private final TriggerEventListener mSigMotionListener = new TriggerEventListener() {
+        @Override
+        public void onTrigger(TriggerEvent event) {
+            if (DEBUG) Log.d(mTag, "sigMotion.onTrigger: " + triggerEventToString(event));
+            if (DEBUG) {
+                final Vibrator v = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE);
+                if (v != null) {
+                    v.vibrate(1000);
+                }
+            }
+            requestTease();
+            listenForSignificantMotion(true);  // reregister, this sensor only fires once
+        }
+    };
+
+    private final BroadcastReceiver mTeaseReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (DEBUG) Log.d(mTag, "Received tease intent");
+            requestTease();
+        }
+    };
+
+    private final Host.Callback mHostCallback = new Host.Callback() {
+        @Override
+        public void onNewNotifications() {
+            if (DEBUG) Log.d(mTag, "onNewNotifications");
+            requestTease();
+        }
+    };
+
+    public interface Host {
+        void addCallback(Callback callback);
+        void removeCallback(Callback callback);
+        void requestDoze(DozeService dozeService);
+        void requestTease(DozeService dozeService);
+        void dozingStopped(DozeService dozeService);
+
+        public interface Callback {
+            void onNewNotifications();
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java
index f6f78e9..1550217 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java
@@ -25,6 +25,8 @@
 import android.graphics.BitmapShader;
 import android.graphics.Canvas;
 import android.graphics.Color;
+import android.graphics.ColorMatrix;
+import android.graphics.ColorMatrixColorFilter;
 import android.graphics.Paint;
 import android.graphics.PorterDuff;
 import android.graphics.PorterDuffColorFilter;
@@ -89,6 +91,8 @@
             = new PathInterpolator(0, 0, 0.5f, 1);
 
     private boolean mDimmed;
+    private boolean mDark;
+    private final Paint mDarkPaint = createDarkPaint();
 
     private int mBgResId = com.android.internal.R.drawable.notification_material_bg;
     private int mDimmedBgResId = com.android.internal.R.drawable.notification_material_bg_dim;
@@ -295,6 +299,34 @@
         }
     }
 
+    public void setDark(boolean dark, boolean fade) {
+        // TODO implement fade
+        if (mDark != dark) {
+            mDark = dark;
+            if (mDark) {
+                setLayerType(View.LAYER_TYPE_HARDWARE, mDarkPaint);
+            } else {
+                setLayerType(View.LAYER_TYPE_NONE, null);
+            }
+        }
+    }
+
+    private static Paint createDarkPaint() {
+        final Paint p = new Paint();
+        final float[] invert = {
+            -1f,  0f,  0f, 1f, 1f,
+             0f, -1f,  0f, 1f, 1f,
+             0f,  0f, -1f, 1f, 1f,
+             0f,  0f,  0f, 1f, 0f
+        };
+        final ColorMatrix m = new ColorMatrix(invert);
+        final ColorMatrix grayscale = new ColorMatrix();
+        grayscale.setSaturation(0);
+        m.preConcat(grayscale);
+        p.setColorFilter(new ColorMatrixColorFilter(m));
+        return p;
+    }
+
     /**
      * Sets the resource id for the background of this notification.
      *
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java
index 4d4a8ab..c3fb83c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java
@@ -167,6 +167,15 @@
     }
 
     /**
+     * Sets the notification as dark. The default implementation does nothing.
+     *
+     * @param dark Whether the notification should be dark.
+     * @param fade Whether an animation should be played to change the state.
+     */
+    public void setDark(boolean dark, boolean fade) {
+    }
+
+    /**
      * @return The desired notification height.
      */
     public int getIntrinsicHeight() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
index 00951b2..714bedf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
@@ -100,6 +100,7 @@
 import com.android.systemui.DemoMode;
 import com.android.systemui.EventLogTags;
 import com.android.systemui.R;
+import com.android.systemui.doze.DozeService;
 import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.qs.CircularClipper;
 import com.android.systemui.qs.QSPanel;
@@ -225,6 +226,7 @@
     private int mStatusBarWindowState = WINDOW_STATE_SHOWING;
     private StatusBarWindowManager mStatusBarWindowManager;
     private UnlockMethodCache mUnlockMethodCache;
+    private DozeServiceHost mDozeServiceHost;
 
     int mPixelFormat;
     Object mQueueLock = new Object();
@@ -405,6 +407,7 @@
     private boolean mSettingsClosing;
     private boolean mVisible;
     private boolean mWaitingForKeyguardExit;
+    private boolean mDozing;
 
     private Interpolator mLinearOutSlowIn;
     private Interpolator mAlphaOut = new PathInterpolator(0f, 0.4f, 1f, 1f);
@@ -428,6 +431,8 @@
     private final ArraySet<String> mCurrentlyVisibleNotifications = new ArraySet<String>();
     private long mLastVisibilityReportUptimeMs;
 
+    private final ShadeUpdates mShadeUpdates = new ShadeUpdates();
+
     private static final int VISIBLE_LOCATIONS = ViewState.LOCATION_FIRST_CARD
             | ViewState.LOCATION_TOP_STACK_PEEKING
             | ViewState.LOCATION_MAIN_AREA
@@ -537,6 +542,9 @@
         }
         mUnlockMethodCache = UnlockMethodCache.getInstance(mContext);
         startKeyguard();
+
+        mDozeServiceHost = new DozeServiceHost();
+        putComponent(DozeService.Host.class, mDozeServiceHost);
     }
 
     // ================================================================================
@@ -1232,7 +1240,6 @@
         for (View remove : toRemove) {
             mStackScroller.removeView(remove);
         }
-
         for (int i=0; i<toShow.size(); i++) {
             View v = toShow.get(i);
             if (v.getParent() == null) {
@@ -1265,6 +1272,7 @@
         updateRowStates();
         updateSpeedbump();
         mNotificationPanel.setQsExpansionEnabled(provisioned && mUserSetup);
+        mShadeUpdates.check();
     }
 
     private void updateSpeedbump() {
@@ -2273,6 +2281,7 @@
         pw.println(windowStateToString(mStatusBarWindowState));
         pw.print("  mStatusBarMode=");
         pw.println(BarTransitions.modeToString(mStatusBarMode));
+        pw.print("  mDozing="); pw.println(mDozing);
         pw.print("  mZenMode=");
         pw.println(Settings.Global.zenModeToString(mZenMode));
         pw.print("  mUseHeadsUp=");
@@ -2675,7 +2684,6 @@
         if (newlyVisible.isEmpty() && noLongerVisible.isEmpty()) {
             return;
         }
-
         String[] newlyVisibleAr = newlyVisible.toArray(new String[newlyVisible.size()]);
         String[] noLongerVisibleAr = noLongerVisible.toArray(new String[noLongerVisible.size()]);
         try {
@@ -2950,6 +2958,7 @@
             mNotificationPanel.setKeyguardShowing(false);
             mScrimController.setKeyguardShowing(false);
         }
+        updateDozingState();
         updateStackScrollerState();
         updatePublicMode();
         updateNotifications();
@@ -2957,6 +2966,26 @@
         updateCarrierLabelVisibility(false);
     }
 
+    private void updateDozingState() {
+        final boolean bottomGone = mKeyguardBottomArea.getVisibility() == View.GONE;
+        if (mDozing) {
+            mNotificationPanel.setBackgroundColor(0xff000000);
+            mHeader.setVisibility(View.INVISIBLE);
+            if (!bottomGone) {
+                mKeyguardBottomArea.setVisibility(View.INVISIBLE);
+            }
+            mStackScroller.setDark(true, false /*animate*/);
+        } else {
+            mNotificationPanel.setBackground(null);
+            mHeader.setVisibility(View.VISIBLE);
+            if (!bottomGone) {
+                mKeyguardBottomArea.setVisibility(View.VISIBLE);
+            }
+            mStackScroller.setDark(false, false /*animate*/);
+        }
+        mScrimController.setDozing(mDozing);
+    }
+
     public void updateStackScrollerState() {
         if (mStackScroller == null) return;
         boolean onKeyguard = mState == StatusBarState.KEYGUARD;
@@ -3229,4 +3258,121 @@
         }
         notifyUiVisibilityChanged(mSystemUiVisibility);
     }
+
+    private final class ShadeUpdates {
+        private final ArraySet<String> mVisibleNotifications = new ArraySet<String>();
+        private final ArraySet<String> mNewVisibleNotifications = new ArraySet<String>();
+
+        public void check() {
+            mNewVisibleNotifications.clear();
+            for (int i = 0; i < mNotificationData.size(); i++) {
+                final Entry entry = mNotificationData.get(i);
+                final boolean visible = entry.row != null
+                        && entry.row.getVisibility() == View.VISIBLE;
+                if (visible) {
+                    mNewVisibleNotifications.add(entry.key + entry.notification.getPostTime());
+                }
+            }
+            final boolean updates = !mVisibleNotifications.containsAll(mNewVisibleNotifications);
+            mVisibleNotifications.clear();
+            mVisibleNotifications.putAll(mNewVisibleNotifications);
+
+            // We have new notifications
+            if (updates && mDozeServiceHost != null) {
+                mDozeServiceHost.fireNewNotifications();
+            }
+        }
+    }
+
+    private final class DozeServiceHost implements DozeService.Host {
+        // Amount of time to allow to update the time shown on the screen before releasing
+        // the wakelock.  This timeout is design to compensate for the fact that we don't
+        // currently have a way to know when time display contents have actually been
+        // refreshed once we've finished rendering a new frame.
+        private static final long PROCESSING_TIME = 500;
+
+        private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>();
+        private final H mHandler = new H();
+
+        private DozeService mCurrentDozeService;
+
+        public void fireNewNotifications() {
+            for (Callback callback : mCallbacks) {
+                callback.onNewNotifications();
+            }
+        }
+
+        @Override
+        public void addCallback(Callback callback) {
+            mCallbacks.add(callback);
+        }
+
+        @Override
+        public void removeCallback(Callback callback) {
+            mCallbacks.remove(callback);
+        }
+
+        @Override
+        public void requestDoze(DozeService dozeService) {
+            if (dozeService == null) return;
+            dozeService.stayAwake(PROCESSING_TIME);
+            mHandler.obtainMessage(H.REQUEST_DOZE, dozeService).sendToTarget();
+        }
+
+        @Override
+        public void requestTease(DozeService dozeService) {
+            if (dozeService == null) return;
+            dozeService.stayAwake(PROCESSING_TIME);
+            mHandler.obtainMessage(H.REQUEST_TEASE, dozeService).sendToTarget();
+        }
+
+        @Override
+        public void dozingStopped(DozeService dozeService) {
+            if (dozeService == null) return;
+            dozeService.stayAwake(PROCESSING_TIME);
+            mHandler.obtainMessage(H.DOZING_STOPPED, dozeService).sendToTarget();
+        }
+
+        private void handleRequestDoze(DozeService dozeService) {
+            mCurrentDozeService = dozeService;
+            if (!mDozing) {
+                mDozing = true;
+                updateDozingState();
+            }
+            mCurrentDozeService.startDozing();
+        }
+
+        private void handleRequestTease(DozeService dozeService) {
+            if (!dozeService.equals(mCurrentDozeService)) return;
+            final long stayAwake = mScrimController.tease();
+            mCurrentDozeService.stayAwake(stayAwake);
+        }
+
+        private void handleDozingStopped(DozeService dozeService) {
+            if (dozeService.equals(mCurrentDozeService)) {
+                mCurrentDozeService = null;
+            }
+            if (mDozing) {
+                mDozing = false;
+                updateDozingState();
+            }
+        }
+
+        private final class H extends Handler {
+            private static final int REQUEST_DOZE = 1;
+            private static final int REQUEST_TEASE = 2;
+            private static final int DOZING_STOPPED = 3;
+
+            @Override
+            public void handleMessage(Message msg) {
+                if (msg.what == REQUEST_DOZE) {
+                    handleRequestDoze((DozeService) msg.obj);
+                } else if (msg.what == REQUEST_TEASE) {
+                    handleRequestTease((DozeService) msg.obj);
+                } else if (msg.what == DOZING_STOPPED) {
+                    handleDozingStopped((DozeService) msg.obj);
+                }
+            }
+        }
+    }
 }
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 1264d75..bf63f7f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -21,21 +21,35 @@
 import android.animation.ValueAnimator;
 import android.graphics.Color;
 import android.graphics.drawable.ColorDrawable;
+import android.util.Log;
 import android.view.View;
 import android.view.ViewTreeObserver;
 import android.view.animation.DecelerateInterpolator;
 import android.view.animation.Interpolator;
 
+import com.android.systemui.R;
+
 /**
  * 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 {
+    private static final String TAG = "ScrimController";
+    private static final boolean DEBUG = false;
 
     private static final float SCRIM_BEHIND_ALPHA = 0.62f;
     private static final float SCRIM_BEHIND_ALPHA_KEYGUARD = 0.5f;
     private static final float SCRIM_IN_FRONT_ALPHA = 0.75f;
     private static final long ANIMATION_DURATION = 220;
+    private static final int TAG_KEY_ANIM = R.id.scrim;
+
+    private static final int NUM_TEASES = 3;
+    private static final long TEASE_IN_ANIMATION_DURATION = 500;
+    private static final long TEASE_VISIBLE_DURATION = 3000;
+    private static final long TEASE_OUT_ANIMATION_DURATION = 1000;
+    private static final long TEASE_INVISIBLE_DURATION = 1000;
+    private static final long TEASE_DURATION = TEASE_IN_ANIMATION_DURATION
+            + TEASE_VISIBLE_DURATION + TEASE_OUT_ANIMATION_DURATION + TEASE_INVISIBLE_DURATION;
 
     private final View mScrimBehind;
     private final View mScrimInFront;
@@ -54,6 +68,8 @@
     private long mAnimationDelay;
     private Runnable mOnAnimationFinished;
     private boolean mAnimationStarted;
+    private boolean mDozing;
+    private int mTeasesRemaining;
 
     private final Interpolator mInterpolator = new DecelerateInterpolator();
 
@@ -97,6 +113,29 @@
         scheduleUpdate();
     }
 
+    public void setDozing(boolean dozing) {
+        if (mDozing == dozing) return;
+        mDozing = dozing;
+        if (!mDozing) {
+            cancelTeasing();
+        }
+        scheduleUpdate();
+    }
+
+    /** When dozing, fade screen contents in and out a few times using the front scrim. */
+    public long tease() {
+        if (!mDozing) return 0;
+        mTeasesRemaining = NUM_TEASES;
+        mScrimInFront.post(mTeaseIn);
+        return NUM_TEASES * TEASE_DURATION;
+    }
+
+    private void cancelTeasing() {
+        mTeasesRemaining = 0;
+        mScrimInFront.removeCallbacks(mTeaseIn);
+        mScrimInFront.removeCallbacks(mTeaseOut);
+    }
+
     private void scheduleUpdate() {
         if (mUpdatePending) return;
 
@@ -125,6 +164,8 @@
         } else if (mBouncerShowing) {
             setScrimInFrontColor(SCRIM_IN_FRONT_ALPHA);
             setScrimBehindColor(0f);
+        } else if (mDozing) {
+            setScrimInFrontColor(1);
         } else {
             setScrimInFrontColor(0f);
             setScrimBehindColor(SCRIM_BEHIND_ALPHA_KEYGUARD);
@@ -174,6 +215,10 @@
         if (current == targetColor) {
             return;
         }
+        Object runningAnim = scrim.getTag(TAG_KEY_ANIM);
+        if (runningAnim instanceof ValueAnimator) {
+            ((ValueAnimator) runningAnim).cancel();
+        }
         ValueAnimator anim = ValueAnimator.ofInt(current, target);
         anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
             @Override
@@ -193,9 +238,11 @@
                     mOnAnimationFinished.run();
                     mOnAnimationFinished = null;
                 }
+                scrim.setTag(TAG_KEY_ANIM, null);
             }
         });
         anim.start();
+        scrim.setTag(TAG_KEY_ANIM, anim);
         mAnimationStarted = true;
     }
 
@@ -225,4 +272,51 @@
         mAnimationStarted = false;
         return true;
     }
+
+    private final Runnable mTeaseIn = new Runnable() {
+        @Override
+        public void run() {
+            if (DEBUG) Log.d(TAG, "Tease in, mDozing=" + mDozing
+                    + " mTeasesRemaining=" + mTeasesRemaining);
+            if (!mDozing || mTeasesRemaining == 0) return;
+            mTeasesRemaining--;
+            mDurationOverride = TEASE_IN_ANIMATION_DURATION;
+            mAnimationDelay = 0;
+            mAnimateChange = true;
+            mOnAnimationFinished = mTeaseInFinished;
+            setScrimColor(mScrimInFront, 0);
+        }
+    };
+
+    private final Runnable mTeaseInFinished = new Runnable() {
+        @Override
+        public void run() {
+            if (DEBUG) Log.d(TAG, "Tease in finished, mDozing=" + mDozing);
+            if (!mDozing) return;
+            mScrimInFront.postDelayed(mTeaseOut, TEASE_VISIBLE_DURATION);
+        }
+    };
+
+    private final Runnable mTeaseOut = new Runnable() {
+        @Override
+        public void run() {
+            if (DEBUG) Log.d(TAG, "Tease in finished, mDozing=" + mDozing);
+            if (!mDozing) return;
+            mDurationOverride = TEASE_OUT_ANIMATION_DURATION;
+            mAnimationDelay = 0;
+            mAnimateChange = true;
+            mOnAnimationFinished = mTeaseOutFinished;
+            setScrimColor(mScrimInFront, 1);
+        }
+    };
+
+    private final Runnable mTeaseOutFinished = new Runnable() {
+        @Override
+        public void run() {
+            if (DEBUG) Log.d(TAG, "Tease out finished, mTeasesRemaining=" + mTeasesRemaining);
+            if (mTeasesRemaining > 0) {
+                mScrimInFront.postDelayed(mTeaseIn, TEASE_INVISIBLE_DURATION);
+            }
+        }
+    };
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/AmbientState.java
index fcc951e..0582140 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/AmbientState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/AmbientState.java
@@ -33,6 +33,7 @@
     private float mOverScrollBottomAmount;
     private int mSpeedBumpIndex = -1;
     private float mScrimAmount;
+    private boolean mDark;
 
     public int getScrollY() {
         return mScrollY;
@@ -62,6 +63,11 @@
         mDimmed = dimmed;
     }
 
+    /** In dark mode, we draw as little as possible, assuming a black background */
+    public void setDark(boolean dark) {
+        mDark = dark;
+    }
+
     /**
      * In dimmed mode, a child can be activated, which happens on the first tap of the double-tap
      * interaction. This child is then scaled normally and its background is fully opaque.
@@ -74,6 +80,10 @@
         return mDimmed;
     }
 
+    public boolean isDark() {
+        return mDark;
+    }
+
     public ActivatableNotificationView getActivatedChild() {
         return mActivatedChild;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/AnimationFilter.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/AnimationFilter.java
index cf56fa57..99d3a01 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/AnimationFilter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/AnimationFilter.java
@@ -29,6 +29,7 @@
     boolean animateHeight;
     boolean animateTopInset;
     boolean animateDimmed;
+    boolean animateDark;
     boolean hasDelays;
 
     public AnimationFilter animateAlpha() {
@@ -71,6 +72,11 @@
         return this;
     }
 
+    public AnimationFilter animateDark() {
+        animateDark = true;
+        return this;
+    }
+
     /**
      * Combines multiple filters into {@code this} filter, using or as the operand .
      *
@@ -92,6 +98,7 @@
         animateHeight |= filter.animateHeight;
         animateTopInset |= filter.animateTopInset;
         animateDimmed |= filter.animateDimmed;
+        animateDark |= filter.animateDark;
         hasDelays |= filter.hasDelays;
     }
 
@@ -103,6 +110,7 @@
         animateHeight = false;
         animateTopInset = false;
         animateDimmed = false;
+        animateDark = false;
         hasDelays = false;
     }
 }
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 f6e9aef..4220efe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
@@ -128,6 +128,7 @@
     private boolean mNeedsAnimation;
     private boolean mTopPaddingNeedsAnimation;
     private boolean mDimmedNeedsAnimation;
+    private boolean mDarkNeedsAnimation;
     private boolean mActivateNeedsAnimation;
     private boolean mIsExpanded = true;
     private boolean mChildrenUpdateRequested;
@@ -356,14 +357,6 @@
         return getNotGoneChildCount() > 1;
     }
 
-    private boolean isViewExpanded(View view) {
-        if (view != null) {
-            ExpandableView expandView = (ExpandableView) view;
-            return expandView.getActualHeight() > mCollapsedSize;
-        }
-        return false;
-    }
-
     /**
      * Updates the children views according to the stack scroll algorithm. Call this whenever
      * modifications to {@link #mOwnScrollY} are performed to reflect it in the view layout.
@@ -1479,6 +1472,7 @@
         generateTopPaddingEvent();
         generateActivateEvent();
         generateDimmedEvent();
+        generateDarkEvent();
         mNeedsAnimation = false;
     }
 
@@ -1554,6 +1548,14 @@
         mDimmedNeedsAnimation = false;
     }
 
+    private void generateDarkEvent() {
+        if (mDarkNeedsAnimation) {
+            mAnimationEvents.add(
+                    new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_DARK));
+        }
+        mDarkNeedsAnimation = false;
+    }
+
     private boolean onInterceptTouchEventScroll(MotionEvent ev) {
         if (!isScrollingEnabled()) {
             return false;
@@ -1852,6 +1854,18 @@
     }
 
     /**
+     * See {@link AmbientState#setDark}.
+     */
+    public void setDark(boolean dark, boolean animate) {
+        mAmbientState.setDark(dark);
+        if (animate && mAnimationsEnabled) {
+            mDarkNeedsAnimation = true;
+            mNeedsAnimation =  true;
+        }
+        requestChildrenUpdate();
+    }
+
+    /**
      * A listener that is notified when some child locations might have changed.
      */
     public interface OnChildLocationsChangedListener {
@@ -1940,7 +1954,11 @@
                         .animateHeight()
                         .animateTopInset()
                         .animateY()
-                        .animateZ()
+                        .animateZ(),
+
+                // ANIMATION_TYPE_DARK
+                new AnimationFilter()
+                        .animateDark(),
         };
 
         static int[] LENGTHS = new int[] {
@@ -1971,6 +1989,9 @@
 
                 // ANIMATION_TYPE_CHANGE_POSITION
                 StackStateAnimator.ANIMATION_DURATION_STANDARD,
+
+                // ANIMATION_TYPE_DARK
+                StackStateAnimator.ANIMATION_DURATION_STANDARD,
         };
 
         static final int ANIMATION_TYPE_ADD = 0;
@@ -1982,6 +2003,7 @@
         static final int ANIMATION_TYPE_ACTIVATED_CHILD = 6;
         static final int ANIMATION_TYPE_DIMMED = 7;
         static final int ANIMATION_TYPE_CHANGE_POSITION = 8;
+        static final int ANIMATION_TYPE_DARK = 9;
 
         final long eventStartTime;
         final View changingView;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java
index 9a4b798..4956fe8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java
@@ -233,12 +233,14 @@
     private void updateDimmedActivated(AmbientState ambientState, StackScrollState resultState,
             StackScrollAlgorithmState algorithmState) {
         boolean dimmed = ambientState.isDimmed();
+        boolean dark = ambientState.isDark();
         View activatedChild = ambientState.getActivatedChild();
         int childCount = algorithmState.visibleChildren.size();
         for (int i = 0; i < childCount; i++) {
             View child = algorithmState.visibleChildren.get(i);
             StackScrollState.ViewState childViewState = resultState.getViewStateForView(child);
             childViewState.dimmed = dimmed;
+            childViewState.dark = dark;
             boolean isActivatedChild = activatedChild == child;
             childViewState.scale = !dimmed || isActivatedChild
                     ? 1.0f
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java
index 02f2cd6..d8407d5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java
@@ -148,6 +148,9 @@
                 // apply dimming
                 child.setDimmed(state.dimmed, false /* animate */);
 
+                // apply dark
+                child.setDark(state.dark, false /* animate */);
+
                 // apply scrimming
                 child.setScrimAmount(state.scrimAmount);
 
@@ -224,6 +227,7 @@
         boolean gone;
         float scale;
         boolean dimmed;
+        boolean dark;
 
         /**
          * A value between 0 and 1 indicating how much the view should be scrimmed.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java
index 0006dad..5efbc99 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java
@@ -176,6 +176,9 @@
         // start dimmed animation
         child.setDimmed(viewState.dimmed, mAnimationFilter.animateDimmed);
 
+        // start dark animation
+        child.setDark(viewState.dark, mAnimationFilter.animateDark);
+
         // apply scrimming
         child.setScrimAmount(viewState.scrimAmount);