Merge "Add no more than 20 timestamps for a config in StatsdStats."
diff --git a/api/current.txt b/api/current.txt
index 8329aca..00a9452 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -23256,16 +23256,20 @@
     field public static final int MEDIA_TRACK_TYPE_VIDEO = 1; // 0x1
   }
 
-  public class MediaRecorder {
+  public class MediaRecorder implements android.media.AudioRouting {
     ctor public MediaRecorder();
+    method public void addOnRoutingChangedListener(android.media.AudioRouting.OnRoutingChangedListener, android.os.Handler);
     method protected void finalize();
     method public static final int getAudioSourceMax();
     method public int getMaxAmplitude() throws java.lang.IllegalStateException;
     method public android.os.PersistableBundle getMetrics();
+    method public android.media.AudioDeviceInfo getPreferredDevice();
+    method public android.media.AudioDeviceInfo getRoutedDevice();
     method public android.view.Surface getSurface();
     method public void pause() throws java.lang.IllegalStateException;
     method public void prepare() throws java.io.IOException, java.lang.IllegalStateException;
     method public void release();
+    method public void removeOnRoutingChangedListener(android.media.AudioRouting.OnRoutingChangedListener);
     method public void reset();
     method public void resume() throws java.lang.IllegalStateException;
     method public void setAudioChannels(int);
@@ -23288,6 +23292,7 @@
     method public void setOutputFile(java.io.File);
     method public void setOutputFile(java.lang.String) throws java.lang.IllegalStateException;
     method public void setOutputFormat(int) throws java.lang.IllegalStateException;
+    method public boolean setPreferredDevice(android.media.AudioDeviceInfo);
     method public void setPreviewDisplay(android.view.Surface);
     method public void setProfile(android.media.CamcorderProfile);
     method public void setVideoEncoder(int) throws java.lang.IllegalStateException;
diff --git a/cmds/screencap/screencap.cpp b/cmds/screencap/screencap.cpp
index 6ded246..3172281 100644
--- a/cmds/screencap/screencap.cpp
+++ b/cmds/screencap/screencap.cpp
@@ -159,7 +159,7 @@
     void const* mapbase = MAP_FAILED;
     ssize_t mapsize = -1;
 
-    void const* base = NULL;
+    void* base = NULL;
     uint32_t w, s, h, f;
     android_dataspace d;
     size_t size = 0;
@@ -179,7 +179,6 @@
     ProcessState::self()->setThreadPoolMaxThreadCount(0);
     ProcessState::self()->startThreadPool();
 
-    ScreenshotClient screenshot;
     sp<IBinder> display = SurfaceComposerClient::getBuiltInDisplay(displayId);
     if (display == NULL) {
         fprintf(stderr, "Unable to get handle for display %d\n", displayId);
@@ -199,51 +198,57 @@
     uint8_t displayOrientation = configs[activeConfig].orientation;
     uint32_t captureOrientation = ORIENTATION_MAP[displayOrientation];
 
-    status_t result = screenshot.update(display, Rect(),
-            0 /* reqWidth */, 0 /* reqHeight */,
-            INT32_MIN, INT32_MAX, /* all layers */
-            false, captureOrientation);
-    if (result == NO_ERROR) {
-        base = screenshot.getPixels();
-        w = screenshot.getWidth();
-        h = screenshot.getHeight();
-        s = screenshot.getStride();
-        f = screenshot.getFormat();
-        d = screenshot.getDataSpace();
-        size = screenshot.getSize();
+    sp<GraphicBuffer> outBuffer;
+    status_t result = ScreenshotClient::capture(display, Rect(), 0 /* reqWidth */,
+            0 /* reqHeight */, INT32_MIN, INT32_MAX, /* all layers */ false, captureOrientation,
+            &outBuffer);
+    if (result != NO_ERROR) {
+        close(fd);
+        _exit(1);
     }
 
-    if (base != NULL) {
-        if (png) {
-            const SkImageInfo info =
-                SkImageInfo::Make(w, h, flinger2skia(f), kPremul_SkAlphaType,
-                    dataSpaceToColorSpace(d));
-            SkPixmap pixmap(info, base, s * bytesPerPixel(f));
-            struct FDWStream final : public SkWStream {
-              size_t fBytesWritten = 0;
-              int fFd;
-              FDWStream(int f) : fFd(f) {}
-              size_t bytesWritten() const override { return fBytesWritten; }
-              bool write(const void* buffer, size_t size) override {
-                fBytesWritten += size;
-                return size == 0 || ::write(fFd, buffer, size) > 0;
-              }
-            } fdStream(fd);
-            (void)SkEncodeImage(&fdStream, pixmap, SkEncodedImageFormat::kPNG, 100);
-            if (fn != NULL) {
-                notifyMediaScanner(fn);
-            }
-        } else {
-            uint32_t c = dataSpaceToInt(d);
-            write(fd, &w, 4);
-            write(fd, &h, 4);
-            write(fd, &f, 4);
-            write(fd, &c, 4);
-            size_t Bpp = bytesPerPixel(f);
-            for (size_t y=0 ; y<h ; y++) {
-                write(fd, base, w*Bpp);
-                base = (void *)((char *)base + s*Bpp);
-            }
+    result = outBuffer->lock(GraphicBuffer::USAGE_SW_READ_OFTEN, &base);
+
+    if (base == NULL) {
+        close(fd);
+        _exit(1);
+    }
+
+    w = outBuffer->getWidth();
+    h = outBuffer->getHeight();
+    s = outBuffer->getStride();
+    f = outBuffer->getPixelFormat();
+    d = HAL_DATASPACE_UNKNOWN;
+    size = s * h * bytesPerPixel(f);
+
+    if (png) {
+        const SkImageInfo info =
+            SkImageInfo::Make(w, h, flinger2skia(f), kPremul_SkAlphaType, dataSpaceToColorSpace(d));
+        SkPixmap pixmap(info, base, s * bytesPerPixel(f));
+        struct FDWStream final : public SkWStream {
+          size_t fBytesWritten = 0;
+          int fFd;
+          FDWStream(int f) : fFd(f) {}
+          size_t bytesWritten() const override { return fBytesWritten; }
+          bool write(const void* buffer, size_t size) override {
+            fBytesWritten += size;
+            return size == 0 || ::write(fFd, buffer, size) > 0;
+          }
+        } fdStream(fd);
+        (void)SkEncodeImage(&fdStream, pixmap, SkEncodedImageFormat::kPNG, 100);
+        if (fn != NULL) {
+            notifyMediaScanner(fn);
+        }
+    } else {
+        uint32_t c = dataSpaceToInt(d);
+        write(fd, &w, 4);
+        write(fd, &h, 4);
+        write(fd, &f, 4);
+        write(fd, &c, 4);
+        size_t Bpp = bytesPerPixel(f);
+        for (size_t y=0 ; y<h ; y++) {
+            write(fd, base, w*Bpp);
+            base = (void *)((char *)base + s*Bpp);
         }
     }
     close(fd);
@@ -253,4 +258,4 @@
 
     // b/36066697: Avoid running static destructors.
     _exit(0);
-}
+}
\ No newline at end of file
diff --git a/core/java/android/database/OWNERS b/core/java/android/database/OWNERS
new file mode 100644
index 0000000..84e81e4
--- /dev/null
+++ b/core/java/android/database/OWNERS
@@ -0,0 +1,2 @@
+fkupolov@google.com
+omakoto@google.com
\ No newline at end of file
diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java
index 5dd8d05..068f5f7 100644
--- a/core/java/android/os/PowerManager.java
+++ b/core/java/android/os/PowerManager.java
@@ -555,6 +555,11 @@
         int FORCE_ALL_APPS_STANDBY = 11;
 
         /**
+         * Whether to enable background check on all apps or not.
+         */
+        int FORCE_BACKGROUND_CHECK = 12;
+
+        /**
          * Whether to disable non-essential sensors. (e.g. edge sensors.)
          */
         int OPTIONAL_SENSORS = 13;
diff --git a/core/java/android/service/autofill/AutofillService.java b/core/java/android/service/autofill/AutofillService.java
index 953501c..cd362c7 100644
--- a/core/java/android/service/autofill/AutofillService.java
+++ b/core/java/android/service/autofill/AutofillService.java
@@ -438,8 +438,21 @@
  *  AutofillValue password = passwordNode.getAutofillValue().getTextValue().toString();
  *
  *  save(username, password);
- *
  * </pre>
+ *
+ *
+ * <a name="Privacy"></a>
+ * <h3>Privacy</h3>
+ *
+ * <p>The {@link #onFillRequest(FillRequest, CancellationSignal, FillCallback)} method is called
+ * without the user content. The Android system strips some properties of the
+ * {@link android.app.assist.AssistStructure.ViewNode view nodes} passed to these calls, but not all
+ * of them. For example, the data provided in the {@link android.view.ViewStructure.HtmlInfo}
+ * objects set by {@link android.webkit.WebView} is never stripped out.
+ *
+ * <p>Because this data could contain PII (Personally Identifiable Information, such as username or
+ * email address), the service should only use it locally (i.e., in the app's process) for
+ * heuristics purposes, but it should not be sent to external servers.
  */
 public abstract class AutofillService extends Service {
     private static final String TAG = "AutofillService";
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index 3d01ec2..cf05910 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -55,8 +55,6 @@
     private static native void nativeScreenshot(IBinder displayToken, Surface consumer,
             Rect sourceCrop, int width, int height, int minLayer, int maxLayer,
             boolean allLayers, boolean useIdentityTransform);
-    private static native void nativeCaptureLayers(IBinder layerHandleToken, Surface consumer,
-            Rect sourceCrop, float frameScale);
     private static native GraphicBuffer nativeCaptureLayers(IBinder layerHandleToken,
             Rect sourceCrop, float frameScale);
 
@@ -1179,22 +1177,14 @@
      * Captures a layer and its children into the provided {@link Surface}.
      *
      * @param layerHandleToken The root layer to capture.
-     * @param consumer         The {@link Surface} to capture the layer into.
      * @param sourceCrop       The portion of the root surface to capture; caller may pass in 'new
      *                         Rect()' or null if no cropping is desired.
      * @param frameScale       The desired scale of the returned buffer; the raw
      *                         screen will be scaled up/down.
+     *
+     * @return Returns a GraphicBuffer that contains the layer capture.
      */
-    public static void captureLayers(IBinder layerHandleToken, Surface consumer, Rect sourceCrop,
-            float frameScale) {
-        nativeCaptureLayers(layerHandleToken, consumer, sourceCrop, frameScale);
-    }
-
-    /**
-     * Same as {@link #captureLayers(IBinder, Surface, Rect, float)} except this
-     * captures to a {@link GraphicBuffer} instead of a {@link Surface}.
-     */
-    public static GraphicBuffer captureLayersToBuffer(IBinder layerHandleToken, Rect sourceCrop,
+    public static GraphicBuffer captureLayers(IBinder layerHandleToken, Rect sourceCrop,
             float frameScale) {
         return nativeCaptureLayers(layerHandleToken, sourceCrop, frameScale);
     }
diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java
index 665d694..6f99254 100644
--- a/core/java/android/webkit/WebView.java
+++ b/core/java/android/webkit/WebView.java
@@ -2758,7 +2758,9 @@
      * <p>For example, an HTML form with 2 fields for username and password:
      *
      * <pre class="prettyprint">
+     *    &lt;label&gt;Username:&lt;/label&gt;
      *    &lt;input type="text" name="username" id="user" value="Type your username" autocomplete="username" placeholder="Email or username"&gt;
+     *    &lt;label&gt;Password:&lt;/label&gt;
      *    &lt;input type="password" name="password" id="pass" autocomplete="current-password" placeholder="Password"&gt;
      * </pre>
      *
@@ -2772,6 +2774,7 @@
      *     username.setHtmlInfo(username.newHtmlInfoBuilder("input")
      *         .addAttribute("type", "text")
      *         .addAttribute("name", "username")
+     *         .addAttribute("label", "Username:")
      *         .build());
      *     username.setHint("Email or username");
      *     username.setAutofillType(View.AUTOFILL_TYPE_TEXT);
@@ -2785,6 +2788,7 @@
      *     password.setHtmlInfo(password.newHtmlInfoBuilder("input")
      *         .addAttribute("type", "password")
      *         .addAttribute("name", "password")
+     *         .addAttribute("label", "Password:")
      *         .build());
      *     password.setHint("Password");
      *     password.setAutofillType(View.AUTOFILL_TYPE_TEXT);
diff --git a/core/jni/android_view_Surface.cpp b/core/jni/android_view_Surface.cpp
index 3ad4da6..421e0de 100644
--- a/core/jni/android_view_Surface.cpp
+++ b/core/jni/android_view_Surface.cpp
@@ -517,23 +517,7 @@
         jobject graphicBuffer) {
     Surface* surface = reinterpret_cast<Surface*>(nativeObject);
     sp<GraphicBuffer> bp = graphicBufferForJavaObject(env, graphicBuffer);
-    if (bp == nullptr) {
-        return BAD_VALUE;
-    }
-    int err = ((ANativeWindow*)surface)->perform(surface, NATIVE_WINDOW_API_CONNECT,
-            NATIVE_WINDOW_API_CPU);
-    if (err != OK) {
-        return err;
-    }
-    err = surface->attachBuffer(bp->getNativeBuffer());
-    if (err != OK) {
-        return err;
-    }
-    err = ((ANativeWindow*)surface)->queueBuffer(surface, bp->getNativeBuffer(), -1);
-    if (err != OK) {
-        return err;
-    }
-    err = surface->disconnect(NATIVE_WINDOW_API_CPU);
+    int err = Surface::attachAndQueueBuffer(surface, bp);
     return err;
 }
 
diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp
index f77e6c4..8c968a2 100644
--- a/core/jni/android_view_SurfaceControl.cpp
+++ b/core/jni/android_view_SurfaceControl.cpp
@@ -167,7 +167,7 @@
         maxLayer = INT32_MAX;
     }
     sp<GraphicBuffer> buffer;
-    status_t res = ScreenshotClient::captureToBuffer(displayToken,
+    status_t res = ScreenshotClient::capture(displayToken,
             sourceCrop, width, height, minLayer, maxLayer, useIdentityTransform,
             rotation, &buffer);
     if (res != NO_ERROR) {
@@ -201,15 +201,18 @@
         maxLayer = INT32_MAX;
     }
 
-    res = screenshot->update(displayToken, sourceCrop, width, height,
-        minLayer, maxLayer, useIdentityTransform, static_cast<uint32_t>(rotation));
+    sp<GraphicBuffer> buffer;
+    res = ScreenshotClient::capture(displayToken, sourceCrop, width, height,
+        minLayer, maxLayer, useIdentityTransform, static_cast<uint32_t>(rotation), &buffer);
     if (res != NO_ERROR) {
         return NULL;
     }
 
     SkColorType colorType;
     SkAlphaType alphaType;
-    switch (screenshot->getFormat()) {
+
+    PixelFormat format = buffer->getPixelFormat();
+    switch (format) {
         case PIXEL_FORMAT_RGBX_8888: {
             colorType = kRGBA_8888_SkColorType;
             alphaType = kOpaque_SkAlphaType;
@@ -235,66 +238,20 @@
         }
     }
 
-    sk_sp<SkColorSpace> colorSpace;
-    if (screenshot->getDataSpace() == HAL_DATASPACE_DISPLAY_P3) {
-        colorSpace = SkColorSpace::MakeRGB(
-                SkColorSpace::kSRGB_RenderTargetGamma, SkColorSpace::kDCIP3_D65_Gamut);
-    } else {
-        colorSpace = SkColorSpace::MakeSRGB();
-    }
+    SkImageInfo info = SkImageInfo::Make(buffer->getWidth(), buffer->getHeight(),
+                                         colorType, alphaType,
+                                         SkColorSpace::MakeSRGB());
 
-    SkImageInfo screenshotInfo = SkImageInfo::Make(screenshot->getWidth(),
-                                                   screenshot->getHeight(),
-                                                   colorType,
-                                                   alphaType,
-                                                   colorSpace);
-
-    const size_t rowBytes =
-            screenshot->getStride() * android::bytesPerPixel(screenshot->getFormat());
-
-    if (!screenshotInfo.width() || !screenshotInfo.height()) {
-        return NULL;
-    }
-
-    auto bitmap = new Bitmap(
-            (void*) screenshot->getPixels(), (void*) screenshot.get(), DeleteScreenshot,
-            screenshotInfo, rowBytes);
-    screenshot.release();
-    bitmap->setImmutable();
-    return bitmap::createBitmap(env, bitmap,
-            android::bitmap::kBitmapCreateFlag_Premultiplied, NULL);
+    auto bitmap = sk_sp<Bitmap>(new Bitmap(buffer.get(), info));
+    return bitmap::createBitmap(env, bitmap.release(),
+                                android::bitmap::kBitmapCreateFlag_Premultiplied, NULL);
 }
 
 static void nativeScreenshot(JNIEnv* env, jclass clazz, jobject displayTokenObj,
         jobject surfaceObj, jobject sourceCropObj, jint width, jint height,
         jint minLayer, jint maxLayer, bool allLayers, bool useIdentityTransform) {
     sp<IBinder> displayToken = ibinderForJavaObject(env, displayTokenObj);
-    if (displayToken != NULL) {
-        sp<Surface> consumer = android_view_Surface_getSurface(env, surfaceObj);
-        if (consumer != NULL) {
-            int left = env->GetIntField(sourceCropObj, gRectClassInfo.left);
-            int top = env->GetIntField(sourceCropObj, gRectClassInfo.top);
-            int right = env->GetIntField(sourceCropObj, gRectClassInfo.right);
-            int bottom = env->GetIntField(sourceCropObj, gRectClassInfo.bottom);
-            Rect sourceCrop(left, top, right, bottom);
-
-            if (allLayers) {
-                minLayer = INT32_MIN;
-                maxLayer = INT32_MAX;
-            }
-            ScreenshotClient::capture(displayToken,
-                    consumer->getIGraphicBufferProducer(), sourceCrop,
-                    width, height, minLayer, maxLayer,
-                    useIdentityTransform);
-        }
-    }
-}
-
-static void nativeCaptureLayers(JNIEnv* env, jclass clazz, jobject layerHandleToken,
-        jobject surfaceObj, jobject sourceCropObj, jfloat frameScale) {
-
-    sp<IBinder> layerHandle = ibinderForJavaObject(env, layerHandleToken);
-    if (layerHandle == NULL) {
+    if (displayToken == NULL) {
         return;
     }
 
@@ -308,11 +265,19 @@
         sourceCrop = rectFromObj(env, sourceCropObj);
     }
 
-    ScreenshotClient::captureLayers(layerHandle, consumer->getIGraphicBufferProducer(), sourceCrop,
-            frameScale);
+    if (allLayers) {
+        minLayer = INT32_MIN;
+        maxLayer = INT32_MAX;
+    }
+
+    sp<GraphicBuffer> buffer;
+    ScreenshotClient::capture(displayToken, sourceCrop, width, height, minLayer, maxLayer,
+                              useIdentityTransform, 0, &buffer);
+
+    Surface::attachAndQueueBuffer(consumer.get(), buffer);
 }
 
-static jobject nativeCaptureLayersToBuffer(JNIEnv* env, jclass clazz, jobject layerHandleToken,
+static jobject nativeCaptureLayers(JNIEnv* env, jclass clazz, jobject layerHandleToken,
         jobject sourceCropObj, jfloat frameScale) {
 
     sp<IBinder> layerHandle = ibinderForJavaObject(env, layerHandleToken);
@@ -326,8 +291,7 @@
     }
 
     sp<GraphicBuffer> buffer;
-    status_t res = ScreenshotClient::captureLayersToBuffer(layerHandle, sourceCrop, frameScale,
-            &buffer);
+    status_t res = ScreenshotClient::captureLayers(layerHandle, sourceCrop, frameScale, &buffer);
     if (res != NO_ERROR) {
         return NULL;
     }
@@ -1010,10 +974,8 @@
     {"nativeScreenshotToBuffer",
      "(Landroid/os/IBinder;Landroid/graphics/Rect;IIIIZZI)Landroid/graphics/GraphicBuffer;",
      (void*)nativeScreenshotToBuffer },
-    {"nativeCaptureLayers", "(Landroid/os/IBinder;Landroid/view/Surface;Landroid/graphics/Rect;F)V",
-            (void*)nativeCaptureLayers },
     {"nativeCaptureLayers", "(Landroid/os/IBinder;Landroid/graphics/Rect;F)Landroid/graphics/GraphicBuffer;",
-            (void*)nativeCaptureLayersToBuffer },
+            (void*)nativeCaptureLayers },
 };
 
 int register_android_view_SurfaceControl(JNIEnv* env)
diff --git a/core/proto/android/server/alarmmanagerservice.proto b/core/proto/android/server/alarmmanagerservice.proto
index d2cd190..d724437 100644
--- a/core/proto/android/server/alarmmanagerservice.proto
+++ b/core/proto/android/server/alarmmanagerservice.proto
@@ -20,6 +20,7 @@
 import "frameworks/base/core/proto/android/app/pendingintent.proto";
 import "frameworks/base/core/proto/android/internal/locallog.proto";
 import "frameworks/base/core/proto/android/os/worksource.proto";
+import "frameworks/base/core/proto/android/server/forceappstandbytracker.proto";
 
 package com.android.server;
 
@@ -32,10 +33,9 @@
   optional int64 last_time_change_realtime = 4;
   // Current settings
   optional ConstantsProto settings = 5;
-  // UIDs currently in the foreground.
-  repeated int32 foreground_uids = 6;
-  // Packages forced into app standby.
-  repeated string forced_app_standby_packages = 7;
+
+  // Dump from ForceAppStandbyTracker.
+  optional ForceAppStandbyTrackerProto force_app_standby_tracker = 6;
 
   optional bool is_interactive = 8;
   // Only valid if is_interactive is false.
diff --git a/core/proto/android/server/forceappstandbytracker.proto b/core/proto/android/server/forceappstandbytracker.proto
new file mode 100644
index 0000000..8753bf7
--- /dev/null
+++ b/core/proto/android/server/forceappstandbytracker.proto
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+
+syntax = "proto2";
+
+package com.android.server;
+
+option java_multiple_files = true;
+
+// Dump from ForceAppStandbyTracker.
+message ForceAppStandbyTrackerProto {
+  // Whether all apps are forced standby or not.
+  optional bool force_all_apps_standby = 1;
+
+  // UIDs currently in the foreground.
+  repeated int32 foreground_uids = 2;
+
+  // App ids that are in power-save whitelist.
+  repeated int32 power_save_whitelist_app_ids = 3;
+
+  // App ids that are in temporary power-save whitelist.
+  repeated int32 temp_power_save_whitelist_app_ids = 4;
+
+  message RunAnyInBackgroundRestrictedPackages {
+    optional int32 uid = 1;
+    optional string package_name = 2;
+  }
+
+  // Packages that are disallowed OP_RUN_ANY_IN_BACKGROUND.
+  repeated RunAnyInBackgroundRestrictedPackages run_any_in_background_restricted_packages = 5;
+}
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 3512793..65a7ec6 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -1955,7 +1955,7 @@
     <string name="config_dozeLongPressSensorType" translatable="false"></string>
 
     <!-- Control whether the always on display mode is available. This should only be enabled on
-         devices where the display has be tuned to be power efficient in DOZE and/or DOZE_SUSPEND
+         devices where the display has been tuned to be power efficient in DOZE and/or DOZE_SUSPEND
          states. -->
     <bool name="config_dozeAlwaysOnDisplayAvailable">false</bool>
 
diff --git a/media/java/android/media/MediaRecorder.java b/media/java/android/media/MediaRecorder.java
index 7678490..3c49b80 100644
--- a/media/java/android/media/MediaRecorder.java
+++ b/media/java/android/media/MediaRecorder.java
@@ -25,6 +25,7 @@
 import android.os.Looper;
 import android.os.Message;
 import android.os.PersistableBundle;
+import android.util.ArrayMap;
 import android.util.Log;
 import android.view.Surface;
 
@@ -34,6 +35,8 @@
 import java.io.RandomAccessFile;
 import java.lang.ref.WeakReference;
 
+import com.android.internal.annotations.GuardedBy;
+
 /**
  * Used to record audio and video. The recording control is based on a
  * simple state machine (see below).
@@ -76,7 +79,7 @@
  * <a href="{@docRoot}guide/topics/media/audio-capture.html">Audio Capture</a> developer guide.</p>
  * </div>
  */
-public class MediaRecorder
+public class MediaRecorder implements AudioRouting
 {
     static {
         System.loadLibrary("media_jni");
@@ -1243,6 +1246,7 @@
         private static final int MEDIA_RECORDER_TRACK_EVENT_INFO       = 101;
         private static final int MEDIA_RECORDER_TRACK_EVENT_LIST_END   = 1000;
 
+        private static final int MEDIA_RECORDER_AUDIO_ROUTING_CHANGED  = 10000;
 
         @Override
         public void handleMessage(Message msg) {
@@ -1265,6 +1269,16 @@
 
                 return;
 
+            case MEDIA_RECORDER_AUDIO_ROUTING_CHANGED:
+                AudioManager.resetAudioPortGeneration();
+                synchronized (mRoutingChangeListeners) {
+                    for (NativeRoutingEventHandlerDelegate delegate
+                            : mRoutingChangeListeners.values()) {
+                        delegate.notifyClient();
+                    }
+                }
+                return;
+
             default:
                 Log.e(TAG, "Unknown message type " + msg.what);
                 return;
@@ -1272,6 +1286,155 @@
         }
     }
 
+    //--------------------------------------------------------------------------
+    // Explicit Routing
+    //--------------------
+    private AudioDeviceInfo mPreferredDevice = null;
+
+    /**
+     * Specifies an audio device (via an {@link AudioDeviceInfo} object) to route
+     * the input from this MediaRecorder.
+     * @param deviceInfo The {@link AudioDeviceInfo} specifying the audio source.
+     *  If deviceInfo is null, default routing is restored.
+     * @return true if succesful, false if the specified {@link AudioDeviceInfo} is non-null and
+     * does not correspond to a valid audio input device.
+     */
+    @Override
+    public boolean setPreferredDevice(AudioDeviceInfo deviceInfo) {
+        if (deviceInfo != null && !deviceInfo.isSource()) {
+            return false;
+        }
+        int preferredDeviceId = deviceInfo != null ? deviceInfo.getId() : 0;
+        boolean status = native_setInputDevice(preferredDeviceId);
+        if (status == true) {
+            synchronized (this) {
+                mPreferredDevice = deviceInfo;
+            }
+        }
+        return status;
+    }
+
+    /**
+     * Returns the selected input device specified by {@link #setPreferredDevice}. Note that this
+     * is not guaranteed to correspond to the actual device being used for recording.
+     */
+    @Override
+    public AudioDeviceInfo getPreferredDevice() {
+        synchronized (this) {
+            return mPreferredDevice;
+        }
+    }
+
+    /**
+     * Returns an {@link AudioDeviceInfo} identifying the current routing of this MediaRecorder
+     * Note: The query is only valid if the MediaRecorder is currently recording.
+     * If the recorder is not recording, the returned device can be null or correspond to previously
+     * selected device when the recorder was last active.
+     */
+    @Override
+    public AudioDeviceInfo getRoutedDevice() {
+        int deviceId = native_getRoutedDeviceId();
+        if (deviceId == 0) {
+            return null;
+        }
+        AudioDeviceInfo[] devices =
+                AudioManager.getDevicesStatic(AudioManager.GET_DEVICES_INPUTS);
+        for (int i = 0; i < devices.length; i++) {
+            if (devices[i].getId() == deviceId) {
+                return devices[i];
+            }
+        }
+        return null;
+    }
+
+    /*
+     * Call BEFORE adding a routing callback handler or AFTER removing a routing callback handler.
+     */
+    private void enableNativeRoutingCallbacksLocked(boolean enabled) {
+        if (mRoutingChangeListeners.size() == 0) {
+            native_enableDeviceCallback(enabled);
+        }
+    }
+
+    /**
+     * The list of AudioRouting.OnRoutingChangedListener interfaces added (with
+     * {@link #addOnRoutingChangedListener(android.media.AudioRouting.OnRoutingChangedListener, Handler)}
+     * by an app to receive (re)routing notifications.
+     */
+    @GuardedBy("mRoutingChangeListeners")
+    private ArrayMap<AudioRouting.OnRoutingChangedListener,
+            NativeRoutingEventHandlerDelegate> mRoutingChangeListeners = new ArrayMap<>();
+
+    /**
+     * Adds an {@link AudioRouting.OnRoutingChangedListener} to receive notifications of routing
+     * changes on this MediaRecorder.
+     * @param listener The {@link AudioRouting.OnRoutingChangedListener} interface to receive
+     * notifications of rerouting events.
+     * @param handler  Specifies the {@link Handler} object for the thread on which to execute
+     * the callback. If <code>null</code>, the handler on the main looper will be used.
+     */
+    @Override
+    public void addOnRoutingChangedListener(AudioRouting.OnRoutingChangedListener listener,
+                                            Handler handler) {
+        synchronized (mRoutingChangeListeners) {
+            if (listener != null && !mRoutingChangeListeners.containsKey(listener)) {
+                enableNativeRoutingCallbacksLocked(true);
+                mRoutingChangeListeners.put(
+                        listener, new NativeRoutingEventHandlerDelegate(this, listener, handler));
+            }
+        }
+    }
+
+    /**
+     * Removes an {@link AudioRouting.OnRoutingChangedListener} which has been previously added
+     * to receive rerouting notifications.
+     * @param listener The previously added {@link AudioRouting.OnRoutingChangedListener} interface
+     * to remove.
+     */
+    @Override
+    public void removeOnRoutingChangedListener(AudioRouting.OnRoutingChangedListener listener) {
+        synchronized (mRoutingChangeListeners) {
+            if (mRoutingChangeListeners.containsKey(listener)) {
+                mRoutingChangeListeners.remove(listener);
+                enableNativeRoutingCallbacksLocked(false);
+            }
+        }
+    }
+
+    /**
+     * Helper class to handle the forwarding of native events to the appropriate listener
+     * (potentially) handled in a different thread
+     */
+    private class NativeRoutingEventHandlerDelegate {
+        private MediaRecorder mMediaRecorder;
+        private AudioRouting.OnRoutingChangedListener mOnRoutingChangedListener;
+        private Handler mHandler;
+
+        NativeRoutingEventHandlerDelegate(final MediaRecorder mediaRecorder,
+                final AudioRouting.OnRoutingChangedListener listener, Handler handler) {
+            mMediaRecorder = mediaRecorder;
+            mOnRoutingChangedListener = listener;
+            mHandler = handler != null ? handler : mEventHandler;
+        }
+
+        void notifyClient() {
+            if (mHandler != null) {
+                mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        if (mOnRoutingChangedListener != null) {
+                            mOnRoutingChangedListener.onRoutingChanged(mMediaRecorder);
+                        }
+                    }
+                });
+            }
+        }
+    }
+
+    private native final boolean native_setInputDevice(int deviceId);
+    private native final int native_getRoutedDeviceId();
+    private native final void native_enableDeviceCallback(boolean enabled);
+
     /**
      * Called from native code when an interesting event happens.  This method
      * just uses the EventHandler system to post the event back to the main app thread.
diff --git a/media/jni/android_media_MediaRecorder.cpp b/media/jni/android_media_MediaRecorder.cpp
index 497684c..d2bc174 100644
--- a/media/jni/android_media_MediaRecorder.cpp
+++ b/media/jni/android_media_MediaRecorder.cpp
@@ -657,6 +657,56 @@
     return mybundle;
 
 }
+
+static jboolean
+android_media_MediaRecorder_setInputDevice(JNIEnv *env, jobject thiz, jint device_id)
+{
+    ALOGV("android_media_MediaRecorder_setInputDevice");
+
+    sp<MediaRecorder> mr = getMediaRecorder(env, thiz);
+    if (mr == NULL) {
+        jniThrowException(env, "java/lang/IllegalStateException", NULL);
+        return false;
+    }
+
+    if (process_media_recorder_call(env, mr->setInputDevice(device_id),
+            "java/lang/RuntimeException", "setInputDevice failed.")) {
+        return false;
+    }
+    return true;
+}
+
+static jint
+android_media_MediaRecorder_getRoutedDeviceId(JNIEnv *env, jobject thiz)
+{
+    ALOGV("android_media_MediaRecorder_getRoutedDeviceId");
+
+    sp<MediaRecorder> mr = getMediaRecorder(env, thiz);
+    if (mr == NULL) {
+        jniThrowException(env, "java/lang/IllegalStateException", NULL);
+        return AUDIO_PORT_HANDLE_NONE;
+    }
+
+    audio_port_handle_t deviceId;
+    process_media_recorder_call(env, mr->getRoutedDeviceId(&deviceId),
+            "java/lang/RuntimeException", "getRoutedDeviceId failed.");
+    return (jint) deviceId;
+}
+
+static void
+android_media_MediaRecorder_enableDeviceCallback(JNIEnv *env, jobject thiz, jboolean enabled)
+{
+    ALOGV("android_media_MediaRecorder_enableDeviceCallback %d", enabled);
+
+    sp<MediaRecorder> mr = getMediaRecorder(env, thiz);
+    if (mr == NULL) {
+        jniThrowException(env, "java/lang/IllegalStateException", NULL);
+        return;
+    }
+
+    process_media_recorder_call(env, mr->enableAudioDeviceCallback(enabled),
+            "java/lang/RuntimeException", "enableDeviceCallback failed.");
+}
 // ----------------------------------------------------------------------------
 
 static const JNINativeMethod gMethods[] = {
@@ -689,6 +739,10 @@
     {"native_setInputSurface", "(Landroid/view/Surface;)V", (void *)android_media_MediaRecorder_setInputSurface },
 
     {"native_getMetrics",    "()Landroid/os/PersistableBundle;", (void *)android_media_MediaRecorder_native_getMetrics},
+
+    {"native_setInputDevice", "(I)Z",                           (void *)android_media_MediaRecorder_setInputDevice},
+    {"native_getRoutedDeviceId", "()I",                         (void *)android_media_MediaRecorder_getRoutedDeviceId},
+    {"native_enableDeviceCallback", "(Z)V",                      (void *)android_media_MediaRecorder_enableDeviceCallback},
 };
 
 // This function only registers the native methods, and is called from
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index cf79238..8e065d1 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -250,10 +250,7 @@
     <string name="doze_brightness_sensor_type" translatable="false"></string>
 
     <!-- Doze: pulse parameter - how long does it take to fade in? -->
-    <integer name="doze_pulse_duration_in">900</integer>
-
-    <!-- Doze: pulse parameter - how long does it take to fade in after a pickup? -->
-    <integer name="doze_pulse_duration_in_pickup">130</integer>
+    <integer name="doze_pulse_duration_in">130</integer>
 
     <!-- Doze: pulse parameter - once faded in, how long does it stay visible? -->
     <integer name="doze_pulse_duration_visible">6000</integer>
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index cfd95b4..884e189 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -47,6 +47,7 @@
     <item type="id" name="qs_icon_tag"/>
     <item type="id" name="qs_slash_tag"/>
     <item type="id" name="scrim"/>
+    <item type="id" name="scrim_blanking"/>
     <item type="id" name="scrim_target"/>
     <item type="id" name="scrim_alpha_start"/>
     <item type="id" name="scrim_alpha_end"/>
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java
index 0c067ff..526a8f4 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java
@@ -28,6 +28,7 @@
 import com.android.systemui.keyguard.DismissCallbackRegistry;
 import com.android.systemui.statusbar.KeyguardIndicationController;
 import com.android.systemui.statusbar.ScrimView;
+import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.phone.KeyguardBouncer;
 import com.android.systemui.statusbar.phone.LightBarController;
 import com.android.systemui.statusbar.phone.LockIcon;
@@ -86,10 +87,10 @@
 
     public ScrimController createScrimController(LightBarController lightBarController,
             ScrimView scrimBehind, ScrimView scrimInFront, View headsUpScrim,
-            LockscreenWallpaper lockscreenWallpaper,
-            Consumer<Boolean> scrimVisibleListener) {
+            LockscreenWallpaper lockscreenWallpaper, Consumer<Boolean> scrimVisibleListener,
+            DozeParameters dozeParameters) {
         return new ScrimController(lightBarController, scrimBehind, scrimInFront, headsUpScrim,
-                scrimVisibleListener);
+                scrimVisibleListener, dozeParameters);
     }
 
     public NotificationIconAreaController createNotificationIconAreaController(Context context,
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java b/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java
index 7db118d..2f607ee 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeHost.java
@@ -35,7 +35,6 @@
     boolean isBlockingDoze();
 
     void startPendingIntentDismissingKeyguard(PendingIntent intent);
-    void abortPulsing();
     void extendPulse();
 
     void setAnimateWakeup(boolean animateWakeup);
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index b99e76a..c92acd0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -2028,12 +2028,9 @@
     }
 
     public StatusBarKeyguardViewManager registerStatusBar(StatusBar statusBar,
-            ViewGroup container,
-            ScrimController scrimController,
-            FingerprintUnlockController fingerprintUnlockController) {
+            ViewGroup container, FingerprintUnlockController fingerprintUnlockController) {
         mStatusBarKeyguardViewManager.registerStatusBar(statusBar, container,
-                scrimController, fingerprintUnlockController,
-                mDismissCallbackRegistry);
+                fingerprintUnlockController, mDismissCallbackRegistry);
         return mStatusBarKeyguardViewManager;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java
index 991c3c8..e909644 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java
@@ -162,7 +162,7 @@
         Matrix matrix = new Matrix();
         int overlayColor = 0x40FFFFFF;
 
-        Bitmap picture = Bitmap.createBitmap(previewWidth, previewHeight, data.image.getConfig());
+        Bitmap picture = Bitmap.createBitmap(previewWidth, previewHeight, Bitmap.Config.ARGB_8888);
         matrix.setTranslate((previewWidth - mImageWidth) / 2, (previewHeight - mImageHeight) / 2);
         c.setBitmap(picture);
         c.drawBitmap(data.image, matrix, paint);
@@ -171,7 +171,7 @@
 
         // Note, we can't use the preview for the small icon, since it is non-square
         float scale = (float) iconSize / Math.min(mImageWidth, mImageHeight);
-        Bitmap icon = Bitmap.createBitmap(iconSize, iconSize, data.image.getConfig());
+        Bitmap icon = Bitmap.createBitmap(iconSize, iconSize, Bitmap.Config.ARGB_8888);
         matrix.setScale(scale, scale);
         matrix.postTranslate((iconSize - (scale * mImageWidth)) / 2,
                 (iconSize - (scale * mImageHeight)) / 2);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ScrimView.java b/packages/SystemUI/src/com/android/systemui/statusbar/ScrimView.java
index a53e348..fb9c037 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ScrimView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ScrimView.java
@@ -252,6 +252,13 @@
         return false;
     }
 
+    /**
+     * It might look counterintuitive to have another method to set the alpha instead of
+     * only using {@link #setAlpha(float)}. In this case we're in a hardware layer
+     * optimizing blend modes, so it makes sense.
+     *
+     * @param alpha Gradient alpha from 0 to 1.
+     */
     public void setViewAlpha(float alpha) {
         if (alpha != mViewAlpha) {
             mViewAlpha = alpha;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
index 6b7397b..3f57c2f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
@@ -46,10 +46,8 @@
     public void dump(PrintWriter pw) {
         pw.println("  DozeParameters:");
         pw.print("    getDisplayStateSupported(): "); pw.println(getDisplayStateSupported());
-        pw.print("    getPulseDuration(pickup=false): "); pw.println(getPulseDuration(false));
-        pw.print("    getPulseDuration(pickup=true): "); pw.println(getPulseDuration(true));
-        pw.print("    getPulseInDuration(pickup=false): "); pw.println(getPulseInDuration(false));
-        pw.print("    getPulseInDuration(pickup=true): "); pw.println(getPulseInDuration(true));
+        pw.print("    getPulseDuration(): "); pw.println(getPulseDuration());
+        pw.print("    getPulseInDuration(): "); pw.println(getPulseInDuration());
         pw.print("    getPulseInVisibleDuration(): "); pw.println(getPulseVisibleDuration());
         pw.print("    getPulseOutDuration(): "); pw.println(getPulseOutDuration());
         pw.print("    getPulseOnSigMotion(): "); pw.println(getPulseOnSigMotion());
@@ -81,14 +79,12 @@
         return mContext.getResources().getBoolean(R.bool.doze_suspend_display_state_supported);
     }
 
-    public int getPulseDuration(boolean pickup) {
-        return getPulseInDuration(pickup) + getPulseVisibleDuration() + getPulseOutDuration();
+    public int getPulseDuration() {
+        return getPulseInDuration() + getPulseVisibleDuration() + getPulseOutDuration();
     }
 
-    public int getPulseInDuration(boolean pickupOrDoubleTap) {
-        return pickupOrDoubleTap
-                ? getInt("doze.pulse.duration.in.pickup", R.integer.doze_pulse_duration_in_pickup)
-                : getInt("doze.pulse.duration.in", R.integer.doze_pulse_duration_in);
+    public int getPulseInDuration() {
+        return getInt("doze.pulse.duration.in", R.integer.doze_pulse_duration_in);
     }
 
     public int getPulseVisibleDuration() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java
index 8afb849..1011383 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java
@@ -16,16 +16,11 @@
 
 package com.android.systemui.statusbar.phone;
 
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
 import android.annotation.NonNull;
 import android.content.Context;
 import android.os.Handler;
 import android.util.Log;
-import android.view.animation.Interpolator;
 
-import com.android.systemui.Interpolators;
 import com.android.systemui.doze.DozeHost;
 import com.android.systemui.doze.DozeLog;
 
@@ -40,74 +35,59 @@
     private final Handler mHandler = new Handler();
     private final ScrimController mScrimController;
 
-    private final Context mContext;
-
     private boolean mDozing;
     private DozeHost.PulseCallback mPulseCallback;
     private int mPulseReason;
-    private Animator mInFrontAnimator;
-    private Animator mBehindAnimator;
-    private float mInFrontTarget;
-    private float mBehindTarget;
-    private boolean mDozingAborted;
-    private boolean mWakeAndUnlocking;
     private boolean mFullyPulsing;
 
-    private float mAodFrontScrimOpacity = 0;
-    private Runnable mSetDozeInFrontAlphaDelayed;
+    private final ScrimController.Callback mScrimCallback = new ScrimController.Callback() {
+        @Override
+        public void onDisplayBlanked() {
+            if (DEBUG) {
+                Log.d(TAG, "Pulse in, mDozing=" + mDozing + " mPulseReason="
+                        + DozeLog.pulseReasonToString(mPulseReason));
+            }
+            if (!mDozing) {
+                return;
+            }
+
+            // Signal that the pulse is ready to turn the screen on and draw.
+            pulseStarted();
+        }
+
+        @Override
+        public void onFinished() {
+            if (DEBUG) {
+                Log.d(TAG, "Pulse in finished, mDozing=" + mDozing);
+            }
+            if (!mDozing) {
+                return;
+            }
+            mHandler.postDelayed(mPulseOut, mDozeParameters.getPulseVisibleDuration());
+            mHandler.postDelayed(mPulseOutExtended,
+                    mDozeParameters.getPulseVisibleDurationExtended());
+            mFullyPulsing = true;
+        }
+
+        /**
+         * Transition was aborted before it was over.
+         */
+        @Override
+        public void onCancelled() {
+            pulseFinished();
+        }
+    };
 
     public DozeScrimController(ScrimController scrimController, Context context) {
-        mContext = context;
         mScrimController = scrimController;
         mDozeParameters = new DozeParameters(context);
     }
 
-    public void setDozing(boolean dozing, boolean animate) {
+    public void setDozing(boolean dozing) {
         if (mDozing == dozing) return;
         mDozing = dozing;
-        mWakeAndUnlocking = false;
-        if (mDozing) {
-            mDozingAborted = false;
-            abortAnimations();
-            mScrimController.setDozeBehindAlpha(1f);
-            setDozeInFrontAlpha(mDozeParameters.getAlwaysOn() ? mAodFrontScrimOpacity : 1f);
-        } else {
+        if (!mDozing) {
             cancelPulsing();
-            if (animate) {
-                startScrimAnimation(false /* inFront */, 0f /* target */,
-                        NotificationPanelView.DOZE_ANIMATION_DURATION,
-                        Interpolators.LINEAR_OUT_SLOW_IN);
-                startScrimAnimation(true /* inFront */, 0f /* target */,
-                        NotificationPanelView.DOZE_ANIMATION_DURATION,
-                        Interpolators.LINEAR_OUT_SLOW_IN);
-            } else {
-                abortAnimations();
-                mScrimController.setDozeBehindAlpha(0f);
-                setDozeInFrontAlpha(0f);
-            }
-        }
-    }
-
-    /**
-     * Set the opacity of the front scrim when showing AOD1
-     *
-     * Used to emulate lower brightness values than the hardware supports natively.
-     */
-    public void setAodDimmingScrim(float scrimOpacity) {
-        mAodFrontScrimOpacity = scrimOpacity;
-        if (mDozing && !isPulsing() && !mDozingAborted && !mWakeAndUnlocking
-                && mDozeParameters.getAlwaysOn()) {
-            setDozeInFrontAlpha(mAodFrontScrimOpacity);
-        }
-    }
-
-    public void setWakeAndUnlocking() {
-        // Immediately abort the doze scrims in case of wake-and-unlock
-        // for pulsing so the Keyguard fade-out animation scrim can take over.
-        if (!mWakeAndUnlocking) {
-            mWakeAndUnlocking = true;
-            mScrimController.setDozeBehindAlpha(0f);
-            setDozeInFrontAlpha(0f);
         }
     }
 
@@ -118,37 +98,21 @@
         }
 
         if (!mDozing || mPulseCallback != null) {
+            if (DEBUG) {
+                Log.d(TAG, "Pulse supressed. Dozing: " + mDozeParameters + " had callback? "
+                        + (mPulseCallback != null));
+            }
             // Pulse suppressed.
             callback.onPulseFinished();
             return;
         }
 
-        // Begin pulse.  Note that it's very important that the pulse finished callback
+        // Begin pulse. Note that it's very important that the pulse finished callback
         // be invoked when we're done so that the caller can drop the pulse wakelock.
         mPulseCallback = callback;
         mPulseReason = reason;
-        setDozeInFrontAlpha(1f);
-        mHandler.post(mPulseIn);
-    }
 
-    /**
-     * Aborts pulsing immediately.
-     */
-    public void abortPulsing() {
-        cancelPulsing();
-        if (mDozing && !mWakeAndUnlocking) {
-            mScrimController.setDozeBehindAlpha(1f);
-            setDozeInFrontAlpha(mDozeParameters.getAlwaysOn() && !mDozingAborted
-                    ? mAodFrontScrimOpacity : 1f);
-        }
-    }
-
-    /**
-     * Aborts dozing immediately.
-     */
-    public void abortDoze() {
-        mDozingAborted = true;
-        abortPulsing();
+        mScrimController.transitionTo(ScrimState.PULSING, mScrimCallback);
     }
 
     public void pulseOutNow() {
@@ -157,17 +121,6 @@
         }
     }
 
-    public void onScreenTurnedOn() {
-        if (isPulsing()) {
-            final boolean pickupOrDoubleTap = mPulseReason == DozeLog.PULSE_REASON_SENSOR_PICKUP
-                    || mPulseReason == DozeLog.PULSE_REASON_SENSOR_DOUBLE_TAP;
-            startScrimAnimation(true /* inFront */, 0f,
-                    mDozeParameters.getPulseInDuration(pickupOrDoubleTap),
-                    pickupOrDoubleTap ? Interpolators.LINEAR_OUT_SLOW_IN : Interpolators.ALPHA_OUT,
-                    mPulseInFinished);
-        }
-    }
-
     public boolean isPulsing() {
         return mPulseCallback != null;
     }
@@ -181,11 +134,9 @@
     }
 
     private void cancelPulsing() {
-        if (DEBUG) Log.d(TAG, "Cancel pulsing");
-
         if (mPulseCallback != null) {
+            if (DEBUG) Log.d(TAG, "Cancel pulsing");
             mFullyPulsing = false;
-            mHandler.removeCallbacks(mPulseIn);
             mHandler.removeCallbacks(mPulseOut);
             mHandler.removeCallbacks(mPulseOutExtended);
             pulseFinished();
@@ -193,151 +144,20 @@
     }
 
     private void pulseStarted() {
+        DozeLog.tracePulseStart(mPulseReason);
         if (mPulseCallback != null) {
             mPulseCallback.onPulseStarted();
         }
     }
 
     private void pulseFinished() {
+        DozeLog.tracePulseFinish();
         if (mPulseCallback != null) {
             mPulseCallback.onPulseFinished();
             mPulseCallback = null;
         }
     }
 
-    private void abortAnimations() {
-        if (mInFrontAnimator != null) {
-            mInFrontAnimator.cancel();
-        }
-        if (mBehindAnimator != null) {
-            mBehindAnimator.cancel();
-        }
-    }
-
-    private void startScrimAnimation(final boolean inFront, float target, long duration,
-            Interpolator interpolator) {
-        startScrimAnimation(inFront, target, duration, interpolator, null /* endRunnable */);
-    }
-
-    private void startScrimAnimation(final boolean inFront, float target, long duration,
-            Interpolator interpolator, final Runnable endRunnable) {
-        Animator current = getCurrentAnimator(inFront);
-        if (current != null) {
-            float currentTarget = getCurrentTarget(inFront);
-            if (currentTarget == target) {
-                return;
-            }
-            current.cancel();
-        }
-        ValueAnimator anim = ValueAnimator.ofFloat(getDozeAlpha(inFront), target);
-        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
-            @Override
-            public void onAnimationUpdate(ValueAnimator animation) {
-                float value = (float) animation.getAnimatedValue();
-                setDozeAlpha(inFront, value);
-            }
-        });
-        anim.setInterpolator(interpolator);
-        anim.setDuration(duration);
-        anim.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                setCurrentAnimator(inFront, null);
-                if (endRunnable != null) {
-                    endRunnable.run();
-                }
-            }
-        });
-        anim.start();
-        setCurrentAnimator(inFront, anim);
-        setCurrentTarget(inFront, target);
-    }
-
-    private float getCurrentTarget(boolean inFront) {
-        return inFront ? mInFrontTarget : mBehindTarget;
-    }
-
-    private void setCurrentTarget(boolean inFront, float target) {
-        if (inFront) {
-            mInFrontTarget = target;
-        } else {
-            mBehindTarget = target;
-        }
-    }
-
-    private Animator getCurrentAnimator(boolean inFront) {
-        return inFront ? mInFrontAnimator : mBehindAnimator;
-    }
-
-    private void setCurrentAnimator(boolean inFront, Animator animator) {
-        if (inFront) {
-            mInFrontAnimator = animator;
-        } else {
-            mBehindAnimator = animator;
-        }
-    }
-
-    private void setDozeAlpha(boolean inFront, float alpha) {
-        if (mWakeAndUnlocking) {
-            return;
-        }
-        if (inFront) {
-            mScrimController.setDozeInFrontAlpha(alpha);
-        } else {
-            mScrimController.setDozeBehindAlpha(alpha);
-        }
-    }
-
-    private float getDozeAlpha(boolean inFront) {
-        return inFront
-                ? mScrimController.getDozeInFrontAlpha()
-                : mScrimController.getDozeBehindAlpha();
-    }
-
-    private void setDozeInFrontAlpha(float opacity) {
-        setDozeInFrontAlphaDelayed(opacity, 0 /* delay */);
-
-    }
-
-    private void setDozeInFrontAlphaDelayed(float opacity, long delayMs) {
-        if (mSetDozeInFrontAlphaDelayed != null) {
-            mHandler.removeCallbacks(mSetDozeInFrontAlphaDelayed);
-            mSetDozeInFrontAlphaDelayed = null;
-        }
-        if (delayMs <= 0) {
-            mScrimController.setDozeInFrontAlpha(opacity);
-        } else {
-            mHandler.postDelayed(mSetDozeInFrontAlphaDelayed = () -> {
-                setDozeInFrontAlpha(opacity);
-            }, delayMs);
-        }
-    }
-
-    private final Runnable mPulseIn = new Runnable() {
-        @Override
-        public void run() {
-            if (DEBUG) Log.d(TAG, "Pulse in, mDozing=" + mDozing + " mPulseReason="
-                    + DozeLog.pulseReasonToString(mPulseReason));
-            if (!mDozing) return;
-            DozeLog.tracePulseStart(mPulseReason);
-
-            // Signal that the pulse is ready to turn the screen on and draw.
-            pulseStarted();
-        }
-    };
-
-    private final Runnable mPulseInFinished = new Runnable() {
-        @Override
-        public void run() {
-            if (DEBUG) Log.d(TAG, "Pulse in finished, mDozing=" + mDozing);
-            if (!mDozing) return;
-            mHandler.postDelayed(mPulseOut, mDozeParameters.getPulseVisibleDuration());
-            mHandler.postDelayed(mPulseOutExtended,
-                    mDozeParameters.getPulseVisibleDurationExtended());
-            mFullyPulsing = true;
-        }
-    };
-
     private final Runnable mPulseOutExtended = new Runnable() {
         @Override
         public void run() {
@@ -354,38 +174,13 @@
             mHandler.removeCallbacks(mPulseOutExtended);
             if (DEBUG) Log.d(TAG, "Pulse out, mDozing=" + mDozing);
             if (!mDozing) return;
-            startScrimAnimation(true /* inFront */, 1,
-                    mDozeParameters.getPulseOutDuration(),
-                    Interpolators.ALPHA_IN, mPulseOutFinishing);
+            mScrimController.transitionTo(ScrimState.AOD,
+                    new ScrimController.Callback() {
+                        @Override
+                        public void onDisplayBlanked() {
+                            pulseFinished();
+                        }
+                    });
         }
     };
-
-    private final Runnable mPulseOutFinishing = new Runnable() {
-        @Override
-        public void run() {
-            if (DEBUG) Log.d(TAG, "Pulse out finished");
-            DozeLog.tracePulseFinish();
-            if (mDozeParameters.getAlwaysOn() && mDozing) {
-                // Setting power states can block rendering. For AOD, delay finishing the pulse and
-                // setting the power state until the fully black scrim had time to hit the
-                // framebuffer.
-                mHandler.postDelayed(mPulseOutFinished, 30);
-            } else {
-                mPulseOutFinished.run();
-            }
-        }
-    };
-
-    private final Runnable mPulseOutFinished = new Runnable() {
-        @Override
-        public void run() {
-            // Signal that the pulse is all finished so we can turn the screen off now.
-            DozeScrimController.this.pulseFinished();
-            if (mDozeParameters.getAlwaysOn()) {
-                // Setting power states can happen after we push out the frame. Make sure we
-                // stay fully opaque until the power state request reaches the lower levels.
-                setDozeInFrontAlphaDelayed(mAodFrontScrimOpacity, 100);
-            }
-        }
-    };
-}
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/FingerprintUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/FingerprintUnlockController.java
index 91369db..80d4061 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/FingerprintUnlockController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/FingerprintUnlockController.java
@@ -181,9 +181,9 @@
     }
 
     private boolean pulsingOrAod() {
-        boolean pulsing = mDozeScrimController.isPulsing();
-        boolean dozingWithScreenOn = mStatusBar.isDozing() && !mStatusBar.isScreenFullyOff();
-        return pulsing || dozingWithScreenOn;
+        final ScrimState scrimState = mScrimController.getState();
+        return scrimState == ScrimState.AOD
+                || scrimState == ScrimState.PULSING;
     }
 
     @Override
