Merge "Add first frame timestamps to AppStartInfo" into main
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index 5e9fdfb..1e824a1 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -6257,6 +6257,29 @@
      * {@link #RESTRICTION_LEVEL_ADAPTIVE} is a normal state, where there is default lifecycle
      * management applied to the app. Also, {@link #RESTRICTION_LEVEL_EXEMPTED} is used when the
      * app is being put in a power-save allowlist.
+     * <p>
+     * Example arguments when user force-stops an app from Settings:
+     * <pre>
+     * noteAppRestrictionEnabled(
+     *     "com.example.app",
+     *     appUid,
+     *     RESTRICTION_LEVEL_FORCE_STOPPED,
+     *     true,
+     *     RESTRICTION_REASON_USER,
+     *     "settings",
+     *     0);
+     * </pre>
+     * Example arguments when app is put in restricted standby bucket for exceeding X hours of jobs:
+     * <pre>
+     * noteAppRestrictionEnabled(
+     *     "com.example.app",
+     *     appUid,
+     *     RESTRICTION_LEVEL_RESTRICTED_BUCKET,
+     *     true,
+     *     RESTRICTION_REASON_SYSTEM_HEALTH,
+     *     "job_duration",
+     *     X * 3600 * 1000L);
+     * </pre>
      *
      * @param packageName the package name of the app
      * @param uid the uid of the app
@@ -6264,11 +6287,20 @@
      * @param enabled whether the state is being applied or removed
      * @param reason the reason for the restriction state change, from {@code RestrictionReason}
      * @param subReason a string sub reason limited to 16 characters that specifies additional
-     *                  information about the reason for restriction.
+     *                  information about the reason for restriction. This string must only contain
+     *                  reasons related to excessive system resource usage or in some cases,
+     *                  source of the restriction. This string must not contain any details that
+     *                  identify user behavior beyond their actions to restrict/unrestrict/launch
+     *                  apps in some way.
+     *                  Examples of system resource usage: wakelock, wakeups, mobile_data,
+     *                  binder_calls, memory, excessive_threads, excessive_cpu, gps_scans, etc.
+     *                  Examples of user actions: settings, notification, command_line, launch, etc.
+     *
      * @param threshold for reasons that are due to exceeding some threshold, the threshold value
      *                  must be specified. The unit of the threshold depends on the reason and/or
      *                  subReason. For time, use milliseconds. For memory, use KB. For count, use
-     *                  the actual count or normalized as per-hour. For power, use milliwatts. Etc.
+     *                  the actual count or if rate limited, normalized per-hour. For power,
+     *                  use milliwatts. For CPU, use mcycles.
      *
      * @hide
      */
diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig
index e3c367f8..63ffaa0 100644
--- a/core/java/android/app/notification.aconfig
+++ b/core/java/android/app/notification.aconfig
@@ -39,6 +39,13 @@
 }
 
 flag {
+  name: "check_autogroup_before_post"
+  namespace: "systemui"
+  description: "Does a check to see if notification should be autogrouped before posting, and if so groups before post."
+  bug: "330214226"
+}
+
+flag {
   name: "visit_risky_uris"
   namespace: "systemui"
   description: "Guards the security fix that ensures all URIs in intents and Person.java are valid"
diff --git a/core/java/android/content/PermissionChecker.java b/core/java/android/content/PermissionChecker.java
index 0e3217d..cb8eb83 100644
--- a/core/java/android/content/PermissionChecker.java
+++ b/core/java/android/content/PermissionChecker.java
@@ -73,13 +73,12 @@
     public static final int PERMISSION_GRANTED = PermissionCheckerManager.PERMISSION_GRANTED;
 
     /**
-     * The permission is denied. Applicable only to runtime and app op permissions.
+     * The permission is denied. Applicable only to runtime permissions.
      *
      * <p>Returned when:
      * <ul>
      *   <li>the runtime permission is granted, but the corresponding app op is denied
      *       for runtime permissions.</li>
-     *   <li>the app ops is ignored for app op permissions.</li>
      * </ul>
      *
      * @hide
diff --git a/core/java/android/permission/PermissionManager.java b/core/java/android/permission/PermissionManager.java
index 55bb430..7e51cb0 100644
--- a/core/java/android/permission/PermissionManager.java
+++ b/core/java/android/permission/PermissionManager.java
@@ -112,7 +112,7 @@
     public static final int PERMISSION_GRANTED = 0;
 
     /**
-     * The permission is denied. Applicable only to runtime and app op permissions.
+     * The permission is denied. Applicable only to runtime permissions.
      * <p>
      * The app isn't expecting the permission to be denied so that a "no-op" action should be taken,
      * such as returning an empty result.
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index 1cdcd20..0e7de48 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -2463,22 +2463,22 @@
     public boolean hideSoftInputFromView(@NonNull View view, @HideFlags int flags) {
         final boolean isFocusedAndWindowFocused = view.hasWindowFocus() && view.isFocused();
         synchronized (mH) {
-            if (!isFocusedAndWindowFocused && !hasServedByInputMethodLocked(view)) {
+            final boolean hasServedByInputMethod = hasServedByInputMethodLocked(view);
+            if (!isFocusedAndWindowFocused && !hasServedByInputMethod) {
                 // Fail early if the view is not focused and not served
                 // to avoid logging many erroneous calls.
                 return false;
             }
-        }
 
-        final int reason = SoftInputShowHideReason.HIDE_SOFT_INPUT_FROM_VIEW;
-        final var statsToken = ImeTracker.forLogging().onStart(ImeTracker.TYPE_HIDE,
-                ImeTracker.ORIGIN_CLIENT, reason, ImeTracker.isFromUser(view));
-        ImeTracker.forLatency().onRequestHide(statsToken,
-                ImeTracker.ORIGIN_CLIENT, reason, ActivityThread::currentApplication);
-        ImeTracing.getInstance().triggerClientDump("InputMethodManager#hideSoftInputFromView",
-                this, null /* icProto */);
-        synchronized (mH) {
-            if (!hasServedByInputMethodLocked(view)) {
+            final int reason = SoftInputShowHideReason.HIDE_SOFT_INPUT_FROM_VIEW;
+            final var statsToken = ImeTracker.forLogging().onStart(ImeTracker.TYPE_HIDE,
+                    ImeTracker.ORIGIN_CLIENT, reason, ImeTracker.isFromUser(view));
+            ImeTracker.forLatency().onRequestHide(statsToken,
+                    ImeTracker.ORIGIN_CLIENT, reason, ActivityThread::currentApplication);
+            ImeTracing.getInstance().triggerClientDump("InputMethodManager#hideSoftInputFromView",
+                    this, null /* icProto */);
+
+            if (!hasServedByInputMethod) {
                 ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
                 ImeTracker.forLatency().onShowFailed(statsToken,
                         ImeTracker.PHASE_CLIENT_VIEW_SERVED, ActivityThread::currentApplication);
diff --git a/core/java/com/android/internal/util/ProcFileReader.java b/core/java/com/android/internal/util/ProcFileReader.java
index 6cf241e..ddbb586 100644
--- a/core/java/com/android/internal/util/ProcFileReader.java
+++ b/core/java/com/android/internal/util/ProcFileReader.java
@@ -89,6 +89,12 @@
         mTail -= count;
         if (mTail == 0) {
             fillBuf();
+
+            if (mTail > 0 && mBuffer[0] == ' ') {
+                // After filling the buffer, it contains more consecutive
+                // delimiters that need to be skipped.
+                consumeBuf(0);
+            }
         }
     }
 
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 77a9912..bfbfb3a 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -178,6 +178,7 @@
     <protected-broadcast android:name="android.bluetooth.device.action.CONNECTION_ACCESS_REPLY" />
     <protected-broadcast android:name="android.bluetooth.device.action.CONNECTION_ACCESS_CANCEL" />
     <protected-broadcast android:name="android.bluetooth.device.action.CONNECTION_ACCESS_REQUEST" />
+    <protected-broadcast android:name="android.bluetooth.device.action.KEY_MISSING" />
     <protected-broadcast android:name="android.bluetooth.device.action.SDP_RECORD" />
     <protected-broadcast android:name="android.bluetooth.device.action.BATTERY_LEVEL_CHANGED" />
     <protected-broadcast android:name="android.bluetooth.device.action.REMOTE_ISSUE_OCCURRED" />
diff --git a/core/tests/utiltests/src/com/android/internal/util/ProcFileReaderTest.java b/core/tests/utiltests/src/com/android/internal/util/ProcFileReaderTest.java
index 4c00c16..9785ca7 100644
--- a/core/tests/utiltests/src/com/android/internal/util/ProcFileReaderTest.java
+++ b/core/tests/utiltests/src/com/android/internal/util/ProcFileReaderTest.java
@@ -216,6 +216,46 @@
     }
 
     @Test
+    public void testBufferSizeWithConsecutiveDelimiters() throws Exception {
+        // Read numbers using very small buffer size, exercising fillBuf()
+        // Include more consecutive delimiters than the buffer size.
+        final ProcFileReader reader =
+                buildReader("1   21  3  41           5  61  7  81 9   10\n", 3);
+
+        assertEquals(1, reader.nextInt());
+        assertEquals(21, reader.nextInt());
+        assertEquals(3, reader.nextInt());
+        assertEquals(41, reader.nextInt());
+        assertEquals(5, reader.nextInt());
+        assertEquals(61, reader.nextInt());
+        assertEquals(7, reader.nextInt());
+        assertEquals(81, reader.nextInt());
+        assertEquals(9, reader.nextInt());
+        assertEquals(10, reader.nextInt());
+        reader.finishLine();
+        assertFalse(reader.hasMoreData());
+    }
+
+    @Test
+    public void testBufferSizeWithConsecutiveDelimitersAndMultipleLines() throws Exception {
+        final ProcFileReader reader =
+                buildReader("1 21  41    \n    5  7     81   \n    9 10     \n", 3);
+
+        assertEquals(1, reader.nextInt());
+        assertEquals(21, reader.nextInt());
+        assertEquals(41, reader.nextInt());
+        reader.finishLine();
+        assertEquals(5, reader.nextInt());
+        assertEquals(7, reader.nextInt());
+        assertEquals(81, reader.nextInt());
+        reader.finishLine();
+        assertEquals(9, reader.nextInt());
+        assertEquals(10, reader.nextInt());
+        reader.finishLine();
+        assertFalse(reader.hasMoreData());
+    }
+
+    @Test
     public void testIgnore() throws Exception {
         final ProcFileReader reader = buildReader("a b c\n");
 
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
index 9f0a425..9599658 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
@@ -23,7 +23,8 @@
     android:orientation="horizontal"
     android:gravity="center"
     android:padding="16dp"
-    android:background="@drawable/desktop_mode_maximize_menu_background">
+    android:background="@drawable/desktop_mode_maximize_menu_background"
+    android:elevation="1dp">
 
     <LinearLayout
         android:layout_width="wrap_content"
@@ -37,7 +38,8 @@
             android:background="@drawable/desktop_mode_maximize_menu_layout_background"
             android:padding="4dp"
             android:layout_marginRight="8dp"
-            android:layout_marginBottom="4dp">
+            android:layout_marginBottom="4dp"
+            android:alpha="0">
             <Button
                 android:id="@+id/maximize_menu_maximize_button"
                 style="?android:attr/buttonBarButtonStyle"
@@ -48,6 +50,7 @@
         </FrameLayout>
 
         <TextView
+            android:id="@+id/maximize_menu_maximize_window_text"
             android:layout_width="94dp"
             android:layout_height="18dp"
             android:textSize="11sp"
@@ -55,7 +58,8 @@
             android:gravity="center"
             android:fontFamily="google-sans-text"
             android:text="@string/desktop_mode_maximize_menu_maximize_text"
-            android:textColor="?androidprv:attr/materialColorOnSurface"/>
+            android:textColor="?androidprv:attr/materialColorOnSurface"
+            android:alpha="0"/>
     </LinearLayout>
 
     <LinearLayout
@@ -69,7 +73,8 @@
             android:orientation="horizontal"
             android:padding="4dp"
             android:background="@drawable/desktop_mode_maximize_menu_layout_background"
-            android:layout_marginBottom="4dp">
+            android:layout_marginBottom="4dp"
+            android:alpha="0">
             <Button
                 android:id="@+id/maximize_menu_snap_left_button"
                 style="?android:attr/buttonBarButtonStyle"
@@ -88,6 +93,7 @@
                 android:stateListAnimator="@null"/>
         </LinearLayout>
         <TextView
+            android:id="@+id/maximize_menu_snap_window_text"
             android:layout_width="94dp"
             android:layout_height="18dp"
             android:textSize="11sp"
@@ -96,6 +102,8 @@
             android:gravity="center"
             android:fontFamily="google-sans-text"
             android:text="@string/desktop_mode_maximize_menu_snap_text"
-            android:textColor="?androidprv:attr/materialColorOnSurface"/>
+            android:textColor="?androidprv:attr/materialColorOnSurface"
+            android:alpha="0"/>
     </LinearLayout>
-</LinearLayout>
\ No newline at end of file
+</LinearLayout>
+
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index c8bfe7a4..f532f96 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -464,6 +464,9 @@
     <!-- The height of the maximize menu in desktop mode. -->
     <dimen name="desktop_mode_maximize_menu_height">114dp</dimen>
 
+    <!-- The padding of the maximize menu in desktop mode. -->
+    <dimen name="desktop_mode_menu_padding">16dp</dimen>
+
     <!-- The height of the buttons in the maximize menu. -->
     <dimen name="desktop_mode_maximize_menu_button_height">52dp</dimen>
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
index 899b7cc..22f0adc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
@@ -16,6 +16,9 @@
 
 package com.android.wm.shell.windowdecor
 
+import android.animation.AnimatorSet
+import android.animation.ObjectAnimator
+import android.animation.ValueAnimator
 import android.annotation.IdRes
 import android.app.ActivityManager.RunningTaskInfo
 import android.content.Context
@@ -30,16 +33,21 @@
 import android.view.View.OnClickListener
 import android.view.View.OnGenericMotionListener
 import android.view.View.OnTouchListener
+import android.view.View.SCALE_Y
+import android.view.View.TRANSLATION_Y
+import android.view.View.TRANSLATION_Z
 import android.view.WindowManager
 import android.view.WindowlessWindowManager
 import android.widget.Button
 import android.widget.FrameLayout
 import android.widget.LinearLayout
+import android.widget.TextView
 import android.window.TaskConstants
 import androidx.core.content.withStyledAttributes
 import com.android.internal.R.attr.colorAccentPrimary
 import com.android.wm.shell.R
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
+import com.android.wm.shell.animation.Interpolators.EMPHASIZED_DECELERATE
 import com.android.wm.shell.common.DisplayController
 import com.android.wm.shell.common.SyncTransactionQueue
 import com.android.wm.shell.windowdecor.WindowDecoration.AdditionalWindow
@@ -65,14 +73,13 @@
     private var maximizeMenu: AdditionalWindow? = null
     private lateinit var viewHost: SurfaceControlViewHost
     private lateinit var leash: SurfaceControl
-    private val shadowRadius = loadDimensionPixelSize(
-            R.dimen.desktop_mode_maximize_menu_shadow_radius
-    ).toFloat()
+    private val openMenuAnimatorSet = AnimatorSet()
     private val cornerRadius = loadDimensionPixelSize(
             R.dimen.desktop_mode_maximize_menu_corner_radius
     ).toFloat()
     private val menuWidth = loadDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_width)
     private val menuHeight = loadDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_height)
+    private val menuPadding = loadDimensionPixelSize(R.dimen.desktop_mode_menu_padding)
 
     private lateinit var snapRightButton: Button
     private lateinit var snapLeftButton: Button
@@ -91,10 +98,12 @@
         if (maximizeMenu != null) return
         createMaximizeMenu()
         setupMaximizeMenu()
+        animateOpenMenu()
     }
 
     /** Closes the maximize window and releases its view. */
     fun close() {
+        openMenuAnimatorSet.cancel()
         maximizeMenu?.releaseView()
         maximizeMenu = null
     }
@@ -134,8 +143,6 @@
         // Bring menu to front when open
         t.setLayer(leash, TaskConstants.TASK_CHILD_LAYER_FLOATING_MENU)
                 .setPosition(leash, menuPosition.x, menuPosition.y)
-                .setWindowCrop(leash, menuWidth, menuHeight)
-                .setShadowRadius(leash, shadowRadius)
                 .setCornerRadius(leash, cornerRadius)
                 .show(leash)
         maximizeMenu = AdditionalWindow(leash, viewHost, transactionSupplier)
@@ -146,6 +153,77 @@
         }
     }
 
