Merge "Clear WIFI namspace on primary user keystore reset." into sc-dev
diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java
index ab610e4..d962fa3 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -48,6 +48,23 @@
  */
 public abstract class ActivityManagerInternal {
 
+    public enum ServiceNotificationPolicy {
+        /**
+         * The Notification is not associated with any foreground service.
+         */
+        NOT_FOREGROUND_SERVICE,
+        /**
+         * The Notification is associated with a foreground service, but the
+         * notification system should handle it just like non-FGS notifications.
+         */
+        SHOW_IMMEDIATELY,
+        /**
+         * The Notification is associated with a foreground service, and the
+         * notification system should ignore it unless it has already been shown (in
+         * which case it should be used to update the currently displayed UI).
+         */
+        UPDATE_ONLY
+    }
 
     // Access modes for handleIncomingUser.
     public static final int ALLOW_NON_FULL = 0;
@@ -458,6 +475,24 @@
             String channelId);
 
     /**
+     * Tell the service lifecycle logic that the given Notification content is now
+     * canonical for any foreground-service visibility policy purposes.
+     *
+     * Returns a description of any FGs-related policy around the given Notification:
+     * not associated with an FGS; ensure display; or only update if already displayed.
+     */
+    public abstract ServiceNotificationPolicy applyForegroundServiceNotification(
+            Notification notification, int id, String pkg, @UserIdInt int userId);
+
+    /**
+     * Callback from the notification subsystem that the given FGS notification has
+     * been shown or updated.  This can happen after either Service.startForeground()
+     * or NotificationManager.notify().
+     */
+    public abstract void onForegroundServiceNotificationUpdate(Notification notification,
+            int id, String pkg, @UserIdInt int userId);
+
+    /**
      * If the given app has any FGSs whose notifications are in the given channel,
      * stop them.
      */
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 432d99d..60fe2ef 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -450,10 +450,12 @@
         STANDARD_LAYOUTS.add(R.layout.notification_template_material_big_text);
         STANDARD_LAYOUTS.add(R.layout.notification_template_material_inbox);
         STANDARD_LAYOUTS.add(R.layout.notification_template_material_messaging);
+        STANDARD_LAYOUTS.add(R.layout.notification_template_material_big_messaging);
         STANDARD_LAYOUTS.add(R.layout.notification_template_material_conversation);
         STANDARD_LAYOUTS.add(R.layout.notification_template_material_media);
         STANDARD_LAYOUTS.add(R.layout.notification_template_material_big_media);
         STANDARD_LAYOUTS.add(R.layout.notification_template_material_call);
+        STANDARD_LAYOUTS.add(R.layout.notification_template_material_big_call);
         STANDARD_LAYOUTS.add(R.layout.notification_template_header);
     }
 