@@ -246,15 +246,12 @@
                             true /* allowEnterAnimation */);
                 } else if (mMode == MODE_WAKE_AND_UNLOCK){
                     Trace.beginSection("MODE_WAKE_AND_UNLOCK");
-                    mDozeScrimController.abortDoze();
                 } else {
                     Trace.beginSection("MODE_WAKE_AND_UNLOCK_FROM_DREAM");
                     mUpdateMonitor.awakenFromDream();
                 }
                 mStatusBarWindowManager.setStatusBarFocusable(false);
                 mKeyguardViewMediator.onWakeAndUnlocking();
-                mScrimController.setWakeAndUnlocking();
-                mDozeScrimController.setWakeAndUnlocking();
                 if (mStatusBar.getNavigationBarView() != null) {
                     mStatusBar.getNavigationBarView().setWakeAndUnlocking(true);
                 }
@@ -269,6 +266,7 @@
     }
 
     private void showBouncer() {
+        mScrimController.transitionTo(ScrimState.BOUNCER);
         mStatusBarKeyguardViewManager.animateCollapsePanels(
                 FINGERPRINT_COLLAPSE_SPEEDUP_FACTOR);
         mPendingShowBouncer = false;
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 702afa3..dfd4c17 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -25,7 +25,9 @@
 import android.graphics.Color;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
+import android.os.Handler;
 import android.os.Trace;
+import android.util.Log;
 import android.util.MathUtils;
 import android.view.View;
 import android.view.ViewGroup;
@@ -34,12 +36,14 @@
 import android.view.animation.Interpolator;
 import android.view.animation.PathInterpolator;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.colorextraction.ColorExtractor;
 import com.android.internal.colorextraction.ColorExtractor.GradientColors;
 import com.android.internal.colorextraction.ColorExtractor.OnColorsChangedListener;
 import com.android.internal.graphics.ColorUtils;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.Dependency;
+import com.android.systemui.Dumpable;
 import com.android.systemui.R;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.statusbar.ExpandableNotificationRow;
@@ -47,7 +51,10 @@
 import com.android.systemui.statusbar.ScrimView;
 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
 import com.android.systemui.statusbar.stack.ViewState;
+import com.android.systemui.util.wakelock.DelayedWakeLock;
+import com.android.systemui.util.wakelock.WakeLock;
 
+import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.function.Consumer;
 
@@ -56,33 +63,54 @@
  * security method gets shown).
  */
 public class ScrimController implements ViewTreeObserver.OnPreDrawListener,