+    private fun animateOpenMenu() {
+        val viewHost = maximizeMenu?.mWindowViewHost
+        val maximizeMenuView = viewHost?.view ?: return
+        val maximizeWindowText = maximizeMenuView.requireViewById<TextView>(
+                R.id.maximize_menu_maximize_window_text)
+        val snapWindowText = maximizeMenuView.requireViewById<TextView>(
+                R.id.maximize_menu_snap_window_text)
+
+        openMenuAnimatorSet.playTogether(
+                ObjectAnimator.ofFloat(maximizeMenuView, SCALE_Y, STARTING_MENU_HEIGHT_SCALE, 1f)
+                        .apply {
+                            duration = MENU_HEIGHT_ANIMATION_DURATION_MS
+                            interpolator = EMPHASIZED_DECELERATE
+                        },
+                ValueAnimator.ofFloat(STARTING_MENU_HEIGHT_SCALE, 1f)
+                        .apply {
+                            duration = MENU_HEIGHT_ANIMATION_DURATION_MS
+                            interpolator = EMPHASIZED_DECELERATE
+                            addUpdateListener {
+                                // Animate padding so that controls stay pinned to the bottom of
+                                // the menu.
+                                val value = animatedValue as Float
+                                val topPadding = menuPadding -
+                                        ((1 - value) * menuHeight).toInt()
+                                maximizeMenuView.setPadding(menuPadding, topPadding,
+                                        menuPadding, menuPadding)
+                            }
+                        },
+                ValueAnimator.ofFloat(1 / STARTING_MENU_HEIGHT_SCALE, 1f).apply {
+                            duration = MENU_HEIGHT_ANIMATION_DURATION_MS
+                            interpolator = EMPHASIZED_DECELERATE
+                            addUpdateListener {
+                                // Scale up the children of the maximize menu so that the menu
+                                // scale is cancelled out and only the background is scaled.
+                                val value = animatedValue as Float
+                                maximizeButtonLayout.scaleY = value
+                                snapButtonsLayout.scaleY = value
+                                maximizeWindowText.scaleY = value
+                                snapWindowText.scaleY = value
+                            }
+                        },
+                ObjectAnimator.ofFloat(maximizeMenuView, TRANSLATION_Y,
+                        (STARTING_MENU_HEIGHT_SCALE - 1) * menuHeight, 0f).apply {
+                    duration = MENU_HEIGHT_ANIMATION_DURATION_MS
+                    interpolator = EMPHASIZED_DECELERATE
+                },
+                ObjectAnimator.ofInt(maximizeMenuView.background, "alpha",
+                        MAX_DRAWABLE_ALPHA_VALUE).apply {
+                    duration = ALPHA_ANIMATION_DURATION_MS
+                },
+                ValueAnimator.ofFloat(0f, 1f)
+                        .apply {
+                            duration = ALPHA_ANIMATION_DURATION_MS
+                            startDelay = CONTROLS_ALPHA_ANIMATION_DELAY_MS
+                            addUpdateListener {
+                                val value = animatedValue as Float
+                                maximizeButtonLayout.alpha = value
+                                snapButtonsLayout.alpha = value
+                                maximizeWindowText.alpha = value
+                                snapWindowText.alpha = value
+                            }
+                        },
+                ObjectAnimator.ofFloat(maximizeMenuView, TRANSLATION_Z, MENU_Z_TRANSLATION)
+                        .apply {
+                            duration = ELEVATION_ANIMATION_DURATION_MS
+                            startDelay = CONTROLS_ALPHA_ANIMATION_DELAY_MS
+                        }
+        )
+        openMenuAnimatorSet.start()
+    }
+
     private fun loadDimensionPixelSize(resourceId: Int): Int {
         return if (resourceId == Resources.ID_NULL) {
             0
@@ -263,6 +341,14 @@
     }
 
     companion object {
+        // Open menu animation constants
+        private const val ALPHA_ANIMATION_DURATION_MS = 50L
+        private const val MAX_DRAWABLE_ALPHA_VALUE = 255
+        private const val STARTING_MENU_HEIGHT_SCALE = 0.8f
+        private const val MENU_HEIGHT_ANIMATION_DURATION_MS = 300L
+        private const val ELEVATION_ANIMATION_DURATION_MS = 50L
+        private const val CONTROLS_ALPHA_ANIMATION_DELAY_MS = 33L
+        private const val MENU_Z_TRANSLATION = 1f
         fun isMaximizeMenuView(@IdRes viewId: Int): Boolean {
             return viewId == R.id.maximize_menu ||
                     viewId == R.id.maximize_menu_maximize_button ||
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java
index c8f9135..991ce12 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java
+++ b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java
@@ -31,7 +31,6 @@
 import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.PACKAGE_NAME;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
 
 import android.accessibilityservice.AccessibilityServiceInfo;
 import android.app.Instrumentation;
@@ -90,6 +89,7 @@
     private static Instrumentation sInstrumentation;
     private static UiAutomation sUiAutomation;
     private static UiDevice sUiDevice;
+    private static String sLockSettings;
     private static final AtomicInteger sLastGlobalAction = new AtomicInteger(NO_GLOBAL_ACTION);
     private static final AtomicBoolean sOpenBlocked = new AtomicBoolean(false);
 
@@ -108,6 +108,11 @@
         sUiAutomation.adoptShellPermissionIdentity(
                 UiAutomation.ALL_PERMISSIONS.toArray(new String[0]));
         sUiDevice = UiDevice.getInstance(sInstrumentation);
+        sLockSettings = sUiDevice.executeShellCommand("locksettings get-disabled");
+        Log.i(TAG, "locksettings get-disabled returns " + sLockSettings);
+        // Some test in the test class requires the device to be in lock screen
+        // ensure we have locksettings enabled before running the tests
+        sUiDevice.executeShellCommand("locksettings set-disabled false");
 
         final Context context = sInstrumentation.getTargetContext();
         sAccessibilityManager = context.getSystemService(AccessibilityManager.class);
@@ -157,9 +162,10 @@
     }
 
     @AfterClass
-    public static void classTeardown() {
+    public static void classTeardown() throws IOException {
         Settings.Secure.putString(sInstrumentation.getTargetContext().getContentResolver(),
                 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, "");
+        sUiDevice.executeShellCommand("locksettings set-disabled " + sLockSettings);
     }
 
     @Before
@@ -184,17 +190,17 @@
         return root != null && root.getPackageName().toString().equals(PACKAGE_NAME);
     }
 
-    private static void wakeUpScreen() throws IOException {
+    private static void wakeUpScreen() {
         sUiDevice.pressKeyCode(KeyEvent.KEYCODE_WAKEUP);
         WaitUtils.waitForValueToSettle("Screen On", AccessibilityMenuServiceTest::isScreenOn);
-        assertWithMessage("Screen is on").that(isScreenOn()).isTrue();
+        WaitUtils.ensureThat("Screen is on", AccessibilityMenuServiceTest::isScreenOn);
     }
 
-    private static void closeScreen() throws Throwable {
+    private static void closeScreen() {
         // go/adb-cheats#lock-screen
         sUiDevice.pressKeyCode(KeyEvent.KEYCODE_SLEEP);
         WaitUtils.waitForValueToSettle("Screen Off", AccessibilityMenuServiceTest::isScreenOff);
-        assertWithMessage("Screen is off").that(isScreenOff()).isTrue();
+        WaitUtils.ensureThat("Screen is off", AccessibilityMenuServiceTest::isScreenOff);
         WaitUtils.ensureThat(
                 "Screen is locked", () -> sKeyguardManager.isKeyguardLocked());
     }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt
index eedff89..5f84dd4 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt
@@ -17,6 +17,11 @@
 package com.android.systemui.qs.footer.ui.compose
 
 import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.shrinkVertically
 import androidx.compose.foundation.BorderStroke
 import androidx.compose.foundation.Canvas
 import androidx.compose.foundation.LocalIndication
@@ -87,10 +92,24 @@
 fun SceneScope.FooterActionsWithAnimatedVisibility(
     viewModel: FooterActionsViewModel,
     isCustomizing: Boolean,
+    customizingAnimationDuration: Int,
     lifecycleOwner: LifecycleOwner,
     modifier: Modifier = Modifier,
 ) {
-    AnimatedVisibility(visible = !isCustomizing, modifier = modifier.fillMaxWidth()) {
+    AnimatedVisibility(
+        visible = !isCustomizing,
+        enter =
+            expandVertically(
+                animationSpec = tween(customizingAnimationDuration),
+                initialHeight = { 0 },
+            ) + fadeIn(tween(customizingAnimationDuration)),
+        exit =
+            shrinkVertically(
+                animationSpec = tween(customizingAnimationDuration),
+                targetHeight = { 0 },
+            ) + fadeOut(tween(customizingAnimationDuration)),
+        modifier = modifier.fillMaxWidth()
+    ) {
         QuickSettingsTheme {
             // This view has its own horizontal padding
             // TODO(b/321716470) This should use a lifecycle tied to the scene.
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
index a87a8df..46be6b8 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
@@ -162,7 +162,8 @@
     modifier: Modifier = Modifier,
 ) {
     val qsView by qsSceneAdapter.qsView.collectAsState(null)
-    val isCustomizing by qsSceneAdapter.isCustomizing.collectAsState()
+    val isCustomizing by
+        qsSceneAdapter.isCustomizerShowing.collectAsState(qsSceneAdapter.isCustomizerShowing.value)
     QuickSettingsTheme {
         val context = LocalContext.current
 
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
index a32cc04..4c0f2e1 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
@@ -19,12 +19,15 @@
 import android.view.ViewGroup
 import androidx.activity.compose.BackHandler
 import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.animateDpAsState
 import androidx.compose.animation.core.animateFloatAsState
 import androidx.compose.animation.core.tween
 import androidx.compose.animation.expandVertically
 import androidx.compose.animation.fadeIn
 import androidx.compose.animation.fadeOut
 import androidx.compose.animation.shrinkVertically
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutVertically
 import androidx.compose.foundation.background
 import androidx.compose.foundation.clipScrollableContainer
 import androidx.compose.foundation.gestures.Orientation
@@ -178,6 +181,9 @@
                 }
     ) {
         val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsState()
+        val isCustomizerShowing by viewModel.qsSceneAdapter.isCustomizerShowing.collectAsState()
+        val customizingAnimationDuration by
+            viewModel.qsSceneAdapter.customizerAnimationDuration.collectAsState()
         val screenHeight = LocalRawScreenHeight.current
 
         BackHandler(
@@ -217,6 +223,18 @@
         val navBarBottomHeight =
             WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
         val density = LocalDensity.current
+        val bottomPadding by
+            animateDpAsState(
+                targetValue = if (isCustomizing) 0.dp else navBarBottomHeight,
+                animationSpec = tween(customizingAnimationDuration),
+                label = "animateQSSceneBottomPaddingAsState"
+            )
+        val topPadding by
+            animateDpAsState(
+                targetValue = if (isCustomizing) ShadeHeader.Dimensions.CollapsedHeight else 0.dp,
+                animationSpec = tween(customizingAnimationDuration),
+                label = "animateQSSceneTopPaddingAsState"
+            )
 
         LaunchedEffect(navBarBottomHeight, density) {
             with(density) {
@@ -236,17 +254,14 @@
             horizontalAlignment = Alignment.CenterHorizontally,
             modifier =
                 Modifier.fillMaxSize()
-                    .then(
-                        if (isCustomizing) {
-                            Modifier.padding(top = 48.dp)
-                        } else {
-                            Modifier.padding(bottom = navBarBottomHeight)
-                        }
+                    .padding(
+                        top = topPadding.coerceAtLeast(0.dp),
+                        bottom = bottomPadding.coerceAtLeast(0.dp)
                     )
         ) {
             Box(modifier = Modifier.fillMaxSize().weight(1f)) {
                 val shadeHeaderAndQuickSettingsModifier =
-                    if (isCustomizing) {
+                    if (isCustomizerShowing) {
                         Modifier.fillMaxHeight().align(Alignment.TopCenter)
                     } else {
                         Modifier.verticalNestedScrollToScene()
@@ -269,15 +284,22 @@
                                 visible = !isCustomizing,
                                 enter =
                                     expandVertically(
-                                        animationSpec = tween(100),
-                                        initialHeight = { collapsedHeaderHeight },
-                                    ) + fadeIn(tween(100)),
+                                        animationSpec = tween(customizingAnimationDuration),
+                                        expandFrom = Alignment.Top,
+                                    ) +
+                                        slideInVertically(
+                                            animationSpec = tween(customizingAnimationDuration),
+                                        ) +
+                                        fadeIn(tween(customizingAnimationDuration)),
                                 exit =
                                     shrinkVertically(
-                                        animationSpec = tween(100),
-                                        targetHeight = { collapsedHeaderHeight },
+                                        animationSpec = tween(customizingAnimationDuration),
                                         shrinkTowards = Alignment.Top,
-                                    ) + fadeOut(tween(100)),
+                                    ) +
+                                        slideOutVertically(
+                                            animationSpec = tween(customizingAnimationDuration),
+                                        ) +
+                                        fadeOut(tween(customizingAnimationDuration)),
                             ) {
                                 ExpandedShadeHeader(
                                     viewModel = viewModel.shadeHeaderViewModel,
@@ -303,7 +325,7 @@
                         viewModel.qsSceneAdapter,
                         { viewModel.qsSceneAdapter.qsHeight },
                         isSplitShade = false,
-                        modifier = Modifier.sysuiResTag("expanded_qs_scroll_view"),
+                        modifier = Modifier.sysuiResTag("expanded_qs_scroll_view")
                     )
 
                     MediaCarousel(
@@ -318,6 +340,7 @@
             FooterActionsWithAnimatedVisibility(
                 viewModel = footerActionsViewModel,
                 isCustomizing = isCustomizing,
+                customizingAnimationDuration = customizingAnimationDuration,
                 lifecycleOwner = lifecycleOwner,
                 modifier = Modifier.align(Alignment.CenterHorizontally),
             )
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index 72b8026..ef5d4e1 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -17,7 +17,9 @@
 package com.android.systemui.shade.ui.composable
 
 import android.view.ViewGroup
+import androidx.compose.animation.core.animateDpAsState
 import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.tween
 import androidx.compose.foundation.background
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.clipScrollableContainer
@@ -301,6 +303,9 @@
     modifier: Modifier = Modifier,
 ) {
     val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsState()
+    val isCustomizerShowing by viewModel.qsSceneAdapter.isCustomizerShowing.collectAsState()
+    val customizingAnimationDuration by
+        viewModel.qsSceneAdapter.customizerAnimationDuration.collectAsState()
     val lifecycleOwner = LocalLifecycleOwner.current
     val footerActionsViewModel =
         remember(lifecycleOwner, viewModel) { viewModel.getFooterActionsViewModel(lifecycleOwner) }
@@ -320,6 +325,12 @@
             .collectAsState(0f)
 
     val navBarBottomHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
+    val bottomPadding by
+        animateDpAsState(
+            targetValue = if (isCustomizing) 0.dp else navBarBottomHeight,
+            animationSpec = tween(customizingAnimationDuration),
+            label = "animateQSSceneBottomPaddingAsState"
+        )
     val density = LocalDensity.current
     LaunchedEffect(navBarBottomHeight, density) {
         with(density) {
@@ -390,16 +401,13 @@
                     )
                     Column(
                         verticalArrangement = Arrangement.Top,
-                        modifier =
-                            Modifier.fillMaxSize().thenIf(!isCustomizing) {
-                                Modifier.padding(bottom = navBarBottomHeight)
-                            },
+                        modifier = Modifier.fillMaxSize().padding(bottom = bottomPadding),
                     ) {
                         Column(
                             modifier =
                                 Modifier.fillMaxSize()
                                     .weight(1f)
-                                    .thenIf(!isCustomizing) {
+                                    .thenIf(!isCustomizerShowing) {
                                         Modifier.verticalNestedScrollToScene()
                                             .verticalScroll(
                                                 quickSettingsScrollState,
@@ -432,6 +440,7 @@
                         FooterActionsWithAnimatedVisibility(
                             viewModel = footerActionsViewModel,
                             isCustomizing = isCustomizing,
+                            customizingAnimationDuration = customizingAnimationDuration,
                             lifecycleOwner = lifecycleOwner,
                             modifier =
                                 Modifier.align(Alignment.CenterHorizontally)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
index f2eb7f4..c660ff3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
@@ -273,21 +273,56 @@
         }
 
     @Test
-    fun customizing_QS() =
+    fun customizing_QS_noAnimations() =
         testScope.runTest {
-            val customizing by collectLastValue(underTest.isCustomizing)
+            val customizerState by collectLastValue(underTest.customizerState)
 
             underTest.inflate(context)
             runCurrent()
             underTest.setState(QSSceneAdapter.State.QS)
 
-            assertThat(customizing).isFalse()
+            assertThat(customizerState).isEqualTo(CustomizerState.Hidden)
 
             underTest.setCustomizerShowing(true)
-            assertThat(customizing).isTrue()
+            assertThat(customizerState).isEqualTo(CustomizerState.Showing)
 
             underTest.setCustomizerShowing(false)
-            assertThat(customizing).isFalse()
+            assertThat(customizerState).isEqualTo(CustomizerState.Hidden)
+        }
+
+    // This matches the calls made by QSCustomizer
+    @Test
+    fun customizing_QS_animations_correctStates() =
+        testScope.runTest {
+            val customizerState by collectLastValue(underTest.customizerState)
+            val animatingInDuration = 100L
+            val animatingOutDuration = 50L
+
+            underTest.inflate(context)
+            runCurrent()
+            underTest.setState(QSSceneAdapter.State.QS)
+
+            assertThat(customizerState).isEqualTo(CustomizerState.Hidden)
+
+            // Start showing customizer with animation
+            underTest.setCustomizerAnimating(true)
+            underTest.setCustomizerShowing(true, animatingInDuration)
+            assertThat(customizerState)
+                .isEqualTo(CustomizerState.AnimatingIntoCustomizer(animatingInDuration))
+
+            // Finish animation
+            underTest.setCustomizerAnimating(false)
+            assertThat(customizerState).isEqualTo(CustomizerState.Showing)
+
+            // Start closing customizer with animation
+            underTest.setCustomizerAnimating(true)
+            underTest.setCustomizerShowing(false, animatingOutDuration)
+            assertThat(customizerState)
+                .isEqualTo(CustomizerState.AnimatingOutOfCustomizer(animatingOutDuration))
+
+            // Finish animation
+            underTest.setCustomizerAnimating(false)
+            assertThat(customizerState).isEqualTo(CustomizerState.Hidden)
         }
 
     @Test
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepository.kt
index 0534824..f1c3f94 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepository.kt
@@ -323,6 +323,9 @@
         alternateBouncerUIAvailable
             .logDiffsForTable(buffer, "", "IsAlternateBouncerUIAvailable", false)
             .launchIn(applicationScope)
+        alternateBouncerVisible
+            .logDiffsForTable(buffer, "", "AlternateBouncerVisible", false)
+            .launchIn(applicationScope)
         lastShownSecurityMode
             .map { it.name }
             .logDiffsForTable(buffer, "", "lastShownSecurityMode", null)
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffectViewBinder.kt b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffectViewBinder.kt
index c464ed1..4875f48 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffectViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffectViewBinder.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.qs.tileimpl.QSTileViewImpl
 import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.flow.filterNotNull
 
 object QSLongPressEffectViewBinder {
 
@@ -49,63 +50,55 @@
                 launch({ "${tileSpec ?: "unknownTileSpec"}#LongPressEffect#action" }) {
                     var effectAnimator: ValueAnimator? = null
 
-                    qsLongPressEffect.actionType.collect { action ->
-                        action?.let {
-                            when (it) {
-                                QSLongPressEffect.ActionType.CLICK -> {
-                                    tile.performClick()
-                                    qsLongPressEffect.clearActionType()
-                                }
-                                QSLongPressEffect.ActionType.LONG_PRESS -> {
-                                    tile.prepareForLaunch()
-                                    tile.performLongClick()
-                                    qsLongPressEffect.clearActionType()
-                                }
-                                QSLongPressEffect.ActionType.RESET_AND_LONG_PRESS -> {
-                                    tile.resetLongPressEffectProperties()
-                                    tile.performLongClick()
-                                    qsLongPressEffect.clearActionType()
-                                }
-                                QSLongPressEffect.ActionType.START_ANIMATOR -> {
-                                    if (effectAnimator?.isRunning != true) {
-                                        effectAnimator =
-                                            ValueAnimator.ofFloat(0f, 1f).apply {
-                                                this.duration =
-                                                    qsLongPressEffect.effectDuration.toLong()
-                                                interpolator = AccelerateDecelerateInterpolator()
+                    qsLongPressEffect.actionType.filterNotNull().collect { action ->
+                        when (action) {
+                            QSLongPressEffect.ActionType.CLICK -> {
+                                tile.performClick()
+                                qsLongPressEffect.clearActionType()
+                            }
+                            QSLongPressEffect.ActionType.LONG_PRESS -> {
+                                tile.prepareForLaunch()
+                                tile.performLongClick()
+                                qsLongPressEffect.clearActionType()
+                            }
+                            QSLongPressEffect.ActionType.RESET_AND_LONG_PRESS -> {
+                                tile.resetLongPressEffectProperties()
+                                tile.performLongClick()
+                                qsLongPressEffect.clearActionType()
+                            }
+                            QSLongPressEffect.ActionType.START_ANIMATOR -> {
+                                if (effectAnimator?.isRunning != true) {
+                                    effectAnimator =
+                                        ValueAnimator.ofFloat(0f, 1f).apply {
+                                            this.duration =
+                                                qsLongPressEffect.effectDuration.toLong()
+                                            interpolator = AccelerateDecelerateInterpolator()
 
-                                                doOnStart {
-                                                    qsLongPressEffect.handleAnimationStart()
+                                            doOnStart { qsLongPressEffect.handleAnimationStart() }
+                                            addUpdateListener {
+                                                val value = animatedValue as Float
+                                                if (value == 0f) {
+                                                    tile.bringToFront()
+                                                } else {
+                                                    tile.updateLongPressEffectProperties(value)
                                                 }
-                                                addUpdateListener {
-                                                    val value = animatedValue as Float
-                                                    if (value == 0f) {
-                                                        tile.bringToFront()
-                                                    } else {
-                                                        tile.updateLongPressEffectProperties(value)
-                                                    }
-                                                }
-                                                doOnEnd {
-                                                    qsLongPressEffect.handleAnimationComplete()
-                                                }
-                                                doOnCancel {
-                                                    qsLongPressEffect.handleAnimationCancel()
-                                                }
-                                                start()
                                             }
-                                    }
+                                            doOnEnd { qsLongPressEffect.handleAnimationComplete() }
+                                            doOnCancel { qsLongPressEffect.handleAnimationCancel() }
+                                            start()
+                                        }
                                 }
-                                QSLongPressEffect.ActionType.REVERSE_ANIMATOR -> {
-                                    effectAnimator?.let {
-                                        val pausedProgress = it.animatedFraction
-                                        qsLongPressEffect.playReverseHaptics(pausedProgress)
-                                        it.reverse()
-                                    }
+                            }
+                            QSLongPressEffect.ActionType.REVERSE_ANIMATOR -> {
+                                effectAnimator?.let {
+                                    val pausedProgress = it.animatedFraction
+                                    qsLongPressEffect.playReverseHaptics(pausedProgress)
+                                    it.reverse()
                                 }
-                                QSLongPressEffect.ActionType.CANCEL_ANIMATOR -> {
-                                    tile.resetLongPressEffectProperties()
-                                    effectAnimator?.cancel()
-                                }
+                            }
+                            QSLongPressEffect.ActionType.CANCEL_ANIMATOR -> {
+                                tile.resetLongPressEffectProperties()
+                                effectAnimator?.cancel()
                             }
                         }
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewController.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewController.java
index 00ec1a1..44e795c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewController.java
@@ -187,18 +187,15 @@
             return;
         }
 
-        // current indication is updated to empty
+        // Current indication is updated to empty.
+        // Update to empty even if `currMsgShownForMinTime` is false.
         if (mCurrIndicationType == type
                 && !hasNewIndication
                 && showAsap) {
-            if (currMsgShownForMinTime) {
-                if (mShowNextIndicationRunnable != null) {
-                    mShowNextIndicationRunnable.runImmediately();
-                } else {
-                    showIndication(INDICATION_TYPE_NONE);
-                }
+            if (mShowNextIndicationRunnable != null) {
+                mShowNextIndicationRunnable.runImmediately();
             } else {
-                scheduleShowNextIndication(minShowDuration - timeSinceLastIndicationSwitch);
+                showIndication(INDICATION_TYPE_NONE);
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt
index a49b3ae..4a726ae 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt
@@ -65,43 +65,23 @@
      * @param blueprintId
      * @return whether the transition has succeeded.
      */
-    fun applyBlueprint(index: Int): Boolean {
-        ArrayList(blueprintIdMap.values)[index]?.let {
-            applyBlueprint(it)
-            return true
-        }
-        return false
-    }
-
-    /**
-     * Emits the blueprint value to the collectors.
-     *
-     * @param blueprintId
-     * @return whether the transition has succeeded.
-     */
     fun applyBlueprint(blueprintId: String?): Boolean {
         val blueprint = blueprintIdMap[blueprintId]
-        return if (blueprint != null) {
-            applyBlueprint(blueprint)
-            true
-        } else {
+        if (blueprint == null) {
             Log.e(
                 TAG,
                 "Could not find blueprint with id: $blueprintId. " +
                     "Perhaps it was not added to KeyguardBlueprintModule?"
             )
-            false
+            return false
         }
-    }
 
-    /** Emits the blueprint value to the collectors. */
-    fun applyBlueprint(blueprint: KeyguardBlueprint?) {
         if (blueprint == this.blueprint.value) {
-            refreshBlueprint()
-            return
+            return true
         }
 
-        blueprint?.let { this.blueprint.value = it }
+        this.blueprint.value = blueprint
+        return true
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
index da4f85e..cf995fa 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
@@ -82,12 +82,17 @@
     }
 
     /**
-     * Transitions to a blueprint.
+     * Transitions to a blueprint, or refreshes it if already applied.
      *
      * @param blueprintId
      * @return whether the transition has succeeded.
      */
-    fun transitionToBlueprint(blueprintId: String): Boolean {
+    fun transitionOrRefreshBlueprint(blueprintId: String): Boolean {
+        if (blueprintId == blueprint.value.id) {
+            refreshBlueprint()
+            return true
+        }
+
         return keyguardBlueprintRepository.applyBlueprint(blueprintId)
     }
 
@@ -97,7 +102,7 @@
      * @param blueprintId
      * @return whether the transition has succeeded.
      */
-    fun transitionToBlueprint(blueprintId: Int): Boolean {
+    fun transitionToBlueprint(blueprintId: String): Boolean {
         return keyguardBlueprintRepository.applyBlueprint(blueprintId)
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt
index 53f0132..18022a9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.keyguard.ui.binder
 
 import android.graphics.PixelFormat
+import android.util.Log
 import android.view.Gravity
 import android.view.LayoutInflater
 import android.view.View
@@ -95,11 +96,11 @@
         applicationScope.launch("$TAG#alternateBouncerWindowViewModel") {
             alternateBouncerWindowViewModel.get().alternateBouncerWindowRequired.collect {
                 addAlternateBouncerWindowView ->
+                Log.d(TAG, "alternateBouncerWindowRequired=$addAlternateBouncerWindowView")
                 if (addAlternateBouncerWindowView) {
                     addViewToWindowManager()
-                    val scrim =
+                    val scrim: ScrimView =
                         alternateBouncerView!!.requireViewById(R.id.alternate_bouncer_scrim)
-                            as ScrimView
                     scrim.viewAlpha = 0f
                     bind(alternateBouncerView!!, alternateBouncerDependencies.get())
                 } else {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
index b0d45ed..db47bfb 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
@@ -132,8 +132,12 @@
                             view.getIconState(viewModel.type, viewModel.useAodVariant),
                             /* merge */ false
                         )
-                        fgIconView.contentDescription =
-                            fgIconView.resources.getString(viewModel.type.contentDescriptionResId)
+                        if (viewModel.type.contentDescriptionResId != -1) {
+                            fgIconView.contentDescription =
+                                fgIconView.resources.getString(
+                                    viewModel.type.contentDescriptionResId
+                                )
+                        }
                         fgIconView.imageTintList = ColorStateList.valueOf(viewModel.tint)
                         fgIconView.setPadding(
                             viewModel.padding,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt
index 2735aed..5713a15 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt
@@ -214,7 +214,7 @@
             R.id.unlocked,
             R.id.locked_aod,
             context.getDrawable(R.drawable.unlocked_to_aod_lock) as AnimatedVectorDrawable,
-            /* reversible */ true,
+            /* reversible */ false,
         )
     }
 
@@ -252,6 +252,7 @@
             IconType.LOCK -> lockIconState[0] = android.R.attr.state_first
             IconType.UNLOCK -> lockIconState[0] = android.R.attr.state_last
             IconType.FINGERPRINT -> lockIconState[0] = android.R.attr.state_middle
+            IconType.NONE -> return StateSet.NOTHING
         }
         if (aod) {
             lockIconState[1] = android.R.attr.state_single
@@ -265,6 +266,7 @@
         LOCK(R.string.accessibility_lock_icon),
         UNLOCK(R.string.accessibility_unlock_button),
         FINGERPRINT(R.string.accessibility_fingerprint_label),
+        NONE(-1),
     }
 
     enum class AccessibilityHintType {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListener.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListener.kt
index ce7ec0e..962cdf1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListener.kt
@@ -46,15 +46,14 @@
                 return
             }
 
-            if (
-                arg.isDigitsOnly() && keyguardBlueprintInteractor.transitionToBlueprint(arg.toInt())
-            ) {
-                pw.println("Transition succeeded!")
-            } else if (keyguardBlueprintInteractor.transitionToBlueprint(arg)) {
-                pw.println("Transition succeeded!")
-            } else {
-                pw.println("Invalid argument! To see available blueprint ids, run:")
-                pw.println("$ adb shell cmd statusbar blueprint help")
+            when {
+                arg.isDigitsOnly() -> pw.println("Invalid argument! Use string ids.")
+                keyguardBlueprintInteractor.transitionOrRefreshBlueprint(arg) ->
+                    pw.println("Transition succeeded!")
+                else -> {
+                    pw.println("Invalid argument! To see available blueprint ids, run:")
+                    pw.println("$ adb shell cmd statusbar blueprint help")
+                }
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt
index e26b75f..40be73e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt
@@ -195,7 +195,14 @@
             isUnlocked,
         ) { isListeningForUdfps, isUnlocked ->
             if (isListeningForUdfps) {
-                DeviceEntryIconView.IconType.FINGERPRINT
+                if (isUnlocked) {
+                    // Don't show any UI until isUnlocked=false. This covers the case
+                    // when the "Power button instantly locks > 0s" or the device doesn't lock
+                    // immediately after a screen time.
+                    DeviceEntryIconView.IconType.NONE
+                } else {
+                    DeviceEntryIconView.IconType.FINGERPRINT
+                }
             } else if (isUnlocked) {
                 DeviceEntryIconView.IconType.UNLOCK
             } else {
@@ -211,7 +218,8 @@
             when (deviceEntryStatus) {
                 DeviceEntryIconView.IconType.LOCK -> isUdfps
                 DeviceEntryIconView.IconType.UNLOCK -> true
-                DeviceEntryIconView.IconType.FINGERPRINT -> false
+                DeviceEntryIconView.IconType.FINGERPRINT,
+                DeviceEntryIconView.IconType.NONE -> false
             }
         }
 
@@ -239,8 +247,8 @@
             DeviceEntryIconView.IconType.LOCK ->
                 DeviceEntryIconView.AccessibilityHintType.AUTHENTICATE
             DeviceEntryIconView.IconType.UNLOCK -> DeviceEntryIconView.AccessibilityHintType.ENTER
-            DeviceEntryIconView.IconType.FINGERPRINT ->
-                DeviceEntryIconView.AccessibilityHintType.NONE
+            DeviceEntryIconView.IconType.FINGERPRINT,
+            DeviceEntryIconView.IconType.NONE -> DeviceEntryIconView.AccessibilityHintType.NONE
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
index 3d86e3c..63acbb0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
@@ -49,21 +49,44 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 
 // TODO(307945185) Split View concerns into a ViewBinder
 /** Adapter to use between Scene system and [QSImpl] */
 interface QSSceneAdapter {
-    /** Whether [QSImpl] is currently customizing */
+
+    /**
+     * Whether we are currently customizing or entering the customizer.
+     *
+     * @see CustomizerState.isCustomizing
+     */
     val isCustomizing: StateFlow<Boolean>
 
     /**
+     * Whether the customizer is showing. This includes animating into and out of it.
+     *
+     * @see CustomizerState.isShowing
+     */
+    val isCustomizerShowing: StateFlow<Boolean>
+
+    /**
+     * The duration of the current animation in/out of customizer. If not in an animating state,
+     * this duration is 0 (to match show/hide immediately).
+     *
+     * @see CustomizerState.Animating.animationDuration
+     */
+    val customizerAnimationDuration: StateFlow<Int>
+
+    /**
      * A view with the QS content ([QSContainerImpl]), managed by an instance of [QSImpl] tracked by
      * the interactor.
      */
@@ -181,8 +204,35 @@
             onBufferOverflow = BufferOverflow.DROP_OLDEST,
         )
     private val state = MutableStateFlow<QSSceneAdapter.State>(QSSceneAdapter.State.CLOSED)
-    private val _isCustomizing: MutableStateFlow<Boolean> = MutableStateFlow(false)
-    override val isCustomizing = _isCustomizing.asStateFlow()
+    private val _customizingState: MutableStateFlow<CustomizerState> =
+        MutableStateFlow(CustomizerState.Hidden)
+    val customizerState = _customizingState.asStateFlow()
+
+    override val isCustomizing: StateFlow<Boolean> =
+        customizerState
+            .map { it.isCustomizing }
+            .stateIn(
+                applicationScope,
+                SharingStarted.WhileSubscribed(),
+                customizerState.value.isCustomizing,
+            )
+    override val isCustomizerShowing: StateFlow<Boolean> =
+        customizerState
+            .map { it.isShowing }
+            .stateIn(
+                applicationScope,
+                SharingStarted.WhileSubscribed(),
+                customizerState.value.isShowing
+            )
+    override val customizerAnimationDuration: StateFlow<Int> =
+        customizerState
+            .map { (it as? CustomizerState.Animating)?.animationDuration?.toInt() ?: 0 }
+            .stateIn(
+                applicationScope,
+                SharingStarted.WhileSubscribed(),
+                (customizerState.value as? CustomizerState.Animating)?.animationDuration?.toInt()
+                    ?: 0,
+            )
 
     private val _qsImpl: MutableStateFlow<QSImpl?> = MutableStateFlow(null)
     val qsImpl = _qsImpl.asStateFlow()
@@ -209,9 +259,9 @@
         dumpManager.registerDumpable(this)
         applicationScope.launch {
             launch {
-                state.sample(_isCustomizing, ::Pair).collect { (state, customizing) ->
+                state.sample(_customizingState, ::Pair).collect { (state, customizing) ->
                     qsImpl.value?.apply {
-                        if (state != QSSceneAdapter.State.QS && customizing) {
+                        if (state != QSSceneAdapter.State.QS && customizing.isShowing) {
                             this@apply.closeCustomizerImmediately()
                         }
                         applyState(state)
@@ -243,14 +293,38 @@
         }
     }
 
-    override fun setCustomizerAnimating(animating: Boolean) {}
+    override fun setCustomizerAnimating(animating: Boolean) {
+        if (_customizingState.value is CustomizerState.Animating && !animating) {
+            _customizingState.update {
+                if (it is CustomizerState.AnimatingIntoCustomizer) {
+                    CustomizerState.Showing
+                } else {
+                    CustomizerState.Hidden
+                }
+            }
+        }
+    }
 
     override fun setCustomizerShowing(showing: Boolean) {
-        _isCustomizing.value = showing
+        setCustomizerShowing(showing, 0L)
     }
 
     override fun setCustomizerShowing(showing: Boolean, animationDuration: Long) {
-        setCustomizerShowing(showing)
+        _customizingState.update { _ ->
+            if (showing) {
+                if (animationDuration > 0) {
+                    CustomizerState.AnimatingIntoCustomizer(animationDuration)
+                } else {
+                    CustomizerState.Showing
+                }
+            } else {
+                if (animationDuration > 0) {
+                    CustomizerState.AnimatingOutOfCustomizer(animationDuration)
+                } else {
+                    CustomizerState.Hidden
+                }
+            }
+        }
     }
 
     override fun setDetailShowing(showing: Boolean) {}
@@ -302,9 +376,50 @@
     override fun dump(pw: PrintWriter, args: Array<out String>) {
         pw.apply {
             println("Last state: ${state.value}")
-            println("Customizing: ${isCustomizing.value}")
+            println("CustomizerState: ${_customizingState.value}")
             println("QQS height: $qqsHeight")
             println("QS height: $qsHeight")
         }
     }
 }
+
+/** Current state of the customizer */
+sealed interface CustomizerState {
+
+    /**
+     * This indicates that some part of the customizer is showing. It could be animating in or out.
+     */
+    val isShowing: Boolean
+        get() = true
+
+    /**
+     * This indicates that we are currently customizing or animating into it. In particular, when
+     * animating out, this is false.
+     *
+     * @see QSCustomizer.isCustomizing
+     */
+    val isCustomizing: Boolean
+        get() = false
+
+    sealed interface Animating : CustomizerState {
+        val animationDuration: Long
+    }
+
+    /** Customizer is completely hidden, and not animating */
+    data object Hidden : CustomizerState {
+        override val isShowing = false
+    }
+
+    /** Customizer is completely showing, and not animating */
+    data object Showing : CustomizerState {
+        override val isCustomizing = true
+    }
+
+    /** Animating from [Hidden] into [Showing]. */
+    data class AnimatingIntoCustomizer(override val animationDuration: Long) : Animating {
+        override val isCustomizing = true
+    }
+
+    /** Animating from [Showing] into [Hidden]. */
+    data class AnimatingOutOfCustomizer(override val animationDuration: Long) : Animating
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
index 257c4d5..17698f9d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
@@ -79,13 +79,13 @@
         combine(
                 deviceEntryInteractor.isUnlocked,
                 deviceEntryInteractor.canSwipeToEnter,
-                qsSceneAdapter.isCustomizing,
+                qsSceneAdapter.isCustomizerShowing,
                 backScene,
-            ) { isUnlocked, canSwipeToDismiss, isCustomizing, backScene ->
+            ) { isUnlocked, canSwipeToDismiss, isCustomizerShowing, backScene ->
                 destinationScenes(
                     isUnlocked,
                     canSwipeToDismiss,
-                    isCustomizing,
+                    isCustomizerShowing,
                     backScene,
                 )
             }
@@ -96,7 +96,7 @@
                     destinationScenes(
                         isUnlocked = deviceEntryInteractor.isUnlocked.value,
                         canSwipeToDismiss = deviceEntryInteractor.canSwipeToEnter.value,
-                        isCustomizing = qsSceneAdapter.isCustomizing.value,
+                        isCustomizing = qsSceneAdapter.isCustomizerShowing.value,
                         backScene = backScene.value,
                     ),
             )
diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerSceneImpl.kt
index 3462993..864e39a 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerSceneImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerSceneImpl.kt
@@ -33,7 +33,7 @@
         get() = shadeInteractor.isQsExpanded.value
 
     override val isCustomizing: Boolean
-        get() = qsSceneAdapter.isCustomizing.value
+        get() = qsSceneAdapter.isCustomizerShowing.value
 
     @Deprecated("specific to legacy touch handling")
     override fun shouldQuickSettingsIntercept(x: Float, y: Float, yDiff: Float): Boolean {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
index 5b76acb..ac76bec 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
@@ -72,13 +72,13 @@
                 deviceEntryInteractor.isUnlocked,
                 deviceEntryInteractor.canSwipeToEnter,
                 shadeInteractor.shadeMode,
-                qsSceneAdapter.isCustomizing
-            ) { isUnlocked, canSwipeToDismiss, shadeMode, isCustomizing ->
+                qsSceneAdapter.isCustomizerShowing
+            ) { isUnlocked, canSwipeToDismiss, shadeMode, isCustomizerShowing ->
                 destinationScenes(
                     isUnlocked = isUnlocked,
                     canSwipeToDismiss = canSwipeToDismiss,
                     shadeMode = shadeMode,
-                    isCustomizing = isCustomizing
+                    isCustomizing = isCustomizerShowing
                 )
             }
             .stateIn(
@@ -89,7 +89,7 @@
                         isUnlocked = deviceEntryInteractor.isUnlocked.value,
                         canSwipeToDismiss = deviceEntryInteractor.canSwipeToEnter.value,
                         shadeMode = shadeInteractor.shadeMode.value,
-                        isCustomizing = qsSceneAdapter.isCustomizing.value,
+                        isCustomizing = qsSceneAdapter.isCustomizerShowing.value,
                     ),
             )
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
index 7c1101b..d7d3732 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
@@ -382,16 +382,15 @@
 
     private void clearCurrentMediaNotificationSession() {
         mMediaMetadata = null;
-        mBackgroundExecutor.execute(() -> {
-            if (mMediaController != null) {
-                if (DEBUG_MEDIA) {
-                    Log.v(TAG, "DEBUG_MEDIA: Disconnecting from old controller: "
-                            + mMediaController.getPackageName());
-                }
-                mMediaController.unregisterCallback(mMediaListener);
-                mMediaController = null;
+        if (mMediaController != null) {
+            if (DEBUG_MEDIA) {
+                Log.v(TAG, "DEBUG_MEDIA: Disconnecting from old controller: "
+                        + mMediaController.getPackageName());
             }
-        });
+            // TODO(b/336612071): move to background thread
+            mMediaController.unregisterCallback(mMediaListener);
+        }
+        mMediaController = null;
     }
 
     public interface MediaListener {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index f6161c5..be6bef7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -2350,6 +2350,7 @@
             } else if (mState == StatusBarState.KEYGUARD
                     && !mStatusBarKeyguardViewManager.primaryBouncerIsOrWillBeShowing()
                     && mStatusBarKeyguardViewManager.isSecure()) {
+                Log.d(TAG, "showBouncerOrLockScreenIfKeyguard, showingBouncer");
                 mStatusBarKeyguardViewManager.showBouncer(true /* scrimmed */);
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java
index 5deb08a7..cff46ab 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java
@@ -37,6 +37,7 @@
 import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger;
 import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView;
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel;
+import com.android.systemui.statusbar.pipeline.shared.ui.view.ModernStatusBarView;
 import com.android.systemui.statusbar.pipeline.wifi.ui.view.ModernStatusBarWifiView;
 import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel;
 
@@ -277,6 +278,15 @@
         addView(view, viewIndex, createLayoutParams());
     }
 
+    /** Adds a bindable icon to the demo mode view. */
+    public void addBindableIcon(StatusBarIconHolder.BindableIconHolder holder) {
+        // This doesn't do any correct ordering, and also doesn't check if we already have an
+        // existing icon for the slot. But since we hope to remove this class soon, we won't spend
+        // the time adding that logic.
+        ModernStatusBarView view = holder.getInitializer().createAndBind(mContext);
+        addView(view, createLayoutParams());
+    }
+
     public void onRemoveIcon(StatusIconDisplayable view) {
         if (view.getSlot().equals("wifi")) {
             if (view instanceof ModernStatusBarWifiView) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.kt
index bef0b28..08a890d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.kt
@@ -169,16 +169,19 @@
      * StatusBarIconController will register all available bindable icons on init (see
      * [BindableIconsRepository]), and will ignore any call to setIcon for these.
      *
-     * [initializer] a view creator that can bind the relevant view models to the created view.
+     * @property initializer a view creator that can bind the relevant view models to the created
+     *   view.
+     * @property slot the name of the slot that this holder is used for.
      */
-    class BindableIconHolder(val initializer: ModernStatusBarViewCreator) : StatusBarIconHolder() {
+    class BindableIconHolder(val initializer: ModernStatusBarViewCreator, val slot: String) :
+        StatusBarIconHolder() {
         override var type: Int = TYPE_BINDABLE
 
         /** This is unused, as bindable icons use their own view binders to control visibility */
         override var isVisible: Boolean = true
 
         override fun toString(): String {
-            return ("StatusBarIconHolder(type=BINDABLE)")
+            return ("StatusBarIconHolder(type=BINDABLE, slot=$slot)")
         }
     }
 }
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 7301b87..f0dab3b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -744,6 +744,7 @@
     public void showBouncer(boolean scrimmed) {
         if (DeviceEntryUdfpsRefactor.isEnabled()) {
             if (mAlternateBouncerInteractor.canShowAlternateBouncerForFingerprint()) {
+                Log.d(TAG, "showBouncer:alternateBouncer.forceShow()");
                 mAlternateBouncerInteractor.forceShow();
                 updateAlternateBouncerShowing(mAlternateBouncerInteractor.isVisibleState());
             } else {
@@ -869,6 +870,7 @@
                     }
 
                     if (DeviceEntryUdfpsRefactor.isEnabled()) {
+                        Log.d(TAG, "dismissWithAction:alternateBouncer.forceShow()");
                         mAlternateBouncerInteractor.forceShow();
                         updateAlternateBouncerShowing(mAlternateBouncerInteractor.isVisibleState());
                     } else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/IconManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/IconManager.java
index 0ed9420..5ad7376 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/IconManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/IconManager.java
@@ -37,6 +37,7 @@
 import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider;
 import com.android.systemui.statusbar.phone.DemoStatusIcons;
 import com.android.systemui.statusbar.phone.StatusBarIconHolder;
+import com.android.systemui.statusbar.phone.StatusBarIconHolder.BindableIconHolder;
 import com.android.systemui.statusbar.phone.StatusBarLocation;
 import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter;
 import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconsBinder;
@@ -49,7 +50,9 @@
 import com.android.systemui.util.Assert;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 /**
  * Turns info from StatusBarIconController into ImageViews in a ViewGroup.
@@ -60,6 +63,11 @@
     private final LocationBasedWifiViewModel mWifiViewModel;
     private final MobileIconsViewModel mMobileIconsViewModel;
 
+    /**
+     * Stores the list of bindable icons that have been added, keyed on slot name. This ensures
+     * we don't accidentally add the same bindable icon twice.
+     */
+    private final Map<String, BindableIconHolder> mBindableIcons = new HashMap<>();
     protected final Context mContext;
     protected int mIconSize;
     // Whether or not these icons show up in dumpsys
@@ -142,7 +150,7 @@
             case TYPE_MOBILE_NEW -> addNewMobileIcon(index, slot, holder.getTag());
             case TYPE_BINDABLE ->
                 // Safe cast, since only BindableIconHolders can set this tag on themselves
-                addBindableIcon((StatusBarIconHolder.BindableIconHolder) holder, index);
+                addBindableIcon((BindableIconHolder) holder, index);
             default -> null;
         };
     }
@@ -162,10 +170,14 @@
      * icon view, we can simply create the icon when requested and allow the
      * ViewBinder to control its visual state.
      */
-    protected StatusIconDisplayable addBindableIcon(StatusBarIconHolder.BindableIconHolder holder,
+    protected StatusIconDisplayable addBindableIcon(BindableIconHolder holder,
             int index) {
+        mBindableIcons.put(holder.getSlot(), holder);
         ModernStatusBarView view = holder.getInitializer().createAndBind(mContext);
         mGroup.addView(view, index, onCreateLayoutParams());
+        if (mIsInDemoMode) {
+            mDemoStatusIcons.addBindableIcon(holder);
+        }
         return view;
     }
 
@@ -278,6 +290,9 @@
         if (mDemoStatusIcons == null) {
             mDemoStatusIcons = createDemoStatusIcons();
             mDemoStatusIcons.addModernWifiView(mWifiViewModel);
+            for (BindableIconHolder holder : mBindableIcons.values()) {
+                mDemoStatusIcons.addBindableIcon(holder);
+            }
         }
         mDemoStatusIcons.onDemoModeStarted();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/StatusBarIconControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/StatusBarIconControllerImpl.java
index 92d90af..fabf858d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/StatusBarIconControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/StatusBarIconControllerImpl.java
@@ -213,7 +213,8 @@
         StatusBarIconHolder existingHolder = mStatusBarIconList.getIconHolder(icon.getSlot(), 0);
         // Expected to be null
         if (existingHolder == null) {
-            BindableIconHolder bindableIcon = new BindableIconHolder(icon.getInitializer());
+            BindableIconHolder bindableIcon =
+                    new BindableIconHolder(icon.getInitializer(), icon.getSlot());
             setIcon(icon.getSlot(), bindableIcon);
         } else {
             Log.e(TAG, "addBindableIcon called, but icon has already been added. Ignoring");
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
index b80ff38..226a84a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
@@ -41,6 +41,8 @@
 import com.android.systemui.statusbar.pipeline.mobile.util.SubscriptionManagerProxy
 import com.android.systemui.statusbar.pipeline.mobile.util.SubscriptionManagerProxyImpl
 import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository
+import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepositorySwitcher
+import com.android.systemui.statusbar.pipeline.satellite.data.RealDeviceBasedSatelliteRepository
 import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl
 import com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel.DeviceBasedSatelliteViewModel
 import com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel.DeviceBasedSatelliteViewModelImpl
@@ -83,8 +85,13 @@
     abstract fun connectivityRepository(impl: ConnectivityRepositoryImpl): ConnectivityRepository
 
     @Binds
-    abstract fun deviceBasedSatelliteRepository(
+    abstract fun realDeviceBasedSatelliteRepository(
         impl: DeviceBasedSatelliteRepositoryImpl
+    ): RealDeviceBasedSatelliteRepository
+
+    @Binds
+    abstract fun deviceBasedSatelliteRepository(
+        impl: DeviceBasedSatelliteRepositorySwitcher
     ): DeviceBasedSatelliteRepository
 
     @Binds
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepository.kt
index ad8b810..d38e834 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepository.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.statusbar.pipeline.satellite.data
 
 import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
-import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
 
 /**
  * Device-based satellite refers to the capability of a device to connect directly to a satellite
@@ -26,12 +26,22 @@
  */
 interface DeviceBasedSatelliteRepository {
     /** See [SatelliteConnectionState] for available states */
-    val connectionState: Flow<SatelliteConnectionState>
+    val connectionState: StateFlow<SatelliteConnectionState>
 
     /** 0-4 level (similar to wifi and mobile) */
     // @IntRange(from = 0, to = 4)
-    val signalStrength: Flow<Int>
+    val signalStrength: StateFlow<Int>
 
     /** Clients must observe this property, as device-based satellite is location-dependent */
-    val isSatelliteAllowedForCurrentLocation: Flow<Boolean>
+    val isSatelliteAllowedForCurrentLocation: StateFlow<Boolean>
 }
+
+/**
+ * A no-op interface used for Dagger bindings.
+ *
+ * [DeviceBasedSatelliteRepositorySwitcher] needs to inject both the real repository and the demo
+ * mode repository, both of which implement the [DeviceBasedSatelliteRepository] interface. To help
+ * distinguish the two for the switcher, [DeviceBasedSatelliteRepositoryImpl] will implement this
+ * [RealDeviceBasedSatelliteRepository] interface.
+ */
+interface RealDeviceBasedSatelliteRepository : DeviceBasedSatelliteRepository
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepositorySwitcher.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepositorySwitcher.kt
new file mode 100644
index 0000000..6b1bc65
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepositorySwitcher.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.satellite.data
+
+import android.os.Bundle
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.demomode.DemoMode
+import com.android.systemui.demomode.DemoModeController
+import com.android.systemui.statusbar.pipeline.satellite.data.demo.DemoDeviceBasedSatelliteRepository
+import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * A provider for the [DeviceBasedSatelliteRepository] interface that can choose between the Demo
+ * and Prod concrete implementations at runtime. It works by defining a base flow, [activeRepo],
+ * which switches based on the latest information from [DemoModeController], and switches every flow
+ * in the interface to point to the currently-active provider. This allows us to put the demo mode
+ * interface in its own repository, completely separate from the real version, while still using all
+ * of the prod implementations for the rest of the pipeline (interactors and onward). Looks
+ * something like this:
+ * ```
+ * RealRepository
+ *                 │
+ *                 ├──►RepositorySwitcher──►RealInteractor──►RealViewModel
+ *                 │
+ * DemoRepository
+ * ```
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class DeviceBasedSatelliteRepositorySwitcher
+@Inject
+constructor(
+    private val realImpl: RealDeviceBasedSatelliteRepository,
+    private val demoImpl: DemoDeviceBasedSatelliteRepository,
+    private val demoModeController: DemoModeController,
+    @Application scope: CoroutineScope,
+) : DeviceBasedSatelliteRepository {
+    private val isDemoMode =
+        conflatedCallbackFlow {
+                val callback =
+                    object : DemoMode {
+                        override fun dispatchDemoCommand(command: String?, args: Bundle?) {
+                            // Don't care
+                        }
+
+                        override fun onDemoModeStarted() {
+                            demoImpl.startProcessingCommands()
+                            trySend(true)
+                        }
+
+                        override fun onDemoModeFinished() {
+                            demoImpl.stopProcessingCommands()
+                            trySend(false)
+                        }
+                    }
+
+                demoModeController.addCallback(callback)
+                awaitClose { demoModeController.removeCallback(callback) }
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), demoModeController.isInDemoMode)
+
+    @VisibleForTesting
+    val activeRepo: StateFlow<DeviceBasedSatelliteRepository> =
+        isDemoMode
+            .mapLatest { isDemoMode ->
+                if (isDemoMode) {
+                    demoImpl
+                } else {
+                    realImpl
+                }
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), realImpl)
+
+    override val connectionState: StateFlow<SatelliteConnectionState> =
+        activeRepo
+            .flatMapLatest { it.connectionState }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), realImpl.connectionState.value)
+
+    override val signalStrength: StateFlow<Int> =
+        activeRepo
+            .flatMapLatest { it.signalStrength }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), realImpl.signalStrength.value)
+
+    override val isSatelliteAllowedForCurrentLocation: StateFlow<Boolean> =
+        activeRepo
+            .flatMapLatest { it.isSatelliteAllowedForCurrentLocation }
+            .stateIn(
+                scope,
+                SharingStarted.WhileSubscribed(),
+                realImpl.isSatelliteAllowedForCurrentLocation.value
+            )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/demo/DemoDeviceBasedSatelliteDataSource.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/demo/DemoDeviceBasedSatelliteDataSource.kt
new file mode 100644
index 0000000..7ecc29b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/demo/DemoDeviceBasedSatelliteDataSource.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.satellite.data.demo
+
+import android.os.Bundle
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.demomode.DemoMode
+import com.android.systemui.demomode.DemoModeController
+import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * Reads the incoming demo commands and emits the satellite-related commands to [satelliteEvents]
+ * for the demo repository to consume.
+ */
+@SysUISingleton
+class DemoDeviceBasedSatelliteDataSource
+@Inject
+constructor(
+    demoModeController: DemoModeController,
+    @Application scope: CoroutineScope,
+) {
+    private val demoCommandStream = demoModeController.demoFlowForCommand(DemoMode.COMMAND_NETWORK)
+    private val _satelliteCommands =
+        demoCommandStream.map { args -> args.toSatelliteEvent() }.filterNotNull()
+
+    /** A flow that emits the demo commands that are satellite-related. */
+    val satelliteEvents =
+        _satelliteCommands.stateIn(scope, SharingStarted.WhileSubscribed(), DEFAULT_VALUE)
+
+    private fun Bundle.toSatelliteEvent(): DemoSatelliteEvent? {
+        val satellite = getString("satellite") ?: return null
+        if (satellite != "show") {
+            return null
+        }
+
+        return DemoSatelliteEvent(
+            connectionState = getString("connection").toConnectionState(),
+            signalStrength = getString("level")?.toInt() ?: 0,
+        )
+    }
+
+    data class DemoSatelliteEvent(
+        val connectionState: SatelliteConnectionState,
+        val signalStrength: Int,
+    )
+
+    private fun String?.toConnectionState(): SatelliteConnectionState {
+        if (this == null) {
+            return SatelliteConnectionState.Unknown
+        }
+        return try {
+            // Lets people use "connected" on the command line and have it be correctly converted
+            // to [SatelliteConnectionState.Connected] with a capital C.
+            SatelliteConnectionState.valueOf(this.replaceFirstChar { it.uppercase() })
+        } catch (e: IllegalArgumentException) {
+            SatelliteConnectionState.Unknown
+        }
+    }
+
+    private companion object {
+        val DEFAULT_VALUE = DemoSatelliteEvent(SatelliteConnectionState.Unknown, signalStrength = 0)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/demo/DemoDeviceBasedSatelliteRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/demo/DemoDeviceBasedSatelliteRepository.kt
new file mode 100644
index 0000000..56034f0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/demo/DemoDeviceBasedSatelliteRepository.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.satellite.data.demo
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository
+import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.launch
+
+/** A satellite repository that represents the latest satellite values sent via demo mode. */
+@SysUISingleton
+class DemoDeviceBasedSatelliteRepository
+@Inject
+constructor(
+    private val dataSource: DemoDeviceBasedSatelliteDataSource,
+    @Application private val scope: CoroutineScope,
+) : DeviceBasedSatelliteRepository {
+    private var demoCommandJob: Job? = null
+
+    override val connectionState = MutableStateFlow(SatelliteConnectionState.Unknown)
+    override val signalStrength = MutableStateFlow(0)
+    override val isSatelliteAllowedForCurrentLocation = MutableStateFlow(true)
+
+    fun startProcessingCommands() {
+        demoCommandJob =
+            scope.launch { dataSource.satelliteEvents.collect { event -> processEvent(event) } }
+    }
+
+    fun stopProcessingCommands() {
+        demoCommandJob?.cancel()
+    }
+
+    private fun processEvent(event: DemoDeviceBasedSatelliteDataSource.DemoSatelliteEvent) {
+        connectionState.value = event.connectionState
+        signalStrength.value = event.signalStrength
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
index 3e3ea85..0739b836 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
@@ -31,7 +31,7 @@
 import com.android.systemui.log.core.MessageInitializer
 import com.android.systemui.log.core.MessagePrinter
 import com.android.systemui.statusbar.pipeline.dagger.OemSatelliteInputLog
-import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository
+import com.android.systemui.statusbar.pipeline.satellite.data.RealDeviceBasedSatelliteRepository
 import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Companion.whenSupported
 import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.NotSupported
 import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Supported
@@ -50,12 +50,14 @@
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.suspendCancellableCoroutine
 import kotlinx.coroutines.withContext
@@ -134,7 +136,7 @@
     @Application private val scope: CoroutineScope,
     @OemSatelliteInputLog private val logBuffer: LogBuffer,
     private val systemClock: SystemClock,
-) : DeviceBasedSatelliteRepository {
+) : RealDeviceBasedSatelliteRepository {
 
     private val satelliteManager: SatelliteManager?
 
@@ -200,10 +202,12 @@
     }
 
     override val connectionState =
-        satelliteSupport.whenSupported(
-            supported = ::connectionStateFlow,
-            orElse = flowOf(SatelliteConnectionState.Off)
-        )
+        satelliteSupport
+            .whenSupported(
+                supported = ::connectionStateFlow,
+                orElse = flowOf(SatelliteConnectionState.Off)
+            )
+            .stateIn(scope, SharingStarted.Eagerly, SatelliteConnectionState.Off)
 
     // By using the SupportedSatelliteManager here, we expect registration never to fail
     private fun connectionStateFlow(sm: SupportedSatelliteManager): Flow<SatelliteConnectionState> =
@@ -227,7 +231,9 @@
             .flowOn(bgDispatcher)
 
     override val signalStrength =
-        satelliteSupport.whenSupported(supported = ::signalStrengthFlow, orElse = flowOf(0))
+        satelliteSupport
+            .whenSupported(supported = ::signalStrengthFlow, orElse = flowOf(0))
+            .stateIn(scope, SharingStarted.Eagerly, 0)
 
     // By using the SupportedSatelliteManager here, we expect registration never to fail
     private fun signalStrengthFlow(sm: SupportedSatelliteManager) =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewControllerTest.java
index 39fcd41..5b836b6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewControllerTest.java
@@ -346,6 +346,24 @@
     }
 
     @Test
+    public void testStartDozing_withMinShowTime() {
+        // GIVEN a biometric message is showing
+        mController.updateIndication(INDICATION_TYPE_BIOMETRIC_MESSAGE,
+                new KeyguardIndication.Builder()
+                        .setMessage("test_message")
+                        .setMinVisibilityMillis(5000L)
+                        .setTextColor(ColorStateList.valueOf(Color.WHITE))
+                        .build(),
+                true);
+
+        // WHEN the device wants to hide the biometric message
+        mController.hideIndication(INDICATION_TYPE_BIOMETRIC_MESSAGE);
+
+        // THEN switch to INDICATION_TYPE_NONE
+        verify(mView).switchIndication(null);
+    }
+
+    @Test
     public void testStoppedDozing() {
         // GIVEN we're dozing & we have an indication message
         mStatusBarStateListener.onDozingChanged(true);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt
index bcaad01..f5b5261 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt
@@ -19,24 +19,20 @@
 
 package com.android.systemui.keyguard.data.repository
 
-import android.os.fakeExecutorHandler
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.ui.data.repository.ConfigurationRepository
-import com.android.systemui.concurrency.fakeExecutor
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint
-import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint.Companion.DEFAULT
+import com.android.systemui.keyguard.ui.view.layout.blueprints.SplitShadeKeyguardBlueprint
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.testKosmos
 import com.android.systemui.util.ThreadAssert
-import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
@@ -50,31 +46,32 @@
 class KeyguardBlueprintRepositoryTest : SysuiTestCase() {
     private lateinit var underTest: KeyguardBlueprintRepository
     @Mock lateinit var configurationRepository: ConfigurationRepository
-    @Mock lateinit var defaultLockscreenBlueprint: DefaultKeyguardBlueprint
     @Mock lateinit var threadAssert: ThreadAssert
+
     private val testScope = TestScope(StandardTestDispatcher())
     private val kosmos: Kosmos = testKosmos()
 
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
-        with(kosmos) {
-            whenever(defaultLockscreenBlueprint.id).thenReturn(DEFAULT)
-            underTest =
-                KeyguardBlueprintRepository(
-                    setOf(defaultLockscreenBlueprint),
-                    fakeExecutorHandler,
-                    threadAssert,
-                )
-        }
+        underTest = kosmos.keyguardBlueprintRepository
     }
 
     @Test
     fun testApplyBlueprint_DefaultLayout() {
         testScope.runTest {
             val blueprint by collectLastValue(underTest.blueprint)
-            underTest.applyBlueprint(defaultLockscreenBlueprint)
-            assertThat(blueprint).isEqualTo(defaultLockscreenBlueprint)
+            underTest.applyBlueprint(DefaultKeyguardBlueprint.DEFAULT)
+            assertThat(blueprint).isEqualTo(kosmos.defaultKeyguardBlueprint)
+        }
+    }
+
+    @Test
+    fun testApplyBlueprint_SplitShadeLayout() {
+        testScope.runTest {
+            val blueprint by collectLastValue(underTest.blueprint)
+            underTest.applyBlueprint(SplitShadeKeyguardBlueprint.ID)
+            assertThat(blueprint).isEqualTo(kosmos.splitShadeBlueprint)
         }
     }
 
@@ -83,33 +80,22 @@
         testScope.runTest {
             val blueprint by collectLastValue(underTest.blueprint)
             underTest.refreshBlueprint()
-            assertThat(blueprint).isEqualTo(defaultLockscreenBlueprint)
+            assertThat(blueprint).isEqualTo(kosmos.defaultKeyguardBlueprint)
         }
     }
 
     @Test
-    fun testTransitionToLayout_validId() {
-        assertThat(underTest.applyBlueprint(DEFAULT)).isTrue()
+    fun testTransitionToDefaultLayout_validId() {
+        assertThat(underTest.applyBlueprint(DefaultKeyguardBlueprint.DEFAULT)).isTrue()
+    }
+
+    @Test
+    fun testTransitionToSplitShadeLayout_validId() {
+        assertThat(underTest.applyBlueprint(SplitShadeKeyguardBlueprint.ID)).isTrue()
     }
 
     @Test
     fun testTransitionToLayout_invalidId() {
         assertThat(underTest.applyBlueprint("abc")).isFalse()
     }
-
-    @Test
-    fun testTransitionToSameBlueprint_refreshesBlueprint() =
-        with(kosmos) {
-            testScope.runTest {
-                val transition by collectLastValue(underTest.refreshTransition)
-                fakeExecutor.runAllReady()
-                runCurrent()
-
-                underTest.applyBlueprint(defaultLockscreenBlueprint)
-                fakeExecutor.runAllReady()
-                runCurrent()
-
-                assertThat(transition).isNotNull()
-            }
-        }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListenerTest.kt
index dbf6a29..8a0613f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListenerTest.kt
@@ -66,25 +66,19 @@
     fun testHelp() {
         command().execute(pw, listOf("help"))
         verify(pw, atLeastOnce()).println(anyString())
-        verify(keyguardBlueprintInteractor, never()).transitionToBlueprint(anyString())
+        verify(keyguardBlueprintInteractor, never()).transitionOrRefreshBlueprint(anyString())
     }
 
     @Test
     fun testBlank() {
         command().execute(pw, listOf())
         verify(pw, atLeastOnce()).println(anyString())
-        verify(keyguardBlueprintInteractor, never()).transitionToBlueprint(anyString())
+        verify(keyguardBlueprintInteractor, never()).transitionOrRefreshBlueprint(anyString())
     }
 
     @Test
     fun testValidArg() {
         command().execute(pw, listOf("fake"))
-        verify(keyguardBlueprintInteractor).transitionToBlueprint("fake")
-    }
-
-    @Test
-    fun testValidArg_Int() {
-        command().execute(pw, listOf("1"))
-        verify(keyguardBlueprintInteractor).transitionToBlueprint(1)
+        verify(keyguardBlueprintInteractor).transitionOrRefreshBlueprint("fake")
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelTest.kt
index 4bb0d47..0bca367 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelTest.kt
@@ -26,7 +26,7 @@
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
-import com.android.systemui.keyguard.data.repository.keyguardRepository
+import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
 import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryIconViewModel.Companion.UNLOCKED_DELAY_MS
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.testKosmos
@@ -110,6 +110,46 @@
             assertThat(isVisible).isTrue()
         }
 
+    @Test
+    fun iconType_fingerprint() =
+        testScope.runTest {
+            val iconType by collectLastValue(underTest.iconType)
+            keyguardRepository.setKeyguardDismissible(false)
+            fingerprintPropertyRepository.supportsUdfps()
+            fingerprintAuthRepository.setIsRunning(true)
+            assertThat(iconType).isEqualTo(DeviceEntryIconView.IconType.FINGERPRINT)
+        }
+
+    @Test
+    fun iconType_locked() =
+        testScope.runTest {
+            val iconType by collectLastValue(underTest.iconType)
+            keyguardRepository.setKeyguardDismissible(false)
+            fingerprintAuthRepository.setIsRunning(false)
+            assertThat(iconType).isEqualTo(DeviceEntryIconView.IconType.LOCK)
+        }
+
+    @Test
+    fun iconType_unlocked() =
+        testScope.runTest {
+            val iconType by collectLastValue(underTest.iconType)
+            keyguardRepository.setKeyguardDismissible(true)
+            advanceTimeBy(UNLOCKED_DELAY_MS * 2) // wait for unlocked delay
+            fingerprintAuthRepository.setIsRunning(false)
+            assertThat(iconType).isEqualTo(DeviceEntryIconView.IconType.UNLOCK)
+        }
+
+    @Test
+    fun iconType_none() =
+        testScope.runTest {
+            val iconType by collectLastValue(underTest.iconType)
+            keyguardRepository.setKeyguardDismissible(true)
+            advanceTimeBy(UNLOCKED_DELAY_MS * 2) // wait for unlocked delay
+            fingerprintPropertyRepository.supportsUdfps()
+            fingerprintAuthRepository.setIsRunning(true)
+            assertThat(iconType).isEqualTo(DeviceEntryIconView.IconType.NONE)
+        }
+
     private fun deviceEntryIconTransitionAlpha(alpha: Float) {
         deviceEntryIconTransition.setDeviceEntryParentViewAlpha(alpha)
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepositorySwitcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepositorySwitcherTest.kt
new file mode 100644
index 0000000..7ca3b1c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepositorySwitcherTest.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.satellite.data
+
+import android.telephony.satellite.SatelliteManager
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.demomode.DemoMode
+import com.android.systemui.demomode.DemoModeController
+import com.android.systemui.log.core.FakeLogBuffer
+import com.android.systemui.statusbar.pipeline.satellite.data.demo.DemoDeviceBasedSatelliteDataSource
+import com.android.systemui.statusbar.pipeline.satellite.data.demo.DemoDeviceBasedSatelliteRepository
+import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl
+import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
+import com.android.systemui.util.mockito.kotlinArgumentCaptor
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import java.util.Optional
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+@SmallTest
+class DeviceBasedSatelliteRepositorySwitcherTest : SysuiTestCase() {
+    private val testDispatcher = StandardTestDispatcher()
+    private val testScope = TestScope(testDispatcher)
+
+    private val demoModeController =
+        mock<DemoModeController>().apply { whenever(this.isInDemoMode).thenReturn(false) }
+    private val satelliteManager = mock<SatelliteManager>()
+    private val systemClock = FakeSystemClock()
+
+    private val realImpl =
+        DeviceBasedSatelliteRepositoryImpl(
+            Optional.of(satelliteManager),
+            testDispatcher,
+            testScope.backgroundScope,
+            FakeLogBuffer.Factory.create(),
+            systemClock,
+        )
+    private val demoDataSource =
+        mock<DemoDeviceBasedSatelliteDataSource>().also {
+            whenever(it.satelliteEvents)
+                .thenReturn(
+                    MutableStateFlow(
+                        DemoDeviceBasedSatelliteDataSource.DemoSatelliteEvent(
+                            connectionState = SatelliteConnectionState.Unknown,
+                            signalStrength = 0,
+                        )
+                    )
+                )
+        }
+    private val demoImpl =
+        DemoDeviceBasedSatelliteRepository(demoDataSource, testScope.backgroundScope)
+
+    private val underTest =
+        DeviceBasedSatelliteRepositorySwitcher(
+            realImpl,
+            demoImpl,
+            demoModeController,
+            testScope.backgroundScope,
+        )
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun switcherActiveRepo_updatesWhenDemoModeChanges() =
+        testScope.runTest {
+            assertThat(underTest.activeRepo.value).isSameInstanceAs(realImpl)
+
+            val latest by collectLastValue(underTest.activeRepo)
+            runCurrent()
+
+            startDemoMode()
+
+            assertThat(latest).isSameInstanceAs(demoImpl)
+
+            finishDemoMode()
+
+            assertThat(latest).isSameInstanceAs(realImpl)
+        }
+
+    private fun startDemoMode() {
+        whenever(demoModeController.isInDemoMode).thenReturn(true)
+        getDemoModeCallback().onDemoModeStarted()
+    }
+
+    private fun finishDemoMode() {
+        whenever(demoModeController.isInDemoMode).thenReturn(false)
+        getDemoModeCallback().onDemoModeFinished()
+    }
+
+    private fun getDemoModeCallback(): DemoMode {
+        val captor = kotlinArgumentCaptor<DemoMode>()
+        verify(demoModeController).addCallback(captor.capture())
+        return captor.value
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/demo/DemoDeviceBasedSatelliteRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/demo/DemoDeviceBasedSatelliteRepositoryTest.kt
new file mode 100644
index 0000000..f77fd19
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/demo/DemoDeviceBasedSatelliteRepositoryTest.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.satellite.data.demo
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+
+@SmallTest
+class DemoDeviceBasedSatelliteRepositoryTest : SysuiTestCase() {
+
+    private val testDispatcher = StandardTestDispatcher()
+    private val testScope = TestScope(testDispatcher)
+
+    private val fakeSatelliteEvents =
+        MutableStateFlow(
+            DemoDeviceBasedSatelliteDataSource.DemoSatelliteEvent(
+                connectionState = SatelliteConnectionState.Unknown,
+                signalStrength = 0,
+            )
+        )
+
+    private lateinit var dataSource: DemoDeviceBasedSatelliteDataSource
+
+    private lateinit var underTest: DemoDeviceBasedSatelliteRepository
+
+    @Before
+    fun setUp() {
+        dataSource =
+            mock<DemoDeviceBasedSatelliteDataSource>().also {
+                whenever(it.satelliteEvents).thenReturn(fakeSatelliteEvents)
+            }
+
+        underTest = DemoDeviceBasedSatelliteRepository(dataSource, testScope.backgroundScope)
+    }
+
+    @Test
+    fun startProcessing_getsNewUpdates() =
+        testScope.runTest {
+            val latestConnection by collectLastValue(underTest.connectionState)
+            val latestSignalStrength by collectLastValue(underTest.signalStrength)
+
+            underTest.startProcessingCommands()
+
+            fakeSatelliteEvents.value =
+                DemoDeviceBasedSatelliteDataSource.DemoSatelliteEvent(
+                    connectionState = SatelliteConnectionState.On,
+                    signalStrength = 3,
+                )
+
+            assertThat(latestConnection).isEqualTo(SatelliteConnectionState.On)
+            assertThat(latestSignalStrength).isEqualTo(3)
+
+            fakeSatelliteEvents.value =
+                DemoDeviceBasedSatelliteDataSource.DemoSatelliteEvent(
+                    connectionState = SatelliteConnectionState.Connected,
+                    signalStrength = 4,
+                )
+
+            assertThat(latestConnection).isEqualTo(SatelliteConnectionState.Connected)
+            assertThat(latestSignalStrength).isEqualTo(4)
+        }
+
+    @Test
+    fun stopProcessing_stopsGettingUpdates() =
+        testScope.runTest {
+            val latestConnection by collectLastValue(underTest.connectionState)
+            val latestSignalStrength by collectLastValue(underTest.signalStrength)
+
+            underTest.startProcessingCommands()
+
+            fakeSatelliteEvents.value =
+                DemoDeviceBasedSatelliteDataSource.DemoSatelliteEvent(
+                    connectionState = SatelliteConnectionState.On,
+                    signalStrength = 3,
+                )
+            assertThat(latestConnection).isEqualTo(SatelliteConnectionState.On)
+            assertThat(latestSignalStrength).isEqualTo(3)
+
+            underTest.stopProcessingCommands()
+
+            // WHEN new values are emitted
+            fakeSatelliteEvents.value =
+                DemoDeviceBasedSatelliteDataSource.DemoSatelliteEvent(
+                    connectionState = SatelliteConnectionState.Connected,
+                    signalStrength = 4,
+                )
+
+            // THEN they're not collected because we stopped processing commands, so the old values
+            // are still present
+            assertThat(latestConnection).isEqualTo(SatelliteConnectionState.On)
+            assertThat(latestSignalStrength).isEqualTo(3)
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
index 77e48bff..6b0ad4b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
@@ -156,7 +156,7 @@
                     verify(satelliteManager).registerForNtnSignalStrengthChanged(any(), capture())
                 }
 
-            assertThat(latest).isNull()
+            assertThat(latest).isEqualTo(0)
 
             callback.onNtnSignalStrengthChanged(NtnSignalStrength(1))
             assertThat(latest).isEqualTo(1)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt
index a654d6f..e0f60e9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt
@@ -21,7 +21,6 @@
 import com.android.systemui.settings.brightness.MirrorController
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.filterNotNull
 
@@ -30,8 +29,16 @@
     override val qqsHeight: Int = 0,
     override val qsHeight: Int = 0,
 ) : QSSceneAdapter {
+    private val _customizerState = MutableStateFlow<CustomizerState>(CustomizerState.Hidden)
+
+    private val _customizerShowing = MutableStateFlow(false)
+    override val isCustomizerShowing = _customizerShowing.asStateFlow()
+
     private val _customizing = MutableStateFlow(false)
-    override val isCustomizing: StateFlow<Boolean> = _customizing.asStateFlow()
+    override val isCustomizing = _customizing.asStateFlow()
+
+    private val _animationDuration = MutableStateFlow(0)
+    override val customizerAnimationDuration = _animationDuration.asStateFlow()
 
     private val _view = MutableStateFlow<View?>(null)
     override val qsView: Flow<View> = _view.filterNotNull()
@@ -58,7 +65,7 @@
     }
 
     fun setCustomizing(value: Boolean) {
-        _customizing.value = value
+        updateCustomizerFlows(if (value) CustomizerState.Showing else CustomizerState.Hidden)
     }
 
     override suspend fun applyBottomNavBarPadding(padding: Int) {
@@ -66,10 +73,18 @@
     }
 
     override fun requestCloseCustomizer() {
-        _customizing.value = false
+        updateCustomizerFlows(CustomizerState.Hidden)
     }
 
     override fun setBrightnessMirrorController(mirrorController: MirrorController?) {
         brightnessMirrorController = mirrorController
     }
+
+    private fun updateCustomizerFlows(customizerState: CustomizerState) {
+        _customizerState.value = customizerState
+        _customizing.value = customizerState.isCustomizing
+        _customizerShowing.value = customizerState.isShowing
+        _animationDuration.value =
+            (customizerState as? CustomizerState.Animating)?.animationDuration?.toInt() ?: 0
+    }
 }
diff --git a/services/autofill/java/com/android/server/autofill/SaveEventLogger.java b/services/autofill/java/com/android/server/autofill/SaveEventLogger.java
index 28e8e30..4f95893 100644
--- a/services/autofill/java/com/android/server/autofill/SaveEventLogger.java
+++ b/services/autofill/java/com/android/server/autofill/SaveEventLogger.java
@@ -34,6 +34,7 @@
 import static com.android.server.autofill.Helper.sVerbose;
 
 import android.annotation.IntDef;
+import android.os.SystemClock;
 import android.util.Slog;
 
 import com.android.internal.util.FrameworkStatsLog;
@@ -45,7 +46,7 @@
 /**
  * Helper class to log Autofill Save event stats.
  */
-public final class SaveEventLogger {
+public class SaveEventLogger {
   private static final String TAG = "SaveEventLogger";
 
   /**
@@ -112,19 +113,21 @@
   public static final int NO_SAVE_REASON_WITH_DONT_SAVE_ON_FINISH_FLAG =
       AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_WITH_DONT_SAVE_ON_FINISH_FLAG;
 
+  public static final long UNINITIATED_TIMESTAMP = Long.MIN_VALUE;
+
   private final int mSessionId;
   private Optional<SaveEventInternal> mEventInternal;
+  private long mSessionStartTimestamp;
 
-  private SaveEventLogger(int sessionId) {
-    mSessionId = sessionId;
-    mEventInternal = Optional.of(new SaveEventInternal());
+  private SaveEventLogger(int sessionId, long sessionStartTimestamp) {
+      mSessionId = sessionId;
+      mEventInternal = Optional.of(new SaveEventInternal());
+      mSessionStartTimestamp = sessionStartTimestamp;
   }
 
-  /**
-   * A factory constructor to create FillRequestEventLogger.
-   */
-  public static SaveEventLogger forSessionId(int sessionId) {
-    return new SaveEventLogger(sessionId);
+  /** A factory constructor to create FillRequestEventLogger. */
+  public static SaveEventLogger forSessionId(int sessionId, long sessionStartTimestamp) {
+        return new SaveEventLogger(sessionId, sessionStartTimestamp);
   }
 
   /**
@@ -224,6 +227,15 @@
     });
   }
 
+  /* Returns timestamp (relative to mSessionStartTimestamp) or  UNINITIATED_TIMESTAMP if mSessionStartTimestamp is not set */
+  private long getCurrentTimestamp() {
+    long timestamp = UNINITIATED_TIMESTAMP;
+    if (mSessionStartTimestamp != UNINITIATED_TIMESTAMP) {
+      timestamp = SystemClock.elapsedRealtime() - mSessionStartTimestamp;
+    }
+    return timestamp;
+  }
+
   /**
    * Set latency_save_ui_display_millis as long as mEventInternal presents.
    */
@@ -233,6 +245,11 @@
     });
   }
 
+  /** Set latency_save_ui_display_millis as long as mEventInternal presents. */
+  public void maybeSetLatencySaveUiDisplayMillis() {
+    maybeSetLatencySaveUiDisplayMillis(getCurrentTimestamp());
+  }
+
   /**
    * Set latency_save_request_millis as long as mEventInternal presents.
    */
@@ -242,6 +259,11 @@
     });
   }
 
+  /** Set latency_save_request_millis as long as mEventInternal presents. */
+  public void maybeSetLatencySaveRequestMillis() {
+    maybeSetLatencySaveRequestMillis(getCurrentTimestamp());
+  }
+
   /**
    * Set latency_save_finish_millis as long as mEventInternal presents.
    */
@@ -251,6 +273,11 @@
     });
   }
 
+  /** Set latency_save_finish_millis as long as mEventInternal presents. */
+  public void maybeSetLatencySaveFinishMillis() {
+    maybeSetLatencySaveFinishMillis(getCurrentTimestamp());
+  }
+
   /**
    * Set is_framework_created_save_info as long as mEventInternal presents.
    */
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index cd1ef88..ba6b067 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -1531,7 +1531,7 @@
         mFillResponseEventLogger = FillResponseEventLogger.forSessionId(sessionId);
         mSessionCommittedEventLogger = SessionCommittedEventLogger.forSessionId(sessionId);
         mSessionCommittedEventLogger.maybeSetComponentPackageUid(uid);
-        mSaveEventLogger = SaveEventLogger.forSessionId(sessionId);
+        mSaveEventLogger = SaveEventLogger.forSessionId(sessionId, mLatencyBaseTime);
         mIsPrimaryCredential = isPrimaryCredential;
         mIgnoreViewStateResetToEmpty = AutofillFeatureFlags.shouldIgnoreViewStateResetToEmpty();
 
@@ -3931,13 +3931,10 @@
                     return new SaveResult(/* logSaveShown= */ false, /* removeSession= */ true,
                             Event.NO_SAVE_UI_REASON_NONE);
                 }
-                final long saveUiDisplayStartTimestamp = SystemClock.elapsedRealtime();
                 getUiForShowing().showSaveUi(serviceLabel, serviceIcon,
                         mService.getServicePackageName(), saveInfo, this,
                         mComponentName, this, mContext,  mPendingSaveUi, isUpdate, mCompatMode,
                         response.getShowSaveDialogIcon(), mSaveEventLogger);
-                mSaveEventLogger.maybeSetLatencySaveUiDisplayMillis(
-                    SystemClock.elapsedRealtime()- saveUiDisplayStartTimestamp);
                 if (client != null) {
                     try {
                         client.setSaveUiState(id, true);
diff --git a/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java b/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java
index 602855d..3b9c54f 100644
--- a/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java
+++ b/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java
@@ -413,6 +413,8 @@
                     callback.startIntentSender(intentSender, intent);
                 }
             }, mUiModeMgr.isNightMode(), isUpdate, compatMode, showServiceIcon);
+
+            mSaveEventLogger.maybeSetLatencySaveUiDisplayMillis();
         });
     }
 
diff --git a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
index 64bca33..04edb57 100644
--- a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
+++ b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
@@ -27,6 +27,7 @@
 import static com.android.internal.util.FrameworkStatsLog.SENSITIVE_CONTENT_MEDIA_PROJECTION_SESSION__STATE__START;
 import static com.android.internal.util.FrameworkStatsLog.SENSITIVE_CONTENT_MEDIA_PROJECTION_SESSION__STATE__STOP;
 import static com.android.internal.util.FrameworkStatsLog.SENSITIVE_NOTIFICATION_APP_PROTECTION_SESSION;
+import static com.android.server.wm.WindowManagerInternal.OnWindowRemovedListener;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -210,6 +211,12 @@
                 }
             };
 
+    private final OnWindowRemovedListener mOnWindowRemovedListener = token -> {
+        synchronized (mSensitiveContentProtectionLock) {
+            mPackagesShowingSensitiveContent.removeIf(pkgInfo -> pkgInfo.getWindowToken() == token);
+        }
+    };
+
     public SensitiveContentProtectionManagerService(@NonNull Context context) {
         super(context);
         if (sensitiveNotificationAppProtection()) {
@@ -265,6 +272,10 @@
                 // Intra-process call, should never happen.
             }
         }
+
+        if (sensitiveContentAppProtection()) {
+            mWindowManager.registerOnWindowRemovedListener(mOnWindowRemovedListener);
+        }
     }
 
     /** Cleanup any callbacks and listeners */
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 8ec0e3d..1b3b198 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -20283,7 +20283,7 @@
         final int userId = UserHandle.getCallingUserId();
         final long callingId = Binder.clearCallingIdentity();
         try {
-            if (uid == -1) {
+            if (uid == INVALID_UID) {
                 uid = mPackageManagerInt.getPackageUid(packageName, 0, userId);
             }
             mAppRestrictionController.noteAppRestrictionEnabled(packageName, uid, restrictionType,
diff --git a/services/core/java/com/android/server/am/AppRestrictionController.java b/services/core/java/com/android/server/am/AppRestrictionController.java
index c5cad14..f5f1928 100644
--- a/services/core/java/com/android/server/am/AppRestrictionController.java
+++ b/services/core/java/com/android/server/am/AppRestrictionController.java
@@ -2387,8 +2387,8 @@
 
         // Limit the length of the free-form subReason string
         if (subReason != null && subReason.length() > RESTRICTION_SUBREASON_MAX_LENGTH) {
+            Slog.e(TAG, "subReason is too long, truncating " + subReason);
             subReason = subReason.substring(0, RESTRICTION_SUBREASON_MAX_LENGTH);
-            Slog.e(TAG, "Subreason is too long, truncating: " + subReason);
         }
 
         // Log the restriction reason
diff --git a/services/core/java/com/android/server/apphibernation/AppHibernationService.java b/services/core/java/com/android/server/apphibernation/AppHibernationService.java
index ce41079..e066c23 100644
--- a/services/core/java/com/android/server/apphibernation/AppHibernationService.java
+++ b/services/core/java/com/android/server/apphibernation/AppHibernationService.java
@@ -572,12 +572,8 @@
                         packageName, uid, ActivityManager.RESTRICTION_LEVEL_FORCE_STOPPED,
                         true, ActivityManager.RESTRICTION_REASON_DORMANT, null,
                         /* TODO: fetch actual timeout - 90 days */ 90 * 24 * 60 * 60_000L);
-            } else {
-                mIActivityManager.noteAppRestrictionEnabled(
-                        packageName, uid, ActivityManager.RESTRICTION_LEVEL_FORCE_STOPPED,
-                        false, ActivityManager.RESTRICTION_REASON_USAGE, null,
-                        0L);
             }
+            // No need to log the unhibernate case as an unstop is logged already in ActivityMS
         } catch (RemoteException e) {
             Slog.e(TAG, "Couldn't set restriction state change");
         }
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java b/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
index 3e23f97..b709174 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
@@ -21,6 +21,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.UserIdInt;
 import android.app.ActivityOptions;
 import android.app.PendingIntent;
 import android.content.ComponentName;
@@ -63,6 +64,7 @@
     /** Time in milliseconds that the IME service has to bind before it is reconnected. */
     static final long TIME_TO_RECONNECT = 3 * 1000;
 
+    @UserIdInt final int mUserId;
     @NonNull private final InputMethodManagerService mService;
     @NonNull private final Context mContext;
     @NonNull private final PackageManagerInternal mPackageManagerInternal;
@@ -107,12 +109,15 @@
                     | Context.BIND_INCLUDE_CAPABILITIES
                     | Context.BIND_SHOWING_UI;
 
-    InputMethodBindingController(@NonNull InputMethodManagerService service) {
-        this(service, IME_CONNECTION_BIND_FLAGS, null /* latchForTesting */);
+    InputMethodBindingController(@UserIdInt int userId,
+            @NonNull InputMethodManagerService service) {
+        this(userId, service, IME_CONNECTION_BIND_FLAGS, null /* latchForTesting */);
     }
 
-    InputMethodBindingController(@NonNull InputMethodManagerService service,
-            int imeConnectionBindFlags, CountDownLatch latchForTesting) {
+    InputMethodBindingController(@UserIdInt int userId,
+            @NonNull InputMethodManagerService service, int imeConnectionBindFlags,
+            CountDownLatch latchForTesting) {
+        mUserId = userId;
         mService = service;
         mContext = mService.mContext;
         mPackageManagerInternal = mService.mPackageManagerInternal;
@@ -301,7 +306,8 @@
                     }
                     if (DEBUG) Slog.v(TAG, "Initiating attach with token: " + mCurToken);
                     final InputMethodInfo info =
-                            mService.queryInputMethodForCurrentUserLocked(mSelectedMethodId);
+                            InputMethodSettingsRepository.get(mUserId).getMethodMap().get(
+                                    mSelectedMethodId);
                     boolean supportsStylusHwChanged =
                             mSupportsStylusHw != info.supportsStylusHandwriting();
                     mSupportsStylusHw = info.supportsStylusHandwriting();
@@ -339,7 +345,7 @@
         private void updateCurrentMethodUid() {
             final String curMethodPackage = mCurIntent.getComponent().getPackageName();
             final int curMethodUid = mPackageManagerInternal.getPackageUid(
-                    curMethodPackage, 0 /* flags */, mService.getCurrentImeUserIdLocked());
+                    curMethodPackage, 0 /* flags */, mUserId);
             if (curMethodUid < 0) {
                 Slog.e(TAG, "Failed to get UID for package=" + curMethodPackage);
                 mCurMethodUid = Process.INVALID_UID;
@@ -425,7 +431,8 @@
             return InputBindResult.NO_IME;
         }
 
-        InputMethodInfo info = mService.queryInputMethodForCurrentUserLocked(mSelectedMethodId);
+        InputMethodInfo info = InputMethodSettingsRepository.get(mUserId).getMethodMap().get(
+                mSelectedMethodId);
         if (info == null) {
             throw new IllegalArgumentException("Unknown id: " + mSelectedMethodId);
         }
@@ -497,8 +504,7 @@
             Slog.e(TAG, "--- bind failed: service = " + mCurIntent + ", conn = " + conn);
             return false;
         }
-        return mContext.bindServiceAsUser(mCurIntent, conn, flags,
-                new UserHandle(mService.getCurrentImeUserIdLocked()));
+        return mContext.bindServiceAsUser(mCurIntent, conn, flags, new UserHandle(mUserId));
     }
 
     @GuardedBy("ImfLock.class")
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 8985022e..14d04119 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -205,6 +205,7 @@
 import java.util.concurrent.TimeUnit;
 import java.util.function.Consumer;
 import java.util.function.IntConsumer;
+import java.util.function.IntFunction;
 
 /**
  * This class provides a system service that manages input methods.
@@ -306,8 +307,6 @@
     @MultiUserUnawareField
     private final InputMethodMenuController mMenuController;
     @MultiUserUnawareField
-    @NonNull private final InputMethodBindingController mBindingController;
-    @MultiUserUnawareField
     @NonNull private final AutofillSuggestionsController mAutofillController;
 
     @GuardedBy("ImfLock.class")
@@ -478,7 +477,8 @@
     @GuardedBy("ImfLock.class")
     @Nullable
     String getSelectedMethodIdLocked() {
-        return mBindingController.getSelectedMethodId();
+        final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+        return userData.mBindingController.getSelectedMethodId();
     }
 
     /**
@@ -487,7 +487,8 @@
      */
     @GuardedBy("ImfLock.class")
     private int getSequenceNumberLocked() {
-        return mBindingController.getSequenceNumber();
+        final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+        return userData.mBindingController.getSequenceNumber();
     }
 
     /**
@@ -496,7 +497,8 @@
      */
     @GuardedBy("ImfLock.class")
     private void advanceSequenceNumberLocked() {
-        mBindingController.advanceSequenceNumber();
+        final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+        userData.mBindingController.advanceSequenceNumber();
     }
 
     @GuardedBy("ImfLock.class")
@@ -556,7 +558,8 @@
     @GuardedBy("ImfLock.class")
     @Nullable
     private String getCurIdLocked() {
-        return mBindingController.getCurId();
+        final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+        return userData.mBindingController.getCurId();
     }
 
     /**
@@ -580,7 +583,8 @@
      */
     @GuardedBy("ImfLock.class")
     private boolean hasConnectionLocked() {
-        return mBindingController.hasMainConnection();
+        final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+        return userData.mBindingController.hasMainConnection();
     }
 
     /**
@@ -603,7 +607,8 @@
     @GuardedBy("ImfLock.class")
     @Nullable
     private Intent getCurIntentLocked() {
-        return mBindingController.getCurIntent();
+        final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+        return userData.mBindingController.getCurIntent();
     }
 
     /**
@@ -613,7 +618,8 @@
     @GuardedBy("ImfLock.class")
     @Nullable
     IBinder getCurTokenLocked() {
-        return mBindingController.getCurToken();
+        final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+        return userData.mBindingController.getCurToken();
     }
 
     /**
@@ -654,7 +660,8 @@
     @GuardedBy("ImfLock.class")
     @Nullable
     IInputMethodInvoker getCurMethodLocked() {
-        return mBindingController.getCurMethod();
+        final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+        return userData.mBindingController.getCurMethod();
     }
 
     /**
@@ -662,7 +669,8 @@
      */
     @GuardedBy("ImfLock.class")
     private int getCurMethodUidLocked() {
-        return mBindingController.getCurMethodUid();
+        final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+        return userData.mBindingController.getCurMethodUid();
     }
 
     /**
@@ -671,7 +679,8 @@
      */
     @GuardedBy("ImfLock.class")
     private long getLastBindTimeLocked() {
-        return mBindingController.getLastBindTime();
+        final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+        return userData.mBindingController.getLastBindTime();
     }
 
     /**
@@ -1353,7 +1362,7 @@
     InputMethodManagerService(
             Context context,
             @Nullable ServiceThread serviceThreadForTesting,
-            @Nullable InputMethodBindingController bindingControllerForTesting) {
+            @Nullable IntFunction<InputMethodBindingController> bindingControllerForTesting) {
         synchronized (ImfLock.class) {
             mContext = context;
             mRes = context.getResources();
@@ -1392,7 +1401,12 @@
             AdditionalSubtypeMapRepository.initialize(mHandler, mContext);
 
             mCurrentUserId = mActivityManagerInternal.getCurrentUserId();
-            mUserDataRepository = new UserDataRepository(mHandler, mUserManagerInternal);
+            @SuppressWarnings("GuardedBy") final IntFunction<InputMethodBindingController>
+                    bindingControllerFactory = userId -> new InputMethodBindingController(userId,
+                            InputMethodManagerService.this);
+            mUserDataRepository = new UserDataRepository(mHandler, mUserManagerInternal,
+                    bindingControllerForTesting != null ? bindingControllerForTesting
+                            : bindingControllerFactory);
             for (int id : mUserManagerInternal.getUserIds()) {
                 mUserDataRepository.getOrCreate(id);
             }
@@ -1406,12 +1420,7 @@
                     new HardwareKeyboardShortcutController(settings.getMethodMap(),
                             settings.getUserId());
             mMenuController = new InputMethodMenuController(this);
-            mBindingController =
-                    bindingControllerForTesting != null
-                            ? bindingControllerForTesting
-                            : new InputMethodBindingController(this);
             mAutofillController = new AutofillSuggestionsController(this);
-
             mVisibilityStateComputer = new ImeVisibilityStateComputer(this);
             mVisibilityApplier = new DefaultImeVisibilityApplier(this);
 
@@ -1544,9 +1553,9 @@
 
         // Note that in b/197848765 we want to see if we can keep the binding alive for better
         // profile switching.
-        mBindingController.unbindCurrentMethod();
-        // TODO(b/325515685): No need to do this once BindingController becomes per-user.
-        mBindingController.setSelectedMethodId(null);
+        final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+        userData.mBindingController.unbindCurrentMethod();
+
         unbindCurrentClientLocked(UnbindReason.SWITCH_USER);
 
         // Hereafter we start initializing things for "newUserId".
@@ -1763,9 +1772,10 @@
 
             // Check if selected IME of current user supports handwriting.
             if (userId == mCurrentUserId) {
-                return mBindingController.supportsStylusHandwriting()
+                final var userData = mUserDataRepository.getOrCreate(userId);
+                return userData.mBindingController.supportsStylusHandwriting()
                         && (!connectionless
-                                || mBindingController.supportsConnectionlessStylusHandwriting());
+                        || userData.mBindingController.supportsConnectionlessStylusHandwriting());
             }
             final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
             final InputMethodInfo imi = settings.getMethodMap().get(
@@ -2095,7 +2105,8 @@
                 curInputMethodInfo != null && curInputMethodInfo.suppressesSpellChecker();
         final SparseArray<IAccessibilityInputMethodSession> accessibilityInputMethodSessions =
                 createAccessibilityInputMethodSessions(mCurClient.mAccessibilitySessions);
-        if (mBindingController.supportsStylusHandwriting() && hasSupportedStylusLocked()) {
+        final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+        if (userData.mBindingController.supportsStylusHandwriting() && hasSupportedStylusLocked()) {
             mHwController.setInkWindowInitializer(new InkWindowInitializer());
         }
         return new InputBindResult(InputBindResult.ResultCode.SUCCESS_WITH_IME_SESSION,
@@ -2216,6 +2227,8 @@
         if (connectionIsActive != connectionWasActive) {
             mInputManagerInternal.notifyInputMethodConnectionActive(connectionIsActive);
         }
+        final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+
 
         // If configured, we want to avoid starting up the IME if it is not supposed to be showing
         if (shouldPreventImeStartupLocked(selectedMethodId, startInputFlags,
@@ -2224,7 +2237,7 @@
                 Slog.d(TAG, "Avoiding IME startup and unbinding current input method.");
             }
             invalidateAutofillSessionLocked();
-            mBindingController.unbindCurrentMethod();
+            userData.mBindingController.unbindCurrentMethod();
             return InputBindResult.NO_EDITOR;
         }
 
@@ -2256,9 +2269,8 @@
             }
         }
 
-        mBindingController.unbindCurrentMethod();
-
-        return mBindingController.bindCurrentMethod();
+        userData.mBindingController.unbindCurrentMethod();
+        return userData.mBindingController.bindCurrentMethod();
     }
 
     /**
@@ -2518,11 +2530,13 @@
 
     @GuardedBy("ImfLock.class")
     void resetCurrentMethodAndClientLocked(@UnbindReason int unbindClientReason) {
-        mBindingController.setSelectedMethodId(null);
+        final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+        userData.mBindingController.setSelectedMethodId(null);
+
         // Callback before clean-up binding states.
         // TODO(b/338461930): Check if this is still necessary or not.
         onUnbindCurrentMethodByReset();
-        mBindingController.unbindCurrentMethod();
+        userData.mBindingController.unbindCurrentMethod();
         unbindCurrentClientLocked(unbindClientReason);
     }
 
@@ -3099,7 +3113,8 @@
             // mCurMethodId should be updated after setSelectedInputMethodAndSubtypeLocked()
             // because mCurMethodId is stored as a history in
             // setSelectedInputMethodAndSubtypeLocked().
-            mBindingController.setSelectedMethodId(id);
+            final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+            userData.mBindingController.setSelectedMethodId(id);
 
             if (mActivityManagerInternal.isSystemReady()) {
                 Intent intent = new Intent(Intent.ACTION_INPUT_METHOD_CHANGED);
@@ -3154,7 +3169,8 @@
             @Nullable String delegatorPackageName,
             @NonNull IConnectionlessHandwritingCallback callback) {
         synchronized (ImfLock.class) {
-            if (!mBindingController.supportsConnectionlessStylusHandwriting()) {
+            final var userData = mUserDataRepository.getOrCreate(userId);
+            if (!userData.mBindingController.supportsConnectionlessStylusHandwriting()) {
                 Slog.w(TAG, "Connectionless stylus handwriting mode unsupported by IME.");
                 try {
                     callback.onError(CONNECTIONLESS_HANDWRITING_ERROR_UNSUPPORTED);
@@ -3237,7 +3253,8 @@
                 }
                 final long ident = Binder.clearCallingIdentity();
                 try {
-                    if (!mBindingController.supportsStylusHandwriting()) {
+                    final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+                    if (!userData.mBindingController.supportsStylusHandwriting()) {
                         Slog.w(TAG,
                                 "Stylus HW unsupported by IME. Ignoring startStylusHandwriting()");
                         return false;
@@ -3420,7 +3437,8 @@
         mVisibilityStateComputer.requestImeVisibility(windowToken, true);
 
         // Ensure binding the connection when IME is going to show.
-        mBindingController.setCurrentMethodVisible();
+        final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+        userData.mBindingController.setCurrentMethodVisible();
         final IInputMethodInvoker curMethod = getCurMethodLocked();
         ImeTracker.forLogging().onCancelled(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME);
         final boolean readyToDispatchToIme;
@@ -3528,7 +3546,8 @@
         } else {
             ImeTracker.forLogging().onCancelled(statsToken, ImeTracker.PHASE_SERVER_SHOULD_HIDE);
         }
-        mBindingController.setCurrentMethodNotVisible();
+        final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+        userData.mBindingController.setCurrentMethodNotVisible();
         mVisibilityStateComputer.clearImeShowFlags();
         // Cancel existing statsToken for show IME as we got a hide request.
         ImeTracker.forLogging().onCancelled(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME);
@@ -3810,7 +3829,8 @@
                 // Note that we can trust client's display ID as long as it matches
                 // to the display ID obtained from the window.
                 if (cs.mSelfReportedDisplayId != mCurTokenDisplayId) {
-                    mBindingController.unbindCurrentMethod();
+                    final var userData = mUserDataRepository.getOrCreate(userId);
+                    userData.mBindingController.unbindCurrentMethod();
                 }
             }
         }
@@ -4271,8 +4291,9 @@
         mStylusIds.add(deviceId);
         // a new Stylus is detected. If IME supports handwriting, and we don't have
         // handwriting initialized, lets do it now.
+        final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
         if (!mHwController.getCurrentRequestId().isPresent()
-                && mBindingController.supportsStylusHandwriting()) {
+                && userData.mBindingController.supportsStylusHandwriting()) {
             scheduleResetStylusHandwriting();
         }
     }
@@ -4841,7 +4862,8 @@
 
             case MSG_RESET_HANDWRITING: {
                 synchronized (ImfLock.class) {
-                    if (mBindingController.supportsStylusHandwriting()
+                    final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+                    if (userData.mBindingController.supportsStylusHandwriting()
                             && getCurMethodLocked() != null && hasSupportedStylusLocked()) {
                         Slog.d(TAG, "Initializing Handwriting Spy");
                         mHwController.initializeHandwritingSpy(mCurTokenDisplayId);
@@ -4866,11 +4888,12 @@
                     if (curMethod == null || mImeBindingState.mFocusedWindow == null) {
                         return true;
                     }
+                    final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
                     final HandwritingModeController.HandwritingSession session =
                             mHwController.startHandwritingSession(
                                     msg.arg1 /*requestId*/,
                                     msg.arg2 /*pid*/,
-                                    mBindingController.getCurMethodUid(),
+                                    userData.mBindingController.getCurMethodUid(),
                                     mImeBindingState.mFocusedWindow);
                     if (session == null) {
                         Slog.e(TAG,
@@ -5164,7 +5187,8 @@
 
     @GuardedBy("ImfLock.class")
     void sendOnNavButtonFlagsChangedLocked() {
-        final IInputMethodInvoker curMethod = mBindingController.getCurMethod();
+        final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+        final IInputMethodInvoker curMethod = userData.mBindingController.getCurMethod();
         if (curMethod == null) {
             // No need to send the data if the IME is not yet bound.
             return;
@@ -5917,9 +5941,10 @@
             p.println("  mCurClient=" + client + " mCurSeq=" + getSequenceNumberLocked());
             p.println("  mFocusedWindowPerceptible=" + mFocusedWindowPerceptible);
             mImeBindingState.dump("  ", p);
+            final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
             p.println("  mCurId=" + getCurIdLocked() + " mHaveConnection=" + hasConnectionLocked()
                     + " mBoundToMethod=" + mBoundToMethod + " mVisibleBound="
-                    + mBindingController.isVisibleBound());
+                    + userData.mBindingController.isVisibleBound());
             p.println("  mCurToken=" + getCurTokenLocked());
             p.println("  mCurTokenDisplayId=" + mCurTokenDisplayId);
             p.println("  mCurHostInputToken=" + mCurHostInputToken);
@@ -6413,7 +6438,8 @@
                     if (userId == mCurrentUserId) {
                         hideCurrentInputLocked(mImeBindingState.mFocusedWindow, 0 /* flags */,
                                 SoftInputShowHideReason.HIDE_RESET_SHELL_COMMAND);
-                        mBindingController.unbindCurrentMethod();
+                        final var userData = mUserDataRepository.getOrCreate(userId);
+                        userData.mBindingController.unbindCurrentMethod();
 
                         // Enable default IMEs, disable others
                         var toDisable = settings.getEnabledInputMethodList();
diff --git a/services/core/java/com/android/server/inputmethod/UserDataRepository.java b/services/core/java/com/android/server/inputmethod/UserDataRepository.java
index 7f00229..825cfcb 100644
--- a/services/core/java/com/android/server/inputmethod/UserDataRepository.java
+++ b/services/core/java/com/android/server/inputmethod/UserDataRepository.java
@@ -15,6 +15,7 @@
  */
 
 package com.android.server.inputmethod;
+
 import android.annotation.NonNull;
 import android.annotation.UserIdInt;
 import android.content.pm.UserInfo;
@@ -25,18 +26,21 @@
 import com.android.server.pm.UserManagerInternal;
 
 import java.util.function.Consumer;
+import java.util.function.IntFunction;
 
 final class UserDataRepository {
 
     @GuardedBy("ImfLock.class")
     private final SparseArray<UserData> mUserData = new SparseArray<>();
 
+    private final IntFunction<InputMethodBindingController> mBindingControllerFactory;
+
     @GuardedBy("ImfLock.class")
     @NonNull
     UserData getOrCreate(@UserIdInt int userId) {
         UserData userData = mUserData.get(userId);
         if (userData == null) {
-            userData = new UserData(userId);
+            userData = new UserData(userId, mBindingControllerFactory.apply(userId));
             mUserData.put(userId, userData);
         }
         return userData;
@@ -49,7 +53,9 @@
         }
     }
 
-    UserDataRepository(@NonNull Handler handler, @NonNull UserManagerInternal userManagerInternal) {
+    UserDataRepository(@NonNull Handler handler, @NonNull UserManagerInternal userManagerInternal,
+            @NonNull IntFunction<InputMethodBindingController> bindingControllerFactory) {
+        mBindingControllerFactory = bindingControllerFactory;
         userManagerInternal.addUserLifecycleListener(
                 new UserManagerInternal.UserLifecycleListener() {
                     @Override
@@ -79,11 +85,16 @@
         @UserIdInt
         final int mUserId;
 
-       /**
+        @NonNull
+        final InputMethodBindingController mBindingController;
+
+        /**
          * Intended to be instantiated only from this file.
          */
-        private UserData(@UserIdInt int userId) {
+        private UserData(@UserIdInt int userId,
+                @NonNull InputMethodBindingController bindingController) {
             mUserId = userId;
+            mBindingController = bindingController;
         }
     }
 }
diff --git a/services/core/java/com/android/server/notification/GroupHelper.java b/services/core/java/com/android/server/notification/GroupHelper.java
index babb6c2..13cc99c 100644
--- a/services/core/java/com/android/server/notification/GroupHelper.java
+++ b/services/core/java/com/android/server/notification/GroupHelper.java
@@ -108,16 +108,25 @@
         return (flags & mask) != 0;
     }
 
-    public void onNotificationPosted(StatusBarNotification sbn, boolean autogroupSummaryExists) {
+    /**
+     * Called when a notification is newly posted. Checks whether that notification, and all other
+     * active notifications should be grouped or ungrouped atuomatically, and returns whether.
+     * @param sbn The posted notification.
+     * @param autogroupSummaryExists Whether a summary for this notification already exists.
+     * @return Whether the provided notification should be autogrouped synchronously.
+     */
+    public boolean onNotificationPosted(StatusBarNotification sbn, boolean autogroupSummaryExists) {
+        boolean sbnToBeAutogrouped = false;
         try {
             if (!sbn.isAppGroup()) {
-                maybeGroup(sbn, autogroupSummaryExists);
+                sbnToBeAutogrouped = maybeGroup(sbn, autogroupSummaryExists);
             } else {
                 maybeUngroup(sbn, false, sbn.getUserId());
             }
         } catch (Exception e) {
             Slog.e(TAG, "Failure processing new notification", e);
         }
+        return sbnToBeAutogrouped;
     }
 
     public void onNotificationRemoved(StatusBarNotification sbn) {
@@ -137,20 +146,22 @@
      *
      * And stores the list of upgrouped notifications & their flags
      */
-    private void maybeGroup(StatusBarNotification sbn, boolean autogroupSummaryExists) {
+    private boolean maybeGroup(StatusBarNotification sbn, boolean autogroupSummaryExists) {
         int flags = 0;
         List<String> notificationsToGroup = new ArrayList<>();
         List<NotificationAttributes> childrenAttr = new ArrayList<>();
+        // Indicates whether the provided sbn should be autogrouped by the caller.
+        boolean sbnToBeAutogrouped = false;
         synchronized (mUngroupedNotifications) {
-            String key = generatePackageKey(sbn.getUserId(), sbn.getPackageName());
+            String packageKey = generatePackageKey(sbn.getUserId(), sbn.getPackageName());
             final ArrayMap<String, NotificationAttributes> children =
-                    mUngroupedNotifications.getOrDefault(key, new ArrayMap<>());
+                    mUngroupedNotifications.getOrDefault(packageKey, new ArrayMap<>());
 
             NotificationAttributes attr = new NotificationAttributes(sbn.getNotification().flags,
                     sbn.getNotification().getSmallIcon(), sbn.getNotification().color,
                     sbn.getNotification().visibility);
             children.put(sbn.getKey(), attr);
-            mUngroupedNotifications.put(key, children);
+            mUngroupedNotifications.put(packageKey, children);
 
             if (children.size() >= mAutoGroupAtCount || autogroupSummaryExists) {
                 flags = getAutogroupSummaryFlags(children);
@@ -187,10 +198,20 @@
                 mCallback.addAutoGroupSummary(sbn.getUserId(), sbn.getPackageName(), sbn.getKey(),
                         attr);
             }
-            for (String key : notificationsToGroup) {
-                mCallback.addAutoGroup(key);
+            for (String keyToGroup : notificationsToGroup) {
+                if (android.app.Flags.checkAutogroupBeforePost()) {
+                    if (keyToGroup.equals(sbn.getKey())) {
+                        // Autogrouping for the provided notification is to be done synchronously.
+                        sbnToBeAutogrouped = true;
+                    } else {
+                        mCallback.addAutoGroup(keyToGroup, /*requestSort=*/true);
+                    }
+                } else {
+                    mCallback.addAutoGroup(keyToGroup, /*requestSort=*/true);
+                }
             }
         }
+        return sbnToBeAutogrouped;
     }
 
     /**
@@ -406,7 +427,7 @@
     }
 
     protected interface Callback {
-        void addAutoGroup(String key);
+        void addAutoGroup(String key, boolean requestSort);
         void removeAutoGroup(String key);
 
         void addAutoGroupSummary(int userId, String pkg, String triggeringKey,
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index ca6ae63..3dd2f1e 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -2793,10 +2793,10 @@
         return new GroupHelper(getContext(), getContext().getPackageManager(),
                 mAutoGroupAtCount, new GroupHelper.Callback() {
             @Override
-            public void addAutoGroup(String key) {
-                synchronized (mNotificationLock) {
-                    addAutogroupKeyLocked(key);
-                }
+            public void addAutoGroup(String key, boolean requestSort) {
+                        synchronized (mNotificationLock) {
+                            addAutogroupKeyLocked(key, requestSort);
+                        }
             }
 
             @Override
@@ -6538,7 +6538,7 @@
     }
 
     @GuardedBy("mNotificationLock")
-    void addAutogroupKeyLocked(String key) {
+    void addAutogroupKeyLocked(String key, boolean requestSort) {
         NotificationRecord r = mNotificationsByKey.get(key);
         if (r == null) {
             return;
@@ -6546,7 +6546,9 @@
         if (r.getSbn().getOverrideGroupKey() == null) {
             addAutoGroupAdjustment(r, GroupHelper.AUTOGROUP_KEY);
             EventLogTags.writeNotificationAutogrouped(key);
-            mRankingHandler.requestSort();
+            if (!android.app.Flags.checkAutogroupBeforePost() || requestSort) {
+                mRankingHandler.requestSort();
+            }
         }
     }
 
@@ -8609,6 +8611,29 @@
                         notification.flags |= FLAG_NO_CLEAR;
                     }
 
+                    // Posts the notification if it has a small icon, and potentially autogroup
+                    // the new notification.
+                    if (android.app.Flags.checkAutogroupBeforePost()) {
+                        if (notification.getSmallIcon() != null && !isCritical(r)) {
+                            StatusBarNotification oldSbn = (old != null) ? old.getSbn() : null;
+                            if (oldSbn == null || !Objects.equals(oldSbn.getGroup(), n.getGroup())
+                                    || oldSbn.getNotification().flags
+                                    != n.getNotification().flags) {
+                                synchronized (mNotificationLock) {
+                                    boolean willBeAutogrouped = mGroupHelper.onNotificationPosted(n,
+                                            hasAutoGroupSummaryLocked(n));
+                                    if (willBeAutogrouped) {
+                                        // The newly posted notification will be autogrouped, but
+                                        // was not autogrouped onPost, to avoid an unnecessary sort.
+                                        // We add the autogroup key to the notification without a
+                                        // sort here, and it'll be sorted below with extractSignals.
+                                        addAutogroupKeyLocked(key, /*requestSort=*/false);
+                                    }
+                                }
+                            }
+                        }
+                    }
+
                     mRankingHelper.extractSignals(r);
                     mRankingHelper.sort(mNotificationList);
                     final int position = mRankingHelper.indexOf(mNotificationList, r);
@@ -8629,17 +8654,20 @@
                         notifyListenersPostedAndLogLocked(r, old, mTracker, maybeReport);
                         posted = true;
 
-                        StatusBarNotification oldSbn = (old != null) ? old.getSbn() : null;
-                        if (oldSbn == null
-                                || !Objects.equals(oldSbn.getGroup(), n.getGroup())
-                                || oldSbn.getNotification().flags != n.getNotification().flags) {
-                            if (!isCritical(r)) {
-                                mHandler.post(() -> {
-                                    synchronized (mNotificationLock) {
-                                        mGroupHelper.onNotificationPosted(
-                                                n, hasAutoGroupSummaryLocked(n));
-                                    }
-                                });
+                        if (!android.app.Flags.checkAutogroupBeforePost()) {
+                            StatusBarNotification oldSbn = (old != null) ? old.getSbn() : null;
+                            if (oldSbn == null
+                                    || !Objects.equals(oldSbn.getGroup(), n.getGroup())
+                                    || oldSbn.getNotification().flags
+                                        != n.getNotification().flags) {
+                                if (!isCritical(r)) {
+                                    mHandler.post(() -> {
+                                        synchronized (mNotificationLock) {
+                                            mGroupHelper.onNotificationPosted(
+                                                    n, hasAutoGroupSummaryLocked(n));
+                                        }
+                                    });
+                                }
                             }
                         }
                     } else {
diff --git a/services/core/java/com/android/server/om/OverlayManagerService.java b/services/core/java/com/android/server/om/OverlayManagerService.java
index 6c93fe7..56e4590 100644
--- a/services/core/java/com/android/server/om/OverlayManagerService.java
+++ b/services/core/java/com/android/server/om/OverlayManagerService.java
@@ -89,6 +89,7 @@
 import com.android.server.SystemConfig;
 import com.android.server.SystemService;
 import com.android.server.pm.KnownPackages;
+import com.android.server.pm.UserManagerInternal;
 import com.android.server.pm.UserManagerService;
 import com.android.server.pm.pkg.PackageState;
 
@@ -289,6 +290,9 @@
             getContext().registerReceiverAsUser(new UserReceiver(), UserHandle.ALL,
                     userFilter, null, null);
 
+            UserManagerInternal umi = LocalServices.getService(UserManagerInternal.class);
+            umi.addUserLifecycleListener(new UserLifecycleListener());
+
             restoreSettings();
 
             // Wipe all shell overlays on boot, to recover from a potentially broken device
@@ -339,6 +343,7 @@
         if (newUserId == mPrevStartedUserId) {
             return;
         }
+        Slog.i(TAG, "Updating overlays for starting user " + newUserId);
         try {
             traceBegin(TRACE_TAG_RRO, "OMS#onStartUser " + newUserId);
             // ensure overlays in the settings are up-to-date, and propagate
@@ -515,14 +520,46 @@
         }
     }
 
+    /**
+     * Indicates that the given user is of great importance so that when it is created, we quickly
+     * update its overlays by using a Listener mechanism rather than a Broadcast mechanism. This
+     * is especially important for {@link UserManager#isHeadlessSystemUserMode() HSUM}'s MainUser,
+     * which is created and switched-to immediately on first boot.
+     */
+    private static boolean isHighPriorityUserCreation(UserInfo user) {
+        // TODO: Consider extending this to all created users (guarded behind a flag in that case).
+        return user != null && user.isMain();
+    }
+
+    private final class UserLifecycleListener implements UserManagerInternal.UserLifecycleListener {
+        @Override
+        public void onUserCreated(UserInfo user, Object token) {
+            if (isHighPriorityUserCreation(user)) {
+                final int userId = user.id;
+                try {
+                    Slog.i(TAG, "Updating overlays for onUserCreated " + userId);
+                    traceBegin(TRACE_TAG_RRO, "OMS#onUserCreated " + userId);
+                    synchronized (mLock) {
+                        updatePackageManagerLocked(mImpl.updateOverlaysForUser(userId));
+                    }
+                } finally {
+                    traceEnd(TRACE_TAG_RRO);
+                }
+            }
+        }
+    }
+
     private final class UserReceiver extends BroadcastReceiver {
         @Override
         public void onReceive(@NonNull final Context context, @NonNull final Intent intent) {
             final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
             switch (intent.getAction()) {
                 case ACTION_USER_ADDED:
-                    if (userId != UserHandle.USER_NULL) {
+                    UserManagerInternal umi = LocalServices.getService(UserManagerInternal.class);
+                    UserInfo userInfo = umi.getUserInfo(userId);
+                    if (userId != UserHandle.USER_NULL && !isHighPriorityUserCreation(userInfo)) {
                         try {
+                            Slog.i(TAG, "Updating overlays for added user " + userId);
                             traceBegin(TRACE_TAG_RRO, "OMS ACTION_USER_ADDED");
                             synchronized (mLock) {
                                 updatePackageManagerLocked(mImpl.updateOverlaysForUser(userId));
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index f3e1dfb..5e95a4b 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -4381,7 +4381,8 @@
      */
     protected boolean dumpActivity(FileDescriptor fd, PrintWriter pw, String name, String[] args,
             int opti, boolean dumpAll, boolean dumpVisibleRootTasksOnly,
-            boolean dumpFocusedRootTaskOnly, int displayIdFilter, @UserIdInt int userId) {
+            boolean dumpFocusedRootTaskOnly, int displayIdFilter, @UserIdInt int userId,
+            long timeout) {
         ArrayList<ActivityRecord> activities;
 
         synchronized (mGlobalLock) {
@@ -4426,7 +4427,7 @@
                     }
                 }
             }
-            dumpActivity("  ", fd, pw, activities.get(i), newArgs, dumpAll);
+            dumpActivity("  ", fd, pw, activities.get(i), newArgs, dumpAll, timeout);
         }
         if (!printedAnything) {
             // Typically happpens when no task matches displayIdFilter
@@ -4440,7 +4441,7 @@
      * there is a thread associated with the activity.
      */
     private void dumpActivity(String prefix, FileDescriptor fd, PrintWriter pw,
-            ActivityRecord r, String[] args, boolean dumpAll) {
+            ActivityRecord r, String[] args, boolean dumpAll, long timeout) {
         String innerPrefix = prefix + "  ";
         IApplicationThread appThread = null;
         synchronized (mGlobalLock) {
@@ -4471,7 +4472,7 @@
             pw.flush();
             try (TransferPipe tp = new TransferPipe()) {
                 appThread.dumpActivity(tp.getWriteFd(), r.token, innerPrefix, args);
-                tp.go(fd);
+                tp.go(fd, timeout);
             } catch (IOException e) {
                 pw.println(innerPrefix + "Failure while dumping the activity: " + e);
             } catch (RemoteException e) {
@@ -6970,7 +6971,8 @@
                 boolean dumpFocusedRootTaskOnly, int displayIdFilter,
                 @UserIdInt int userId) {
             return ActivityTaskManagerService.this.dumpActivity(fd, pw, name, args, opti, dumpAll,
-                    dumpVisibleRootTasksOnly, dumpFocusedRootTaskOnly, displayIdFilter, userId);
+                    dumpVisibleRootTasksOnly, dumpFocusedRootTaskOnly, displayIdFilter, userId,
+                    /* timeout= */ 5000);
         }
 
         @Override
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 2f37e88..4147249 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -2738,6 +2738,10 @@
         if (!mVisibleBackgroundUserEnabled) {
             return true;
         }
+        if (isPrivate()) {
+            // UserManager doesn't track the user visibility for private displays.
+            return true;
+        }
         final int userId = UserHandle.getUserId(uid);
         return userId == UserHandle.USER_SYSTEM
                 || mWmService.mUmInternal.isUserVisible(userId, mDisplayId);
diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java
index eeedec3..19053f7 100644
--- a/services/core/java/com/android/server/wm/WindowManagerInternal.java
+++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java
@@ -306,6 +306,18 @@
     }
 
     /**
+     * An interface to be notified on window removal.
+     */
+    public interface OnWindowRemovedListener {
+        /**
+         * Called when a window is removed.
+         *
+         * @param token the client token
+         */
+        void onWindowRemoved(IBinder token);
+    }
+
+    /**
      * An interface to be notified when keyguard exit animation should start.
      */
     public interface KeyguardExitAnimationStartListener {
@@ -1076,6 +1088,20 @@
     public abstract void clearBlockedApps();
 
     /**
+     * Register a listener to receive a callback on window removal.
+     *
+     * @param listener the listener to be registered.
+     */
+    public abstract void registerOnWindowRemovedListener(OnWindowRemovedListener listener);
+
+    /**
+     * Removes the listener.
+     *
+     * @param listener the listener to be removed.
+     */
+    public abstract void unregisterOnWindowRemovedListener(OnWindowRemovedListener listener);
+
+    /**
      * Moves the current focus to the adjacent activity if it has the latest created window.
      */
     public abstract boolean moveFocusToAdjacentEmbeddedActivityIfNeeded();
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index feede01..d0a50d5 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -53,6 +53,7 @@
 import static android.view.ContentRecordingSession.RECORD_CONTENT_TASK;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.INVALID_DISPLAY;
+import static android.view.flags.Flags.sensitiveContentAppProtection;
 import static android.view.WindowManager.DISPLAY_IME_POLICY_FALLBACK_DISPLAY;
 import static android.view.WindowManager.DISPLAY_IME_POLICY_LOCAL;
 import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW;
@@ -147,6 +148,7 @@
 import static com.android.server.wm.WindowManagerDebugConfig.SHOW_VERBOSE_TRANSACTIONS;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
+import static com.android.server.wm.WindowManagerInternal.OnWindowRemovedListener;
 import static com.android.server.wm.WindowManagerServiceDumpProto.BACK_NAVIGATION;
 import static com.android.server.wm.WindowManagerServiceDumpProto.DISPLAY_FROZEN;
 import static com.android.server.wm.WindowManagerServiceDumpProto.FOCUSED_APP;
@@ -489,6 +491,9 @@
 
     private final RemoteCallbackList<IKeyguardLockedStateListener> mKeyguardLockedStateListeners =
             new RemoteCallbackList<>();
+
+    private final List<OnWindowRemovedListener> mOnWindowRemovedListeners = new ArrayList<>();
+
     private boolean mDispatchedKeyguardLockedState = false;
 
     // VR Vr2d Display Id.
@@ -534,6 +539,21 @@
         }
 
         @Override
+        public void dumpHigh(FileDescriptor fd, PrintWriter pw, String[] args,
+                boolean asProto) {
+            if (asProto) {
+                return;
+            }
+            mAtmService.dumpActivity(fd, pw, /* name= */ "all", /* args= */ new String[]{},
+                    /* opti= */ 0,
+                    /* dumpAll= */ true,
+                    /* dumpVisibleRootTasksOnly= */ true,
+                    /* dumpFocusedRootTaskOnly= */ false, INVALID_DISPLAY, UserHandle.USER_ALL,
+                    /* timeout= */ 1000
+            );
+        }
+
+        @Override
         public void dump(FileDescriptor fd, PrintWriter pw, String[] args, boolean asProto) {
             doDump(fd, pw, args, asProto);
         }
@@ -2073,7 +2093,11 @@
      */
     void postWindowRemoveCleanupLocked(WindowState win) {
         ProtoLog.v(WM_DEBUG_ADD_REMOVE, "postWindowRemoveCleanupLocked: %s", win);
-        mWindowMap.remove(win.mClient.asBinder());
+        final IBinder client = win.mClient.asBinder();
+        mWindowMap.remove(client);
+        if (sensitiveContentAppProtection()) {
+            notifyWindowRemovedListeners(client);
+        }
 
         final DisplayContent dc = win.getDisplayContent();
         dc.getDisplayRotation().markForSeamlessRotation(win, false /* seamlesslyRotated */);
@@ -5335,6 +5359,23 @@
         }
     }
 
+    private void notifyWindowRemovedListeners(IBinder client) {
+        OnWindowRemovedListener[] windowRemovedListeners;
+        synchronized (mGlobalLock) {
+            if (mOnWindowRemovedListeners.isEmpty()) {
+                return;
+            }
+            windowRemovedListeners = new OnWindowRemovedListener[mOnWindowRemovedListeners.size()];
+            mOnWindowRemovedListeners.toArray(windowRemovedListeners);
+        }
+        mH.post(() -> {
+            int size = windowRemovedListeners.length;
+            for (int i = 0; i < size; i++) {
+                windowRemovedListeners[i].onWindowRemoved(client);
+            }
+        });
+    }
+
     private void notifyWindowsChanged() {
         WindowChangeListener[] windowChangeListeners;
         synchronized (mGlobalLock) {
@@ -8868,6 +8909,20 @@
         }
 
         @Override
+        public void registerOnWindowRemovedListener(OnWindowRemovedListener listener) {
+            synchronized (mGlobalLock) {
+                mOnWindowRemovedListeners.add(listener);
+            }
+        }
+
+        @Override
+        public void unregisterOnWindowRemovedListener(OnWindowRemovedListener listener) {
+            synchronized (mGlobalLock) {
+                mOnWindowRemovedListeners.remove(listener);
+            }
+        }
+
+        @Override
         public boolean moveFocusToAdjacentEmbeddedActivityIfNeeded() {
             synchronized (mGlobalLock) {
                 final WindowState focusedWindow = getFocusedWindow();
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 8caf5ae..8755a80 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -1618,7 +1618,8 @@
             wm = WindowManagerService.main(context, inputManager, !mFirstBoot,
                     new PhoneWindowManager(), mActivityManagerService.mActivityTaskManager);
             ServiceManager.addService(Context.WINDOW_SERVICE, wm, /* allowIsolated= */ false,
-                    DUMP_FLAG_PRIORITY_CRITICAL | DUMP_FLAG_PROTO);
+                    DUMP_FLAG_PRIORITY_CRITICAL | DUMP_FLAG_PRIORITY_HIGH
+                            | DUMP_FLAG_PROTO);
             ServiceManager.addService(Context.INPUT_SERVICE, inputManager,
                     /* allowIsolated= */ false, DUMP_FLAG_PRIORITY_CRITICAL);
             t.traceEnd();
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodBindingControllerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodBindingControllerTest.java
index 1f0a375..70903cb 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodBindingControllerTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodBindingControllerTest.java
@@ -77,9 +77,13 @@
         mCountDownLatch = new CountDownLatch(1);
         // Remove flag Context.BIND_SCHEDULE_LIKE_TOP_APP because in tests we are not calling
         // from system.
-        mBindingController =
-                new InputMethodBindingController(
-                        mInputMethodManagerService, mImeConnectionBindFlags, mCountDownLatch);
+        synchronized (ImfLock.class) {
+            mBindingController =
+                    new InputMethodBindingController(
+                            mInputMethodManagerService.getCurrentImeUserIdLocked(),
+                            mInputMethodManagerService, mImeConnectionBindFlags,
+                            mCountDownLatch);
+        }
     }
 
     @Test
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
index b4cf799..cff2265 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
@@ -222,7 +222,7 @@
                         Process.THREAD_PRIORITY_FOREGROUND, /* allowIo */
                         false);
         mInputMethodManagerService = new InputMethodManagerService(mContext, mServiceThread,
-                mMockInputMethodBindingController);
+                unusedUserId -> mMockInputMethodBindingController);
         spyOn(mInputMethodManagerService);
 
         // Start a InputMethodManagerService.Lifecycle to publish and manage the lifecycle of
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/UserDataRepositoryTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/UserDataRepositoryTest.java
index a15b170..c3a87da 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/UserDataRepositoryTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/UserDataRepositoryTest.java
@@ -18,6 +18,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
@@ -38,6 +39,7 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.function.IntFunction;
 
 // This test is designed to run on both device and host (Ravenwood) side.
 public final class UserDataRepositoryTest {
@@ -51,19 +53,34 @@
     @Mock
     private UserManagerInternal mMockUserManagerInternal;
 
+    @Mock
+    private InputMethodManagerService mMockInputMethodManagerService;
+
     private Handler mHandler;
 
+    private IntFunction<InputMethodBindingController> mBindingControllerFactory;
+
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         mHandler = new Handler(Looper.getMainLooper());
+        mBindingControllerFactory = new IntFunction<InputMethodBindingController>() {
+
+            @Override
+            public InputMethodBindingController apply(int userId) {
+                return new InputMethodBindingController(userId, mMockInputMethodManagerService);
+            }
+        };
     }
 
     @Test
     public void testUserDataRepository_addsNewUserInfoOnUserCreatedEvent() {
         // Create UserDataRepository and capture the user lifecycle listener
         final var captor = ArgumentCaptor.forClass(UserManagerInternal.UserLifecycleListener.class);
-        final var repository = new UserDataRepository(mHandler, mMockUserManagerInternal);
+        final var bindingControllerFactorySpy = spy(mBindingControllerFactory);
+        final var repository = new UserDataRepository(mHandler, mMockUserManagerInternal,
+                bindingControllerFactorySpy);
+
         verify(mMockUserManagerInternal, times(1)).addUserLifecycleListener(captor.capture());
         final var listener = captor.getValue();
 
@@ -77,14 +94,20 @@
         // Assert UserDataRepository contains the expected UserData
         final var allUserData = collectUserData(repository);
         assertThat(allUserData).hasSize(1);
-        assertThat(allUserData.get(0).mUserId).isEqualTo(userInfo.id);
+        assertThat(allUserData.get(0).mUserId).isEqualTo(ANY_USER_ID);
+
+        // Assert UserDataRepository called the InputMethodBindingController creator function.
+        verify(bindingControllerFactorySpy).apply(ANY_USER_ID);
+        assertThat(allUserData.get(0).mBindingController.mUserId).isEqualTo(ANY_USER_ID);
     }
 
     @Test
     public void testUserDataRepository_removesUserInfoOnUserRemovedEvent() {
         // Create UserDataRepository and capture the user lifecycle listener
         final var captor = ArgumentCaptor.forClass(UserManagerInternal.UserLifecycleListener.class);
-        final var repository = new UserDataRepository(mHandler, mMockUserManagerInternal);
+        final var repository = new UserDataRepository(mHandler, mMockUserManagerInternal,
+                userId -> new InputMethodBindingController(userId, mMockInputMethodManagerService));
+
         verify(mMockUserManagerInternal, times(1)).addUserLifecycleListener(captor.capture());
         final var listener = captor.getValue();
 
@@ -104,7 +127,8 @@
 
     @Test
     public void testGetOrCreate() {
-        final var repository = new UserDataRepository(mHandler, mMockUserManagerInternal);
+        final var repository = new UserDataRepository(mHandler, mMockUserManagerInternal,
+                mBindingControllerFactory);
 
         synchronized (ImfLock.class) {
             final var userData = repository.getOrCreate(ANY_USER_ID);
@@ -114,6 +138,9 @@
         final var allUserData = collectUserData(repository);
         assertThat(allUserData).hasSize(1);
         assertThat(allUserData.get(0).mUserId).isEqualTo(ANY_USER_ID);
+
+        // Assert UserDataRepository called the InputMethodBindingController creator function.
+        assertThat(allUserData.get(0).mBindingController.mUserId).isEqualTo(ANY_USER_ID);
     }
 
     private List<UserDataRepository.UserData> collectUserData(UserDataRepository repository) {
diff --git a/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceContentTest.java b/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceContentTest.java
index 7aafa8e..5ddd8a5 100644
--- a/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceContentTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceContentTest.java
@@ -26,7 +26,6 @@
 import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyZeroInteractions;
 
 import android.content.pm.PackageManagerInternal;
 import android.media.projection.MediaProjectionInfo;
@@ -108,7 +107,7 @@
         mMediaPorjectionCallback.onStart(exemptedRecorderPackage);
         mSensitiveContentProtectionManagerService.setSensitiveContentProtection(
                 mPackageInfo.getWindowToken(), mPackageInfo.getPkg(), mPackageInfo.getUid(), true);
-        verifyZeroInteractions(mWindowManager);
+        verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
     }
 
     @Test
@@ -135,7 +134,7 @@
         // when screen sharing is not active, no app window should be blocked.
         mSensitiveContentProtectionManagerService.setSensitiveContentProtection(
                 mPackageInfo.getWindowToken(), mPackageInfo.getPkg(), mPackageInfo.getUid(), true);
-        verifyZeroInteractions(mWindowManager);
+        verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
     }
 
     @Test
@@ -158,8 +157,7 @@
         mMediaPorjectionCallback.onStart(mMediaProjectionInfo);
         mSensitiveContentProtectionManagerService.setSensitiveContentProtection(
                 mPackageInfo.getWindowToken(), mPackageInfo.getPkg(), mPackageInfo.getUid(), true);
-        verify(mWindowManager, never())
-                .addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
+        verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
     }
 
     @Test
@@ -168,7 +166,7 @@
         mMediaProjectionCallbackCaptor.getValue().onStart(mMediaProjectionInfo);
         mSensitiveContentProtectionManagerService.setSensitiveContentProtection(
                 mPackageInfo.getWindowToken(), mPackageInfo.getPkg(), mPackageInfo.getUid(), true);
-        verifyZeroInteractions(mWindowManager);
+        verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
     }
 
     private void mockDisabledViaDeveloperOption() {
diff --git a/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceNotificationTest.java b/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceNotificationTest.java
index a20d935..8b65337 100644
--- a/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceNotificationTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceNotificationTest.java
@@ -30,7 +30,6 @@
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
 import android.content.pm.PackageManagerInternal;
@@ -102,6 +101,8 @@
 
     @Captor
     ArgumentCaptor<MediaProjectionManager.Callback> mMediaProjectionCallbackCaptor;
+    @Captor
+    private ArgumentCaptor<ArraySet<PackageInfo>> mPackageInfoCaptor;
 
     @Mock
     private MediaProjectionManager mProjectionManager;
@@ -309,7 +310,7 @@
 
         mMediaProjectionCallbackCaptor.getValue().onStart(mediaProjectionInfo);
 
-        verifyZeroInteractions(mWindowManager);
+        verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
     }
 
     @Test
@@ -469,7 +470,7 @@
 
         mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
 
-        verifyZeroInteractions(mWindowManager);
+        verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
     }
 
     @Test
@@ -480,7 +481,7 @@
 
         mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected();
 
-        verifyZeroInteractions(mWindowManager);
+        verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
     }
 
     @Test
@@ -495,7 +496,7 @@
 
         mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected();
 
-        verifyZeroInteractions(mWindowManager);
+        verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
     }
 
     @Test
@@ -519,7 +520,7 @@
 
         mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected();
 
-        verifyZeroInteractions(mWindowManager);
+        verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
     }
 
     @Test
@@ -530,7 +531,7 @@
 
         mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected();
 
-        verifyZeroInteractions(mWindowManager);
+        verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
     }
 
     @Test
@@ -541,7 +542,7 @@
 
         mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected();
 
-        verifyZeroInteractions(mWindowManager);
+        verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
     }
 
     @Test
@@ -557,7 +558,7 @@
 
         mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected();
 
-        verifyZeroInteractions(mWindowManager);
+        verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
     }
 
     @Test
@@ -574,7 +575,7 @@
 
         mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected();
 
-        verifyZeroInteractions(mWindowManager);
+        verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
     }
 
     @Test
@@ -586,7 +587,7 @@
         mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
         mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected();
 
-        verifyZeroInteractions(mWindowManager);
+        verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
     }
 
     @Test
@@ -598,7 +599,7 @@
         mSensitiveContentProtectionManagerService.mNotificationListener
                 .onNotificationRankingUpdate(mRankingMap);
 
-        verifyZeroInteractions(mWindowManager);
+        verifyNoBlockOrClearInteractionWithWindowManager();
     }
 
     @Test
@@ -614,7 +615,7 @@
         mSensitiveContentProtectionManagerService.mNotificationListener
                 .onNotificationRankingUpdate(mRankingMap);
 
-        verifyZeroInteractions(mWindowManager);
+        verifyNoBlockOrClearInteractionWithWindowManager();
     }
 
     @Test
@@ -640,7 +641,7 @@
         mSensitiveContentProtectionManagerService.mNotificationListener
                 .onNotificationRankingUpdate(mRankingMap);
 
-        verifyZeroInteractions(mWindowManager);
+        verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
     }
 
     @Test
@@ -652,7 +653,7 @@
         mSensitiveContentProtectionManagerService.mNotificationListener
                 .onNotificationRankingUpdate(mRankingMap);
 
-        verifyZeroInteractions(mWindowManager);
+        verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
     }
 
     @Test
@@ -666,7 +667,7 @@
         mSensitiveContentProtectionManagerService.mNotificationListener
                 .onNotificationRankingUpdate(null);
 
-        verifyZeroInteractions(mWindowManager);
+        verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
     }
 
     @Test
@@ -684,7 +685,7 @@
         mSensitiveContentProtectionManagerService.mNotificationListener
                 .onNotificationRankingUpdate(mRankingMap);
 
-        verifyZeroInteractions(mWindowManager);
+        verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
     }
 
     @Test
@@ -702,7 +703,7 @@
         mSensitiveContentProtectionManagerService.mNotificationListener
                 .onNotificationRankingUpdate(mRankingMap);
 
-        verifyZeroInteractions(mWindowManager);
+        verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
     }
 
     @Test
@@ -715,7 +716,7 @@
         mSensitiveContentProtectionManagerService.mNotificationListener
                 .onNotificationRankingUpdate(mRankingMap);
 
-        verifyZeroInteractions(mWindowManager);
+        verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
     }
 
     @Test
@@ -727,7 +728,7 @@
         mSensitiveContentProtectionManagerService.mNotificationListener
                 .onNotificationPosted(mNotification1, mRankingMap);
 
-        verifyZeroInteractions(mWindowManager);
+        verifyNoBlockOrClearInteractionWithWindowManager();
     }
 
     @Test
@@ -743,7 +744,7 @@
         mSensitiveContentProtectionManagerService.mNotificationListener
                 .onNotificationPosted(mNotification1, mRankingMap);
 
-        verifyZeroInteractions(mWindowManager);
+        verifyNoBlockOrClearInteractionWithWindowManager();
     }
 
     @Test
@@ -773,7 +774,7 @@
         mSensitiveContentProtectionManagerService.mNotificationListener
                 .onNotificationPosted(mNotification2, mRankingMap);
 
-        verifyZeroInteractions(mWindowManager);
+        verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
     }
 
     @Test
@@ -787,7 +788,7 @@
         mSensitiveContentProtectionManagerService.mNotificationListener
                 .onNotificationPosted(null, mRankingMap);
 
-        verifyZeroInteractions(mWindowManager);
+        verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
     }
 
     @Test
@@ -801,7 +802,7 @@
         mSensitiveContentProtectionManagerService.mNotificationListener
                 .onNotificationPosted(mNotification1, null);
 
-        verifyZeroInteractions(mWindowManager);
+        verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
     }
 
     @Test
@@ -816,7 +817,7 @@
         mSensitiveContentProtectionManagerService.mNotificationListener
                 .onNotificationPosted(mNotification1, mRankingMap);
 
-        verifyZeroInteractions(mWindowManager);
+        verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
     }
 
     @Test
@@ -829,7 +830,14 @@
         mSensitiveContentProtectionManagerService.mNotificationListener
                 .onNotificationPosted(mNotification1, mRankingMap);
 
-        verifyZeroInteractions(mWindowManager);
+        verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
+    }
+
+    private void verifyNoBlockOrClearInteractionWithWindowManager() {
+        verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
+        verify(mWindowManager, never()).clearBlockedApps();
+        verify(mWindowManager, never())
+                .removeBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
     }
 
     private void mockDisabledViaDevelopOption() {
diff --git a/services/tests/servicestests/src/com/android/server/autofill/SaveEventLoggerTest.java b/services/tests/servicestests/src/com/android/server/autofill/SaveEventLoggerTest.java
new file mode 100644
index 0000000..0bca59d
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/autofill/SaveEventLoggerTest.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2024 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.autofill;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
+
+@RunWith(JUnit4.class)
+public class SaveEventLoggerTest {
+
+    @Test
+    public void testTimestampsInitialized() {
+        SaveEventLogger mLogger = spy(SaveEventLogger.forSessionId(1, 1));
+
+        mLogger.maybeSetLatencySaveUiDisplayMillis();
+        mLogger.maybeSetLatencySaveRequestMillis();
+        mLogger.maybeSetLatencySaveFinishMillis();
+
+        ArgumentCaptor<Long> latencySaveUiDisplayMillis = ArgumentCaptor.forClass(Long.class);
+        ArgumentCaptor<Long> latencySaveRequestMillis = ArgumentCaptor.forClass(Long.class);
+        ArgumentCaptor<Long> latencySaveFinishMillis = ArgumentCaptor.forClass(Long.class);
+
+        verify(mLogger, times(1))
+                .maybeSetLatencySaveUiDisplayMillis(latencySaveUiDisplayMillis.capture());
+        verify(mLogger, times(1))
+                .maybeSetLatencySaveRequestMillis(latencySaveRequestMillis.capture());
+        verify(mLogger, times(1))
+                .maybeSetLatencySaveFinishMillis(latencySaveFinishMillis.capture());
+
+        assertThat(latencySaveUiDisplayMillis.getValue())
+                .isNotEqualTo(SaveEventLogger.UNINITIATED_TIMESTAMP);
+        assertThat(latencySaveRequestMillis.getValue())
+                .isNotEqualTo(SaveEventLogger.UNINITIATED_TIMESTAMP);
+        assertThat(latencySaveFinishMillis.getValue())
+                .isNotEqualTo(SaveEventLogger.UNINITIATED_TIMESTAMP);
+    }
+
+    @Test
+    public void testTimestampsNotInitialized() {
+        SaveEventLogger mLogger =
+                spy(SaveEventLogger.forSessionId(1, SaveEventLogger.UNINITIATED_TIMESTAMP));
+
+        mLogger.maybeSetLatencySaveUiDisplayMillis();
+        mLogger.maybeSetLatencySaveRequestMillis();
+        mLogger.maybeSetLatencySaveFinishMillis();
+        ArgumentCaptor<Long> latencySaveUiDisplayMillis = ArgumentCaptor.forClass(Long.class);
+        ArgumentCaptor<Long> latencySaveRequestMillis = ArgumentCaptor.forClass(Long.class);
+        ArgumentCaptor<Long> latencySaveFinishMillis = ArgumentCaptor.forClass(Long.class);
+
+        verify(mLogger, times(1))
+                .maybeSetLatencySaveUiDisplayMillis(latencySaveUiDisplayMillis.capture());
+        verify(mLogger, times(1))
+                .maybeSetLatencySaveRequestMillis(latencySaveRequestMillis.capture());
+        verify(mLogger, times(1))
+                .maybeSetLatencySaveFinishMillis(latencySaveFinishMillis.capture());
+
+        assertThat(latencySaveUiDisplayMillis.getValue())
+                .isEqualTo(SaveEventLogger.UNINITIATED_TIMESTAMP);
+        assertThat(latencySaveRequestMillis.getValue())
+                .isEqualTo(SaveEventLogger.UNINITIATED_TIMESTAMP);
+        assertThat(latencySaveFinishMillis.getValue())
+                .isEqualTo(SaveEventLogger.UNINITIATED_TIMESTAMP);
+    }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
index 1194973..c7c97e4 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
@@ -34,6 +34,7 @@
 import static junit.framework.Assert.assertEquals;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.Matchers.anyInt;
 import static org.mockito.Matchers.anyString;
 import static org.mockito.Matchers.eq;
@@ -54,6 +55,7 @@
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.Icon;
 import android.os.UserHandle;
+import android.platform.test.annotations.DisableFlags;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.service.notification.StatusBarNotification;
@@ -271,7 +273,8 @@
     }
 
     @Test
-    public void testAddSummary() {
+    @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
+    public void testAddSummary_alwaysAutogroup() {
         final String pkg = "package";
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
             mGroupHelper.onNotificationPosted(
@@ -279,13 +282,52 @@
         }
         verify(mCallback, times(1)).addAutoGroupSummary(
                 anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS)));
-        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString());
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean());
         verify(mCallback, never()).removeAutoGroup(anyString());
         verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
         verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
     }
 
     @Test
+    @EnableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
+    public void testAddSummary() {
+        final String pkg = "package";
+        for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) {
+            assertThat(mGroupHelper.onNotificationPosted(
+                    getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM), false)).isFalse();
+        }
+        assertThat(mGroupHelper.onNotificationPosted(
+                getSbn(pkg, AUTOGROUP_AT_COUNT - 1, String.valueOf(AUTOGROUP_AT_COUNT - 1),
+                        UserHandle.SYSTEM), false)).isTrue();
+        verify(mCallback, times(1)).addAutoGroupSummary(
+                anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS)));
+        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean());
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
+    }
+
+    @Test
+    @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
+    public void testAddSummary_oneChildOngoing_summaryOngoing_alwaysAutogroup() {
+        final String pkg = "package";
+        for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+            StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM);
+            if (i == 0) {
+                sbn.getNotification().flags |= FLAG_ONGOING_EVENT;
+            }
+            mGroupHelper.onNotificationPosted(sbn, false);
+        }
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                eq(getNotificationAttributes(BASE_FLAGS | FLAG_ONGOING_EVENT)));
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean());
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
+    }
+
+    @Test
+    @EnableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
     public void testAddSummary_oneChildOngoing_summaryOngoing() {
         final String pkg = "package";
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
@@ -297,13 +339,33 @@
         }
         verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
                 eq(getNotificationAttributes(BASE_FLAGS | FLAG_ONGOING_EVENT)));
-        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString());
+        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean());
         verify(mCallback, never()).removeAutoGroup(anyString());
         verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
         verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
     }
 
     @Test