@@ -5817,7 +5819,7 @@
          *   @hide
          */
         public RemoteViews createContentView(boolean increasedHeight) {
-            if (mN.contentView != null && useExistingRemoteView()) {
+            if (useExistingRemoteView(mN.contentView)) {
                 return fullyCustomViewRequiresDecoration(false /* fromStyle */)
                         ? minimallyDecoratedContentView(mN.contentView) : mN.contentView;
             } else if (mStyle != null) {
@@ -5833,8 +5835,24 @@
             return applyStandardTemplate(getBaseLayoutResource(), p, null /* result */);
         }
 
-        private boolean useExistingRemoteView() {
-            return mStyle == null || !mStyle.displayCustomViewInline();
+        private boolean useExistingRemoteView(RemoteViews customContent) {
+            if (customContent == null) {
+                return false;
+            }
+            if (styleDisplaysCustomViewInline()) {
+                // the provided custom view is intended to be wrapped by the style.
+                return false;
+            }
+            if (fullyCustomViewRequiresDecoration(false)
+                    && STANDARD_LAYOUTS.contains(customContent.getLayoutId())) {
+                // If the app's custom views are objects returned from Builder.create*ContentView()
+                // then the app is most likely attempting to spoof the user.  Even if they are not,
+                // the result would be broken (b/189189308) so we will ignore it.
+                Log.w(TAG, "For apps targeting S, a custom content view that is a modified "
+                        + "version of any standard layout is disallowed.");
+                return false;
+            }
+            return true;
         }
 
         /**
@@ -5842,7 +5860,7 @@
          */
         public RemoteViews createBigContentView() {
             RemoteViews result = null;
-            if (mN.bigContentView != null && useExistingRemoteView()) {
+            if (useExistingRemoteView(mN.bigContentView)) {
                 return fullyCustomViewRequiresDecoration(false /* fromStyle */)
                         ? minimallyDecoratedBigContentView(mN.bigContentView) : mN.bigContentView;
             }
@@ -5947,7 +5965,7 @@
          * @hide
          */
         public RemoteViews createHeadsUpContentView(boolean increasedHeight) {
-            if (mN.headsUpContentView != null && useExistingRemoteView()) {
+            if (useExistingRemoteView(mN.headsUpContentView)) {
                 return fullyCustomViewRequiresDecoration(false /* fromStyle */)
                         ? minimallyDecoratedHeadsUpContentView(mN.headsUpContentView)
                         : mN.headsUpContentView;
@@ -6396,7 +6414,7 @@
             mN.reduceImageSizes(mContext);
 
             if (mContext.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.N
-                    && (useExistingRemoteView())) {
+                    && !styleDisplaysCustomViewInline()) {
                 if (mN.contentView == null) {
                     mN.contentView = createContentView();
                     mN.extras.putInt(EXTRA_REBUILD_CONTENT_VIEW_ACTION_COUNT,
@@ -6427,6 +6445,10 @@
             return mN;
         }
 
+        private boolean styleDisplaysCustomViewInline() {
+            return mStyle != null && mStyle.displayCustomViewInline();
+        }
+
         /**
          * Apply this Builder to an existing {@link Notification} object.
          *
@@ -6576,7 +6598,7 @@
         public boolean usesTemplate() {
             return (mN.contentView == null && mN.headsUpContentView == null
                     && mN.bigContentView == null)
-                    || (mStyle != null && mStyle.displayCustomViewInline());
+                    || styleDisplaysCustomViewInline();
         }
     }
 
diff --git a/core/java/android/appwidget/AppWidgetManager.java b/core/java/android/appwidget/AppWidgetManager.java
index a88aed7..18c6381 100644
--- a/core/java/android/appwidget/AppWidgetManager.java
+++ b/core/java/android/appwidget/AppWidgetManager.java
@@ -963,6 +963,9 @@
     /**
      * Set the component for a given appWidgetId.
      *
+     * If successful, the app widget provider will receive a {@link #ACTION_APPWIDGET_UPDATE}
+     * broadcast.
+     *
      * <p class="note">You need the BIND_APPWIDGET permission or the user must have enabled binding
      *         widgets always for your component. Should be used by apps that host widgets; if this
      *         method returns false, call {@link #ACTION_APPWIDGET_BIND} to request permission to
@@ -983,6 +986,9 @@
     /**
      * Set the component for a given appWidgetId.
      *
+     * If successful, the app widget provider will receive a {@link #ACTION_APPWIDGET_UPDATE}
+     * broadcast.
+     *
      * <p class="note">You need the BIND_APPWIDGET permission or the user must have enabled binding
      *         widgets always for your component. Should be used by apps that host widgets; if this
      *         method returns false, call {@link #ACTION_APPWIDGET_BIND} to request permission to
@@ -1006,6 +1012,10 @@
 
     /**
      * Set the provider for a given appWidgetId if the caller has a permission.
+     *
+     * If successful, the app widget provider will receive a {@link #ACTION_APPWIDGET_UPDATE}
+     * broadcast.
+     *
      * <p>
      * <strong>Note:</strong> You need the {@link android.Manifest.permission#BIND_APPWIDGET}
      * permission or the user must have enabled binding widgets always for your component.
diff --git a/core/java/android/permission/PermissionControllerService.java b/core/java/android/permission/PermissionControllerService.java
index 0b99b85..9839046 100644
--- a/core/java/android/permission/PermissionControllerService.java
+++ b/core/java/android/permission/PermissionControllerService.java
@@ -17,6 +17,7 @@
 package android.permission;
 
 import static android.app.admin.DevicePolicyManager.PERMISSION_GRANT_STATE_DENIED;
+import static android.app.admin.DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED;
 import static android.permission.PermissionControllerManager.COUNT_ONLY_WHEN_GRANTED;
 import static android.permission.PermissionControllerManager.COUNT_WHEN_SYSTEM;
 
@@ -510,7 +511,7 @@
                     String callerPackageName, AdminPermissionControlParams params,
                     AndroidFuture callback) {
                 checkStringNotEmpty(callerPackageName);
-                if (params.getGrantState() == PERMISSION_GRANT_STATE_DENIED) {
+                if (params.getGrantState() == PERMISSION_GRANT_STATE_GRANTED) {
                     enforceSomePermissionsGrantedToCaller(
                             Manifest.permission.GRANT_RUNTIME_PERMISSIONS);
                 }
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 908d236..8138b3d3 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -4727,9 +4727,10 @@
         WindowInsetsAnimation.Callback mWindowInsetsAnimationCallback;
 
         /**
-         * This lives here since it's only valid for interactive views.
+         * This lives here since it's only valid for interactive views. This list is null until the
+         * first use.
          */
-        private List<Rect> mSystemGestureExclusionRects;
+        private List<Rect> mSystemGestureExclusionRects = null;
 
         /**
          * Used to track {@link #mSystemGestureExclusionRects}
@@ -11603,8 +11604,6 @@
      * a precision touch gesture in a small area in either the X or Y dimension, such as
      * an edge swipe or dragging a <code>SeekBar</code> thumb.</p>
      *
-     * <p>Do not modify the provided list after this method is called.</p>
-     *
      * <p>Note: the system will put a limit of <code>200dp</code> on the vertical extent of the
      * exclusions it takes into account. The limit does not apply while the navigation
      * bar is {@link #SYSTEM_UI_FLAG_IMMERSIVE_STICKY stickily} hidden, nor to the
@@ -11618,13 +11617,17 @@
         if (rects.isEmpty() && mListenerInfo == null) return;
 
         final ListenerInfo info = getListenerInfo();
+        if (info.mSystemGestureExclusionRects != null) {
+            info.mSystemGestureExclusionRects.clear();
+            info.mSystemGestureExclusionRects.addAll(rects);
+        } else {
+            info.mSystemGestureExclusionRects = new ArrayList<>(rects);
+        }
         if (rects.isEmpty()) {
-            info.mSystemGestureExclusionRects = null;
             if (info.mPositionUpdateListener != null) {
                 mRenderNode.removePositionUpdateListener(info.mPositionUpdateListener);
             }
         } else {
-            info.mSystemGestureExclusionRects = rects;
             if (info.mPositionUpdateListener == null) {
                 info.mPositionUpdateListener = new RenderNode.PositionUpdateListener() {
                     @Override
diff --git a/core/java/com/android/internal/policy/IKeyguardService.aidl b/core/java/com/android/internal/policy/IKeyguardService.aidl
index b01e4a8..76aa7a0 100644
--- a/core/java/com/android/internal/policy/IKeyguardService.aidl
+++ b/core/java/com/android/internal/policy/IKeyguardService.aidl
@@ -63,8 +63,10 @@
 
      * @param pmWakeReason One of PowerManager.WAKE_REASON_*, detailing the reason we're waking up,
      * such as WAKE_REASON_POWER_BUTTON or WAKE_REASON_GESTURE.
+     * @param cameraGestureTriggered Whether we're waking up due to a power button double tap
+     * gesture.
      */
-    void onStartedWakingUp(int pmWakeReason);
+    void onStartedWakingUp(int pmWakeReason,  boolean cameraGestureTriggered);
 
     /**
      * Called when the device has finished waking up.
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 1ddff32..c864cd6 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -1792,6 +1792,9 @@
     <!-- Boolean indicating if placing the phone face down will result in a screen off. -->
     <bool name="config_flipToScreenOffEnabled">true</bool>
 
+    <!-- Integer to set a max latency the accelerometer will batch sensor requests with. -->
+    <integer name="config_flipToScreenOffMaxLatencyMicros">2000000</integer>
+
     <!-- Boolean indicating if current platform supports bluetooth SCO for off call
     use cases -->
     <bool name="config_bluetooth_sco_off_call">true</bool>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 4da5859..f30f40a 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -276,6 +276,7 @@
   <java-symbol type="bool" name="config_avoidGfxAccel" />
   <java-symbol type="bool" name="config_bluetooth_address_validation" />
   <java-symbol type="bool" name="config_flipToScreenOffEnabled" />
+  <java-symbol type="integer" name="config_flipToScreenOffMaxLatencyMicros" />
   <java-symbol type="bool" name="config_bluetooth_sco_off_call" />
   <java-symbol type="bool" name="config_bluetooth_le_peripheral_mode_supported" />
   <java-symbol type="bool" name="config_bluetooth_hfp_inband_ringing_support" />
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index 536165b..7e48a7e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -938,6 +938,8 @@
             if (ev.getAction() == MotionEvent.ACTION_DOWN) {
                 if (mShowingManage) {
                     showManageMenu(false /* show */);
+                } else if (mStackEduView != null && mStackEduView.getVisibility() == VISIBLE) {
+                    mStackEduView.hide(false);
                 } else if (mBubbleData.isExpanded()) {
                     mBubbleData.setExpanded(false);
                 }
@@ -1152,6 +1154,7 @@
             mStackEduView = new StackEducationView(mContext);
             addView(mStackEduView);
         }
+        mBubbleContainer.bringToFront();
         return mStackEduView.show(mPositioner.getDefaultStartPosition());
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
index 093c272..84a9ce5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
@@ -135,7 +135,7 @@
                 com.android.wm.shell.R.dimen.starting_surface_exit_animation_window_shift_length);
     }
 
-    private int getSystemBGColor() {
+    private static int getSystemBGColor() {
         final Context systemContext = ActivityThread.currentApplication();
         if (systemContext == null) {
             Slog.e(TAG, "System context does not exist!");
@@ -145,17 +145,18 @@
         return res.getColor(com.android.wm.shell.R.color.splash_window_background_default);
     }
 
-    private Drawable createDefaultBackgroundDrawable() {
+    private static Drawable createDefaultBackgroundDrawable() {
         return new ColorDrawable(getSystemBGColor());
     }
 
-    private @ColorInt int peekWindowBGColor(Context context) {
+    /** Extract the window background color from {@code attrs}. */
+    public static int peekWindowBGColor(Context context, SplashScreenWindowAttrs attrs) {
         Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "peekWindowBGColor");
         final Drawable themeBGDrawable;
-        if (mTmpAttrs.mWindowBgColor != 0) {
-            themeBGDrawable = new ColorDrawable(mTmpAttrs.mWindowBgColor);
-        } else if (mTmpAttrs.mWindowBgResId != 0) {
-            themeBGDrawable = context.getDrawable(mTmpAttrs.mWindowBgResId);
+        if (attrs.mWindowBgColor != 0) {
+            themeBGDrawable = new ColorDrawable(attrs.mWindowBgColor);
+        } else if (attrs.mWindowBgResId != 0) {
+            themeBGDrawable = context.getDrawable(attrs.mWindowBgResId);
         } else {
             themeBGDrawable = createDefaultBackgroundDrawable();
             Slog.w(TAG, "Window background does not exist, using " + themeBGDrawable);
@@ -165,7 +166,7 @@
         return estimatedWindowBGColor;
     }
 
-    private int estimateWindowBGColor(Drawable themeBGDrawable) {
+    private static int estimateWindowBGColor(Drawable themeBGDrawable) {
         final DrawableColorTester themeBGTester =
                 new DrawableColorTester(themeBGDrawable, true /* filterTransparent */);
         if (themeBGTester.nonTransparentRatio() == 0) {
@@ -183,7 +184,7 @@
 
         getWindowAttrs(context, mTmpAttrs);
         final StartingWindowViewBuilder builder = new StartingWindowViewBuilder();
-        final int themeBGColor = peekWindowBGColor(context);
+        final int themeBGColor = peekWindowBGColor(context, this.mTmpAttrs);
         // TODO (b/173975965) Tracking the performance on improved splash screen.
         return builder
                 .setContext(context)
@@ -193,7 +194,11 @@
                 .build();
     }
 
-    private static void getWindowAttrs(Context context, SplashScreenWindowAttrs attrs) {
+    /**
+     * Get the {@link SplashScreenWindowAttrs} from {@code context} and fill them into
+     * {@code attrs}.
+     */
+    public static void getWindowAttrs(Context context, SplashScreenWindowAttrs attrs) {
         final TypedArray typedArray = context.obtainStyledAttributes(
                 com.android.internal.R.styleable.Window);
         attrs.mWindowBgResId = typedArray.getResourceId(R.styleable.Window_windowBackground, 0);
@@ -216,7 +221,8 @@
         }
     }
 
-    private static class SplashScreenWindowAttrs {
+    /** The configuration of the splash screen window. */
+    public static class SplashScreenWindowAttrs {
         private int mWindowBgResId = 0;
         private int mWindowBgColor = Color.TRANSPARENT;
         private Drawable mReplaceIcon = null;
diff --git a/packages/SettingsLib/SettingsTheme/res/values-v31/colors.xml b/packages/SettingsLib/SettingsTheme/res/values-v31/colors.xml
index c9bc583..89e9a005 100644
--- a/packages/SettingsLib/SettingsTheme/res/values-v31/colors.xml
+++ b/packages/SettingsLib/SettingsTheme/res/values-v31/colors.xml
@@ -15,18 +15,18 @@
   limitations under the License.
   -->
 
-<resources xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+<resources>
     <!-- Material next state on color-->
-    <color name="settingslib_state_on_color">?androidprv:attr/colorAccentPrimary</color>
+    <color name="settingslib_state_on_color">@android:color/system_accent1_100</color>
 
     <!-- Material next state off color-->
-    <color name="settingslib_state_off_color">?androidprv:attr/colorAccentSecondary</color>
+    <color name="settingslib_state_off_color">@android:color/system_accent2_100</color>
 
     <!-- Material next thumb off color-->
     <color name="settingslib_thumb_off_color">@android:color/system_neutral2_100</color>
 
     <!-- Material next track on color-->
-    <color name="settingslib_track_on_color">?androidprv:attr/colorAccentPrimaryVariant</color>
+    <color name="settingslib_track_on_color">@android:color/system_accent1_600</color>
 
     <!-- Material next track off color-->
     <color name="settingslib_track_off_color">@android:color/system_neutral2_600</color>
diff --git a/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminController.java b/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminController.java
index 6d7f8df..a537394 100644
--- a/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminController.java
+++ b/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminController.java
@@ -16,6 +16,7 @@
 
 package com.android.settingslib.enterprise;
 
+import android.annotation.UserIdInt;
 import android.content.Context;
 
 import androidx.annotation.Nullable;
@@ -28,10 +29,15 @@
 public interface ActionDisabledByAdminController {
 
     /**
+     * Sets the {@link ActionDisabledLearnMoreButtonLauncher}.
+     */
+    void initialize(ActionDisabledLearnMoreButtonLauncher launcher);
+
+    /**
      * Handles the adding and setting up of the learn more button. If button is not needed, then
      * this method can be left empty.
      */
-    void setupLearnMoreButton(Context context, Object alertDialogBuilder);
+    void setupLearnMoreButton(Context context);
 
     /**
      * Returns the admin support dialog's title resource id.
@@ -41,11 +47,11 @@
     /**
      * Returns the admin support dialog's content string.
      */
-    CharSequence getAdminSupportContentString(
-            Context context, @Nullable CharSequence supportMessage);
+    CharSequence getAdminSupportContentString(Context context,
+            @Nullable CharSequence supportMessage);
 
     /**
      * Updates the enforced admin
      */
-    void updateEnforcedAdmin(RestrictedLockUtils.EnforcedAdmin admin, int adminUserId);
+    void updateEnforcedAdmin(RestrictedLockUtils.EnforcedAdmin admin, @UserIdInt int adminUserId);
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java b/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java
index 7eecd19..da42e33 100644
--- a/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java
+++ b/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java
@@ -19,29 +19,30 @@
 import static android.app.admin.DevicePolicyManager.DEVICE_OWNER_TYPE_FINANCED;
 
 import android.app.admin.DevicePolicyManager;
+import android.content.Context;
 
 /**
  * A factory that returns the relevant instance of {@link ActionDisabledByAdminController}.
  */
-public class ActionDisabledByAdminControllerFactory {
+public final class ActionDisabledByAdminControllerFactory {
 
     /**
      * Returns the relevant instance of {@link ActionDisabledByAdminController}.
      */
-    public static ActionDisabledByAdminController createInstance(
-            DevicePolicyManager dpm,
-            ActionDisabledLearnMoreButtonLauncher helper,
-            DeviceAdminStringProvider deviceAdminStringProvider) {
-        if (isFinancedDevice(dpm)) {
-            return new FinancedDeviceActionDisabledByAdminController(
-                    helper, deviceAdminStringProvider);
-        }
-        return new ManagedDeviceActionDisabledByAdminController(
-                helper, deviceAdminStringProvider);
+    public static ActionDisabledByAdminController createInstance(Context context,
+            DeviceAdminStringProvider stringProvider) {
+        return isFinancedDevice(context)
+                ? new FinancedDeviceActionDisabledByAdminController(stringProvider)
+                : new ManagedDeviceActionDisabledByAdminController(stringProvider);
     }
 
-    private static boolean isFinancedDevice(DevicePolicyManager dpm) {
+    private static boolean isFinancedDevice(Context context) {
+        DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class);
         return dpm.isDeviceManaged() && dpm.getDeviceOwnerType(
                 dpm.getDeviceOwnerComponentOnAnyUser()) == DEVICE_OWNER_TYPE_FINANCED;
     }
+
+    private ActionDisabledByAdminControllerFactory() {
+        throw new UnsupportedOperationException("provides only static methods");
+    }
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledLearnMoreButtonLauncher.java b/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledLearnMoreButtonLauncher.java
index 65b91f1..78a42be 100644
--- a/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledLearnMoreButtonLauncher.java
+++ b/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledLearnMoreButtonLauncher.java
@@ -16,29 +16,100 @@
 
 package com.android.settingslib.enterprise;
 
-import android.content.Context;
+import static java.util.Objects.requireNonNull;
 
-import com.android.settingslib.RestrictedLockUtils;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.UserHandle;
+import android.os.UserManager;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
 
 /**
- * Helper interface meant to set up the "Learn more" button in the action disabled dialog.
+ * Helper class meant to set up the "Learn more" button in the action disabled dialog.
  */
-public interface ActionDisabledLearnMoreButtonLauncher {
+public abstract class ActionDisabledLearnMoreButtonLauncher {
 
     /**
      * Sets up a "learn more" button which shows a screen with device policy settings
      */
-    void setupLearnMoreButtonToShowAdminPolicies(
-            Context context,
-            Object alertDialogBuilder,
-            int enforcementAdminUserId,
-            RestrictedLockUtils.EnforcedAdmin enforcedAdmin);
+    public final void setupLearnMoreButtonToShowAdminPolicies(Context context,
+            int enforcementAdminUserId, EnforcedAdmin enforcedAdmin) {
+        requireNonNull(context, "context cannot be null");
+        requireNonNull(enforcedAdmin, "enforcedAdmin cannot be null");
+
+        // The "Learn more" button appears only if the restriction is enforced by an admin in the
+        // same profile group. Otherwise the admin package and its policies are not accessible to
+        // the current user.
+        if (isSameProfileGroup(context, enforcementAdminUserId)) {
+            setLearnMoreButton(() -> showAdminPolicies(context, enforcedAdmin));
+        }
+    }
 
     /**
      * Sets up a "learn more" button which launches a help page
      */
-    void setupLearnMoreButtonToLaunchHelpPage(
-            Context context,
-            Object alertDialogBuilder,
-            String url);
+    public final void setupLearnMoreButtonToLaunchHelpPage(Context context, String url) {
+        requireNonNull(context, "context cannot be null");
+        requireNonNull(url, "url cannot be null");
+
+        setLearnMoreButton(() -> showHelpPage(context, url));
+    }
+
+    /**
+     * Sets the "learning more" button.
+     *
+     * @param action action to be run when the button is tapped.
+     */
+    public abstract void setLearnMoreButton(Runnable action);
+
+    /**
+     * Launches the settings page with info about the given admin.
+     */
+    protected abstract void launchShowAdminPolicies(Context context, UserHandle user,
+            ComponentName admin);
+
+    /**
+     * Launches the settings page that shows all admins.
+     */
+    protected abstract void launchShowAdminSettings(Context context);
+
+    /**
+     * Callback to finish the activity associated with the launcher.
+     */
+    protected void finishSelf() {
+    }
+
+    @VisibleForTesting
+    protected boolean isSameProfileGroup(Context context, int enforcementAdminUserId) {
+        UserManager um = context.getSystemService(UserManager.class);
+
+        return um.isSameProfileGroup(enforcementAdminUserId, um.getUserHandle());
+    }
+
+    /**
+     * Shows the help page using the given {@code url}.
+     */
+    @VisibleForTesting
+    public void showHelpPage(Context context, String url) {
+        context.startActivityAsUser(createLearnMoreIntent(url), UserHandle.of(context.getUserId()));
+        finishSelf();
+    }
+
+    private void showAdminPolicies(Context context, EnforcedAdmin enforcedAdmin) {
+        if (enforcedAdmin.component != null) {
+            launchShowAdminPolicies(context, enforcedAdmin.user, enforcedAdmin.component);
+        } else {
+            launchShowAdminSettings(context);
+        }
+        finishSelf();
+    }
+
+    private static Intent createLearnMoreIntent(String url) {
+        return new Intent(Intent.ACTION_VIEW, Uri.parse(url)).setFlags(
+                Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+    }
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/enterprise/BaseActionDisabledByAdminController.java b/packages/SettingsLib/src/com/android/settingslib/enterprise/BaseActionDisabledByAdminController.java
new file mode 100644
index 0000000..dd71557
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/enterprise/BaseActionDisabledByAdminController.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2021 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.settingslib.enterprise;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.UserIdInt;
+
+import com.android.internal.util.Preconditions;
+import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
+
+/**
+ * Base class for {@link ActionDisabledByAdminController} implementations.
+ */
+abstract class BaseActionDisabledByAdminController
+        implements ActionDisabledByAdminController {
+
+    protected @UserIdInt int mEnforcementAdminUserId;
+    protected EnforcedAdmin mEnforcedAdmin;
+    protected ActionDisabledLearnMoreButtonLauncher mLauncher;
+    protected final DeviceAdminStringProvider mStringProvider;
+
+    BaseActionDisabledByAdminController(DeviceAdminStringProvider stringProvider) {
+        mStringProvider = stringProvider;
+    }
+
+    @Override
+    public final void initialize(ActionDisabledLearnMoreButtonLauncher launcher) {
+        mLauncher = requireNonNull(launcher, "launcher cannot be null");
+    }
+
+    @Override
+    public final void updateEnforcedAdmin(EnforcedAdmin admin, int adminUserId) {
+        assertInitialized();
+        mEnforcementAdminUserId = adminUserId;
+        mEnforcedAdmin = requireNonNull(admin, "admin cannot be null");
+    }
+
+    protected final void assertInitialized() {
+        Preconditions.checkState(mLauncher != null, "must call initialize() first");
+    }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/enterprise/FinancedDeviceActionDisabledByAdminController.java b/packages/SettingsLib/src/com/android/settingslib/enterprise/FinancedDeviceActionDisabledByAdminController.java
index cd816e88..2ed0dc4 100644
--- a/packages/SettingsLib/src/com/android/settingslib/enterprise/FinancedDeviceActionDisabledByAdminController.java
+++ b/packages/SettingsLib/src/com/android/settingslib/enterprise/FinancedDeviceActionDisabledByAdminController.java
@@ -16,52 +16,31 @@
 
 package com.android.settingslib.enterprise;
 
-import static java.util.Objects.requireNonNull;
-
-import android.annotation.UserIdInt;
 import android.content.Context;
 
 import androidx.annotation.Nullable;
 
-import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
-
 /**
  * An {@link ActionDisabledByAdminController} to be used with financed devices.
  */
-public class FinancedDeviceActionDisabledByAdminController
-        implements ActionDisabledByAdminController {
+final class FinancedDeviceActionDisabledByAdminController
+        extends BaseActionDisabledByAdminController {
 
-    private @UserIdInt int mEnforcementAdminUserId;
-    private EnforcedAdmin mEnforcedAdmin;
-    private final ActionDisabledLearnMoreButtonLauncher mHelper;
-    private final DeviceAdminStringProvider mDeviceAdminStringProvider;
-
-    FinancedDeviceActionDisabledByAdminController(
-            ActionDisabledLearnMoreButtonLauncher helper,
-            DeviceAdminStringProvider deviceAdminStringProvider) {
-        mHelper = requireNonNull(helper, "helper cannot be null");
-        mDeviceAdminStringProvider = requireNonNull(deviceAdminStringProvider,
-                "deviceAdminStringProvider cannot be null");
+    FinancedDeviceActionDisabledByAdminController(DeviceAdminStringProvider stringProvider) {
+        super(stringProvider);
     }
 
     @Override
-    public void updateEnforcedAdmin(EnforcedAdmin admin, int adminUserId) {
-        mEnforcementAdminUserId = adminUserId;
-        mEnforcedAdmin = requireNonNull(admin, "admin cannot be null");
-    }
+    public void setupLearnMoreButton(Context context) {
+        assertInitialized();
 
-    @Override
-    public void setupLearnMoreButton(Context context, Object alertDialogBuilder) {
-        mHelper.setupLearnMoreButtonToShowAdminPolicies(
-                context,
-                alertDialogBuilder,
-                mEnforcementAdminUserId,
+        mLauncher.setupLearnMoreButtonToShowAdminPolicies(context, mEnforcementAdminUserId,
                 mEnforcedAdmin);
     }
 
     @Override
     public String getAdminSupportTitle(@Nullable String restriction) {
-        return mDeviceAdminStringProvider.getDisabledByPolicyTitleForFinancedDevice();
+        return mStringProvider.getDisabledByPolicyTitleForFinancedDevice();
     }
 
     @Override
diff --git a/packages/SettingsLib/src/com/android/settingslib/enterprise/ManagedDeviceActionDisabledByAdminController.java b/packages/SettingsLib/src/com/android/settingslib/enterprise/ManagedDeviceActionDisabledByAdminController.java
index 70e19f9..df6bab7 100644
--- a/packages/SettingsLib/src/com/android/settingslib/enterprise/ManagedDeviceActionDisabledByAdminController.java
+++ b/packages/SettingsLib/src/com/android/settingslib/enterprise/ManagedDeviceActionDisabledByAdminController.java
@@ -16,55 +16,39 @@
 
 package com.android.settingslib.enterprise;
 
-import static java.util.Objects.requireNonNull;
-
-import android.annotation.UserIdInt;
 import android.app.admin.DevicePolicyManager;
 import android.content.Context;
 import android.os.UserManager;
 import android.text.TextUtils;
 
-import com.android.settingslib.RestrictedLockUtils;
+import androidx.annotation.Nullable;
+
 
 /**
  * An {@link ActionDisabledByAdminController} to be used with managed devices.
  */
-class ManagedDeviceActionDisabledByAdminController implements
-        ActionDisabledByAdminController {
-    private @UserIdInt int mEnforcementAdminUserId;
-    private RestrictedLockUtils.EnforcedAdmin mEnforcedAdmin;
-    private final ActionDisabledLearnMoreButtonLauncher mHelper;
-    private final DeviceAdminStringProvider mStringProvider;
+final class ManagedDeviceActionDisabledByAdminController
+        extends BaseActionDisabledByAdminController {
 
-    ManagedDeviceActionDisabledByAdminController(
-            ActionDisabledLearnMoreButtonLauncher helper,
-            DeviceAdminStringProvider stringProvider) {
-        mHelper = requireNonNull(helper);
-        mStringProvider = requireNonNull(stringProvider);
+    ManagedDeviceActionDisabledByAdminController(DeviceAdminStringProvider stringProvider) {
+        super(stringProvider);
     }
 
     @Override
-    public void updateEnforcedAdmin(RestrictedLockUtils.EnforcedAdmin admin, int adminUserId) {
-        mEnforcementAdminUserId = adminUserId;
-        mEnforcedAdmin = requireNonNull(admin, "admin cannot be null");
-    }
+    public void setupLearnMoreButton(Context context) {
+        assertInitialized();
 
-    @Override
-    public void setupLearnMoreButton(Context context, Object alertDialogBuilder) {
         String url = mStringProvider.getLearnMoreHelpPageUrl();
         if (TextUtils.isEmpty(url)) {
-            mHelper.setupLearnMoreButtonToShowAdminPolicies(
-                    context,
-                    alertDialogBuilder,
-                    mEnforcementAdminUserId,
+            mLauncher.setupLearnMoreButtonToShowAdminPolicies(context, mEnforcementAdminUserId,
                     mEnforcedAdmin);
         } else {
-            mHelper.setupLearnMoreButtonToLaunchHelpPage(context, alertDialogBuilder, url);
+            mLauncher.setupLearnMoreButtonToLaunchHelpPage(context, url);
         }
     }
 
     @Override
-    public String getAdminSupportTitle(String restriction) {
+    public String getAdminSupportTitle(@Nullable String restriction) {
         if (restriction == null) {
             return mStringProvider.getDefaultDisabledByPolicyTitle();
         }
@@ -88,9 +72,8 @@
 
     @Override
     public CharSequence getAdminSupportContentString(Context context, CharSequence supportMessage) {
-        if (supportMessage != null) {
-            return supportMessage;
-        }
-        return mStringProvider.getDefaultDisabledByPolicyContent();
+        return supportMessage != null
+                ? supportMessage
+                : mStringProvider.getDefaultDisabledByPolicyContent();
     }
 }
diff --git a/packages/SettingsLib/tests/robotests/Android.bp b/packages/SettingsLib/tests/robotests/Android.bp
index 63cfe59..2d1a516 100644
--- a/packages/SettingsLib/tests/robotests/Android.bp
+++ b/packages/SettingsLib/tests/robotests/Android.bp
@@ -45,6 +45,7 @@
         "SettingsLib-robo-testutils",
         "androidx.test.core",
         "androidx.core_core",
+        "testng", // TODO: remove once JUnit on Android provides assertThrows
     ],
     java_resource_dirs: ["config"],
     instrumentation_for: "SettingsLibShell",
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerTestUtils.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerTestUtils.java
index 4b51790..e57335f 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerTestUtils.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerTestUtils.java
@@ -16,51 +16,81 @@
 
 package com.android.settingslib.enterprise;
 
-import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 
-import android.app.Activity;
+import android.content.ComponentName;
 import android.content.Context;
+import android.os.UserHandle;
+import android.util.DebugUtils;
 
-import androidx.appcompat.app.AlertDialog;
-
-import com.android.settingslib.RestrictedLockUtils;
+import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
 
 /**
  * Utils related to the action disabled by admin dialogs.
  */
-class ActionDisabledByAdminControllerTestUtils {
-    static final int LEARN_MORE_ACTION_NONE = 0;
-    static final int LEARN_MORE_ACTION_SHOW_ADMIN_POLICIES = 1;
-    static final int LEARN_MORE_ACTION_LAUNCH_HELP_PAGE = 2;
+// NOTE: must be public because of DebugUtils.constantToString() call
+public final class ActionDisabledByAdminControllerTestUtils {
+
+    static final int ENFORCEMENT_ADMIN_USER_ID = 123;
+    static final UserHandle ENFORCEMENT_ADMIN_USER = UserHandle.of(ENFORCEMENT_ADMIN_USER_ID);
+
+    static final String SUPPORT_MESSAGE = "support message";
+
+    static final ComponentName ADMIN_COMPONENT =
+            new ComponentName("some.package.name", "some.package.name.SomeClass");
+    static final EnforcedAdmin ENFORCED_ADMIN = new EnforcedAdmin(
+                    ADMIN_COMPONENT, UserHandle.of(ENFORCEMENT_ADMIN_USER_ID));
+    static final EnforcedAdmin ENFORCED_ADMIN_WITHOUT_COMPONENT = new EnforcedAdmin(
+            /* component= */ null, UserHandle.of(ENFORCEMENT_ADMIN_USER_ID));
+
+    static final String URL = "https://testexample.com";
+
+    // NOTE: fields below must be public because of DebugUtils.constantToString() call
+    public static final int LEARN_MORE_ACTION_NONE = 0;
+    public static final int LEARN_MORE_ACTION_SHOW_ADMIN_POLICIES = 1;
+    public static final int LEARN_MORE_ACTION_SHOW_ADMIN_SETTINGS = 2;
+    public static final int LEARN_MORE_ACTION_LAUNCH_HELP_PAGE = 3;
 
     private int mLearnMoreButtonAction = LEARN_MORE_ACTION_NONE;
 
     ActionDisabledLearnMoreButtonLauncher createLearnMoreButtonLauncher() {
         return new ActionDisabledLearnMoreButtonLauncher() {
+
             @Override
-            public void setupLearnMoreButtonToShowAdminPolicies(Context context,
-                    Object alertDialogBuilder, int enforcementAdminUserId,
-                    RestrictedLockUtils.EnforcedAdmin enforcedAdmin) {
+            public void setLearnMoreButton(Runnable action) {
+                action.run();
+            }
+
+            @Override
+            protected void launchShowAdminPolicies(Context context, UserHandle user,
+                    ComponentName admin) {
                 mLearnMoreButtonAction = LEARN_MORE_ACTION_SHOW_ADMIN_POLICIES;
             }
 
             @Override
-            public void setupLearnMoreButtonToLaunchHelpPage(Context context,
-                    Object alertDialogBuilder, String url) {
+            protected void launchShowAdminSettings(Context context) {
+                mLearnMoreButtonAction = LEARN_MORE_ACTION_SHOW_ADMIN_SETTINGS;
+            }
+
+            @Override
+            public void showHelpPage(Context context, String url) {
                 mLearnMoreButtonAction = LEARN_MORE_ACTION_LAUNCH_HELP_PAGE;
             }
+
+            @Override
+            protected boolean isSameProfileGroup(Context context, int enforcementAdminUserId) {
+                return true;
+            }
         };
     }
 
     void assertLearnMoreAction(int learnMoreActionShowAdminPolicies) {
-        assertThat(learnMoreActionShowAdminPolicies).isEqualTo(mLearnMoreButtonAction);
+        assertWithMessage("action").that(actionToString(mLearnMoreButtonAction))
+                .isEqualTo(actionToString(learnMoreActionShowAdminPolicies));
     }
 
-    AlertDialog createAlertDialog(ActionDisabledByAdminController mController, Activity activity) {
-        AlertDialog.Builder builder = new AlertDialog.Builder(activity);
-        mController.setupLearnMoreButton(activity, builder);
-        AlertDialog alertDialog = builder.create();
-        alertDialog.show();
-        return alertDialog;
+    private static String actionToString(int action) {
+        return DebugUtils.constantToString(ActionDisabledByAdminControllerTestUtils.class,
+                "LEARN_MORE_ACTION_", action);
     }
 }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/ActionDisabledLearnMoreButtonLauncherTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/ActionDisabledLearnMoreButtonLauncherTest.java
new file mode 100644
index 0000000..7014da0
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/ActionDisabledLearnMoreButtonLauncherTest.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2021 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.settingslib.enterprise;
+
+import static com.android.settingslib.enterprise.ActionDisabledByAdminControllerTestUtils.ADMIN_COMPONENT;
+import static com.android.settingslib.enterprise.ActionDisabledByAdminControllerTestUtils.ENFORCED_ADMIN;
+import static com.android.settingslib.enterprise.ActionDisabledByAdminControllerTestUtils.ENFORCED_ADMIN_WITHOUT_COMPONENT;
+import static com.android.settingslib.enterprise.ActionDisabledByAdminControllerTestUtils.ENFORCEMENT_ADMIN_USER;
+import static com.android.settingslib.enterprise.ActionDisabledByAdminControllerTestUtils.ENFORCEMENT_ADMIN_USER_ID;
+import static com.android.settingslib.enterprise.ActionDisabledByAdminControllerTestUtils.URL;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertThrows;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.UserHandle;
+import android.os.UserManager;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)// NOTE: this test doesn't need RoboElectric...
+public final class ActionDisabledLearnMoreButtonLauncherTest {
+
+    private static final int CONTEXT_USER_ID = -ENFORCEMENT_ADMIN_USER_ID;
+    private static final UserHandle CONTEXT_USER = UserHandle.of(CONTEXT_USER_ID);
+
+    @Rule
+    public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+    @Mock
+    private Context mContext;
+
+    @Mock
+    private UserManager mUserManager;
+
+    @Spy
+    private ActionDisabledLearnMoreButtonLauncher mLauncher;
+
+    @Captor
+    private ArgumentCaptor<Runnable> mLearnMoreActionCaptor;
+
+    @Captor
+    private ArgumentCaptor<Intent> mIntentCaptor;
+
+    @Before
+    public void setUp() {
+        when(mContext.getUserId()).thenReturn(CONTEXT_USER_ID);
+        when(mUserManager.getUserHandle()).thenReturn(CONTEXT_USER_ID);
+        when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager);
+    }
+
+    @Test
+    public void testSetupLearnMoreButtonToShowAdminPolicies_nullContext() {
+        assertThrows(NullPointerException.class,
+                () -> mLauncher.setupLearnMoreButtonToShowAdminPolicies(/* context= */ null,
+                        ENFORCEMENT_ADMIN_USER_ID, ENFORCED_ADMIN));
+    }
+
+    @Test
+    public void testSetupLearnMoreButtonToShowAdminPolicies_nullEnforcedAdmin() {
+        assertThrows(NullPointerException.class,
+                () -> mLauncher.setupLearnMoreButtonToShowAdminPolicies(/* context= */ null,
+                        ENFORCEMENT_ADMIN_USER_ID, /* enforcedAdmin= */ null));
+    }
+
+    @Test
+    public void testSetupLearnMoreButtonToShowAdminPolicies_differentProfileGroup() {
+        mockDifferentProfileGroup();
+
+        mLauncher.setupLearnMoreButtonToShowAdminPolicies(mContext, ENFORCEMENT_ADMIN_USER_ID,
+                ENFORCED_ADMIN);
+
+        verify(mLauncher, never()).setLearnMoreButton(any());
+    }
+
+    @Test
+    public void testSetupLearnMoreButtonToShowAdminPolicies_sameProfileGroup_noComponent() {
+        mockSameProfileGroup();
+
+        mLauncher.setupLearnMoreButtonToShowAdminPolicies(mContext, ENFORCEMENT_ADMIN_USER_ID,
+                ENFORCED_ADMIN_WITHOUT_COMPONENT);
+        tapLearnMore();
+
+        verify(mLauncher, never()).launchShowAdminPolicies(any(), any(), any());
+        verify(mLauncher).launchShowAdminSettings(mContext);
+        verifyFinishSelf();
+    }
+
+    @Test
+    public void testSetupLearnMoreButtonToShowAdminPolicies_sameProfileGroup_withComponent() {
+        mockSameProfileGroup();
+
+        mLauncher.setupLearnMoreButtonToShowAdminPolicies(mContext, ENFORCEMENT_ADMIN_USER_ID,
+                ENFORCED_ADMIN);
+        tapLearnMore();
+
+        verify(mLauncher).launchShowAdminPolicies(mContext, ENFORCEMENT_ADMIN_USER,
+                ADMIN_COMPONENT);
+        verify(mLauncher, never()).launchShowAdminSettings(any());
+        verifyFinishSelf();
+    }
+
+    @Test
+    public void testSetupLearnMoreButtonToLaunchHelpPage_nullContext() {
+        assertThrows(NullPointerException.class,
+                () -> mLauncher.setupLearnMoreButtonToLaunchHelpPage(/* context= */ null, URL));
+    }
+
+    @Test
+    public void testSetupLearnMoreButtonToLaunchHelpPage_nullUrl() {
+        assertThrows(NullPointerException.class,
+                () -> mLauncher.setupLearnMoreButtonToLaunchHelpPage(mContext, /* url= */ null));
+    }
+
+    @Test
+    public void testSetupLearnMoreButtonToLaunchHelpPage() {
+        mLauncher.setupLearnMoreButtonToLaunchHelpPage(mContext, URL);
+        tapLearnMore();
+
+        verify(mContext).startActivityAsUser(mIntentCaptor.capture(), eq(CONTEXT_USER));
+        Intent intent = mIntentCaptor.getValue();
+        assertWithMessage("wrong url on intent %s", intent).that(intent.getData())
+                .isEqualTo(Uri.parse(URL));
+        verifyFinishSelf();
+    }
+
+    private void mockDifferentProfileGroup() {
+        // No need to mock anything - isSameProfileGroup() will return false by default
+    }
+
+    private void mockSameProfileGroup() {
+        when(mUserManager.isSameProfileGroup(ENFORCEMENT_ADMIN_USER_ID, CONTEXT_USER_ID))
+                .thenReturn(true);
+    }
+
+    private void tapLearnMore() {
+        verify(mLauncher).setLearnMoreButton(mLearnMoreActionCaptor.capture());
+        mLearnMoreActionCaptor.getValue().run();
+    }
+
+    private void verifyFinishSelf() {
+        verify(mLauncher).finishSelf();
+    }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/FakeDeviceAdminStringProvider.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/FakeDeviceAdminStringProvider.java
index 8c07d75..be3e9fc 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/FakeDeviceAdminStringProvider.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/FakeDeviceAdminStringProvider.java
@@ -30,6 +30,8 @@
     static final String DEFAULT_DISABLED_BY_POLICY_CONTENT = "default_disabled_by_policy_content";
     static final String DEFAULT_DISABLED_BY_POLICY_TITLE_FINANCED_DEVICE =
             "default_disabled_by_policy_title_financed_device";
+    static final DeviceAdminStringProvider DEFAULT_DEVICE_ADMIN_STRING_PROVIDER =
+            new FakeDeviceAdminStringProvider(/* url = */ null);
 
     private final String mUrl;
 
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/FinancedDeviceActionDisabledByAdminControllerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/FinancedDeviceActionDisabledByAdminControllerTest.java
index 2fe2262..7b08fee 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/FinancedDeviceActionDisabledByAdminControllerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/FinancedDeviceActionDisabledByAdminControllerTest.java
@@ -16,22 +16,21 @@
 
 package com.android.settingslib.enterprise;
 
+import static com.android.settingslib.enterprise.ActionDisabledByAdminControllerTestUtils.ENFORCED_ADMIN;
+import static com.android.settingslib.enterprise.ActionDisabledByAdminControllerTestUtils.ENFORCEMENT_ADMIN_USER_ID;
 import static com.android.settingslib.enterprise.ActionDisabledByAdminControllerTestUtils.LEARN_MORE_ACTION_SHOW_ADMIN_POLICIES;
+import static com.android.settingslib.enterprise.ActionDisabledByAdminControllerTestUtils.SUPPORT_MESSAGE;
+import static com.android.settingslib.enterprise.FakeDeviceAdminStringProvider.DEFAULT_DEVICE_ADMIN_STRING_PROVIDER;
 import static com.android.settingslib.enterprise.FakeDeviceAdminStringProvider.DEFAULT_DISABLED_BY_POLICY_TITLE_FINANCED_DEVICE;
 
 import static com.google.common.truth.Truth.assertThat;
 
 import android.app.Activity;
-import android.app.Dialog;
-import android.content.ComponentName;
 import android.content.Context;
-import android.os.UserHandle;
 
-import androidx.appcompat.app.AlertDialog;
 import androidx.test.core.app.ApplicationProvider;
 
 import com.android.settingslib.R;
-import com.android.settingslib.RestrictedLockUtils;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -41,73 +40,46 @@
 
 @RunWith(RobolectricTestRunner.class)
 public class FinancedDeviceActionDisabledByAdminControllerTest {
-    private static final int ENFORCEMENT_ADMIN_USER_ID = 123;
-    private static final ComponentName ADMIN_COMPONENT =
-            new ComponentName("some.package.name", "some.package.name.SomeClass");
-    private static final String SUPPORT_MESSAGE = "support message";
-    private static final DeviceAdminStringProvider DEVICE_ADMIN_STRING_PROVIDER =
-            new FakeDeviceAdminStringProvider(/* url = */ null);
-    private static final RestrictedLockUtils.EnforcedAdmin ENFORCED_ADMIN =
-            new RestrictedLockUtils.EnforcedAdmin(
-                    ADMIN_COMPONENT, UserHandle.of(ENFORCEMENT_ADMIN_USER_ID));
 
     private final Context mContext = ApplicationProvider.getApplicationContext();
     private final Activity mActivity = ActivityController.of(new Activity()).get();
     private final ActionDisabledByAdminControllerTestUtils mTestUtils =
             new ActionDisabledByAdminControllerTestUtils();
-    private final ActionDisabledLearnMoreButtonLauncher mLauncher =
-            mTestUtils.createLearnMoreButtonLauncher();
+    private final FinancedDeviceActionDisabledByAdminController mController =
+            new FinancedDeviceActionDisabledByAdminController(
+                    DEFAULT_DEVICE_ADMIN_STRING_PROVIDER);
 
     @Before
     public void setUp() {
         mActivity.setTheme(R.style.Theme_AppCompat_DayNight);
+
+        mController.initialize(mTestUtils.createLearnMoreButtonLauncher());
+        mController.updateEnforcedAdmin(ENFORCED_ADMIN, ENFORCEMENT_ADMIN_USER_ID);
+
     }
 
     @Test
     public void setupLearnMoreButton_negativeButtonSet() {
-        FinancedDeviceActionDisabledByAdminController mController = createController(mLauncher);
-        AlertDialog alertDialog = mTestUtils.createAlertDialog(mController, mActivity);
-
-        alertDialog.getButton(Dialog.BUTTON_NEUTRAL).performClick();
+        mController.setupLearnMoreButton(mContext);
 
         mTestUtils.assertLearnMoreAction(LEARN_MORE_ACTION_SHOW_ADMIN_POLICIES);
     }
 
     @Test
     public void getAdminSupportTitleResource_works() {
-        FinancedDeviceActionDisabledByAdminController mController = createController();
-
         assertThat(mController.getAdminSupportTitle(null))
                 .isEqualTo(DEFAULT_DISABLED_BY_POLICY_TITLE_FINANCED_DEVICE);
     }
 
     @Test
     public void getAdminSupportContentString_withSupportMessage_returnsSupportMessage() {
-        FinancedDeviceActionDisabledByAdminController mController = createController();
-
         assertThat(mController.getAdminSupportContentString(mContext, SUPPORT_MESSAGE))
                 .isEqualTo(SUPPORT_MESSAGE);
     }
 
     @Test
     public void getAdminSupportContentString_noSupportMessage_returnsNull() {
-        FinancedDeviceActionDisabledByAdminController mController = createController();
-
         assertThat(mController.getAdminSupportContentString(mContext, /* supportMessage= */ null))
                 .isNull();
     }
-
-    private FinancedDeviceActionDisabledByAdminController createController() {
-        return createController(mLauncher);
-    }
-
-    private FinancedDeviceActionDisabledByAdminController createController(
-            ActionDisabledLearnMoreButtonLauncher buttonHelper) {
-        FinancedDeviceActionDisabledByAdminController controller =
-                new FinancedDeviceActionDisabledByAdminController(
-                        buttonHelper,
-                        DEVICE_ADMIN_STRING_PROVIDER);
-        controller.updateEnforcedAdmin(ENFORCED_ADMIN, ENFORCEMENT_ADMIN_USER_ID);
-        return controller;
-    }
 }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/ManagedDeviceActionDisabledByAdminControllerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/ManagedDeviceActionDisabledByAdminControllerTest.java
index eb1dc96..19f6aa1 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/ManagedDeviceActionDisabledByAdminControllerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/ManagedDeviceActionDisabledByAdminControllerTest.java
@@ -16,8 +16,12 @@
 
 package com.android.settingslib.enterprise;
 
+import static com.android.settingslib.enterprise.ActionDisabledByAdminControllerTestUtils.ENFORCED_ADMIN;
+import static com.android.settingslib.enterprise.ActionDisabledByAdminControllerTestUtils.ENFORCEMENT_ADMIN_USER_ID;
 import static com.android.settingslib.enterprise.ActionDisabledByAdminControllerTestUtils.LEARN_MORE_ACTION_LAUNCH_HELP_PAGE;
 import static com.android.settingslib.enterprise.ActionDisabledByAdminControllerTestUtils.LEARN_MORE_ACTION_SHOW_ADMIN_POLICIES;
+import static com.android.settingslib.enterprise.ActionDisabledByAdminControllerTestUtils.SUPPORT_MESSAGE;
+import static com.android.settingslib.enterprise.ActionDisabledByAdminControllerTestUtils.URL;
 import static com.android.settingslib.enterprise.FakeDeviceAdminStringProvider.DEFAULT_DISABLED_BY_POLICY_CONTENT;
 import static com.android.settingslib.enterprise.FakeDeviceAdminStringProvider.DEFAULT_DISABLED_BY_POLICY_TITLE;
 import static com.android.settingslib.enterprise.FakeDeviceAdminStringProvider.DISALLOW_ADJUST_VOLUME_TITLE;
@@ -25,15 +29,12 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import android.app.Activity;
-import android.app.Dialog;
-import android.content.ComponentName;
-import android.os.UserHandle;
+import android.content.Context;
 import android.os.UserManager;
 
-import androidx.appcompat.app.AlertDialog;
+import androidx.test.core.app.ApplicationProvider;
 
 import com.android.settingslib.R;
-import com.android.settingslib.RestrictedLockUtils;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -43,23 +44,15 @@
 
 @RunWith(RobolectricTestRunner.class)
 public class ManagedDeviceActionDisabledByAdminControllerTest {
-    private static final int ENFORCEMENT_ADMIN_USER_ID = 123;
-    private static final ComponentName ADMIN_COMPONENT =
-            new ComponentName("some.package.name", "some.package.name.SomeClass");
-    private static final String SUPPORT_MESSAGE = "support message";
+
     private static final String RESTRICTION = UserManager.DISALLOW_ADJUST_VOLUME;
-    private static final String URL = "https://testexample.com";
     private static final String EMPTY_URL = "";
-    private static final RestrictedLockUtils.EnforcedAdmin ENFORCED_ADMIN =
-            new RestrictedLockUtils.EnforcedAdmin(
-                    ADMIN_COMPONENT, UserHandle.of(ENFORCEMENT_ADMIN_USER_ID));
     private static final String SUPPORT_TITLE_FOR_RESTRICTION = DISALLOW_ADJUST_VOLUME_TITLE;
 
+    private final Context mContext = ApplicationProvider.getApplicationContext();
     private final Activity mActivity = ActivityController.of(new Activity()).get();
     private final ActionDisabledByAdminControllerTestUtils mTestUtils =
             new ActionDisabledByAdminControllerTestUtils();
-    private final ActionDisabledLearnMoreButtonLauncher mLauncher =
-            mTestUtils.createLearnMoreButtonLauncher();
 
     @Before
     public void setUp() {
@@ -68,68 +61,63 @@
 
     @Test
     public void setupLearnMoreButton_validUrl_negativeButtonSet() {
-        ManagedDeviceActionDisabledByAdminController mController =
-                createController(mLauncher, URL);
-        AlertDialog alertDialog = mTestUtils.createAlertDialog(mController, mActivity);
+        ManagedDeviceActionDisabledByAdminController controller = createController(URL);
 
-        alertDialog.getButton(Dialog.BUTTON_NEUTRAL).performClick();
+        controller.setupLearnMoreButton(mContext);
 
         mTestUtils.assertLearnMoreAction(LEARN_MORE_ACTION_LAUNCH_HELP_PAGE);
     }
 
     @Test
     public void setupLearnMoreButton_noUrl_negativeButtonSet() {
-        ManagedDeviceActionDisabledByAdminController mController =
-                createController(mLauncher, EMPTY_URL);
-        AlertDialog alertDialog = mTestUtils.createAlertDialog(mController, mActivity);
+        ManagedDeviceActionDisabledByAdminController controller = createController(EMPTY_URL);
 
-        alertDialog.getButton(Dialog.BUTTON_NEUTRAL).performClick();
+        controller.setupLearnMoreButton(mContext);
 
         mTestUtils.assertLearnMoreAction(LEARN_MORE_ACTION_SHOW_ADMIN_POLICIES);
     }
 
     @Test
     public void getAdminSupportTitleResource_noRestriction_works() {
-        ManagedDeviceActionDisabledByAdminController mController = createController();
+        ManagedDeviceActionDisabledByAdminController controller = createController();
 
-        assertThat(mController.getAdminSupportTitle(null))
+        assertThat(controller.getAdminSupportTitle(null))
                 .isEqualTo(DEFAULT_DISABLED_BY_POLICY_TITLE);
     }
 
     @Test
     public void getAdminSupportTitleResource_withRestriction_works() {
-        ManagedDeviceActionDisabledByAdminController mController = createController();
+        ManagedDeviceActionDisabledByAdminController controller = createController();
 
-        assertThat(mController.getAdminSupportTitle(RESTRICTION))
+        assertThat(controller.getAdminSupportTitle(RESTRICTION))
                 .isEqualTo(SUPPORT_TITLE_FOR_RESTRICTION);
     }
 
     @Test
     public void getAdminSupportContentString_withSupportMessage_returnsSupportMessage() {
-        ManagedDeviceActionDisabledByAdminController mController = createController();
+        ManagedDeviceActionDisabledByAdminController controller = createController();
 
-        assertThat(mController.getAdminSupportContentString(mActivity, SUPPORT_MESSAGE))
+        assertThat(controller.getAdminSupportContentString(mActivity, SUPPORT_MESSAGE))
                 .isEqualTo(SUPPORT_MESSAGE);
     }
 
     @Test
     public void getAdminSupportContentString_noSupportMessage_returnsDefault() {
-        ManagedDeviceActionDisabledByAdminController mController = createController();
+        ManagedDeviceActionDisabledByAdminController controller = createController();
 
-        assertThat(mController.getAdminSupportContentString(mActivity, /* supportMessage= */ null))
+        assertThat(controller.getAdminSupportContentString(mActivity, /* supportMessage= */ null))
                 .isEqualTo(DEFAULT_DISABLED_BY_POLICY_CONTENT);
     }
 
     private ManagedDeviceActionDisabledByAdminController createController() {
-        return createController(mLauncher, /* url= */ null);
+        return createController(/* url= */ null);
     }
 
-    private ManagedDeviceActionDisabledByAdminController createController(
-            ActionDisabledLearnMoreButtonLauncher buttonHelper, String url) {
+    private ManagedDeviceActionDisabledByAdminController createController(String url) {
         ManagedDeviceActionDisabledByAdminController controller =
                 new ManagedDeviceActionDisabledByAdminController(
-                        buttonHelper,
                         new FakeDeviceAdminStringProvider(url));
+        controller.initialize(mTestUtils.createLearnMoreButtonLauncher());
         controller.updateEnforcedAdmin(ENFORCED_ADMIN, ENFORCEMENT_ADMIN_USER_ID);
         return controller;
     }
diff --git a/packages/SystemUI/animation/Android.bp b/packages/SystemUI/animation/Android.bp
index 1b15d20..761b1f4 100644
--- a/packages/SystemUI/animation/Android.bp
+++ b/packages/SystemUI/animation/Android.bp
@@ -36,6 +36,7 @@
 
     static_libs: [
         "PluginCoreLib",
+        "WindowManager-Shell",
     ],
 
     manifest: "AndroidManifest.xml",
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
index 4f179c4..d185ba36 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
@@ -5,13 +5,18 @@
 import android.animation.ValueAnimator
 import android.app.ActivityManager
 import android.app.ActivityTaskManager
+import android.app.AppGlobals
 import android.app.PendingIntent
 import android.content.Context
 import android.graphics.Matrix
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffXfermode
 import android.graphics.Rect
 import android.graphics.RectF
+import android.graphics.drawable.GradientDrawable
 import android.os.Looper
 import android.os.RemoteException
+import android.os.UserHandle
 import android.util.Log
 import android.util.MathUtils
 import android.view.IRemoteAnimationFinishedCallback
@@ -26,6 +31,8 @@
 import android.view.animation.PathInterpolator
 import com.android.internal.annotations.VisibleForTesting
 import com.android.internal.policy.ScreenDecorationsUtils
+import com.android.wm.shell.startingsurface.SplashscreenContentDrawer
+import com.android.wm.shell.startingsurface.SplashscreenContentDrawer.SplashScreenWindowAttrs
 import kotlin.math.roundToInt
 
 /**
@@ -40,9 +47,9 @@
 
     companion object {
         const val ANIMATION_DURATION = 500L
-        const val ANIMATION_DURATION_FADE_OUT_CONTENT = 183L
-        const val ANIMATION_DURATION_FADE_IN_WINDOW = 217L
-        const val ANIMATION_DELAY_FADE_IN_WINDOW = 167L
+        private const val ANIMATION_DURATION_FADE_OUT_CONTENT = 150L
+        private const val ANIMATION_DURATION_FADE_IN_WINDOW = 183L
+        private const val ANIMATION_DELAY_FADE_IN_WINDOW = ANIMATION_DURATION_FADE_OUT_CONTENT
         private const val ANIMATION_DURATION_NAV_FADE_IN = 266L
         private const val ANIMATION_DURATION_NAV_FADE_OUT = 133L
         private const val ANIMATION_DELAY_NAV_FADE_IN =
@@ -54,6 +61,8 @@
         private val NAV_FADE_IN_INTERPOLATOR = PathInterpolator(0f, 0f, 0f, 1f)
         private val NAV_FADE_OUT_INTERPOLATOR = PathInterpolator(0.2f, 0f, 1f, 1f)
 
+        private val SRC_MODE = PorterDuffXfermode(PorterDuff.Mode.SRC)
+
         /**
          * Given the [linearProgress] of a launch animation, return the linear progress of the
          * sub-animation starting [delay] ms after the launch animation and that lasts [duration].
@@ -68,6 +77,8 @@
         }
     }
 
+    private val packageManager = AppGlobals.getPackageManager()
+
     /** The interpolator used for the width, height, Y position and corner radius. */
     private val animationInterpolator = AnimationUtils.loadInterpolator(context,
             R.interpolator.launch_animation_interpolator_y)
@@ -76,6 +87,8 @@
     private val animationInterpolatorX = AnimationUtils.loadInterpolator(context,
             R.interpolator.launch_animation_interpolator_x)
 
+    private val cornerRadii = FloatArray(8)
+
     /**
      * Start an intent and animate the opening window. The intent will be started by running
      * [intentStarter], which should use the provided [RemoteAnimationAdapter] and return the launch
@@ -288,10 +301,7 @@
         var right: Int,
 
         var topCornerRadius: Float = 0f,
-        var bottomCornerRadius: Float = 0f,
-
-        var contentAlpha: Float = 1f,
-        var backgroundAlpha: Float = 1f
+        var bottomCornerRadius: Float = 0f
     ) {
         private val startTop = top
         private val startBottom = bottom
@@ -331,6 +341,9 @@
 
         val centerY: Float
             get() = top + height / 2f
+
+        /** Whether the expanded view should be visible or hidden. */
+        var visible: Boolean = true
     }
 
     @VisibleForTesting
@@ -452,22 +465,39 @@
                 0f
             }
 
+            // We add an extra layer with the same color as the app splash screen background color,
+            // which is usually the same color of the app background. We first fade in this layer
+            // to hide the expanding view, then we fade it out with SRC mode to draw a hole in the
+            // launch container and reveal the opening window.
+            val windowBackgroundColor = extractSplashScreenBackgroundColor(window)
+            val windowBackgroundLayer = GradientDrawable().apply {
+                setColor(windowBackgroundColor)
+                alpha = 0
+            }
+
             // Update state.
             val animator = ValueAnimator.ofFloat(0f, 1f)
             this.animator = animator
             animator.duration = ANIMATION_DURATION
             animator.interpolator = Interpolators.LINEAR
 
+            val launchContainerOverlay = launchContainer.overlay
             animator.addListener(object : AnimatorListenerAdapter() {
                 override fun onAnimationStart(animation: Animator?, isReverse: Boolean) {
                     Log.d(TAG, "Animation started")
                     controller.onLaunchAnimationStart(isExpandingFullyAbove)
+
+                    // Add the drawable to the launch container overlay. Overlays always draw
+                    // drawables after views, so we know that it will be drawn above any view added
+                    // by the controller.
+                    launchContainerOverlay.add(windowBackgroundLayer)
                 }
 
                 override fun onAnimationEnd(animation: Animator?) {
                     Log.d(TAG, "Animation ended")
                     iCallback?.invoke()
                     controller.onLaunchAnimationEnd(isExpandingFullyAbove)
+                    launchContainerOverlay.remove(windowBackgroundLayer)
                 }
             })
 
@@ -491,24 +521,61 @@
                 state.bottomCornerRadius =
                     MathUtils.lerp(startBottomCornerRadius, endRadius, progress)
 
-                val contentAlphaProgress = getProgress(linearProgress, 0,
-                        ANIMATION_DURATION_FADE_OUT_CONTENT)
-                state.contentAlpha =
-                        1 - CONTENT_FADE_OUT_INTERPOLATOR.getInterpolation(contentAlphaProgress)
-
-                val backgroundAlphaProgress = getProgress(linearProgress,
-                        ANIMATION_DELAY_FADE_IN_WINDOW, ANIMATION_DURATION_FADE_IN_WINDOW)
-                state.backgroundAlpha =
-                        1 - WINDOW_FADE_IN_INTERPOLATOR.getInterpolation(backgroundAlphaProgress)
+                // The expanding view can/should be hidden once it is completely coverred by the
+                // windowBackgroundLayer.
+                state.visible =
+                        getProgress(linearProgress, 0, ANIMATION_DURATION_FADE_OUT_CONTENT) < 1
 
                 applyStateToWindow(window, state)
+                applyStateToWindowBackgroundLayer(windowBackgroundLayer, state, linearProgress)
                 navigationBar?.let { applyStateToNavigationBar(it, state, linearProgress) }
+
+                // If we started expanding the view, we make it 1 pixel smaller on all sides to
+                // avoid artefacts on the corners caused by anti-aliasing of the view background and
+                // the window background layer.
+                if (state.top != startTop && state.left != startLeft &&
+                        state.bottom != startBottom && state.right != startRight) {
+                    state.top += 1
+                    state.left += 1
+                    state.right -= 1
+                    state.bottom -= 1
+                }
                 controller.onLaunchAnimationProgress(state, progress, linearProgress)
             }
 
             animator.start()
         }
 
+        /** Extract the background color of the app splash screen. */
+        private fun extractSplashScreenBackgroundColor(window: RemoteAnimationTarget): Int {
+            val taskInfo = window.taskInfo
+            val windowPackage = taskInfo.topActivity.packageName
+            val userId = taskInfo.userId
+            val windowContext = context.createPackageContextAsUser(
+                    windowPackage, Context.CONTEXT_RESTRICTED, UserHandle.of(userId))
+            val activityInfo = taskInfo.topActivityInfo
+            val splashScreenThemeName = packageManager.getSplashScreenTheme(windowPackage, userId)
+            val splashScreenThemeId = if (splashScreenThemeName != null) {
+                windowContext.resources.getIdentifier(splashScreenThemeName, null, null)
+            } else {
+                0
+            }
+
+            val themeResId = when {
+                splashScreenThemeId != 0 -> splashScreenThemeId
+                activityInfo.themeResource != 0 -> activityInfo.themeResource
+                else -> com.android.internal.R.style.Theme_DeviceDefault_DayNight
+            }
+
+            if (themeResId != windowContext.themeResId) {
+                windowContext.setTheme(themeResId)
+            }
+
+            val windowAttrs = SplashScreenWindowAttrs()
+            SplashscreenContentDrawer.getWindowAttrs(windowContext, windowAttrs)
+            return SplashscreenContentDrawer.peekWindowBGColor(windowContext, windowAttrs)
+        }
+
         private fun applyStateToWindow(window: RemoteAnimationTarget, state: State) {
             val screenBounds = window.screenSpaceBounds
             val centerX = (screenBounds.left + screenBounds.right) / 2f
@@ -563,6 +630,41 @@
             transactionApplier.scheduleApply(params)
         }
 
+        private fun applyStateToWindowBackgroundLayer(
+            drawable: GradientDrawable,
+            state: State,
+            linearProgress: Float
+        ) {
+            // Update position.
+            drawable.setBounds(state.left, state.top, state.right, state.bottom)
+
+            // Update radius.
+            cornerRadii[0] = state.topCornerRadius
+            cornerRadii[1] = state.topCornerRadius
+            cornerRadii[2] = state.topCornerRadius
+            cornerRadii[3] = state.topCornerRadius
+            cornerRadii[4] = state.bottomCornerRadius
+            cornerRadii[5] = state.bottomCornerRadius
+            cornerRadii[6] = state.bottomCornerRadius
+            cornerRadii[7] = state.bottomCornerRadius
+            drawable.cornerRadii = cornerRadii
+
+            // We first fade in the background layer to hide the expanding view, then fade it out
+            // with SRC mode to draw a hole punch in the status bar and reveal the opening window.
+            val fadeInProgress = getProgress(linearProgress, 0, ANIMATION_DURATION_FADE_OUT_CONTENT)
+            if (fadeInProgress < 1) {
+                val alpha = CONTENT_FADE_OUT_INTERPOLATOR.getInterpolation(fadeInProgress)
+                drawable.alpha = (alpha * 0xFF).roundToInt()
+                drawable.setXfermode(null)
+            } else {
+                val fadeOutProgress = getProgress(linearProgress,
+                        ANIMATION_DELAY_FADE_IN_WINDOW, ANIMATION_DURATION_FADE_IN_WINDOW)
+                val alpha = 1 - WINDOW_FADE_IN_INTERPOLATOR.getInterpolation(fadeOutProgress)
+                drawable.alpha = (alpha * 0xFF).roundToInt()
+                drawable.setXfermode(SRC_MODE)
+            }
+        }
+
         private fun applyStateToNavigationBar(
             navigationBar: RemoteAnimationTarget,
             state: State,
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt
index ce9feed..4b655a1 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt
@@ -4,8 +4,6 @@
 import android.graphics.ColorFilter
 import android.graphics.Matrix
 import android.graphics.PixelFormat
-import android.graphics.PorterDuff
-import android.graphics.PorterDuffXfermode
 import android.graphics.Rect
 import android.graphics.drawable.Drawable
 import android.graphics.drawable.GradientDrawable
@@ -111,9 +109,7 @@
     }
 
     override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
-        backgroundView = FrameLayout(launchContainer.context).apply {
-            forceHasOverlappingRendering(false)
-        }
+        backgroundView = FrameLayout(launchContainer.context)
         launchContainerOverlay.add(backgroundView)
 
         // We wrap the ghosted view background and use it to draw the expandable background. Its
@@ -125,9 +121,7 @@
 
         // Create a ghost of the view that will be moving and fading out. This allows to fade out
         // the content before fading out the background.
-        ghostView = GhostView.addGhost(ghostedView, launchContainer).apply {
-            setLayerType(View.LAYER_TYPE_HARDWARE, null)
-        }
+        ghostView = GhostView.addGhost(ghostedView, launchContainer)
 
         val matrix = ghostView?.animationMatrix ?: Matrix.IDENTITY_MATRIX
         matrix.getValues(initialGhostViewMatrixValues)
@@ -139,7 +133,18 @@
         linearProgress: Float
     ) {
         val ghostView = this.ghostView!!
-        ghostView.alpha = state.contentAlpha
+        val backgroundView = this.backgroundView!!
+
+        if (!state.visible) {
+            if (ghostView.visibility == View.VISIBLE) {
+                // Making the ghost view invisible will make the ghosted view visible, so order is
+                // important here.
+                ghostView.visibility = View.INVISIBLE
+                ghostedView.visibility = View.INVISIBLE
+                backgroundView.visibility = View.INVISIBLE
+            }
+            return
+        }
 
         val scale = min(state.widthRatio, state.heightRatio)
         ghostViewMatrix.setValues(initialGhostViewMatrixValues)
@@ -150,14 +155,12 @@
         )
         ghostView.animationMatrix = ghostViewMatrix
 
-        val backgroundView = this.backgroundView!!
         backgroundView.top = state.top
         backgroundView.bottom = state.bottom
         backgroundView.left = state.left
         backgroundView.right = state.right
 
         val backgroundDrawable = backgroundDrawable!!
-        backgroundDrawable.alpha = (0xFF * state.backgroundAlpha).toInt()
         backgroundDrawable.wrapped?.let {
             setBackgroundCornerRadius(it, state.topCornerRadius, state.bottomCornerRadius)
         }
@@ -168,6 +171,7 @@
 
         GhostView.removeGhost(ghostedView)
         launchContainerOverlay.remove(backgroundView)
+        ghostedView.visibility = View.VISIBLE
         ghostedView.invalidate()
     }
 
@@ -203,10 +207,6 @@
     }
 
     private class WrappedDrawable(val wrapped: Drawable?) : Drawable() {
-        companion object {
-            private val SRC_MODE = PorterDuffXfermode(PorterDuff.Mode.SRC)
-        }
-
         private var currentAlpha = 0xFF
         private var previousBounds = Rect()
 
@@ -220,7 +220,6 @@
 
             wrapped.alpha = currentAlpha
             wrapped.bounds = bounds
-            setXfermode(wrapped, SRC_MODE)
             applyBackgroundRadii()
 
             wrapped.draw(canvas)
@@ -230,7 +229,6 @@
             // background.
             wrapped.alpha = 0
             wrapped.bounds = previousBounds
-            setXfermode(wrapped, null)
             restoreBackgroundRadii()
         }
 
@@ -257,27 +255,6 @@
             wrapped?.colorFilter = filter
         }
 
-        private fun setXfermode(background: Drawable, mode: PorterDuffXfermode?) {
-            if (background is InsetDrawable) {
-                background.drawable?.let { setXfermode(it, mode) }
-                return
-            }
-
-            if (background !is LayerDrawable) {
-                background.setXfermode(mode)
-                return
-            }
-
-            // We set the xfermode on the first layer that is not a mask. Most of the time it will
-            // be the "background layer".
-            for (i in 0 until background.numberOfLayers) {
-                if (background.getId(i) != android.R.id.mask) {
-                    background.getDrawable(i).setXfermode(mode)
-                    break
-                }
-            }
-        }
-
         fun setBackgroundRadius(topCornerRadius: Float, bottomCornerRadius: Float) {
             updateRadii(cornerRadii, topCornerRadius, bottomCornerRadius)
             invalidateSelf()
diff --git a/packages/SystemUI/docs/qs-tiles.md b/packages/SystemUI/docs/qs-tiles.md
index 5216209..89c28a0 100644
--- a/packages/SystemUI/docs/qs-tiles.md
+++ b/packages/SystemUI/docs/qs-tiles.md
@@ -305,6 +305,7 @@
     * Inject a `Provider` for the tile created before.
     * Add a case to the `switch` with a unique String spec for the chosen tile.
 5. In [SystemUI/res/values/config.xml](/packages/SystemUI/res/values/config.xml), modify `quick_settings_tiles_stock` and add the spec defined in the previous step. If necessary, add it also to `quick_settings_tiles_default`. The first one contains a list of all the tiles that SystemUI knows how to create (to show to the user in the customization screen). The second one contains only the default tiles that the user will experience on a fresh boot or after they reset their tiles.
+6. In [SystemUI/res/values/tiles_states_strings.xml](/packages/SystemUI/res/values/tiles_states_strings.xml), add a new array for your tile. The name has to be `tile_states_<spec>`. Use a good description to help the translators.
 
 #### Abstract methods in QSTileImpl
 
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
index f1c1477..77018d7 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
@@ -166,11 +166,13 @@
         public boolean handlesLongClick = true;
         public boolean showRippleEffect = true;
         public Drawable sideViewCustomDrawable;
+        public String spec;
 
         public boolean copyTo(State other) {
             if (other == null) throw new IllegalArgumentException();
             if (!other.getClass().equals(getClass())) throw new IllegalArgumentException();
-            final boolean changed = !Objects.equals(other.icon, icon)
+            final boolean changed = !Objects.equals(other.spec, spec)
+                    || !Objects.equals(other.icon, icon)
                     || !Objects.equals(other.iconSupplier, iconSupplier)
                     || !Objects.equals(other.label, label)
                     || !Objects.equals(other.secondaryLabel, secondaryLabel)
@@ -188,6 +190,7 @@
                     || !Objects.equals(other.handlesLongClick, handlesLongClick)
                     || !Objects.equals(other.showRippleEffect, showRippleEffect)
                     || !Objects.equals(other.sideViewCustomDrawable, sideViewCustomDrawable);
+            other.spec = spec;
             other.icon = icon;
             other.iconSupplier = iconSupplier;
             other.label = label;
@@ -216,6 +219,7 @@
         // This string may be used for CTS testing of tiles, so removing elements is discouraged.
         protected StringBuilder toStringBuilder() {
             final StringBuilder sb = new StringBuilder(getClass().getSimpleName()).append('[');
+            sb.append("spec=").append(spec);
             sb.append(",icon=").append(icon);
             sb.append(",iconSupplier=").append(iconSupplier);
             sb.append(",label=").append(label);
diff --git a/packages/SystemUI/res/layout/keyguard_bottom_area.xml b/packages/SystemUI/res/layout/keyguard_bottom_area.xml
index 0dc1473..80b7d1f 100644
--- a/packages/SystemUI/res/layout/keyguard_bottom_area.xml
+++ b/packages/SystemUI/res/layout/keyguard_bottom_area.xml
@@ -53,6 +53,7 @@
             android:paddingEnd="@dimen/keyguard_indication_text_padding"
             android:textAppearance="@style/TextAppearance.Keyguard.BottomArea"
             android:alpha=".8"
+            android:accessibilityLiveRegion="polite"
             android:visibility="gone"/>
 
     </LinearLayout>
diff --git a/packages/SystemUI/res/layout/privacy_dialog.xml b/packages/SystemUI/res/layout/privacy_dialog.xml
index 720ae8db..459fb66 100644
--- a/packages/SystemUI/res/layout/privacy_dialog.xml
+++ b/packages/SystemUI/res/layout/privacy_dialog.xml
@@ -24,8 +24,6 @@
     android:layout_marginEnd="@dimen/ongoing_appops_dialog_side_margins"
     android:layout_marginTop="8dp"
     android:orientation="vertical"
-    android:paddingLeft="@dimen/ongoing_appops_dialog_side_padding"
-    android:paddingRight="@dimen/ongoing_appops_dialog_side_padding"
     android:paddingBottom="12dp"
     android:paddingTop="8dp"
     android:background="@drawable/qs_dialog_bg"
diff --git a/packages/SystemUI/res/layout/privacy_dialog_item.xml b/packages/SystemUI/res/layout/privacy_dialog_item.xml
index b91fb29c..7c8945e 100644
--- a/packages/SystemUI/res/layout/privacy_dialog_item.xml
+++ b/packages/SystemUI/res/layout/privacy_dialog_item.xml
@@ -23,6 +23,9 @@
     android:orientation="horizontal"
     android:layout_marginTop="4dp"
     android:importantForAccessibility="yes"
+    android:background="?android:attr/selectableItemBackground"
+    android:paddingLeft="@dimen/ongoing_appops_dialog_side_padding"
+    android:paddingRight="@dimen/ongoing_appops_dialog_side_padding"
     android:focusable="true"
     >
     <!-- 4dp marginTop makes 20dp minimum between icons -->
diff --git a/packages/SystemUI/res/layout/qs_tile_side_icon.xml b/packages/SystemUI/res/layout/qs_tile_side_icon.xml
index 1ae0a1c..f1b7259 100644
--- a/packages/SystemUI/res/layout/qs_tile_side_icon.xml
+++ b/packages/SystemUI/res/layout/qs_tile_side_icon.xml
@@ -23,7 +23,7 @@
     <ImageView
         android:id="@+id/customDrawable"
         android:layout_width="wrap_content"
-        android:layout_height="@dimen/qs_icon_size"
+        android:layout_height="@dimen/qs_side_view_size"
         android:layout_marginEnd="@dimen/qs_drawable_end_margin"
         android:adjustViewBounds="true"
         android:scaleType="fitCenter"
@@ -39,4 +39,4 @@
         android:visibility="gone"
         android:importantForAccessibility="no"
     />
-</FrameLayout>
\ No newline at end of file
+</FrameLayout>
diff --git a/packages/SystemUI/res/layout/status_bar_expanded.xml b/packages/SystemUI/res/layout/status_bar_expanded.xml
index fb39d3e..dbbf641 100644
--- a/packages/SystemUI/res/layout/status_bar_expanded.xml
+++ b/packages/SystemUI/res/layout/status_bar_expanded.xml
@@ -64,18 +64,6 @@
         android:clipToPadding="false"
         android:clipChildren="false">
 
-        <com.android.systemui.scrim.ScrimView
-            android:id="@+id/scrim_notifications"
-            android:layout_width="0dp"
-            android:layout_height="0dp"
-            android:importantForAccessibility="no"
-            systemui:ignoreRightInset="true"
-            systemui:layout_constraintStart_toStartOf="parent"
-            systemui:layout_constraintEnd_toEndOf="parent"
-            systemui:layout_constraintTop_toTopOf="parent"
-            systemui:layout_constraintBottom_toBottomOf="parent"
-        />
-
         <include
             layout="@layout/keyguard_status_view"
             android:visibility="gone"/>
diff --git a/packages/SystemUI/res/layout/super_notification_shade.xml b/packages/SystemUI/res/layout/super_notification_shade.xml
index 08284a0..bea50e8 100644
--- a/packages/SystemUI/res/layout/super_notification_shade.xml
+++ b/packages/SystemUI/res/layout/super_notification_shade.xml
@@ -51,6 +51,14 @@
         sysui:ignoreRightInset="true"
         />
 
+    <com.android.systemui.scrim.ScrimView
+        android:id="@+id/scrim_notifications"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:importantForAccessibility="no"
+        sysui:ignoreRightInset="true"
+        />
+
     <com.android.systemui.statusbar.LightRevealScrim
             android:id="@+id/light_reveal_scrim"
             android:layout_width="match_parent"
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index ab606ba..c0ebff6 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -561,6 +561,7 @@
     <dimen name="qs_tile_icon_background_stroke_width">-1dp</dimen>
     <dimen name="qs_tile_background_size">56dp</dimen>
     <dimen name="qs_icon_size">20dp</dimen>
+    <dimen name="qs_side_view_size">28dp</dimen>
     <dimen name="qs_label_container_margin">10dp</dimen>
     <dimen name="qs_quick_tile_size">60dp</dimen>
     <dimen name="qs_tile_padding">12dp</dimen>
diff --git a/packages/SystemUI/res/values/tiles_states_strings.xml b/packages/SystemUI/res/values/tiles_states_strings.xml
new file mode 100644
index 0000000..5ac7c1d
--- /dev/null
+++ b/packages/SystemUI/res/values/tiles_states_strings.xml
@@ -0,0 +1,284 @@
+<!--
+  ~ Copyright (C) 2021 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.
+  -->
+
+<!-- This resources set the default subtitle for tiles. This way, each tile can be translated
+     separately.
+     The indices in the array correspond to the state values in QSTile:
+      * STATE_UNAVAILABLE
+      * STATE_INACTIVE
+      * STATE_ACTIVE
+     This subtitle is shown when the tile is in that particular state but does not set its own
+     subtitle, so some of these may never appear on screen. They should still be translated as if
+     they could appear.
+-->
+<resources>
+    <!-- Default names for tiles states: unavailable, off, on.
+         This subtitle is shown when the tile is in that particular state but does not set its own
+         subtitle, so some of these may never appear on screen. They should still be translated as
+         if they could appear. [CHAR LIMIT=32] -->
+    <string-array name="tile_states_default">
+        <item>@string/tile_unavailable</item>
+        <item>@string/switch_bar_off</item>
+        <item>@string/switch_bar_on</item>
+    </string-array>
+
+    <!-- State names for internet tile: unavailable, off, on.
+         This subtitle is shown when the tile is in that particular state but does not set its own
+         subtitle, so some of these may never appear on screen. They should still be translated as
+         if they could appear. [CHAR LIMIT=32] -->
+    <string-array name="tile_states_internet">
+        <item>@string/tile_unavailable</item>
+        <item>@string/switch_bar_off</item>
+        <item>@string/switch_bar_on</item>
+    </string-array>
+
+    <!-- State names for wifi tile: unavailable, off, on.
+         This subtitle is shown when the tile is in that particular state but does not set its own
+         subtitle, so some of these may never appear on screen. They should still be translated as
+         if they could appear. [CHAR LIMIT=32] -->
+    <string-array name="tile_states_wifi">
+        <item>@string/tile_unavailable</item>
+        <item>@string/switch_bar_off</item>
+        <item>@string/switch_bar_on</item>
+    </string-array>
+
+    <!-- State names for cell (data) tile: unavailable, off, on.
+         This subtitle is shown when the tile is in that particular state but does not set its own
+         subtitle, so some of these may never appear on screen. They should still be translated as
+         if they could appear.[CHAR LIMIT=32] -->
+    <string-array name="tile_states_cell">
+        <item>@string/tile_unavailable</item>
+        <item>@string/switch_bar_off</item>
+        <item>@string/switch_bar_on</item>
+    </string-array>
+
+    <!-- State names for battery (saver) tile: unavailable, off, on.
+         This subtitle is shown when the tile is in that particular state but does not set its own
+         subtitle, so some of these may never appear on screen. They should still be translated as
+         if they could appear. [CHAR LIMIT=32] -->
+    <string-array name="tile_states_battery">
+        <item>@string/tile_unavailable</item>
+        <item>@string/switch_bar_off</item>
+        <item>@string/switch_bar_on</item>
+    </string-array>
+
+    <!-- State names for dnd (Do not disturb) tile: unavailable, off, on.
+         This subtitle is shown when the tile is in that particular state but does not set its own
+         subtitle, so some of these may never appear on screen. They should still be translated as
+         if they could appear. [CHAR LIMIT=32] -->
+    <string-array name="tile_states_dnd">
+        <item>@string/tile_unavailable</item>
+        <item>@string/switch_bar_off</item>
+        <item>@string/switch_bar_on</item>
+    </string-array>
+
+    <!-- State names for flashlight tile: unavailable, off, on.
+         This subtitle is shown when the tile is in that particular state but does not set its own
+         subtitle, so some of these may never appear on screen. They should still be translated as
+         if they could appear. [CHAR LIMIT=32] -->
+    <string-array name="tile_states_flashlight">
+        <item>@string/tile_unavailable</item>
+        <item>@string/switch_bar_off</item>
+        <item>@string/switch_bar_on</item>
+    </string-array>
+
+    <!-- State names for rotation (lock) tile: unavailable, off, on.
+         This subtitle is shown when the tile is in that particular state but does not set its own
+         subtitle, so some of these may never appear on screen. They should still be translated as
+         if they could appear. [CHAR LIMIT=32] -->
+    <string-array name="tile_states_rotation">
+        <item>@string/tile_unavailable</item>
+        <item>@string/switch_bar_off</item>
+        <item>@string/switch_bar_on</item>
+    </string-array>
+
+    <!-- State names for bt (bluetooth) tile: unavailable, off, on.
+         This subtitle is shown when the tile is in that particular state but does not set its own
+         subtitle, so some of these may never appear on screen. They should still be translated as
+         if they could appear. [CHAR LIMIT=32] -->
+    <string-array name="tile_states_bt">
+        <item>@string/tile_unavailable</item>
+        <item>@string/switch_bar_off</item>
+        <item>@string/switch_bar_on</item>
+    </string-array>
+
+    <!-- State names for airplane tile: unavailable, off, on [CHAR LIMIT=32] -->
+    <string-array name="tile_states_airplane">
+        <item>@string/tile_unavailable</item>
+        <item>@string/switch_bar_off</item>
+        <item>@string/switch_bar_on</item>
+    </string-array>
+
+    <!-- State names for location tile: unavailable, off, on.
+         This subtitle is shown when the tile is in that particular state but does not set its own
+         subtitle, so some of these may never appear on screen. They should still be translated as
+         if they could appear. [CHAR LIMIT=32] -->
+    <string-array name="tile_states_location">
+        <item>@string/tile_unavailable</item>
+        <item>@string/switch_bar_off</item>
+        <item>@string/switch_bar_on</item>
+    </string-array>
+
+    <!-- State names for hotspot tile: unavailable, off, on.
+         This subtitle is shown when the tile is in that particular state but does not set its own
+         subtitle, so some of these may never appear on screen. They should still be translated as
+         if they could appear. [CHAR LIMIT=32] -->
+    <string-array name="tile_states_hotspot">
+        <item>@string/tile_unavailable</item>
+        <item>@string/switch_bar_off</item>
+        <item>@string/switch_bar_on</item>
+    </string-array>
+
+    <!-- State names for (color) inversion tile: unavailable, off, on.
+         This subtitle is shown when the tile is in that particular state but does not set its own
+         subtitle, so some of these may never appear on screen. They should still be translated as
+         if they could appear. [CHAR LIMIT=32] -->
+    <string-array name="tile_states_inversion">
+        <item>@string/tile_unavailable</item>
+        <item>@string/switch_bar_off</item>
+        <item>@string/switch_bar_on</item>
+    </string-array>
+
+    <!-- State names for (data) saver tile: unavailable, off, on.
+         This subtitle is shown when the tile is in that particular state but does not set its own
+         subtitle, so some of these may never appear on screen. They should still be translated as
+         if they could appear. [CHAR LIMIT=32] -->
+    <string-array name="tile_states_saver">
+        <item>@string/tile_unavailable</item>
+        <item>@string/switch_bar_off</item>
+        <item>@string/switch_bar_on</item>
+    </string-array>
+
+    <!-- State names for dark (mode) tile: unavailable, off, on.
+         This subtitle is shown when the tile is in that particular state but does not set its own
+         subtitle, so some of these may never appear on screen. They should still be translated as
+         if they could appear. [CHAR LIMIT=32] -->
+    <string-array name="tile_states_dark">
+        <item>@string/tile_unavailable</item>
+        <item>@string/switch_bar_off</item>
+        <item>@string/switch_bar_on</item>
+    </string-array>
+
+    <!-- State names for work (mode) tile: unavailable, off, on.
+         This subtitle is shown when the tile is in that particular state but does not set its own
+         subtitle, so some of these may never appear on screen. They should still be translated as
+         if they could appear. [CHAR LIMIT=32] -->
+    <string-array name="tile_states_work">
+        <item>@string/tile_unavailable</item>
+        <item>@string/switch_bar_off</item>
+        <item>@string/switch_bar_on</item>
+    </string-array>
+
+    <!-- State names for cast tile: unavailable, off, on.
+         This subtitle is shown when the tile is in that particular state but does not set its own
+         subtitle, so some of these may never appear on screen. They should still be translated as
+         if they could appear. [CHAR LIMIT=32] -->
+    <string-array name="tile_states_cast">
+        <item>@string/tile_unavailable</item>
+        <item>@string/switch_bar_off</item>
+        <item>@string/switch_bar_on</item>
+    </string-array>
+
+    <!-- State names for night (light) tile: unavailable, off, on.
+         This subtitle is shown when the tile is in that particular state but does not set its own
+         subtitle, so some of these may never appear on screen. They should still be translated as
+         if they could appear. [CHAR LIMIT=32] -->
+    <string-array name="tile_states_night">
+        <item>@string/tile_unavailable</item>
+        <item>@string/switch_bar_off</item>
+        <item>@string/switch_bar_on</item>
+    </string-array>
+
+    <!-- State names for screenrecord tile: unavailable, off, on.
+         This subtitle is shown when the tile is in that particular state but does not set its own
+         subtitle, so some of these may never appear on screen. They should still be translated as
+         if they could appear. [CHAR LIMIT=32] -->
+    <string-array name="tile_states_screenrecord">
+        <item>@string/tile_unavailable</item>
+        <item>@string/switch_bar_off</item>
+        <item>@string/switch_bar_on</item>
+    </string-array>
+
+    <!-- State names for reverse (charging) tile: unavailable, off, on.
+         This subtitle is shown when the tile is in that particular state but does not set its own
+         subtitle, so some of these may never appear on screen. They should still be translated as
+         if they could appear. [CHAR LIMIT=32] -->
+    <string-array name="tile_states_reverse">
+        <item>@string/tile_unavailable</item>
+        <item>@string/switch_bar_off</item>
+        <item>@string/switch_bar_on</item>
+    </string-array>
+
+    <!-- State names for reduce_brightness tile: unavailable, off, on.
+         This subtitle is shown when the tile is in that particular state but does not set its own
+         subtitle, so some of these may never appear on screen. They should still be translated as
+         if they could appear. [CHAR LIMIT=32] -->
+    <string-array name="tile_states_reduce_brightness">
+        <item>@string/tile_unavailable</item>
+        <item>@string/switch_bar_off</item>
+        <item>@string/switch_bar_on</item>
+    </string-array>
+
+    <!-- State names for cameratoggle tile: unavailable, off, on.
+         This subtitle is shown when the tile is in that particular state but does not set its own
+         subtitle, so some of these may never appear on screen. They should still be translated as
+         if they could appear.[CHAR LIMIT=32] -->
+    <string-array name="tile_states_cameratoggle">
+        <item>@string/tile_unavailable</item>
+        <item>@string/switch_bar_off</item>
+        <item>@string/switch_bar_on</item>
+    </string-array>
+
+    <!-- State names for mictoggle tile: unavailable, off, on.
+         This subtitle is shown when the tile is in that particular state but does not set its own
+         subtitle, so some of these may never appear on screen. They should still be translated as
+         if they could appear. [CHAR LIMIT=32] -->
+    <string-array name="tile_states_mictoggle">
+        <item>@string/tile_unavailable</item>
+        <item>@string/switch_bar_off</item>
+        <item>@string/switch_bar_on</item>
+    </string-array>
+
+    <!-- State names for (home) controls tile: unavailable, off, on.
+         This subtitle is shown when the tile is in that particular state but does not set its own
+         subtitle, so some of these may never appear on screen. They should still be translated as
+         if they could appear. [CHAR LIMIT=32] -->
+    <string-array name="tile_states_controls">
+        <item>@string/tile_unavailable</item>
+        <item>@string/switch_bar_off</item>
+        <item>@string/switch_bar_on</item>
+    </string-array>
+
+    <!-- State names for (quick access) wallet tile: unavailable, off, on.
+         This subtitle is shown when the tile is in that particular state but does not set its own
+         subtitle, so some of these may never appear on screen. They should still be translated as
+         if they could appear. [CHAR LIMIT=32] -->
+    <string-array name="tile_states_wallet">
+        <item>@string/tile_unavailable</item>
+        <item>@string/switch_bar_off</item>
+        <item>@string/switch_bar_on</item>
+    </string-array>
+
+    <!-- State names for alarm tile: unavailable, off, on.
+         This subtitle is shown when the tile is in that particular state but does not set its own
+         subtitle, so some of these may never appear on screen. They should still be translated as
+         if they could appear. [CHAR LIMIT=32] -->
+    <string-array name="tile_states_alarm">
+        <item>@string/tile_unavailable</item>
+        <item>@string/switch_bar_off</item>
+        <item>@string/switch_bar_on</item>
+    </string-array>
+</resources>
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java b/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java
index 7cb4846..92af58e 100644
--- a/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java
+++ b/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java
@@ -107,6 +107,21 @@
         }
     };
 
+    private final StatusBarStateController.StateListener mStatusBarStatePersistentListener =
+            new StatusBarStateController.StateListener() {
+                @Override
+                public void onDozeAmountChanged(float linear, float eased) {
+                    boolean noAnimation = (mDozeAmount == 0f && linear == 1f)
+                            || (mDozeAmount == 1f && linear == 0f);
+                    boolean isDozing = linear > mDozeAmount;
+                    mDozeAmount = linear;
+                    if (mIsDozing != isDozing) {
+                        mIsDozing = isDozing;
+                        mView.animateDoze(mIsDozing, !noAnimation);
+                    }
+                }
+            };
+
     private final KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback =
             new KeyguardUpdateMonitorCallback() {
         @Override
@@ -133,14 +148,15 @@
         updateLocale();
         mBroadcastDispatcher.registerReceiver(mLocaleBroadcastReceiver,
                 new IntentFilter(Intent.ACTION_LOCALE_CHANGED));
-        mStatusBarStateController.addCallback(mStatusBarStateListener);
-
         mIsDozing = mStatusBarStateController.isDozing();
         mDozeAmount = mStatusBarStateController.getDozeAmount();
         mBatteryController.addCallback(mBatteryCallback);
         mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback);
         mKeyguardShowing = true;
 
+        mStatusBarStateController.removeCallback(mStatusBarStatePersistentListener);
+        mStatusBarStateController.addCallback(mStatusBarStatePersistentListener);
+
         refreshTime();
         initColors();
         mView.animateDoze(mIsDozing, false);
@@ -149,9 +165,11 @@
     @Override
     protected void onViewDetached() {
         mBroadcastDispatcher.unregisterReceiver(mLocaleBroadcastReceiver);
-        mStatusBarStateController.removeCallback(mStatusBarStateListener);
         mKeyguardUpdateMonitor.removeCallback(mKeyguardUpdateMonitorCallback);
         mBatteryController.removeCallback(mBatteryCallback);
+        if (!mView.isAttachedToWindow()) {
+            mStatusBarStateController.removeCallback(mStatusBarStatePersistentListener);
+        }
     }
 
     /** Animate the clock appearance */
@@ -199,19 +217,4 @@
         mView.setColors(mDozingColor, mLockScreenColor);
         mView.animateDoze(mIsDozing, false);
     }
-
-    private final StatusBarStateController.StateListener mStatusBarStateListener =
-            new StatusBarStateController.StateListener() {
-                @Override
-                public void onDozeAmountChanged(float linear, float eased) {
-                    boolean noAnimation = (mDozeAmount == 0f && linear == 1f)
-                            || (mDozeAmount == 1f && linear == 0f);
-                    boolean isDozing = linear > mDozeAmount;
-                    mDozeAmount = linear;
-                    if (mIsDozing != isDozing) {
-                        mIsDozing = isDozing;
-                        mView.animateDoze(mIsDozing, !noAnimation);
-                    }
-                }
-            };
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index 3e084b7..fbea1e9 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -305,10 +305,6 @@
             // know that it should re-position our SmartSpace.
             if (mKeyguardUnlockAnimationController.isUnlockingWithSmartSpaceTransition()) {
                 mKeyguardUnlockAnimationController.updateLockscreenSmartSpacePosition();
-            } else {
-                // Otherwise, reset Y translation in case it's still offset from a previous shared
-                // element transition.
-                ((View) mSmartspaceView).setTranslationY(0f);
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
index 7b6514a..e8cc5c8 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
@@ -27,6 +27,7 @@
 import com.android.systemui.statusbar.notification.stack.AnimationProperties;
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
 import com.android.systemui.statusbar.phone.DozeParameters;
+import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.util.ViewController;
@@ -66,7 +67,8 @@
             ConfigurationController configurationController,
             DozeParameters dozeParameters,
             KeyguardUnlockAnimationController keyguardUnlockAnimationController,
-            SmartspaceTransitionController smartspaceTransitionController) {
+            SmartspaceTransitionController smartspaceTransitionController,
+            UnlockedScreenOffAnimationController unlockedScreenOffAnimationController) {
         super(keyguardStatusView);
         mKeyguardSliceViewController = keyguardSliceViewController;
         mKeyguardClockSwitchController = keyguardClockSwitchController;
@@ -75,7 +77,7 @@
         mDozeParameters = dozeParameters;
         mKeyguardStateController = keyguardStateController;
         mKeyguardVisibilityHelper = new KeyguardVisibilityHelper(mView, keyguardStateController,
-                dozeParameters);
+                dozeParameters, unlockedScreenOffAnimationController);
         mKeyguardUnlockAnimationController = keyguardUnlockAnimationController;
         mSmartspaceTransitionController = smartspaceTransitionController;
 
@@ -87,7 +89,6 @@
                 // element transition.
                 if (keyguardStateController.isShowing()) {
                     mView.setChildrenAlphaExcludingClockView(1f);
-                    mKeyguardClockSwitchController.setChildrenAlphaExcludingSmartspace(1f);
                 }
             }
         });
@@ -238,13 +239,6 @@
     }
 
     /**
-     * @return {@code true} if we are currently animating the screen off from unlock
-     */
-    public boolean isAnimatingScreenOffFromUnlocked() {
-        return mKeyguardVisibilityHelper.isAnimatingScreenOffFromUnlocked();
-    }
-
-    /**
      * Set the visibility of the keyguard status view based on some new state.
      */
     public void setKeyguardStatusViewVisibility(
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java b/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
index b6a58dc..7edecc8 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
@@ -27,6 +27,7 @@
 import com.android.systemui.statusbar.notification.stack.AnimationProperties;
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
 import com.android.systemui.statusbar.phone.DozeParameters;
+import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
 /**
@@ -38,16 +39,19 @@
     private View mView;
     private final KeyguardStateController mKeyguardStateController;
     private final DozeParameters mDozeParameters;
+    private final UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
     private boolean mKeyguardViewVisibilityAnimating;
     private boolean mLastOccludedState = false;
-    private boolean mAnimatingScreenOff;
     private final AnimationProperties mAnimationProperties = new AnimationProperties();
 
-    public KeyguardVisibilityHelper(View view, KeyguardStateController keyguardStateController,
-            DozeParameters dozeParameters) {
+    public KeyguardVisibilityHelper(View view,
+            KeyguardStateController keyguardStateController,
+            DozeParameters dozeParameters,
+            UnlockedScreenOffAnimationController unlockedScreenOffAnimationController) {
         mView = view;
         mKeyguardStateController = keyguardStateController;
         mDozeParameters = dozeParameters;
+        mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController;
     }
 
     public boolean isVisibilityAnimating() {
@@ -122,32 +126,14 @@
                         .alpha(1f)
                         .withEndAction(mAnimateKeyguardStatusViewVisibleEndRunnable)
                         .start();
-            } else if (mDozeParameters.shouldControlUnlockedScreenOff()) {
+            } else if (mUnlockedScreenOffAnimationController
+                        .isScreenOffLightRevealAnimationPlaying()) {
                 mKeyguardViewVisibilityAnimating = true;
-                mAnimatingScreenOff = true;
 
-                mView.setVisibility(View.VISIBLE);
-                mView.setAlpha(0f);
-                float currentY = mView.getY();
-                mView.setY(currentY - mView.getHeight() * 0.1f);
-                int duration = StackStateAnimator.ANIMATION_DURATION_WAKEUP;
-                int delay = (int) (duration * .6f);
-                // We animate the Y properly separately using the PropertyAnimator, as the panel
-                // view als needs to update the end position.
-                mAnimationProperties.setDuration(duration).setDelay(delay);
-                PropertyAnimator.cancelAnimation(mView, AnimatableProperty.Y);
-                PropertyAnimator.setProperty(mView, AnimatableProperty.Y, currentY,
-                        mAnimationProperties,
-                        true /* animate */);
-
-                mView.animate()
-                        .setStartDelay(delay)
-                        .setDuration(duration)
-                        .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
-                        .alpha(1f)
-                        .withEndAction(mAnimateKeyguardStatusViewVisibleEndRunnable)
-                        .start();
-
+                // Ask the screen off animation controller to animate the keyguard visibility for us
+                // since it may need to be cancelled due to keyguard lifecycle events.
+                mUnlockedScreenOffAnimationController.animateInKeyguard(
+                        mView, mAnimateKeyguardStatusViewVisibleEndRunnable);
             } else {
                 mView.setVisibility(View.VISIBLE);
                 mView.setAlpha(1f);
@@ -172,13 +158,5 @@
 
     private final Runnable mAnimateKeyguardStatusViewVisibleEndRunnable = () -> {
         mKeyguardViewVisibilityAnimating = false;
-        mAnimatingScreenOff = false;
     };
-
-    /**
-     * @return {@code true} if we are currently animating the screen off from unlock
-     */
-    public boolean isAnimatingScreenOffFromUnlocked() {
-        return mAnimatingScreenOff;
-    }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconView.java b/packages/SystemUI/src/com/android/keyguard/LockIconView.java
index 2167876..eff412e 100644
--- a/packages/SystemUI/src/com/android/keyguard/LockIconView.java
+++ b/packages/SystemUI/src/com/android/keyguard/LockIconView.java
@@ -87,6 +87,11 @@
         updateSensorRect(h, w);
     }
 
+    @Override
+    public boolean hasOverlappingRendering() {
+        return false;
+    }
+
     float getLocationTop() {
         return mLockIconCenter.y - mRadius;
     }
diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
index 6b85ba8..ccc4879 100644
--- a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
@@ -451,4 +451,11 @@
 
     private final AccessibilityManager.TouchExplorationStateChangeListener
             mTouchExplorationStateChangeListener = enabled -> updateClickListener();
+
+    /**
+     * Set the alpha of this view.
+     */
+    public void setAlpha(float alpha) {
+        mView.setAlpha(alpha);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
index 1ac1df1..29cd76d 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
@@ -596,10 +596,6 @@
         Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this);
     }
 
-    private void setText(TextView view, CharSequence charSequence) {
-        view.setText(charSequence);
-    }
-
     // Remove all pending icon and text animations
     private void removePendingAnimations() {
         mHandler.removeCallbacks(mResetHelpRunnable);
@@ -688,7 +684,7 @@
      */
     @VisibleForTesting
     void onAttachedToWindowInternal() {
-        setText(mTitleView, mPromptInfo.getTitle());
+        mTitleView.setText(mPromptInfo.getTitle());
 
         if (isDeviceCredentialAllowed()) {
             final CharSequence credentialButtonText;
@@ -718,7 +714,7 @@
             mUseCredentialButton.setText(credentialButtonText);
             mUseCredentialButton.setVisibility(View.VISIBLE);
         } else {
-            setText(mNegativeButton, mPromptInfo.getNegativeButtonText());
+            mNegativeButton.setText(mPromptInfo.getNegativeButtonText());
         }
 
         setTextOrHide(mSubtitleView, mPromptInfo.getSubtitle());
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index 6f1a387..9ec7bd0c 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
@@ -575,7 +575,7 @@
         if (mBiometricView != null) {
             mBiometricView.restoreState(savedState);
         }
-        wm.addView(this, getLayoutParams(mWindowToken));
+        wm.addView(this, getLayoutParams(mWindowToken, mConfig.mPromptInfo.getTitle()));
     }
 
     @Override
@@ -728,11 +728,9 @@
         }
     }
 
-    /**
-     * @param windowToken token for the window
-     * @return
-     */
-    public static WindowManager.LayoutParams getLayoutParams(IBinder windowToken) {
+    @VisibleForTesting
+    static WindowManager.LayoutParams getLayoutParams(IBinder windowToken,
+            CharSequence title) {
         final int windowFlags = WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
                 | WindowManager.LayoutParams.FLAG_SECURE;
         final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
@@ -744,15 +742,11 @@
         lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
         lp.setFitInsetsTypes(lp.getFitInsetsTypes() & ~WindowInsets.Type.ime());
         lp.setTitle("BiometricPrompt");
+        lp.accessibilityTitle = title;
         lp.token = windowToken;
         return lp;
     }
 
-    private boolean hasFaceAndFingerprintSensors() {
-        final int[] ids = findFaceAndFingerprintSensors();
-        return ids[0] >= 0 && ids[1] >= 0;
-    }
-
     // returns [face, fingerprint] sensor ids (id is -1 if not present)
     private int[] findFaceAndFingerprintSensors() {
         int faceSensorId = -1;
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java
index 2b4c1ab..9a32412 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java
@@ -108,6 +108,7 @@
         mStatusBarStateController.addCallback(mStateListener);
 
         mUdfpsRequested = false;
+
         mStatusBarState = mStatusBarStateController.getState();
         mQsExpanded = mKeyguardViewManager.isQsExpanded();
         mInputBouncerHiddenAmount = KeyguardBouncer.EXPANSION_HIDDEN;
@@ -125,7 +126,7 @@
         mFaceDetectRunning = false;
 
         mStatusBarStateController.removeCallback(mStateListener);
-        mKeyguardViewManager.setAlternateAuthInterceptor(null);
+        mKeyguardViewManager.removeAlternateAuthInterceptor(mAlternateAuthInterceptor);
         mTransitioningFromHome = false;
         mKeyguardUpdateMonitor.requestFaceAuthOnOccludingApp(false);
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/Utils.java b/packages/SystemUI/src/com/android/systemui/biometrics/Utils.java
index 076c7cb..4d4e4dd 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/Utils.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/Utils.java
@@ -29,7 +29,6 @@
 import android.hardware.biometrics.PromptInfo;
 import android.hardware.biometrics.SensorPropertiesInternal;
 import android.os.UserManager;
-import android.util.DisplayMetrics;
 import android.view.ViewGroup;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
@@ -50,17 +49,6 @@
     @IntDef({CREDENTIAL_PIN, CREDENTIAL_PATTERN, CREDENTIAL_PASSWORD})
     @interface CredentialType {}
 
-
-    static float dpToPixels(Context context, float dp) {
-        return dp * ((float) context.getResources().getDisplayMetrics().densityDpi
-                / DisplayMetrics.DENSITY_DEFAULT);
-    }
-
-    static float pixelsToDp(Context context, float pixels) {
-        return pixels / ((float) context.getResources().getDisplayMetrics().densityDpi
-                / DisplayMetrics.DENSITY_DEFAULT);
-    }
-
     static void notifyAccessibilityContentChanged(AccessibilityManager am, ViewGroup view) {
         if (!am.isEnabled()) {
             return;
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt
index 06fbf73..8a47a36 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt
@@ -102,6 +102,10 @@
         override fun onReleased() {
             removeDetailTask()
         }
+
+        override fun onBackPressedOnTaskRoot(taskId: Int) {
+            dismiss()
+        }
     }
 
     init {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
index 9b372b8..5a5cce8f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
@@ -232,10 +232,11 @@
         }
 
         @Override // Binder interface
-        public void onStartedWakingUp(@PowerManager.WakeReason int pmWakeReason) {
+        public void onStartedWakingUp(
+                @PowerManager.WakeReason int pmWakeReason, boolean cameraGestureTriggered) {
             Trace.beginSection("KeyguardService.mBinder#onStartedWakingUp");
             checkPermission();
-            mKeyguardViewMediator.onStartedWakingUp();
+            mKeyguardViewMediator.onStartedWakingUp(cameraGestureTriggered);
             mKeyguardLifecyclesDispatcher.dispatch(
                     KeyguardLifecyclesDispatcher.STARTED_WAKING_UP, pmWakeReason);
             Trace.endSection();
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 387f24f..da8a3b9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -106,12 +106,14 @@
 import com.android.systemui.navigationbar.NavigationModeController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shared.system.QuickStepContract;
+import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.phone.BiometricUnlockController;
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.phone.NotificationPanelViewController;
 import com.android.systemui.statusbar.phone.StatusBar;
+import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.util.DeviceConfigProxy;
 
@@ -234,6 +236,7 @@
     private StatusBarManager mStatusBarManager;
     private final SysuiStatusBarStateController mStatusBarStateController;
     private final Executor mUiBgExecutor;
+    private final UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
 
     private boolean mSystemReady;
     private boolean mBootCompleted;
@@ -387,6 +390,19 @@
     private boolean mPendingLock;
 
     /**
+     * Whether a power button gesture (such as double tap for camera) has been detected. This is
+     * delivered directly from {@link KeyguardService}, immediately upon the gesture being detected.
+     * This is used in {@link #onStartedWakingUp} to decide whether to execute the pending lock, or
+     * ignore and reset it because we are actually launching an activity.
+     *
+     * This needs to be delivered directly to us, rather than waiting for
+     * {@link CommandQueue#onCameraLaunchGestureDetected}, because that call is asynchronous and is
+     * often delivered after the call to {@link #onStartedWakingUp}, which results in us locking the
+     * keyguard and then launching the activity behind it.
+     */
+    private boolean mPowerGestureIntercepted = false;
+
+    /**
      * Controller for showing individual "work challenge" lock screen windows inside managed profile
      * tasks when the current user has been unlocked but the profile is still locked.
      */
@@ -790,7 +806,8 @@
             DozeParameters dozeParameters,
             SysuiStatusBarStateController statusBarStateController,
             KeyguardStateController keyguardStateController,
-            Lazy<KeyguardUnlockAnimationController> keyguardUnlockAnimationControllerLazy) {
+            Lazy<KeyguardUnlockAnimationController> keyguardUnlockAnimationControllerLazy,
+            UnlockedScreenOffAnimationController unlockedScreenOffAnimationController) {
         super(context);
         mFalsingCollector = falsingCollector;
         mLockPatternUtils = lockPatternUtils;
@@ -822,6 +839,7 @@
 
         mKeyguardStateController = keyguardStateController;
         mKeyguardUnlockAnimationControllerLazy = keyguardUnlockAnimationControllerLazy;
+        mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController;
     }
 
     public void userActivity() {
@@ -941,6 +959,7 @@
         if (DEBUG) Log.d(TAG, "onStartedGoingToSleep(" + offReason + ")");
         synchronized (this) {
             mDeviceInteractive = false;
+            mPowerGestureIntercepted = false;
             mGoingToSleep = true;
 
             // Reset keyguard going away state so we can start listening for fingerprint. We
@@ -1010,7 +1029,6 @@
             notifyFinishedGoingToSleep();
 
             if (cameraGestureTriggered) {
-                Log.i(TAG, "Camera gesture was triggered, preventing Keyguard locking.");
 
                 // Just to make sure, make sure the device is awake.
                 mContext.getSystemService(PowerManager.class).wakeUp(SystemClock.uptimeMillis(),
@@ -1025,10 +1043,7 @@
                 mPendingReset = false;
             }
 
-            if (mPendingLock) {
-                doKeyguardLocked(null);
-                mPendingLock = false;
-            }
+            maybeHandlePendingLock();
 
             // We do not have timeout and power button instant lock setting for profile lock.
             // So we use the personal setting if there is any. But if there is no device
@@ -1041,6 +1056,20 @@
         mUpdateMonitor.dispatchFinishedGoingToSleep(offReason);
     }
 
+    /**
+     * Locks the keyguard if {@link #mPendingLock} is true, unless we're playing the screen off
+     * animation.
+     *
+     * If we are, we will lock the keyguard either when the screen off animation ends, or in
+     * {@link #onStartedWakingUp} if the animation is cancelled.
+     */
+    public void maybeHandlePendingLock() {
+        if (mPendingLock && !mUnlockedScreenOffAnimationController.isScreenOffAnimationPlaying()) {
+            doKeyguardLocked(null);
+            mPendingLock = false;
+        }
+    }
+
     private boolean isKeyguardServiceEnabled() {
         try {
             return mContext.getPackageManager().getServiceInfo(
@@ -1149,12 +1178,15 @@
     /**
      * Let's us know when the device is waking up.
      */
-    public void onStartedWakingUp() {
+    public void onStartedWakingUp(boolean cameraGestureTriggered) {
         Trace.beginSection("KeyguardViewMediator#onStartedWakingUp");
 
         // TODO: Rename all screen off/on references to interactive/sleeping
         synchronized (this) {
             mDeviceInteractive = true;
+            if (mPendingLock && !cameraGestureTriggered) {
+                doKeyguardLocked(null);
+            }
             mAnimatingScreenOff = false;
             cancelDoKeyguardLaterLocked();
             cancelDoKeyguardForChildProfilesLocked();
@@ -1971,6 +2003,7 @@
 
             mHiding = false;
             mWakeAndUnlocking = false;
+            mPendingLock = false;
             setShowingLocked(true);
             mKeyguardViewControllerLazy.get().show(options);
             resetKeyguardDonePendingLocked();
@@ -2620,7 +2653,12 @@
         if (!dozing) {
             mAnimatingScreenOff = false;
         }
-        setShowingLocked(mShowing);
+
+        // Don't hide the keyguard due to a doze change if there's a lock pending, because we're
+        // just going to show it again.
+        if (mShowing || !mPendingLock) {
+            setShowingLocked(mShowing);
+        }
     }
 
     @Override
@@ -2677,14 +2715,7 @@
         mAodShowing = aodShowing;
         if (notifyDefaultDisplayCallbacks) {
             notifyDefaultDisplayCallbacks(showing);
-
-            if (!showing || !mAnimatingScreenOff) {
-                // Update the activity lock screen state unless we're animating in the keyguard
-                // for a screen off animation. In that case, we want the activity to remain visible
-                // until the animation completes. setShowingLocked is called again when the
-                // animation ends, so the activity lock screen will be shown at that time.
-                updateActivityLockScreenState(showing, aodShowing);
-            }
+            updateActivityLockScreenState(showing, aodShowing);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
index 119e9c4..b071b943 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
@@ -51,6 +51,7 @@
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.phone.KeyguardLiftController;
 import com.android.systemui.statusbar.phone.StatusBar;
+import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.util.DeviceConfigProxy;
 import com.android.systemui.util.sensors.AsyncSensorManager;
@@ -97,7 +98,8 @@
             DozeParameters dozeParameters,
             SysuiStatusBarStateController statusBarStateController,
             KeyguardStateController keyguardStateController,
-            Lazy<KeyguardUnlockAnimationController> keyguardUnlockAnimationController) {
+            Lazy<KeyguardUnlockAnimationController> keyguardUnlockAnimationController,
+            UnlockedScreenOffAnimationController unlockedScreenOffAnimationController) {
         return new KeyguardViewMediator(
                 context,
                 falsingCollector,
@@ -116,7 +118,8 @@
                 dozeParameters,
                 statusBarStateController,
                 keyguardStateController,
-                keyguardUnlockAnimationController
+                keyguardUnlockAnimationController,
+                unlockedScreenOffAnimationController
         );
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
index 47d80bb..a938821 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
@@ -181,14 +181,15 @@
         mStatusBarStateController = statusBarStateController;
         mActivityStarter = activityStarter;
 
-        mState = newTileState();
-        mTmpState = newTileState();
+        resetStates();
         mUiHandler.post(() -> mLifecycle.setCurrentState(CREATED));
     }
 
     protected final void resetStates() {
         mState = newTileState();
         mTmpState = newTileState();
+        mState.spec = mTileSpec;
+        mTmpState.spec = mTileSpec;
     }
 
     @NonNull
@@ -225,6 +226,8 @@
 
     public void setTileSpec(String tileSpec) {
         mTileSpec = tileSpec;
+        mState.spec = tileSpec;
+        mTmpState.spec = tileSpec;
     }
 
     public QSHost getHost() {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
index cd76b4d..8aed0a8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
@@ -38,6 +38,7 @@
 import android.widget.LinearLayout
 import android.widget.Switch
 import android.widget.TextView
+import androidx.annotation.VisibleForTesting
 import com.android.settingslib.Utils
 import com.android.systemui.FontSizeUtils
 import com.android.systemui.R
@@ -62,6 +63,8 @@
         private const val SECONDARY_LABEL_NAME = "secondaryLabel"
         private const val CHEVRON_NAME = "chevron"
         const val UNAVAILABLE_ALPHA = 0.3f
+        @VisibleForTesting
+        internal const val TILE_STATE_RES_PREFIX = "tile_states_"
     }
 
     override var heightOverride: Int = HeightOverrideable.NO_OVERRIDE
@@ -484,16 +487,18 @@
     }
 
     private fun getStateText(state: QSTile.State): String {
-        return if (state.disabledByPolicy) {
-            context.getString(R.string.tile_disabled)
-        } else if (state.state == Tile.STATE_UNAVAILABLE) {
-            context.getString(R.string.tile_unavailable)
-        } else if (state is BooleanState) {
-            if (state.state == Tile.STATE_INACTIVE) {
-                context.getString(R.string.switch_bar_off)
-            } else {
-                context.getString(R.string.switch_bar_on)
+        if (state.disabledByPolicy) {
+            return context.getString(R.string.tile_disabled)
+        }
+
+        return if (state.state == Tile.STATE_UNAVAILABLE || state is BooleanState) {
+            val resName = "$TILE_STATE_RES_PREFIX${state.spec}"
+            var arrayResId = resources.getIdentifier(resName, "array", context.packageName)
+            if (arrayResId == 0) {
+                arrayResId = R.array.tile_states_default
             }
+            val array = resources.getStringArray(arrayResId)
+            array[state.state]
         } else {
             ""
         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
index 74d3425..7cb1421 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
@@ -45,7 +45,6 @@
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.qs.QSIconView;
-import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.plugins.qs.QSTile.SignalState;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.qs.AlphaControlledSignalTileView;
@@ -72,7 +71,6 @@
 
     protected final NetworkController mController;
     private final DataUsageController mDataController;
-    private final QSTile.SignalState mStateBeforeClick = newTileState();
     // The last updated tile state, 0: mobile, 1: wifi, 2: ethernet.
     private int mLastTileState = -1;
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java
index efac141..41a3020 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java
@@ -94,6 +94,7 @@
         mWifiController = accessPointController;
         mDetailAdapter = (WifiDetailAdapter) createDetailAdapter();
         mController.observe(getLifecycle(), mSignalCallback);
+        mStateBeforeClick.spec = "wifi";
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
index af8d808..4ed376a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
@@ -287,6 +287,8 @@
                     // TODO: appear media also in split shade
                     val mediaAmount = if (useSplitShade) 0f else field
                     mediaHierarchyManager.setTransitionToFullShadeAmount(mediaAmount)
+                    // Fade out all content only visible on the lockscreen
+                    notificationPanelController.setKeyguardOnlyContentAlpha(1.0f - scrimProgress)
                 }
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt
index 33aa7c7..8479b30 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt
@@ -315,7 +315,11 @@
     ) : AnimatorListenerAdapter() {
         override fun onAnimationEnd(p0: Animator?) {
             chipAnimationController.onChipAnimationEnd(animationState)
-            animationState = endState
+            animationState = if (endState == SHOWING_PERSISTENT_DOT && !hasPersistentDot) {
+                IDLE
+            } else {
+                endState
+            }
         }
 
         override fun onAnimationStart(p0: Animator?) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
index 7afb015..84728f6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
@@ -28,6 +28,7 @@
 import com.android.systemui.statusbar.phone.DozeParameters
 import com.android.systemui.statusbar.phone.KeyguardBypassController
 import com.android.systemui.statusbar.phone.PanelExpansionListener
+import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController
 import com.android.systemui.statusbar.policy.HeadsUpManager
 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener
 import javax.inject.Inject
@@ -38,7 +39,8 @@
     private val mHeadsUpManager: HeadsUpManager,
     private val statusBarStateController: StatusBarStateController,
     private val bypassController: KeyguardBypassController,
-    private val dozeParameters: DozeParameters
+    private val dozeParameters: DozeParameters,
+    private val unlockedScreenOffAnimationController: UnlockedScreenOffAnimationController
 ) : OnHeadsUpChangedListener, StatusBarStateController.StateListener, PanelExpansionListener {
 
     private val mNotificationVisibility = object : FloatProperty<NotificationWakeUpCoordinator>(
@@ -264,7 +266,7 @@
     }
 
     override fun onStateChanged(newState: Int) {
-        if (dozeParameters.shouldControlUnlockedScreenOff()) {
+        if (unlockedScreenOffAnimationController.shouldPlayScreenOffAnimation()) {
             if (animatingScreenOff &&
                     state == StatusBarState.KEYGUARD &&
                     newState == StatusBarState.SHADE) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index c6753b3..d68271a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -73,7 +73,6 @@
 import com.android.internal.widget.CallLayout;
 import com.android.systemui.Dependency;
 import com.android.systemui.R;
-import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.plugins.FalsingManager;
@@ -2020,6 +2019,14 @@
         if (params == null) {
             return;
         }
+
+        if (!params.getVisible()) {
+            if (getVisibility() == View.VISIBLE) {
+                setVisibility(View.INVISIBLE);
+            }
+            return;
+        }
+
         float zProgress = Interpolators.FAST_OUT_SLOW_IN.getInterpolation(
                 params.getProgress(0, 50));
         float translationZ = MathUtils.lerp(params.getStartTranslationZ(),
@@ -2077,10 +2084,6 @@
             contentView = mGuts;
         }
         if (expandAnimationRunning) {
-            contentView.animate()
-                    .alpha(0f)
-                    .setDuration(ActivityLaunchAnimator.ANIMATION_DURATION_FADE_OUT_CONTENT)
-                    .setInterpolator(ActivityLaunchAnimator.CONTENT_FADE_OUT_INTERPOLATOR);
             setAboveShelf(true);
             mExpandAnimationRunning = true;
             getViewState().cancelAnimations(this);
@@ -2088,6 +2091,7 @@
         } else {
             mExpandAnimationRunning = false;
             setAboveShelf(isAboveShelf());
+            setVisibility(View.VISIBLE);
             if (mGuts != null) {
                 mGuts.setAlpha(1.0f);
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java
index f6ab409..4b1f679 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java
@@ -20,7 +20,6 @@
 import android.content.res.ColorStateList;
 import android.graphics.Canvas;
 import android.graphics.PorterDuff;
-import android.graphics.PorterDuffXfermode;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.GradientDrawable;
 import android.graphics.drawable.LayerDrawable;
@@ -30,8 +29,6 @@
 
 import com.android.internal.util.ArrayUtils;
 import com.android.systemui.R;
-import com.android.systemui.animation.ActivityLaunchAnimator;
-import com.android.systemui.animation.Interpolators;
 import com.android.systemui.statusbar.notification.ExpandAnimationParameters;
 
 /**
@@ -281,11 +278,6 @@
     public void setExpandAnimationParams(ExpandAnimationParameters params) {
         mActualHeight = params.getHeight();
         mActualWidth = params.getWidth();
-        float alphaProgress = Interpolators.ALPHA_IN.getInterpolation(
-                params.getProgress(
-                        ActivityLaunchAnimator.ANIMATION_DELAY_FADE_IN_WINDOW /* delay */,
-                        ActivityLaunchAnimator.ANIMATION_DURATION_FADE_IN_WINDOW /* duration */));
-        mBackground.setAlpha((int) (mDrawableAlpha * (1.0f - alphaProgress)));
         invalidate();
     }
 
@@ -294,8 +286,6 @@
         if (mBackground instanceof LayerDrawable) {
             GradientDrawable gradientDrawable =
                     (GradientDrawable) ((LayerDrawable) mBackground).getDrawable(0);
-            gradientDrawable.setXfermode(
-                    running ? new PorterDuffXfermode(PorterDuff.Mode.SRC) : null);
             // Speed optimization: disable AA if transfer mode is not SRC_OVER. AA is not easy to
             // spot during animation anyways.
             gradientDrawable.setAntiAlias(!running);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 94edbd0..64f228f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -43,6 +43,7 @@
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.os.Bundle;
+import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.util.AttributeSet;
@@ -129,7 +130,12 @@
 
     public static final float BACKGROUND_ALPHA_DIMMED = 0.7f;
     private static final String TAG = "StackScroller";
-    private static final boolean DEBUG = false;
+
+    // Usage:
+    // adb shell setprop persist.debug.nssl true && adb reboot
+    private static final boolean DEBUG = SystemProperties.getBoolean("persist.debug.nssl",
+            false /* default */);
+
     private static final float RUBBER_BAND_FACTOR_NORMAL = 0.35f;
     private static final float RUBBER_BAND_FACTOR_AFTER_EXPAND = 0.15f;
     private static final float RUBBER_BAND_FACTOR_ON_PANEL_EXPAND = 0.21f;
@@ -4667,8 +4673,9 @@
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         pw.println(String.format("[%s: pulsing=%s qsCustomizerShowing=%s visibility=%s"
-                        + " alpha:%f scrollY:%d maxTopPadding:%d showShelfOnly=%s"
-                        + " qsExpandFraction=%f]",
+                        + " alpha=%f scrollY:%d maxTopPadding=%d showShelfOnly=%s"
+                        + " qsExpandFraction=%f"
+                        + " hideAmount=%f]",
                 this.getClass().getSimpleName(),
                 mPulsing ? "T" : "f",
                 mAmbientState.isQsCustomizerShowing() ? "T" : "f",
@@ -4679,7 +4686,8 @@
                 mAmbientState.getScrollY(),
                 mMaxTopPadding,
                 mShouldShowShelfOnly ? "T" : "f",
-                mQsExpansionFraction));
+                mQsExpansionFraction,
+                mAmbientState.getHideAmount()));
         int childCount = getChildCount();
         pw.println("  Number of children: " + childCount);
         pw.println();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
index 7737420..eb46fe3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
@@ -334,6 +334,8 @@
     private NotificationsQuickSettingsContainer mNotificationContainerParent;
     private boolean mAnimateNextPositionUpdate;
     private float mQuickQsOffsetHeight;
+    private UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
+
     private int mTrackingPointer;
     private VelocityTracker mQsVelocityTracker;
     private boolean mQsTracking;
@@ -577,6 +579,11 @@
      */
     private ValueAnimator mQsClippingAnimation = null;
     private final Rect mKeyguardStatusAreaClipBounds = new Rect();
+
+    /**
+     * The alpha of the views which only show on the keyguard but not in shade / shade locked
+     */
+    private float mKeyguardOnlyContentAlpha = 1.0f;
     private int mOldLayoutDirection;
     private NotificationShelfController mNotificationShelfController;
     private int mScrimCornerRadius;
@@ -670,7 +677,8 @@
             FragmentService fragmentService,
             QuickAccessWalletController quickAccessWalletController,
             @Main Executor uiExecutor,
-            SecureSettings secureSettings) {
+            SecureSettings secureSettings,
+            UnlockedScreenOffAnimationController unlockedScreenOffAnimationController) {
         super(view, falsingManager, dozeLog, keyguardStateController,
                 (SysuiStatusBarStateController) statusBarStateController, vibratorHelper,
                 statusBarKeyguardViewManager, latencyTracker, flingAnimationUtilsBuilder.get(),
@@ -765,6 +773,7 @@
         mConversationNotificationManager = conversationNotificationManager;
         mAuthController = authController;
         mLockIconViewController = lockIconViewController;
+        mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController;
 
         mView.setBackgroundColor(Color.TRANSPARENT);
         OnAttachStateChangeListener onAttachStateChangeListener = new OnAttachStateChangeListener();
@@ -1240,10 +1249,11 @@
         int userIconHeight = mKeyguardQsUserSwitchController != null
                 ? mKeyguardQsUserSwitchController.getUserIconHeight() : 0;
         float expandedFraction =
-                mKeyguardStatusViewController.isAnimatingScreenOffFromUnlocked() ? 1.0f
-                        : getExpandedFraction();
-        float darkamount = mKeyguardStatusViewController.isAnimatingScreenOffFromUnlocked() ? 1.0f
-                : mInterpolatedDarkAmount;
+                mUnlockedScreenOffAnimationController.isScreenOffAnimationPlaying()
+                        ? 1.0f : getExpandedFraction();
+        float darkamount =
+                mUnlockedScreenOffAnimationController.isScreenOffAnimationPlaying()
+                        ? 1.0f : mInterpolatedDarkAmount;
         mClockPositionAlgorithm.setup(mStatusBarHeaderHeightKeyguard,
                 totalHeight - bottomPadding,
                 mNotificationStackScrollLayoutController.getIntrinsicContentHeight(),
@@ -1412,12 +1422,13 @@
     }
 
     private void updateClock() {
-        mKeyguardStatusViewController.setAlpha(mClockPositionResult.clockAlpha);
+        float alpha = mClockPositionResult.clockAlpha * mKeyguardOnlyContentAlpha;
+        mKeyguardStatusViewController.setAlpha(alpha);
         if (mKeyguardQsUserSwitchController != null) {
-            mKeyguardQsUserSwitchController.setAlpha(mClockPositionResult.clockAlpha);
+            mKeyguardQsUserSwitchController.setAlpha(alpha);
         }
         if (mKeyguardUserSwitcherController != null) {
-            mKeyguardUserSwitcherController.setAlpha(mClockPositionResult.clockAlpha);
+            mKeyguardUserSwitcherController.setAlpha(alpha);
         }
     }
 
@@ -2024,6 +2035,7 @@
     private void maybeAnimateBottomAreaAlpha() {
         mBottomAreaShadeAlphaAnimator.cancel();
         if (mBarState == StatusBarState.SHADE_LOCKED) {
+            mBottomAreaShadeAlphaAnimator.setFloatValues(mBottomAreaShadeAlpha, 0.0f);
             mBottomAreaShadeAlphaAnimator.start();
         } else {
             mBottomAreaShadeAlpha = 1f;
@@ -2439,6 +2451,20 @@
         updateQsExpansion();
     }
 
+    /**
+     * Set the alpha of the keyguard elements which only show on the lockscreen, but not in
+     * shade locked / shade. This is used when dragging down to the full shade.
+     */
+    public void setKeyguardOnlyContentAlpha(float keyguardAlpha) {
+        mKeyguardOnlyContentAlpha = Interpolators.ALPHA_IN.getInterpolation(keyguardAlpha);
+        if (mBarState == KEYGUARD) {
+            // If the animator is running, it's already fading out the content and this is a reset
+            mBottomAreaShadeAlpha = mKeyguardOnlyContentAlpha;
+            updateKeyguardBottomAreaAlpha();
+        }
+        updateClock();
+    }
+
     private void trackMovement(MotionEvent event) {
         if (mQsVelocityTracker != null) mQsVelocityTracker.addMovement(event);
     }
@@ -2897,6 +2923,7 @@
         if (ambientIndicationContainer != null) {
             ambientIndicationContainer.setAlpha(alpha);
         }
+        mLockIconViewController.setAlpha(alpha);
     }
 
     /**
@@ -4209,7 +4236,9 @@
             int oldState = mBarState;
             boolean keyguardShowing = statusBarState == KEYGUARD;
 
-            if (mDozeParameters.shouldControlUnlockedScreenOff() && isDozing() && keyguardShowing) {
+            if (mUnlockedScreenOffAnimationController.shouldPlayScreenOffAnimation()
+                    && oldState == StatusBarState.SHADE
+                    && statusBarState == KEYGUARD) {
                 // This means we're doing the screen off animation - position the keyguard status
                 // view where it'll be on AOD, so we can animate it in.
                 mKeyguardStatusViewController.updatePosition(
@@ -4280,6 +4309,18 @@
     }
 
     /**
+     * Reconfigures the shade to show the AOD UI (clock, smartspace, etc). This is called by the
+     * screen off animation controller in order to animate in AOD without "actually" fully switching
+     * to the KEYGUARD state.
+     */
+    public void showAodUi() {
+        setDozing(true /* dozing */, false /* animate */, null);
+        mStatusBarStateListener.onStateChanged(KEYGUARD);
+        mStatusBarStateListener.onDozeAmountChanged(1f, 1f);
+        setExpandedFraction(1f);
+    }
+
+    /**
      * Sets the overstretch amount in raw pixels when dragging down.
      */
     public void setOverStrechAmount(float amount) {
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 b92f7c0..1331829 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -169,6 +169,7 @@
     private final KeyguardVisibilityCallback mKeyguardVisibilityCallback;
     private final Handler mHandler;
     private final Executor mMainExecutor;
+    private UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
 
     private GradientColors mColors;
     private boolean mNeedsDrawableColorUpdate;
@@ -224,7 +225,8 @@
             AlarmManager alarmManager, KeyguardStateController keyguardStateController,
             DelayedWakeLock.Builder delayedWakeLockBuilder, Handler handler,
             KeyguardUpdateMonitor keyguardUpdateMonitor, DockManager dockManager,
-            ConfigurationController configurationController, @Main Executor mainExecutor) {
+            ConfigurationController configurationController, @Main Executor mainExecutor,
+            UnlockedScreenOffAnimationController unlockedScreenOffAnimationController) {
         mScrimStateListener = lightBarController::setScrimState;
         mDefaultScrimAlpha = BUSY_SCRIM_ALPHA;
         ScrimState.BUBBLE_EXPANDED.setBubbleAlpha(BUBBLE_SCRIM_ALPHA);
@@ -235,6 +237,7 @@
         mKeyguardVisibilityCallback = new KeyguardVisibilityCallback();
         mHandler = handler;
         mMainExecutor = mainExecutor;
+        mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController;
         mTimeTicker = new AlarmTimeout(alarmManager, this::onHideWallpaperTimeout,
                 "hide_aod_wallpaper", mHandler);
         mWakeLock = delayedWakeLockBuilder.setHandler(mHandler).setTag("Scrims").build();
@@ -640,17 +643,20 @@
         }
 
         if (mState == ScrimState.UNLOCKED) {
-            // Darken scrim as you pull down the shade when unlocked
-            float behindFraction = getInterpolatedFraction();
-            behindFraction = (float) Math.pow(behindFraction, 0.8f);
-            if (mClipsQsScrim) {
-                mBehindAlpha = 1;
-                mNotificationsAlpha = behindFraction * mDefaultScrimAlpha;
-            } else {
-                mBehindAlpha = behindFraction * mDefaultScrimAlpha;
-                mNotificationsAlpha = mBehindAlpha;
+            // Darken scrim as you pull down the shade when unlocked, unless the shade is expanding
+            // because we're doing the screen off animation.
+            if (!mUnlockedScreenOffAnimationController.isScreenOffAnimationPlaying()) {
+                float behindFraction = getInterpolatedFraction();
+                behindFraction = (float) Math.pow(behindFraction, 0.8f);
+                if (mClipsQsScrim) {
+                    mBehindAlpha = 1;
+                    mNotificationsAlpha = behindFraction * mDefaultScrimAlpha;
+                } else {
+                    mBehindAlpha = behindFraction * mDefaultScrimAlpha;
+                    mNotificationsAlpha = mBehindAlpha;
+                }
+                mInFrontAlpha = 0;
             }
-            mInFrontAlpha = 0;
         } else if (mState == ScrimState.BUBBLE_EXPANDED) {
             // Darken scrim as you pull down the shade when unlocked
             float behindFraction = getInterpolatedFraction();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java
index 35dda44..e52e1fa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java
@@ -189,9 +189,11 @@
             mBubbleAlpha = ScrimController.TRANSPARENT;
 
             mAnimationDuration = ScrimController.ANIMATION_DURATION_LONG;
-            // DisplayPowerManager may blank the screen for us,
-            // in this case we just need to set our state.
-            mAnimateChange = mDozeParameters.shouldControlScreenOff();
+            // DisplayPowerManager may blank the screen for us, or we might blank it for ourselves
+            // by animating the screen off via the LightRevelScrim. In either case we just need to
+            // set our state.
+            mAnimateChange = mDozeParameters.shouldControlScreenOff()
+                    && !mDozeParameters.shouldControlUnlockedScreenOff();
         }
 
         @Override
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 2396272..2eece18 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -468,6 +468,7 @@
     private final BrightnessSlider.Factory mBrightnessSliderFactory;
     private final FeatureFlags mFeatureFlags;
     private final KeyguardUnlockAnimationController mKeyguardUnlockAnimationController;
+    private final UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
 
     private final List<ExpansionChangedListener> mExpansionChangedListeners;
 
@@ -806,7 +807,8 @@
             StatusBarLocationPublisher locationPublisher,
             LockscreenShadeTransitionController lockscreenShadeTransitionController,
             FeatureFlags featureFlags,
-            KeyguardUnlockAnimationController keyguardUnlockAnimationController) {
+            KeyguardUnlockAnimationController keyguardUnlockAnimationController,
+            UnlockedScreenOffAnimationController unlockedScreenOffAnimationController) {
         super(context);
         mNotificationsController = notificationsController;
         mLightBarController = lightBarController;
@@ -890,6 +892,7 @@
         mStatusBarLocationPublisher = locationPublisher;
         mFeatureFlags = featureFlags;
         mKeyguardUnlockAnimationController = keyguardUnlockAnimationController;
+        mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController;
 
         mLockscreenShadeTransitionController = lockscreenShadeTransitionController;
         lockscreenShadeTransitionController.setStatusbar(this);
@@ -1244,6 +1247,7 @@
         mScrimController.attachViews(scrimBehind, notificationsScrim, scrimInFront, scrimForBubble);
 
         mLightRevealScrim = mNotificationShadeWindowView.findViewById(R.id.light_reveal_scrim);
+        mUnlockedScreenOffAnimationController.initialize(this, mLightRevealScrim);
         updateLightRevealScrimVisibility();
 
         mNotificationPanelViewController.initDependencies(
@@ -1474,7 +1478,9 @@
      * @param why the reason for the wake up
      */
     public void wakeUpIfDozing(long time, View where, String why) {
-        if (mDozing && !mKeyguardViewMediator.isAnimatingScreenOff()) {
+        if (mDozing && !(mKeyguardViewMediator.isAnimatingScreenOff()
+                || mUnlockedScreenOffAnimationController
+                    .isScreenOffLightRevealAnimationPlaying())) {
             mPowerManager.wakeUp(
                     time, PowerManager.WAKE_REASON_GESTURE, "com.android.systemui:" + why);
             mWakeUpComingFromTouch = true;
@@ -3416,8 +3422,9 @@
             updatePanelExpansionForKeyguard();
         }
         if (shouldBeKeyguard) {
-            if (isGoingToSleep()
-                    && mScreenLifecycle.getScreenState() == ScreenLifecycle.SCREEN_TURNING_OFF) {
+            if (mUnlockedScreenOffAnimationController.isScreenOffAnimationPlaying()
+                    || (isGoingToSleep()
+                    && mScreenLifecycle.getScreenState() == ScreenLifecycle.SCREEN_TURNING_OFF)) {
                 // Delay showing the keyguard until screen turned off.
             } else {
                 showKeyguardImpl();
@@ -3588,6 +3595,7 @@
         mNotificationPanelViewController.cancelAnimation();
         mNotificationPanelViewController.setAlpha(1f);
         mNotificationPanelViewController.resetViewGroupFade();
+        updateDozingState();
         updateScrimController();
         Trace.endSection();
         return staying;
@@ -4050,6 +4058,13 @@
             mWakeUpCoordinator.setFullyAwake(false);
             mBypassHeadsUpNotifier.setFullyAwake(false);
             mKeyguardBypassController.onStartedGoingToSleep();
+
+            // The screen off animation uses our LightRevealScrim - we need to be expanded for it to
+            // be visible.
+            if (mUnlockedScreenOffAnimationController.shouldPlayScreenOffAnimation()) {
+                makeExpandedVisible(true);
+            }
+
             DejankUtils.stopDetectingBlockingIpcs(tag);
         }
 
@@ -4070,6 +4085,13 @@
             // once we fully woke up.
             updateNotificationPanelTouchState();
             mPulseExpansionHandler.onStartedWakingUp();
+
+            // If we are waking up during the screen off animation, we should undo making the
+            // expanded visible (we did that so the LightRevealScrim would be visible).
+            if (mUnlockedScreenOffAnimationController.isScreenOffLightRevealAnimationPlaying()) {
+                makeExpandedInvisible();
+            }
+
             DejankUtils.stopDetectingBlockingIpcs(tag);
         }
 
@@ -4442,8 +4464,9 @@
     }
 
     public boolean shouldIgnoreTouch() {
-        return mStatusBarStateController.isDozing()
-                && mDozeServiceHost.getIgnoreTouchWhilePulsing();
+        return (mStatusBarStateController.isDozing()
+                && mDozeServiceHost.getIgnoreTouchWhilePulsing())
+                || mUnlockedScreenOffAnimationController.isScreenOffAnimationPlaying();
     }
 
     // Begin Extra BaseStatusBar methods.
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 91d1bd7..c0957c0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -35,6 +35,7 @@
 import android.view.ViewRootImpl;
 import android.view.WindowManagerGlobal;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
@@ -63,6 +64,7 @@
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.Objects;
 import java.util.Optional;
 
 import javax.inject.Inject;
@@ -268,10 +270,23 @@
         registerListeners();
     }
 
-    public void setAlternateAuthInterceptor(@Nullable AlternateAuthInterceptor authInterceptor) {
-        final boolean newlyNull = authInterceptor == null && mAlternateAuthInterceptor != null;
+    /**
+     * Sets the given alt auth interceptor to null if it's the current auth interceptor. Else,
+     * does nothing.
+     */
+    public void removeAlternateAuthInterceptor(@NonNull AlternateAuthInterceptor authInterceptor) {
+        if (Objects.equals(mAlternateAuthInterceptor, authInterceptor)) {
+            mAlternateAuthInterceptor = null;
+            resetAlternateAuth(true);
+        }
+    }
+
+    /**
+     * Sets a new alt auth interceptor.
+     */
+    public void setAlternateAuthInterceptor(@NonNull AlternateAuthInterceptor authInterceptor) {
         mAlternateAuthInterceptor = authInterceptor;
-        resetAlternateAuth(newlyNull);
+        resetAlternateAuth(false);
     }
 
     private void registerListeners() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
new file mode 100644
index 0000000..e135cc5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
@@ -0,0 +1,178 @@
+package com.android.systemui.statusbar.phone
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.os.Handler
+import android.view.View
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.KeyguardViewMediator
+import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.statusbar.LightRevealScrim
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.StatusBarStateControllerImpl
+import com.android.systemui.statusbar.notification.AnimatableProperty
+import com.android.systemui.statusbar.notification.PropertyAnimator
+import com.android.systemui.statusbar.notification.stack.AnimationProperties
+import com.android.systemui.statusbar.notification.stack.StackStateAnimator
+import javax.inject.Inject
+
+/**
+ * When to show the keyguard (AOD) view. This should be once the light reveal scrim is barely
+ * visible, because the transition to KEYGUARD causes brief jank.
+ */
+private const val ANIMATE_IN_KEYGUARD_DELAY = 600L
+
+/**
+ * Duration for the light reveal portion of the animation.
+ */
+private const val LIGHT_REVEAL_ANIMATION_DURATION = 750L
+
+/**
+ * Controller for the unlocked screen off animation, which runs when the device is going to sleep
+ * and we're unlocked.
+ *
+ * This animation uses a [LightRevealScrim] that lives in the status bar to hide the screen contents
+ * and then animates in the AOD UI.
+ */
+@SysUISingleton
+class UnlockedScreenOffAnimationController @Inject constructor(
+    private val wakefulnessLifecycle: WakefulnessLifecycle,
+    private val statusBarStateControllerImpl: StatusBarStateControllerImpl,
+    private val keyguardViewMediatorLazy: dagger.Lazy<KeyguardViewMediator>,
+    private val dozeParameters: DozeParameters
+) : WakefulnessLifecycle.Observer {
+    private val handler = Handler()
+
+    private lateinit var statusBar: StatusBar
+    private lateinit var lightRevealScrim: LightRevealScrim
+
+    private var lightRevealAnimationPlaying = false
+    private var aodUiAnimationPlaying = false
+
+    private val lightRevealAnimator = ValueAnimator.ofFloat(1f, 0f).apply {
+        duration = LIGHT_REVEAL_ANIMATION_DURATION
+        interpolator = Interpolators.FAST_OUT_SLOW_IN_REVERSE
+        addUpdateListener { lightRevealScrim.revealAmount = it.animatedValue as Float }
+        addListener(object : AnimatorListenerAdapter() {
+            override fun onAnimationCancel(animation: Animator?) {
+                lightRevealScrim.revealAmount = 1f
+                lightRevealAnimationPlaying = false
+            }
+
+            override fun onAnimationEnd(animation: Animator?) {
+                lightRevealAnimationPlaying = false
+            }
+        })
+    }
+
+    fun initialize(
+        statusBar: StatusBar,
+        lightRevealScrim: LightRevealScrim
+    ) {
+        this.lightRevealScrim = lightRevealScrim
+        this.statusBar = statusBar
+
+        wakefulnessLifecycle.addObserver(this)
+    }
+
+    /**
+     * Animates in the provided keyguard view, ending in the same position that it will be in on
+     * AOD.
+     */
+    fun animateInKeyguard(keyguardView: View, after: Runnable) {
+        keyguardView.alpha = 0f
+        keyguardView.visibility = View.VISIBLE
+
+        val currentY = keyguardView.y
+
+        // Move the keyguard up by 10% so we can animate it back down.
+        keyguardView.y = currentY - keyguardView.height * 0.1f
+
+        val duration = StackStateAnimator.ANIMATION_DURATION_WAKEUP
+
+        // We animate the Y properly separately using the PropertyAnimator, as the panel
+        // view also needs to update the end position.
+        PropertyAnimator.cancelAnimation(keyguardView, AnimatableProperty.Y)
+        PropertyAnimator.setProperty<View>(keyguardView, AnimatableProperty.Y, currentY,
+                AnimationProperties().setDuration(duration.toLong()),
+                true /* animate */)
+
+        keyguardView.animate()
+                .setDuration(duration.toLong())
+                .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
+                .alpha(1f)
+                .withEndAction {
+                    aodUiAnimationPlaying = false
+
+                    // Lock the keyguard if it was waiting for the screen off animation to end.
+                    keyguardViewMediatorLazy.get().maybeHandlePendingLock()
+
+                    // Tell the StatusBar to become keyguard for real - we waited on that since it
+                    // is slow and would have caused the animation to jank.
+                    statusBar.updateIsKeyguard()
+
+                    // Run the callback given to us by the KeyguardVisibilityHelper.
+                    after.run()
+                }
+                .start()
+    }
+
+    override fun onStartedWakingUp() {
+        lightRevealAnimator.cancel()
+        handler.removeCallbacksAndMessages(null)
+    }
+
+    override fun onFinishedWakingUp() {
+        // Set this to false in onFinishedWakingUp rather than onStartedWakingUp so that other
+        // observers (such as StatusBar) can ask us whether we were playing the screen off animation
+        // and reset accordingly.
+        lightRevealAnimationPlaying = false
+        aodUiAnimationPlaying = false
+
+        // Make sure the status bar is in the correct keyguard state, since we might have left it in
+        // the KEYGUARD state if this wakeup cancelled the screen off animation.
+        statusBar.updateIsKeyguard()
+    }
+
+    override fun onStartedGoingToSleep() {
+        if (shouldPlayScreenOffAnimation()) {
+            lightRevealAnimationPlaying = true
+            lightRevealAnimator.start()
+
+            handler.postDelayed({
+                aodUiAnimationPlaying = true
+
+                // Show AOD. That'll cause the KeyguardVisibilityHelper to call #animateInKeyguard.
+                statusBar.notificationPanelViewController.showAodUi()
+            }, ANIMATE_IN_KEYGUARD_DELAY)
+        }
+    }
+
+    /**
+     * Whether we should play the screen off animation when the phone starts going to sleep. We can
+     * do that if dozeParameters says we can control the unlocked screen off animation and we are in
+     * the SHADE state. If we're in KEYGUARD or SHADE_LOCKED, the regular
+     */
+    fun shouldPlayScreenOffAnimation(): Boolean {
+        return dozeParameters.shouldControlUnlockedScreenOff() &&
+                statusBarStateControllerImpl.state == StatusBarState.SHADE
+    }
+
+    /**
+     * Whether we're doing the light reveal animation or we're done with that and animating in the
+     * AOD UI.
+     */
+    fun isScreenOffAnimationPlaying(): Boolean {
+        return lightRevealAnimationPlaying || aodUiAnimationPlaying
+    }
+
+    /**
+     * Whether the light reveal animation is playing. The second part of the screen off animation,
+     * where AOD animates in, might still be playing if this returns false.
+     */
+    fun isScreenOffLightRevealAnimationPlaying(): Boolean {
+        return lightRevealAnimationPlaying
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java
index 9722d68..2611ab5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java
@@ -94,6 +94,7 @@
 import com.android.systemui.statusbar.phone.StatusBarNotificationActivityStarter;
 import com.android.systemui.statusbar.phone.StatusBarSignalPolicy;
 import com.android.systemui.statusbar.phone.StatusBarTouchableRegionManager;
+import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -216,7 +217,8 @@
             StatusBarLocationPublisher locationPublisher,
             LockscreenShadeTransitionController transitionController,
             FeatureFlags featureFlags,
-            KeyguardUnlockAnimationController keyguardUnlockAnimationController) {
+            KeyguardUnlockAnimationController keyguardUnlockAnimationController,
+            UnlockedScreenOffAnimationController unlockedScreenOffAnimationController) {
         return new StatusBar(
                 context,
                 notificationsController,
@@ -303,6 +305,7 @@
                 locationPublisher,
                 transitionController,
                 featureFlags,
-                keyguardUnlockAnimationController);
+                keyguardUnlockAnimationController,
+                unlockedScreenOffAnimationController);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java
index f39acf9..2b79733 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java
@@ -31,6 +31,7 @@
 
 import androidx.annotation.NonNull;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.settingslib.bluetooth.BluetoothCallback;
 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 import com.android.settingslib.bluetooth.LocalBluetoothManager;
@@ -66,6 +67,7 @@
     private final WeakHashMap<CachedBluetoothDevice, ActuallyCachedState> mCachedState =
             new WeakHashMap<>();
     private final Handler mBgHandler;
+    @GuardedBy("mConnectedDevices")
     private final List<CachedBluetoothDevice> mConnectedDevices = new ArrayList<>();
 
     private boolean mEnabled;
@@ -118,7 +120,7 @@
         pw.print("  mConnectionState="); pw.println(stateToString(mConnectionState));
         pw.print("  mAudioProfileOnly="); pw.println(mAudioProfileOnly);
         pw.print("  mIsActive="); pw.println(mIsActive);
-        pw.print("  mConnectedDevices="); pw.println(mConnectedDevices);
+        pw.print("  mConnectedDevices="); pw.println(getConnectedDevices());
         pw.print("  mCallbacks.size="); pw.println(mHandler.mCallbacks.size());
         pw.println("  Bluetooth Devices:");
         for (CachedBluetoothDevice device : getDevices()) {
@@ -151,7 +153,11 @@
 
     @Override
     public List<CachedBluetoothDevice> getConnectedDevices() {
-        return mConnectedDevices;
+        List<CachedBluetoothDevice> out;
+        synchronized (mConnectedDevices) {
+            out = new ArrayList<>(mConnectedDevices);
+        }
+        return out;
     }
 
     @Override
@@ -226,8 +232,10 @@
 
     @Override
     public String getConnectedDeviceName() {
-        if (mConnectedDevices.size() == 1) {
-            return mConnectedDevices.get(0).getName();
+        synchronized (mConnectedDevices) {
+            if (mConnectedDevices.size() == 1) {
+                return mConnectedDevices.get(0).getName();
+            }
         }
         return null;
     }
@@ -242,7 +250,7 @@
     private void updateConnected() {
         // Make sure our connection state is up to date.
         int state = mLocalBluetoothManager.getBluetoothAdapter().getConnectionState();
-        mConnectedDevices.clear();
+        List<CachedBluetoothDevice> newList = new ArrayList<>();
         // If any of the devices are in a higher state than the adapter, move the adapter into
         // that state.
         for (CachedBluetoothDevice device : getDevices()) {
@@ -251,15 +259,19 @@
                 state = maxDeviceState;
             }
             if (device.isConnected()) {
-                mConnectedDevices.add(device);
+                newList.add(device);
             }
         }
 
-        if (mConnectedDevices.isEmpty() && state == BluetoothAdapter.STATE_CONNECTED) {
+        if (newList.isEmpty() && state == BluetoothAdapter.STATE_CONNECTED) {
             // If somehow we think we are connected, but have no connected devices, we aren't
             // connected.
             state = BluetoothAdapter.STATE_DISCONNECTED;
         }
+        synchronized (mConnectedDevices) {
+            mConnectedDevices.clear();
+            mConnectedDevices.addAll(newList);
+        }
         if (state != mConnectionState) {
             mConnectionState = state;
             mHandler.sendEmptyMessage(H.MSG_STATE_CHANGED);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java
index 9d667805f..2ecd4b3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java
@@ -45,6 +45,7 @@
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.phone.NotificationPanelViewController;
+import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
 import com.android.systemui.statusbar.phone.UserAvatarView;
 import com.android.systemui.util.ViewController;
 
@@ -122,7 +123,8 @@
             ConfigurationController configurationController,
             SysuiStatusBarStateController statusBarStateController,
             DozeParameters dozeParameters,
-            Provider<UserDetailView.Adapter> userDetailViewAdapterProvider) {
+            Provider<UserDetailView.Adapter> userDetailViewAdapterProvider,
+            UnlockedScreenOffAnimationController unlockedScreenOffAnimationController) {
         super(view);
         if (DEBUG) Log.d(TAG, "New KeyguardQsUserSwitchController");
         mContext = context;
@@ -135,7 +137,7 @@
         mConfigurationController = configurationController;
         mStatusBarStateController = statusBarStateController;
         mKeyguardVisibilityHelper = new KeyguardVisibilityHelper(mView,
-                keyguardStateController, dozeParameters);
+                keyguardStateController, dozeParameters, unlockedScreenOffAnimationController);
         mUserDetailAdapter = new KeyguardUserDetailAdapter(context, userDetailViewAdapterProvider);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java
index e2c52f9..68f2a62 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java
@@ -51,6 +51,7 @@
 import com.android.systemui.statusbar.notification.stack.AnimationProperties;
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
 import com.android.systemui.statusbar.phone.DozeParameters;
+import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
 import com.android.systemui.util.ViewController;
 
 import java.util.ArrayList;
@@ -159,7 +160,8 @@
             KeyguardStateController keyguardStateController,
             SysuiStatusBarStateController statusBarStateController,
             KeyguardUpdateMonitor keyguardUpdateMonitor,
-            DozeParameters dozeParameters) {
+            DozeParameters dozeParameters,
+            UnlockedScreenOffAnimationController unlockedScreenOffAnimationController) {
         super(keyguardUserSwitcherView);
         if (DEBUG) Log.d(TAG, "New KeyguardUserSwitcherController");
         mContext = context;
@@ -171,7 +173,7 @@
         mAdapter = new KeyguardUserAdapter(mContext, resources, layoutInflater,
                 mUserSwitcherController, this);
         mKeyguardVisibilityHelper = new KeyguardVisibilityHelper(mView,
-                keyguardStateController, dozeParameters);
+                keyguardStateController, dozeParameters, unlockedScreenOffAnimationController);
         mBackground = new KeyguardUserSwitcherScrim(context);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/usb/UsbDebuggingActivity.java b/packages/SystemUI/src/com/android/systemui/usb/UsbDebuggingActivity.java
index b1241b1..941cd77 100644
--- a/packages/SystemUI/src/com/android/systemui/usb/UsbDebuggingActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/usb/UsbDebuggingActivity.java
@@ -68,7 +68,9 @@
 
         super.onCreate(icicle);
 
-        if (SystemProperties.getInt("service.adb.tcp.port", 0) == 0) {
+        // Emulator does not support reseating the usb cable to reshow the dialog.
+        boolean isEmulator = SystemProperties.get("ro.boot.qemu").equals("1");
+        if (SystemProperties.getInt("service.adb.tcp.port", 0) == 0 && !isEmulator) {
             mDisconnectedReceiver = new UsbDisconnectedReceiver(this);
             IntentFilter filter = new IntentFilter(UsbManager.ACTION_USB_STATE);
             mBroadcastDispatcher.registerReceiver(mDisconnectedReceiver, filter);
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
index f9b6d44..83c2227 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
@@ -25,6 +25,7 @@
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.shared.system.smartspace.SmartspaceTransitionController;
 import com.android.systemui.statusbar.phone.DozeParameters;
+import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
@@ -56,6 +57,8 @@
     KeyguardUnlockAnimationController mKeyguardUnlockAnimationController;
     @Mock
     SmartspaceTransitionController mSmartSpaceTransitionController;
+    @Mock
+    UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
 
     private KeyguardStatusViewController mController;
 
@@ -72,7 +75,8 @@
                 mConfigurationController,
                 mDozeParameters,
                 mKeyguardUnlockAnimationController,
-                mSmartSpaceTransitionController);
+                mSmartSpaceTransitionController,
+                mUnlockedScreenOffAnimationController);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt
index 9a0ed98..d499011 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt
@@ -2,6 +2,9 @@
 
 import android.app.ActivityManager
 import android.app.WindowConfiguration
+import android.content.ComponentName
+import android.content.pm.ActivityInfo
+import android.content.pm.ApplicationInfo
 import android.graphics.Point
 import android.graphics.Rect
 import android.os.Looper
@@ -167,10 +170,16 @@
 
     private fun fakeWindow(): RemoteAnimationTarget {
         val bounds = Rect(10 /* left */, 20 /* top */, 30 /* right */, 40 /* bottom */)
+        val taskInfo = ActivityManager.RunningTaskInfo()
+        taskInfo.topActivity = ComponentName("com.android.systemui", "FakeActivity")
+        taskInfo.topActivityInfo = ActivityInfo().apply {
+            applicationInfo = ApplicationInfo()
+        }
+
         return RemoteAnimationTarget(
                 0, RemoteAnimationTarget.MODE_OPENING, SurfaceControl(), false, Rect(), Rect(), 0,
                 Point(), Rect(), bounds, WindowConfiguration(), false, SurfaceControl(), Rect(),
-                ActivityManager.RunningTaskInfo()
+                taskInfo
         )
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.java
index f41c100..0b399cf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.java
@@ -224,7 +224,7 @@
     public void testLayoutParams_hasSecureWindowFlag() {
         final IBinder windowToken = mock(IBinder.class);
         final WindowManager.LayoutParams layoutParams =
-                AuthContainerView.getLayoutParams(windowToken);
+                AuthContainerView.getLayoutParams(windowToken, "");
         assertTrue((layoutParams.flags & WindowManager.LayoutParams.FLAG_SECURE) != 0);
     }
 
@@ -232,7 +232,7 @@
     public void testLayoutParams_excludesImeInsets() {
         final IBinder windowToken = mock(IBinder.class);
         final WindowManager.LayoutParams layoutParams =
-                AuthContainerView.getLayoutParams(windowToken);
+                AuthContainerView.getLayoutParams(windowToken, "");
         assertTrue((layoutParams.getFitInsetsTypes() & WindowInsets.Type.ime()) == 0);
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java
index a7c63c1..5923de6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java
@@ -254,15 +254,15 @@
 
     @Test
     public void testOnDetachedStateReset() {
-        // GIVEN view is attached, alt auth is force being shown
+        // GIVEN view is attached
         mController.onViewAttached();
-        captureStatusBarStateListeners();
+        captureAltAuthInterceptor();
 
         // WHEN view is detached
         mController.onViewDetached();
 
-        // THEN set alternate auth interceptor to null
-        verify(mStatusBarKeyguardViewManager).setAlternateAuthInterceptor(null);
+        // THEN remove alternate auth interceptor
+        verify(mStatusBarKeyguardViewManager).removeAlternateAuthInterceptor(mAltAuthInterceptor);
     }
 
     private void sendStatusBarStateChanged(int statusBarState) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index 6f03f5d..e6f9aaf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -49,6 +49,7 @@
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
+import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.util.DeviceConfigProxy;
 import com.android.systemui.util.DeviceConfigProxyFake;
@@ -82,6 +83,7 @@
     private @Mock SysuiStatusBarStateController mStatusBarStateController;
     private @Mock KeyguardStateController mKeyguardStateController;
     private @Mock KeyguardUnlockAnimationController mKeyguardUnlockAnimationController;
+    private @Mock UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
     private DeviceConfigProxy mDeviceConfig = new DeviceConfigProxyFake();
     private FakeExecutor mUiBgExecutor = new FakeExecutor(new FakeSystemClock());
 
@@ -101,7 +103,8 @@
                 mDismissCallbackRegistry, mUpdateMonitor, mDumpManager, mUiBgExecutor,
                 mPowerManager, mTrustManager, mDeviceConfig, mNavigationModeController,
                 mKeyguardDisplayManager, mDozeParameters, mStatusBarStateController,
-                mKeyguardStateController, () -> mKeyguardUnlockAnimationController);
+                mKeyguardStateController, () -> mKeyguardUnlockAnimationController,
+                mUnlockedScreenOffAnimationController);
         mViewMediator.start();
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt
index 126dca5..a2b5013 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt
@@ -23,6 +23,7 @@
 import android.testing.TestableLooper
 import android.text.TextUtils
 import android.view.View
+import android.widget.TextView
 import androidx.test.filters.SmallTest
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
@@ -52,6 +53,7 @@
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
+        context.ensureTestableResources()
 
         tileView = FakeTileView(context, iconView, false)
         customDrawableView = tileView.requireViewById(R.id.customDrawable)
@@ -117,7 +119,7 @@
     }
 
     @Test
-    fun testSecondaryLabelDescription_unavailable() {
+    fun testSecondaryLabelDescription_unavailable_default() {
         val state = QSTile.State()
         state.state = Tile.STATE_UNAVAILABLE
         state.secondaryLabel = ""
@@ -130,7 +132,7 @@
     }
 
     @Test
-    fun testSecondaryLabelDescription_booleanInactive() {
+    fun testSecondaryLabelDescription_booleanInactive_default() {
         val state = QSTile.BooleanState()
         state.state = Tile.STATE_INACTIVE
         state.secondaryLabel = ""
@@ -143,7 +145,7 @@
     }
 
     @Test
-    fun testSecondaryLabelDescription_booleanActive() {
+    fun testSecondaryLabelDescription_booleanActive_default() {
         val state = QSTile.BooleanState()
         state.state = Tile.STATE_ACTIVE
         state.secondaryLabel = ""
@@ -220,6 +222,41 @@
         assertThat(chevronView.visibility).isEqualTo(View.GONE)
     }
 
+    @Test
+    fun testUseStateStringsForKnownSpec_Boolean() {
+        val state = QSTile.BooleanState()
+        val spec = "internet"
+        state.spec = spec
+
+        val unavailableString = "${spec}_unavailable"
+        val offString = "${spec}_off"
+        val onString = "${spec}_on"
+
+        context.orCreateTestableResources.addOverride(R.array.tile_states_internet, arrayOf(
+            unavailableString,
+            offString,
+            onString
+        ))
+
+        // State UNAVAILABLE
+        state.secondaryLabel = ""
+        state.state = Tile.STATE_UNAVAILABLE
+        tileView.changeState(state)
+        assertThat((tileView.secondaryLabel as TextView).text).isEqualTo(unavailableString)
+
+        // State INACTIVE
+        state.secondaryLabel = ""
+        state.state = Tile.STATE_INACTIVE
+        tileView.changeState(state)
+        assertThat((tileView.secondaryLabel as TextView).text).isEqualTo(offString)
+
+        // State ACTIVE
+        state.secondaryLabel = ""
+        state.state = Tile.STATE_ACTIVE
+        tileView.changeState(state)
+        assertThat((tileView.secondaryLabel as TextView).text).isEqualTo(onString)
+    }
+
     class FakeTileView(
         context: Context,
         icon: QSIconView,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/TilesStatesTextTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/TilesStatesTextTest.kt
new file mode 100644
index 0000000..19ffa49
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/TilesStatesTextTest.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2021 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.qs.tileimpl
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class TilesStatesTextTest : SysuiTestCase() {
+
+    @Test
+    fun testStockTilesHaveStatesArray() {
+        val tiles = mContext.getString(R.string.quick_settings_tiles_stock).split(",")
+        tiles.forEach { spec ->
+            val resName = "${QSTileViewImpl.TILE_STATE_RES_PREFIX}$spec"
+            val resId = mContext.resources.getIdentifier(resName, "array", mContext.packageName)
+
+            assertNotEquals("Missing resource for $resName", 0, resId)
+
+            val array = mContext.resources.getStringArray(resId)
+
+            assertEquals("Array for $spec is of wrong size", 3, array.size)
+        }
+    }
+
+    @Test
+    fun testDefaultArray() {
+        val array = mContext.resources.getStringArray(R.array.tile_states_default)
+
+        assertThat(array.size).isEqualTo(3)
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewTest.java
index ee8d120..ffb53a8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewTest.java
@@ -160,6 +160,8 @@
     @Mock
     private DozeParameters mDozeParameters;
     @Mock
+    private UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
+    @Mock
     private NotificationPanelView mView;
     @Mock
     private LayoutInflater mLayoutInflater;
@@ -328,7 +330,8 @@
                         mock(HeadsUpManagerPhone.class),
                         new StatusBarStateControllerImpl(new UiEventLoggerFake()),
                         mKeyguardBypassController,
-                        mDozeParameters);
+                        mDozeParameters,
+                        mUnlockedScreenOffAnimationController);
         PulseExpansionHandler expansionHandler = new PulseExpansionHandler(
                 mContext,
                 coordinator,
@@ -386,7 +389,8 @@
                 mFragmentService,
                 mQuickAccessWalletController,
                 new FakeExecutor(new FakeSystemClock()),
-                mSecureSettings);
+                mSecureSettings,
+                mUnlockedScreenOffAnimationController);
         mNotificationPanelViewController.initDependencies(
                 mStatusBar,
                 mNotificationShelfController);
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
index 8b0b579..075d1dd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
@@ -108,6 +108,8 @@
     private DockManager mDockManager;
     @Mock
     private ConfigurationController mConfigurationController;
+    @Mock
+    private UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
 
 
     private static class AnimatorListener implements Animator.AnimatorListener {
@@ -221,7 +223,8 @@
         mScrimController = new ScrimController(mLightBarController,
                 mDozeParameters, mAlarmManager, mKeyguardStateController, mDelayedWakeLockBuilder,
                 new FakeHandler(mLooper.getLooper()), mKeyguardUpdateMonitor,
-                mDockManager, mConfigurationController, new FakeExecutor(new FakeSystemClock()));
+                mDockManager, mConfigurationController, new FakeExecutor(new FakeSystemClock()),
+                mUnlockedScreenOffAnimationController);
         mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible);
         mScrimController.attachViews(mScrimBehind, mNotificationsScrim, mScrimInFront,
                 mScrimForBubble);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
index 5a3683e..deff204 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
@@ -272,6 +272,7 @@
     @Mock private FeatureFlags mFeatureFlags;
     @Mock private IWallpaperManager mWallpaperManager;
     @Mock private KeyguardUnlockAnimationController mKeyguardUnlockAnimationController;
+    @Mock private UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
     private ShadeController mShadeController;
     private FakeExecutor mUiBgExecutor = new FakeExecutor(new FakeSystemClock());
     private InitController mInitController = new InitController();
@@ -441,7 +442,8 @@
                 mLocationPublisher,
                 mLockscreenTransitionController,
                 mFeatureFlags,
-                mKeyguardUnlockAnimationController);
+                mKeyguardUnlockAnimationController,
+                mUnlockedScreenOffAnimationController);
         when(mKeyguardViewMediator.registerStatusBar(any(StatusBar.class), any(ViewGroup.class),
                 any(NotificationPanelViewController.class), any(BiometricUnlockController.class),
                 any(ViewGroup.class), any(KeyguardBypassController.class)))
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 71d6a48..8041ec4 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -79,8 +79,10 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UptimeMillisLong;
+import android.annotation.UserIdInt;
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
+import android.app.ActivityManagerInternal.ServiceNotificationPolicy;
 import android.app.ActivityThread;
 import android.app.AppGlobals;
 import android.app.AppOpsManager;
@@ -213,6 +215,9 @@
     // at the same time.
     final int mMaxStartingBackground;
 
+    /**
+     * Master service bookkeeping, keyed by user number.
+     */
     final SparseArray<ServiceMap> mServiceMap = new SparseArray<>();
 
     /**
@@ -1811,7 +1816,7 @@
                         showFgsBgRestrictedNotificationLocked(r);
                         updateServiceForegroundLocked(psr, true);
                         ignoreForeground = true;
-                        logForegroundServiceStateChanged(r,
+                        logFGSStateChangeLocked(r,
                                 FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__DENIED,
                                 0);
                         if (CompatChanges.isChangeEnabled(FGS_START_EXCEPTION_CHANGE_ID,
@@ -1859,6 +1864,7 @@
                             active.mNumActive++;
                         }
                         r.isForeground = true;
+                        r.mLogEntering = true;
                         enterForeground = true;
                         r.mStartForegroundCount++;
                         r.mFgsEnterTime = SystemClock.uptimeMillis();
@@ -1881,14 +1887,7 @@
                     }
                     // Even if the service is already a FGS, we need to update the notification,
                     // so we need to call it again.
-                    postFgsNotificationLocked(r);
-                    if (enterForeground) {
-                        // Because we want to log what's updated in postFgsNotificationLocked(),
-                        // this must be called after postFgsNotificationLocked().
-                        logForegroundServiceStateChanged(r,
-                                FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__ENTER,
-                                0);
-                    }
+                    r.postNotification();
                     if (r.app != null) {
                         updateServiceForegroundLocked(psr, true);
                     }
@@ -1937,7 +1936,7 @@
                         AppOpsManager.getToken(mAm.mAppOpsService),
                         AppOpsManager.OP_START_FOREGROUND, r.appInfo.uid, r.packageName, null);
                 unregisterAppOpCallbackLocked(r);
-                logForegroundServiceStateChanged(r,
+                logFGSStateChangeLocked(r,
                         FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__EXIT,
                         r.mFgsExitTime > r.mFgsEnterTime
                                 ? (int)(r.mFgsExitTime - r.mFgsEnterTime) : 0);
@@ -1964,7 +1963,18 @@
         }
     }
 
-    private boolean withinFgsDeferRateLimit(final int uid, final long now) {
+    private boolean withinFgsDeferRateLimit(ServiceRecord sr, final long now) {
+        // If we're still within the service's deferral period, then by definition
+        // deferral is not rate limited.
+        if (now < sr.fgDisplayTime) {
+            if (DEBUG_FOREGROUND_SERVICE) {
+                Slog.d(TAG_SERVICE, "FGS transition for " + sr
+                        + " within deferral period, no rate limit applied");
+            }
+            return false;
+        }
+
+        final int uid = sr.appInfo.uid;
         final long eligible = mFgsDeferralEligible.get(uid, 0L);
         if (DEBUG_FOREGROUND_SERVICE) {
             if (now < eligible) {
@@ -1975,62 +1985,137 @@
         return now < eligible;
     }
 
-    // TODO: remove as part of fixing b/173627642
+    ServiceNotificationPolicy applyForegroundServiceNotificationLocked(Notification notification,
+            final int id, final String pkg, final int userId) {
+        if (DEBUG_FOREGROUND_SERVICE) {
+            Slog.d(TAG_SERVICE, "Evaluating FGS policy for id=" + id
+                    + " pkg=" + pkg + " not=" + notification);
+        }
+        // Is there an FGS using this notification?
+        final ServiceMap smap = mServiceMap.get(userId);
+        if (smap == null) {
+            // No services in this user at all
+            return ServiceNotificationPolicy.NOT_FOREGROUND_SERVICE;
+        }
+
+        for (int i = 0; i < smap.mServicesByInstanceName.size(); i++) {
+            final ServiceRecord sr = smap.mServicesByInstanceName.valueAt(i);
+            if (id != sr.foregroundId || !pkg.equals(sr.appInfo.packageName)) {
+                // Not this one; keep looking
+                continue;
+            }
+
+            // Found; it is associated with an FGS.  Make sure that it's flagged:
+            // it may have entered the bookkeeping outside of Service-related
+            // APIs.  We also make sure to take this latest Notification as
+            // the content to be shown (immediately or eventually).
+            if (DEBUG_FOREGROUND_SERVICE) {
+                Slog.d(TAG_SERVICE, "   FOUND: notification is for " + sr);
+            }
+            notification.flags |= Notification.FLAG_FOREGROUND_SERVICE;
+            sr.foregroundNoti = notification;
+
+            // ...and determine immediate vs deferred display policy for it
+            final boolean showNow = shouldShowFgsNotificationLocked(sr);
+            if (showNow) {
+                if (DEBUG_FOREGROUND_SERVICE) {
+                    Slog.d(TAG_SERVICE, "   Showing immediately due to policy");
+                }
+                sr.mFgsNotificationDeferred = false;
+                return ServiceNotificationPolicy.SHOW_IMMEDIATELY;
+            }
+
+            // Deferring - kick off the timer if necessary, and tell the caller
+            // that it's to be shown only if it's an update to already-
+            // visible content (e.g. if it's an FGS adopting a
+            // previously-posted Notification).
+            if (DEBUG_FOREGROUND_SERVICE) {
+                Slog.d(TAG_SERVICE, "   Deferring / update-only");
+            }
+            startFgsDeferralTimerLocked(sr);
+            return ServiceNotificationPolicy.UPDATE_ONLY;
+        }
+
+        // None of the services in this user are FGSs
+        return ServiceNotificationPolicy.NOT_FOREGROUND_SERVICE;
+    }
+
+    // No legacy-app behavior skew intended but there's a runtime E-stop if a need
+    // arises, so note that
     @SuppressWarnings("AndroidFrameworkCompatChange")
-    private void postFgsNotificationLocked(ServiceRecord r) {
-        final int uid = r.appInfo.uid;
+    private boolean shouldShowFgsNotificationLocked(ServiceRecord r) {
         final long now = SystemClock.uptimeMillis();
-        final boolean isLegacyApp = (r.appInfo.targetSdkVersion < Build.VERSION_CODES.S);
 
         // Is the behavior enabled at all?
-        boolean showNow = !mAm.mConstants.mFlagFgsNotificationDeferralEnabled;
-        if (!showNow) {
-            // Did the app have another FGS notification deferred recently?
-            showNow = withinFgsDeferRateLimit(uid, now);
+        if (!mAm.mConstants.mFlagFgsNotificationDeferralEnabled) {
+            return true;
         }
-        if (!showNow) {
-            // Legacy apps' FGS notifications are not deferred unless the relevant
+
+        // Has this service's deferral timer expired?
+        if (r.mFgsNotificationDeferred && now >= r.fgDisplayTime) {
+            if (DEBUG_FOREGROUND_SERVICE) {
+                Slog.d(TAG, "FGS reached end of deferral period: " + r);
+            }
+            return true;
+        }
+
+        // Did the app have another FGS notification deferred recently?
+        if (withinFgsDeferRateLimit(r, now)) {
+            return true;
+        }
+
+        if (mAm.mConstants.mFlagFgsNotificationDeferralApiGated) {
+            // Legacy apps' FGS notifications are also deferred unless the relevant
             // DeviceConfig element has been set
-            showNow = isLegacyApp && mAm.mConstants.mFlagFgsNotificationDeferralApiGated;
+            final boolean isLegacyApp = (r.appInfo.targetSdkVersion < Build.VERSION_CODES.S);
+            if (isLegacyApp) {
+                return true;
+            }
         }
-        if (!showNow) {
-            // has the app forced deferral?
-            if (!r.foregroundNoti.isForegroundDisplayForceDeferred()) {
-                // is the notification such that it should show right away?
-                showNow = r.foregroundNoti.shouldShowForegroundImmediately();
-                if (DEBUG_FOREGROUND_SERVICE && showNow) {
+
+        // did we already show it?
+        if (r.mFgsNotificationShown) {
+            return true;
+        }
+
+        // has the app forced deferral?
+        if (!r.foregroundNoti.isForegroundDisplayForceDeferred()) {
+            // is the notification such that it should show right away?
+            if (r.foregroundNoti.shouldShowForegroundImmediately()) {
+                if (DEBUG_FOREGROUND_SERVICE) {
                     Slog.d(TAG_SERVICE, "FGS " + r
                             + " notification policy says show immediately");
                 }
-                // or is this an type of FGS that always shows immediately?
-                if (!showNow) {
-                    if ((r.foregroundServiceType & FGS_IMMEDIATE_DISPLAY_MASK) != 0) {
-                        if (DEBUG_FOREGROUND_SERVICE) {
-                            Slog.d(TAG_SERVICE, "FGS " + r
-                                    + " type gets immediate display");
-                        }
-                        showNow = true;
-                    }
-                }
-            } else {
+                return true;
+            }
+
+            // or is this an type of FGS that always shows immediately?
+            if ((r.foregroundServiceType & FGS_IMMEDIATE_DISPLAY_MASK) != 0) {
                 if (DEBUG_FOREGROUND_SERVICE) {
-                    Slog.d(TAG_SERVICE, "FGS " + r + " notification is app deferred");
+                    Slog.d(TAG_SERVICE, "FGS " + r
+                            + " type gets immediate display");
                 }
+                return true;
             }
-        }
 
-        if (showNow) {
+            // fall through to return false: no policy dictates immediate display
+        } else {
             if (DEBUG_FOREGROUND_SERVICE) {
-                Slog.d(TAG_SERVICE, "FGS " + r + " non-deferred notification");
+                Slog.d(TAG_SERVICE, "FGS " + r + " notification is app deferred");
             }
-            r.postNotification();
-            r.mFgsNotificationDeferred = false;
-            r.mFgsNotificationShown = true;
-            return;
+            // fall through to return false
         }
 
-        r.mFgsNotificationDeferred = true;
-        r.mFgsNotificationShown = false;
+        return false;
+    }
+
+    // Target SDK consultation here is strictly for logging purposes, not
+    // behavioral variation.
+    @SuppressWarnings("AndroidFrameworkCompatChange")
+    private void startFgsDeferralTimerLocked(ServiceRecord r) {
+        final long now = SystemClock.uptimeMillis();
+        final int uid = r.appInfo.uid;
+
         // schedule the actual notification post
         long when = now + mAm.mConstants.mFgsNotificationDeferralInterval;
         // If there are already deferred FGS notifications for this app,
@@ -2053,11 +2138,14 @@
         final long nextEligible = when + mAm.mConstants.mFgsNotificationDeferralExclusionTime;
         mFgsDeferralEligible.put(uid, nextEligible);
         r.fgDisplayTime = when;
+        r.mFgsNotificationDeferred = true;
+        r.mFgsNotificationShown = false;
         mPendingFgsNotifications.add(r);
         if (DEBUG_FOREGROUND_SERVICE) {
             Slog.d(TAG_SERVICE, "FGS " + r
                     + " notification in " + (when - now) + " ms");
         }
+        final boolean isLegacyApp = (r.appInfo.targetSdkVersion < Build.VERSION_CODES.S);
         if (isLegacyApp) {
             Slog.i(TAG_SERVICE, "Deferring FGS notification in legacy app "
                     + r.appInfo.packageName + "/" + UserHandle.formatUid(r.appInfo.uid)
@@ -2089,9 +2177,16 @@
                         if (r.isForeground && r.app != null) {
                             r.postNotification();
                             r.mFgsNotificationShown = true;
-                        } else if (DEBUG_FOREGROUND_SERVICE) {
-                            Slog.d(TAG_SERVICE, "  - service no longer running/fg, ignoring");
+                        } else {
+                            if (DEBUG_FOREGROUND_SERVICE) {
+                                Slog.d(TAG_SERVICE, "  - service no longer running/fg, ignoring");
+                            }
                         }
+                        // Regardless of whether we needed to post the notification or the
+                        // service is no longer running, we may not have logged its FGS
+                        // transition yet depending on the timing and API sequence that led
+                        // to this point - so make sure to do so.
+                        maybeLogFGSStateEnteredLocked(r);
                     }
                 }
                 if (DEBUG_FOREGROUND_SERVICE) {
@@ -2102,6 +2197,60 @@
         }
     };
 
+    private void maybeLogFGSStateEnteredLocked(ServiceRecord r) {
+        if (r.mLogEntering) {
+            logFGSStateChangeLocked(r,
+                    FrameworkStatsLog
+                            .FOREGROUND_SERVICE_STATE_CHANGED__STATE__ENTER,
+                    0);
+            r.mLogEntering = false;
+        }
+    }
+
+    /**
+     * Callback from NotificationManagerService whenever it posts a notification
+     * associated with a foreground service.  This is the unified handling point
+     * for the disjoint code flows that affect an FGS's notifiation content and
+     * visibility, starting with both Service.startForeground() and
+     * NotificationManager.notify().
+     */
+    public void onForegroundServiceNotificationUpdateLocked(Notification notification,
+            final int id, final String pkg, @UserIdInt final int userId) {
+        // If this happens to be a Notification for an FGS still in its deferral period,
+        // drop the deferral and make sure our content bookkeeping is up to date.
+        for (int i = mPendingFgsNotifications.size() - 1; i >= 0; i--) {
+            final ServiceRecord sr = mPendingFgsNotifications.get(i);
+            if (userId == sr.userId
+                    && id == sr.foregroundId
+                    && sr.appInfo.packageName.equals(pkg)) {
+                if (DEBUG_FOREGROUND_SERVICE) {
+                    Slog.d(TAG_SERVICE, "Notification shown; canceling deferral of "
+                            + sr);
+                }
+                maybeLogFGSStateEnteredLocked(sr);
+                sr.mFgsNotificationShown = true;
+                sr.mFgsNotificationDeferred = false;
+                mPendingFgsNotifications.remove(i);
+            }
+        }
+        // And make sure to retain the latest notification content for the FGS
+        ServiceMap smap = mServiceMap.get(userId);
+        if (smap != null) {
+            for (int i = 0; i < smap.mServicesByInstanceName.size(); i++) {
+                final ServiceRecord sr = smap.mServicesByInstanceName.valueAt(i);
+                if (sr.isForeground
+                        && id == sr.foregroundId
+                        && sr.appInfo.packageName.equals(pkg)) {
+                    if (DEBUG_FOREGROUND_SERVICE) {
+                        Slog.d(TAG_SERVICE, "Recording shown notification for "
+                                + sr);
+                    }
+                    sr.foregroundNoti = notification;
+                }
+            }
+        }
+    }
+
     /** Registers an AppOpCallback for monitoring special AppOps for this foreground service. */
     private void registerAppOpCallbackLocked(@NonNull ServiceRecord r) {
         if (r.app == null) {
@@ -4016,7 +4165,7 @@
                     AppOpsManager.OP_START_FOREGROUND, r.appInfo.uid, r.packageName, null);
             unregisterAppOpCallbackLocked(r);
             r.mFgsExitTime = SystemClock.uptimeMillis();
-            logForegroundServiceStateChanged(r,
+            logFGSStateChangeLocked(r,
                     FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__EXIT,
                     r.mFgsExitTime > r.mFgsEnterTime
                             ? (int)(r.mFgsExitTime - r.mFgsEnterTime) : 0);
@@ -6022,7 +6171,7 @@
      * @param state one of ENTER/EXIT/DENIED event.
      * @param durationMs Only meaningful for EXIT event, the duration from ENTER and EXIT state.
      */
-    private void logForegroundServiceStateChanged(ServiceRecord r, int state, int durationMs) {
+    private void logFGSStateChangeLocked(ServiceRecord r, int state, int durationMs) {
         if (!ActivityManagerUtils.shouldSamplePackageForAtom(
                 r.packageName, mAm.mConstants.mFgsAtomSampleRate)) {
             return;
diff --git a/services/core/java/com/android/server/am/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java
index 0fff8be..0d19efc 100644
--- a/services/core/java/com/android/server/am/ActivityManagerConstants.java
+++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java
@@ -453,7 +453,7 @@
     volatile long mFgsNotificationDeferralInterval = 10_000;
 
     // Rate limit: minimum time after an app's FGS notification is deferred
-    // before another FGS notifiction from that app can be deferred.
+    // before another FGS notification from that app can be deferred.
     volatile long mFgsNotificationDeferralExclusionTime = 2 * 60 * 1000L;
 
     /**
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 3e6a0a8..6e500e4 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -16059,6 +16059,24 @@
         }
 
         @Override
+        public ServiceNotificationPolicy applyForegroundServiceNotification(
+                Notification notification, int id, String pkg, int userId) {
+            synchronized (ActivityManagerService.this) {
+                return mServices.applyForegroundServiceNotificationLocked(notification,
+                        id, pkg, userId);
+            }
+        }
+
+        @Override
+        public void onForegroundServiceNotificationUpdate(Notification notification,
+                int id, String pkg, @UserIdInt int userId) {
+            synchronized (ActivityManagerService.this) {
+                mServices.onForegroundServiceNotificationUpdateLocked(notification,
+                        id, pkg, userId);
+            }
+        }
+
+        @Override
         public void stopForegroundServicesForChannel(String pkg, int userId,
                 String channelId) {
             synchronized (ActivityManagerService.this) {
diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java
index dbb2f65..dd1ddd7 100644
--- a/services/core/java/com/android/server/am/ServiceRecord.java
+++ b/services/core/java/com/android/server/am/ServiceRecord.java
@@ -109,6 +109,7 @@
     boolean fgWaiting;      // is a timeout for going foreground already scheduled?
     boolean isNotAppComponentUsage; // is service binding not considered component/package usage?
     boolean isForeground;   // is service currently in foreground mode?
+    boolean mLogEntering;    // need to report fgs transition once deferral policy is known
     int foregroundId;       // Notification ID of last foreground req.
     Notification foregroundNoti; // Notification record of foreground state.
     long fgDisplayTime;     // time at which the FGS notification should become visible
diff --git a/services/core/java/com/android/server/notification/NotificationManagerInternal.java b/services/core/java/com/android/server/notification/NotificationManagerInternal.java
index dc9839c..0528b95 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerInternal.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerInternal.java
@@ -30,6 +30,9 @@
     void cancelNotification(String pkg, String basePkg, int callingUid, int callingPid,
             String tag, int id, int userId);
 
+    /** is the given notification currently showing? */
+    boolean isNotificationShown(String pkg, String tag, int notificationId, int userId);
+
     void removeForegroundServiceFlagFromNotification(String pkg, int notificationId, int userId);
 
     void onConversationRemoved(String pkg, int uid, Set<String> shortcuts);
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 0dd9b29..28c64f8 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -130,6 +130,7 @@
 import android.annotation.WorkerThread;
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
+import android.app.ActivityManagerInternal.ServiceNotificationPolicy;
 import android.app.AlarmManager;
 import android.app.AppGlobals;
 import android.app.AppOpsManager;
@@ -211,9 +212,7 @@
 import android.os.Trace;
 import android.os.UserHandle;
 import android.os.UserManager;
-import android.os.VibrationAttributes;
 import android.os.VibrationEffect;
-import android.os.Vibrator;
 import android.provider.DeviceConfig;
 import android.provider.Settings;
 import android.service.notification.Adjustment;
@@ -369,12 +368,8 @@
     // 1 second past the ANR timeout.
     static final int FINISH_TOKEN_TIMEOUT = 11 * 1000;
 
-    static final long[] DEFAULT_VIBRATE_PATTERN = {0, 250, 250, 250};
-
     static final long SNOOZE_UNTIL_UNSPECIFIED = -1;
 
-    static final int VIBRATE_PATTERN_MAXLEN = 8 * 2 + 1; // up to eight bumps
-
     static final int INVALID_UID = -1;
     static final String ROOT_PKG = "root";
 
@@ -482,7 +477,6 @@
     AudioManagerInternal mAudioManagerInternal;
     // Can be null for wear
     @Nullable StatusBarManagerInternal mStatusBar;
-    Vibrator mVibrator;
     private WindowManagerInternal mWindowManagerInternal;
     private AlarmManager mAlarmManager;
     private ICompanionDeviceManager mCompanionManager;
@@ -504,7 +498,6 @@
     private LogicalLight mNotificationLight;
     LogicalLight mAttentionLight;
 
-    private long[] mFallbackVibrationPattern;
     private boolean mUseAttentionLight;
     boolean mHasLight = true;
     boolean mLightEnabled;
@@ -583,6 +576,7 @@
     RankingHelper mRankingHelper;
     @VisibleForTesting
     PreferencesHelper mPreferencesHelper;
+    private VibratorHelper mVibratorHelper;
 
     private final UserProfiles mUserProfiles = new UserProfiles();
     private NotificationListeners mListeners;
@@ -1597,10 +1591,7 @@
         mVibrateNotificationKey = null;
         final long identity = Binder.clearCallingIdentity();
         try {
-            // Stop all vibrations with usage of class alarm (ringtone, alarm, notification usages).
-            int usageFilter =
-                    VibrationAttributes.USAGE_CLASS_ALARM | ~VibrationAttributes.USAGE_CLASS_MASK;
-            mVibrator.cancel(usageFilter);
+            mVibratorHelper.cancelVibration();
         } finally {
             Binder.restoreCallingIdentity(identity);
         }
@@ -1994,19 +1985,6 @@
     private SettingsObserver mSettingsObserver;
     protected ZenModeHelper mZenModeHelper;
 
-    static long[] getLongArray(Resources r, int resid, int maxlen, long[] def) {
-        int[] ar = r.getIntArray(resid);
-        if (ar == null) {
-            return def;
-        }
-        final int len = ar.length > maxlen ? maxlen : ar.length;
-        long[] out = new long[len];
-        for (int i=0; i<len; i++) {
-            out[i] = ar[i];
-        }
-        return out;
-    }
-
     public NotificationManagerService(Context context) {
         this(context,
                 new NotificationRecordLoggerImpl(),
@@ -2040,13 +2018,18 @@
     }
 
     @VisibleForTesting
-    void setHints(int hints) {
-        mListenerHints = hints;
+    VibratorHelper getVibratorHelper() {
+        return mVibratorHelper;
     }
 
     @VisibleForTesting
-    void setVibrator(Vibrator vibrator) {
-        mVibrator = vibrator;
+    void setVibratorHelper(VibratorHelper helper) {
+        mVibratorHelper = helper;
+    }
+
+    @VisibleForTesting
+    void setHints(int hints) {
+        mListenerHints = hints;
     }
 
     @VisibleForTesting
@@ -2121,11 +2104,6 @@
     }
 
     @VisibleForTesting
-    void setFallbackVibrationPattern(long[] vibrationPattern) {
-        mFallbackVibrationPattern = vibrationPattern;
-    }
-
-    @VisibleForTesting
     void setPackageManager(IPackageManager packageManager) {
         mPackageManager = packageManager;
     }
@@ -2199,7 +2177,6 @@
         mPackageManager = packageManager;
         mPackageManagerClient = packageManagerClient;
         mAppOps = appOps;
-        mVibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE);
         mAppUsageStats = appUsageStats;
         mAlarmManager = (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE);
         mCompanionManager = companionManager;
@@ -2287,6 +2264,7 @@
                 extractorNames);
         mSnoozeHelper = snoozeHelper;
         mGroupHelper = groupHelper;
+        mVibratorHelper = new VibratorHelper(getContext());
         mHistoryManager = historyManager;
 
         // This is a ManagedServices object that keeps track of the listeners.
@@ -2308,10 +2286,6 @@
         mNotificationLight = lightsManager.getLight(LightsManager.LIGHT_ID_NOTIFICATIONS);
         mAttentionLight = lightsManager.getLight(LightsManager.LIGHT_ID_ATTENTION);
 
-        mFallbackVibrationPattern = getLongArray(resources,
-                R.array.config_notificationFallbackVibePattern,
-                VIBRATE_PATTERN_MAXLEN,
-                DEFAULT_VIBRATE_PATTERN);
         mInCallNotificationUri = Uri.parse("file://" +
                 resources.getString(R.string.config_inCallNotificationSound));
         mInCallNotificationAudioAttributes = new AudioAttributes.Builder()
@@ -3078,6 +3052,20 @@
         }
     }
 
+    protected void maybeReportForegroundServiceUpdate(final NotificationRecord r) {
+        if (r.isForegroundService()) {
+            // snapshot live state for the asynchronous operation
+            final StatusBarNotification sbn = r.getSbn();
+            final Notification notification = sbn.getNotification();
+            final int id = sbn.getId();
+            final String pkg = sbn.getPackageName();
+            final int userId = sbn.getUser().getIdentifier();
+            mHandler.post(() -> {
+                mAmi.onForegroundServiceNotificationUpdate(notification, id, pkg, userId);
+            });
+        }
+    }
+
     private String getHistoryTitle(Notification n) {
         CharSequence title = null;
         if (n.extras != null) {
@@ -5070,7 +5058,7 @@
         }
 
         @Override
-        public boolean isNotificationPolicyAccessGrantedForPackage(String pkg) {;
+        public boolean isNotificationPolicyAccessGrantedForPackage(String pkg) {
             enforceSystemOrSystemUIOrSamePackage(pkg,
                     "request policy access status for another package");
             return checkPolicyAccess(pkg);
@@ -5725,9 +5713,7 @@
                 summaryNotification.extras.putAll(extras);
                 Intent appIntent = getContext().getPackageManager().getLaunchIntentForPackage(pkg);
                 if (appIntent != null) {
-                    final ActivityManagerInternal ami = LocalServices
-                            .getService(ActivityManagerInternal.class);
-                    summaryNotification.contentIntent = ami.getPendingIntentActivityAsApp(
+                    summaryNotification.contentIntent = mAmi.getPendingIntentActivityAsApp(
                             0, appIntent, PendingIntent.FLAG_IMMUTABLE, null,
                             pkg, appInfo.uid);
                 }
@@ -5778,7 +5764,7 @@
             return "callState";
         }
         return null;
-    };
+    }
 
     private void dumpJson(PrintWriter pw, @NonNull DumpFilter filter) {
         JSONObject dump = new JSONObject();
@@ -6059,6 +6045,11 @@
         }
 
         @Override
+        public boolean isNotificationShown(String pkg, String tag, int notificationId, int userId) {
+            return isNotificationShownInternal(pkg, tag, notificationId, userId);
+        }
+
+        @Override
         public void removeForegroundServiceFlagFromNotification(String pkg, int notificationId,
                 int userId) {
             checkCallerIsSystem();
@@ -6141,16 +6132,22 @@
                 mustNotHaveFlags, false, userId, REASON_APP_CANCEL, null);
     }
 
+    boolean isNotificationShownInternal(String pkg, String tag, int notificationId, int userId) {
+        synchronized (mNotificationLock) {
+            return findNotificationLocked(pkg, tag, notificationId, userId) != null;
+        }
+    }
+
     void enqueueNotificationInternal(final String pkg, final String opPkg, final int callingUid,
             final int callingPid, final String tag, final int id, final Notification notification,
             int incomingUserId) {
         enqueueNotificationInternal(pkg, opPkg, callingUid, callingPid, tag, id, notification,
-        incomingUserId, false);
+                incomingUserId, false);
     }
 
     void enqueueNotificationInternal(final String pkg, final String opPkg, final int callingUid,
-        final int callingPid, final String tag, final int id, final Notification notification,
-        int incomingUserId, boolean postSilently) {
+            final int callingPid, final String tag, final int id, final Notification notification,
+            int incomingUserId, boolean postSilently) {
         if (DBG) {
             Slog.v(TAG, "enqueueNotificationInternal: pkg=" + pkg + " id=" + id
                     + " notification=" + notification);
@@ -6185,6 +6182,22 @@
             return;
         }
 
+        // Notifications passed to setForegroundService() have FLAG_FOREGROUND_SERVICE,
+        // but it's also possible that the app has called notify() with an update to an
+        // FGS notification that hasn't yet been displayed.  Make sure we check for any
+        // FGS-related situation up front, outside of any locks so it's safe to call into
+        // the Activity Manager.
+        final ServiceNotificationPolicy policy = mAmi.applyForegroundServiceNotification(
+                notification, id, pkg, userId);
+        if (policy == ServiceNotificationPolicy.UPDATE_ONLY) {
+            // Proceed if the notification is already showing/known, otherwise ignore
+            // because the service lifecycle logic has retained responsibility for its
+            // handling.
+            if (!isNotificationShownInternal(pkg, tag, id, userId)) {
+                return;
+            }
+        }
+
         mUsageStats.registerEnqueuedByApp(pkg);
 
         final StatusBarNotification n = new StatusBarNotification(
@@ -6287,19 +6300,17 @@
         if (notification.allPendingIntents != null) {
             final int intentCount = notification.allPendingIntents.size();
             if (intentCount > 0) {
-                final ActivityManagerInternal am = LocalServices
-                        .getService(ActivityManagerInternal.class);
                 final long duration = LocalServices.getService(
                         DeviceIdleInternal.class).getNotificationAllowlistDuration();
                 for (int i = 0; i < intentCount; i++) {
                     PendingIntent pendingIntent = notification.allPendingIntents.valueAt(i);
                     if (pendingIntent != null) {
-                        am.setPendingIntentAllowlistDuration(pendingIntent.getTarget(),
+                        mAmi.setPendingIntentAllowlistDuration(pendingIntent.getTarget(),
                                 ALLOWLIST_TOKEN, duration,
                                 TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED,
                                 REASON_NOTIFICATION_SERVICE,
                                 "NotificationManagerService");
-                        am.setPendingIntentAllowBgActivityStarts(pendingIntent.getTarget(),
+                        mAmi.setPendingIntentAllowBgActivityStarts(pendingIntent.getTarget(),
                                 ALLOWLIST_TOKEN, (FLAG_ACTIVITY_SENDER | FLAG_BROADCAST_SENDER
                                         | FLAG_SERVICE_SENDER));
                     }
@@ -6821,12 +6832,14 @@
                         mUsageStats.registerClickedByUser(r);
                     }
 
-                    if (mReason == REASON_LISTENER_CANCEL
-                            && r.getNotification().isBubbleNotification()) {
+                    if ((mReason == REASON_LISTENER_CANCEL
+                            && r.getNotification().isBubbleNotification())
+                            || (mReason == REASON_CLICK && r.canBubble()
+                            && r.isFlagBubbleRemoved())) {
                         boolean isBubbleSuppressed = r.getNotification().getBubbleMetadata() != null
                                 && r.getNotification().getBubbleMetadata().isBubbleSuppressed();
                         mNotificationDelegate.onBubbleNotificationSuppressionChanged(
-                                r.getKey(), true /* suppressed */, isBubbleSuppressed);
+                                r.getKey(), true /* notifSuppressed */, isBubbleSuppressed);
                         return;
                     }
 
@@ -7108,6 +7121,7 @@
 
                     maybeRecordInterruptionLocked(r);
                     maybeRegisterMessageSent(r);
+                    maybeReportForegroundServiceUpdate(r);
 
                     // Log event to statsd
                     mNotificationRecordLogger.maybeLogNotificationPosted(r, old, position,
@@ -7398,7 +7412,7 @@
             if (mSystemReady && mAudioManager != null) {
                 Uri soundUri = record.getSound();
                 hasValidSound = soundUri != null && !Uri.EMPTY.equals(soundUri);
-                long[] vibration = record.getVibration();
+                VibrationEffect vibration = record.getVibration();
                 // Demote sound to vibration if vibration missing & phone in vibration mode.
                 if (vibration == null
                         && hasValidSound
@@ -7406,7 +7420,8 @@
                         == AudioManager.RINGER_MODE_VIBRATE)
                         && mAudioManager.getStreamVolume(
                         AudioAttributes.toLegacyStreamType(record.getAudioAttributes())) == 0) {
-                    vibration = mFallbackVibrationPattern;
+                    boolean insistent = (record.getFlags() & Notification.FLAG_INSISTENT) != 0;
+                    vibration = mVibratorHelper.createFallbackVibration(insistent);
                 }
                 hasValidVibrate = vibration != null;
                 boolean hasAudibleAlert = hasValidSound || hasValidVibrate;
@@ -7633,23 +7648,12 @@
         return false;
     }
 
-    private boolean playVibration(final NotificationRecord record, long[] vibration,
+    private boolean playVibration(final NotificationRecord record, final VibrationEffect effect,
             boolean delayVibForSound) {
         // Escalate privileges so we can use the vibrator even if the
         // notifying app does not have the VIBRATE permission.
         final long identity = Binder.clearCallingIdentity();
         try {
-            final VibrationEffect effect;
-            try {
-                final boolean insistent =
-                        (record.getNotification().flags & Notification.FLAG_INSISTENT) != 0;
-                effect = VibrationEffect.createWaveform(
-                        vibration, insistent ? 0 : -1 /*repeatIndex*/);
-            } catch (IllegalArgumentException e) {
-                Slog.e(TAG, "Error creating vibration waveform with pattern: " +
-                        Arrays.toString(vibration));
-                return false;
-            }
             if (delayVibForSound) {
                 new Thread(() -> {
                     // delay the vibration by the same amount as the notification sound
@@ -7686,8 +7690,7 @@
         // to the reason so we can still debug from bugreports
         String reason = "Notification (" + record.getSbn().getOpPkg() + " "
                 + record.getSbn().getUid() + ") " + (delayed ? "(Delayed)" : "");
-        mVibrator.vibrate(Process.SYSTEM_UID, PackageManagerService.PLATFORM_PACKAGE_NAME,
-                effect, reason, record.getAudioAttributes());
+        mVibratorHelper.vibrate(effect, record.getAudioAttributes(), reason);
     }
 
     private boolean isNotificationForCurrentUser(NotificationRecord record) {
diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java
index e875065..f66cfa9 100644
--- a/services/core/java/com/android/server/notification/NotificationRecord.java
+++ b/services/core/java/com/android/server/notification/NotificationRecord.java
@@ -47,6 +47,7 @@
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.UserHandle;
+import android.os.VibrationEffect;
 import android.provider.Settings;
 import android.service.notification.Adjustment;
 import android.service.notification.NotificationListenerService;
@@ -158,7 +159,7 @@
     private String mUserExplanation;
     private boolean mPreChannelsNotification = true;
     private Uri mSound;
-    private long[] mVibration;
+    private VibrationEffect mVibration;
     private AudioAttributes mAttributes;
     private NotificationChannel mChannel;
     private ArrayList<String> mPeopleOverride;
@@ -287,29 +288,28 @@
         return light;
     }
 
-    private long[] calculateVibration() {
-        long[] vibration;
-        final long[] defaultVibration =  NotificationManagerService.getLongArray(
-                mContext.getResources(),
-                com.android.internal.R.array.config_defaultNotificationVibePattern,
-                NotificationManagerService.VIBRATE_PATTERN_MAXLEN,
-                NotificationManagerService.DEFAULT_VIBRATE_PATTERN);
+    private VibrationEffect calculateVibration() {
+        VibratorHelper helper = new VibratorHelper(mContext);
+        final Notification notification = getSbn().getNotification();
+        final boolean insistent = (notification.flags & Notification.FLAG_INSISTENT) != 0;
+        VibrationEffect defaultVibration = helper.createDefaultVibration(insistent);
+        VibrationEffect vibration;
         if (getChannel().shouldVibrate()) {
             vibration = getChannel().getVibrationPattern() == null
-                    ? defaultVibration : getChannel().getVibrationPattern();
+                    ? defaultVibration
+                    : helper.createWaveformVibration(getChannel().getVibrationPattern(), insistent);
         } else {
             vibration = null;
         }
         if (mPreChannelsNotification
                 && (getChannel().getUserLockedFields()
                 & NotificationChannel.USER_LOCKED_VIBRATION) == 0) {
-            final Notification notification = getSbn().getNotification();
             final boolean useDefaultVibrate =
                     (notification.defaults & Notification.DEFAULT_VIBRATE) != 0;
             if (useDefaultVibrate) {
                 vibration = defaultVibration;
             } else {
-                vibration = notification.vibrate;
+                vibration = helper.createWaveformVibration(notification.vibrate, insistent);
             }
         }
         return vibration;
@@ -877,6 +877,10 @@
         return mHidden;
     }
 
+    public boolean isForegroundService() {
+        return 0 != (getFlags() & Notification.FLAG_FOREGROUND_SERVICE);
+    }
+
     /**
      * Override of all alerting information on the channel and notification. Used when notifications
      * are reposted in response to direct user action and thus don't need to alert.
@@ -1067,7 +1071,7 @@
         return mSound;
     }
 
-    public long[] getVibration() {
+    public VibrationEffect getVibration() {
         return mVibration;
     }
 
diff --git a/services/core/java/com/android/server/notification/VibratorHelper.java b/services/core/java/com/android/server/notification/VibratorHelper.java
new file mode 100644
index 0000000..f25b047
--- /dev/null
+++ b/services/core/java/com/android/server/notification/VibratorHelper.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2021 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.notification;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Resources;
+import android.media.AudioAttributes;
+import android.os.Process;
+import android.os.VibrationAttributes;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+import android.util.Slog;
+
+import com.android.internal.R;
+import com.android.server.pm.PackageManagerService;
+
+import java.util.Arrays;
+
+/**
+ * NotificationManagerService helper for functionality related to the vibrator.
+ */
+public final class VibratorHelper {
+    private static final String TAG = "NotificationVibratorHelper";
+
+    private static final long[] DEFAULT_VIBRATE_PATTERN = {0, 250, 250, 250};
+    private static final int VIBRATE_PATTERN_MAXLEN = 8 * 2 + 1; // up to eight bumps
+
+    private final Vibrator mVibrator;
+    private final long[] mDefaultPattern;
+    private final long[] mFallbackPattern;
+
+    public VibratorHelper(Context context) {
+        mVibrator = context.getSystemService(Vibrator.class);
+        mDefaultPattern = getLongArray(
+                context.getResources(),
+                com.android.internal.R.array.config_defaultNotificationVibePattern,
+                VIBRATE_PATTERN_MAXLEN,
+                DEFAULT_VIBRATE_PATTERN);
+        mFallbackPattern = getLongArray(context.getResources(),
+                R.array.config_notificationFallbackVibePattern,
+                VIBRATE_PATTERN_MAXLEN,
+                DEFAULT_VIBRATE_PATTERN);
+    }
+
+    /**
+     * Safely create a {@link VibrationEffect} from given vibration {@code pattern}.
+     *
+     * <p>This method returns {@code null} if the pattern is also {@code null} or invalid.
+     *
+     * @param pattern The off/on vibration pattern, where each item is a duration in milliseconds.
+     * @param insistent {@code true} if the vibration should loop until it is cancelled.
+     */
+    @Nullable
+    public static VibrationEffect createWaveformVibration(@Nullable long[] pattern,
+            boolean insistent) {
+        try {
+            if (pattern != null) {
+                return VibrationEffect.createWaveform(pattern, /* repeat= */ insistent ? 0 : -1);
+            }
+        } catch (IllegalArgumentException e) {
+            Slog.e(TAG, "Error creating vibration waveform with pattern: "
+                    + Arrays.toString(pattern));
+        }
+        return null;
+    }
+
+    /**
+     * Vibrate the device with given {@code effect}.
+     *
+     * <p>We need to vibrate as "android" so we can breakthrough DND.
+     */
+    public void vibrate(VibrationEffect effect, AudioAttributes attrs, String reason) {
+        mVibrator.vibrate(Process.SYSTEM_UID, PackageManagerService.PLATFORM_PACKAGE_NAME,
+                effect, reason, attrs);
+    }
+
+    /** Stop all notification vibrations (ringtone, alarm, notification usages). */
+    public void cancelVibration() {
+        int usageFilter =
+                VibrationAttributes.USAGE_CLASS_ALARM | ~VibrationAttributes.USAGE_CLASS_MASK;
+        mVibrator.cancel(usageFilter);
+    }
+
+    /**
+     * Creates a vibration to be used as fallback when the device is in vibrate mode.
+     *
+     * @param insistent {@code true} if the vibration should loop until it is cancelled.
+     */
+    public VibrationEffect createFallbackVibration(boolean insistent) {
+        if (mVibrator.hasFrequencyControl()) {
+            return createChirpVibration(insistent);
+        }
+        return createWaveformVibration(mFallbackPattern, insistent);
+    }
+
+    /**
+     * Creates a vibration to be used by notifications without a custom pattern.
+     *
+     * @param insistent {@code true} if the vibration should loop until it is cancelled.
+     */
+    public VibrationEffect createDefaultVibration(boolean insistent) {
+        if (mVibrator.hasFrequencyControl()) {
+            return createChirpVibration(insistent);
+        }
+        return createWaveformVibration(mDefaultPattern, insistent);
+    }
+
+    private static VibrationEffect createChirpVibration(boolean insistent) {
+        VibrationEffect.WaveformBuilder waveformBuilder = VibrationEffect.startWaveform()
+                .addStep(/* amplitude= */ 0, /* frequency= */ -0.85f, /* duration= */ 0)
+                .addRamp(/* amplitude= */ 1, /* frequency= */ -0.25f, /* duration= */ 100)
+                .addStep(/* amplitude= */ 1, /* duration= */ 150)
+                .addRamp(/* amplitude= */ 0, /* frequency= */ -0.85f, /* duration= */ 250);
+
+        if (insistent) {
+            return waveformBuilder.build(/* repeat= */ 0);
+        }
+
+        VibrationEffect singleBeat = waveformBuilder.build();
+        return VibrationEffect.startComposition()
+                .addEffect(singleBeat)
+                .addEffect(singleBeat)
+                .compose();
+    }
+
+    private static long[] getLongArray(Resources resources, int resId, int maxLength, long[] def) {
+        int[] ar = resources.getIntArray(resId);
+        if (ar == null) {
+            return def;
+        }
+        final int len = ar.length > maxLength ? maxLength : ar.length;
+        long[] out = new long[len];
+        for (int i = 0; i < len; i++) {
+            out[i] = ar[i];
+        }
+        return out;
+    }
+}
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 9c25159..ae8f967 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -436,6 +436,7 @@
     volatile boolean mPowerKeyHandled;
     volatile boolean mBackKeyHandled;
     volatile boolean mEndCallKeyHandled;
+    volatile boolean mCameraGestureTriggered;
     volatile boolean mCameraGestureTriggeredDuringGoingToSleep;
 
     /**
@@ -3833,6 +3834,9 @@
         final MutableBoolean outLaunched = new MutableBoolean(false);
         final boolean gesturedServiceIntercepted = gestureService.interceptPowerKeyDown(event,
                 interactive, outLaunched);
+        if (outLaunched.value) {
+            mCameraGestureTriggered = true;
+        }
         if (outLaunched.value && mRequestedOrSleepingDefaultDisplay) {
             mCameraGestureTriggeredDuringGoingToSleep = true;
         }
@@ -4209,13 +4213,13 @@
         mDefaultDisplayRotation.updateOrientationListener();
 
         if (mKeyguardDelegate != null) {
-            mKeyguardDelegate.onFinishedGoingToSleep(pmSleepReason,
-                    mCameraGestureTriggeredDuringGoingToSleep);
+            mKeyguardDelegate.onFinishedGoingToSleep(pmSleepReason, mCameraGestureTriggered);
         }
         if (mDisplayFoldController != null) {
             mDisplayFoldController.finishedGoingToSleep();
         }
         mCameraGestureTriggeredDuringGoingToSleep = false;
+        mCameraGestureTriggered = false;
     }
 
     // Called on the PowerManager's Notifier thread.
@@ -4242,8 +4246,10 @@
         mDefaultDisplayRotation.updateOrientationListener();
 
         if (mKeyguardDelegate != null) {
-            mKeyguardDelegate.onStartedWakingUp(pmWakeReason);
+            mKeyguardDelegate.onStartedWakingUp(pmWakeReason, mCameraGestureTriggered);
         }
+
+        mCameraGestureTriggered = false;
     }
 
     // Called on the PowerManager's Notifier thread.
diff --git a/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java b/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java
index 44f14b4..cdce660 100644
--- a/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java
+++ b/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java
@@ -178,7 +178,8 @@
                 // This is used to hide the scrim once keyguard displays.
                 if (mKeyguardState.interactiveState == INTERACTIVE_STATE_AWAKE
                         || mKeyguardState.interactiveState == INTERACTIVE_STATE_WAKING) {
-                    mKeyguardService.onStartedWakingUp(PowerManager.WAKE_REASON_UNKNOWN);
+                    mKeyguardService.onStartedWakingUp(PowerManager.WAKE_REASON_UNKNOWN,
+                            false /* cameraGestureTriggered */);
                 }
                 if (mKeyguardState.interactiveState == INTERACTIVE_STATE_AWAKE) {
                     mKeyguardService.onFinishedWakingUp();
@@ -297,10 +298,11 @@
         mKeyguardState.dreaming = false;
     }
 
-    public void onStartedWakingUp(@PowerManager.WakeReason int pmWakeReason) {
+    public void onStartedWakingUp(
+            @PowerManager.WakeReason int pmWakeReason, boolean cameraGestureTriggered) {
         if (mKeyguardService != null) {
             if (DEBUG) Log.v(TAG, "onStartedWakingUp()");
-            mKeyguardService.onStartedWakingUp(pmWakeReason);
+            mKeyguardService.onStartedWakingUp(pmWakeReason, cameraGestureTriggered);
         }
         mKeyguardState.interactiveState = INTERACTIVE_STATE_WAKING;
     }
diff --git a/services/core/java/com/android/server/policy/keyguard/KeyguardServiceWrapper.java b/services/core/java/com/android/server/policy/keyguard/KeyguardServiceWrapper.java
index 0872b3a..855a1cc 100644
--- a/services/core/java/com/android/server/policy/keyguard/KeyguardServiceWrapper.java
+++ b/services/core/java/com/android/server/policy/keyguard/KeyguardServiceWrapper.java
@@ -121,9 +121,10 @@
     }
 
     @Override
-    public void onStartedWakingUp(@PowerManager.WakeReason int pmWakeReason) {
+    public void onStartedWakingUp(
+            @PowerManager.WakeReason int pmWakeReason, boolean cameraGestureTriggered) {
         try {
-            mService.onStartedWakingUp(pmWakeReason);
+            mService.onStartedWakingUp(pmWakeReason, cameraGestureTriggered);
         } catch (RemoteException e) {
             Slog.w(TAG , "Remote Exception", e);
         }
diff --git a/services/core/java/com/android/server/power/FaceDownDetector.java b/services/core/java/com/android/server/power/FaceDownDetector.java
index 816c81d..b237ca2 100644
--- a/services/core/java/com/android/server/power/FaceDownDetector.java
+++ b/services/core/java/com/android/server/power/FaceDownDetector.java
@@ -77,6 +77,8 @@
 
     private boolean mIsEnabled;
 
+    private int mSensorMaxLatencyMicros;
+
     /**
      * DeviceConfig flag name, determines how long to disable sensor when user interacts while
      * device is flipped.
@@ -202,7 +204,11 @@
         if (mActive != shouldBeActive) {
             if (shouldBeActive) {
                 mSensorManager.registerListener(
-                        this, mAccelerometer, SensorManager.SENSOR_DELAY_NORMAL);
+                        this,
+                        mAccelerometer,
+                        SensorManager.SENSOR_DELAY_NORMAL,
+                        mSensorMaxLatencyMicros
+                );
                 if (mPreviousResultType == SCREEN_OFF_RESULT) {
                     logScreenOff();
                 }
@@ -226,6 +232,7 @@
         pw.println("  mFaceDown=" + mFaceDown);
         pw.println("  mActive=" + mActive);
         pw.println("  mLastFlipTime=" + mLastFlipTime);
+        pw.println("  mSensorMaxLatencyMicros=" + mSensorMaxLatencyMicros);
         pw.println("  mUserInteractionBackoffMillis=" + mUserInteractionBackoffMillis);
         pw.println("  mPreviousResultTime=" + mPreviousResultTime);
         pw.println("  mPreviousResultType=" + mPreviousResultType);
@@ -356,6 +363,11 @@
                 3600_000);
     }
 
+    private int getSensorMaxLatencyMicros() {
+        return mContext.getResources().getInteger(
+                com.android.internal.R.integer.config_flipToScreenOffMaxLatencyMicros);
+    }
+
     private float getFloatFlagValue(String key, float defaultValue, float min, float max) {
         final float value = DeviceConfig.getFloat(NAMESPACE_ATTENTION_MANAGER_SERVICE,
                 key,
@@ -416,6 +428,7 @@
         mZAccelerationThreshold = getZAccelerationThreshold();
         mZAccelerationThresholdLenient = mZAccelerationThreshold + 1.0f;
         mTimeThreshold = getTimeThreshold();
+        mSensorMaxLatencyMicros = getSensorMaxLatencyMicros();
         mUserInteractionBackoffMillis = getUserInteractionBackoffMillis();
         final boolean oldEnabled = mIsEnabled;
         mIsEnabled = isEnabled();
diff --git a/services/core/java/com/android/server/vibrator/VibrationThread.java b/services/core/java/com/android/server/vibrator/VibrationThread.java
index bc61478..b3f1bcd4 100644
--- a/services/core/java/com/android/server/vibrator/VibrationThread.java
+++ b/services/core/java/com/android/server/vibrator/VibrationThread.java
@@ -45,8 +45,10 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Iterator;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.PriorityQueue;
+import java.util.Queue;
 
 /** Plays a {@link Vibration} in dedicated thread. */
 final class VibrationThread extends Thread implements IBinder.DeathRecipient {
@@ -171,7 +173,7 @@
                 Slog.d(TAG, "Synced vibration complete reported by vibrator manager");
             }
             for (int i = 0; i < mVibrators.size(); i++) {
-                mStepQueue.consumeOnVibratorComplete(mVibrators.keyAt(i));
+                mStepQueue.notifyVibratorComplete(mVibrators.keyAt(i));
             }
             mLock.notify();
         }
@@ -183,7 +185,7 @@
             if (DEBUG) {
                 Slog.d(TAG, "Vibration complete reported by vibrator " + vibratorId);
             }
-            mStepQueue.consumeOnVibratorComplete(vibratorId);
+            mStepQueue.notifyVibratorComplete(vibratorId);
             mLock.notify();
         }
     }
@@ -192,25 +194,24 @@
         Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "playVibration");
         try {
             CombinedVibration.Sequential effect = toSequential(mVibration.getEffect());
+            mStepQueue.offer(new StartVibrateStep(effect));
+
             int stepsPlayed = 0;
-
-            synchronized (mLock) {
-                mStepQueue.offer(new StartVibrateStep(effect));
-                Step topOfQueue;
-
-                while ((topOfQueue = mStepQueue.peek()) != null) {
-                    long waitTime = topOfQueue.calculateWaitTime();
-                    if (waitTime <= 0) {
-                        stepsPlayed += mStepQueue.consume();
-                    } else {
+            while (!mStepQueue.isEmpty()) {
+                long waitTime = mStepQueue.calculateWaitTime();
+                if (waitTime <= 0) {
+                    stepsPlayed += mStepQueue.consumeNext();
+                } else {
+                    synchronized (mLock) {
                         try {
                             mLock.wait(waitTime);
-                        } catch (InterruptedException e) { }
+                        } catch (InterruptedException e) {
+                        }
                     }
-                    if (mForceStop) {
-                        mStepQueue.cancel();
-                        return Vibration.Status.CANCELLED;
-                    }
+                }
+                if (mForceStop) {
+                    mStepQueue.cancel();
+                    return Vibration.Status.CANCELLED;
                 }
             }
 
@@ -295,54 +296,75 @@
     private final class StepQueue {
         @GuardedBy("mLock")
         private final PriorityQueue<Step> mNextSteps = new PriorityQueue<>();
-
         @GuardedBy("mLock")
+        private final Queue<Step> mPendingOnVibratorCompleteSteps = new LinkedList<>();
+
         public void offer(@NonNull Step step) {
-            mNextSteps.offer(step);
+            synchronized (mLock) {
+                mNextSteps.offer(step);
+            }
         }
 
-        @GuardedBy("mLock")
-        @Nullable
-        public Step peek() {
-            return mNextSteps.peek();
+        public boolean isEmpty() {
+            synchronized (mLock) {
+                return mPendingOnVibratorCompleteSteps.isEmpty() && mNextSteps.isEmpty();
+            }
+        }
+
+        /** Returns the time in millis to wait before calling {@link #consumeNext()}. */
+        public long calculateWaitTime() {
+            Step nextStep;
+            synchronized (mLock) {
+                if (!mPendingOnVibratorCompleteSteps.isEmpty()) {
+                    // Steps anticipated by vibrator complete callback should be played right away.
+                    return 0;
+                }
+                nextStep = mNextSteps.peek();
+            }
+            return nextStep == null ? 0 : nextStep.calculateWaitTime();
         }
 
         /**
-         * Play and remove the step at the top of this queue, and also adds the next steps
-         * generated to be played next.
+         * Play and remove the step at the top of this queue, and also adds the next steps generated
+         * to be played next.
          *
          * @return the number of steps played
          */
-        @GuardedBy("mLock")
-        public int consume() {
-            Step nextStep = mNextSteps.poll();
+        public int consumeNext() {
+            Step nextStep = pollNext();
             if (nextStep != null) {
-                mNextSteps.addAll(nextStep.play());
+                // This might turn on the vibrator and have a HAL latency. Execute this outside any
+                // lock to avoid blocking other interactions with the thread.
+                List<Step> nextSteps = nextStep.play();
+                synchronized (mLock) {
+                    mNextSteps.addAll(nextSteps);
+                }
                 return 1;
             }
             return 0;
         }
 
         /**
-         * Play and remove the step in this queue that should be anticipated by the vibrator
-         * completion callback.
+         * Notify the step in this queue that should be anticipated by the vibrator completion
+         * callback and keep it separate to be consumed by {@link #consumeNext()}.
+         *
+         * <p>This is a lightweight method that do not trigger any operation from {@link
+         * VibratorController}, so it can be called directly from a native callback.
          *
          * <p>This assumes only one of the next steps is waiting on this given vibrator, so the
-         * first step found is played by this method, in no particular order.
+         * first step found will be anticipated by this method, in no particular order.
          */
         @GuardedBy("mLock")
-        public void consumeOnVibratorComplete(int vibratorId) {
+        public void notifyVibratorComplete(int vibratorId) {
             Iterator<Step> it = mNextSteps.iterator();
-            List<Step> nextSteps = EMPTY_STEP_LIST;
             while (it.hasNext()) {
                 Step step = it.next();
                 if (step.shouldPlayWhenVibratorComplete(vibratorId)) {
                     it.remove();
-                    nextSteps = step.play();
+                    mPendingOnVibratorCompleteSteps.offer(step);
                     break;
                 }
             }
-            mNextSteps.addAll(nextSteps);
         }
 
         /**
@@ -350,13 +372,25 @@
          *
          * <p>This will remove and trigger {@link Step#cancel()} in all steps, in order.
          */
-        @GuardedBy("mLock")
         public void cancel() {
             Step step;
-            while ((step = mNextSteps.poll()) != null) {
+            while ((step = pollNext()) != null) {
+                // This might turn off the vibrator and have a HAL latency. Execute this outside
+                // any lock to avoid blocking other interactions with the thread.
                 step.cancel();
             }
         }
+
+        @Nullable
+        private Step pollNext() {
+            synchronized (mLock) {
+                // Prioritize the steps anticipated by a vibrator complete callback.
+                if (!mPendingOnVibratorCompleteSteps.isEmpty()) {
+                    return mPendingOnVibratorCompleteSteps.poll();
+                }
+                return mNextSteps.poll();
+            }
+        }
     }
 
     /**
diff --git a/services/core/java/com/android/server/vibrator/VibratorController.java b/services/core/java/com/android/server/vibrator/VibratorController.java
index a09bfc5..5dceac2d0 100644
--- a/services/core/java/com/android/server/vibrator/VibratorController.java
+++ b/services/core/java/com/android/server/vibrator/VibratorController.java
@@ -67,7 +67,7 @@
         mNativeWrapper = nativeWrapper;
         mNativeWrapper.init(vibratorId, listener);
         // TODO(b/167947076): load suggested range from config
-        mVibratorInfo = mNativeWrapper.getInfo(/* suggestedFrequencyRange= */ 100);
+        mVibratorInfo = mNativeWrapper.getInfo(/* suggestedFrequencyRange= */ 200);
         Preconditions.checkNotNull(mVibratorInfo, "Failed to retrieve data for vibrator %d",
                 vibratorId);
     }
diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java
index 5743982..ac3e05d 100644
--- a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java
+++ b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java
@@ -85,12 +85,17 @@
     private static final String PACKAGE_NAME = "package";
     private static final VibrationAttributes ATTRS = new VibrationAttributes.Builder().build();
 
-    @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
+    @Rule
+    public MockitoRule mMockitoRule = MockitoJUnit.rule();
 
-    @Mock private VibrationThread.VibrationCallbacks mThreadCallbacks;
-    @Mock private VibratorController.OnVibrationCompleteListener mControllerCallbacks;
-    @Mock private IBinder mVibrationToken;
-    @Mock private IBatteryStats mIBatteryStatsMock;
+    @Mock
+    private VibrationThread.VibrationCallbacks mThreadCallbacks;
+    @Mock
+    private VibratorController.OnVibrationCompleteListener mControllerCallbacks;
+    @Mock
+    private IBinder mVibrationToken;
+    @Mock
+    private IBatteryStats mIBatteryStatsMock;
 
     private final Map<Integer, FakeVibratorControllerProvider> mVibratorProviders = new HashMap<>();
     private PowerManager.WakeLock mWakeLock;
@@ -253,7 +258,7 @@
         Thread cancellingThread = new Thread(() -> vibrationThread.cancel());
         cancellingThread.start();
 
-        waitForCompletion(vibrationThread, 20);
+        waitForCompletion(vibrationThread, /* timeout= */ 50);
         waitForCompletion(cancellingThread);
 
         verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.CANCELLED));
@@ -278,7 +283,7 @@
         Thread cancellingThread = new Thread(() -> vibrationThread.cancel());
         cancellingThread.start();
 
-        waitForCompletion(vibrationThread, 20);
+        waitForCompletion(vibrationThread, /* timeout= */ 50);
         waitForCompletion(cancellingThread);
 
         verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.CANCELLED));
@@ -844,6 +849,39 @@
                 delay < maxDelay);
     }
 
+    @LargeTest
+    @Test
+    public void vibrate_cancelSlowVibrator_cancelIsNotBlockedByVibrationThread() throws Exception {
+        FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID);
+        fakeVibrator.setSupportedEffects(VibrationEffect.EFFECT_CLICK);
+
+        long latency = 5_000; // 5s
+        fakeVibrator.setLatency(latency);
+
+        long vibrationId = 1;
+        VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
+        VibrationThread vibrationThread = startThreadAndDispatcher(vibrationId, effect);
+
+        assertTrue(waitUntil(
+                t -> !fakeVibrator.getEffectSegments().isEmpty(),
+                vibrationThread, TEST_TIMEOUT_MILLIS));
+        assertTrue(vibrationThread.isAlive());
+
+        // Run cancel in a separate thread so if VibrationThread.cancel blocks then this test should
+        // fail at waitForCompletion(cancellingThread).
+        Thread cancellingThread = new Thread(() -> vibrationThread.cancel());
+        cancellingThread.start();
+
+        // Cancelling the vibration should be fast and return right away, even if the thread is
+        // stuck at the slow call to the vibrator.
+        waitForCompletion(cancellingThread, /* timeout= */ 50);
+
+        // After the vibrator call ends the vibration is cancelled and the vibrator is turned off.
+        waitForCompletion(vibrationThread, /* timeout= */ latency + TEST_TIMEOUT_MILLIS);
+        verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.CANCELLED));
+        assertFalse(vibrationThread.getVibrators().get(VIBRATOR_ID).isVibrating());
+    }
+
     @Test
     public void vibrate_multiplePredefinedCancel_cancelsVibrationImmediately() throws Exception {
         mockVibrators(1, 2);
@@ -870,7 +908,7 @@
         Thread cancellingThread = new Thread(() -> vibrationThread.cancel());
         cancellingThread.start();
 
-        waitForCompletion(vibrationThread, 20);
+        waitForCompletion(vibrationThread, /* timeout= */ 50);
         waitForCompletion(cancellingThread);
 
         verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.CANCELLED));
@@ -902,7 +940,7 @@
         Thread cancellingThread = new Thread(() -> vibrationThread.cancel());
         cancellingThread.start();
 
-        waitForCompletion(vibrationThread, 20);
+        waitForCompletion(vibrationThread, /* timeout= */ 50);
         waitForCompletion(cancellingThread);
 
         verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.CANCELLED));
diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibratorControllerTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibratorControllerTest.java
index 59370e2..9e98e7d 100644
--- a/services/tests/servicestests/src/com/android/server/vibrator/VibratorControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/vibrator/VibratorControllerTest.java
@@ -21,6 +21,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyFloat;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
@@ -297,7 +298,7 @@
     private void mockVibratorCapabilities(int capabilities) {
         VibratorInfo.FrequencyMapping frequencyMapping = new VibratorInfo.FrequencyMapping(
                 Float.NaN, Float.NaN, Float.NaN, Float.NaN, null);
-        when(mNativeWrapperMock.getInfo(/* suggestedFrequencyRange= */ 100)).thenReturn(
+        when(mNativeWrapperMock.getInfo(anyFloat())).thenReturn(
                 new VibratorInfo.Builder(VIBRATOR_ID)
                         .setCapabilities(capabilities)
                         .setFrequencyMapping(frequencyMapping)
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java b/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java
index 00eb0f2..3862d75 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java
@@ -93,6 +93,9 @@
 import org.mockito.Mock;
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
+import org.mockito.verification.VerificationMode;
+
+import java.util.Objects;
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
@@ -137,20 +140,19 @@
     private static final int CUSTOM_LIGHT_COLOR = Color.BLACK;
     private static final int CUSTOM_LIGHT_ON = 10000;
     private static final int CUSTOM_LIGHT_OFF = 10000;
-    private static final long[] FALLBACK_VIBRATION_PATTERN = new long[] {100, 100, 100};
-    private static final VibrationEffect FALLBACK_VIBRATION =
-            VibrationEffect.createWaveform(FALLBACK_VIBRATION_PATTERN, -1);
     private static final int MAX_VIBRATION_DELAY = 1000;
 
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
+        getContext().addMockSystemService(Vibrator.class, mVibrator);
 
         when(mAudioManager.isAudioFocusExclusive()).thenReturn(false);
         when(mAudioManager.getRingtonePlayer()).thenReturn(mRingtonePlayer);
         when(mAudioManager.getStreamVolume(anyInt())).thenReturn(10);
         when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_NORMAL);
         when(mUsageStats.isAlertRateLimited(any())).thenReturn(false);
+        when(mVibrator.hasFrequencyControl()).thenReturn(false);
 
         long serviceReturnValue = IntPair.of(
                 AccessibilityManager.STATE_FLAG_ACCESSIBILITY_ENABLED,
@@ -164,13 +166,12 @@
 
         mService = spy(new NotificationManagerService(getContext(), mNotificationRecordLogger,
                 mNotificationInstanceIdSequence));
+        mService.setVibratorHelper(new VibratorHelper(getContext()));
         mService.setAudioManager(mAudioManager);
-        mService.setVibrator(mVibrator);
         mService.setSystemReady(true);
         mService.setHandler(mHandler);
         mService.setLights(mLight);
         mService.setScreenOn(false);
-        mService.setFallbackVibrationPattern(FALLBACK_VIBRATION_PATTERN);
         mService.setUsageStats(mUsageStats);
         mService.setAccessibilityManager(accessibilityManager);
         mService.mScreenOn = false;
@@ -416,32 +417,34 @@
     }
 
     private void verifyVibrate() {
-        ArgumentCaptor<AudioAttributes> captor = ArgumentCaptor.forClass(AudioAttributes.class);
-        verify(mVibrator, times(1)).vibrate(anyInt(), anyString(), argThat(mVibrateOnceMatcher),
-                anyString(), captor.capture());
-        assertEquals(0, (captor.getValue().getAllFlags()
-                & AudioAttributes.FLAG_BYPASS_INTERRUPTION_POLICY));
+        verifyVibrate(/* times= */ 1);
     }
 
     private void verifyVibrate(int times) {
-        verify(mVibrator, times(times)).vibrate(eq(Process.SYSTEM_UID),
-                eq(PackageManagerService.PLATFORM_PACKAGE_NAME), any(), anyString(),
-                any(AudioAttributes.class));
+        verifyVibrate(mVibrateOnceMatcher, times(times));
     }
 
     private void verifyVibrateLooped() {
-        verify(mVibrator, times(1)).vibrate(anyInt(), anyString(), argThat(mVibrateLoopMatcher),
-                anyString(), any(AudioAttributes.class));
+        verifyVibrate(mVibrateLoopMatcher, times(1));
     }
 
     private void verifyDelayedVibrateLooped() {
-        verify(mVibrator, timeout(MAX_VIBRATION_DELAY).times(1)).vibrate(anyInt(), anyString(),
-                argThat(mVibrateLoopMatcher), anyString(), any(AudioAttributes.class));
+        verifyVibrate(mVibrateLoopMatcher, timeout(MAX_VIBRATION_DELAY).times(1));
     }
 
-    private void verifyDelayedVibrate() {
-        verify(mVibrator, timeout(MAX_VIBRATION_DELAY).times(1)).vibrate(anyInt(), anyString(),
-                argThat(mVibrateOnceMatcher), anyString(), any(AudioAttributes.class));
+    private void verifyDelayedVibrate(VibrationEffect effect) {
+        verifyVibrate(argument -> Objects.equals(effect, argument),
+                timeout(MAX_VIBRATION_DELAY).times(1));
+    }
+
+    private void verifyVibrate(ArgumentMatcher<VibrationEffect> effectMatcher,
+            VerificationMode verification) {
+        ArgumentCaptor<AudioAttributes> captor = ArgumentCaptor.forClass(AudioAttributes.class);
+        verify(mVibrator, verification).vibrate(eq(Process.SYSTEM_UID),
+                eq(PackageManagerService.PLATFORM_PACKAGE_NAME), argThat(effectMatcher),
+                anyString(), captor.capture());
+        assertEquals(0, (captor.getValue().getAllFlags()
+                & AudioAttributes.FLAG_BYPASS_INTERRUPTION_POLICY));
     }
 
     private void verifyStopVibrate() {
@@ -761,11 +764,7 @@
 
         mService.buzzBeepBlinkLocked(r);
 
-        VibrationEffect effect = VibrationEffect.createWaveform(r.getVibration(), -1);
-
-        verify(mVibrator, timeout(MAX_VIBRATION_DELAY).times(1)).vibrate(anyInt(), anyString(),
-                eq(effect), anyString(),
-                (AudioAttributes) anyObject());
+        verifyDelayedVibrate(r.getVibration());
         assertTrue(r.isInterruptive());
         assertNotEquals(-1, r.getLastAudiblyAlertedMs());
     }
@@ -800,8 +799,8 @@
 
         mService.buzzBeepBlinkLocked(r);
 
-        verify(mVibrator, timeout(MAX_VIBRATION_DELAY).times(1)).vibrate(anyInt(), anyString(),
-                eq(FALLBACK_VIBRATION), anyString(), (AudioAttributes) anyObject());
+        verifyDelayedVibrate(
+                mService.getVibratorHelper().createFallbackVibration(/* insistent= */ false));
         verify(mRingtonePlayer, never()).playAsync
                 (anyObject(), anyObject(), anyBoolean(), anyObject());
         assertTrue(r.isInterruptive());
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationComparatorTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationComparatorTest.java
index 6a8f602..5eed30b 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationComparatorTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationComparatorTest.java
@@ -37,6 +37,7 @@
 import android.graphics.Color;
 import android.os.Build;
 import android.os.UserHandle;
+import android.os.Vibrator;
 import android.provider.Settings;
 import android.service.notification.StatusBarNotification;
 import android.telecom.TelecomManager;
@@ -63,6 +64,7 @@
     @Mock TelecomManager mTm;
     @Mock RankingHandler handler;
     @Mock PackageManager mPm;
+    @Mock Vibrator mVibrator;
 
     private final String callPkg = "com.android.server.notification";
     private final int callUid = 10;
@@ -98,6 +100,7 @@
         when(mContext.getContentResolver()).thenReturn(getContext().getContentResolver());
         when(mContext.getPackageManager()).thenReturn(mPm);
         when(mContext.getSystemService(eq(Context.TELECOM_SERVICE))).thenReturn(mTm);
+        when(mContext.getSystemService(Vibrator.class)).thenReturn(mVibrator);
         when(mContext.getString(anyInt())).thenCallRealMethod();
         when(mContext.getColor(anyInt())).thenCallRealMethod();
         when(mTm.getDefaultDialerPackage()).thenReturn(callPkg);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index c33287c..3869fae 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -7223,6 +7223,47 @@
                 mNotificationRecordLogger.event(0));
     }
 
+    /**
+     * When something is bubble'd and the bubble is dismissed, but the notification is still
+     * visible, clicking on the notification shouldn't auto-cancel it because clicking on
+     * it will produce a bubble.
+     */
+    @Test
+    public void testNotificationBubbles_bubbleStays_whenClicked_afterBubbleDismissed()
+            throws Exception {
+        setUpPrefsForBubbles(PKG, mUid,
+                true /* global */,
+                BUBBLE_PREFERENCE_ALL /* app */,
+                true /* channel */);
+
+        // GIVEN a notification that has the auto cancels flag (cancel on click) and is a bubble
+        final NotificationRecord nr = generateNotificationRecord(mTestNotificationChannel);
+        nr.getSbn().getNotification().flags |= FLAG_BUBBLE | FLAG_AUTO_CANCEL;
+        nr.setAllowBubble(true);
+        mService.addNotification(nr);
+
+        // And the bubble is dismissed
+        mService.mNotificationDelegate.onNotificationBubbleChanged(nr.getKey(),
+                false /* isBubble */, 0 /* bubbleFlags */);
+        waitForIdle();
+        assertTrue(nr.isFlagBubbleRemoved());
+
+        // WHEN we click the notification
+        final NotificationVisibility nv = NotificationVisibility.obtain(nr.getKey(), 1, 2, true);
+        mService.mNotificationDelegate.onNotificationClick(mUid, Binder.getCallingPid(),
+                nr.getKey(), nv);
+        waitForIdle();
+
+        // THEN the bubble should still exist
+        StatusBarNotification[] notifsAfter = mBinderService.getActiveNotifications(PKG);
+        assertEquals(1, notifsAfter.length);
+
+        // Check we got the click log
+        assertEquals(1, mNotificationRecordLogger.numCalls());
+        assertEquals(NotificationRecordLogger.NotificationEvent.NOTIFICATION_CLICKED,
+                mNotificationRecordLogger.event(0));
+    }
+
     @Test
     public void testLoadDefaultApprovedServices_emptyResources() {
         TestableResources tr = mContext.getOrCreateTestableResources();
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java
index 38c470d..49b6386 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java
@@ -62,6 +62,7 @@
 import android.os.Build;
 import android.os.Bundle;
 import android.os.UserHandle;
+import android.os.Vibrator;
 import android.provider.Settings;
 import android.service.notification.Adjustment;
 import android.service.notification.StatusBarNotification;
@@ -82,7 +83,6 @@
 import org.mockito.MockitoAnnotations;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
@@ -91,6 +91,7 @@
     private final Context mMockContext = mock(Context.class);
     @Mock private PackageManager mPm;
     @Mock private ContentResolver mContentResolver;
+    @Mock private Vibrator mVibrator;
 
     private final String mPkg = PKG_O;
     private final int uid = 9583;
@@ -122,6 +123,7 @@
     public void setUp() {
         MockitoAnnotations.initMocks(this);
 
+        when(mMockContext.getSystemService(eq(Vibrator.class))).thenReturn(mVibrator);
         when(mMockContext.getResources()).thenReturn(getContext().getResources());
         when(mMockContext.getPackageManager()).thenReturn(mPm);
         when(mMockContext.getContentResolver()).thenReturn(mContentResolver);
@@ -203,6 +205,26 @@
         return new StatusBarNotification(mPkg, mPkg, id1, tag1, uid, uid, n, mUser, null, uid);
     }
 
+
+    private StatusBarNotification getInsistentNotification(boolean defaultVibration) {
+        final Builder builder = new Builder(mMockContext)
+                .setContentTitle("foo")
+                .setSmallIcon(android.R.drawable.sym_def_app_icon)
+                .setPriority(Notification.PRIORITY_HIGH);
+        int defaults = 0;
+        if (defaultVibration) {
+            defaults |= Notification.DEFAULT_VIBRATE;
+        } else {
+            builder.setVibrate(CUSTOM_VIBRATION);
+            channel.setVibrationPattern(CUSTOM_CHANNEL_VIBRATION);
+        }
+        builder.setDefaults(defaults);
+        builder.setFlag(Notification.FLAG_INSISTENT, true);
+
+        Notification n = builder.build();
+        return new StatusBarNotification(mPkg, mPkg, id1, tag1, uid, uid, n, mUser, null, uid);
+    }
+
     private StatusBarNotification getMessagingStyleNotification() {
         return getMessagingStyleNotification(mPkg);
     }
@@ -309,7 +331,8 @@
                 false /* lights */, false /* defaultLights */, null /* group */);
 
         NotificationRecord record = new NotificationRecord(mMockContext, sbn, defaultChannel);
-        assertEquals(CUSTOM_VIBRATION, record.getVibration());
+        assertEquals(VibratorHelper.createWaveformVibration(
+                CUSTOM_VIBRATION, /* insistent= */ false), record.getVibration());
     }
 
     @Test
@@ -322,7 +345,8 @@
                 false /* lights */, false /* defaultLights */, null /* group */);
 
         NotificationRecord record = new NotificationRecord(mMockContext, sbn, defaultChannel);
-        assertTrue(!Arrays.equals(CUSTOM_VIBRATION, record.getVibration()));
+        assertNotEquals(VibratorHelper.createWaveformVibration(
+                CUSTOM_VIBRATION, /* insistent= */ false), record.getVibration());
     }
 
     @Test
@@ -334,7 +358,18 @@
                 false /* lights */, false /* defaultLights */, null /* group */);
 
         NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel);
-        assertEquals(CUSTOM_CHANNEL_VIBRATION, record.getVibration());
+        assertEquals(VibratorHelper.createWaveformVibration(
+                CUSTOM_CHANNEL_VIBRATION, /* insistent= */ false), record.getVibration());
+    }
+
+    @Test
+    public void testVibration_insistent_createsInsistentVibrationEffect() {
+        channel.enableVibration(true);
+        StatusBarNotification sbn = getInsistentNotification(false /* defaultBuzz */);
+
+        NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel);
+        assertEquals(VibratorHelper.createWaveformVibration(
+                CUSTOM_CHANNEL_VIBRATION, /* insistent= */ true), record.getVibration());
     }
 
     @Test
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java
index b83f9f2..4217881 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java
@@ -41,6 +41,7 @@
 import android.net.Uri;
 import android.os.Build;
 import android.os.UserHandle;
+import android.os.Vibrator;
 import android.service.notification.StatusBarNotification;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.testing.TestableContentResolver;
@@ -84,6 +85,7 @@
     @Mock Context mContext;
     @Mock ZenModeHelper mMockZenModeHelper;
     @Mock RankingConfig mConfig;
+    @Mock Vibrator mVibrator;
 
     private NotificationManager.Policy mTestNotificationPolicy;
     private Notification mNotiGroupGSortA;
@@ -125,6 +127,7 @@
                 InstrumentationRegistry.getContext().getContentResolver());
         when(mContext.getPackageManager()).thenReturn(mPm);
         when(mContext.getApplicationInfo()).thenReturn(legacy);
+        when(mContext.getSystemService(Vibrator.class)).thenReturn(mVibrator);
         TestableContentResolver contentResolver = getContext().getContentResolver();
         contentResolver.setFallbackToExisting(false);
 
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/VibratorHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/VibratorHelperTest.java
new file mode 100644
index 0000000..c77a474
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/notification/VibratorHelperTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2021 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.notification;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.assertTrue;
+
+import static org.mockito.Mockito.when;
+
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.UiServiceTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class VibratorHelperTest extends UiServiceTestCase {
+
+    private static final long[] CUSTOM_PATTERN = new long[] { 100, 200, 300, 400 };
+
+    @Mock private Vibrator mVibrator;
+
+    VibratorHelper mVibratorHelper;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        getContext().addMockSystemService(Vibrator.class, mVibrator);
+
+        mVibratorHelper = new VibratorHelper(getContext());
+    }
+
+    @Test
+    public void createWaveformVibration_insistent_createsRepeatingVibration() {
+        assertRepeatingVibration(
+                VibratorHelper.createWaveformVibration(CUSTOM_PATTERN, /* insistent= */ true));
+    }
+
+    @Test
+    public void createWaveformVibration_nonInsistent_createsSingleShotVibration() {
+        assertSingleVibration(
+                VibratorHelper.createWaveformVibration(CUSTOM_PATTERN, /* insistent= */ false));
+    }
+
+    @Test
+    public void createWaveformVibration_invalidPattern_returnsNullAndDoesNotCrash() {
+        assertNull(VibratorHelper.createWaveformVibration(null, false));
+        assertNull(VibratorHelper.createWaveformVibration(new long[0], false));
+        assertNull(VibratorHelper.createWaveformVibration(new long[] { 0, 0 }, false));
+    }
+
+    @Test
+    public void createVibration_insistent_createsRepeatingVibration() {
+        when(mVibrator.hasFrequencyControl()).thenReturn(false);
+        assertRepeatingVibration(mVibratorHelper.createDefaultVibration(/* insistent= */ true));
+        assertRepeatingVibration(mVibratorHelper.createFallbackVibration(/* insistent= */ true));
+
+        when(mVibrator.hasFrequencyControl()).thenReturn(true);
+        assertRepeatingVibration(mVibratorHelper.createDefaultVibration(/* insistent= */ true));
+        assertRepeatingVibration(mVibratorHelper.createFallbackVibration(/* insistent= */ true));
+    }
+
+    @Test
+    public void createVibration_nonInsistent_createsSingleShotVibration() {
+        when(mVibrator.hasFrequencyControl()).thenReturn(false);
+        assertSingleVibration(mVibratorHelper.createDefaultVibration(/* insistent= */ false));
+        assertSingleVibration(mVibratorHelper.createFallbackVibration(/* insistent= */ false));
+
+        when(mVibrator.hasFrequencyControl()).thenReturn(true);
+        assertSingleVibration(mVibratorHelper.createDefaultVibration(/* insistent= */ false));
+        assertSingleVibration(mVibratorHelper.createFallbackVibration(/* insistent= */ false));
+    }
+
+    private void assertRepeatingVibration(VibrationEffect effect) {
+        assertTrue(getRepeatIndex(effect) >= 0);
+    }
+
+    private void assertSingleVibration(VibrationEffect effect) {
+        assertEquals(-1, getRepeatIndex(effect));
+    }
+
+    private static int getRepeatIndex(VibrationEffect effect) {
+        assertTrue("Unknown vibration effect " + effect,
+                effect instanceof VibrationEffect.Composed);
+        return ((VibrationEffect.Composed) effect).getRepeatIndex();
+    }
+}