-        OnHeadsUpChangedListener, OnColorsChangedListener {
+        OnHeadsUpChangedListener, OnColorsChangedListener, Dumpable {
+
+    private static final String TAG = "ScrimController";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
     public static final long ANIMATION_DURATION = 220;
     public static final Interpolator KEYGUARD_FADE_OUT_INTERPOLATOR
             = new PathInterpolator(0f, 0, 0.7f, 1f);
     public static final Interpolator KEYGUARD_FADE_OUT_INTERPOLATOR_LOCKED
             = new PathInterpolator(0.3f, 0f, 0.8f, 1f);
-    // Default alpha value for most scrims, if unsure use this constant
+    /**
+     * Default alpha value for most scrims.
+     */
     public static final float GRADIENT_SCRIM_ALPHA = 0.45f;
-    // A scrim varies its opacity based on a busyness factor, for example
-    // how many notifications are currently visible.
+    /**
+     * A scrim varies its opacity based on a busyness factor, for example
+     * how many notifications are currently visible.
+     */
     public static final float GRADIENT_SCRIM_ALPHA_BUSY = 0.70f;
+    /**
+     * The most common scrim, the one under the keyguard.
+     */
     protected static final float SCRIM_BEHIND_ALPHA_KEYGUARD = GRADIENT_SCRIM_ALPHA;
+    /**
+     * We fade out the bottom scrim when the bouncer is visible.
+     */
     protected static final float SCRIM_BEHIND_ALPHA_UNLOCKING = 0.2f;
-    private static final float SCRIM_IN_FRONT_ALPHA = GRADIENT_SCRIM_ALPHA_BUSY;
-    private static final float SCRIM_IN_FRONT_ALPHA_LOCKED = GRADIENT_SCRIM_ALPHA_BUSY;
-    private static final int TAG_KEY_ANIM = R.id.scrim;
+    /**
+     * Opacity of the scrim behind the bouncer (the one doing actual background protection.)
+     */
+    protected static final float SCRIM_IN_FRONT_ALPHA_LOCKED = GRADIENT_SCRIM_ALPHA_BUSY;
+
+    static final int TAG_KEY_ANIM = R.id.scrim;
+    static final int TAG_KEY_ANIM_BLANK = R.id.scrim_blanking;
     private static final int TAG_KEY_ANIM_TARGET = R.id.scrim_target;
     private static final int TAG_START_ALPHA = R.id.scrim_alpha_start;
     private static final int TAG_END_ALPHA = R.id.scrim_alpha_end;
     private static final float NOT_INITIALIZED = -1;
 
-    private final LightBarController mLightBarController;
+    private ScrimState mState = ScrimState.UNINITIALIZED;
+    private final Context mContext;
     protected final ScrimView mScrimBehind;
     protected final ScrimView mScrimInFront;
-    private final UnlockMethodCache mUnlockMethodCache;
     private final View mHeadsUpScrim;
+    private final LightBarController mLightBarController;
+    private final UnlockMethodCache mUnlockMethodCache;
     private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+    private final DozeParameters mDozeParameters;
 
     private final SysuiColorExtractor mColorExtractor;
     private GradientColors mLockColors;
@@ -94,61 +122,53 @@
     protected float mScrimBehindAlphaKeyguard = SCRIM_BEHIND_ALPHA_KEYGUARD;
     protected float mScrimBehindAlphaUnlocking = SCRIM_BEHIND_ALPHA_UNLOCKING;
 
-    protected boolean mKeyguardShowing;
     private float mFraction;
 
     private boolean mDarkenWhileDragging;
-    protected boolean mBouncerShowing;
-    protected boolean mBouncerIsKeyguard = false;
-    private boolean mWakeAndUnlocking;
     protected boolean mAnimateChange;
     private boolean mUpdatePending;
     private boolean mTracking;
     private boolean mAnimateKeyguardFadingOut;
-    protected long mDurationOverride = -1;
+    protected long mAnimationDuration = -1;
     private long mAnimationDelay;
     private Runnable mOnAnimationFinished;
     private boolean mDeferFinishedListener;
     private final Interpolator mInterpolator = new DecelerateInterpolator();
-    private boolean mDozing;
-    private float mDozeInFrontAlpha;
-    private float mDozeBehindAlpha;
     private float mCurrentInFrontAlpha  = NOT_INITIALIZED;
     private float mCurrentBehindAlpha = NOT_INITIALIZED;
-    private float mCurrentHeadsUpAlpha = NOT_INITIALIZED;
+    private int mCurrentInFrontTint;
+    private int mCurrentBehindTint;
     private int mPinnedHeadsUpCount;
     private float mTopHeadsUpDragAmount;
     private View mDraggedHeadsUpView;
-    private boolean mForceHideScrims;
-    private boolean mSkipFirstFrame;
-    private boolean mDontAnimateBouncerChanges;
     private boolean mKeyguardFadingOutInProgress;
-    private boolean mAnimatingDozeUnlock;
     private ValueAnimator mKeyguardFadeoutAnimation;
-    /** Wake up from AOD transition is starting; need fully opaque front scrim */
-    private boolean mWakingUpFromAodStarting;
-    /** Wake up from AOD transition is in progress; need black tint */
-    private boolean mWakingUpFromAodInProgress;
-    /** Wake up from AOD transition is animating; need to reset when animation finishes */
-    private boolean mWakingUpFromAodAnimationRunning;
-    private boolean mScrimsVisble;
+    private boolean mScrimsVisible;
     private final Consumer<Boolean> mScrimVisibleListener;
+    private boolean mBlankScreen;
+    private boolean mScreenBlankingCallbackCalled;
+    private Callback mCallback;
+
+    private final WakeLock mWakeLock;
+    private boolean mWakeLockHeld;
 
     public ScrimController(LightBarController lightBarController, ScrimView scrimBehind,
-            ScrimView scrimInFront, View headsUpScrim,
-            Consumer<Boolean> scrimVisibleListener) {
+            ScrimView scrimInFront, View headsUpScrim, Consumer<Boolean> scrimVisibleListener,
+            DozeParameters dozeParameters) {
         mScrimBehind = scrimBehind;
         mScrimInFront = scrimInFront;
         mHeadsUpScrim = headsUpScrim;
         mScrimVisibleListener = scrimVisibleListener;
-        final Context context = scrimBehind.getContext();
-        mUnlockMethodCache = UnlockMethodCache.getInstance(context);
-        mKeyguardUpdateMonitor = KeyguardUpdateMonitor.getInstance(context);
+        mContext = scrimBehind.getContext();
+        mUnlockMethodCache = UnlockMethodCache.getInstance(mContext);
+        mKeyguardUpdateMonitor = KeyguardUpdateMonitor.getInstance(mContext);
         mLightBarController = lightBarController;
-        mScrimBehindAlphaResValue = context.getResources().getFloat(R.dimen.scrim_behind_alpha);
+        mScrimBehindAlphaResValue = mContext.getResources().getFloat(R.dimen.scrim_behind_alpha);
+        mWakeLock = createWakeLock();
         // Scrim alpha is initially set to the value on the resource but might be changed
         // to make sure that text on top of it is legible.
         mScrimBehindAlpha = mScrimBehindAlphaResValue;
+        mDozeParameters = dozeParameters;
 
         mColorExtractor = Dependency.get(SysuiColorExtractor.class);
         mColorExtractor.addOnColorsChangedListener(this);
@@ -158,22 +178,90 @@
                 ColorExtractor.TYPE_DARK, true /* ignoreVisibility */);
         mNeedsDrawableColorUpdate = true;
 
+        final ScrimState[] states = ScrimState.values();
+        for (int i = 0; i < states.length; i++) {
+            states[i].init(mScrimInFront, mScrimBehind, mDozeParameters);
+            states[i].setScrimBehindAlphaKeyguard(mScrimBehindAlphaKeyguard);
+        }
+        mState = ScrimState.UNINITIALIZED;
+
         updateHeadsUpScrim(false);
         updateScrims();
     }
 
-    public void setKeyguardShowing(boolean showing) {
-        mKeyguardShowing = showing;
+    public void transitionTo(ScrimState state) {
+        transitionTo(state, null);
+    }
 
-        // Showing/hiding the keyguard means that scrim colors have to be switched
-        mNeedsDrawableColorUpdate = true;
-        scheduleUpdate();
+    public void transitionTo(ScrimState state, Callback callback) {
+        if (state == mState) {
+            return;
+        } else if (DEBUG) {
+            Log.d(TAG, "State changed to: " + state);
+        }
+
+        if (state == ScrimState.UNINITIALIZED) {
+            throw new IllegalArgumentException("Cannot change to UNINITIALIZED.");
+        }
+
+        if (mCallback != null) {
+            mCallback.onCancelled();
+        }
+        mCallback = callback;
+
+        state.prepare(mState);
+        mScreenBlankingCallbackCalled = false;
+        mAnimationDelay = 0;
+        mBlankScreen = state.getBlanksScreen();
+        mAnimateChange = state.getAnimateChange();
+        mAnimationDuration = state.getAnimationDuration();
+        mCurrentInFrontTint = state.getFrontTint();
+        mCurrentBehindTint = state.getBehindTint();
+        mCurrentInFrontAlpha = state.getFrontAlpha();
+        mCurrentBehindAlpha = state.getBehindAlpha();
+
+        // Showing/hiding the keyguard means that scrim colors have to be switched, not necessary
+        // to do the same when you're just showing the brightness mirror.
+        mNeedsDrawableColorUpdate = state != ScrimState.BRIGHTNESS_MIRROR;
+
+        if (mKeyguardFadeoutAnimation != null) {
+            mKeyguardFadeoutAnimation.cancel();
+        }
+
+        mState = state;
+
+        // Do not let the device sleep until we're done with all animations
+        if (!mWakeLockHeld) {
+            if (mWakeLock != null) {
+                mWakeLockHeld = true;
+                mWakeLock.acquire();
+            } else {
+                Log.w(TAG, "Cannot hold wake lock, it has not been set yet");
+            }
+        }
+
+        if (!mKeyguardUpdateMonitor.needsSlowUnlockTransition()) {
+            scheduleUpdate();
+        } else {
+            // In case the user isn't unlocked, make sure to delay a bit because the system is hosed
+            // with too many things at this case, in order to not skip the initial frames.
+            mScrimInFront.postOnAnimationDelayed(this::scheduleUpdate, 16);
+            mAnimationDelay = StatusBar.FADE_KEYGUARD_START_DELAY;
+        }
+    }
+
+    public ScrimState getState() {
+        return mState;
     }
 
     protected void setScrimBehindValues(float scrimBehindAlphaKeyguard,
             float scrimBehindAlphaUnlocking) {
         mScrimBehindAlphaKeyguard = scrimBehindAlphaKeyguard;
         mScrimBehindAlphaUnlocking = scrimBehindAlphaUnlocking;
+        ScrimState[] states = ScrimState.values();
+        for (int i = 0; i < states.length; i++) {
+            states[i].setScrimBehindAlphaKeyguard(scrimBehindAlphaKeyguard);
+        }
         scheduleUpdate();
     }
 
@@ -186,131 +274,59 @@
         mTracking = false;
     }
 
+    /**
+     * Current state of the shade expansion when pulling it from the top.
+     * This value is 1 when on top of the keyguard and goes to 0 as the user drags up.
+     *
+     * The expansion fraction is tied to the scrim opacity.
+     *
+     * @param fraction From 0 to 1 where 0 means collapse and 1 expanded.
+     */
     public void setPanelExpansion(float fraction) {
         if (mFraction != fraction) {
             mFraction = fraction;
-            scheduleUpdate();
+
+            if (mState == ScrimState.UNLOCKED) {
+                // Darken scrim as you pull down the shade when unlocked
+                float behindFraction = getInterpolatedFraction();
+                behindFraction = (float) Math.pow(behindFraction, 0.8f);
+                mCurrentBehindAlpha = behindFraction * mScrimBehindAlphaKeyguard;
+                mCurrentInFrontAlpha = 0;
+            } else if (mState == ScrimState.KEYGUARD) {
+                if (mUpdatePending) {
+                    return;
+                }
+
+                // Either darken of make the scrim transparent when you
+                // pull down the shade
+                float interpolatedFract = getInterpolatedFraction();
+                if (mDarkenWhileDragging) {
+                    mCurrentBehindAlpha = MathUtils.lerp(mScrimBehindAlphaUnlocking,
+                            mScrimBehindAlphaKeyguard, interpolatedFract);
+                    mCurrentInFrontAlpha = (1f - interpolatedFract) * SCRIM_IN_FRONT_ALPHA_LOCKED;
+                } else {
+                    mCurrentBehindAlpha = MathUtils.lerp(0 /* start */, mScrimBehindAlphaKeyguard,
+                            interpolatedFract);
+                    mCurrentInFrontAlpha = 0;
+                }
+            } else {
+                Log.w(TAG, "Invalid state, cannot set panel expansion when: " + mState);
+                return;
+            }
+
             if (mPinnedHeadsUpCount != 0) {
                 updateHeadsUpScrim(false);
             }
-            if (mKeyguardFadeoutAnimation != null && mTracking) {
-                mKeyguardFadeoutAnimation.cancel();
-            }
+
+            updateScrim(false /* animate */, mScrimInFront, mCurrentInFrontAlpha);
+            updateScrim(false /* animate */, mScrimBehind, mCurrentBehindAlpha);
         }
     }
 
-    public void setBouncerShowing(boolean showing) {
-        mBouncerShowing = showing;
-        mAnimateChange = !mTracking && !mDontAnimateBouncerChanges && !mKeyguardFadingOutInProgress;
-        scheduleUpdate();
-    }
-
-    /** Prepares the wakeUpFromAod animation (while turning on screen); Forces black scrims. */
-    public void prepareWakeUpFromAod() {
-        if (mWakingUpFromAodInProgress) {
-            return;
-        }
-        mWakingUpFromAodInProgress = true;
-        mWakingUpFromAodStarting = true;
-        mAnimateChange = false;
-        scheduleUpdate();
-        onPreDraw();
-    }
-
-    /** Starts the wakeUpFromAod animation (once screen is on); animate to transparent scrims. */
-    public void wakeUpFromAod() {
-        if (mWakeAndUnlocking || mAnimateKeyguardFadingOut) {
-            // Wake and unlocking has a separate transition that must not be interfered with.
-            mWakingUpFromAodStarting = false;
-            mWakingUpFromAodInProgress = false;
-            return;
-        }
-        if (mWakingUpFromAodStarting) {
-            mWakingUpFromAodInProgress = true;
-            mWakingUpFromAodStarting = false;
-            mAnimateChange = true;
-            scheduleUpdate();
-        }
-    }
-
-    public void setWakeAndUnlocking() {
-        mWakeAndUnlocking = true;
-        mAnimatingDozeUnlock = true;
-        mWakingUpFromAodStarting = false;
-        mWakingUpFromAodInProgress = false;
-        scheduleUpdate();
-    }
-
-    public void animateKeyguardFadingOut(long delay, long duration, Runnable onAnimationFinished,
-            boolean skipFirstFrame) {
-        mWakeAndUnlocking = false;
-        mAnimateKeyguardFadingOut = true;
-        mDurationOverride = duration;
-        mAnimationDelay = delay;
-        mAnimateChange = true;
-        mSkipFirstFrame = skipFirstFrame;
-        mOnAnimationFinished = onAnimationFinished;
-
-        if (!mKeyguardUpdateMonitor.needsSlowUnlockTransition()) {
-            scheduleUpdate();
-
-            // No need to wait for the next frame to be drawn for this case - onPreDraw will execute
-            // the changes we just scheduled.
-            onPreDraw();
-        } else {
-
-            // In case the user isn't unlocked, make sure to delay a bit because the system is hosed
-            // with too many things in this case, in order to not skip the initial frames.
-            mScrimInFront.postOnAnimationDelayed(this::scheduleUpdate, 16);
-        }
-    }
-
-    public void abortKeyguardFadingOut() {
-        if (mAnimateKeyguardFadingOut) {
-            endAnimateKeyguardFadingOut(true /* force */);
-        }
-    }
-
-    public void animateKeyguardUnoccluding(long duration) {
-        mAnimateChange = false;
-        setScrimBehindAlpha(0f);
-        mAnimateChange = true;
-        scheduleUpdate();
-        mDurationOverride = duration;
-    }
-
-    public void animateGoingToFullShade(long delay, long duration) {
-        mDurationOverride = duration;
-        mAnimationDelay = delay;
-        mAnimateChange = true;
-        scheduleUpdate();
-    }
-
-    public void setDozing(boolean dozing) {
-        if (mDozing != dozing) {
-            mDozing = dozing;
-            scheduleUpdate();
-        }
-    }
-
-    public void setDozeInFrontAlpha(float alpha) {
-        mDozeInFrontAlpha = alpha;
-        updateScrimColor(mScrimInFront);
-    }
-
-    public void setDozeBehindAlpha(float alpha) {
-        mDozeBehindAlpha = alpha;
-        updateScrimColor(mScrimBehind);
-    }
-
-    public float getDozeBehindAlpha() {
-        return mDozeBehindAlpha;
-    }
-
-    public float getDozeInFrontAlpha() {
-        return mDozeInFrontAlpha;
-    }
-
+    /**
+     * Keyguard and shade scrim opacity varies according to how many notifications are visible.
+     * @param notificationCount Number of visible notifications.
+     */
     public void setNotificationCount(int notificationCount) {
         final float maxNotificationDensity = 3;
         float notificationDensity = Math.min(notificationCount / maxNotificationDensity, 1f);
@@ -319,15 +335,11 @@
                 notificationDensity);
         if (mScrimBehindAlphaKeyguard != newAlpha) {
             mScrimBehindAlphaKeyguard = newAlpha;
-            mAnimateChange = true;
-            scheduleUpdate();
-        }
-    }
 
-    private float getScrimInFrontAlpha() {
-        return mKeyguardUpdateMonitor.needsSlowUnlockTransition()
-                ? SCRIM_IN_FRONT_ALPHA_LOCKED
-                : SCRIM_IN_FRONT_ALPHA;
+            if (mState == ScrimState.KEYGUARD || mState == ScrimState.BOUNCER) {
+                scheduleUpdate();
+            }
+        }
     }
 
     /**
@@ -352,7 +364,7 @@
         if (mNeedsDrawableColorUpdate) {
             mNeedsDrawableColorUpdate = false;
             final GradientColors currentScrimColors;
-            if (mKeyguardShowing) {
+            if (mState == ScrimState.KEYGUARD || mState == ScrimState.BOUNCER) {
                 // Always animate color changes if we're seeing the keyguard
                 mScrimInFront.setColors(mLockColors, true /* animated */);
                 mScrimBehind.setColors(mLockColors, true /* animated */);
@@ -375,77 +387,31 @@
             mLightBarController.setScrimColor(mScrimInFront.getColors());
         }
 
-        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. This must never be animated.
-            mAnimateChange = false;
-            if (mDozing) {
-                setScrimInFrontAlpha(0f);
-                setScrimBehindAlpha(1f);
-            } else {
-                setScrimInFrontAlpha(1f);
-                setScrimBehindAlpha(0f);
-            }
-        } else if (!mKeyguardShowing && !mBouncerShowing && !mWakingUpFromAodStarting) {
-            updateScrimNormal();
-            setScrimInFrontAlpha(0);
-        } else {
-            updateScrimKeyguard();
-        }
-        mAnimateChange = false;
+        setScrimInFrontAlpha(mCurrentInFrontAlpha);
+        setScrimBehindAlpha(mCurrentBehindAlpha);
+
         dispatchScrimsVisible();
     }
 
     private void dispatchScrimsVisible() {
         boolean scrimsVisible = mScrimBehind.getViewAlpha() > 0 || mScrimInFront.getViewAlpha() > 0;
 
-        if (mScrimsVisble != scrimsVisible) {
-            mScrimsVisble = scrimsVisible;
+        if (mScrimsVisible != scrimsVisible) {
+            mScrimsVisible = scrimsVisible;
 
             mScrimVisibleListener.accept(scrimsVisible);
         }
     }
 
-    private void updateScrimKeyguard() {
-        if (mTracking && mDarkenWhileDragging) {
-            float behindFraction = Math.max(0, Math.min(mFraction, 1));
-            float fraction = 1 - behindFraction;
-            fraction = (float) Math.pow(fraction, 0.8f);
-            behindFraction = (float) Math.pow(behindFraction, 0.8f);
-            setScrimInFrontAlpha(fraction * getScrimInFrontAlpha());
-            setScrimBehindAlpha(behindFraction * mScrimBehindAlphaKeyguard);
-        } else if (mBouncerShowing && !mBouncerIsKeyguard) {
-            setScrimInFrontAlpha(getScrimInFrontAlpha());
-            updateScrimNormal();
-        } else if (mBouncerShowing) {
-            setScrimInFrontAlpha(0f);
-            setScrimBehindAlpha(mScrimBehindAlpha);
-        } else {
-            float fraction = Math.max(0, Math.min(mFraction, 1));
-            if (mWakingUpFromAodStarting) {
-                setScrimInFrontAlpha(1f);
-            } else {
-                setScrimInFrontAlpha(0f);
-            }
-            setScrimBehindAlpha(fraction
-                    * (mScrimBehindAlphaKeyguard - mScrimBehindAlphaUnlocking)
-                    + mScrimBehindAlphaUnlocking);
-        }
-    }
-
-    private void updateScrimNormal() {
+    private float getInterpolatedFraction() {
         float frac = mFraction;
         // let's start this 20% of the way down the screen
         frac = frac * 1.2f - 0.2f;
         if (frac <= 0) {
-            setScrimBehindAlpha(0);
+            return 0;
         } else {
             // woo, special effects
-            final float k = (float)(1f-0.5f*(1f-Math.cos(3.14159f * Math.pow(1f-frac, 2f))));
-            setScrimBehindAlpha(k * mScrimBehindAlpha);
+            return (float)(1f-0.5f*(1f-Math.cos(3.14159f * Math.pow(1f-frac, 2f))));
         }
     }
 
@@ -455,102 +421,76 @@
 
     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 setScrimAlpha(View scrim, float alpha) {
-        updateScrim(mAnimateChange, scrim, alpha, getCurrentScrimAlpha(scrim));
-    }
-
-    protected float getDozeAlpha(View scrim) {
-        return scrim == mScrimBehind ? mDozeBehindAlpha : mDozeInFrontAlpha;
-    }
-
-    protected float getCurrentScrimAlpha(View scrim) {
-        return scrim == mScrimBehind ? mCurrentBehindAlpha
-                : scrim == mScrimInFront ? mCurrentInFrontAlpha
-                : mCurrentHeadsUpAlpha;
-    }
-
-    private void setCurrentScrimAlpha(View scrim, float alpha) {
-        if (scrim == mScrimBehind) {
-            mCurrentBehindAlpha = alpha;
-            mLightBarController.setScrimAlpha(mCurrentBehindAlpha);
-        } else if (scrim == mScrimInFront) {
-            mCurrentInFrontAlpha = alpha;
+        if (alpha == 0f) {
+            scrim.setClickable(false);
         } else {
-            alpha = Math.max(0.0f, Math.min(1.0f, alpha));
-            mCurrentHeadsUpAlpha = alpha;
+            // Eat touch events (unless dozing).
+            scrim.setClickable(!(mState == ScrimState.AOD));
         }
+        updateScrim(mAnimateChange, scrim, alpha);
     }
 
-    private void updateScrimColor(View scrim) {
-        float alpha1 = getCurrentScrimAlpha(scrim);
+    private void updateScrimColor(View scrim, float alpha, int tint) {
+        alpha = Math.max(0, Math.min(1.0f, alpha));
         if (scrim instanceof ScrimView) {
             ScrimView scrimView = (ScrimView) scrim;
-            float dozeAlpha = getDozeAlpha(scrim);
-            float alpha = 1 - (1 - alpha1) * (1 - dozeAlpha);
-            alpha = Math.max(0, Math.min(1.0f, alpha));
-            scrimView.setViewAlpha(alpha);
 
             Trace.traceCounter(Trace.TRACE_TAG_APP,
                     scrim == mScrimInFront ? "front_scrim_alpha" : "back_scrim_alpha",
                     (int) (alpha * 255));
 
-            int dozeTint = Color.TRANSPARENT;
-
-            boolean dozing = mAnimatingDozeUnlock || mDozing;
-            boolean frontScrimDozing = mWakingUpFromAodInProgress;
-            if (dozing || frontScrimDozing && scrim == mScrimInFront) {
-                dozeTint = Color.BLACK;
-            }
             Trace.traceCounter(Trace.TRACE_TAG_APP,
                     scrim == mScrimInFront ? "front_scrim_tint" : "back_scrim_tint",
-                    dozeTint == Color.BLACK ? 1 : 0);
+                    Color.alpha(tint));
 
-            scrimView.setTint(dozeTint);
+            scrimView.setTint(tint);
+            scrimView.setViewAlpha(alpha);
         } else {
-            scrim.setAlpha(alpha1);
+            scrim.setAlpha(alpha);
         }
         dispatchScrimsVisible();
     }
 
-    private void startScrimAnimation(final View scrim, float target) {
-        float current = getCurrentScrimAlpha(scrim);
-        ValueAnimator anim = ValueAnimator.ofFloat(current, target);
+    private int getCurrentScrimTint(View scrim) {
+        return scrim == mScrimInFront ? mCurrentInFrontTint : mCurrentBehindTint;
+    }
+
+    private void startScrimAnimation(final View scrim, float current, float target) {
+        ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
+        final int initialScrimTint = scrim instanceof ScrimView ? ((ScrimView) scrim).getTint() :
+                Color.TRANSPARENT;
         anim.addUpdateListener(animation -> {
-            float alpha = (float) animation.getAnimatedValue();
-            setCurrentScrimAlpha(scrim, alpha);
-            updateScrimColor(scrim);
+            final float animAmount = (float) animation.getAnimatedValue();
+            final int finalScrimTint = scrim == mScrimInFront ?
+                    mCurrentInFrontTint : mCurrentBehindTint;
+            float alpha = MathUtils.lerp(current, target, animAmount);
+            int tint = ColorUtils.blendARGB(initialScrimTint, finalScrimTint, animAmount);
+            updateScrimColor(scrim, alpha, tint);
             dispatchScrimsVisible();
         });
         anim.setInterpolator(getInterpolator());
         anim.setStartDelay(mAnimationDelay);
-        anim.setDuration(mDurationOverride != -1 ? mDurationOverride : ANIMATION_DURATION);
+        anim.setDuration(mAnimationDuration);
         anim.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(Animator animation) {
+                if (mKeyguardFadingOutInProgress) {
+                    mKeyguardFadeoutAnimation = null;
+                    mKeyguardFadingOutInProgress = false;
+                }
+                onFinished();
+
+                scrim.setTag(TAG_KEY_ANIM, null);
+                scrim.setTag(TAG_KEY_ANIM_TARGET, null);
+                dispatchScrimsVisible();
+
                 if (!mDeferFinishedListener && mOnAnimationFinished != null) {
                     mOnAnimationFinished.run();
                     mOnAnimationFinished = null;
                 }
-                if (mKeyguardFadingOutInProgress) {
-                    mKeyguardFadeoutAnimation = null;
-                    mKeyguardFadingOutInProgress = false;
-                    mAnimatingDozeUnlock = false;
-                }
-                if (mWakingUpFromAodAnimationRunning && !mDeferFinishedListener) {
-                    mWakingUpFromAodAnimationRunning = false;
-                    mWakingUpFromAodInProgress = false;
-                }
-                scrim.setTag(TAG_KEY_ANIM, null);
-                scrim.setTag(TAG_KEY_ANIM_TARGET, null);
-                dispatchScrimsVisible();
             }
         });
         anim.start();
@@ -558,12 +498,6 @@
             mKeyguardFadingOutInProgress = true;
             mKeyguardFadeoutAnimation = anim;
         }
-        if (mWakingUpFromAodInProgress) {
-            mWakingUpFromAodAnimationRunning = true;
-        }
-        if (mSkipFirstFrame) {
-            anim.setCurrentPlayTime(16);
-        }
         scrim.setTag(TAG_KEY_ANIM, anim);
         scrim.setTag(TAG_KEY_ANIM_TARGET, target);
     }
@@ -582,19 +516,33 @@
     public boolean onPreDraw() {
         mScrimBehind.getViewTreeObserver().removeOnPreDrawListener(this);
         mUpdatePending = false;
-        if (mDontAnimateBouncerChanges) {
-            mDontAnimateBouncerChanges = false;
+        if (mCallback != null) {
+            mCallback.onStart();
         }
         updateScrims();
-        mDurationOverride = -1;
-        mAnimationDelay = 0;
-        mSkipFirstFrame = false;
 
         // Make sure that we always call the listener even if we didn't start an animation.
         endAnimateKeyguardFadingOut(false /* force */);
         return true;
     }
 
+    private void onFinished() {
+        if (mWakeLockHeld) {
+            mWakeLock.release();
+            mWakeLockHeld = false;
+        }
+        if (mCallback != null) {
+            mCallback.onFinished();
+            mCallback = null;
+        }
+        // When unlocking with fingerprint, we'll fade the scrims from black to transparent.
+        // At the end of the animation we need to remove the tint.
+        if (mState == ScrimState.UNLOCKED) {
+            mCurrentInFrontTint = Color.TRANSPARENT;
+            mCurrentBehindTint = Color.TRANSPARENT;
+        }
+    }
+
     private void endAnimateKeyguardFadingOut(boolean force) {
         mAnimateKeyguardFadingOut = false;
         if (force || (!isAnimating(mScrimInFront) && !isAnimating(mScrimBehind))) {
@@ -603,8 +551,6 @@
                 mOnAnimationFinished = null;
             }
             mKeyguardFadingOutInProgress = false;
-            if (!mWakeAndUnlocking || force)
-                mAnimatingDozeUnlock = false;
         }
     }
 
@@ -641,16 +587,19 @@
     }
 
     private void updateHeadsUpScrim(boolean animate) {
-        updateScrim(animate, mHeadsUpScrim, calculateHeadsUpAlpha(), mCurrentHeadsUpAlpha);
+        updateScrim(animate, mHeadsUpScrim, calculateHeadsUpAlpha());
     }
 
-    private void updateScrim(boolean animate, View scrim, float alpha, float currentAlpha) {
-        if (mKeyguardFadingOutInProgress && mKeyguardFadeoutAnimation.getCurrentPlayTime() != 0) {
-            return;
-        }
+    @VisibleForTesting
+    void setOnAnimationFinished(Runnable onAnimationFinished) {
+        mOnAnimationFinished = onAnimationFinished;
+    }
 
-        ValueAnimator previousAnimator = ViewState.getChildTag(scrim,
-                TAG_KEY_ANIM);
+    private void updateScrim(boolean animate, View scrim, float alpha) {
+        final float currentAlpha = scrim instanceof ScrimView ? ((ScrimView) scrim).getViewAlpha()
+            : scrim.getAlpha();
+
+        ValueAnimator previousAnimator = ViewState.getChildTag(scrim, TAG_KEY_ANIM);
         float animEndValue = -1;
         if (previousAnimator != null) {
             if (animate || alpha == currentAlpha) {
@@ -664,9 +613,32 @@
                 animEndValue = ViewState.getChildTag(scrim, TAG_END_ALPHA);
             }
         }
-        if (alpha != currentAlpha && alpha != animEndValue) {
+
+        final boolean blankingInProgress = mScrimInFront.getTag(TAG_KEY_ANIM_BLANK) != null;
+        if (mBlankScreen || blankingInProgress) {
+            if (!blankingInProgress) {
+                blankDisplay();
+            }
+            return;
+        } else if (!mScreenBlankingCallbackCalled) {
+            // Not blanking the screen. Letting the callback know that we're ready
+            // to replace what was on the screen before.
+            if (mCallback != null) {
+                mCallback.onDisplayBlanked();
+                mScreenBlankingCallbackCalled = true;
+            }
+        }
+
+        final ScrimView scrimView = scrim instanceof  ScrimView ? (ScrimView) scrim : null;
+        final boolean wantsAlphaUpdate = alpha != currentAlpha && alpha != animEndValue;
+        final boolean wantsTintUpdate = scrimView != null
+                && scrimView.getTint() != getCurrentScrimTint(scrimView);
+
+        if (wantsAlphaUpdate || wantsTintUpdate) {
             if (animate) {
-                startScrimAnimation(scrim, alpha);
+                final float fromAlpha = scrimView == null ? scrim.getAlpha()
+                        : scrimView.getViewAlpha();
+                startScrimAnimation(scrim, fromAlpha, alpha);
                 scrim.setTag(TAG_START_ALPHA, currentAlpha);
                 scrim.setTag(TAG_END_ALPHA, alpha);
             } else {
@@ -685,13 +657,62 @@
                     previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
                 } else {
                     // update the alpha directly
-                    setCurrentScrimAlpha(scrim, alpha);
-                    updateScrimColor(scrim);
+                    updateScrimColor(scrim, alpha, getCurrentScrimTint(scrim));
+                    onFinished();
                 }
             }
+        } else {
+            onFinished();
         }
     }
 
+    private void blankDisplay() {
+        final float initialAlpha = mScrimInFront.getViewAlpha();
+        final int initialTint = mScrimInFront.getTint();
+        ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
+        anim.addUpdateListener(animation -> {
+            final float amount = (float) animation.getAnimatedValue();
+            float animAlpha = MathUtils.lerp(initialAlpha, 1, amount);
+            int animTint = ColorUtils.blendARGB(initialTint, Color.BLACK, amount);
+            updateScrimColor(mScrimInFront, animAlpha, animTint);
+            dispatchScrimsVisible();
+        });
+        anim.setInterpolator(getInterpolator());
+        anim.setDuration(mDozeParameters.getPulseInDuration());
+        anim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                if (mCallback != null) {
+                    mCallback.onDisplayBlanked();
+                    mScreenBlankingCallbackCalled = true;
+                }
+                Runnable blankingCallback = () -> {
+                    mScrimInFront.setTag(TAG_KEY_ANIM_BLANK, null);
+                    mBlankScreen = false;
+                    // Try again.
+                    updateScrims();
+                };
+
+                // Setting power states can happen after we push out the frame. Make sure we
+                // stay fully opaque until the power state request reaches the lower levels.
+                getHandler().postDelayed(blankingCallback, 100);
+
+            }
+        });
+        anim.start();
+        mScrimInFront.setTag(TAG_KEY_ANIM_BLANK, anim);
+
+        // Finish animation if we're already at its final state
+        if (initialAlpha == 1 && mScrimInFront.getTint() == Color.BLACK) {
+            anim.end();
+        }
+    }
+
+    @VisibleForTesting
+    protected Handler getHandler() {
+        return Handler.getMain();
+    }
+
     /**
      * Set the amount the current top heads up view is dragged. The range is from 0 to 1 and 0 means
      * the heads up is in its resting space and 1 means it's fully dragged out.
@@ -719,23 +740,13 @@
         return alpha * expandFactor;
     }
 
-    public void forceHideScrims(boolean hide, boolean animated) {
-        mForceHideScrims = hide;
-        mAnimateChange = animated;
-        scheduleUpdate();
-    }
-
-    public void dontAnimateBouncerChangesUntilNextFrame() {
-        mDontAnimateBouncerChanges = true;
-    }
-
     public void setExcludedBackgroundArea(Rect area) {
         mScrimBehind.setExcludedArea(area);
     }
 
     public int getBackgroundColor() {
         int color = mLockColors.getMainColor();
-        return Color.argb((int) (mScrimBehind.getAlpha() * Color.alpha(color)),
+        return Color.argb((int) (mScrimBehind.getViewAlpha() * Color.alpha(color)),
                 Color.red(color), Color.green(color), Color.blue(color));
     }
 
@@ -764,27 +775,41 @@
         }
         if ((which & WallpaperManager.FLAG_SYSTEM) != 0) {
             mSystemColors = mColorExtractor.getColors(WallpaperManager.FLAG_SYSTEM,
-                    ColorExtractor.TYPE_DARK, mKeyguardShowing);
+                    ColorExtractor.TYPE_DARK, mState != ScrimState.UNLOCKED);
             mNeedsDrawableColorUpdate = true;
             scheduleUpdate();
         }
     }
 
-    public void dump(PrintWriter pw) {
-        pw.println(" ScrimController:");
+    @VisibleForTesting
+    protected WakeLock createWakeLock() {
+         return new DelayedWakeLock(getHandler(),
+                WakeLock.createPartial(mContext, "Doze"));
+    }
 
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println(" ScrimController:");
+        pw.print(" state:"); pw.println(mState);
         pw.print("   frontScrim:"); pw.print(" viewAlpha="); pw.print(mScrimInFront.getViewAlpha());
         pw.print(" alpha="); pw.print(mCurrentInFrontAlpha);
-        pw.print(" dozeAlpha="); pw.print(mDozeInFrontAlpha);
         pw.print(" tint=0x"); pw.println(Integer.toHexString(mScrimInFront.getTint()));
 
         pw.print("   backScrim:"); pw.print(" viewAlpha="); pw.print(mScrimBehind.getViewAlpha());
         pw.print(" alpha="); pw.print(mCurrentBehindAlpha);
-        pw.print(" dozeAlpha="); pw.print(mDozeBehindAlpha);
         pw.print(" tint=0x"); pw.println(Integer.toHexString(mScrimBehind.getTint()));
 
-        pw.print("   mBouncerShowing="); pw.println(mBouncerShowing);
         pw.print("   mTracking="); pw.println(mTracking);
-        pw.print("   mForceHideScrims="); pw.println(mForceHideScrims);
+    }
+
+    public interface Callback {
+        default void onStart() {
+        }
+        default void onDisplayBlanked() {
+        }
+        default void onFinished() {
+        }
+        default void onCancelled() {
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java
new file mode 100644
index 0000000..0db98f3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java
@@ -0,0 +1,208 @@
+/*
+ * 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.phone;
+
+import android.graphics.Color;
+import android.os.Trace;
+
+import com.android.systemui.statusbar.ScrimView;
+
+/**
+ * Possible states of the ScrimController state machine.
+ */
+public enum ScrimState {
+
+    /**
+     * Initial state.
+     */
+    UNINITIALIZED,
+
+    /**
+     * On the lock screen.
+     */
+    KEYGUARD {
+
+        @Override
+        public void prepare(ScrimState previousState) {
+            // DisplayPowerManager will blank the screen, we'll just
+            // set our scrim to black in this frame to avoid flickering and
+            // fade it out afterwards.
+            mBlankScreen = previousState == ScrimState.AOD;
+            if (previousState == ScrimState.AOD) {
+                updateScrimColor(mScrimInFront, 1, Color.BLACK);
+            }
+            mCurrentBehindAlpha = mScrimBehindAlphaKeyguard;
+            mCurrentInFrontAlpha = 0;
+        }
+    },
+
+    /**
+     * Showing password challenge.
+     */
+    BOUNCER {
+        @Override
+        public void prepare(ScrimState previousState) {
+            mCurrentBehindAlpha = ScrimController.SCRIM_BEHIND_ALPHA_UNLOCKING;
+            mCurrentInFrontAlpha = ScrimController.SCRIM_IN_FRONT_ALPHA_LOCKED;
+        }
+    },
+
+    /**
+     * Changing screen brightness from quick settings.
+     */
+    BRIGHTNESS_MIRROR {
+        @Override
+        public void prepare(ScrimState previousState) {
+            mCurrentBehindAlpha = 0;
+            mCurrentInFrontAlpha = 0;
+        }
+    },
+
+    /**
+     * Always on display or screen off.
+     */
+    AOD {
+        @Override
+        public void prepare(ScrimState previousState) {
+            if (previousState == ScrimState.PULSING) {
+                updateScrimColor(mScrimInFront, 1, Color.BLACK);
+            }
+            final boolean alwaysOnEnabled = mDozeParameters.getAlwaysOn();
+            mBlankScreen = previousState == ScrimState.PULSING;
+            mCurrentBehindAlpha = 1;
+            mCurrentInFrontAlpha = alwaysOnEnabled ? mAodFrontScrimAlpha : 1f;
+            mCurrentInFrontTint = Color.BLACK;
+            mCurrentBehindTint = Color.BLACK;
+            // DisplayPowerManager will blank the screen for us, we just need
+            // to set our state.
+            mAnimateChange = false;
+        }
+    },
+
+    /**
+     * When phone wakes up because you received a notification.
+     */
+    PULSING {
+        @Override
+        public void prepare(ScrimState previousState) {
+            mCurrentBehindAlpha = 1;
+            mCurrentInFrontAlpha = 0;
+            mCurrentInFrontTint = Color.BLACK;
+            mCurrentBehindTint = Color.BLACK;
+            mBlankScreen = true;
+            updateScrimColor(mScrimInFront, 1, Color.BLACK);
+        }
+    },
+
+    /**
+     * Unlocked on top of an app (launcher or any other activity.)
+     */
+    UNLOCKED {
+        @Override
+        public void prepare(ScrimState previousState) {
+            mCurrentBehindAlpha = 0;
+            mCurrentInFrontAlpha = 0;
+            mAnimationDuration = StatusBar.FADE_KEYGUARD_DURATION;
+
+            if (previousState == ScrimState.AOD) {
+                // Fade from black to transparent when coming directly from AOD
+                updateScrimColor(mScrimInFront, 1, Color.BLACK);
+                updateScrimColor(mScrimBehind, 1, Color.BLACK);
+                // Scrims should still be black at the end of the transition.
+                mCurrentInFrontTint = Color.BLACK;
+                mCurrentBehindTint = Color.BLACK;
+                mBlankScreen = true;
+            } else {
+                // Scrims should still be black at the end of the transition.
+                mCurrentInFrontTint = Color.TRANSPARENT;
+                mCurrentBehindTint = Color.TRANSPARENT;
+                mBlankScreen = false;
+            }
+        }
+    };
+
+    boolean mBlankScreen = false;
+    long mAnimationDuration = ScrimController.ANIMATION_DURATION;
+    int mCurrentInFrontTint = Color.TRANSPARENT;
+    int mCurrentBehindTint = Color.TRANSPARENT;
+    boolean mAnimateChange = true;
+    float mCurrentInFrontAlpha;
+    float mCurrentBehindAlpha;
+    float mAodFrontScrimAlpha;
+    float mScrimBehindAlphaKeyguard;
+    ScrimView mScrimInFront;
+    ScrimView mScrimBehind;
+    DozeParameters mDozeParameters;
+
+    public void init(ScrimView scrimInFront, ScrimView scrimBehind, DozeParameters dozeParameters) {
+        mScrimInFront = scrimInFront;
+        mScrimBehind = scrimBehind;
+        mDozeParameters = dozeParameters;
+    }
+
+    public void prepare(ScrimState previousState) {
+    }
+
+    public float getFrontAlpha() {
+        return mCurrentInFrontAlpha;
+    }
+
+    public float getBehindAlpha() {
+        return mCurrentBehindAlpha;
+    }
+
+    public int getFrontTint() {
+        return mCurrentInFrontTint;
+    }
+
+    public int getBehindTint() {
+        return mCurrentBehindTint;
+    }
+
+    public long getAnimationDuration() {
+        return mAnimationDuration;
+    }
+
+    public boolean getBlanksScreen() {
+        return mBlankScreen;
+    }
+
+    public void updateScrimColor(ScrimView scrim, float alpha, int tint) {
+        Trace.traceCounter(Trace.TRACE_TAG_APP,
+                scrim == mScrimInFront ? "front_scrim_alpha" : "back_scrim_alpha",
+                (int) (alpha * 255));
+
+        Trace.traceCounter(Trace.TRACE_TAG_APP,
+                scrim == mScrimInFront ? "front_scrim_tint" : "back_scrim_tint",
+                Color.alpha(tint));
+
+        scrim.setTint(tint);
+        scrim.setViewAlpha(alpha);
+    }
+
+    public boolean getAnimateChange() {
+        return mAnimateChange;
+    }
+
+    public void setAodFrontScrimAlpha(float aodFrontScrimAlpha) {
+        mAodFrontScrimAlpha = aodFrontScrimAlpha;
+    }
+
+    public void setScrimBehindAlphaKeyguard(float scrimBehindAlphaKeyguard) {
+        mScrimBehindAlphaKeyguard = scrimBehindAlphaKeyguard;
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
index 43cc0f7..a5609da 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -238,6 +238,7 @@
         .OnChildLocationsChangedListener;
 import com.android.systemui.util.NotificationChannels;
 import com.android.systemui.util.leak.LeakDetector;
+import com.android.systemui.util.wakelock.WakeLock;
 import com.android.systemui.volume.VolumeComponent;
 
 import java.io.FileDescriptor;
@@ -386,6 +387,7 @@
 
     private VolumeComponent mVolumeComponent;
     private BrightnessMirrorController mBrightnessMirrorController;
+    private boolean mBrightnessMirrorVisible;
     protected FingerprintUnlockController mFingerprintUnlockController;
     private LightBarController mLightBarController;
     protected LockscreenWallpaper mLockscreenWallpaper;
@@ -646,6 +648,31 @@
         }
     };
 