+    @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
+    public void testAddSummary_oneChildAutoCancel_summaryNotAutoCancel_alwaysAutogroup() {
+        final String pkg = "package";
+        for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+            StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM);
+            if (i == 0) {
+                sbn.getNotification().flags |= FLAG_AUTO_CANCEL;
+            }
+            mGroupHelper.onNotificationPosted(sbn, false);
+        }
+        verify(mCallback, times(1)).addAutoGroupSummary(
+                anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS)));
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean());
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
+    }
+
+    @Test
+    @EnableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
     public void testAddSummary_oneChildAutoCancel_summaryNotAutoCancel() {
         final String pkg = "package";
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
@@ -315,13 +377,31 @@
         }
         verify(mCallback, times(1)).addAutoGroupSummary(
                 anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS)));
-        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString());
+        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean());
         verify(mCallback, never()).removeAutoGroup(anyString());
         verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
         verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
     }
 
     @Test
+    @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
+    public void testAddSummary_allChildrenAutoCancel_summaryAutoCancel_alwaysAutogroup() {
+        final String pkg = "package";
+        for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+            StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM);
+            sbn.getNotification().flags |= FLAG_AUTO_CANCEL;
+            mGroupHelper.onNotificationPosted(sbn, false);
+        }
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                eq(getNotificationAttributes(BASE_FLAGS | FLAG_AUTO_CANCEL)));
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean());
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
+    }
+
+    @Test
+    @EnableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
     public void testAddSummary_allChildrenAutoCancel_summaryAutoCancel() {
         final String pkg = "package";
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
@@ -331,13 +411,34 @@
         }
         verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
                 eq(getNotificationAttributes(BASE_FLAGS | FLAG_AUTO_CANCEL)));