+    // Notifies StatusBarKeyguardViewManager every time the keyguard transition is over,
+    // this animation is tied to the scrim for historic reasons.
+    // TODO: notify when keyguard has faded away instead of the scrim.
+    private final ScrimController.Callback mUnlockScrimCallback = new ScrimController
+            .Callback() {
+        @Override
+        public void onFinished() {
+            notifyKeyguardState();
+        }
+
+        @Override
+        public void onCancelled() {
+            notifyKeyguardState();
+        }
+
+        private void notifyKeyguardState() {
+            if (mStatusBarKeyguardViewManager == null) {
+                Log.w(TAG, "Tried to notify keyguard visibility when "
+                        + "mStatusBarKeyguardViewManager was null");
+                return;
+            }
+            mStatusBarKeyguardViewManager.onKeyguardFadedAway();
+        }
+    };
+
     private NotificationMessagingUtil mMessagingUtil;
     private KeyguardUserSwitcher mKeyguardUserSwitcher;
     private UserSwitcherController mUserSwitcherController;
@@ -1045,7 +1072,7 @@
                     if (mStatusBarWindowManager != null) {
                         mStatusBarWindowManager.setScrimsVisible(scrimsVisible);
                     }
-                });
+                }, new DozeParameters(mContext));
         if (mScrimSrcModeEnabled) {
             Runnable runnable = () -> {
                 boolean asSrc = mBackdrop.getVisibility() != View.VISIBLE;
@@ -1081,7 +1108,10 @@
             final QSTileHost qsh = SystemUIFactory.getInstance().createQSTileHost(mContext, this,
                     mIconController);
             mBrightnessMirrorController = new BrightnessMirrorController(mStatusBarWindow,
-                    mScrimController);
+                    (visible) -> {
+                        mBrightnessMirrorVisible = visible;
+                        updateScrimController();
+                    });
             fragmentHostManager.addTagListener(QS.TAG, (tag, f) -> {
                 QS qs = (QS) f;
                 if (qs instanceof QSFragment) {
@@ -1457,8 +1487,7 @@
                 mDozeScrimController, keyguardViewMediator,
                 mScrimController, this, UnlockMethodCache.getInstance(mContext));
         mStatusBarKeyguardViewManager = keyguardViewMediator.registerStatusBar(this,
-                getBouncerContainer(), mScrimController,
-                mFingerprintUnlockController);
+                getBouncerContainer(), mFingerprintUnlockController);
         mKeyguardIndicationController
                 .setStatusBarKeyguardViewManager(mStatusBarKeyguardViewManager);
         mFingerprintUnlockController.setStatusBarKeyguardViewManager(mStatusBarKeyguardViewManager);
@@ -2640,7 +2669,7 @@
     }
 
     public boolean isPulsing() {
-        return mDozeScrimController.isPulsing();
+        return mDozeScrimController != null && mDozeScrimController.isPulsing();
     }
 
     @Override
@@ -3348,7 +3377,7 @@
         }
 
         if (mScrimController != null) {
-            mScrimController.dump(pw);
+            mScrimController.dump(fd, pw, args);
         }
 
         if (DUMPTRUCK) {
@@ -4081,7 +4110,6 @@
         releaseGestureWakeLock();
         runLaunchTransitionEndRunnable();
         mLaunchTransitionFadingAway = false;
-        mScrimController.forceHideScrims(false /* hide */, false /* animated */);
         updateMediaMetaData(true /* metaDataChanged */, true);
     }
 
@@ -4114,7 +4142,7 @@
             if (beforeFading != null) {
                 beforeFading.run();
             }
-            mScrimController.forceHideScrims(true /* hide */, false /* animated */);
+            updateScrimController();
             updateMediaMetaData(false, true);
             mNotificationPanel.setAlpha(1);
             mStackScroller.setParentNotFullyVisible(true);
@@ -4145,6 +4173,13 @@
                 .setStartDelay(0)
                 .setDuration(FADE_KEYGUARD_DURATION_PULSING)
                 .setInterpolator(ScrimController.KEYGUARD_FADE_OUT_INTERPOLATOR)
+                .setListener(new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        hideKeyguard();
+                        mStatusBarKeyguardViewManager.onKeyguardFadedAway();
+                    }
+                })
                 .start();
     }
 
@@ -4152,7 +4187,6 @@
      * Plays the animation when an activity that was occluding Keyguard goes away.
      */
     public void animateKeyguardUnoccluding() {
-        mScrimController.animateKeyguardUnoccluding(500);
         mNotificationPanel.setExpandedFraction(0f);
         animateExpandNotificationsPanel();
     }
@@ -4340,11 +4374,6 @@
                 mAmbientIndicationContainer.setVisibility(View.INVISIBLE);
             }
         }
-        if (mState == StatusBarState.KEYGUARD || mState == StatusBarState.SHADE_LOCKED) {
-            mScrimController.setKeyguardShowing(true);
-        } else {
-            mScrimController.setKeyguardShowing(false);
-        }
         mNotificationPanel.setBarState(mState, mKeyguardFadingAway, goingToFullShade);
         updateTheme();
         updateDozingState();
@@ -4352,6 +4381,7 @@
         updateStackScrollerState(goingToFullShade, fromShadeLocked);
         updateNotifications();
         checkBarModes();
+        updateScrimController();
         updateMediaMetaData(false, mState != StatusBarState.KEYGUARD);
         mKeyguardMonitor.notifyKeyguardState(mStatusBarKeyguardViewManager.isShowing(),
                 mUnlockMethodCache.isMethodSecure(),
@@ -4415,11 +4445,10 @@
         boolean animate = !mDozing && mDozeServiceHost.shouldAnimateWakeup();
         mNotificationPanel.setDozing(mDozing, animate);
         mStackScroller.setDark(mDozing, animate, mWakeUpTouchLocation);
-        mScrimController.setDozing(mDozing);
+        mDozeScrimController.setDozing(mDozing);
         mKeyguardIndicationController.setDozing(mDozing);
         mNotificationPanel.setDark(mDozing, animate);
         updateQsExpansionEnabled();
-        mDozeScrimController.setDozing(mDozing, animate);
         updateRowStates();
         Trace.endSection();
     }
@@ -4914,6 +4943,7 @@
         if (mStatusBarView != null) mStatusBarView.setBouncerShowing(bouncerShowing);
         updateHideIconsForBouncer(true /* animate */);
         recomputeDisableFlags(true /* animate */);
+        updateScrimController();
     }
 
     public void cancelCurrentTouch() {
@@ -4965,12 +4995,10 @@
             mStackScroller.setAnimationsEnabled(true);
             mVisualStabilityManager.setScreenOn(true);
             mNotificationPanel.setTouchDisabled(false);
-
-            maybePrepareWakeUpFromAod();
-
             mDozeServiceHost.stopDozing();
             updateVisibleToUser();
             updateIsKeyguard();
+            updateScrimController();
         }
     };
 
@@ -4980,18 +5008,16 @@
             mFalsingManager.onScreenTurningOn();
             mNotificationPanel.onScreenTurningOn();
 
-            maybePrepareWakeUpFromAod();
-
             if (mLaunchCameraOnScreenTurningOn) {
                 mNotificationPanel.launchCamera(false, mLastCameraLaunchSource);
                 mLaunchCameraOnScreenTurningOn = false;
             }
+
+            updateScrimController();
         }
 
         @Override
         public void onScreenTurnedOn() {
-            mScrimController.wakeUpFromAod();
-            mDozeScrimController.onScreenTurnedOn();
         }
 
         @Override
@@ -5009,13 +5035,6 @@
         return mWakefulnessLifecycle.getWakefulness();
     }
 
-    private void maybePrepareWakeUpFromAod() {
-        int wakefulness = mWakefulnessLifecycle.getWakefulness();
-        if (mDozing && wakefulness == WAKEFULNESS_WAKING && !isPulsing()) {
-            mScrimController.prepareWakeUpFromAod();
-        }
-    }
-
     private void vibrateForCameraGesture() {
         // Make sure to pass -1 for repeat so VibratorService doesn't stop us when going to sleep.
         mVibrator.vibrate(mCameraLaunchGestureVibePattern, -1 /* repeat */);
@@ -5098,12 +5117,12 @@
             if (!mDeviceInteractive) {
                 // Avoid flickering of the scrim when we instant launch the camera and the bouncer
                 // comes on.
-                mScrimController.dontAnimateBouncerChangesUntilNextFrame();
                 mGestureWakeLock.acquire(LAUNCH_TRANSITION_TIMEOUT_MS + 1000L);
             }
             if (isScreenTurningOnOrOn()) {
                 if (DEBUG_CAMERA_LIFT) Slog.d(TAG, "Launching camera");
                 mNotificationPanel.launchCamera(mDeviceInteractive /* animate */, source);
+                updateScrimController();
             } else {
                 // We need to defer the camera launch until the screen comes on, since otherwise
                 // we will dismiss us too early since we are waiting on an activity to be drawn and
@@ -5145,15 +5164,16 @@
     private void updateDozing() {
         Trace.beginSection("StatusBar#updateDozing");
         // When in wake-and-unlock while pulsing, keep dozing state until fully unlocked.
-        mDozing = mDozingRequested && mState == StatusBarState.KEYGUARD
+        boolean dozing = mDozingRequested && mState == StatusBarState.KEYGUARD
                 || mFingerprintUnlockController.getMode()
                         == FingerprintUnlockController.MODE_WAKE_AND_UNLOCK_PULSING;
         // When in wake-and-unlock we may not have received a change to mState
         // but we still should not be dozing, manually set to false.
         if (mFingerprintUnlockController.getMode() ==
                 FingerprintUnlockController.MODE_WAKE_AND_UNLOCK) {
-            mDozing = false;
+            dozing = false;
         }
+        mDozing = dozing;
         mStatusBarWindowManager.setDozing(mDozing);
         mStatusBarKeyguardViewManager.setDozing(mDozing);
         if (mAmbientIndicationContainer instanceof DozeReceiver) {
@@ -5163,6 +5183,24 @@
         Trace.endSection();
     }
 
+    public void updateScrimController() {
+        if (mBouncerShowing) {
+            mScrimController.transitionTo(ScrimState.BOUNCER);
+        } else if (mLaunchCameraOnScreenTurningOn || isInLaunchTransition()) {
+            mScrimController.transitionTo(ScrimState.UNLOCKED, mUnlockScrimCallback);
+        } else if (mBrightnessMirrorVisible) {
+            mScrimController.transitionTo(ScrimState.BRIGHTNESS_MIRROR);
+        } else if (isPulsing()) {
+            // Handled in DozeScrimController#setPulsing
+        } else if (mDozing) {
+            mScrimController.transitionTo(ScrimState.AOD);
+        } else if (mIsKeyguard) {
+            mScrimController.transitionTo(ScrimState.KEYGUARD);
+        } else {
+            mScrimController.transitionTo(ScrimState.UNLOCKED, mUnlockScrimCallback);
+        }
+    }
+
     public boolean isKeyguardShowing() {
         if (mStatusBarKeyguardViewManager == null) {
             Slog.i(TAG, "isKeyguardShowing() called before startKeyguard(), returning true");
@@ -5222,7 +5260,6 @@
             }
 
             mDozeScrimController.pulse(new PulseCallback() {
-
                 @Override
                 public void onPulseStarted() {
                     callback.onPulseStarted();
@@ -5307,11 +5344,6 @@
         }
 
         @Override
-        public void abortPulsing() {
-            mDozeScrimController.abortPulsing();
-        }
-
-        @Override
         public void extendPulse() {
             mDozeScrimController.extendPulse();
         }
@@ -5347,7 +5379,7 @@
 
         @Override
         public void setAodDimmingScrim(float scrimOpacity) {
-            mDozeScrimController.setAodDimmingScrim(scrimOpacity);
+            ScrimState.AOD.setAodFrontScrimAlpha(scrimOpacity);
         }
 
         public void dispatchDoubleTap(float viewX, float viewY) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index bcde556..ef05bbb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -24,7 +24,6 @@
 import android.content.Context;
 import android.os.Bundle;
 import android.os.SystemClock;
-import android.os.Trace;
 import android.view.KeyEvent;
 import android.view.View;
 import android.view.ViewGroup;
@@ -71,17 +70,14 @@
 
     protected final Context mContext;
     private final StatusBarWindowManager mStatusBarWindowManager;
-    private final boolean mDisplayBlanksAfterDoze;
 
     protected LockPatternUtils mLockPatternUtils;
     protected ViewMediatorCallback mViewMediatorCallback;
     protected StatusBar mStatusBar;
-    private ScrimController mScrimController;
     private FingerprintUnlockController mFingerprintUnlockController;
 
     private ViewGroup mContainer;
 
-    private boolean mScreenTurnedOn;
     protected KeyguardBouncer mBouncer;
     protected boolean mShowing;
     protected boolean mOccluded;
@@ -95,12 +91,10 @@
     private boolean mLastBouncerDismissible;
     protected boolean mLastRemoteInputActive;
     private boolean mLastDozing;
-    private boolean mLastDeferScrimFadeOut;
     private int mLastFpMode;
 
     private OnDismissAction mAfterKeyguardGoneAction;
     private final ArrayList<Runnable> mAfterKeyguardGoneRunnables = new ArrayList<>();
-    private boolean mDeferScrimFadeOut;
 
     // Dismiss action to be launched when we stop dozing or the keyguard is gone.
     private DismissWithActionRequest mPendingWakeupAction;
@@ -125,18 +119,14 @@
         mLockPatternUtils = lockPatternUtils;
         mStatusBarWindowManager = Dependency.get(StatusBarWindowManager.class);
         KeyguardUpdateMonitor.getInstance(context).registerCallback(mUpdateMonitorCallback);
-        mDisplayBlanksAfterDoze = context.getResources().getBoolean(
-                com.android.internal.R.bool.config_displayBlanksAfterDoze);
     }
 
     public void registerStatusBar(StatusBar statusBar,
             ViewGroup container,
-            ScrimController scrimController,
             FingerprintUnlockController fingerprintUnlockController,
             DismissCallbackRegistry dismissCallbackRegistry) {
         mStatusBar = statusBar;
         mContainer = container;
-        mScrimController = scrimController;
         mFingerprintUnlockController = fingerprintUnlockController;
         mBouncer = SystemUIFactory.getInstance().createKeyguardBouncer(mContext,
                 mViewMediatorCallback, mLockPatternUtils, container, dismissCallbackRegistry);
@@ -149,7 +139,6 @@
     public void show(Bundle options) {
         mShowing = true;
         mStatusBarWindowManager.setKeyguardShowing(true);
-        mScrimController.abortKeyguardFadingOut();
         reset(true /* hideBouncerWhenShowing */);
     }
 
@@ -253,15 +242,7 @@
     }
 
     public void onScreenTurnedOn() {
-        Trace.beginSection("StatusBarKeyguardViewManager#onScreenTurnedOn");
-        mScreenTurnedOn = true;
-        if (mDeferScrimFadeOut) {
-            mDeferScrimFadeOut = false;
-            animateScrimControllerKeyguardFadingOut(0, WAKE_AND_UNLOCK_SCRIM_FADEOUT_DURATION_MS,
-                    true /* skipFirstFrame */);
-            updateStates();
-        }
-        Trace.endSection();
+        // TODO: remove
     }
 
     @Override
@@ -285,7 +266,7 @@
     }
 
     public void onScreenTurnedOff() {
-        mScreenTurnedOn = false;
+        // TODO: remove
     }
 
     public void notifyDeviceWakeUpRequested() {
@@ -374,10 +355,6 @@
                     mStatusBarWindowManager.setKeyguardFadingAway(true);
                     hideBouncer(true /* destroyView */);
                     updateStates();
-                    mScrimController.animateKeyguardFadingOut(
-                            StatusBar.FADE_KEYGUARD_START_DELAY,
-                            StatusBar.FADE_KEYGUARD_DURATION, null,
-                            false /* skipFirstFrame */);
                 }
             }, new Runnable() {
                 @Override
@@ -400,36 +377,16 @@
             mFingerprintUnlockController.startKeyguardFadingAway();
             hideBouncer(true /* destroyView */);
             if (wakeUnlockPulsing) {
-                mStatusBarWindowManager.setKeyguardFadingAway(true);
                 mStatusBar.fadeKeyguardWhilePulsing();
-                animateScrimControllerKeyguardFadingOut(delay, fadeoutDuration,
-                        mStatusBar::hideKeyguard, false /* skipFirstFrame */);
+                wakeAndUnlockDejank();
             } else {
                 mFingerprintUnlockController.startKeyguardFadingAway();
                 mStatusBar.setKeyguardFadingAway(startTime, delay, fadeoutDuration);
                 boolean staying = mStatusBar.hideKeyguard();
                 if (!staying) {
                     mStatusBarWindowManager.setKeyguardFadingAway(true);
-                    if (mFingerprintUnlockController.getMode() == MODE_WAKE_AND_UNLOCK) {
-                        boolean turnedOnSinceAuth =
-                                mFingerprintUnlockController.hasScreenTurnedOnSinceAuthenticating();
-                        if (!mScreenTurnedOn || mDisplayBlanksAfterDoze && !turnedOnSinceAuth) {
-                            // Not ready to animate yet; either because the screen is not on yet,
-                            // or it is on but will turn off before waking out of doze.
-                            mDeferScrimFadeOut = true;
-                        } else {
-
-                            // Screen is already on, don't defer with fading out.
-                            animateScrimControllerKeyguardFadingOut(0,
-                                    WAKE_AND_UNLOCK_SCRIM_FADEOUT_DURATION_MS,
-                                    true /* skipFirstFrame */);
-                        }
-                    } else {
-                        animateScrimControllerKeyguardFadingOut(delay, fadeoutDuration,
-                                false /* skipFirstFrame */);
-                    }
+                    wakeAndUnlockDejank();
                 } else {
-                    mScrimController.animateGoingToFullShade(delay, fadeoutDuration);
                     mStatusBar.finishKeyguardFadingAway();
                     mFingerprintUnlockController.finishKeyguardFadingAway();
                 }
@@ -449,30 +406,17 @@
         mBouncer.prepare();
     }
 
-    private void animateScrimControllerKeyguardFadingOut(long delay, long duration,
-            boolean skipFirstFrame) {
-        animateScrimControllerKeyguardFadingOut(delay, duration, null /* endRunnable */,
-                skipFirstFrame);
+    public void onKeyguardFadedAway() {
+        mContainer.postDelayed(() -> mStatusBarWindowManager.setKeyguardFadingAway(false),
+                100);
+        mStatusBar.finishKeyguardFadingAway();
+        mFingerprintUnlockController.finishKeyguardFadingAway();
+        WindowManagerGlobal.getInstance().trimMemory(
+                ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN);
+
     }
 
-    private void animateScrimControllerKeyguardFadingOut(long delay, long duration,
-            final Runnable endRunnable, boolean skipFirstFrame) {
-        Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW, "Fading out", 0);
-        mScrimController.animateKeyguardFadingOut(delay, duration, new Runnable() {
-            @Override
-            public void run() {
-                if (endRunnable != null) {
-                    endRunnable.run();
-                }
-                mContainer.postDelayed(() -> mStatusBarWindowManager.setKeyguardFadingAway(false),
-                        100);
-                mStatusBar.finishKeyguardFadingAway();
-                mFingerprintUnlockController.finishKeyguardFadingAway();
-                WindowManagerGlobal.getInstance().trimMemory(
-                        ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN);
-                Trace.asyncTraceEnd(Trace.TRACE_TAG_VIEW, "Fading out", 0);
-            }
-        }, skipFirstFrame);
+    private void wakeAndUnlockDejank() {
         if (mFingerprintUnlockController.getMode() == MODE_WAKE_AND_UNLOCK
                 && LatencyTracker.isEnabled(mContext)) {
             DejankUtils.postAfterTraversal(() ->
@@ -593,7 +537,6 @@
         if (bouncerShowing != mLastBouncerShowing || mFirstUpdate) {
             mStatusBarWindowManager.setBouncerShowing(bouncerShowing);
             mStatusBar.setBouncerShowing(bouncerShowing);
-            mScrimController.setBouncerShowing(bouncerShowing);
         }
 
         KeyguardUpdateMonitor updateMonitor = KeyguardUpdateMonitor.getInstance(mContext);
@@ -611,7 +554,6 @@
         mLastBouncerDismissible = bouncerDismissible;
         mLastRemoteInputActive = remoteInputActive;
         mLastDozing = mDozing;
-        mLastDeferScrimFadeOut = mDeferScrimFadeOut;
         mLastFpMode = mFingerprintUnlockController.getMode();
         mStatusBar.onKeyguardViewManagerStatesUpdated();
     }
@@ -624,7 +566,7 @@
         boolean keyguardShowing = mShowing && !mOccluded;
         boolean hideWhileDozing = mDozing && fpMode != MODE_WAKE_AND_UNLOCK_PULSING;
         return (!keyguardShowing && !hideWhileDozing || mBouncer.isShowing()
-                || mRemoteInputActive) && !mDeferScrimFadeOut;
+                || mRemoteInputActive);
     }
 
     /**
@@ -634,7 +576,7 @@
         boolean keyguardShowing = mLastShowing && !mLastOccluded;
         boolean hideWhileDozing = mLastDozing && mLastFpMode != MODE_WAKE_AND_UNLOCK_PULSING;
         return (!keyguardShowing && !hideWhileDozing || mLastBouncerShowing
-                || mLastRemoteInputActive) && !mLastDeferScrimFadeOut;
+                || mLastRemoteInputActive);
     }
 
     public boolean shouldDismissOnMenuPressed() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BrightnessMirrorController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BrightnessMirrorController.java
index b0553d7..a011952 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BrightnessMirrorController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BrightnessMirrorController.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.policy;
 
+import android.annotation.NonNull;
 import android.util.ArraySet;
 import android.view.ContextThemeWrapper;
 import android.view.LayoutInflater;
@@ -26,46 +27,47 @@
 import com.android.internal.util.Preconditions;
 import com.android.systemui.Interpolators;
 import com.android.systemui.R;
-import com.android.systemui.statusbar.phone.ScrimController;
 import com.android.systemui.statusbar.phone.StatusBarWindowView;
 import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
 
+import java.util.function.Consumer;
+
 /**
  * Controls showing and hiding of the brightness mirror.
  */
 public class BrightnessMirrorController
         implements CallbackController<BrightnessMirrorController.BrightnessMirrorListener> {
 
-    private final NotificationStackScrollLayout mStackScroller;
-    public long TRANSITION_DURATION_OUT = 150;
-    public long TRANSITION_DURATION_IN = 200;
+    private final static long TRANSITION_DURATION_OUT = 150;
+    private final static long TRANSITION_DURATION_IN = 200;
 
     private final StatusBarWindowView mStatusBarWindow;
-    private final ScrimController mScrimController;
+    private final NotificationStackScrollLayout mStackScroller;
+    private final Consumer<Boolean> mVisibilityCallback;
     private final View mNotificationPanel;
     private final ArraySet<BrightnessMirrorListener> mBrightnessMirrorListeners = new ArraySet<>();
     private final int[] mInt2Cache = new int[2];
     private View mBrightnessMirror;
 
     public BrightnessMirrorController(StatusBarWindowView statusBarWindow,
-            ScrimController scrimController) {
+            @NonNull Consumer<Boolean> visibilityCallback) {
         mStatusBarWindow = statusBarWindow;
         mBrightnessMirror = statusBarWindow.findViewById(R.id.brightness_mirror);
         mNotificationPanel = statusBarWindow.findViewById(R.id.notification_panel);
         mStackScroller = statusBarWindow.findViewById(R.id.notification_stack_scroller);
-        mScrimController = scrimController;
+        mVisibilityCallback = visibilityCallback;
     }
 
     public void showMirror() {
         mBrightnessMirror.setVisibility(View.VISIBLE);
         mStackScroller.setFadingOut(true);
-        mScrimController.forceHideScrims(true /* hide */, true /* animated */);
+        mVisibilityCallback.accept(true);
         outAnimation(mNotificationPanel.animate())
                 .withLayer();
     }
 
     public void hideMirror() {
-        mScrimController.forceHideScrims(false /* hide */, true /* animated */);
+        mVisibilityCallback.accept(false);
         inAnimation(mNotificationPanel.animate())
                 .withLayer()
                 .withEndAction(() -> {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeHostFake.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeHostFake.java
index b0c9f328..1c104cf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeHostFake.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeHostFake.java
@@ -19,12 +19,13 @@
 import android.annotation.NonNull;
 import android.app.PendingIntent;
 
+import com.android.systemui.util.wakelock.WakeLock;
+
 /**
  * A rudimentary fake for DozeHost.
  */
 class DozeHostFake implements DozeHost {
     Callback callback;
-    boolean pulseAborted;
     boolean pulseExtended;
     boolean animateWakeup;
     boolean dozing;
@@ -92,11 +93,6 @@
     }
 
     @Override
-    public void abortPulsing() {
-        pulseAborted = true;
-    }
-
-    @Override
     public void extendPulse() {
         pulseExtended = true;
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ScrimViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ScrimViewTest.java
index 4c3bf10..a4bcc69 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ScrimViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ScrimViewTest.java
@@ -86,9 +86,9 @@
 
     @Test
     public void testSetViewAlpha_propagatesToDrawable() {
-        float alpha = 0.5f;
+        final float alpha = 0.5f;
         mView.setViewAlpha(alpha);
-        assertEquals(mView.getViewAlpha(), alpha);
+        assertEquals("View alpha did not propagate to drawable", alpha, mView.getViewAlpha());
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeScrimControllerTest.java
new file mode 100644
index 0000000..ca2f713
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeScrimControllerTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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.phone;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.os.Debug;
+import android.support.test.filters.SmallTest;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.doze.DozeHost;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+@SmallTest
+public class DozeScrimControllerTest extends SysuiTestCase {
+
+    private ScrimController mScrimController;
+    private DozeScrimController mDozeScrimController;
+
+    @Before
+    public void setup() {
+        mScrimController = mock(ScrimController.class);
+        // Make sure callbacks will be invoked to complete the lifecycle.
+        doAnswer(invocationOnMock -> {
+            ScrimController.Callback callback = invocationOnMock.getArgument(1);
+            callback.onStart();
+            callback.onDisplayBlanked();
+            callback.onFinished();
+            return null;
+        }).when(mScrimController).transitionTo(any(ScrimState.class),
+                any(ScrimController.Callback.class));
+
+        mDozeScrimController = new DozeScrimController(mScrimController, getContext());
+        mDozeScrimController.setDozing(true);
+    }
+
+    @Test
+    public void changesScrimControllerState() {
+        mDozeScrimController.pulse(mock(DozeHost.PulseCallback.class), 0);
+        verify(mScrimController).transitionTo(eq(ScrimState.PULSING),
+                any(ScrimController.Callback.class));
+    }
+
+    @Test
+    public void callsPulseCallback() {
+        DozeHost.PulseCallback callback = mock(DozeHost.PulseCallback.class);
+        mDozeScrimController.pulse(callback, 0);
+
+        verify(callback).onPulseStarted();
+        mDozeScrimController.pulseOutNow();
+        verify(callback).onPulseFinished();
+    }
+
+    @Test
+    public void secondPulseIsSuppressed() {
+        DozeHost.PulseCallback callback1 = mock(DozeHost.PulseCallback.class);
+        DozeHost.PulseCallback callback2 = mock(DozeHost.PulseCallback.class);
+        mDozeScrimController.pulse(callback1, 0);
+        mDozeScrimController.pulse(callback2, 0);
+
+        verify(callback1, never()).onPulseFinished();
+        verify(callback2).onPulseFinished();
+    }
+
+    @Test
+    public void suppressesPulseIfNotDozing() {
+        mDozeScrimController.setDozing(false);
+        DozeHost.PulseCallback callback = mock(DozeHost.PulseCallback.class);
+        mDozeScrimController.pulse(callback, 0);
+
+        verify(callback).onPulseFinished();
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
new file mode 100644
index 0000000..b9f695b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
@@ -0,0 +1,300 @@
+/*
+ * 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.phone;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.animation.Animator;
+import android.graphics.Color;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.test.filters.SmallTest;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.View;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.statusbar.ScrimView;
+import com.android.systemui.util.wakelock.WakeLock;
+import com.android.systemui.utils.os.FakeHandler;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.function.Consumer;
+
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+@SmallTest
+public class ScrimControllerTest extends SysuiTestCase {
+
+    private SynchronousScrimController mScrimController;
+    private ScrimView mScrimBehind;
+    private ScrimView mScrimInFront;
+    private View mHeadsUpScrim;
+    private Consumer<Boolean> mScrimVisibilityCallback;
+    private Boolean mScrimVisibile;
+    private LightBarController mLightBarController;
+    private DozeParameters mDozeParamenters;
+    private WakeLock mWakeLock;
+    private boolean mAlwaysOnEnabled;
+
+    @Before
+    public void setup() {
+        mLightBarController = mock(LightBarController.class);
+        mScrimBehind = new ScrimView(getContext());
+        mScrimInFront = new ScrimView(getContext());
+        mHeadsUpScrim = mock(View.class);
+        mWakeLock = mock(WakeLock.class);
+        mAlwaysOnEnabled = true;
+        mScrimVisibilityCallback = (Boolean visible) -> mScrimVisibile = visible;
+        mDozeParamenters = mock(DozeParameters.class);
+        when(mDozeParamenters.getAlwaysOn()).thenAnswer(invocation -> mAlwaysOnEnabled);
+        mScrimController = new SynchronousScrimController(mLightBarController, mScrimBehind,
+                mScrimInFront, mHeadsUpScrim, mScrimVisibilityCallback, mDozeParamenters);
+    }
+
+    @Test
+    public void initialState() {
+        Assert.assertEquals("ScrimController should start initialized",
+                mScrimController.getState(), ScrimState.UNINITIALIZED);
+    }
+
+    @Test
+    public void transitionToKeyguard() {
+        mScrimController.transitionTo(ScrimState.KEYGUARD);
+        mScrimController.finishAnimationsImmediately();
+        // Front scrim should be transparent
+        // Back scrim should be visible without tint
+        assertScrimVisibility(false /* front */, true /* behind */);
+        assertScrimTint(mScrimBehind, false /* tinted */);
+    }
+
+    @Test
+    public void transitionToAod() {
+        mScrimController.transitionTo(ScrimState.AOD);
+        mScrimController.finishAnimationsImmediately();
+        // Front scrim should be transparent
+        // Back scrim should be visible with tint
+        assertScrimVisibility(false /* front */, true /* behind */);
+        assertScrimTint(mScrimBehind, true /* tinted */);
+        assertScrimTint(mScrimInFront, true /* tinted */);
+    }
+
+    @Test
+    public void transitionToPulsing() {
+        mScrimController.transitionTo(ScrimState.PULSING);
+        mScrimController.finishAnimationsImmediately();
+        // Front scrim should be transparent
+        // Back scrim should be visible with tint
+        // Pulse callback should have been invoked
+        assertScrimVisibility(false /* front */, true /* behind */);
+        assertScrimTint(mScrimBehind, true /* tinted */);
+    }
+
+    @Test
+    public void transitionToBouncer() {
+        mScrimController.transitionTo(ScrimState.BOUNCER);
+        mScrimController.finishAnimationsImmediately();
+        // Front scrim should be transparent
+        // Back scrim should be visible without tint
+        assertScrimVisibility(true /* front */, true /* behind */);
+        assertScrimTint(mScrimBehind, false /* tinted */);
+    }
+
+    @Test
+    public void transitionToUnlocked() {
+        mScrimController.transitionTo(ScrimState.UNLOCKED);
+        mScrimController.finishAnimationsImmediately();
+        // Front scrim should be transparent
+        // Back scrim should be transparent
+        assertScrimVisibility(false /* front */, false /* behind */);
+        assertScrimTint(mScrimBehind, false /* tinted */);
+        assertScrimTint(mScrimInFront, false /* tinted */);
+
+        // Back scrim should be visible after start dragging
+        mScrimController.setPanelExpansion(0.5f);
+        assertScrimVisibility(false /* front */, true /* behind */);
+    }
+
+    @Test
+    public void transitionToUnlockedFromAod() {
+        // Simulate unlock with fingerprint
+        mScrimController.transitionTo(ScrimState.AOD);
+        mScrimController.finishAnimationsImmediately();
+        mScrimController.transitionTo(ScrimState.UNLOCKED);
+        // Immediately tinted after the transition starts
+        assertScrimTint(mScrimInFront, true /* tinted */);
+        assertScrimTint(mScrimBehind, true /* tinted */);
+        mScrimController.finishAnimationsImmediately();
+        // Front scrim should be transparent
+        // Back scrim should be transparent
+        // Neither scrims should be tinted anymore after the animation.
+        assertScrimVisibility(false /* front */, false /* behind */);
+        assertScrimTint(mScrimInFront, false /* tinted */);
+        assertScrimTint(mScrimBehind, false /* tinted */);
+    }
+
+    @Test
+    public void scrimBlanksBeforeLeavingAoD() {
+        // Simulate unlock with fingerprint
+        mScrimController.transitionTo(ScrimState.AOD);
+        mScrimController.finishAnimationsImmediately();
+        mScrimController.transitionTo(ScrimState.UNLOCKED,
+                new ScrimController.Callback() {
+                    @Override
+                    public void onDisplayBlanked() {
+                        // Front scrim should be black in the middle of the transition
+                        Assert.assertTrue("Scrim should be visible during transition. Alpha: "
+                                + mScrimInFront.getViewAlpha(), mScrimInFront.getViewAlpha() > 0);
+                        assertScrimTint(mScrimInFront, true /* tinted */);
+                        Assert.assertTrue("Scrim should be visible during transition.",
+                                mScrimVisibile);
+                    }
+                });
+        mScrimController.finishAnimationsImmediately();
+    }
+
+    @Test
+    public void testScrimCallback() {
+        int[] callOrder = {0, 0, 0};
+        int[] currentCall = {0};
+        mScrimController.transitionTo(ScrimState.AOD, new ScrimController.Callback() {
+            @Override
+            public void onStart() {
+                callOrder[0] = ++currentCall[0];
+            }
+
+            @Override
+            public void onDisplayBlanked() {
+                callOrder[1] = ++currentCall[0];
+            }
+
+            @Override
+            public void onFinished() {
+                callOrder[2] = ++currentCall[0];
+            }
+        });
+        mScrimController.finishAnimationsImmediately();
+        Assert.assertEquals("onStart called in wrong order", 1, callOrder[0]);
+        Assert.assertEquals("onDisplayBlanked called in wrong order", 2, callOrder[1]);
+        Assert.assertEquals("onFinished called in wrong order", 3, callOrder[2]);
+    }
+
+    @Test
+    public void testScrimCallbacksWithoutAmbientDisplay() {
+        mAlwaysOnEnabled = false;
+        testScrimCallback();
+    }
+
+    @Test
+    public void testScrimCallbackCancelled() {
+        boolean[] cancelledCalled = {false};
+        mScrimController.transitionTo(ScrimState.AOD, new ScrimController.Callback() {
+            @Override
+            public void onCancelled() {
+                cancelledCalled[0] = true;
+            }
+        });
+        mScrimController.transitionTo(ScrimState.PULSING);
+        Assert.assertTrue("onCancelled should have been called", cancelledCalled[0]);
+    }
+
+    @Test
+    public void testHoldsWakeLock() {
+        mScrimController.transitionTo(ScrimState.AOD);
+        verify(mWakeLock, times(1)).acquire();
+        verify(mWakeLock, never()).release();
+        mScrimController.finishAnimationsImmediately();
+        verify(mWakeLock, times(1)).release();
+    }
+
+    private void assertScrimTint(ScrimView scrimView, boolean tinted) {
+        final boolean viewIsTinted = scrimView.getTint() != Color.TRANSPARENT;
+        final String name = scrimView == mScrimInFront ? "front" : "back";
+        Assert.assertEquals("Tint test failed at state " + mScrimController.getState()
+                +" with scrim: " + name + " and tint: " + Integer.toHexString(scrimView.getTint()),
+                tinted, viewIsTinted);
+    }
+
+    private void assertScrimVisibility(boolean inFront, boolean behind) {
+        Assert.assertEquals("Unexpected front scrim visibility. Alpha is "
+                + mScrimInFront.getViewAlpha(), inFront, mScrimInFront.getViewAlpha() > 0);
+        Assert.assertEquals("Unexpected back scrim visibility. Alpha is "
+                + mScrimBehind.getViewAlpha(), behind, mScrimBehind.getViewAlpha() > 0);
+        Assert.assertEquals("Invalid visibility.", inFront || behind, mScrimVisibile);
+    }
+
+    /**
+     * Special version of ScrimController where animations have 0 duration for test purposes.
+     */
+    private class SynchronousScrimController extends ScrimController {
+
+        private FakeHandler mHandler;
+
+        public SynchronousScrimController(LightBarController lightBarController,
+                ScrimView scrimBehind, ScrimView scrimInFront, View headsUpScrim,
+                Consumer<Boolean> scrimVisibleListener, DozeParameters dozeParameters) {
+            super(lightBarController, scrimBehind, scrimInFront, headsUpScrim,
+                    scrimVisibleListener, dozeParameters);
+            mHandler = new FakeHandler(Looper.myLooper());
+        }
+
+        public void finishAnimationsImmediately() {
+            boolean[] animationFinished = {false};
+            setOnAnimationFinished(()-> animationFinished[0] = true);
+
+            // Execute code that will trigger animations.
+            onPreDraw();
+
+            // Force finish screen blanking.
+            endAnimation(mScrimInFront, TAG_KEY_ANIM_BLANK);
+            mHandler.dispatchQueuedMessages();
+            // Force finish all animations.
+            endAnimation(mScrimBehind, TAG_KEY_ANIM);
+            endAnimation(mScrimInFront, TAG_KEY_ANIM);
+
+            if (!animationFinished[0]) {
+                throw new IllegalStateException("Animation never finished");
+            }
+        }
+
+        private void endAnimation(ScrimView scrimView, int tag) {
+            Animator animator = (Animator) scrimView.getTag(tag);
+            if (animator != null) {
+                animator.end();
+            }
+        }
+
+        @Override
+        protected Handler getHandler() {
+            return mHandler;
+        }
+
+        @Override
+        protected WakeLock createWakeLock() {
+            return mWakeLock;
+        }
+    }
+
+}
diff --git a/services/core/java/com/android/server/AlarmManagerService.java b/services/core/java/com/android/server/AlarmManagerService.java
index 3904fc9..ca15249 100644
--- a/services/core/java/com/android/server/AlarmManagerService.java
+++ b/services/core/java/com/android/server/AlarmManagerService.java
@@ -46,16 +46,15 @@
 import android.os.PowerManager;
 import android.os.Process;
 import android.os.RemoteException;
-import android.os.ServiceManager;
 import android.os.SystemClock;
 import android.os.SystemProperties;
+import android.os.Trace;
 import android.os.UserHandle;
 import android.os.WorkSource;
 import android.provider.Settings;
 import android.text.TextUtils;
 import android.text.format.DateFormat;
 import android.util.ArrayMap;
-import android.util.ArraySet;
 import android.util.KeyValueListParser;
 import android.util.Log;
 import android.util.Slog;
@@ -81,6 +80,7 @@
 import java.util.Random;
 import java.util.TimeZone;
 import java.util.TreeSet;
+import java.util.function.Predicate;
 
 import static android.app.AlarmManager.RTC_WAKEUP;
 import static android.app.AlarmManager.RTC;
@@ -88,11 +88,17 @@
 import static android.app.AlarmManager.ELAPSED_REALTIME;
 
 import com.android.internal.annotations.GuardedBy;
-import com.android.internal.app.IAppOpsCallback;
-import com.android.internal.app.IAppOpsService;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.LocalLog;
+import com.android.server.ForceAppStandbyTracker.Listener;
 
+/**
+ * Alarm manager implementaion.
+ *
+ * Unit test:
+ atest $ANDROID_BUILD_TOP/frameworks/base/services/tests/servicestests/src/com/android/server/AlarmManagerServiceTest.java
+ */
 class AlarmManagerService extends SystemService {
     private static final int RTC_WAKEUP_MASK = 1 << RTC_WAKEUP;
     private static final int RTC_MASK = 1 << RTC;
@@ -131,13 +137,10 @@
     final LocalLog mLog = new LocalLog(TAG);
 
     AppOpsManager mAppOps;
-    IAppOpsService mAppOpsService;
     DeviceIdleController.LocalService mLocalDeviceIdleController;
 
     final Object mLock = new Object();
 
-    ArraySet<String> mForcedAppStandbyPackages = new ArraySet<>();
-    SparseBooleanArray mForegroundUids = new SparseBooleanArray();
     // List of alarms per uid deferred due to user applied background restrictions on the source app
     SparseArray<ArrayList<Alarm>> mPendingBackgroundAlarms = new SparseArray<>();
     long mNativeData;
@@ -184,12 +187,6 @@
     int mSystemUiUid;
 
     /**
-     * The current set of user whitelisted apps for device idle mode, meaning these are allowed
-     * to freely schedule alarms.
-     */
-    int[] mDeviceIdleUserWhitelist = new int[0];
-
-    /**
      * For each uid, this is the last time we dispatched an "allow while idle" alarm,
      * used to determine the earliest we can dispatch the next such alarm. Times are in the
      * 'elapsed' timebase.
@@ -223,6 +220,8 @@
     private final SparseArray<AlarmManager.AlarmClockInfo> mHandlerSparseAlarmClockArray =
             new SparseArray<>();
 
+    private final ForceAppStandbyTracker mForceAppStandbyTracker;
+
     /**
      * All times are in milliseconds. These constants are kept synchronized with the system
      * global Settings. Any access to this class or its fields should be done while
@@ -757,6 +756,9 @@
     public AlarmManagerService(Context context) {
         super(context);
         mConstants = new Constants(mHandler);
+
+        mForceAppStandbyTracker = ForceAppStandbyTracker.getInstance(context);
+        mForceAppStandbyTracker.addListener(mForceAppStandbyListener);
     }
 
     static long convertToElapsed(long when, int type) {
@@ -894,17 +896,48 @@
         deliverPendingBackgroundAlarmsLocked(alarmsToDeliver, SystemClock.elapsedRealtime());
     }
 
-    void sendPendingBackgroundAlarmsForAppIdLocked(int appId) {
+    /**
+     * Check all alarms in {@link #mPendingBackgroundAlarms} and send the ones that are not
+     * restricted.
+     *
+     * This is only called when the global "force all apps-standby" flag changes or when the
+     * power save whitelist changes, so it's okay to be slow.
+     */
+    void sendAllUnrestrictedPendingBackgroundAlarmsLocked() {
         final ArrayList<Alarm> alarmsToDeliver = new ArrayList<>();
-        for (int i = mPendingBackgroundAlarms.size() - 1; i >= 0; i--) {
-            final int uid = mPendingBackgroundAlarms.keyAt(i);
-            final ArrayList<Alarm> alarmsForUid = mPendingBackgroundAlarms.valueAt(i);
-            if (UserHandle.getAppId(uid) == appId) {
-                alarmsToDeliver.addAll(alarmsForUid);
-                mPendingBackgroundAlarms.removeAt(i);
+
+        findAllUnrestrictedPendingBackgroundAlarmsLockedInner(
+                mPendingBackgroundAlarms, alarmsToDeliver, this::isBackgroundRestricted);
+
+        if (alarmsToDeliver.size() > 0) {
+            deliverPendingBackgroundAlarmsLocked(alarmsToDeliver, SystemClock.elapsedRealtime());
+        }
+    }
+
+    @VisibleForTesting
+    static void findAllUnrestrictedPendingBackgroundAlarmsLockedInner(
+            SparseArray<ArrayList<Alarm>> pendingAlarms, ArrayList<Alarm> unrestrictedAlarms,
+            Predicate<Alarm> isBackgroundRestricted) {
+
+        for (int uidIndex = pendingAlarms.size() - 1; uidIndex >= 0; uidIndex--) {
+            final int uid = pendingAlarms.keyAt(uidIndex);
+            final ArrayList<Alarm> alarmsForUid = pendingAlarms.valueAt(uidIndex);
+
+            for (int alarmIndex = alarmsForUid.size() - 1; alarmIndex >= 0; alarmIndex--) {
+                final Alarm alarm = alarmsForUid.get(alarmIndex);
+
+                if (isBackgroundRestricted.test(alarm)) {
+                    continue;
+                }
+
+                unrestrictedAlarms.add(alarm);
+                alarmsForUid.remove(alarmIndex);
+            }
+
+            if (alarmsForUid.size() == 0) {
+                pendingAlarms.removeAt(uidIndex);
             }
         }
-        deliverPendingBackgroundAlarmsLocked(alarmsToDeliver, SystemClock.elapsedRealtime());
     }
 
     private void deliverPendingBackgroundAlarmsLocked(ArrayList<Alarm> alarms, long nowELAPSED) {
@@ -1234,10 +1267,8 @@
         } catch (RemoteException e) {
             // ignored; both services live in system_server
         }
-        mAppOpsService = IAppOpsService.Stub.asInterface(
-                ServiceManager.getService(Context.APP_OPS_SERVICE));
         publishBinderService(Context.ALARM_SERVICE, mService);
-        publishLocalService(LocalService.class, new LocalService());
+        mForceAppStandbyTracker.start();
     }
 
     @Override
@@ -1247,13 +1278,6 @@
             mAppOps = (AppOpsManager) getContext().getSystemService(Context.APP_OPS_SERVICE);
             mLocalDeviceIdleController
                     = LocalServices.getService(DeviceIdleController.LocalService.class);
-            try {
-                mAppOpsService.startWatchingMode(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, null,
-                        new AppOpsWatcher());
-            } catch (RemoteException rexc) {
-                // Shouldn't happen as they are in the same process.
-                Slog.e(TAG, "AppOps service not reachable", rexc);
-            }
         }
     }
 
@@ -1582,8 +1606,7 @@
             // timing restrictions.
             } else if (workSource == null && (callingUid < Process.FIRST_APPLICATION_UID
                     || callingUid == mSystemUiUid
-                    || Arrays.binarySearch(mDeviceIdleUserWhitelist,
-                            UserHandle.getAppId(callingUid)) >= 0)) {
+                    || mForceAppStandbyTracker.isUidPowerSaveWhitelisted(callingUid))) {
                 flags |= AlarmManager.FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED;
                 flags &= ~AlarmManager.FLAG_ALLOW_WHILE_IDLE;
             }
@@ -1660,24 +1683,14 @@
         }
     };
 
-    public final class LocalService {
-        public void setDeviceIdleUserWhitelist(int[] appids) {
-            setDeviceIdleUserWhitelistImpl(appids);
-        }
-    }
-
     void dumpImpl(PrintWriter pw) {
         synchronized (mLock) {
             pw.println("Current Alarm Manager state:");
             mConstants.dump(pw);
             pw.println();
 
-            pw.print("  Foreground uids: [");
-            for (int i = 0; i < mForegroundUids.size(); i++) {
-                if (mForegroundUids.valueAt(i)) pw.print(mForegroundUids.keyAt(i) + " ");
-            }
-            pw.println("]");
-            pw.println("  Forced app standby packages: " + mForcedAppStandbyPackages);
+            mForceAppStandbyTracker.dump(pw, "  ");
+
             final long nowRTC = System.currentTimeMillis();
             final long nowELAPSED = SystemClock.elapsedRealtime();
             SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@@ -1717,7 +1730,6 @@
             pw.print(" set at "); TimeUtils.formatDuration(mLastWakeupSet, nowELAPSED, pw);
             pw.println();
             pw.print("  Num time change events: "); pw.println(mNumTimeChanged);
-            pw.println("  mDeviceIdleUserWhitelist=" + Arrays.toString(mDeviceIdleUserWhitelist));
 
             pw.println();
             pw.println("  Next alarm clock information: ");
@@ -1990,15 +2002,8 @@
 
             mConstants.dumpProto(proto, AlarmManagerServiceProto.SETTINGS);
 
-            final int foregroundUidsSize = mForegroundUids.size();
-            for (int i = 0; i < foregroundUidsSize; i++) {
-                if (mForegroundUids.valueAt(i)) {
-                    proto.write(AlarmManagerServiceProto.FOREGROUND_UIDS, mForegroundUids.keyAt(i));
-                }
-            }
-            for (String pkg : mForcedAppStandbyPackages) {
-                proto.write(AlarmManagerServiceProto.FORCED_APP_STANDBY_PACKAGES, pkg);
-            }
+            mForceAppStandbyTracker.dumpProto(proto,
+                    AlarmManagerServiceProto.FORCE_APP_STANDBY_TRACKER);
 
             proto.write(AlarmManagerServiceProto.IS_INTERACTIVE, mInteractive);
             if (!mInteractive) {
@@ -2022,9 +2027,6 @@
             proto.write(AlarmManagerServiceProto.TIME_SINCE_LAST_WAKEUP_SET_MS,
                     nowElapsed - mLastWakeupSet);
             proto.write(AlarmManagerServiceProto.TIME_CHANGE_EVENT_COUNT, mNumTimeChanged);
-            for (int i : mDeviceIdleUserWhitelist) {
-                proto.write(AlarmManagerServiceProto.DEVICE_IDLE_USER_WHITELIST_APP_IDS, i);
-            }
 
             final TreeSet<Integer> users = new TreeSet<>();
             final int nextAlarmClockForUserSize = mNextAlarmClockForUser.size();
@@ -2266,28 +2268,6 @@
         }
     }
 
-    void setDeviceIdleUserWhitelistImpl(int[] appids) {
-        synchronized (mLock) {
-            // appids are sorted, just send pending alarms for any new appids added to the whitelist
-            int i = 0, j = 0;
-            while (i < appids.length) {
-                while (j < mDeviceIdleUserWhitelist.length
-                        && mDeviceIdleUserWhitelist[j] < appids[i]) {
-                    j++;
-                }
-                if (j < mDeviceIdleUserWhitelist.length
-                        && appids[i] != mDeviceIdleUserWhitelist[j]) {
-                    if (DEBUG_BG_LIMIT) {
-                        Slog.d(TAG, "Sending blocked alarms for whitelisted appid " + appids[j]);
-                    }
-                    sendPendingBackgroundAlarmsForAppIdLocked(appids[j]);
-                }
-                i++;
-            }
-            mDeviceIdleUserWhitelist = appids;
-        }
-    }
-
     AlarmManager.AlarmClockInfo getNextAlarmClockImpl(int userId) {
         synchronized (mLock) {
             return mNextAlarmClockForUser.get(userId);
@@ -2710,9 +2690,7 @@
         final String sourcePackage =
                 (alarm.operation != null) ? alarm.operation.getCreatorPackage() : alarm.packageName;
         final int sourceUid = alarm.creatorUid;
-        return mForcedAppStandbyPackages.contains(sourcePackage) && !mForegroundUids.get(sourceUid)
-                && Arrays.binarySearch(mDeviceIdleUserWhitelist, UserHandle.getAppId(sourceUid))
-                < 0;
+        return mForceAppStandbyTracker.areAlarmsRestricted(sourceUid, sourcePackage);
     }
 
     private native long init();
@@ -2859,7 +2837,8 @@
         }
     }
 
-    private static class Alarm {
+    @VisibleForTesting
+    static class Alarm {
         public final int type;
         public final long origWhen;
         public final boolean wakeup;
@@ -3073,6 +3052,11 @@
         for (int i=0; i<triggerList.size(); i++) {
             Alarm alarm = triggerList.get(i);
             final boolean allowWhileIdle = (alarm.flags&AlarmManager.FLAG_ALLOW_WHILE_IDLE) != 0;
+            if (alarm.wakeup) {
+              Trace.traceBegin(Trace.TRACE_TAG_POWER, "Dispatch wakeup alarm to " + alarm.packageName);
+            } else {
+              Trace.traceBegin(Trace.TRACE_TAG_POWER, "Dispatch non-wakeup alarm to " + alarm.packageName);
+            }
             try {
                 if (localLOGV) {
                     Slog.v(TAG, "sending alarm " + alarm);
@@ -3092,6 +3076,7 @@
             } catch (RuntimeException e) {
                 Slog.w(TAG, "Failure sending alarm.", e);
             }
+            Trace.traceEnd(Trace.TRACE_TAG_POWER);
         }
     }
 
@@ -3476,17 +3461,10 @@
                 if (disabled) {
                     removeForStoppedLocked(uid);
                 }
-                mForegroundUids.delete(uid);
             }
         }
 
         @Override public void onUidActive(int uid) {
-            synchronized (mLock) {
-                if (!mForegroundUids.get(uid)) {
-                    mForegroundUids.put(uid, true);
-                    sendPendingBackgroundAlarmsLocked(uid, null);
-                }
-            }
         }
 
         @Override public void onUidIdle(int uid, boolean disabled) {
@@ -3494,7 +3472,6 @@
                 if (disabled) {
                     removeForStoppedLocked(uid);
                 }
-                mForegroundUids.delete(uid);
             }
         }
 
@@ -3502,27 +3479,29 @@
         }
     };
 
-    private final class AppOpsWatcher extends IAppOpsCallback.Stub {
+
+    private final Listener mForceAppStandbyListener = new Listener() {
         @Override
-        public void opChanged(int op, int uid, String packageName) throws RemoteException {
+        public void unblockAllUnrestrictedAlarms() {
             synchronized (mLock) {
-                final int mode = mAppOpsService.checkOperation(op, uid, packageName);
-                if (DEBUG_BG_LIMIT) {
-                    Slog.d(TAG,
-                            "Appop changed for " + uid + ", " + packageName + " to " + mode);
-                }
-                final boolean changed;
-                if (mode != AppOpsManager.MODE_ALLOWED) {
-                    changed = mForcedAppStandbyPackages.add(packageName);
-                } else {
-                    changed = mForcedAppStandbyPackages.remove(packageName);
-                }
-                if (changed && mode == AppOpsManager.MODE_ALLOWED) {
-                    sendPendingBackgroundAlarmsLocked(uid, packageName);
-                }
+                sendAllUnrestrictedPendingBackgroundAlarmsLocked();
             }
         }
-    }
+
+        @Override
+        public void unblockAlarmsForUid(int uid) {
+            synchronized (mLock) {
+                sendPendingBackgroundAlarmsLocked(uid, null);
+            }
+        }
+
+        @Override
+        public void unblockAlarmsForUidPackage(int uid, String packageName) {
+            synchronized (mLock) {
+                sendPendingBackgroundAlarmsLocked(uid, packageName);
+            }
+        }
+    };
 
     private final BroadcastStats getStatsLocked(PendingIntent pi) {
         String pkg = pi.getCreatorPackage();
diff --git a/services/core/java/com/android/server/DeviceIdleController.java b/services/core/java/com/android/server/DeviceIdleController.java
index 0921a00..d7aeb8c 100644
--- a/services/core/java/com/android/server/DeviceIdleController.java
+++ b/services/core/java/com/android/server/DeviceIdleController.java
@@ -119,7 +119,6 @@
     private PowerManagerInternal mLocalPowerManager;
     private PowerManager mPowerManager;
     private ConnectivityService mConnectivityService;
-    private AlarmManagerService.LocalService mLocalAlarmManager;
     private INetworkPolicyManager mNetworkPolicyManager;
     private SensorManager mSensorManager;
     private Sensor mMotionSensor;
@@ -1435,7 +1434,6 @@
                 mGoingIdleWakeLock.setReferenceCounted(true);
                 mConnectivityService = (ConnectivityService)ServiceManager.getService(
                         Context.CONNECTIVITY_SERVICE);
-                mLocalAlarmManager = getLocalService(AlarmManagerService.LocalService.class);
                 mNetworkPolicyManager = INetworkPolicyManager.Stub.asInterface(
                         ServiceManager.getService(Context.NETWORK_POLICY_SERVICE));
                 mNetworkPolicyManagerInternal = getLocalService(NetworkPolicyManagerInternal.class);
@@ -1500,8 +1498,8 @@
 
                 mLocalActivityManager.setDeviceIdleWhitelist(mPowerSaveWhitelistAllAppIdArray);
                 mLocalPowerManager.setDeviceIdleWhitelist(mPowerSaveWhitelistAllAppIdArray);
-                mLocalAlarmManager.setDeviceIdleUserWhitelist(mPowerSaveWhitelistUserAppIdArray);
 
+                passWhiteListToForceAppStandbyTrackerLocked();
                 updateInteractivityLocked();
             }
             updateConnectivityState(null);
@@ -2477,13 +2475,7 @@
             }
             mLocalPowerManager.setDeviceIdleWhitelist(mPowerSaveWhitelistAllAppIdArray);
         }
-        if (mLocalAlarmManager != null) {
-            if (DEBUG) {
-                Slog.d(TAG, "Setting alarm whitelist to "
-                        + Arrays.toString(mPowerSaveWhitelistUserAppIdArray));
-            }
-            mLocalAlarmManager.setDeviceIdleUserWhitelist(mPowerSaveWhitelistUserAppIdArray);
-        }
+        passWhiteListToForceAppStandbyTrackerLocked();
     }
 
     private void updateTempWhitelistAppIdsLocked(int appId, boolean adding) {
@@ -2509,6 +2501,7 @@
             }
             mLocalPowerManager.setDeviceIdleTempWhitelist(mTempWhitelistAppIdArray);
         }
+        passWhiteListToForceAppStandbyTrackerLocked();
     }
 
     private void reportPowerSaveWhitelistChangedLocked() {
@@ -2523,6 +2516,12 @@
         getContext().sendBroadcastAsUser(intent, UserHandle.SYSTEM);
     }
 