-        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString());
+        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean());
         verify(mCallback, never()).removeAutoGroup(anyString());
         verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
         verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
     }
 
     @Test
+    @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
+    public void testAddSummary_summaryAutoCancelNoClear_alwaysAutogroup() {
+        final String pkg = "package";
+        for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+            StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM);
+            sbn.getNotification().flags |= FLAG_AUTO_CANCEL;
+            if (i == 0) {
+                sbn.getNotification().flags |= FLAG_NO_CLEAR;
+            }
+            mGroupHelper.onNotificationPosted(sbn, false);
+        }
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                eq(getNotificationAttributes(BASE_FLAGS | FLAG_AUTO_CANCEL | FLAG_NO_CLEAR)));
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean());
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
+    }
+
+    @Test
+    @EnableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
     public void testAddSummary_summaryAutoCancelNoClear() {
         final String pkg = "package";
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
@@ -350,7 +451,7 @@
         }
         verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
                 eq(getNotificationAttributes(BASE_FLAGS | FLAG_AUTO_CANCEL | FLAG_NO_CLEAR)));
-        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString());
+        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean());
         verify(mCallback, never()).removeAutoGroup(anyString());
         verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
         verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
@@ -617,7 +718,7 @@
         }
         verify(mCallback, times(1)).addAutoGroupSummary(
                 anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS)));