+    private void passWhiteListToForceAppStandbyTrackerLocked() {
+        ForceAppStandbyTracker.getInstance(getContext()).setPowerSaveWhitelistAppIds(
+                mPowerSaveWhitelistAllAppIdArray,
+                mTempWhitelistAppIdArray);
+    }
+
     void readConfigFileLocked() {
         if (DEBUG) Slog.d(TAG, "Reading config from " + mConfigFile.getBaseFile());
         mPowerSaveWhitelistUserApps.clear();
diff --git a/services/core/java/com/android/server/ForceAppStandbyTracker.java b/services/core/java/com/android/server/ForceAppStandbyTracker.java
index 5dd3ee0..61d3833 100644
--- a/services/core/java/com/android/server/ForceAppStandbyTracker.java
+++ b/services/core/java/com/android/server/ForceAppStandbyTracker.java
@@ -16,13 +16,18 @@
 package com.android.server;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.AppOpsManager;
 import android.app.AppOpsManager.PackageOps;
+import android.app.IActivityManager;
 import android.app.IUidObserver;
+import android.content.BroadcastReceiver;
 import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
 import android.os.PowerManager.ServiceType;
 import android.os.PowerManagerInternal;
 import android.os.RemoteException;
@@ -30,21 +35,36 @@
 import android.os.UserHandle;
 import android.util.ArraySet;
 import android.util.Pair;
-import android.util.Slog;
 import android.util.SparseBooleanArray;
+import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.IAppOpsCallback;
 import com.android.internal.app.IAppOpsService;
+import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.Preconditions;
+import com.android.server.ForceAppStandbyTrackerProto.RunAnyInBackgroundRestrictedPackages;
 
+import java.io.PrintWriter;
+import java.util.Arrays;
 import java.util.List;
 
 /**
- * Class to track OP_RUN_ANY_IN_BACKGROUND, UID foreground state and "force all app standby".
+ * Class to keep track of the information related to "force app standby", which includes:
+ * - OP_RUN_ANY_IN_BACKGROUND for each package
+ * - UID foreground state
+ * - User+system power save whitelist
+ * - Temporary power save whitelist
+ * - Global "force all apps standby" mode enforced by battery saver.
  *
- * TODO Clean up cache when a user is deleted.
- * TODO Add unit tests. b/68769804.
+ * TODO: In general, we can reduce the number of callbacks by checking all signals before sending
+ *    each callback. For example, even when an UID comes into the foreground, if it wasn't
+ *    originally restricted, then there's no need to send an event.
+ *    Doing this would be error-prone, so we punt it for now, but we should revisit it later.
+ *
+ * Test:
+   atest $ANDROID_BUILD_TOP/frameworks/base/services/tests/servicestests/src/com/android/server/ForceAppStandbyTrackerTest.java
  */
 public class ForceAppStandbyTracker {
     private static final String TAG = "ForceAppStandbyTracker";
@@ -55,22 +75,32 @@
     private final Object mLock = new Object();
     private final Context mContext;
 
+    @VisibleForTesting
+    static final int TARGET_OP = AppOpsManager.OP_RUN_ANY_IN_BACKGROUND;
+
+    IActivityManager mIActivityManager;
     AppOpsManager mAppOpsManager;
     IAppOpsService mAppOpsService;
     PowerManagerInternal mPowerManagerInternal;
 
-    private final Handler mCallbackHandler;
+    private final MyHandler mHandler;
 
     /**
      * Pair of (uid (not user-id), packageName) with OP_RUN_ANY_IN_BACKGROUND *not* allowed.
      */
     @GuardedBy("mLock")
-    final ArraySet<Pair<Integer, String>> mForcedAppStandbyUidPackages = new ArraySet<>();
+    final ArraySet<Pair<Integer, String>> mRunAnyRestrictedPackages = new ArraySet<>();
 
     @GuardedBy("mLock")
     final SparseBooleanArray mForegroundUids = new SparseBooleanArray();
 
     @GuardedBy("mLock")
+    private int[] mPowerWhitelistedAllAppIds = new int[0];
+
+    @GuardedBy("mLock")
+    private int[] mTempWhitelistedAppIds = mPowerWhitelistedAllAppIds;
+
+    @GuardedBy("mLock")
     final ArraySet<Listener> mListeners = new ArraySet<>();
 
     @GuardedBy("mLock")
@@ -80,16 +110,116 @@
     boolean mForceAllAppsStandby;
 
     public static abstract class Listener {
-        public void onRestrictionChanged(int uid, @Nullable String packageName) {
+        /**
+         * This is called when the OP_RUN_ANY_IN_BACKGROUND appops changed for a package.
+         */
+        private void onRunAnyAppOpsChanged(ForceAppStandbyTracker sender,
+                int uid, @NonNull String packageName) {
+            updateJobsForUidPackage(uid, packageName);
+
+            if (!sender.areAlarmsRestricted(uid, packageName)) {
+                unblockAlarmsForUidPackage(uid, packageName);
+            }
         }
 
-        public void onGlobalRestrictionChanged() {
+        /**
+         * This is called when the foreground state changed for a UID.
+         */
+        private void onUidForegroundStateChanged(ForceAppStandbyTracker sender, int uid) {
+            updateJobsForUid(uid);
+
+            if (sender.isInForeground(uid)) {
+                unblockAlarmsForUid(uid);
+            }
+        }
+
+        /**
+         * This is called when an app-id(s) is removed from the power save whitelist.
+         */
+        private void onPowerSaveUnwhitelisted(ForceAppStandbyTracker sender) {
+            updateAllJobs();
+            unblockAllUnrestrictedAlarms();
+        }
+
+        /**
+         * This is called when the power save whitelist changes, excluding the
+         * {@link #onPowerSaveUnwhitelisted} case.
+         */
+        private void onPowerSaveWhitelistedChanged(ForceAppStandbyTracker sender) {
+            updateAllJobs();
+        }
+
+        /**
+         * This is called when the temp whitelist changes.
+         */
+        private void onTempPowerSaveWhitelistChanged(ForceAppStandbyTracker sender) {
+
+            // TODO This case happens rather frequently; consider optimizing and update jobs
+            // only for affected app-ids.
+
+            updateAllJobs();
+        }
+
+        /**
+         * This is called when the global "force all apps standby" flag changes.
+         */
+        private void onForceAllAppsStandbyChanged(ForceAppStandbyTracker sender) {
+            updateAllJobs();
+
+            if (!sender.isForceAllAppsStandbyEnabled()) {
+                unblockAllUnrestrictedAlarms();
+            }
+        }
+
+        /**
+         * Called when the job restrictions for multiple UIDs might have changed, so the job
+         * scheduler should re-evaluate all restrictions for all jobs.
+         */
+        public void updateAllJobs() {
+        }
+
+        /**
+         * Called when the job restrictions for a UID might have changed, so the job
+         * scheduler should re-evaluate all restrictions for all jobs.
+         */
+        public void updateJobsForUid(int uid) {
+        }
+
+        /**
+         * Called when the job restrictions for a UID - package might have changed, so the job
+         * scheduler should re-evaluate all restrictions for all jobs.
+         */
+        public void updateJobsForUidPackage(int uid, String packageName) {
+        }
+
+        /**
+         * Called when the job restrictions for multiple UIDs might have changed, so the alarm
+         * manager should re-evaluate all restrictions for all blocked jobs.
+         */
+        public void unblockAllUnrestrictedAlarms() {
+        }
+
+        /**
+         * Called when all jobs for a specific UID are unblocked.
+         */
+        public void unblockAlarmsForUid(int uid) {
+        }
+
+        /**
+         * Called when all alarms for a specific UID - package are unblocked.
+         */
+        public void unblockAlarmsForUidPackage(int uid, String packageName) {
         }
     }
 
-    private ForceAppStandbyTracker(Context context) {
+    @VisibleForTesting
+    ForceAppStandbyTracker(Context context, Looper looper) {
         mContext = context;
-        mCallbackHandler = FgThread.getHandler();
+        mHandler = new MyHandler(looper);
+    }
+
+    private ForceAppStandbyTracker(Context context) {
+        this(context, FgThread.get().getLooper());
     }
 
     /**
@@ -112,45 +242,65 @@
             }
             mStarted = true;
 
-            mAppOpsManager = Preconditions.checkNotNull(
-                    mContext.getSystemService(AppOpsManager.class));
-            mAppOpsService = Preconditions.checkNotNull(
-                    IAppOpsService.Stub.asInterface(
-                            ServiceManager.getService(Context.APP_OPS_SERVICE)));
-            mPowerManagerInternal = Preconditions.checkNotNull(
-                    LocalServices.getService(PowerManagerInternal.class));
+            mIActivityManager = Preconditions.checkNotNull(injectIActivityManager());
+            mAppOpsManager = Preconditions.checkNotNull(injectAppOpsManager());
+            mAppOpsService = Preconditions.checkNotNull(injectIAppOpsService());
+            mPowerManagerInternal = Preconditions.checkNotNull(injectPowerManagerInternal());
 
             try {
-                ActivityManager.getService().registerUidObserver(new UidObserver(),
+                mIActivityManager.registerUidObserver(new UidObserver(),
                         ActivityManager.UID_OBSERVER_GONE | ActivityManager.UID_OBSERVER_IDLE
                                 | ActivityManager.UID_OBSERVER_ACTIVE,
                         ActivityManager.PROCESS_STATE_UNKNOWN, null);
-                mAppOpsService.startWatchingMode(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, null,
+                mAppOpsService.startWatchingMode(TARGET_OP, null,
                         new AppOpsWatcher());
             } catch (RemoteException e) {
                 // shouldn't happen.
             }
 
-            mPowerManagerInternal.registerLowPowerModeObserver(
-                    ServiceType.FORCE_ALL_APPS_STANDBY,
-                    state -> updateForceAllAppsStandby(state.batterySaverEnabled));
-
-            updateForceAllAppsStandby(
-                    mPowerManagerInternal.getLowPowerState(ServiceType.FORCE_ALL_APPS_STANDBY)
-                            .batterySaverEnabled);
+            IntentFilter filter = new IntentFilter();
+            filter.addAction(Intent.ACTION_USER_REMOVED);
+            mContext.registerReceiver(new MyReceiver(), filter);
 
             refreshForcedAppStandbyUidPackagesLocked();
+
+            mPowerManagerInternal.registerLowPowerModeObserver(
+                    ServiceType.FORCE_ALL_APPS_STANDBY,
+                    (state) -> updateForceAllAppsStandby(state.batterySaverEnabled));
+
+            updateForceAllAppsStandby(mPowerManagerInternal.getLowPowerState(
+                    ServiceType.FORCE_ALL_APPS_STANDBY).batterySaverEnabled);
         }
     }
 
+    @VisibleForTesting
+    AppOpsManager injectAppOpsManager() {
+        return mContext.getSystemService(AppOpsManager.class);
+    }
+
+    @VisibleForTesting
+    IAppOpsService injectIAppOpsService() {
+        return IAppOpsService.Stub.asInterface(
+                ServiceManager.getService(Context.APP_OPS_SERVICE));
+    }
+
+    @VisibleForTesting
+    IActivityManager injectIActivityManager() {
+        return ActivityManager.getService();
+    }
+
+    @VisibleForTesting
+    PowerManagerInternal injectPowerManagerInternal() {
+        return LocalServices.getService(PowerManagerInternal.class);
+    }
+
     /**
-     * Update {@link #mForcedAppStandbyUidPackages} with the current app ops state.
+     * Update {@link #mRunAnyRestrictedPackages} with the current app ops state.
      */
     private void refreshForcedAppStandbyUidPackagesLocked() {
-        final int op = AppOpsManager.OP_RUN_ANY_IN_BACKGROUND;
-
-        mForcedAppStandbyUidPackages.clear();
-        final List<PackageOps> ops = mAppOpsManager.getPackagesForOps(new int[] {op});
+        mRunAnyRestrictedPackages.clear();
+        final List<PackageOps> ops = mAppOpsManager.getPackagesForOps(
+                new int[] {TARGET_OP});
 
         if (ops == null) {
             return;
@@ -162,31 +312,38 @@
 
             for (int j = 0; j < entries.size(); j++) {
                 AppOpsManager.OpEntry ent = entries.get(j);
-                if (ent.getOp() != op) {
+                if (ent.getOp() != TARGET_OP) {
                     continue;
                 }
                 if (ent.getMode() != AppOpsManager.MODE_ALLOWED) {
-                    mForcedAppStandbyUidPackages.add(Pair.create(
+                    mRunAnyRestrictedPackages.add(Pair.create(
                             pkg.getUid(), pkg.getPackageName()));
                 }
             }
         }
     }
 
-    boolean isRunAnyInBackgroundAppOpRestricted(int uid, @NonNull String packageName) {
-        try {
-            return mAppOpsService.checkOperation(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND,
-                    uid, packageName) != AppOpsManager.MODE_ALLOWED;
-        } catch (RemoteException e) {
-            return false; // shouldn't happen.
+    /**
+     * Update {@link #mForceAllAppsStandby} and notifies the listeners.
+     */
+    void updateForceAllAppsStandby(boolean enable) {
+        synchronized (mLock) {
+            if (enable == mForceAllAppsStandby) {
+                return;
+            }
+            mForceAllAppsStandby = enable;
+
+            mHandler.notifyForceAllAppsStandbyChanged();
         }
     }
 
     private int findForcedAppStandbyUidPackageIndexLocked(int uid, @NonNull String packageName) {
-        // TODO Maybe we should switch to indexOf(Pair.create()) if the array size is too big.
-        final int size = mForcedAppStandbyUidPackages.size();
+        final int size = mRunAnyRestrictedPackages.size();
+        if (size > 8) {
+            return mRunAnyRestrictedPackages.indexOf(Pair.create(uid, packageName));
+        }
         for (int i = 0; i < size; i++) {
-            final Pair<Integer, String> pair = mForcedAppStandbyUidPackages.valueAt(i);
+            final Pair<Integer, String> pair = mRunAnyRestrictedPackages.valueAt(i);
 
             if ((pair.first == uid) && packageName.equals(pair.second)) {
                 return i;
@@ -196,13 +353,16 @@
     }
 
     /**
-     * @return whether a uid package-name pair is in mForcedAppStandbyUidPackages.
+     * @return whether a uid package-name pair is in mRunAnyRestrictedPackages.
      */
-    boolean isUidPackageRestrictedLocked(int uid, @NonNull String packageName) {
+    boolean isRunAnyRestrictedLocked(int uid, @NonNull String packageName) {
         return findForcedAppStandbyUidPackageIndexLocked(uid, packageName) >= 0;
     }
 
-    boolean updateRestrictedUidPackageLocked(int uid, @NonNull String packageName,
+    /**
+     * Add to / remove from {@link #mRunAnyRestrictedPackages}.
+     */
+    boolean updateForcedAppStandbyUidPackageLocked(int uid, @NonNull String packageName,
             boolean restricted) {
         final int index =  findForcedAppStandbyUidPackageIndexLocked(uid, packageName);
         final boolean wasRestricted = index >= 0;
@@ -210,13 +370,16 @@
             return false;
         }
         if (restricted) {
-            mForcedAppStandbyUidPackages.add(Pair.create(uid, packageName));
+            mRunAnyRestrictedPackages.add(Pair.create(uid, packageName));
         } else {
-            mForcedAppStandbyUidPackages.removeAt(index);
+            mRunAnyRestrictedPackages.removeAt(index);
         }
         return true;
     }
 
+    /**
+     * Puts a UID to {@link #mForegroundUids}.
+     */
     void uidToForeground(int uid) {
         synchronized (mLock) {
             if (!UserHandle.isApp(uid)) {
@@ -228,10 +391,13 @@
                 return;
             }
             mForegroundUids.put(uid, true);
-            notifyForUidPackage(uid, null);
+            mHandler.notifyUidForegroundStateChanged(uid);
         }
     }
 
+    /**
+     * Sets false for a UID {@link #mForegroundUids}, or remove it when {@code remove} is true.
+     */
     void uidToBackground(int uid, boolean remove) {
         synchronized (mLock) {
             if (!UserHandle.isApp(uid)) {
@@ -247,13 +413,11 @@
             } else {
                 mForegroundUids.put(uid, false);
             }
-            notifyForUidPackage(uid, null);
+            mHandler.notifyUidForegroundStateChanged(uid);
         }
     }
 
-    // Event handlers
-
-    final class UidObserver extends IUidObserver.Stub {
+    private final class UidObserver extends IUidObserver.Stub {
         @Override public void onUidStateChanged(int uid, int procState, long procStateSeq) {
         }
 
@@ -277,11 +441,28 @@
     private final class AppOpsWatcher extends IAppOpsCallback.Stub {
         @Override
         public void opChanged(int op, int uid, String packageName) throws RemoteException {
+            boolean restricted = false;
+            try {
+                restricted = mAppOpsService.checkOperation(TARGET_OP,
+                        uid, packageName) != AppOpsManager.MODE_ALLOWED;
+            } catch (RemoteException e) {
+                // Shouldn't happen
+            }
             synchronized (mLock) {
-                final boolean restricted = isRunAnyInBackgroundAppOpRestricted(uid, packageName);
+                if (updateForcedAppStandbyUidPackageLocked(uid, packageName, restricted)) {
+                    mHandler.notifyRunAnyAppOpsChanged(uid, packageName);
+                }
+            }
+        }
+    }
 
-                if (updateRestrictedUidPackageLocked(uid, packageName, restricted)) {
-                    notifyForUidPackage(uid, packageName);
+    private final class MyReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (Intent.ACTION_USER_REMOVED.equals(intent.getAction())) {
+                final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
+                if (userId > 0) {
+                    mHandler.doUserRemoved(userId);
                 }
             }
         }
@@ -293,31 +474,183 @@
         }
     }
 
-    void notifyForUidPackage(int uid, String packageName) {
-        mCallbackHandler.post(() -> {
-            for (Listener l : cloneListeners()) {
-                l.onRestrictionChanged(uid, packageName);
-            }
-        });
-    }
+    private class MyHandler extends Handler {
+        private static final int MSG_UID_STATE_CHANGED = 1;
+        private static final int MSG_RUN_ANY_CHANGED = 2;
+        private static final int MSG_ALL_UNWHITELISTED = 3;
+        private static final int MSG_ALL_WHITELIST_CHANGED = 4;
+        private static final int MSG_TEMP_WHITELIST_CHANGED = 5;
+        private static final int MSG_FORCE_ALL_CHANGED = 6;
 
-    void notifyGlobal() {
-        mCallbackHandler.post(() -> {
-            for (Listener l : cloneListeners()) {
-                l.onGlobalRestrictionChanged();
-            }
-        });
-    }
+        private static final int MSG_USER_REMOVED = 7;
 
-    void updateForceAllAppsStandby(boolean forceAllAppsStandby) {
-        synchronized (mLock) {
-            if (mForceAllAppsStandby == forceAllAppsStandby) {
-                return;
-            }
-            mForceAllAppsStandby = forceAllAppsStandby;
-            Slog.i(TAG, "Force all app standby: " + mForceAllAppsStandby);
-            notifyGlobal();
+        public MyHandler(Looper looper) {
+            super(looper);
         }
+
+        public void notifyUidForegroundStateChanged(int uid) {
+            obtainMessage(MSG_UID_STATE_CHANGED, uid, 0).sendToTarget();
+        }
+        public void notifyRunAnyAppOpsChanged(int uid, @NonNull String packageName) {
+            obtainMessage(MSG_RUN_ANY_CHANGED, uid, 0, packageName).sendToTarget();
+        }
+
+        public void notifyAllUnwhitelisted() {
+            obtainMessage(MSG_ALL_UNWHITELISTED).sendToTarget();
+        }
+
+        public void notifyAllWhitelistChanged() {
+            obtainMessage(MSG_ALL_WHITELIST_CHANGED).sendToTarget();
+        }
+
+        public void notifyTempWhitelistChanged() {
+            obtainMessage(MSG_TEMP_WHITELIST_CHANGED).sendToTarget();
+        }
+
+        public void notifyForceAllAppsStandbyChanged() {
+            obtainMessage(MSG_FORCE_ALL_CHANGED).sendToTarget();
+        }
+
+        public void doUserRemoved(int userId) {
+            obtainMessage(MSG_USER_REMOVED, userId, 0).sendToTarget();
+        }
+
+        @Override
+        public void dispatchMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_USER_REMOVED:
+                    handleUserRemoved(msg.arg1);
+                    return;
+            }
+
+            // Only notify the listeners when started.
+            synchronized (mLock) {
+                if (!mStarted) {
+                    return;
+                }
+            }
+            final ForceAppStandbyTracker sender = ForceAppStandbyTracker.this;
+
+            switch (msg.what) {
+                case MSG_UID_STATE_CHANGED:
+                    for (Listener l : cloneListeners()) {
+                        l.onUidForegroundStateChanged(sender, msg.arg1);
+                    }
+                    return;
+                case MSG_RUN_ANY_CHANGED:
+                    for (Listener l : cloneListeners()) {
+                        l.onRunAnyAppOpsChanged(sender, msg.arg1, (String) msg.obj);
+                    }
+                    return;
+                case MSG_ALL_UNWHITELISTED:
+                    for (Listener l : cloneListeners()) {
+                        l.onPowerSaveUnwhitelisted(sender);
+                    }
+                    return;
+                case MSG_ALL_WHITELIST_CHANGED:
+                    for (Listener l : cloneListeners()) {
+                        l.onPowerSaveWhitelistedChanged(sender);
+                    }
+                    return;
+                case MSG_TEMP_WHITELIST_CHANGED:
+                    for (Listener l : cloneListeners()) {
+                        l.onTempPowerSaveWhitelistChanged(sender);
+                    }
+                    return;
+                case MSG_FORCE_ALL_CHANGED:
+                    for (Listener l : cloneListeners()) {
+                        l.onForceAllAppsStandbyChanged(sender);
+                    }
+                    return;
+                case MSG_USER_REMOVED:
+                    handleUserRemoved(msg.arg1);
+                    return;
+            }
+        }
+    }
+
+    void handleUserRemoved(int removedUserId) {
+        synchronized (mLock) {
+            for (int i = mRunAnyRestrictedPackages.size() - 1; i >= 0; i--) {
+                final Pair<Integer, String> pair = mRunAnyRestrictedPackages.valueAt(i);
+                final int uid = pair.first;
+                final int userId = UserHandle.getUserId(uid);
+
+                if (userId == removedUserId) {
+                    mRunAnyRestrictedPackages.removeAt(i);
+                }
+            }
+            for (int i = mForegroundUids.size() - 1; i >= 0; i--) {
+                final int uid = mForegroundUids.keyAt(i);
+                final int userId = UserHandle.getUserId(uid);
+
+                if (userId == removedUserId) {
+                    mForegroundUids.removeAt(i);
+                }
+            }
+        }
+    }
+
+    /**
+     * Called by device idle controller to update the power save whitelists.
+     */
+    public void setPowerSaveWhitelistAppIds(
+            int[] powerSaveWhitelistAllAppIdArray, int[] tempWhitelistAppIdArray) {
+        synchronized (mLock) {
+            final int[] previousWhitelist = mPowerWhitelistedAllAppIds;
+            final int[] previousTempWhitelist = mTempWhitelistedAppIds;
+
+            mPowerWhitelistedAllAppIds = powerSaveWhitelistAllAppIdArray;
+            mTempWhitelistedAppIds = tempWhitelistAppIdArray;
+
+            if (isAnyAppIdUnwhitelisted(previousWhitelist, mPowerWhitelistedAllAppIds)) {
+                mHandler.notifyAllUnwhitelisted();
+            } else if (!Arrays.equals(previousWhitelist, mPowerWhitelistedAllAppIds)) {
+                mHandler.notifyAllWhitelistChanged();
+            }
+
+            if (!Arrays.equals(previousTempWhitelist, mTempWhitelistedAppIds)) {
+                mHandler.notifyTempWhitelistChanged();
+            }
+
+        }
+    }
+
+    /**
+     * @retunr true if a sorted app-id array {@code prevArray} has at least one element
+     * that's not in a sorted app-id array {@code newArray}.
+     */
+    @VisibleForTesting
+    static boolean isAnyAppIdUnwhitelisted(int[] prevArray, int[] newArray) {
+        int i1 = 0;
+        int i2 = 0;
+        boolean prevFinished;
+        boolean newFinished;
+
+        for (;;) {
+            prevFinished = i1 >= prevArray.length;
+            newFinished = i2 >= newArray.length;
+            if (prevFinished || newFinished) {
+                break;
+            }
+            int a1 = prevArray[i1];
+            int a2 = newArray[i2];
+
+            if (a1 == a2) {
+                i1++;
+                i2++;
+                continue;
+            }
+            if (a1 < a2) {
+                // prevArray has an element that's not in a2.
+                return true;
+            }
+            i2++;
+        }
+        if (prevFinished) {
+            return false;
+        }
+        return newFinished;
     }
 
     // Public interface.
@@ -332,21 +665,51 @@
     }
 
     /**
-     * Whether force-app-standby is effective for a UID package-name.
+     * @return whether alarms should be restricted for a UID package-name.
      */
-    public boolean isRestricted(int uid, @NonNull String packageName) {
+    public boolean areAlarmsRestricted(int uid, @NonNull String packageName) {
+        return isRestricted(uid, packageName, /*useTempWhitelistToo=*/ false);
+    }
+
+    /**
+     * @return whether jobs should be restricted for a UID package-name.
+     */
+    public boolean areJobsRestricted(int uid, @NonNull String packageName) {
+        return isRestricted(uid, packageName, /*useTempWhitelistToo=*/ true);
+    }
+
+    /**
+     * @return whether force-app-standby is effective for a UID package-name.
+     */
+    private boolean isRestricted(int uid, @NonNull String packageName,
+            boolean useTempWhitelistToo) {
         if (isInForeground(uid)) {
             return false;
         }
         synchronized (mLock) {
+            // Whitelisted?
+            final int appId = UserHandle.getAppId(uid);
+            if (ArrayUtils.contains(mPowerWhitelistedAllAppIds, appId)) {
+                return false;
+            }
+            if (useTempWhitelistToo &&
+                    ArrayUtils.contains(mTempWhitelistedAppIds, appId)) {
+                return false;
+            }
+
             if (mForceAllAppsStandby) {
                 return true;
             }
-            return isUidPackageRestrictedLocked(uid, packageName);
+
+            return isRunAnyRestrictedLocked(uid, packageName);
         }
     }
 
-    /** For dumpsys -- otherwise the callers don't need to know it. */
+    /**
+     * @return whether a UID is in the foreground or not.
+     *
+     * Note clients normally shouldn't need to access it. It's only for dumpsys.
+     */
     public boolean isInForeground(int uid) {
         if (!UserHandle.isApp(uid)) {
             return true;
@@ -356,31 +719,120 @@
         }
     }
 
-    /** For dumpsys -- otherwise the callers don't need to know it. */
-    public boolean isForceAllAppsStandbyEnabled() {
+    /**
+     * @return whether force all apps standby is enabled or not.
+     *
+     * Note clients normally shouldn't need to access it.
+     */
+    boolean isForceAllAppsStandbyEnabled() {
         synchronized (mLock) {
             return mForceAllAppsStandby;
         }
     }
 
-    /** For dumpsys -- otherwise the callers don't need to know it. */
+    /**
+     * @return whether a UID/package has {@code OP_RUN_ANY_IN_BACKGROUND} allowed or not.
+     *
+     * Note clients normally shouldn't need to access it. It's only for dumpsys.
+     */
     public boolean isRunAnyInBackgroundAppOpsAllowed(int uid, @NonNull String packageName) {
         synchronized (mLock) {
-            return !isUidPackageRestrictedLocked(uid, packageName);
+            return !isRunAnyRestrictedLocked(uid, packageName);
         }
     }
 
-    /** For dumpsys -- otherwise the callers don't need to know it. */
-    public SparseBooleanArray getForegroudUids() {
+    /**
+     * @return whether a UID is in the user / system defined power-save whitelist or not.
+     *
+     * Note clients normally shouldn't need to access it. It's only for dumpsys.
+     */
+    public boolean isUidPowerSaveWhitelisted(int uid) {
         synchronized (mLock) {
-            return mForegroundUids.clone();
+            return ArrayUtils.contains(mPowerWhitelistedAllAppIds, UserHandle.getAppId(uid));
         }
     }
 
-    /** For dumpsys -- otherwise the callers don't need to know it. */
-    public ArraySet<Pair<Integer, String>> getRestrictedUidPackages() {
+    /**
+     * @return whether a UID is in the temp power-save whitelist or not.
+     *
+     * Note clients normally shouldn't need to access it. It's only for dumpsys.
+     */
+    public boolean isUidTempPowerSaveWhitelisted(int uid) {
         synchronized (mLock) {
-            return new ArraySet(mForcedAppStandbyUidPackages);
+            return ArrayUtils.contains(mTempWhitelistedAppIds, UserHandle.getAppId(uid));
+        }
+    }
+
+    public void dump(PrintWriter pw, String indent) {
+        synchronized (mLock) {
+            pw.print(indent);
+            pw.print("Force all apps standby: ");
+            pw.println(isForceAllAppsStandbyEnabled());
+
+            pw.print(indent);
+            pw.print("Foreground uids: [");
+
+            String sep = "";
+            for (int i = 0; i < mForegroundUids.size(); i++) {
+                if (mForegroundUids.valueAt(i)) {
+                    pw.print(sep);
+                    pw.print(UserHandle.formatUid(mForegroundUids.keyAt(i)));
+                    sep = " ";
+                }
+            }
+            pw.println("]");
+
+            pw.print(indent);
+            pw.print("Whitelist appids: ");
+            pw.println(Arrays.toString(mPowerWhitelistedAllAppIds));
+
+            pw.print(indent);
+            pw.print("Temp whitelist appids: ");
+            pw.println(Arrays.toString(mTempWhitelistedAppIds));
+
+            pw.print(indent);
+            pw.println("Restricted packages:");
+            for (Pair<Integer, String> uidAndPackage : mRunAnyRestrictedPackages) {
+                pw.print(indent);
+                pw.print("  ");
+                pw.print(UserHandle.formatUid(uidAndPackage.first));
+                pw.print(" ");
+                pw.print(uidAndPackage.second);
+                pw.println();
+            }
+        }
+    }
+
+    public void dumpProto(ProtoOutputStream proto, long fieldId) {
+        synchronized (mLock) {
+            final long token = proto.start(fieldId);
+
+            proto.write(ForceAppStandbyTrackerProto.FORCE_ALL_APPS_STANDBY, mForceAllAppsStandby);
+
+            for (int i = 0; i < mForegroundUids.size(); i++) {
+                if (mForegroundUids.valueAt(i)) {
+                    proto.write(ForceAppStandbyTrackerProto.FOREGROUND_UIDS,
+                            mForegroundUids.keyAt(i));
+                }
+            }
+
+            for (int appId : mPowerWhitelistedAllAppIds) {
+                proto.write(ForceAppStandbyTrackerProto.POWER_SAVE_WHITELIST_APP_IDS, appId);
+            }
+
+            for (int appId : mTempWhitelistedAppIds) {
+                proto.write(ForceAppStandbyTrackerProto.TEMP_POWER_SAVE_WHITELIST_APP_IDS, appId);
+            }
+
+            for (Pair<Integer, String> uidAndPackage : mRunAnyRestrictedPackages) {
+                final long token2 = proto.start(
+                        ForceAppStandbyTrackerProto.RUN_ANY_IN_BACKGROUND_RESTRICTED_PACKAGES);
+                proto.write(RunAnyInBackgroundRestrictedPackages.UID, uidAndPackage.first);
+                proto.write(RunAnyInBackgroundRestrictedPackages.PACKAGE_NAME,
+                        uidAndPackage.second);
+                proto.end(token2);
+            }
+            proto.end(token);
         }
     }
 }
diff --git a/services/core/java/com/android/server/am/ActivityDisplay.java b/services/core/java/com/android/server/am/ActivityDisplay.java
index 3dbee42..b11b16e1 100644
--- a/services/core/java/com/android/server/am/ActivityDisplay.java
+++ b/services/core/java/com/android/server/am/ActivityDisplay.java
@@ -399,6 +399,16 @@
                 otherStack.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
             }
         } finally {
+            if (mHomeStack != null && !isTopStack(mHomeStack)) {
+                // Whenever split-screen is dismissed we want the home stack directly behind the
+                // currently top stack so it shows up when the top stack is finished.
+                final ActivityStack topStack = getTopStack();
+                // TODO: Would be better to use ActivityDisplay.positionChildAt() for this, however
+                // ActivityDisplay doesn't have a direct controller to WM side yet. We can switch
+                // once we have that.
+                mHomeStack.moveToFront("onSplitScreenModeDismissed");
+                topStack.moveToFront("onSplitScreenModeDismissed");
+            }
             mSupervisor.mWindowManager.continueSurfaceLayout();
         }
     }
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 85af2cc..fe992da 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -318,6 +318,7 @@
 import android.os.ParcelFileDescriptor;
 import android.os.PersistableBundle;
 import android.os.PowerManager;
+import android.os.PowerManager.ServiceType;
 import android.os.PowerManagerInternal;
 import android.os.Process;
 import android.os.RemoteCallbackList;
@@ -1781,6 +1782,11 @@
 
     final boolean mPermissionReviewRequired;
 
+    /**
+     * Whether to force background check on all apps (for battery saver) or not.
+     */
+    boolean mForceBackgroundCheck;
+
     private static String sTheRealBuildSerial = Build.UNKNOWN;
 
     /**
@@ -8620,6 +8626,16 @@
         }
         switch (appop) {
             case AppOpsManager.MODE_ALLOWED:
+                // If force-background-check is enabled, restrict all apps that aren't whitelisted.
+                if (mForceBackgroundCheck &&
+                        UserHandle.isApp(uid) &&
+                        !isOnDeviceIdleWhitelistLocked(uid)) {
+                    if (DEBUG_BACKGROUND_CHECK) {
+                        Slog.i(TAG, "Force background check: " +
+                                uid + "/" + packageName + " restricted");
+                    }
+                    return ActivityManager.APP_START_MODE_DELAYED;
+                }
                 return ActivityManager.APP_START_MODE_NORMAL;
             case AppOpsManager.MODE_IGNORED:
                 return ActivityManager.APP_START_MODE_DELAYED;
@@ -8719,6 +8735,9 @@
         return ActivityManager.APP_START_MODE_NORMAL;
     }
 
+    /**
+     * @return whether a UID is in the system, user or temp doze whitelist.
+     */
     boolean isOnDeviceIdleWhitelistLocked(int uid) {
         final int appId = UserHandle.getAppId(uid);
         return Arrays.binarySearch(mDeviceIdleWhitelist, appId) >= 0
@@ -10503,12 +10522,12 @@
                             + " non-standard task " + taskId + " to windowing mode="
                             + windowingMode);
                 }
-                final ActivityDisplay display = task.getStack().getDisplay();
-                final ActivityStack stack = display.getOrCreateStack(windowingMode,
-                        task.getStack().getActivityType(), toTop);
-                // TODO: Use ActivityStack.setWindowingMode instead of re-parenting.
-                task.reparent(stack, toTop, REPARENT_KEEP_STACK_AT_FRONT, ANIMATE, !DEFER_RESUME,
-                        "moveTaskToStack");
+
+                final ActivityStack stack = task.getStack();
+                if (toTop) {
+                    stack.moveToFront("setTaskWindowingMode", task);
+                }
+                stack.setWindowingMode(windowingMode);
             } finally {
                 Binder.restoreCallingIdentity(ident);
             }
@@ -14176,6 +14195,16 @@
             readGrantedUriPermissionsLocked();
         }
 
+        final PowerManagerInternal pmi = LocalServices.getService(PowerManagerInternal.class);
+        if (pmi != null) {
+            pmi.registerLowPowerModeObserver(ServiceType.FORCE_BACKGROUND_CHECK,
+                    state -> updateForceBackgroundCheck(state.batterySaverEnabled));
+            updateForceBackgroundCheck(
+                    pmi.getLowPowerState(ServiceType.FORCE_BACKGROUND_CHECK).batterySaverEnabled);
+        } else {
+            Slog.wtf(TAG, "PowerManagerInternal not found.");
+        }
+
         if (goingCallback != null) goingCallback.run();
         traceLog.traceBegin("ActivityManagerStartApps");
         mBatteryStatsService.noteEvent(BatteryStats.HistoryItem.EVENT_USER_RUNNING_START,
@@ -14274,6 +14303,23 @@
         }
     }
 
+    private void updateForceBackgroundCheck(boolean enabled) {
+        synchronized (this) {
+            if (mForceBackgroundCheck != enabled) {
+                mForceBackgroundCheck = enabled;
+
+                if (DEBUG_BACKGROUND_CHECK) {
+                    Slog.i(TAG, "Force background check " + (enabled ? "enabled" : "disabled"));
+                }
+
+                if (mForceBackgroundCheck) {
+                    // Stop background services for idle UIDs.
+                    doStopUidForIdleUidsLocked();
+                }
+            }
+        }
+    }
+
     void killAppAtUsersRequest(ProcessRecord app, Dialog fromDialog) {
         synchronized (this) {
             mAppErrors.killAppAtUserRequestLocked(app, fromDialog);
@@ -15625,7 +15671,6 @@
     void dumpProcessesLocked(FileDescriptor fd, PrintWriter pw, String[] args,
             int opti, boolean dumpAll, String dumpPackage, int dumpAppId) {
         boolean needSep = false;
-        boolean printedAnything = false;
         int numPers = 0;
 
         pw.println("ACTIVITY MANAGER RUNNING PROCESSES (dumpsys activity processes)");
@@ -15643,7 +15688,6 @@
                     if (!needSep) {
                         pw.println("  All known processes:");
                         needSep = true;
-                        printedAnything = true;
                     }
                     pw.print(r.persistent ? "  *PERS*" : "  *APP*");
                         pw.print(" UID "); pw.print(procs.keyAt(ia));
@@ -15668,7 +15712,6 @@
                         pw.println();
                     }
                     pw.println("  Isolated process list (sorted by uid):");
-                    printedAnything = true;
                     printed = true;
                     needSep = true;
                 }
@@ -15690,7 +15733,6 @@
                         pw.println();
                     }
                     pw.println("  Active instrumentation:");
-                    printedAnything = true;
                     printed = true;
                     needSep = true;
                 }
@@ -15702,14 +15744,14 @@
 
         if (mActiveUids.size() > 0) {
             if (dumpUids(pw, dumpPackage, dumpAppId, mActiveUids, "UID states:", needSep)) {
-                printedAnything = needSep = true;
+                needSep = true;
             }
         }
         if (dumpAll) {
             if (mValidateUids.size() > 0) {
                 if (dumpUids(pw, dumpPackage, dumpAppId, mValidateUids, "UID validation:",
                         needSep)) {
-                    printedAnything = needSep = true;
+                    needSep = true;
                 }
             }
         }
@@ -15726,7 +15768,6 @@
                     pw.println("):");
             dumpProcessOomList(pw, this, mLruProcesses, "    ", "Proc", "PERS", false, dumpPackage);
             needSep = true;
-            printedAnything = true;
         }
 
         if (dumpAll || dumpPackage != null) {
@@ -15742,7 +15783,6 @@
                         needSep = true;
                         pw.println("  PID mappings:");
                         printed = true;
-                        printedAnything = true;
                     }
                     pw.print("    PID #"); pw.print(mPidsSelfLocked.keyAt(i));
                         pw.print(": "); pw.println(mPidsSelfLocked.valueAt(i));
@@ -15765,7 +15805,6 @@
                         needSep = true;
                         pw.println("  Foreground Processes:");
                         printed = true;
-                        printedAnything = true;
                     }
                     pw.print("    PID #"); pw.print(mImportantProcesses.keyAt(i));
                             pw.print(": "); pw.println(mImportantProcesses.valueAt(i));
@@ -15776,7 +15815,6 @@
         if (mPersistentStartingProcesses.size() > 0) {
             if (needSep) pw.println();
             needSep = true;
-            printedAnything = true;
             pw.println("  Persisent processes that are starting:");
             dumpProcessList(pw, this, mPersistentStartingProcesses, "    ",
                     "Starting Norm", "Restarting PERS", dumpPackage);
@@ -15785,7 +15823,6 @@
         if (mRemovedProcesses.size() > 0) {
             if (needSep) pw.println();
             needSep = true;
-            printedAnything = true;
             pw.println("  Processes that are being removed:");
             dumpProcessList(pw, this, mRemovedProcesses, "    ",
                     "Removed Norm", "Removed PERS", dumpPackage);
@@ -15794,7 +15831,6 @@
         if (mProcessesOnHold.size() > 0) {
             if (needSep) pw.println();
             needSep = true;
-            printedAnything = true;
             pw.println("  Processes that are on old until the system is ready:");
             dumpProcessList(pw, this, mProcessesOnHold, "    ",
                     "OnHold Norm", "OnHold PERS", dumpPackage);
@@ -15803,9 +15839,6 @@
         needSep = dumpProcessesToGc(fd, pw, args, opti, needSep, dumpAll, dumpPackage);
 
         needSep = mAppErrors.dumpLocked(fd, pw, needSep, dumpPackage);
-        if (needSep) {
-            printedAnything = true;
-        }
 
         if (dumpPackage == null) {
             pw.println();
@@ -16077,10 +16110,7 @@
                         pw.println();
             }
         }
-
-        if (!printedAnything) {
-            pw.println("  (nothing)");
-        }
+        pw.println("  mForceBackgroundCheck=" + mForceBackgroundCheck);
     }
 
     boolean dumpProcessesToGc(FileDescriptor fd, PrintWriter pw, String[] args,
@@ -23341,6 +23371,24 @@
         }
     }
 
+    /**
+     * Call {@link #doStopUidLocked} (which will also stop background services) for all idle UIDs.
+     */
+    void doStopUidForIdleUidsLocked() {
+        final int size = mActiveUids.size();
+        for (int i = 0; i < size; i++) {
+            final int uid = mActiveUids.keyAt(i);
+            if (!UserHandle.isApp(uid)) {
+                continue;
+            }
+            final UidRecord uidRec = mActiveUids.valueAt(i);
+            if (!uidRec.idle) {
+                continue;
+            }
+            doStopUidLocked(uidRec.uid, uidRec);
+        }
+    }
+
     final void doStopUidLocked(int uid, final UidRecord uidRec) {
         mServices.stopInBackgroundLocked(uid);
         enqueueUidChangeLocked(uidRec, uid, UidRecord.CHANGE_IDLE);
diff --git a/services/core/java/com/android/server/am/ActivityRecord.java b/services/core/java/com/android/server/am/ActivityRecord.java
index 883019e..a089e6c 100644
--- a/services/core/java/com/android/server/am/ActivityRecord.java
+++ b/services/core/java/com/android/server/am/ActivityRecord.java
@@ -647,8 +647,13 @@
             return;
         }
 
+        if (task.getStack().deferScheduleMultiWindowModeChanged()) {
+            // Don't do anything if we are currently deferring multi-window mode change.
+            return;
+        }
+
         // An activity is considered to be in multi-window mode if its task isn't fullscreen.
-        final boolean inMultiWindowMode = task.inMultiWindowMode();
+        final boolean inMultiWindowMode = inMultiWindowMode();
         if (inMultiWindowMode != mLastReportedMultiWindowMode) {
             mLastReportedMultiWindowMode = inMultiWindowMode;
             scheduleMultiWindowModeChanged(getConfiguration());
@@ -675,7 +680,7 @@
             // Picture-in-picture mode changes also trigger a multi-window mode change as well, so
             // update that here in order
             mLastReportedPictureInPictureMode = inPictureInPictureMode;
-            mLastReportedMultiWindowMode = inPictureInPictureMode;
+            mLastReportedMultiWindowMode = inMultiWindowMode();
             final Configuration newConfig = task.computeNewOverrideConfigurationForBounds(
                     targetStackBounds, null);
             schedulePictureInPictureModeChanged(newConfig);
diff --git a/services/core/java/com/android/server/am/ActivityStack.java b/services/core/java/com/android/server/am/ActivityStack.java
index 3cf2283..ced8621 100644
--- a/services/core/java/com/android/server/am/ActivityStack.java
+++ b/services/core/java/com/android/server/am/ActivityStack.java
@@ -494,7 +494,7 @@
 
         // Need to make sure windowing mode is supported.
         int windowingMode = display.resolveWindowingMode(
-                null /* ActivityRecord */, mTmpOptions, topTask, getActivityType());;
+                null /* ActivityRecord */, mTmpOptions, topTask, getActivityType());
         if (splitScreenStack == this && windowingMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY) {
             // Resolution to split-screen secondary for the primary split-screen stack means we want
             // to go fullscreen.
diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
index 52ee67e..445bf67 100644
--- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java
+++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
@@ -4308,6 +4308,10 @@
             final ActivityRecord r = task.mActivities.get(i);
             if (r.app != null && r.app.thread != null) {
                 mPipModeChangedActivities.add(r);
+                // If we are scheduling pip change, then remove this activity from multi-window
+                // change list as the processing of pip change will make sure multi-window changed
+                // message is processed in the right order relative to pip changed.
+                mMultiWindowModeChangedActivities.remove(r);
             }
         }
         mPipModeChangedTargetStackBounds = targetStackBounds;
diff --git a/services/core/java/com/android/server/am/ActivityStarter.java b/services/core/java/com/android/server/am/ActivityStarter.java
index 6010422..27eb985 100644
--- a/services/core/java/com/android/server/am/ActivityStarter.java
+++ b/services/core/java/com/android/server/am/ActivityStarter.java
@@ -2123,18 +2123,13 @@
             return mReuseTask.getStack();
         }
 
-        final int vrDisplayId = mPreferredDisplayId == mService.mVr2dDisplayId
-                ? mPreferredDisplayId : INVALID_DISPLAY;
-        final ActivityStack launchStack = mSupervisor.getLaunchStack(r, aOptions, task, ON_TOP,
-                vrDisplayId);
-
-        if (launchStack != null) {
-            return launchStack;
-        }
-
         if (((launchFlags & FLAG_ACTIVITY_LAUNCH_ADJACENT) == 0)
                  || mPreferredDisplayId != DEFAULT_DISPLAY) {
-            return null;
+            // We don't pass in the default display id into the get launch stack call so it can do a
+            // full resolution.
+            final int candidateDisplay =
+                    mPreferredDisplayId != DEFAULT_DISPLAY ? mPreferredDisplayId : INVALID_DISPLAY;
+            return mSupervisor.getLaunchStack(r, aOptions, task, ON_TOP, candidateDisplay);
         }
         // Otherwise handle adjacent launch.
 
@@ -2166,7 +2161,7 @@
                         mSupervisor.getDefaultDisplay().getSplitScreenPrimaryStack();
                 if (dockedStack != null && !dockedStack.shouldBeVisible(r)) {
                     // There is a docked stack, but it isn't visible, so we can't launch into that.
-                    return null;
+                    return mSupervisor.getLaunchStack(r, aOptions, task, ON_TOP);
                 } else {
                     return dockedStack;
                 }
diff --git a/services/core/java/com/android/server/am/TaskRecord.java b/services/core/java/com/android/server/am/TaskRecord.java
index fd6d618..83965ee 100644
--- a/services/core/java/com/android/server/am/TaskRecord.java
+++ b/services/core/java/com/android/server/am/TaskRecord.java
@@ -1991,7 +1991,6 @@
         final Rect currentBounds = getOverrideBounds();
 
         mTmpConfig.setTo(getOverrideConfiguration());
-        final boolean oldMatchParentBounds = matchParentBounds();
         final Configuration newConfig = getOverrideConfiguration();
 
         final boolean matchParentBounds = bounds == null || bounds.isEmpty();
@@ -2014,12 +2013,17 @@
                     mTmpRect.right != bounds.right, mTmpRect.bottom != bounds.bottom);
         }
         onOverrideConfigurationChanged(newConfig);
+        return !mTmpConfig.equals(newConfig);
+    }
 
-        if (matchParentBounds != oldMatchParentBounds) {
+    @Override
+    public void onConfigurationChanged(Configuration newParentConfig) {
+        final boolean wasInMultiWindowMode = inMultiWindowMode();
+        super.onConfigurationChanged(newParentConfig);
+        if (wasInMultiWindowMode != inMultiWindowMode()) {
             mService.mStackSupervisor.scheduleUpdateMultiWindowMode(this);
         }
-
-        return !mTmpConfig.equals(newConfig);
+        // TODO: Should also take care of Pip mode changes here.
     }
 
     /** Clears passed config and fills it with new override values. */
diff --git a/services/core/java/com/android/server/job/controllers/BackgroundJobsController.java b/services/core/java/com/android/server/job/controllers/BackgroundJobsController.java
index f67bd04..fc4015d 100644
--- a/services/core/java/com/android/server/job/controllers/BackgroundJobsController.java
+++ b/services/core/java/com/android/server/job/controllers/BackgroundJobsController.java
@@ -16,19 +16,12 @@
 
 package com.android.server.job.controllers;
 
-import android.content.BroadcastReceiver;
 import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
 import android.os.IDeviceIdleController;
-import android.os.PowerManager;
-import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.SystemClock;
 import android.os.UserHandle;
-import android.util.Pair;
 import android.util.Slog;
-import android.util.SparseBooleanArray;
 
 import com.android.internal.util.ArrayUtils;
 import com.android.server.ForceAppStandbyTracker;
@@ -37,7 +30,6 @@
 import com.android.server.job.JobStore;
 
 import java.io.PrintWriter;
-import java.util.function.Predicate;
 
 public final class BackgroundJobsController extends StateController {
 
@@ -51,9 +43,6 @@
     private final JobSchedulerService mJobSchedulerService;
     private final IDeviceIdleController mDeviceIdleController;
 
-    private int[] mPowerWhitelistedUserAppIds;
-    private int[] mTempWhitelistedAppIds;
-
     private final ForceAppStandbyTracker mForceAppStandbyTracker;
 
 
@@ -67,28 +56,6 @@
         }
     }
 
-    private BroadcastReceiver mDozeWhitelistReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            synchronized (mLock) {
-                try {
-                    switch (intent.getAction()) {
-                        case PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED:
-                            mPowerWhitelistedUserAppIds =
-                                    mDeviceIdleController.getAppIdUserWhitelist();
-                            break;
-                        case PowerManager.ACTION_POWER_SAVE_TEMP_WHITELIST_CHANGED:
-                            mTempWhitelistedAppIds = mDeviceIdleController.getAppIdTempWhitelist();
-                            break;
-                    }
-                } catch (RemoteException rexc) {
-                    Slog.e(LOG_TAG, "Device idle controller not reachable");
-                }
-                updateAllJobRestrictionsLocked();
-            }
-        }
-    };
-
     private BackgroundJobsController(JobSchedulerService service, Context context, Object lock) {
         super(service, context, lock);
         mJobSchedulerService = service;
@@ -97,19 +64,6 @@
 
         mForceAppStandbyTracker = ForceAppStandbyTracker.getInstance(context);
 
-        try {
-            mPowerWhitelistedUserAppIds = mDeviceIdleController.getAppIdUserWhitelist();
-            mTempWhitelistedAppIds = mDeviceIdleController.getAppIdTempWhitelist();
-        } catch (RemoteException rexc) {
-            // Shouldn't happen as they are in the same process.
-            Slog.e(LOG_TAG, "AppOps or DeviceIdle service not reachable", rexc);
-        }
-        IntentFilter powerWhitelistFilter = new IntentFilter();
-        powerWhitelistFilter.addAction(PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED);
-        powerWhitelistFilter.addAction(PowerManager.ACTION_POWER_SAVE_TEMP_WHITELIST_CHANGED);
-        context.registerReceiverAsUser(mDozeWhitelistReceiver, UserHandle.ALL, powerWhitelistFilter,
-                null, null);
-
         mForceAppStandbyTracker.addListener(mForceAppStandbyListener);
         mForceAppStandbyTracker.start();
     }
@@ -128,31 +82,7 @@
     public void dumpControllerStateLocked(final PrintWriter pw, final int filterUid) {
         pw.println("BackgroundJobsController");
 
-        pw.print("Force all apps standby: ");
-        pw.println(mForceAppStandbyTracker.isForceAllAppsStandbyEnabled());
-
-        pw.print("Foreground uids: [");
-        final SparseBooleanArray foregroundUids = mForceAppStandbyTracker.getForegroudUids();
-
-        String sep = "";
-        for (int i = 0; i < foregroundUids.size(); i++) {
-            if (foregroundUids.valueAt(i)) {
-                pw.print(sep);
-                pw.print(UserHandle.formatUid(foregroundUids.keyAt(i)));
-                sep = " ";
-            }
-        }
-        pw.println("]");
-
-        pw.println("Restricted packages:");
-        for (Pair<Integer, String> uidAndPackage
-                : mForceAppStandbyTracker.getRestrictedUidPackages()) {
-            pw.print("  ");
-            pw.print(UserHandle.formatUid(uidAndPackage.first));
-            pw.print(" ");
-            pw.print(uidAndPackage.second);
-            pw.println();
-        }
+        mForceAppStandbyTracker.dump(pw, "");
 
         pw.println("Job state:");
         mJobSchedulerService.getJobStore().forEachJob((jobStatus) -> {
@@ -165,15 +95,17 @@
             pw.print(" from ");
             UserHandle.formatUid(pw, uid);
             pw.print(mForceAppStandbyTracker.isInForeground(uid) ? " foreground" : " background");
-            if (isWhitelistedLocked(uid)) {
+            if (mForceAppStandbyTracker.isUidPowerSaveWhitelisted(uid) ||
+                    mForceAppStandbyTracker.isUidTempPowerSaveWhitelisted(uid)) {
                 pw.print(", whitelisted");
             }
             pw.print(": ");
             pw.print(jobStatus.getSourcePackageName());
 
-            pw.print(" [background restrictions ");
+            pw.print(" [RUN_ANY_IN_BACKGROUND ");
             pw.print(mForceAppStandbyTracker.isRunAnyInBackgroundAppOpsAllowed(
-                    jobStatus.getSourceUid(), jobStatus.getSourcePackageName()) ? "off]" : "on]");
+                    jobStatus.getSourceUid(), jobStatus.getSourcePackageName())
+                    ? "allowed]" : "disallowed]");
 
             if ((jobStatus.satisfiedConstraints
                     & JobStatus.CONSTRAINT_BACKGROUND_NOT_RESTRICTED) != 0) {
@@ -218,19 +150,12 @@
         }
     }
 
-    private boolean isWhitelistedLocked(int uid) {
-        final int appId = UserHandle.getAppId(uid);
-        return ArrayUtils.contains(mTempWhitelistedAppIds, appId)
-                || ArrayUtils.contains(mPowerWhitelistedUserAppIds, appId);
-    }
-
     boolean updateSingleJobRestrictionLocked(JobStatus jobStatus) {
 
         final int uid = jobStatus.getSourceUid();
         final String packageName = jobStatus.getSourcePackageName();
 
-        final boolean canRun = isWhitelistedLocked(uid)
-                || !mForceAppStandbyTracker.isRestricted(uid, packageName);
+        final boolean canRun = !mForceAppStandbyTracker.areJobsRestricted(uid, packageName);
 
         return jobStatus.setBackgroundNotRestrictedConstraintSatisfied(canRun);
     }
@@ -261,13 +186,18 @@
 
     private final Listener mForceAppStandbyListener = new Listener() {
         @Override
-        public void onRestrictionChanged(int uid, String packageName) {
+        public void updateAllJobs() {
+            updateAllJobRestrictionsLocked();
+        }
+
+        @Override
+        public void updateJobsForUid(int uid) {
             updateJobRestrictionsForUidLocked(uid);
         }
 
         @Override
-        public void onGlobalRestrictionChanged() {
-            updateAllJobRestrictionsLocked();
+        public void updateJobsForUidPackage(int uid, String packageName) {
+            updateJobRestrictionsForUidLocked(uid);
         }
     };
 }
diff --git a/services/core/java/com/android/server/power/BatterySaverPolicy.java b/services/core/java/com/android/server/power/BatterySaverPolicy.java
index 87c9274..7c234f9 100644
--- a/services/core/java/com/android/server/power/BatterySaverPolicy.java
+++ b/services/core/java/com/android/server/power/BatterySaverPolicy.java
@@ -69,6 +69,7 @@
     private static final String KEY_FULLBACKUP_DEFERRED = "fullbackup_deferred";
     private static final String KEY_KEYVALUE_DEFERRED = "keyvaluebackup_deferred";
     private static final String KEY_FORCE_ALL_APPS_STANDBY = "force_all_apps_standby";
+    private static final String KEY_FORCE_BACKGROUND_CHECK = "force_background_check";
     private static final String KEY_OPTIONAL_SENSORS_DISABLED = "optional_sensors_disabled";
 
     private static final String KEY_CPU_FREQ_INTERACTIVE = "cpufreq-i";
@@ -190,6 +191,12 @@
     private boolean mForceAllAppsStandby;
 
     /**
+     * Whether to put all apps in the stand-by mode.
+     */
+    @GuardedBy("mLock")
+    private boolean mForceBackgroundCheck;
+
+    /**
      * Weather to show non-essential sensors (e.g. edge sensors) or not.
      */
     @GuardedBy("mLock")
@@ -326,6 +333,7 @@
         mDataSaverDisabled = parser.getBoolean(KEY_DATASAVER_DISABLED, true);
         mLaunchBoostDisabled = parser.getBoolean(KEY_LAUNCH_BOOST_DISABLED, true);
         mForceAllAppsStandby = parser.getBoolean(KEY_FORCE_ALL_APPS_STANDBY, true);
+        mForceBackgroundCheck = parser.getBoolean(KEY_FORCE_BACKGROUND_CHECK, true);
         mOptionalSensorsDisabled = parser.getBoolean(KEY_OPTIONAL_SENSORS_DISABLED, true);
 
         // Get default value from Settings.Secure
@@ -395,6 +403,12 @@
                 case ServiceType.VIBRATION:
                     return builder.setBatterySaverEnabled(mVibrationDisabled)
                             .build();
+                case ServiceType.FORCE_ALL_APPS_STANDBY:
+                    return builder.setBatterySaverEnabled(mForceAllAppsStandby)
+                            .build();
+                case ServiceType.FORCE_BACKGROUND_CHECK:
+                    return builder.setBatterySaverEnabled(mForceBackgroundCheck)
+                            .build();
                 case ServiceType.OPTIONAL_SENSORS:
                     return builder.setBatterySaverEnabled(mOptionalSensorsDisabled)
                             .build();
@@ -438,6 +452,7 @@
             pw.println("  " + KEY_ADJUST_BRIGHTNESS_FACTOR + "=" + mAdjustBrightnessFactor);
             pw.println("  " + KEY_GPS_MODE + "=" + mGpsMode);
             pw.println("  " + KEY_FORCE_ALL_APPS_STANDBY + "=" + mForceAllAppsStandby);
+            pw.println("  " + KEY_FORCE_BACKGROUND_CHECK + "=" + mForceBackgroundCheck);
             pw.println("  " + KEY_OPTIONAL_SENSORS_DISABLED + "=" + mOptionalSensorsDisabled);
             pw.println();
 
diff --git a/services/core/java/com/android/server/wm/TaskSnapshotController.java b/services/core/java/com/android/server/wm/TaskSnapshotController.java
index 84e475a..43a0893 100644
--- a/services/core/java/com/android/server/wm/TaskSnapshotController.java
+++ b/services/core/java/com/android/server/wm/TaskSnapshotController.java
@@ -226,7 +226,7 @@
         final Rect taskFrame = new Rect();
         task.getBounds(taskFrame);
 
-        final GraphicBuffer buffer = SurfaceControl.captureLayersToBuffer(
+        final GraphicBuffer buffer = SurfaceControl.captureLayers(
                 task.getSurfaceControl().getHandle(), taskFrame, scaleFraction);
 
         if (buffer == null || buffer.getWidth() <= 1 || buffer.getHeight() <= 1) {
diff --git a/services/tests/servicestests/src/com/android/server/AlarmManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/AlarmManagerServiceTest.java
new file mode 100644
index 0000000..918807d
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/AlarmManagerServiceTest.java
@@ -0,0 +1,193 @@
+/*
+ * 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.server;
+
+import static android.app.AlarmManager.RTC;
+import static android.app.AlarmManager.RTC_WAKEUP;
+
+import static org.junit.Assert.assertEquals;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.SparseArray;
+
+import com.android.internal.util.ObjectUtils;
+import com.android.server.AlarmManagerService.Alarm;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class AlarmManagerServiceTest {
+    private SparseArray<ArrayList<Alarm>> addPendingAlarm(
+            SparseArray<ArrayList<Alarm>> all, int uid, String name, boolean removeIt) {
+        ArrayList<Alarm> uidAlarms = all.get(uid);
+        if (uidAlarms == null) {
+            all.put(uid, uidAlarms = new ArrayList<>());
+        }
+        // Details don't matter.
+        uidAlarms.add(new Alarm(
+                removeIt ? RTC : RTC_WAKEUP,
+                0, 0, 0, 0, 0, null, null, null, null, 0, null, uid, name));
+        return all;
+    }
+
+    private static String toString(SparseArray<ArrayList<Alarm>> pendingAlarms) {
+        final StringBuilder sb = new StringBuilder();
+
+        String sep = "";
+        for (int i = 0; i < pendingAlarms.size(); i++) {
+            sb.append(sep);
+            sep = ", ";
+            sb.append("[");
+            sb.append(pendingAlarms.keyAt(i));
+            sb.append(": ");
+            sb.append(toString(pendingAlarms.valueAt(i)));
+            sb.append("]");
+        }
+        return sb.toString();
+    }
+
+    private static String toString(ArrayList<Alarm> alarms) {
+        final StringBuilder sb = new StringBuilder();
+
+        alarms.sort((a, b) -> ObjectUtils.compare(a.packageName, b.packageName));
+
+        String sep = "";
+        for (Alarm a : alarms) {
+            sb.append(sep);
+            sep = ", ";
+            sb.append(a.packageName);
+        }
+        return sb.toString();
+    }
+
+    private void runCheckAllPendingAlarms(
+            SparseArray<ArrayList<Alarm>> pending, ArrayList<Alarm> alarmsToDeliver) {
+        // RTC_WAKEUP alarms are restricted.
+        AlarmManagerService.findAllUnrestrictedPendingBackgroundAlarmsLockedInner(pending,
+                alarmsToDeliver, alarm -> alarm.type == RTC_WAKEUP);
+    }
+
+    @Test
+    public void findAllUnrestrictedPendingBackgroundAlarmsLockedInner_empty() {
+        SparseArray<ArrayList<Alarm>> pending = new SparseArray<>();
+
+        final ArrayList<Alarm> alarmsToDeliver = new ArrayList<>();
+
+        runCheckAllPendingAlarms(pending, alarmsToDeliver);
+
+        assertEquals("", toString(pending));
+        assertEquals("", toString(alarmsToDeliver));
+    }
+
+    @Test
+    public void findAllUnrestrictedPendingBackgroundAlarmsLockedInner_single_remove() {
+        SparseArray<ArrayList<Alarm>> pending = new SparseArray<>();
+
+        addPendingAlarm(pending, 100001, "a1", false);
+
+        final ArrayList<Alarm> alarmsToDeliver = new ArrayList<>();
+
+        runCheckAllPendingAlarms(pending, alarmsToDeliver);
+
+        assertEquals("[100001: a1]", toString(pending));
+        assertEquals("", toString(alarmsToDeliver));
+    }
+
+    @Test
+    public void findAllUnrestrictedPendingBackgroundAlarmsLockedInner_single_nonremove() {
+        SparseArray<ArrayList<Alarm>> pending = new SparseArray<>();
+
+        addPendingAlarm(pending, 100001, "a1", true);
+
+        final ArrayList<Alarm> alarmsToDeliver = new ArrayList<>();
+        runCheckAllPendingAlarms(pending, alarmsToDeliver);
+
+
+        assertEquals("", toString(pending));
+        assertEquals("a1", toString(alarmsToDeliver));
+    }
+
+    @Test
+    public void findAllUnrestrictedPendingBackgroundAlarmsLockedInner_complex() {
+        SparseArray<ArrayList<Alarm>> pending = new SparseArray<>();
+
+        addPendingAlarm(pending, 100001, "a11", false);
+        addPendingAlarm(pending, 100001, "a12", true);
+        addPendingAlarm(pending, 100001, "a13", false);
+        addPendingAlarm(pending, 100001, "a14", true);
+
+        addPendingAlarm(pending, 100002, "a21", false);
+
+        addPendingAlarm(pending, 100003, "a31", true);
+
+        addPendingAlarm(pending, 100004, "a41", false);
+        addPendingAlarm(pending, 100004, "a42", false);
+
+        addPendingAlarm(pending, 100005, "a51", true);
+        addPendingAlarm(pending, 100005, "a52", true);
+
+        addPendingAlarm(pending, 100006, "a61", true);
+        addPendingAlarm(pending, 100006, "a62", false);
+        addPendingAlarm(pending, 100006, "a63", true);
+        addPendingAlarm(pending, 100006, "a64", false);
+
+        final ArrayList<Alarm> alarmsToDeliver = new ArrayList<>();
+        runCheckAllPendingAlarms(pending, alarmsToDeliver);
+
+
+        assertEquals("[100001: a11, a13], [100002: a21], [100004: a41, a42], [100006: a62, a64]",
+                toString(pending));
+        assertEquals("a12, a14, a31, a51, a52, a61, a63", toString(alarmsToDeliver));
+    }
+
+    @Test
+    public void findAllUnrestrictedPendingBackgroundAlarmsLockedInner_complex_allRemove() {
+        SparseArray<ArrayList<Alarm>> pending = new SparseArray<>();
+
+        addPendingAlarm(pending, 100001, "a11", true);
+        addPendingAlarm(pending, 100001, "a12", true);
+        addPendingAlarm(pending, 100001, "a13", true);
+        addPendingAlarm(pending, 100001, "a14", true);
+
+        addPendingAlarm(pending, 100002, "a21", true);
+
+        addPendingAlarm(pending, 100003, "a31", true);
+
+        addPendingAlarm(pending, 100004, "a41", true);
+        addPendingAlarm(pending, 100004, "a42", true);
+
+        addPendingAlarm(pending, 100005, "a51", true);
+        addPendingAlarm(pending, 100005, "a52", true);
+
+        addPendingAlarm(pending, 100006, "a61", true);
+        addPendingAlarm(pending, 100006, "a62", true);
+        addPendingAlarm(pending, 100006, "a63", true);
+        addPendingAlarm(pending, 100006, "a64", true);
+
+        final ArrayList<Alarm> alarmsToDeliver = new ArrayList<>();
+        runCheckAllPendingAlarms(pending, alarmsToDeliver);
+
+
+        assertEquals("", toString(pending));
+        assertEquals("a11, a12, a13, a14, a21, a31, a41, a42, a51, a52, a61, a62, a63, a64",
+                toString(alarmsToDeliver));
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/ForceAppStandbyTrackerTest.java b/services/tests/servicestests/src/com/android/server/ForceAppStandbyTrackerTest.java
new file mode 100644
index 0000000..66d0da1
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/ForceAppStandbyTrackerTest.java
@@ -0,0 +1,900 @@
+/*
+ * 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.server;
+
+import static com.android.server.ForceAppStandbyTracker.TARGET_OP;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManager;
+import android.app.AppOpsManager;
+import android.app.AppOpsManager.OpEntry;
+import android.app.AppOpsManager.PackageOps;
+import android.app.IActivityManager;
+import android.app.IUidObserver;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.PowerManager.ServiceType;
+import android.os.PowerManagerInternal;
+import android.os.PowerSaveState;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.ArraySet;
+import android.util.Pair;
+
+import com.android.internal.app.IAppOpsCallback;
+import com.android.internal.app.IAppOpsService;
+import com.android.server.ForceAppStandbyTracker.Listener;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ForceAppStandbyTrackerTest {
+
+    private class ForceAppStandbyTrackerTestable extends ForceAppStandbyTracker {
+        ForceAppStandbyTrackerTestable() {
+            super(mMockContext, Looper.getMainLooper());
+        }
+
+        @Override
+        AppOpsManager injectAppOpsManager() {
+            return mMockAppOpsManager;
+        }
+
+        @Override
+        IAppOpsService injectIAppOpsService() {
+            return mMockIAppOpsService;
+        }
+
+        @Override
+        IActivityManager injectIActivityManager() {
+            return mMockIActivityManager;
+        }
+
+        @Override
+        PowerManagerInternal injectPowerManagerInternal() {
+            return mMockPowerManagerInternal;
+        }
+    }
+
+    private static final int UID_1 = Process.FIRST_APPLICATION_UID + 1;
+    private static final int UID_2 = Process.FIRST_APPLICATION_UID + 2;
+    private static final int UID_3 = Process.FIRST_APPLICATION_UID + 3;
+    private static final int UID_10_1 = UserHandle.getUid(10, UID_1);
+    private static final int UID_10_2 = UserHandle.getUid(10, UID_2);
+    private static final int UID_10_3 = UserHandle.getUid(10, UID_3);
+    private static final String PACKAGE_1 = "package1";
+    private static final String PACKAGE_2 = "package2";
+    private static final String PACKAGE_3 = "package3";
+    private static final String PACKAGE_SYSTEM = "android";
+
+    private Handler mMainHandler;
+
+    @Mock
+    private Context mMockContext;
+
+    @Mock
+    private IActivityManager mMockIActivityManager;
+
+    @Mock
+    private AppOpsManager mMockAppOpsManager;
+
+    @Mock
+    private IAppOpsService mMockIAppOpsService;
+
+    @Mock
+    private PowerManagerInternal mMockPowerManagerInternal;
+
+    private IUidObserver mIUidObserver;
+    private IAppOpsCallback.Stub mAppOpsCallback;
+    private Consumer<PowerSaveState> mPowerSaveObserver;
+    private BroadcastReceiver mReceiver;
+
+    private boolean mPowerSaveMode;
+
+    private final ArraySet<Pair<Integer, String>> mRestrictedPackages = new ArraySet();
+
+    @Before
+    public void setUp() {
+        mMainHandler = new Handler(Looper.getMainLooper());
+    }
+
+    private void waitUntilMainHandlerDrain() throws Exception {
+        final CountDownLatch l = new CountDownLatch(1);
+        mMainHandler.post(() -> {
+            l.countDown();
+        });
+        assertTrue(l.await(5, TimeUnit.SECONDS));
+    }
+
+    private PowerSaveState getPowerSaveState() {
+        return new PowerSaveState.Builder().setBatterySaverEnabled(mPowerSaveMode).build();
+    }
+
+    private ForceAppStandbyTrackerTestable newInstance() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        when(mMockIAppOpsService.checkOperation(eq(TARGET_OP), anyInt(), anyString()))
+                .thenAnswer(inv -> {
+                    return mRestrictedPackages.indexOf(
+                            Pair.create(inv.getArgument(1), inv.getArgument(2))) >= 0 ?
+                            AppOpsManager.MODE_IGNORED : AppOpsManager.MODE_ALLOWED;
+                });
+
+        final ForceAppStandbyTrackerTestable instance = new ForceAppStandbyTrackerTestable();
+
+        return instance;
+    }
+
+    private void callStart(ForceAppStandbyTrackerTestable instance) throws RemoteException {
+
+        // Set up functions that start() calls.
+        when(mMockPowerManagerInternal.getLowPowerState(eq(ServiceType.FORCE_ALL_APPS_STANDBY)))
+                .thenAnswer(inv -> getPowerSaveState());
+        when(mMockAppOpsManager.getPackagesForOps(
+                any(int[].class)
+                )).thenAnswer(inv -> new ArrayList<AppOpsManager.PackageOps>());
+
+        // Call start.
+        instance.start();
+
+        // Capture the listeners.
+        ArgumentCaptor<IUidObserver> uidObserverArgumentCaptor =
+                ArgumentCaptor.forClass(IUidObserver.class);
+        ArgumentCaptor<IAppOpsCallback.Stub> appOpsCallbackCaptor =
+                ArgumentCaptor.forClass(IAppOpsCallback.Stub.class);
+        ArgumentCaptor<Consumer<PowerSaveState>> powerSaveObserverCaptor =
+                ArgumentCaptor.forClass(Consumer.class);
+        ArgumentCaptor<BroadcastReceiver> receiverCaptor =
+                ArgumentCaptor.forClass(BroadcastReceiver.class);
+
+        verify(mMockIActivityManager).registerUidObserver(
+                uidObserverArgumentCaptor.capture(),
+                eq(ActivityManager.UID_OBSERVER_GONE | ActivityManager.UID_OBSERVER_IDLE
+                        | ActivityManager.UID_OBSERVER_ACTIVE),
+                eq(ActivityManager.PROCESS_STATE_UNKNOWN),
+                isNull());
+        verify(mMockIAppOpsService).startWatchingMode(
+                eq(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND),
+                isNull(),
+                appOpsCallbackCaptor.capture());
+        verify(mMockPowerManagerInternal).registerLowPowerModeObserver(
+                eq(ServiceType.FORCE_ALL_APPS_STANDBY),
+                powerSaveObserverCaptor.capture());
+
+        verify(mMockContext).registerReceiver(
+                receiverCaptor.capture(), any(IntentFilter.class));
+
+        mIUidObserver = uidObserverArgumentCaptor.getValue();
+        mAppOpsCallback = appOpsCallbackCaptor.getValue();
+        mPowerSaveObserver = powerSaveObserverCaptor.getValue();
+        mReceiver = receiverCaptor.getValue();
+
+        assertNotNull(mIUidObserver);
+        assertNotNull(mAppOpsCallback);
+        assertNotNull(mPowerSaveObserver);
+        assertNotNull(mReceiver);
+    }
+
+    private void setAppOps(int uid, String packageName, boolean restrict) throws RemoteException {
+        final Pair p = Pair.create(uid, packageName);
+        if (restrict) {
+            mRestrictedPackages.add(p);
+        } else {
+            mRestrictedPackages.remove(p);
+        }
+        if (mAppOpsCallback != null) {
+            mAppOpsCallback.opChanged(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, uid, packageName);
+        }
+    }
+
+    private static final int NONE = 0;
+    private static final int ALARMS_ONLY = 1 << 0;
+    private static final int JOBS_ONLY = 1 << 1;
+    private static final int JOBS_AND_ALARMS = ALARMS_ONLY | JOBS_ONLY;
+
+    private void areRestricted(ForceAppStandbyTrackerTestable instance, int uid, String packageName,
+            int restrictionTypes) {
+        assertEquals(((restrictionTypes & JOBS_ONLY) != 0),
+                instance.areJobsRestricted(uid, packageName));
+        assertEquals(((restrictionTypes & ALARMS_ONLY) != 0),
+                instance.areAlarmsRestricted(uid, packageName));
+    }
+
+    @Test
+    public void testAll() throws Exception {
+        final ForceAppStandbyTrackerTestable instance = newInstance();
+        callStart(instance);
+
+        assertFalse(instance.isForceAllAppsStandbyEnabled());
+        areRestricted(instance, UID_1, PACKAGE_1, NONE);
+        areRestricted(instance, UID_2, PACKAGE_2, NONE);
+        areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+
+        mPowerSaveMode = true;
+        mPowerSaveObserver.accept(getPowerSaveState());
+
+        assertTrue(instance.isForceAllAppsStandbyEnabled());
+
+        areRestricted(instance, UID_1, PACKAGE_1, JOBS_AND_ALARMS);
+        areRestricted(instance, UID_2, PACKAGE_2, JOBS_AND_ALARMS);
+        areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+
+        // Toggle the foreground state.
+        mPowerSaveMode = true;
+        mPowerSaveObserver.accept(getPowerSaveState());
+
+        assertFalse(instance.isInForeground(UID_1));
+        assertFalse(instance.isInForeground(UID_2));
+        assertTrue(instance.isInForeground(Process.SYSTEM_UID));
+
+        mIUidObserver.onUidActive(UID_1);
+        areRestricted(instance, UID_1, PACKAGE_1, NONE);
+        areRestricted(instance, UID_2, PACKAGE_2, JOBS_AND_ALARMS);
+        areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+        assertTrue(instance.isInForeground(UID_1));
+        assertFalse(instance.isInForeground(UID_2));
+
+        mIUidObserver.onUidGone(UID_1, /*disable=*/ false);
+        areRestricted(instance, UID_1, PACKAGE_1, JOBS_AND_ALARMS);
+        areRestricted(instance, UID_2, PACKAGE_2, JOBS_AND_ALARMS);
+        areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+        assertFalse(instance.isInForeground(UID_1));
+        assertFalse(instance.isInForeground(UID_2));
+
+        mIUidObserver.onUidActive(UID_1);
+        areRestricted(instance, UID_1, PACKAGE_1, NONE);
+        areRestricted(instance, UID_2, PACKAGE_2, JOBS_AND_ALARMS);
+        areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+
+        mIUidObserver.onUidIdle(UID_1, /*disable=*/ false);
+        areRestricted(instance, UID_1, PACKAGE_1, JOBS_AND_ALARMS);
+        areRestricted(instance, UID_2, PACKAGE_2, JOBS_AND_ALARMS);
+        areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+        assertFalse(instance.isInForeground(UID_1));
+        assertFalse(instance.isInForeground(UID_2));
+
+        // Toggle the app ops.
+        mPowerSaveMode = false;
+        mPowerSaveObserver.accept(getPowerSaveState());
+
+        assertTrue(instance.isRunAnyInBackgroundAppOpsAllowed(UID_1, PACKAGE_1));
+        assertTrue(instance.isRunAnyInBackgroundAppOpsAllowed(UID_10_1, PACKAGE_1));
+        assertTrue(instance.isRunAnyInBackgroundAppOpsAllowed(UID_2, PACKAGE_2));
+        assertTrue(instance.isRunAnyInBackgroundAppOpsAllowed(UID_10_2, PACKAGE_2));
+
+        areRestricted(instance, UID_1, PACKAGE_1, NONE);
+        areRestricted(instance, UID_10_1, PACKAGE_1, NONE);
+        areRestricted(instance, UID_2, PACKAGE_2, NONE);
+        areRestricted(instance, UID_10_2, PACKAGE_2, NONE);
+        areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+
+        setAppOps(UID_1, PACKAGE_1, true);
+        setAppOps(UID_10_2, PACKAGE_2, true);
+        assertFalse(instance.isRunAnyInBackgroundAppOpsAllowed(UID_1, PACKAGE_1));
+        assertTrue(instance.isRunAnyInBackgroundAppOpsAllowed(UID_10_1, PACKAGE_1));
+        assertTrue(instance.isRunAnyInBackgroundAppOpsAllowed(UID_2, PACKAGE_2));
+        assertFalse(instance.isRunAnyInBackgroundAppOpsAllowed(UID_10_2, PACKAGE_2));
+
+        areRestricted(instance, UID_1, PACKAGE_1, JOBS_AND_ALARMS);
+        areRestricted(instance, UID_10_1, PACKAGE_1, NONE);
+        areRestricted(instance, UID_2, PACKAGE_2, NONE);
+        areRestricted(instance, UID_10_2, PACKAGE_2, JOBS_AND_ALARMS);
+        areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+
+        // Toggle power saver, should still be the same.
+        mPowerSaveMode = true;
+        mPowerSaveObserver.accept(getPowerSaveState());
+
+        mPowerSaveMode = false;
+        mPowerSaveObserver.accept(getPowerSaveState());
+
+        areRestricted(instance, UID_1, PACKAGE_1, JOBS_AND_ALARMS);
+        areRestricted(instance, UID_10_1, PACKAGE_1, NONE);
+        areRestricted(instance, UID_2, PACKAGE_2, NONE);
+        areRestricted(instance, UID_10_2, PACKAGE_2, JOBS_AND_ALARMS);
+        areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+
+        // Clear the app ops and update the whitelist.
+        setAppOps(UID_1, PACKAGE_1, false);
+        setAppOps(UID_10_2, PACKAGE_2, false);
+
+        mPowerSaveMode = true;
+        mPowerSaveObserver.accept(getPowerSaveState());
+
+        areRestricted(instance, UID_1, PACKAGE_1, JOBS_AND_ALARMS);
+        areRestricted(instance, UID_10_1, PACKAGE_1, JOBS_AND_ALARMS);
+        areRestricted(instance, UID_2, PACKAGE_2, JOBS_AND_ALARMS);
+        areRestricted(instance, UID_10_2, PACKAGE_2, JOBS_AND_ALARMS);
+        areRestricted(instance, UID_3, PACKAGE_3, JOBS_AND_ALARMS);
+        areRestricted(instance, UID_10_3, PACKAGE_3, JOBS_AND_ALARMS);
+        areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+
+        instance.setPowerSaveWhitelistAppIds(new int[] {UID_1}, new int[] {UID_2});
+
+        areRestricted(instance, UID_1, PACKAGE_1, NONE);
+        areRestricted(instance, UID_10_1, PACKAGE_1, NONE);
+        areRestricted(instance, UID_2, PACKAGE_2, ALARMS_ONLY);
+        areRestricted(instance, UID_10_2, PACKAGE_2, ALARMS_ONLY);
+        areRestricted(instance, UID_3, PACKAGE_3, JOBS_AND_ALARMS);
+        areRestricted(instance, UID_10_3, PACKAGE_3, JOBS_AND_ALARMS);
+        areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+
+        // Again, make sure toggling the global state doesn't change it.
+        mPowerSaveMode = false;
+        mPowerSaveObserver.accept(getPowerSaveState());
+
+        mPowerSaveMode = true;
+        mPowerSaveObserver.accept(getPowerSaveState());
+
+        areRestricted(instance, UID_1, PACKAGE_1, NONE);
+        areRestricted(instance, UID_10_1, PACKAGE_1, NONE);
+        areRestricted(instance, UID_2, PACKAGE_2, ALARMS_ONLY);
+        areRestricted(instance, UID_10_2, PACKAGE_2, ALARMS_ONLY);
+        areRestricted(instance, UID_3, PACKAGE_3, JOBS_AND_ALARMS);
+        areRestricted(instance, UID_10_3, PACKAGE_3, JOBS_AND_ALARMS);
+        areRestricted(instance, Process.SYSTEM_UID, PACKAGE_SYSTEM, NONE);
+
+        assertTrue(instance.isUidPowerSaveWhitelisted(UID_1));
+        assertTrue(instance.isUidPowerSaveWhitelisted(UID_10_1));
+        assertFalse(instance.isUidPowerSaveWhitelisted(UID_2));
+        assertFalse(instance.isUidPowerSaveWhitelisted(UID_10_2));
+
+        assertFalse(instance.isUidTempPowerSaveWhitelisted(UID_1));
+        assertFalse(instance.isUidTempPowerSaveWhitelisted(UID_10_1));
+        assertTrue(instance.isUidTempPowerSaveWhitelisted(UID_2));
+        assertTrue(instance.isUidTempPowerSaveWhitelisted(UID_10_2));
+    }
+
+    public void loadPersistedAppOps() throws Exception {
+        final ForceAppStandbyTrackerTestable instance = newInstance();
+
+        final List<PackageOps> ops = new ArrayList<>();
+
+        //--------------------------------------------------
+        List<OpEntry> entries = new ArrayList<>();
+        entries.add(new AppOpsManager.OpEntry(
+                AppOpsManager.OP_ACCESS_NOTIFICATIONS,
+                AppOpsManager.MODE_IGNORED, 0, 0, 0, 0, null));
+        entries.add(new AppOpsManager.OpEntry(
+                ForceAppStandbyTracker.TARGET_OP,
+                AppOpsManager.MODE_IGNORED, 0, 0, 0, 0, null));
+
+        ops.add(new PackageOps(PACKAGE_1, UID_1, entries));
+
+        //--------------------------------------------------
+        entries = new ArrayList<>();
+        entries.add(new AppOpsManager.OpEntry(
+                ForceAppStandbyTracker.TARGET_OP,
+                AppOpsManager.MODE_IGNORED, 0, 0, 0, 0, null));
+
+        ops.add(new PackageOps(PACKAGE_2, UID_2, entries));
+
+        //--------------------------------------------------
+        entries = new ArrayList<>();
+        entries.add(new AppOpsManager.OpEntry(
+                ForceAppStandbyTracker.TARGET_OP,
+                AppOpsManager.MODE_ALLOWED, 0, 0, 0, 0, null));
+
+        ops.add(new PackageOps(PACKAGE_1, UID_10_1, entries));
+
+        //--------------------------------------------------
+        entries = new ArrayList<>();
+        entries.add(new AppOpsManager.OpEntry(
+                ForceAppStandbyTracker.TARGET_OP,
+                AppOpsManager.MODE_IGNORED, 0, 0, 0, 0, null));
+        entries.add(new AppOpsManager.OpEntry(
+                AppOpsManager.OP_ACCESS_NOTIFICATIONS,
+                AppOpsManager.MODE_IGNORED, 0, 0, 0, 0, null));
+
+        ops.add(new PackageOps(PACKAGE_3, UID_10_3, entries));
+
+        callStart(instance);
+
+        assertFalse(instance.isRunAnyInBackgroundAppOpsAllowed(UID_1, PACKAGE_1));
+        assertFalse(instance.isRunAnyInBackgroundAppOpsAllowed(UID_2, PACKAGE_2));
+        assertTrue(instance.isRunAnyInBackgroundAppOpsAllowed(UID_3, PACKAGE_3));
+
+        assertTrue(instance.isRunAnyInBackgroundAppOpsAllowed(UID_10_1, PACKAGE_1));
+        assertTrue(instance.isRunAnyInBackgroundAppOpsAllowed(UID_10_2, PACKAGE_2));
+        assertFalse(instance.isRunAnyInBackgroundAppOpsAllowed(UID_10_3, PACKAGE_3));
+    }
+
+    private void assertNoCallbacks(Listener l) throws Exception {
+        waitUntilMainHandlerDrain();
+        verify(l, times(0)).updateAllJobs();
+        verify(l, times(0)).updateJobsForUid(anyInt());
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+    }
+
+    @Test
+    public void testPowerSaveListener() throws Exception {
+        final ForceAppStandbyTrackerTestable instance = newInstance();
+        callStart(instance);
+
+        ForceAppStandbyTracker.Listener l = mock(ForceAppStandbyTracker.Listener.class);
+        instance.addListener(l);
+
+        // Power save on.
+        mPowerSaveMode = true;
+        mPowerSaveObserver.accept(getPowerSaveState());
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(1)).updateAllJobs();
+        verify(l, times(0)).updateJobsForUid(anyInt());
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+        // Power save off.
+        mPowerSaveMode = false;
+        mPowerSaveObserver.accept(getPowerSaveState());
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(1)).updateAllJobs();
+        verify(l, times(0)).updateJobsForUid(anyInt());
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(1)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+        // Power save on.
+        mPowerSaveMode = true;
+        mPowerSaveObserver.accept(getPowerSaveState());
+
+        assertNoCallbacks(l);
+    }
+
+    @Test
+    public void testAllListeners() throws Exception {
+        final ForceAppStandbyTrackerTestable instance = newInstance();
+        callStart(instance);
+
+        ForceAppStandbyTracker.Listener l = mock(ForceAppStandbyTracker.Listener.class);
+        instance.addListener(l);
+
+        // -------------------------------------------------------------------------
+        // Test with apppops.
+
+        setAppOps(UID_10_2, PACKAGE_2, true);
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(0)).updateAllJobs();
+        verify(l, times(0)).updateJobsForUid(anyInt());
+        verify(l, times(1)).updateJobsForUidPackage(eq(UID_10_2), eq(PACKAGE_2));
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+        setAppOps(UID_10_2, PACKAGE_2, false);
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(0)).updateAllJobs();
+        verify(l, times(0)).updateJobsForUid(anyInt());
+        verify(l, times(1)).updateJobsForUidPackage(eq(UID_10_2), eq(PACKAGE_2));
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(eq(UID_10_2), eq(PACKAGE_2));
+        reset(l);
+
+        setAppOps(UID_10_2, PACKAGE_2, false);
+
+        verify(l, times(0)).updateAllJobs();
+        verify(l, times(0)).updateJobsForUid(anyInt());
+        verify(l, times(1)).updateJobsForUidPackage(eq(UID_10_2), eq(PACKAGE_2));
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+
+        // Unrestrict while battery saver is on. Shouldn't fire.
+        mPowerSaveMode = true;
+        mPowerSaveObserver.accept(getPowerSaveState());
+
+        // Note toggling appops while BS is on will suppress unblockAlarmsForUidPackage().
+        setAppOps(UID_10_2, PACKAGE_2, true);
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(1)).updateAllJobs();
+        verify(l, times(0)).updateJobsForUid(anyInt());
+        verify(l, times(1)).updateJobsForUidPackage(eq(UID_10_2), eq(PACKAGE_2));
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+        // Battery saver off.
+        mPowerSaveMode = false;
+        mPowerSaveObserver.accept(getPowerSaveState());
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(1)).updateAllJobs();
+        verify(l, times(0)).updateJobsForUid(anyInt());
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(1)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+        // -------------------------------------------------------------------------
+        // Tests with system/user/temp whitelist.
+
+        instance.setPowerSaveWhitelistAppIds(new int[] {UID_1, UID_2}, new int[] {});
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(1)).updateAllJobs();
+        verify(l, times(0)).updateJobsForUid(anyInt());
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+        instance.setPowerSaveWhitelistAppIds(new int[] {UID_2}, new int[] {});
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(1)).updateAllJobs();
+        verify(l, times(0)).updateJobsForUid(anyInt());
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(1)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+        // Update temp whitelist.
+        instance.setPowerSaveWhitelistAppIds(new int[] {UID_2}, new int[] {UID_1, UID_3});
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(1)).updateAllJobs();
+        verify(l, times(0)).updateJobsForUid(anyInt());
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+        instance.setPowerSaveWhitelistAppIds(new int[] {UID_2}, new int[] {UID_3});
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(1)).updateAllJobs();
+        verify(l, times(0)).updateJobsForUid(anyInt());
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+        // Do the same thing with battery saver on. (Currently same callbacks are called.)
+        mPowerSaveMode = true;
+        mPowerSaveObserver.accept(getPowerSaveState());
+
+        instance.setPowerSaveWhitelistAppIds(new int[] {UID_1, UID_2}, new int[] {});
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(1)).updateAllJobs();
+        verify(l, times(0)).updateJobsForUid(anyInt());
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+        instance.setPowerSaveWhitelistAppIds(new int[] {UID_2}, new int[] {});
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(1)).updateAllJobs();
+        verify(l, times(0)).updateJobsForUid(anyInt());
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(1)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+        // Update temp whitelist.
+        instance.setPowerSaveWhitelistAppIds(new int[] {UID_2}, new int[] {UID_1, UID_3});
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(1)).updateAllJobs();
+        verify(l, times(0)).updateJobsForUid(anyInt());
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+        instance.setPowerSaveWhitelistAppIds(new int[] {UID_2}, new int[] {UID_3});
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(1)).updateAllJobs();
+        verify(l, times(0)).updateJobsForUid(anyInt());
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+
+        // -------------------------------------------------------------------------
+        // Tests with proc state changes.
+
+        // With battery save.
+        mPowerSaveMode = true;
+        mPowerSaveObserver.accept(getPowerSaveState());
+
+        mIUidObserver.onUidActive(UID_10_1);
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(0)).updateAllJobs();
+        verify(l, times(1)).updateJobsForUid(eq(UID_10_1));
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+        mIUidObserver.onUidGone(UID_10_1, true);
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(0)).updateAllJobs();
+        verify(l, times(1)).updateJobsForUid(eq(UID_10_1));
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+        mIUidObserver.onUidActive(UID_10_1);
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(0)).updateAllJobs();
+        verify(l, times(1)).updateJobsForUid(eq(UID_10_1));
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+        mIUidObserver.onUidIdle(UID_10_1, true);
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(0)).updateAllJobs();
+        verify(l, times(1)).updateJobsForUid(eq(UID_10_1));
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+        // Without battery save.
+        mPowerSaveMode = false;
+        mPowerSaveObserver.accept(getPowerSaveState());
+
+        mIUidObserver.onUidActive(UID_10_1);
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(0)).updateAllJobs();
+        verify(l, times(1)).updateJobsForUid(eq(UID_10_1));
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+        mIUidObserver.onUidGone(UID_10_1, true);
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(0)).updateAllJobs();
+        verify(l, times(1)).updateJobsForUid(eq(UID_10_1));
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+        mIUidObserver.onUidActive(UID_10_1);
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(0)).updateAllJobs();
+        verify(l, times(1)).updateJobsForUid(eq(UID_10_1));
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+
+        mIUidObserver.onUidIdle(UID_10_1, true);
+
+        waitUntilMainHandlerDrain();
+        verify(l, times(0)).updateAllJobs();
+        verify(l, times(1)).updateJobsForUid(eq(UID_10_1));
+        verify(l, times(0)).updateJobsForUidPackage(anyInt(), anyString());
+
+        verify(l, times(0)).unblockAllUnrestrictedAlarms();
+        verify(l, times(0)).unblockAlarmsForUid(anyInt());
+        verify(l, times(0)).unblockAlarmsForUidPackage(anyInt(), anyString());
+        reset(l);
+    }
+
+    @Test
+    public void testUserRemoved() throws Exception {
+        final ForceAppStandbyTrackerTestable instance = newInstance();
+        callStart(instance);
+
+        mIUidObserver.onUidActive(UID_1);
+        mIUidObserver.onUidActive(UID_10_1);
+
+        setAppOps(UID_2, PACKAGE_2, true);
+        setAppOps(UID_10_2, PACKAGE_2, true);
+
+        assertTrue(instance.isInForeground(UID_1));
+        assertTrue(instance.isInForeground(UID_10_1));
+
+        assertFalse(instance.isRunAnyInBackgroundAppOpsAllowed(UID_2, PACKAGE_2));
+        assertFalse(instance.isRunAnyInBackgroundAppOpsAllowed(UID_10_2, PACKAGE_2));
+
+        final Intent intent = new Intent(Intent.ACTION_USER_REMOVED);
+        intent.putExtra(Intent.EXTRA_USER_HANDLE, 10);
+        mReceiver.onReceive(mMockContext, intent);
+
+        waitUntilMainHandlerDrain();
+
+        assertTrue(instance.isInForeground(UID_1));
+        assertFalse(instance.isInForeground(UID_10_1));
+
+        assertFalse(instance.isRunAnyInBackgroundAppOpsAllowed(UID_2, PACKAGE_2));
+        assertTrue(instance.isRunAnyInBackgroundAppOpsAllowed(UID_10_2, PACKAGE_2));
+    }
+
+    static int[] array(int... appIds) {
+        Arrays.sort(appIds);
+        return appIds;
+    }
+
+    private final Random mRandom = new Random();
+
+    int[] makeRandomArray() {
+        final ArrayList<Integer> list = new ArrayList<>();
+        for (int i = 0; i < 5; i++) {
+            if (mRandom.nextDouble() < 0.5) {
+                list.add(i);
+            }
+        }
+        return Arrays.stream(list.toArray(new Integer[list.size()]))
+                .mapToInt(Integer::intValue).toArray();
+    }
+
+    static boolean isAnyAppIdUnwhitelistedSlow(int[] prevArray, int[] newArray) {
+        Arrays.sort(newArray); // Just in case...
+        for (int p : prevArray) {
+            if (Arrays.binarySearch(newArray, p) < 0) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void checkAnyAppIdUnwhitelisted(int[] prevArray, int[] newArray, boolean expected) {
+        assertEquals("Input: " + Arrays.toString(prevArray) + " " + Arrays.toString(newArray),
+                expected, ForceAppStandbyTracker.isAnyAppIdUnwhitelisted(prevArray, newArray));
+
+        // Also test isAnyAppIdUnwhitelistedSlow.
+        assertEquals("Input: " + Arrays.toString(prevArray) + " " + Arrays.toString(newArray),
+                expected, isAnyAppIdUnwhitelistedSlow(prevArray, newArray));
+    }
+
+    @Test
+    public void isAnyAppIdUnwhitelisted() {
+        checkAnyAppIdUnwhitelisted(array(), array(), false);
+
+        checkAnyAppIdUnwhitelisted(array(1), array(), true);
+        checkAnyAppIdUnwhitelisted(array(1), array(1), false);
+        checkAnyAppIdUnwhitelisted(array(1), array(0, 1), false);
+        checkAnyAppIdUnwhitelisted(array(1), array(0, 1, 2), false);
+        checkAnyAppIdUnwhitelisted(array(1), array(0, 1, 2), false);
+
+        checkAnyAppIdUnwhitelisted(array(1, 2, 10), array(), true);
+        checkAnyAppIdUnwhitelisted(array(1, 2, 10), array(1, 2), true);
+        checkAnyAppIdUnwhitelisted(array(1, 2, 10), array(1, 2, 10), false);
+        checkAnyAppIdUnwhitelisted(array(1, 2, 10), array(2, 10), true);
+        checkAnyAppIdUnwhitelisted(array(1, 2, 10), array(0, 1, 2, 4, 3, 10), false);
+        checkAnyAppIdUnwhitelisted(array(1, 2, 10), array(0, 0, 1, 2, 10), false);
+
+        // Random test
+        int trueCount = 0;
+        final int count = 10000;
+        for (int i = 0; i < count; i++) {
+            final int[] array1 = makeRandomArray();
+            final int[] array2 = makeRandomArray();
+
+            final boolean expected = isAnyAppIdUnwhitelistedSlow(array1, array2);
+            final boolean actual = ForceAppStandbyTracker.isAnyAppIdUnwhitelisted(array1, array2);
+
+            assertEquals("Input: " + Arrays.toString(array1) + " " + Arrays.toString(array2),
+                    expected, actual);
+            if (expected) {
+                trueCount++;
+            }
+        }
+
+        // Make sure makeRandomArray() didn't generate all same arrays by accident.
+        assertTrue(trueCount > 0);
+        assertTrue(trueCount < count);
+    }
+}