-        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString());
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean());
         verify(mCallback, never()).removeAutoGroup(anyString());
         verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
         Mockito.reset(mCallback);
@@ -645,7 +746,7 @@
         }
         verify(mCallback, times(1)).addAutoGroupSummary(
                 anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS)));
-        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString());
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean());
         verify(mCallback, never()).removeAutoGroup(anyString());
         verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
         Mockito.reset(mCallback);
@@ -664,7 +765,8 @@
     }
 
     @Test
-    public void testNewNotificationsAddedToAutogroup_ifOriginalNotificationsCanceled() {
+    @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
+    public void testNewNotificationsAddedToAutogroup_ifOriginalNotificationsCanceled_alwaysGroup() {
         final String pkg = "package";
         List<StatusBarNotification> posted = new ArrayList<>();
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
@@ -674,7 +776,7 @@
         }
         verify(mCallback, times(1)).addAutoGroupSummary(
                 anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS)));
-        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString());
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean());
         verify(mCallback, never()).removeAutoGroup(anyString());
         verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
         Mockito.reset(mCallback);
@@ -693,8 +795,8 @@
         // < AUTOGROUP_AT_COUNT
         final StatusBarNotification sbn = getSbn(pkg, 5, String.valueOf(5), UserHandle.SYSTEM);
         posted.add(sbn);
-        mGroupHelper.onNotificationPosted(sbn, true);
-        verify(mCallback, times(1)).addAutoGroup(sbn.getKey());
+        assertThat(mGroupHelper.onNotificationPosted(sbn, true)).isFalse();
+        verify(mCallback, times(1)).addAutoGroup(sbn.getKey(), true);
         verify(mCallback, never()).removeAutoGroup(anyString());
         verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
         verify(mCallback).updateAutogroupSummary(anyInt(), anyString(),
@@ -703,7 +805,84 @@
     }
 
     @Test
+    @EnableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
+    public void testNewNotificationsAddedToAutogroup_ifOriginalNotificationsCanceled() {
+        final String pkg = "package";
+        List<StatusBarNotification> posted = new ArrayList<>();
+        for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+            final StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM);
+            posted.add(sbn);
+            mGroupHelper.onNotificationPosted(sbn, false);
+        }
+
+        verify(mCallback, times(1)).addAutoGroupSummary(
+                anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS)));
+        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean());
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+        Mockito.reset(mCallback);
+
+        for (int i = posted.size() - 2; i >= 0; i--) {
+            mGroupHelper.onNotificationRemoved(posted.remove(i));
+        }
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+        Mockito.reset(mCallback);
+
+        // only one child remains
+        assertEquals(1, mGroupHelper.getNotGroupedByAppCount(UserHandle.USER_SYSTEM, pkg));
+
+        // Add new notification; it should be autogrouped even though the total count is
+        // < AUTOGROUP_AT_COUNT
+        final StatusBarNotification sbn = getSbn(pkg, 5, String.valueOf(5), UserHandle.SYSTEM);
+        posted.add(sbn);
+        assertThat(mGroupHelper.onNotificationPosted(sbn, true)).isTrue();
+        // addAutoGroup not called on sbn, because the autogrouping is expected to be done
+        // synchronously.
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+        verify(mCallback).updateAutogroupSummary(anyInt(), anyString(),
+                eq(getNotificationAttributes(BASE_FLAGS)));
+        verify(mCallback, never()).addAutoGroupSummary(anyInt(), anyString(), anyString(), any());
+    }
+
+    @Test
+    @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
     @EnableFlags(Flags.FLAG_AUTOGROUP_SUMMARY_ICON_UPDATE)
+    public void testAddSummary_sameIcon_sameColor_alwaysAutogroup() {
+        final String pkg = "package";
+        final Icon icon = mock(Icon.class);
+        when(icon.sameAs(icon)).thenReturn(true);
+        final int iconColor = Color.BLUE;
+        final NotificationAttributes attr = new NotificationAttributes(BASE_FLAGS, icon, iconColor,
+                DEFAULT_VISIBILITY);
+
+        // Add notifications with same icon and color
+        for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+            StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, null,
+                    icon, iconColor);
+            mGroupHelper.onNotificationPosted(sbn, false);
+        }
+        // Check that the summary would have the same icon and color
+        verify(mCallback, times(1)).addAutoGroupSummary(
+                anyInt(), eq(pkg), anyString(), eq(attr));
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean());
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+
+        // After auto-grouping, add new notification with the same color
+        StatusBarNotification sbn = getSbn(pkg, AUTOGROUP_AT_COUNT,
+                String.valueOf(AUTOGROUP_AT_COUNT), UserHandle.SYSTEM, null, icon, iconColor);
+        mGroupHelper.onNotificationPosted(sbn, true);
+
+        // Check that the summary was updated
+        //NotificationAttributes newAttr = new NotificationAttributes(BASE_FLAGS, icon, iconColor);
+        verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), eq(attr));
+    }
+
+    @Test
+    @EnableFlags({Flags.FLAG_AUTOGROUP_SUMMARY_ICON_UPDATE,
+            android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST})
     public void testAddSummary_sameIcon_sameColor() {
         final String pkg = "package";
         final Icon icon = mock(Icon.class);
@@ -721,7 +900,7 @@
         // Check that the summary would have the same icon and color
         verify(mCallback, times(1)).addAutoGroupSummary(
                 anyInt(), eq(pkg), anyString(), eq(attr));
-        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString());
+        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean());
         verify(mCallback, never()).removeAutoGroup(anyString());
         verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
 
@@ -761,7 +940,7 @@
         // Check that the summary would have the same icon and color
         verify(mCallback, times(1)).addAutoGroupSummary(
                 anyInt(), eq(pkg), anyString(), eq(initialAttr));
-        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString());
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean());
         verify(mCallback, never()).removeAutoGroup(anyString());
         verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
 
@@ -780,8 +959,9 @@
     }
 
     @Test
+    @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
     @EnableFlags(Flags.FLAG_AUTOGROUP_SUMMARY_ICON_UPDATE)
-    public void testAddSummary_diffVisibility() {
+    public void testAddSummary_diffVisibility_alwaysAutogroup() {
         final String pkg = "package";
         final Icon icon = mock(Icon.class);
         when(icon.sameAs(icon)).thenReturn(true);
@@ -798,7 +978,8 @@
         // Check that the summary has private visibility
         verify(mCallback, times(1)).addAutoGroupSummary(
                 anyInt(), eq(pkg), anyString(), eq(attr));
-        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString());
+
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean());
         verify(mCallback, never()).removeAutoGroup(anyString());
         verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
 
@@ -815,6 +996,48 @@
     }
 
     @Test
+    @EnableFlags({Flags.FLAG_AUTOGROUP_SUMMARY_ICON_UPDATE,
+            android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST})
+    public void testAddSummary_diffVisibility() {
+        final String pkg = "package";
+        final Icon icon = mock(Icon.class);
+        when(icon.sameAs(icon)).thenReturn(true);
+        final int iconColor = Color.BLUE;
+        final NotificationAttributes attr = new NotificationAttributes(BASE_FLAGS, icon, iconColor,
+                VISIBILITY_PRIVATE);
+
+        // Add notifications with same icon and color and default visibility (private)
+        for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) {
+            StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, null,
+                    icon, iconColor);
+            assertThat(mGroupHelper.onNotificationPosted(sbn, false)).isFalse();
+        }
+        // The last notification added will reach the autogroup threshold.
+        StatusBarNotification sbn = getSbn(pkg, AUTOGROUP_AT_COUNT - 1,
+                String.valueOf(AUTOGROUP_AT_COUNT - 1), UserHandle.SYSTEM, null, icon, iconColor);
+        assertThat(mGroupHelper.onNotificationPosted(sbn, false)).isTrue();
+
+        // Check that the summary has private visibility
+        verify(mCallback, times(1)).addAutoGroupSummary(
+                anyInt(), eq(pkg), anyString(), eq(attr));
+        // The last sbn is expected to be added to autogroup synchronously.
+        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean());
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+
+        // After auto-grouping, add new notification with public visibility
+        sbn = getSbn(pkg, AUTOGROUP_AT_COUNT,
+                String.valueOf(AUTOGROUP_AT_COUNT), UserHandle.SYSTEM, null, icon, iconColor);
+        sbn.getNotification().visibility = VISIBILITY_PUBLIC;
+        assertThat(mGroupHelper.onNotificationPosted(sbn, true)).isTrue();
+
+        // Check that the summary visibility was updated
+        NotificationAttributes newAttr = new NotificationAttributes(BASE_FLAGS, icon, iconColor,
+                VISIBILITY_PUBLIC);
+        verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), eq(newAttr));
+    }
+
+    @Test
     @EnableFlags(Flags.FLAG_AUTOGROUP_SUMMARY_ICON_UPDATE)
     public void testAutoGrouped_diffIcon_diffColor_removeChild_updateTo_sameIcon_sameColor() {
         final String pkg = "package";
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 3a0eba1..b366f92 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -311,6 +311,7 @@
 
 import com.google.android.collect.Lists;
 import com.google.common.collect.ImmutableList;
+
 import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
 import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
 
@@ -331,8 +332,6 @@
 import org.mockito.MockitoAnnotations;
 import org.mockito.invocation.InvocationOnMock;
 import org.mockito.stubbing.Answer;
-import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
-import platform.test.runner.parameterized.Parameters;
 
 import java.io.BufferedInputStream;
 import java.io.BufferedOutputStream;
@@ -348,6 +347,9 @@
 import java.util.concurrent.CountDownLatch;
 import java.util.function.Consumer;
 
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
 @SmallTest
 @RunWith(ParameterizedAndroidJunit4.class)
 @RunWithLooper
@@ -5542,7 +5544,7 @@
     public void testAddAutogroup_requestsSort() throws Exception {
         final NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
         mService.addNotification(r);
-        mService.addAutogroupKeyLocked(r.getKey());
+        mService.addAutogroupKeyLocked(r.getKey(), true);
 
         verify(mRankingHandler, times(1)).requestSort();
     }
@@ -5562,12 +5564,30 @@
         final NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
         r.setOverrideGroupKey("TEST");
         mService.addNotification(r);
-        mService.addAutogroupKeyLocked(r.getKey());
+        mService.addAutogroupKeyLocked(r.getKey(), true);
 
         verify(mRankingHandler, never()).requestSort();
     }
 
     @Test
+    @EnableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
+    public void testAutogroupSuppressSort_noSort() throws Exception {
+        final NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
+        mService.addNotification(r);
+        mService.addAutogroupKeyLocked(r.getKey(), false);
+
+        verify(mRankingHandler, never()).requestSort();
+    }
+
+    @Test
+    @EnableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
+    public void testAutogroupOnPost_skipManualSort() throws Exception {
+        final NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
+        mService.addNotification(r);
+        verify(mRankingHandler, never()).requestSort();
+    }
+
+    @Test
     public void testHandleRankingSort_sendsUpdateOnSignalExtractorChange() throws Exception {
         mService.setPreferencesHelper(mPreferencesHelper);
         NotificationManagerService.WorkerHandler handler = mock(
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index 27d9d13..44d1b54 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -114,6 +114,8 @@
 import android.os.Binder;
 import android.os.RemoteException;
 import android.os.SystemClock;
+import android.os.UserHandle;
+import android.os.UserManager;
 import android.platform.test.annotations.Presubmit;
 import android.platform.test.annotations.RequiresFlagsDisabled;
 import android.platform.test.flag.junit.CheckFlagsRule;
@@ -2813,6 +2815,41 @@
                 mDisplayContent.getKeepClearAreas());
     }
 
+    @Test
+    public void testHasAccessConsidersUserVisibilityForBackgroundVisibleUsers() {
+        doReturn(true).when(() -> UserManager.isVisibleBackgroundUsersEnabled());
+        final int appId = 1234;
+        final int userId1 = 11;
+        final int userId2 = 12;
+        final int uid1 = UserHandle.getUid(userId1, appId);
+        final int uid2 = UserHandle.getUid(userId2, appId);
+        final DisplayInfo displayInfo = new DisplayInfo(mDisplayInfo);
+        final DisplayContent dc = createNewDisplay(displayInfo);
+        int displayId = dc.getDisplayId();
+        doReturn(true).when(mWm.mUmInternal).isUserVisible(userId1, displayId);
+        doReturn(false).when(mWm.mUmInternal).isUserVisible(userId2, displayId);
+
+        assertTrue(dc.hasAccess(uid1));
+        assertFalse(dc.hasAccess(uid2));
+    }
+
+    @Test
+    public void testHasAccessIgnoresUserVisibilityForPrivateDisplay() {
+        doReturn(true).when(() -> UserManager.isVisibleBackgroundUsersEnabled());
+        final int appId = 1234;
+        final int userId2 = 12;
+        final int uid2 = UserHandle.getUid(userId2, appId);
+        final DisplayInfo displayInfo = new DisplayInfo(mDisplayInfo);
+        displayInfo.flags = FLAG_PRIVATE;
+        displayInfo.ownerUid = uid2;
+        final DisplayContent dc = createNewDisplay(displayInfo);
+        int displayId = dc.getDisplayId();
+
+        assertTrue(dc.hasAccess(uid2));
+
+        verify(mWm.mUmInternal, never()).isUserVisible(userId2, displayId);
+    }
+
     private void removeRootTaskTests(Runnable runnable) {
         final TaskDisplayArea taskDisplayArea = mRootWindowContainer.getDefaultTaskDisplayArea();
         final Task rootTask1 = taskDisplayArea.createRootTask(WINDOWING_MODE_FULLSCREEN,
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 25e2d82..03ba8fa 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -13787,11 +13787,11 @@
      * <p>This method returns valid data on devices with {@link
      * android.content.pm.PackageManager#FEATURE_TELEPHONY_CARRIERLOCK} enabled.
      *
-     * @deprecated Apps should use {@link getCarriersRestrictionRules} to retrieve the list of
+     * @deprecated Apps should use {@link #getCarrierRestrictionRules} to retrieve the list of
      * allowed and excliuded carriers, as the result of this API is valid only when the excluded
      * list is empty. This API could return an empty list, even if some restrictions are present.
      *
-     * @return List of {@link android.telephony.CarrierIdentifier}; empty list
+     * @return List of {@link android.service.carrier.CarrierIdentifier}; empty list
      * means all carriers are allowed.
      *
      * @throws UnsupportedOperationException If the device does not have