Merge "Fix the namespace for aconfig flag" 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/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java
index e66f7fe..d8df447 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -1270,4 +1270,16 @@
      * @hide
      */
     public abstract boolean shouldDelayHomeLaunch(int userId);
+
+    /**
+     * Add a startup timestamp to the most recent start of the specified process.
+     *
+     * @param key The {@link ApplicationStartInfo} start timestamp key of the timestamp to add.
+     * @param timestampNs The clock monotonic timestamp to add in nanoseconds.
+     * @param uid The UID of the process to add this timestamp to.
+     * @param pid The process id of the process to add this timestamp to.
+     * @param userId The userId in the multi-user environment.
+     */
+    public abstract void addStartInfoTimestamp(int key, long timestampNs, int uid, int pid,
+            int userId);
 }
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/res/res/drawable/ic_thread_network.xml b/core/res/res/drawable/ic_thread_network.xml
new file mode 100644
index 0000000..1d7608f
--- /dev/null
+++ b/core/res/res/drawable/ic_thread_network.xml
@@ -0,0 +1,25 @@
+<!--
+    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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="960"
+    android:viewportHeight="960"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M476,880Q394,879 322,847.5Q250,816 196,761.5Q142,707 111,634.5Q80,562 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,623 791.5,732.5Q703,842 563,871L563,480L607,480Q661,480 699.5,441.5Q738,403 738,349Q738,295 699.5,256.5Q661,218 607,218Q553,218 514.5,256.5Q476,295 476,349L476,393L345,393Q279,393 233,439Q187,485 187,551Q187,617 233,662.5Q279,708 345,708L345,621Q316,621 295,600.5Q274,580 274,551Q274,522 295,501Q316,480 345,480L476,480L476,880ZM563,393L563,349Q563,331 576,318Q589,305 607,305Q625,305 638,318Q651,331 651,349Q651,367 638,380Q625,393 607,393L563,393Z"/>
+</vector>
diff --git a/core/res/res/layout/miniresolver.xml b/core/res/res/layout/miniresolver.xml
index db0ea54..e60e0b0 100644
--- a/core/res/res/layout/miniresolver.xml
+++ b/core/res/res/layout/miniresolver.xml
@@ -33,7 +33,6 @@
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_alwaysShow="true"
-        android:elevation="@dimen/resolver_elevation"
         android:paddingTop="24dp"
         android:paddingStart="@dimen/resolver_edge_margin"
         android:paddingEnd="@dimen/resolver_edge_margin"
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index b3d8f39..54dbc48 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1387,6 +1387,7 @@
   <java-symbol type="drawable" name="platlogo" />
   <java-symbol type="drawable" name="stat_notify_sync_error" />
   <java-symbol type="drawable" name="stat_notify_wifi_in_range" />
+  <java-symbol type="drawable" name="ic_thread_network" />
   <java-symbol type="drawable" name="ic_wifi_signal_0" />
   <java-symbol type="drawable" name="ic_wifi_signal_1" />
   <java-symbol type="drawable" name="ic_wifi_signal_2" />
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/data/etc/platform.xml b/data/etc/platform.xml
index 9d1e507..65615e6 100644
--- a/data/etc/platform.xml
+++ b/data/etc/platform.xml
@@ -322,11 +322,11 @@
     <library name="android.hidl.manager-V1.0-java"
             file="/system/framework/android.hidl.manager-V1.0-java.jar" />
 
-    <!-- These are the standard packages that are white-listed to always have internet
+    <!-- These are the standard packages that are allowed to always have internet
          access while in power save mode, even if they aren't in the foreground. -->
     <allow-in-power-save package="com.android.providers.downloads" />
 
-    <!-- These are the standard packages that are white-listed to always have internet
+    <!-- These are the standard packages that are allowed to always have internet
          access while in data mode, even if they aren't in the foreground. -->
     <allow-in-data-usage-save package="com.android.providers.downloads" />
 
@@ -338,7 +338,7 @@
     <!-- Emergency app needs to run in the background to reliably provide safety features -->
     <allow-in-power-save package="com.android.emergency" />
 
-    <!-- Whitelist system providers -->
+    <!-- Allow system providers -->
     <!-- Calendar provider needs alarms while in idle -->
     <allow-in-power-save package="com.android.providers.calendar" />
     <allow-in-power-save-except-idle package="com.android.providers.contacts" />
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index e38038e..59092d4 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -3137,11 +3137,9 @@
     private static EmbeddedActivityWindowInfo translateActivityWindowInfo(
             @NonNull Activity activity, @NonNull ActivityWindowInfo activityWindowInfo) {
         final boolean isEmbedded = activityWindowInfo.isEmbedded();
-        final Rect activityBounds = new Rect(activity.getResources().getConfiguration()
-                .windowConfiguration.getBounds());
         final Rect taskBounds = new Rect(activityWindowInfo.getTaskBounds());
         final Rect activityStackBounds = new Rect(activityWindowInfo.getTaskFragmentBounds());
-        return new EmbeddedActivityWindowInfo(activity, isEmbedded, activityBounds, taskBounds,
+        return new EmbeddedActivityWindowInfo(activity, isEmbedded, taskBounds,
                 activityStackBounds);
     }
 
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
index 8bc3a30..a525877 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
@@ -1570,8 +1570,6 @@
         mSetFlagRule.enableFlags(Flags.FLAG_ACTIVITY_WINDOW_INFO_FLAG);
 
         final boolean isEmbedded = true;
-        final Rect activityBounds = mActivity.getResources().getConfiguration().windowConfiguration
-                .getBounds();
         final Rect taskBounds = new Rect(0, 0, 1000, 2000);
         final Rect activityStackBounds = new Rect(0, 0, 500, 2000);
         doReturn(isEmbedded).when(mActivityWindowInfo).isEmbedded();
@@ -1579,7 +1577,7 @@
         doReturn(activityStackBounds).when(mActivityWindowInfo).getTaskFragmentBounds();
 
         final EmbeddedActivityWindowInfo expected = new EmbeddedActivityWindowInfo(mActivity,
-                isEmbedded, activityBounds, taskBounds, activityStackBounds);
+                isEmbedded, taskBounds, activityStackBounds);
         assertEquals(expected, mSplitController.getEmbeddedActivityWindowInfo(mActivity));
     }
 
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/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml b/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml
index 2aff2c3..3fc6154 100644
--- a/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml
+++ b/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml
@@ -46,6 +46,7 @@
                     android:paddingEnd="@dimen/autofill_view_right_padding"
                     android:paddingTop="@dimen/autofill_view_top_padding"
                     android:paddingBottom="@dimen/autofill_view_bottom_padding"
+                    android:textDirection="locale"
                     android:orientation="vertical">
 
                         <TextView
@@ -53,6 +54,7 @@
                             android:layout_width="wrap_content"
                             android:layout_height="wrap_content"
                             android:textColor="?androidprv:attr/materialColorOnSurface"
+                            android:textDirection="locale"
                             style="@style/autofill.TextTitle"/>
 
                         <TextView
@@ -60,6 +62,7 @@
                             android:layout_width="wrap_content"
                             android:layout_height="wrap_content"
                             android:textColor="?androidprv:attr/materialColorOnSurfaceVariant"
+                            android:textDirection="locale"
                             style="@style/autofill.TextSubtitle"/>
 
                 </LinearLayout>
diff --git a/packages/InputDevices/res/xml/keyboard_layouts.xml b/packages/InputDevices/res/xml/keyboard_layouts.xml
index c18d73c..d4f8f7d 100644
--- a/packages/InputDevices/res/xml/keyboard_layouts.xml
+++ b/packages/InputDevices/res/xml/keyboard_layouts.xml
@@ -94,7 +94,7 @@
         android:name="keyboard_layout_swiss_german"
         android:label="@string/keyboard_layout_swiss_german_label"
         android:keyboardLayout="@raw/keyboard_layout_swiss_german"
-        android:keyboardLocale="de-Latn-CH|gsw-Latn-CH"
+        android:keyboardLocale="de-Latn-CH,gsw-Latn-CH"
         android:keyboardLayoutType="qwertz" />
 
     <keyboard-layout
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/MaterialColors.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/MaterialColors.kt
index 52c4893..d6704a5 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/MaterialColors.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/MaterialColors.kt
@@ -47,3 +47,7 @@
 
 val ColorScheme.surfaceTone: Color
     get() = primary.copy(SettingsOpacity.SurfaceTone)
+
+/** The overall background color in Settings. */
+val ColorScheme.settingsBackground: Color
+    get() = surfaceContainer
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsColors.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsColors.kt
index 69aba71..0db01e8 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsColors.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsColors.kt
@@ -26,7 +26,6 @@
 import androidx.compose.ui.platform.LocalContext
 
 data class SettingsColorScheme(
-    val background: Color = Color.Unspecified,
     val categoryTitle: Color = Color.Unspecified,
     val surface: Color = Color.Unspecified,
     val surfaceHeader: Color = Color.Unspecified,
@@ -65,7 +64,6 @@
 internal fun dynamicLightColorScheme(context: Context): SettingsColorScheme {
     val tonalPalette = dynamicTonalPalette(context)
     return SettingsColorScheme(
-        background = tonalPalette.neutral95,
         categoryTitle = tonalPalette.primary40,
         surface = tonalPalette.neutral99,
         surfaceHeader = tonalPalette.neutral90,
@@ -88,7 +86,6 @@
 internal fun dynamicDarkColorScheme(context: Context): SettingsColorScheme {
     val tonalPalette = dynamicTonalPalette(context)
     return SettingsColorScheme(
-        background = tonalPalette.neutral10,
         categoryTitle = tonalPalette.primary90,
         surface = tonalPalette.neutral20,
         surfaceHeader = tonalPalette.neutral30,
@@ -102,7 +99,6 @@
 internal fun darkColorScheme(): SettingsColorScheme {
     val tonalPalette = tonalPalette()
     return SettingsColorScheme(
-        background = tonalPalette.neutral10,
         categoryTitle = tonalPalette.primary90,
         surface = tonalPalette.neutral20,
         surfaceHeader = tonalPalette.neutral30,
@@ -116,7 +112,6 @@
 internal fun lightColorScheme(): SettingsColorScheme {
     val tonalPalette = tonalPalette()
     return SettingsColorScheme(
-        background = tonalPalette.neutral95,
         categoryTitle = tonalPalette.primary40,
         surface = tonalPalette.neutral99,
         surfaceHeader = tonalPalette.neutral90,
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTheme.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTheme.kt
index c395558..d14b960 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTheme.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTheme.kt
@@ -29,12 +29,11 @@
 @Composable
 fun SettingsTheme(content: @Composable () -> Unit) {
     val isDarkTheme = isSystemInDarkTheme()
-    val settingsColorScheme = settingsColorScheme(isDarkTheme)
-    val colorScheme = materialColorScheme(isDarkTheme).copy(
-        background = settingsColorScheme.background,
-    )
 
-    MaterialTheme(colorScheme = colorScheme, typography = rememberSettingsTypography()) {
+    MaterialTheme(
+        colorScheme = materialColorScheme(isDarkTheme),
+        typography = rememberSettingsTypography(),
+    ) {
         CompositionLocalProvider(
             LocalColorScheme provides settingsColorScheme(isDarkTheme),
             LocalContentColor provides MaterialTheme.colorScheme.onSurface,
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt
index 56534f4..706bd0a 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt
@@ -75,6 +75,7 @@
 import androidx.compose.ui.unit.dp
 import com.android.settingslib.spa.framework.theme.SettingsDimension
 import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.framework.theme.settingsBackground
 import kotlin.math.abs
 import kotlin.math.max
 import kotlin.math.roundToInt
@@ -140,7 +141,7 @@
 
 @Composable
 private fun topAppBarColors() = TopAppBarColors(
-    containerColor = MaterialTheme.colorScheme.background,
+    containerColor = MaterialTheme.colorScheme.settingsBackground,
     scrolledContainerColor = SettingsTheme.colorScheme.surfaceHeader,
     navigationIconContentColor = MaterialTheme.colorScheme.onSurface,
     titleContentColor = MaterialTheme.colorScheme.onSurface,
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/HomeScaffold.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/HomeScaffold.kt
index 711c8a7..feb9737b 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/HomeScaffold.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/HomeScaffold.kt
@@ -28,13 +28,14 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.framework.theme.settingsBackground
 
 @Composable
 fun HomeScaffold(title: String, content: @Composable () -> Unit) {
     Column(
         Modifier
             .fillMaxSize()
-            .background(color = MaterialTheme.colorScheme.background)
+            .background(color = MaterialTheme.colorScheme.settingsBackground)
             .systemBarsPadding()
             .verticalScroll(rememberScrollState()),
     ) {
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt
index c87178d..a49b358 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt
@@ -59,6 +59,7 @@
 import com.android.settingslib.spa.framework.compose.horizontalValues
 import com.android.settingslib.spa.framework.theme.SettingsOpacity
 import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.framework.theme.settingsBackground
 import com.android.settingslib.spa.widget.preference.Preference
 import com.android.settingslib.spa.widget.preference.PreferenceModel
 
@@ -90,6 +91,7 @@
                 onSearchQueryChange = { viewModel.searchQuery = it },
             )
         },
+        containerColor = MaterialTheme.colorScheme.settingsBackground,
     ) { paddingValues ->
         Box(
             Modifier
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt
index 8919402..af7a146 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt
@@ -25,6 +25,7 @@
 import androidx.compose.foundation.layout.RowScope
 import androidx.compose.foundation.layout.padding
 import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Scaffold
 import androidx.compose.material3.TopAppBarDefaults
 import androidx.compose.runtime.Composable
@@ -36,6 +37,7 @@
 import com.android.settingslib.spa.framework.compose.horizontalValues
 import com.android.settingslib.spa.framework.compose.verticalValues
 import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.framework.theme.settingsBackground
 import com.android.settingslib.spa.widget.preference.Preference
 import com.android.settingslib.spa.widget.preference.PreferenceModel
 
@@ -54,6 +56,7 @@
     Scaffold(
         modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
         topBar = { SettingsTopAppBar(title, scrollBehavior, actions) },
+        containerColor = MaterialTheme.colorScheme.settingsBackground,
     ) { paddingValues ->
         Box(Modifier.padding(paddingValues.horizontalValues())) {
             content(paddingValues.verticalValues())
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SuwScaffold.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SuwScaffold.kt
index e39b175..fc40930 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SuwScaffold.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SuwScaffold.kt
@@ -37,6 +37,7 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.vector.ImageVector
 import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.framework.theme.settingsBackground
 import com.android.settingslib.spa.framework.theme.toMediumWeight
 
 data class BottomAppBarButton(
@@ -54,7 +55,7 @@
     content: @Composable () -> Unit,
 ) {
     ActivityTitle(title)
-    Scaffold { innerPadding ->
+    Scaffold(containerColor = MaterialTheme.colorScheme.settingsBackground) { innerPadding ->
         BoxWithConstraints(
             Modifier
                 .padding(innerPadding)
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/theme/SettingsColorsTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/theme/SettingsColorsTest.kt
index 625663d..f3f89e0 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/theme/SettingsColorsTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/theme/SettingsColorsTest.kt
@@ -31,23 +31,22 @@
     @Test
     fun testDynamicTheme() {
         // The dynamic color could be different in different device, just check basic restrictions:
-        // 1. text color is different with background color
+        // 1. text color is different with surface color
         // 2. primary / spinner color is different with its on-item color
         val ls = dynamicLightColorScheme(context)
-        assertThat(ls.categoryTitle).isNotEqualTo(ls.background)
-        assertThat(ls.secondaryText).isNotEqualTo(ls.background)
+        assertThat(ls.categoryTitle).isNotEqualTo(ls.surface)
+        assertThat(ls.secondaryText).isNotEqualTo(ls.surface)
         assertThat(ls.primaryContainer).isNotEqualTo(ls.onPrimaryContainer)
 
         val ds = dynamicDarkColorScheme(context)
-        assertThat(ds.categoryTitle).isNotEqualTo(ds.background)
-        assertThat(ds.secondaryText).isNotEqualTo(ds.background)
+        assertThat(ds.categoryTitle).isNotEqualTo(ds.surface)
+        assertThat(ds.secondaryText).isNotEqualTo(ds.surface)
         assertThat(ds.primaryContainer).isNotEqualTo(ds.onPrimaryContainer)
     }
 
     @Test
     fun testStaticTheme() {
         val ls = lightColorScheme()
-        assertThat(ls.background).isEqualTo(Color(red = 244, green = 239, blue = 244))
         assertThat(ls.categoryTitle).isEqualTo(Color(red = 103, green = 80, blue = 164))
         assertThat(ls.surface).isEqualTo(Color(red = 255, green = 251, blue = 254))
         assertThat(ls.surfaceHeader).isEqualTo(Color(red = 230, green = 225, blue = 229))
@@ -56,7 +55,6 @@
         assertThat(ls.onPrimaryContainer).isEqualTo(Color(red = 28, green = 27, blue = 31))
 
         val ds = darkColorScheme()
-        assertThat(ds.background).isEqualTo(Color(red = 28, green = 27, blue = 31))
         assertThat(ds.categoryTitle).isEqualTo(Color(red = 234, green = 221, blue = 255))
         assertThat(ds.surface).isEqualTo(Color(red = 49, green = 48, blue = 51))
         assertThat(ds.surfaceHeader).isEqualTo(Color(red = 72, green = 70, blue = 73))
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/theme/SettingsThemeTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/theme/SettingsThemeTest.kt
index bd8a54b..ed7735e 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/theme/SettingsThemeTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/theme/SettingsThemeTest.kt
@@ -26,42 +26,35 @@
 import androidx.compose.ui.text.font.FontFamily
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.google.common.truth.Truth.assertThat
-import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.junit.MockitoJUnit
-import org.mockito.junit.MockitoRule
 import org.mockito.kotlin.any
-import org.mockito.kotlin.whenever
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.stub
 
 @RunWith(AndroidJUnit4::class)
 class SettingsThemeTest {
-    @get:Rule
-    val mockito: MockitoRule = MockitoJUnit.rule()
 
     @get:Rule
     val composeTestRule = createComposeRule()
 
-    @Mock
-    private lateinit var context: Context
+    private val resources = mock<Resources> {
+        on { getString(any()) } doReturn ""
+    }
 
-    @Mock
-    private lateinit var resources: Resources
+    private val context = mock<Context> {
+        on { resources } doReturn resources
+    }
 
     private var nextMockResId = 1
 
-    @Before
-    fun setUp() {
-        whenever(context.resources).thenReturn(resources)
-        whenever(resources.getString(any())).thenReturn("")
-    }
-
     private fun mockAndroidConfig(configName: String, configValue: String) {
-        whenever(resources.getIdentifier(configName, "string", "android"))
-            .thenReturn(nextMockResId)
-        whenever(resources.getString(nextMockResId)).thenReturn(configValue)
+        resources.stub {
+            on { getIdentifier(configName, "string", "android") } doReturn nextMockResId
+            on { getString(nextMockResId) } doReturn configValue
+        }
         nextMockResId++
     }
 
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/facade/enabled/src/com/android/systemui/scene/NotificationsShadeSceneModule.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/NotificationsShadeSceneModule.kt
new file mode 100644
index 0000000..9b736b8
--- /dev/null
+++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/NotificationsShadeSceneModule.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.scene
+
+import com.android.systemui.notifications.ui.composable.NotificationsShadeScene
+import com.android.systemui.scene.shared.model.Scene
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoSet
+
+@Module
+interface NotificationsShadeSceneModule {
+
+    @Binds @IntoSet fun notificationsShade(scene: NotificationsShadeScene): Scene
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
index d59f1f5..f73b6cd 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
@@ -28,6 +28,7 @@
 import com.android.compose.animation.scene.UserActionResult
 import com.android.systemui.bouncer.ui.BouncerDialogFactory
 import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
+import com.android.systemui.compose.modifiers.sysuiResTag
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.ui.composable.ComposableScene
@@ -39,6 +40,10 @@
         val Background = ElementKey("BouncerBackground")
         val Content = ElementKey("BouncerContent")
     }
+
+    object TestTags {
+        const val Root = "bouncer_root"
+    }
 }
 
 /** The bouncer scene displays authentication challenges like PIN, password, or pattern. */
@@ -78,7 +83,9 @@
         BouncerContent(
             viewModel,
             dialogFactory,
-            Modifier.element(Bouncer.Elements.Content).fillMaxSize()
+            Modifier.sysuiResTag(Bouncer.TestTags.Root)
+                .element(Bouncer.Elements.Content)
+                .fillMaxSize()
         )
     }
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt
index c34f2fd..2dcd0ff 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt
@@ -52,6 +52,7 @@
 import com.android.compose.PlatformIconButton
 import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel
 import com.android.systemui.common.ui.compose.SelectedUserAwareInputConnection
+import com.android.systemui.compose.modifiers.sysuiResTag
 import com.android.systemui.res.R
 
 /** UI for the input part of a password-requiring version of the bouncer. */
@@ -105,6 +106,7 @@
                 ),
             modifier =
                 modifier
+                    .sysuiResTag("bouncer_text_entry")
                     .focusRequester(focusRequester)
                     .onFocusChanged { viewModel.onTextFieldFocusChanged(it.isFocused) }
                     .drawBehind {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
index 7af8408..d7e9c10 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
@@ -52,6 +52,7 @@
 import com.android.internal.R
 import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel
 import com.android.systemui.bouncer.ui.viewmodel.PatternDotViewModel
+import com.android.systemui.compose.modifiers.sysuiResTag
 import kotlin.math.min
 import kotlin.math.pow
 import kotlin.math.sqrt
@@ -234,6 +235,7 @@
 
     Canvas(
         modifier
+            .sysuiResTag("bouncer_pattern_root")
             // Because the width also includes spacing to the left and right of the leftmost and
             // rightmost dots in the grid and because UX mocks specify the width without that
             // spacing, the actual width needs to be defined slightly bigger than the UX mock width.
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index a592aa9..79b57ca7 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -469,6 +469,8 @@
                         size = size,
                         selected = selected && !isDragging,
                         widgetConfigurator = widgetConfigurator,
+                        index = index,
+                        contentListState = contentListState
                     )
                 }
             } else {
@@ -478,6 +480,8 @@
                     viewModel = viewModel,
                     size = size,
                     selected = false,
+                    index = index,
+                    contentListState = contentListState
                 )
             }
         }
@@ -782,10 +786,21 @@
     selected: Boolean,
     modifier: Modifier = Modifier,
     widgetConfigurator: WidgetConfigurator? = null,
+    index: Int,
+    contentListState: ContentListState,
 ) {
     when (model) {
         is CommunalContentModel.WidgetContent.Widget ->
-            WidgetContent(viewModel, model, size, selected, widgetConfigurator, modifier)
+            WidgetContent(
+                viewModel,
+                model,
+                size,
+                selected,
+                widgetConfigurator,
+                modifier,
+                index,
+                contentListState
+            )
         is CommunalContentModel.WidgetPlaceholder -> HighlightedItem(modifier)
         is CommunalContentModel.WidgetContent.DisabledWidget ->
             DisabledWidgetPlaceholder(model, viewModel, modifier)
@@ -883,6 +898,8 @@
     selected: Boolean,
     widgetConfigurator: WidgetConfigurator?,
     modifier: Modifier = Modifier,
+    index: Int,
+    contentListState: ContentListState,
 ) {
     val context = LocalContext.current
     val isFocusable by viewModel.isFocusable.collectAsState(initial = false)
@@ -891,6 +908,11 @@
             model.providerInfo.loadLabel(context.packageManager).toString().trim()
         }
     val clickActionLabel = stringResource(R.string.accessibility_action_label_select_widget)
+    val removeWidgetActionLabel = stringResource(R.string.accessibility_action_label_remove_widget)
+    val placeWidgetActionLabel = stringResource(R.string.accessibility_action_label_place_widget)
+    val selectedKey by viewModel.selectedKey.collectAsState()
+    val selectedIndex =
+        selectedKey?.let { key -> contentListState.list.indexOfFirst { it.key == key } }
     Box(
         modifier =
             modifier
@@ -907,6 +929,36 @@
                     Modifier.semantics {
                         contentDescription = accessibilityLabel
                         onClick(label = clickActionLabel, action = null)
+                            val deleteAction =
+                                CustomAccessibilityAction(removeWidgetActionLabel) {
+                                    contentListState.onRemove(index)
+                                    contentListState.onSaveList()
+                                    true
+                                }
+                            val selectWidgetAction =
+                                CustomAccessibilityAction(clickActionLabel) {
+                                    val currentWidgetKey =
+                                        index?.let {
+                                            keyAtIndexIfEditable(contentListState.list, index)
+                                        }
+                                    viewModel.setSelectedKey(currentWidgetKey)
+                                    true
+                                }
+
+                            val actions = mutableListOf(deleteAction, selectWidgetAction)
+
+                            if (selectedIndex != null && selectedIndex != index) {
+                                actions.add(
+                                    CustomAccessibilityAction(placeWidgetActionLabel) {
+                                        contentListState.onMove(selectedIndex!!, index)
+                                        contentListState.onSaveList()
+                                        viewModel.setSelectedKey(null)
+                                        true
+                                    }
+                                )
+                            }
+
+                            customActions = actions
                     }
                 }
     ) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt
index 722032c..63c70c9 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt
@@ -36,6 +36,7 @@
 import com.android.compose.animation.scene.SceneScope
 import com.android.compose.animation.scene.SceneTransitionLayout
 import com.android.compose.modifiers.thenIf
+import com.android.systemui.compose.modifiers.sysuiResTag
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.keyguard.ui.composable.blueprint.ClockScenes.largeClockScene
 import com.android.systemui.keyguard.ui.composable.blueprint.ClockScenes.smallClockScene
@@ -79,7 +80,7 @@
             }
 
         SceneTransitionLayout(
-            modifier = modifier,
+            modifier = modifier.sysuiResTag("keyguard_clock_container"),
             currentScene = currentScene,
             onChangeScene = {},
             transitions = ClockTransition.defaultClockTransitions,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt
new file mode 100644
index 0000000..1f03408
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.notifications.ui.composable
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.android.compose.animation.scene.SceneScope
+import com.android.compose.animation.scene.UserAction
+import com.android.compose.animation.scene.UserActionResult
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.ui.composable.ComposableScene
+import com.android.systemui.shade.ui.composable.OverlayShade
+import com.android.systemui.shade.ui.viewmodel.NotificationsShadeSceneViewModel
+import com.android.systemui.shade.ui.viewmodel.OverlayShadeViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.flow.StateFlow
+
+@SysUISingleton
+class NotificationsShadeScene
+@Inject
+constructor(
+    viewModel: NotificationsShadeSceneViewModel,
+    private val overlayShadeViewModel: OverlayShadeViewModel,
+) : ComposableScene {
+
+    override val key = Scenes.NotificationsShade
+
+    override val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> =
+        viewModel.destinationScenes
+
+    @Composable
+    override fun SceneScope.Content(
+        modifier: Modifier,
+    ) {
+        OverlayShade(
+            viewModel = overlayShadeViewModel,
+            modifier = modifier,
+            horizontalArrangement = Arrangement.Start,
+        ) {
+            Text(
+                text = "Notifications list",
+                modifier = Modifier.padding(NotificationsShade.Dimensions.Padding)
+            )
+        }
+    }
+}
+
+object NotificationsShade {
+    object Dimensions {
+        val Padding = 16.dp
+    }
+}
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/ShadeHeader.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
index d3b3d15..0bd38a1 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
@@ -63,6 +63,7 @@
 import com.android.systemui.battery.BatteryMeterViewController
 import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation
 import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout
+import com.android.systemui.compose.modifiers.sysuiResTag
 import com.android.systemui.privacy.OngoingPrivacyChip
 import com.android.systemui.res.R
 import com.android.systemui.scene.shared.model.Scenes
@@ -100,6 +101,10 @@
         val ColorScheme.shadeHeaderText: Color
             get() = Color.White
     }
+
+    object TestTags {
+        const val Root = "shade_header_root"
+    }
 }
 
 @Composable
@@ -131,7 +136,7 @@
     // This layout assumes it is globally positioned at (0, 0) and is the
     // same size as the screen.
     Layout(
-        modifier = modifier,
+        modifier = modifier.sysuiResTag(ShadeHeader.TestTags.Root),
         contents =
             listOf(
                 {
@@ -261,7 +266,7 @@
 
     val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsState()
 
-    Box(modifier = modifier) {
+    Box(modifier = modifier.sysuiResTag(ShadeHeader.TestTags.Root)) {
         if (isPrivacyChipVisible) {
             Box(modifier = Modifier.height(CollapsedHeight).fillMaxWidth()) {
                 PrivacyChip(
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/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
index dfc04ff..456fb79 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
@@ -57,6 +57,8 @@
 import com.android.systemui.flags.fakeFeatureFlagsClassic
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.activityStarter
@@ -1090,6 +1092,78 @@
                 .isEqualTo(USER_INFO_WORK.id)
         }
 
+    @Test
+    fun showCommunalFromOccluded_enteredOccludedFromHub() =
+        testScope.runTest {
+            kosmos.setCommunalAvailable(true)
+            val showCommunalFromOccluded by collectLastValue(underTest.showCommunalFromOccluded)
+            assertThat(showCommunalFromOccluded).isFalse()
+
+            kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.GLANCEABLE_HUB,
+                to = KeyguardState.OCCLUDED,
+                testScope
+            )
+
+            assertThat(showCommunalFromOccluded).isTrue()
+        }
+
+    @Test
+    fun showCommunalFromOccluded_enteredOccludedFromLockscreen() =
+        testScope.runTest {
+            kosmos.setCommunalAvailable(true)
+            val showCommunalFromOccluded by collectLastValue(underTest.showCommunalFromOccluded)
+            assertThat(showCommunalFromOccluded).isFalse()
+
+            kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.OCCLUDED,
+                testScope
+            )
+
+            assertThat(showCommunalFromOccluded).isFalse()
+        }
+
+    @Test
+    fun showCommunalFromOccluded_communalBecomesUnavailableWhileOccluded() =
+        testScope.runTest {
+            kosmos.setCommunalAvailable(true)
+            val showCommunalFromOccluded by collectLastValue(underTest.showCommunalFromOccluded)
+            assertThat(showCommunalFromOccluded).isFalse()
+
+            kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.GLANCEABLE_HUB,
+                to = KeyguardState.OCCLUDED,
+                testScope
+            )
+            runCurrent()
+            kosmos.setCommunalAvailable(false)
+
+            assertThat(showCommunalFromOccluded).isFalse()
+        }
+
+    @Test
+    fun showCommunalFromOccluded_showBouncerWhileOccluded() =
+        testScope.runTest {
+            kosmos.setCommunalAvailable(true)
+            val showCommunalFromOccluded by collectLastValue(underTest.showCommunalFromOccluded)
+            assertThat(showCommunalFromOccluded).isFalse()
+
+            kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.GLANCEABLE_HUB,
+                to = KeyguardState.OCCLUDED,
+                testScope
+            )
+            runCurrent()
+            kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.OCCLUDED,
+                to = KeyguardState.PRIMARY_BOUNCER,
+                testScope
+            )
+
+            assertThat(showCommunalFromOccluded).isTrue()
+        }
+
     private fun smartspaceTimer(id: String, timestamp: Long = 0L): SmartspaceTarget {
         val timer = mock(SmartspaceTarget::class.java)
         whenever(timer.smartspaceTargetId).thenReturn(id)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/FakeQSTile.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/FakeQSTile.kt
index 7e0e7d1..302ac35 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/FakeQSTile.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/FakeQSTile.kt
@@ -16,8 +16,8 @@
 
 package com.android.systemui.qs.pipeline.domain.interactor
 
-import android.view.View
 import com.android.internal.logging.InstanceId
+import com.android.systemui.animation.Expandable
 import com.android.systemui.plugins.qs.QSTile
 
 class FakeQSTile(
@@ -56,11 +56,11 @@
         callbacks.clear()
     }
 
-    override fun click(view: View?) {}
+    override fun click(expandable: Expandable?) {}
 
-    override fun secondaryClick(view: View?) {}
+    override fun secondaryClick(expandable: Expandable?) {}
 
-    override fun longClick(view: View?) {}
+    override fun longClick(expandable: Expandable?) {}
 
     override fun userSwitch(currentUser: Int) {
         user = currentUser
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingUserActionInteractorTest.kt
index 182a604..d309554 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingUserActionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingUserActionInteractorTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.qs.tiles.impl.fontscaling.domain.interactor
 
+import android.content.Context
 import android.provider.Settings
 import android.view.View
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -23,6 +24,8 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.accessibility.fontscaling.FontScalingDialogDelegate
 import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.animation.Expandable
+import com.android.systemui.animation.LaunchableView
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.plugins.ActivityStarter
@@ -36,7 +39,6 @@
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.nullable
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth
 import kotlinx.coroutines.test.runTest
@@ -63,6 +65,8 @@
     @Mock private lateinit var mDialogTransitionAnimator: DialogTransitionAnimator
     @Mock private lateinit var dialog: SystemUIDialog
     @Mock private lateinit var activityStarter: ActivityStarter
+    @Mock private lateinit var expandable: Expandable
+    @Mock private lateinit var controller: DialogTransitionAnimator.Controller
 
     @Captor private lateinit var argumentCaptor: ArgumentCaptor<Runnable>
 
@@ -73,6 +77,9 @@
         dialog = mock<SystemUIDialog>()
         fontScalingDialogDelegate =
             mock<FontScalingDialogDelegate> { whenever(createDialog()).thenReturn(dialog) }
+        controller = mock<DialogTransitionAnimator.Controller>()
+        expandable =
+            mock<Expandable> { whenever(dialogTransitionController(any())).thenReturn(controller) }
         argumentCaptor = ArgumentCaptor.forClass(Runnable::class.java)
 
         underTest =
@@ -90,9 +97,8 @@
     fun clickTile_screenUnlocked_showDialogAnimationFromView() =
         kosmos.testScope.runTest {
             keyguardStateController.isShowing = false
-            val testView = View(context)
 
-            underTest.handleInput(click(FontScalingTileModel, view = testView))
+            underTest.handleInput(click(FontScalingTileModel, expandable = expandable))
 
             verify(activityStarter)
                 .executeRunnableDismissingKeyguard(
@@ -103,17 +109,15 @@
                     eq(false)
                 )
             argumentCaptor.value.run()
-            verify(mDialogTransitionAnimator)
-                .showFromView(any(), eq(testView), nullable(), anyBoolean())
+            verify(mDialogTransitionAnimator).show(any(), any(), anyBoolean())
         }
 
     @Test
     fun clickTile_onLockScreen_neverShowDialogAnimationFromView_butShowsDialog() =
         kosmos.testScope.runTest {
             keyguardStateController.isShowing = true
-            val testView = View(context)
 
-            underTest.handleInput(click(FontScalingTileModel, view = testView))
+            underTest.handleInput(click(FontScalingTileModel, expandable = expandable))
 
             verify(activityStarter)
                 .executeRunnableDismissingKeyguard(
@@ -124,8 +128,7 @@
                     eq(false)
                 )
             argumentCaptor.value.run()
-            verify(mDialogTransitionAnimator, never())
-                .showFromView(any(), eq(testView), nullable(), anyBoolean())
+            verify(mDialogTransitionAnimator, never()).show(any(), any(), anyBoolean())
             verify(dialog).show()
         }
 
@@ -140,4 +143,8 @@
             val expectedIntentAction = Settings.ACTION_TEXT_READING_SETTINGS
             Truth.assertThat(actualIntentAction).isEqualTo(expectedIntentAction)
         }
+
+    private class FontScalingTileTestView(context: Context) : View(context), LaunchableView {
+        override fun setShouldBlockVisibilityChanges(block: Boolean) {}
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractorTest.kt
index b9321d5..91f4ea8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractorTest.kt
@@ -18,12 +18,11 @@
 
 import android.app.Dialog
 import android.os.UserHandle
-import android.view.View
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.DialogTransitionAnimator
-import com.android.systemui.animation.dialogTransitionAnimator
+import com.android.systemui.animation.Expandable
 import com.android.systemui.flags.featureFlagsClassic
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
@@ -138,12 +137,17 @@
      */
     @Test
     fun handleClickFromView_whenDoingNothing_whenKeyguardNotShowing_showDialogFromView() = runTest {
-        val view = mock<View>()
+        val expandable = mock<Expandable>()
+        val controller = mock<DialogTransitionAnimator.Controller>()
+        whenever(expandable.dialogTransitionController(any())).thenReturn(controller)
+
         kosmos.fakeKeyguardRepository.setKeyguardShowing(false)
 
         val recordingModel = ScreenRecordTileModel.DoingNothing
 
-        underTest.handleInput(QSTileInputTestKtx.click(recordingModel, UserHandle.CURRENT, view))
+        underTest.handleInput(
+            QSTileInputTestKtx.click(recordingModel, UserHandle.CURRENT, expandable)
+        )
         val onStartRecordingClickedCaptor = argumentCaptor<Runnable>()
         verify(recordingController)
             .createScreenRecordDialog(
@@ -158,6 +162,6 @@
         verify(keyguardDismissUtil)
             .executeWhenUnlocked(onDismissActionCaptor.capture(), eq(false), eq(true))
         onDismissActionCaptor.value.onDismiss()
-        verify(dialogTransitionAnimator).showFromView(eq(dialog), eq(view), any(), eq(true))
+        verify(dialogTransitionAnimator).show(eq(dialog), eq(controller), eq(true))
     }
 }
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/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt
index 6e7e402..44c9695 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.shade.domain.startable
 
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.FlagsParameterization
 import androidx.test.filters.SmallTest
 import com.android.compose.animation.scene.ObservableTransitionState
@@ -37,6 +39,7 @@
 import com.android.systemui.shade.ShadeExpansionChangeEvent
 import com.android.systemui.shade.ShadeExpansionListener
 import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.shade.shared.flag.DualShade
 import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.any
@@ -49,7 +52,6 @@
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
-import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import platform.test.runner.parameterized.ParameterizedAndroidJunit4
@@ -67,7 +69,7 @@
     private val fakeConfigurationRepository by lazy { kosmos.fakeConfigurationRepository }
     private val fakeSceneDataSource by lazy { kosmos.fakeSceneDataSource }
 
-    private lateinit var underTest: ShadeStartable
+    private val underTest: ShadeStartable = kosmos.shadeStartable
 
     companion object {
         @JvmStatic
@@ -81,13 +83,9 @@
         mSetFlagsRule.setFlagsParameterization(flags!!)
     }
 
-    @Before
-    fun setup() {
-        underTest = kosmos.shadeStartable
-    }
-
     @Test
-    fun hydrateShadeMode() =
+    @DisableFlags(DualShade.FLAG_NAME)
+    fun hydrateShadeMode_dualShadeDisabled() =
         testScope.runTest {
             overrideResource(R.bool.config_use_split_notification_shade, false)
             val shadeMode by collectLastValue(shadeInteractor.shadeMode)
@@ -105,6 +103,25 @@
         }
 
     @Test
+    @EnableFlags(DualShade.FLAG_NAME)
+    fun hydrateShadeMode_dualShadeEnabled() =
+        testScope.runTest {
+            overrideResource(R.bool.config_use_split_notification_shade, false)
+            val shadeMode by collectLastValue(shadeInteractor.shadeMode)
+
+            underTest.start()
+            assertThat(shadeMode).isEqualTo(ShadeMode.Dual)
+
+            overrideResource(R.bool.config_use_split_notification_shade, true)
+            fakeConfigurationRepository.onAnyConfigurationChange()
+            assertThat(shadeMode).isEqualTo(ShadeMode.Dual)
+
+            overrideResource(R.bool.config_use_split_notification_shade, false)
+            fakeConfigurationRepository.onAnyConfigurationChange()
+            assertThat(shadeMode).isEqualTo(ShadeMode.Dual)
+        }
+
+    @Test
     @EnableSceneContainer
     fun hydrateShadeExpansionStateManager() =
         testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt
new file mode 100644
index 0000000..abc684c
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt
@@ -0,0 +1,123 @@
+/*
+ * 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.shade.ui.viewmodel
+
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.Swipe
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
+import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
+import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper
+@EnableSceneContainer
+class NotificationsShadeSceneViewModelTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val sceneInteractor = kosmos.sceneInteractor
+    private val deviceUnlockedInteractor = kosmos.deviceUnlockedInteractor
+
+    private val underTest = kosmos.notificationsShadeSceneViewModel
+
+    @Test
+    fun upTransitionSceneKey_deviceLocked_lockscreen() =
+        testScope.runTest {
+            val destinationScenes by collectLastValue(underTest.destinationScenes)
+            lockDevice()
+
+            assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(Scenes.Lockscreen)
+        }
+
+    @Test
+    fun upTransitionSceneKey_deviceUnlocked_gone() =
+        testScope.runTest {
+            val destinationScenes by collectLastValue(underTest.destinationScenes)
+            lockDevice()
+            unlockDevice()
+
+            assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(Scenes.Gone)
+        }
+
+    @Test
+    fun upTransitionSceneKey_authMethodSwipe_lockscreenNotDismissed_goesToLockscreen() =
+        testScope.runTest {
+            val destinationScenes by collectLastValue(underTest.destinationScenes)
+            kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true)
+            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.None
+            )
+            sceneInteractor.changeScene(Scenes.Lockscreen, "reason")
+
+            assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(Scenes.Lockscreen)
+        }
+
+    @Test
+    fun upTransitionSceneKey_authMethodSwipe_lockscreenDismissed_goesToGone() =
+        testScope.runTest {
+            val destinationScenes by collectLastValue(underTest.destinationScenes)
+            kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true)
+            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.None
+            )
+            runCurrent()
+            sceneInteractor.changeScene(Scenes.Gone, "reason")
+
+            assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(Scenes.Gone)
+        }
+
+    private fun TestScope.lockDevice() {
+        val deviceUnlockStatus by collectLastValue(deviceUnlockedInteractor.deviceUnlockStatus)
+
+        kosmos.fakeAuthenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
+        assertThat(deviceUnlockStatus?.isUnlocked).isFalse()
+        sceneInteractor.changeScene(Scenes.Lockscreen, "reason")
+        runCurrent()
+    }
+
+    private fun TestScope.unlockDevice() {
+        val deviceUnlockStatus by collectLastValue(deviceUnlockedInteractor.deviceUnlockStatus)
+
+        kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+            SuccessFingerprintAuthenticationStatus(0, true)
+        )
+        assertThat(deviceUnlockStatus?.isUnlocked).isTrue()
+        sceneInteractor.changeScene(Scenes.Gone, "reason")
+        runCurrent()
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/wmshell/WMShellTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/wmshell/WMShellTest.kt
index 55e46dc..e1be6b0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/wmshell/WMShellTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/wmshell/WMShellTest.kt
@@ -24,16 +24,17 @@
 import com.android.keyguard.keyguardUpdateMonitor
 import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.domain.interactor.setCommunalAvailable
 import com.android.systemui.communal.ui.viewmodel.communalTransitionViewModel
 import com.android.systemui.communal.util.fakeCommunalColors
 import com.android.systemui.concurrency.fakeExecutor
-import com.android.systemui.dock.DockManager
-import com.android.systemui.dock.fakeDockManager
 import com.android.systemui.flags.Flags.COMMUNAL_SERVICE_ENABLED
 import com.android.systemui.flags.fakeFeatureFlagsClassic
 import com.android.systemui.keyguard.ScreenLifecycle
 import com.android.systemui.keyguard.WakefulnessLifecycle
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.wakefulnessLifecycle
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.model.SysUiState
@@ -63,7 +64,6 @@
 import java.util.Optional
 import java.util.concurrent.Executor
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -186,29 +186,18 @@
             verify(mRecentTasks).setTransitionBackgroundColor(null)
             verify(mRecentTasks, never()).setTransitionBackgroundColor(black)
 
-            setDocked(true)
-            // Make communal available
-            kosmos.fakeKeyguardRepository.setIsEncryptedOrLockdown(false)
-            kosmos.fakeUserRepository.setSelectedUserInfo(MAIN_USER_INFO)
-            kosmos.fakeKeyguardRepository.setKeyguardShowing(true)
-
+            // Transition to occluded from the glanceable hub.
+            kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.GLANCEABLE_HUB,
+                to = KeyguardState.OCCLUDED,
+                testScope
+            )
+            kosmos.setCommunalAvailable(true)
             runCurrent()
 
             verify(mRecentTasks).setTransitionBackgroundColor(black)
         }
 
-    private fun TestScope.setDocked(docked: Boolean) {
-        kosmos.fakeDockManager.setIsDocked(docked)
-        val event =
-            if (docked) {
-                DockManager.STATE_DOCKED
-            } else {
-                DockManager.STATE_NONE
-            }
-        kosmos.fakeDockManager.setDockEvent(event)
-        runCurrent()
-    }
-
     private companion object {
         val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN)
     }
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
index c9e2989..d13c750 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
@@ -21,11 +21,11 @@
 import android.metrics.LogMaker;
 import android.service.quicksettings.Tile;
 import android.text.TextUtils;
-import android.view.View;
 
 import androidx.annotation.Nullable;
 
 import com.android.internal.logging.InstanceId;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.plugins.annotations.DependsOn;
 import com.android.systemui.plugins.annotations.ProvidesInterface;
 import com.android.systemui.plugins.qs.QSTile.Callback;
@@ -58,23 +58,23 @@
     /**
      * The tile was clicked.
      *
-     * @param view The view that was clicked.
+     * @param expandable {@link Expandable} that was clicked.
      */
-    void click(@Nullable View view);
+    void click(@Nullable Expandable expandable);
 
     /**
      * The tile secondary click was triggered.
      *
-     * @param view The view that was clicked.
+     * @param expandable {@link Expandable} that was clicked.
      */
-    void secondaryClick(@Nullable View view);
+    void secondaryClick(@Nullable Expandable expandable);
 
     /**
      * The tile was long clicked.
      *
-     * @param view The view that was clicked.
+     * @param expandable {@link Expandable} that was clicked.
      */
-    void longClick(@Nullable View view);
+    void longClick(@Nullable Expandable expandable);
 
     void userSwitch(int currentUser);
 
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index dcdd4f0..45bcd82 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1186,6 +1186,10 @@
     <string name="accessibility_content_description_for_communal_hub">Widgets on lock screen</string>
     <!-- Label for accessibility action to select a widget in edit mode. [CHAR LIMIT=NONE] -->
     <string name="accessibility_action_label_select_widget">select widget</string>
+    <!-- Label for accessibility action to remove a widget in edit mode. [CHAR LIMIT=NONE] -->
+    <string name="accessibility_action_label_remove_widget">remove widget</string>
+    <!-- Label for accessibility action to place a widget in edit mode after selecting move widget. [CHAR LIMIT=NONE] -->
+    <string name="accessibility_action_label_place_widget">place selected widget</string>
 
     <!-- Related to user switcher --><skip/>
 
diff --git a/packages/SystemUI/shared/keyguard/src/com/android/keyguard/BasePasswordTextView.java b/packages/SystemUI/shared/keyguard/src/com/android/keyguard/BasePasswordTextView.java
index fe12134..863f0c3 100644
--- a/packages/SystemUI/shared/keyguard/src/com/android/keyguard/BasePasswordTextView.java
+++ b/packages/SystemUI/shared/keyguard/src/com/android/keyguard/BasePasswordTextView.java
@@ -231,7 +231,7 @@
         info.setClassName(EditText.class.getName());
         info.setPassword(true);
         info.setText(getTransformedText());
-
+        info.setSelected(false);
         info.setEditable(true);
 
         info.setInputType(InputType.TYPE_NUMBER_VARIATION_PASSWORD);
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManager.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManager.java
index 623b40f..14e5f34 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManager.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManager.java
@@ -18,7 +18,6 @@
 
 import android.bluetooth.BluetoothDevice;
 import android.util.Log;
-import android.view.View;
 
 import androidx.annotation.Nullable;
 
@@ -26,6 +25,7 @@
 import com.android.settingslib.bluetooth.LocalBluetoothManager;
 import com.android.systemui.animation.DialogCuj;
 import com.android.systemui.animation.DialogTransitionAnimator;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
 
@@ -58,9 +58,9 @@
     /**
      * Shows the dialog.
      *
-     * @param view The view from which the dialog is shown.
+     * @param expandable {@link Expandable} from which the dialog is shown.
      */
-    public void showDialog(View view) {
+    public void showDialog(Expandable expandable) {
         if (mDialog != null) {
             if (DEBUG) {
                 Log.d(TAG, "HearingDevicesDialog already showing. Destroy it first.");
@@ -70,13 +70,17 @@
 
         mDialog = mDialogFactory.create(!isAnyBondedHearingDevice()).createDialog();
 
-        if (view != null) {
-            mDialogTransitionAnimator.showFromView(mDialog, view,
+        if (expandable != null) {
+            DialogTransitionAnimator.Controller controller = expandable.dialogTransitionController(
                     new DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
-                            INTERACTION_JANK_TAG), /* animateBackgroundBoundsChange= */ true);
-        } else {
-            mDialog.show();
+                            INTERACTION_JANK_TAG));
+            if (controller != null) {
+                mDialogTransitionAnimator.show(mDialog,
+                        controller, /* animateBackgroundBoundsChange= */ true);
+                return;
+            }
         }
+        mDialog.show();
     }
 
     private void destroyDialog() {
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
index eb919e3..4369f3f 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
@@ -32,6 +32,7 @@
 import com.android.systemui.Prefs
 import com.android.systemui.animation.DialogCuj
 import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.animation.Expandable
 import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_AUDIO_SHARING
 import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_BLUETOOTH_DEVICE_DETAILS
 import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_PAIR_NEW_DEVICE
@@ -82,7 +83,7 @@
      * @param view The view from which the dialog is shown.
      */
     @kotlinx.coroutines.ExperimentalCoroutinesApi
-    fun showDialog(view: View?) {
+    fun showDialog(expandable: Expandable?) {
         cancelJob()
 
         job =
@@ -93,17 +94,15 @@
                 val dialog = dialogDelegate.createDialog()
                 val context = dialog.context
 
-                view?.let {
-                    dialogTransitionAnimator.showFromView(
-                        dialog,
-                        it,
-                        animateBackgroundBoundsChange = true,
-                        cuj =
-                            DialogCuj(
-                                InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
-                                INTERACTION_JANK_TAG
-                            )
+                val controller =
+                    expandable?.dialogTransitionController(
+                        DialogCuj(
+                            InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
+                            INTERACTION_JANK_TAG
+                        )
                     )
+                controller?.let {
+                    dialogTransitionAnimator.show(dialog, it, animateBackgroundBoundsChange = true)
                 }
                     ?: dialog.show()
 
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/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
index 5cf68b7..0042915 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
@@ -44,9 +44,10 @@
 import com.android.systemui.communal.widgets.WidgetConfigurator
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dock.DockManager
-import com.android.systemui.dock.retrieveIsDocked
+import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.Logger
 import com.android.systemui.log.dagger.CommunalLog
@@ -64,6 +65,7 @@
 import com.android.systemui.util.kotlin.BooleanFlowOperators.or
 import com.android.systemui.util.kotlin.emitOnStart
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.channels.BufferOverflow
@@ -77,9 +79,11 @@
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.shareIn
@@ -92,6 +96,7 @@
 @Inject
 constructor(
     @Application val applicationScope: CoroutineScope,
+    @Background val bgDispatcher: CoroutineDispatcher,
     broadcastDispatcher: BroadcastDispatcher,
     private val communalRepository: CommunalRepository,
     private val widgetRepository: CommunalWidgetRepository,
@@ -99,13 +104,13 @@
     mediaRepository: CommunalMediaRepository,
     smartspaceRepository: SmartspaceRepository,
     keyguardInteractor: KeyguardInteractor,
-    private val communalSettingsInteractor: CommunalSettingsInteractor,
+    keyguardTransitionInteractor: KeyguardTransitionInteractor,
+    communalSettingsInteractor: CommunalSettingsInteractor,
     private val appWidgetHost: CommunalAppWidgetHost,
     private val editWidgetsActivityStarter: EditWidgetsActivityStarter,
     private val userTracker: UserTracker,
     private val activityStarter: ActivityStarter,
     private val userManager: UserManager,
-    private val dockManager: DockManager,
     sceneInteractor: SceneInteractor,
     @CommunalLog logBuffer: LogBuffer,
     @CommunalTableLog tableLogBuffer: TableLogBuffer,
@@ -145,8 +150,18 @@
                 replay = 1,
             )
 
-    /** Whether to show communal by default */
-    val showByDefault: Flow<Boolean> = and(isCommunalAvailable, dockManager.retrieveIsDocked())
+    /** Whether to show communal when exiting the occluded state. */
+    val showCommunalFromOccluded: Flow<Boolean> =
+        keyguardTransitionInteractor.startedKeyguardTransitionStep
+            .filter { step -> step.to == KeyguardState.OCCLUDED }
+            .combine(isCommunalAvailable, ::Pair)
+            .map { (step, available) -> available && step.from == KeyguardState.GLANCEABLE_HUB }
+            .flowOn(bgDispatcher)
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = false,
+            )
 
     /**
      * Target scene as requested by the underlying [SceneTransitionLayout] or through [changeScene].
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModel.kt
index 1bee83b..337d873 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModel.kt
@@ -63,8 +63,8 @@
             )
             .distinctUntilChanged()
 
-    /** Whether to show communal by default */
-    val showByDefault: Flow<Boolean> = communalInteractor.showByDefault
+    /** Whether to show communal when exiting the occluded state. */
+    val showCommunalFromOccluded: Flow<Boolean> = communalInteractor.showCommunalFromOccluded
 
     val transitionFromOccludedEnded =
         keyguardTransitionInteractor.transitionStepsFromState(KeyguardState.OCCLUDED).filter { step
@@ -74,8 +74,11 @@
         }
 
     val recentsBackgroundColor: Flow<Color?> =
-        combine(showByDefault, communalColors.backgroundColor) { showByDefault, backgroundColor ->
-            if (showByDefault) {
+        combine(showCommunalFromOccluded, communalColors.backgroundColor) {
+            showCommunalFromOccluded,
+            backgroundColor,
+            ->
+            if (showCommunalFromOccluded) {
                 backgroundColor
             } else {
                 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/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 6ef39f3..d6fd354 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -177,10 +177,6 @@
 import com.android.systemui.wallpapers.data.repository.WallpaperRepository;
 import com.android.wm.shell.keyguard.KeyguardTransitions;
 
-import dagger.Lazy;
-
-import kotlinx.coroutines.CoroutineDispatcher;
-
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -190,6 +186,9 @@
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 
+import dagger.Lazy;
+import kotlinx.coroutines.CoroutineDispatcher;
+
 /**
  * Mediates requests related to the keyguard.  This includes queries about the
  * state of the keyguard, power management events that effect whether the keyguard
@@ -1234,7 +1233,7 @@
                             mUnoccludeAnimator.cancel();
                         }
 
-                        if (isDream || mShowCommunalByDefault) {
+                        if (isDream || mShowCommunalWhenUnoccluding) {
                             initAlphaForAnimationTargets(wallpapers);
                             if (isDream) {
                                 mDreamViewModel.get().startTransitionFromDream();
@@ -1372,7 +1371,7 @@
     private final Lazy<DreamViewModel> mDreamViewModel;
     private final Lazy<CommunalTransitionViewModel> mCommunalTransitionViewModel;
     private RemoteAnimationTarget mRemoteAnimationTarget;
-    private boolean mShowCommunalByDefault = false;
+    private boolean mShowCommunalWhenUnoccluding = false;
 
     private final Lazy<WindowManagerLockscreenVisibilityManager> mWmLockscreenVisibilityManager;
 
@@ -1630,8 +1629,10 @@
                     getRemoteSurfaceAlphaApplier());
             mJavaAdapter.alwaysCollectFlow(dreamViewModel.getTransitionEnded(),
                     getFinishedCallbackConsumer());
-            mJavaAdapter.alwaysCollectFlow(communalViewModel.getShowByDefault(),
-                    (showByDefault) -> mShowCommunalByDefault = showByDefault);
+            mJavaAdapter.alwaysCollectFlow(communalViewModel.getShowCommunalFromOccluded(),
+                    (showCommunalFromOccluded) -> {
+                        mShowCommunalWhenUnoccluding = showCommunalFromOccluded;
+                    });
             mJavaAdapter.alwaysCollectFlow(communalViewModel.getTransitionFromOccludedEnded(),
                     getFinishedCallbackConsumer());
         }
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/FromOccludedTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
index ee589f4..e51ba83 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
@@ -87,12 +87,15 @@
             scope.launch {
                 keyguardOcclusionInteractor.isShowWhenLockedActivityOnTop
                     .filterRelevantKeyguardStateAnd { onTop -> !onTop }
-                    .sample(communalInteractor.isIdleOnCommunal, communalInteractor.showByDefault)
-                    .collect { (_, isIdleOnCommunal, showCommunalByDefault) ->
+                    .sample(
+                        communalInteractor.isIdleOnCommunal,
+                        communalInteractor.showCommunalFromOccluded,
+                    )
+                    .collect { (_, isIdleOnCommunal, showCommunalFromOccluded) ->
                         // Occlusion signals come from the framework, and should interrupt any
                         // existing transition
                         val to =
-                            if (isIdleOnCommunal || showCommunalByDefault) {
+                            if (isIdleOnCommunal || showCommunalFromOccluded) {
                                 KeyguardState.GLANCEABLE_HUB
                             } else {
                                 KeyguardState.LOCKSCREEN
@@ -106,16 +109,16 @@
                     .sample(
                         keyguardInteractor.isKeyguardShowing,
                         communalInteractor.isIdleOnCommunal,
-                        communalInteractor.showByDefault,
+                        communalInteractor.showCommunalFromOccluded,
                     )
                     .filterRelevantKeyguardStateAnd { (isOccluded, isShowing, _, _) ->
                         !isOccluded && isShowing
                     }
-                    .collect { (_, _, isIdleOnCommunal, showCommunalByDefault) ->
+                    .collect { (_, _, isIdleOnCommunal, showCommunalFromOccluded) ->
                         // Occlusion signals come from the framework, and should interrupt any
                         // existing transition
                         val to =
-                            if (isIdleOnCommunal || showCommunalByDefault) {
+                            if (isIdleOnCommunal || showCommunalFromOccluded) {
                                 KeyguardState.GLANCEABLE_HUB
                             } else {
                                 KeyguardState.LOCKSCREEN
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/BouncerToGoneFlows.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlows.kt
index e0a3af6..570f377 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlows.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlows.kt
@@ -102,37 +102,40 @@
                 to = GONE,
             )
 
-        return shadeInteractor.isAnyExpanded.flatMapLatest { isAnyExpanded ->
-            transitionAnimation
-                .sharedFlow(
-                    duration = duration,
-                    interpolator = EMPHASIZED_ACCELERATE,
-                    onStart = {
-                        leaveShadeOpen = statusBarStateController.leaveOpenOnKeyguardHide()
-                        willRunDismissFromKeyguard = willRunAnimationOnKeyguard()
-                        isShadeExpanded = isAnyExpanded
-                    },
-                    onStep = { 1f - it },
-                )
-                .map {
-                    if (willRunDismissFromKeyguard) {
-                        if (isShadeExpanded) {
+        return shadeInteractor.anyExpansion
+            .map { it > 0f }
+            .distinctUntilChanged()
+            .flatMapLatest { isAnyExpanded ->
+                transitionAnimation
+                    .sharedFlow(
+                        duration = duration,
+                        interpolator = EMPHASIZED_ACCELERATE,
+                        onStart = {
+                            leaveShadeOpen = statusBarStateController.leaveOpenOnKeyguardHide()
+                            willRunDismissFromKeyguard = willRunAnimationOnKeyguard()
+                            isShadeExpanded = isAnyExpanded
+                        },
+                        onStep = { 1f - it },
+                    )
+                    .map {
+                        if (willRunDismissFromKeyguard) {
+                            if (isShadeExpanded) {
+                                ScrimAlpha(
+                                    behindAlpha = it,
+                                    notificationsAlpha = it,
+                                )
+                            } else {
+                                ScrimAlpha()
+                            }
+                        } else if (leaveShadeOpen) {
                             ScrimAlpha(
-                                behindAlpha = it,
-                                notificationsAlpha = it,
+                                behindAlpha = 1f,
+                                notificationsAlpha = 1f,
                             )
                         } else {
-                            ScrimAlpha()
+                            ScrimAlpha(behindAlpha = it)
                         }
-                    } else if (leaveShadeOpen) {
-                        ScrimAlpha(
-                            behindAlpha = 1f,
-                            notificationsAlpha = 1f,
-                        )
-                    } else {
-                        ScrimAlpha(behindAlpha = it)
                     }
-                }
-        }
+            }
     }
 }
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/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt
index d4c8456e..2c1e75e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt
@@ -88,12 +88,11 @@
         isCommunalAvailable: Boolean,
         shadeMode: ShadeMode,
     ): Map<UserAction, UserActionResult> {
+        val shadeSceneKey =
+            if (shadeMode is ShadeMode.Dual) Scenes.NotificationsShade else Scenes.Shade
+
         val quickSettingsIfSingleShade =
-            if (shadeMode is ShadeMode.Single) {
-                Scenes.QuickSettings
-            } else {
-                Scenes.Shade
-            }
+            if (shadeMode is ShadeMode.Single) Scenes.QuickSettings else shadeSceneKey
 
         return mapOf(
                 Swipe.Left to UserActionResult(Scenes.Communal).takeIf { isCommunalAvailable },
@@ -104,8 +103,8 @@
                 swipeDownFromTop(pointerCount = 2) to quickSettingsIfSingleShade,
 
                 // Swiping down, not from the edge, always navigates to the shade scene.
-                swipeDown(pointerCount = 1) to Scenes.Shade,
-                swipeDown(pointerCount = 2) to Scenes.Shade,
+                swipeDown(pointerCount = 1) to shadeSceneKey,
+                swipeDown(pointerCount = 2) to shadeSceneKey,
             )
             .filterValues { it != null }
             .mapValues { checkNotNull(it.value) }
diff --git a/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt b/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt
index d995116..0c07c05 100644
--- a/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt
+++ b/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt
@@ -81,7 +81,10 @@
         val EvaluatorByFlag =
             mapOf<Int, (SceneContainerPluginState) -> Boolean>(
                 SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE to { it.scene != Scenes.Gone },
-                SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED to { it.scene == Scenes.Shade },
+                SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED to
+                    {
+                        it.scene == Scenes.NotificationsShade || it.scene == Scenes.Shade
+                    },
                 SYSUI_STATE_QUICK_SETTINGS_EXPANDED to { it.scene == Scenes.QuickSettings },
                 SYSUI_STATE_BOUNCER_SHOWING to { it.scene == Scenes.Bouncer },
                 SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING to
diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java
index d476e63..a14479b 100644
--- a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java
+++ b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java
@@ -67,6 +67,7 @@
 import com.android.systemui.SystemUIApplication;
 import com.android.systemui.animation.DialogCuj;
 import com.android.systemui.animation.DialogTransitionAnimator;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.broadcast.BroadcastSender;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.plugins.ActivityStarter;
@@ -323,7 +324,7 @@
         // remaining estimate is disabled
         if (!mCurrentBatterySnapshot.isHybrid() || mBucket < -1
                 || mCurrentBatterySnapshot.getTimeRemainingMillis()
-                        < mCurrentBatterySnapshot.getSevereThresholdMillis()) {
+                < mCurrentBatterySnapshot.getSevereThresholdMillis()) {
             nb.setColor(Utils.getColorAttrDefaultColor(mContext, android.R.attr.colorError));
         }
 
@@ -703,17 +704,23 @@
             mSaverConfirmation = null;
             logEvent(BatteryWarningEvents.LowBatteryWarningEvent.SAVER_CONFIRM_DISMISS);
         });
-        WeakReference<View> ref = mBatteryControllerLazy.get().getLastPowerSaverStartView();
-        if (ref != null && ref.get() != null && ref.get().isAggregatedVisible()) {
-            mDialogTransitionAnimator.showFromView(d, ref.get(),
+        WeakReference<Expandable> ref =
+                mBatteryControllerLazy.get().getLastPowerSaverStartExpandable();
+        if (ref != null && ref.get() != null) {
+            DialogTransitionAnimator.Controller controller = ref.get().dialogTransitionController(
                     new DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
                             INTERACTION_JANK_TAG));
+            if (controller != null) {
+                mDialogTransitionAnimator.show(d, controller);
+            } else {
+                d.show();
+            }
         } else {
             d.show();
         }
         logEvent(BatteryWarningEvents.LowBatteryWarningEvent.SAVER_CONFIRM_DIALOG);
         mSaverConfirmation = d;
-        mBatteryControllerLazy.get().clearLastPowerSaverStartView();
+        mBatteryControllerLazy.get().clearLastPowerSaverStartExpandable();
     }
 
     @VisibleForTesting
@@ -873,4 +880,4 @@
             }
         }
     }
-}
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java
index b53c245..d26ae0a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java
@@ -41,7 +41,6 @@
 import android.text.format.DateUtils;
 import android.util.Log;
 import android.view.IWindowManager;
-import android.view.View;
 import android.view.WindowManagerGlobal;
 import android.widget.Switch;
 
@@ -52,6 +51,7 @@
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.systemui.animation.ActivityTransitionAnimator;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.ActivityStarter;
@@ -99,7 +99,7 @@
     @Nullable
     private CharSequence mDefaultLabel;
     @Nullable
-    private View mViewClicked;
+    private Expandable mExpandableClicked;
 
     private final Context mUserContext;
 
@@ -347,7 +347,7 @@
                     mService.onStartListening();
                 }
             } else {
-                mViewClicked = null;
+                mExpandableClicked = null;
                 mService.onStopListening();
                 if (mIsTokenGranted && !mIsShowingDialog) {
                     try {
@@ -409,11 +409,11 @@
     }
 
     @Override
-    protected void handleClick(@Nullable View view) {
+    protected void handleClick(@Nullable Expandable expandable) {
         if (mTile.getState() == Tile.STATE_UNAVAILABLE) {
             return;
         }
-        mViewClicked = view;
+        mExpandableClicked = expandable;
         try {
             if (DEBUG) Log.d(TAG, "Adding token");
             mWindowManager.addWindowToken(mToken, TYPE_QS_DIALOG,
@@ -541,11 +541,9 @@
             Log.i(TAG, "The activity is starting");
 
             ActivityTransitionAnimator.Controller controller =
-                    mViewClicked == null ? null :
-                    ActivityTransitionAnimator.Controller.fromView(
-                            mViewClicked,
-                            InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE
-                    );
+                    mExpandableClicked == null ? null :
+                            mExpandableClicked.activityTransitionController(
+                                    InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE);
             mActivityStarter.startPendingIntentMaybeDismissingKeyguard(
                     pendingIntent,
                     /* intentSentUiThreadCallback= */ null,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileViewModel.kt
index 8fc66d3..a6cfa75 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileViewModel.kt
@@ -16,8 +16,7 @@
 
 package com.android.systemui.qs.panels.ui.viewmodel
 
-import android.view.View
-import android.view.View.OnLongClickListener
+import com.android.systemui.animation.Expandable
 import com.android.systemui.plugins.qs.QSTile
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
@@ -26,8 +25,7 @@
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.onStart
 
-class TileViewModel(private val tile: QSTile, val spec: TileSpec) :
-    OnLongClickListener, View.OnClickListener {
+class TileViewModel(private val tile: QSTile, val spec: TileSpec) {
     val state: Flow<QSTile.State> =
         conflatedCallbackFlow {
                 val callback = QSTile.Callback { trySend(it.copy()) }
@@ -42,13 +40,12 @@
     val currentState: QSTile.State
         get() = tile.state
 
-    override fun onClick(view: View?) {
-        tile.click(view)
+    fun onClick(expandable: Expandable?) {
+        tile.click(expandable)
     }
 
-    override fun onLongClick(view: View?): Boolean {
-        tile.longClick(view)
-        return true
+    fun onLongClick(expandable: Expandable?) {
+        tile.longClick(expandable)
     }
 
     fun startListening(token: Any) = tile.setListening(token, true)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
index 1456747..c24113f1 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
@@ -42,7 +42,6 @@
 import android.util.ArraySet;
 import android.util.Log;
 import android.util.SparseArray;
-import android.view.View;
 
 import androidx.annotation.Nullable;
 import androidx.lifecycle.Lifecycle;
@@ -58,6 +57,7 @@
 import com.android.settingslib.RestrictedLockUtilsInternal;
 import com.android.systemui.Dumpable;
 import com.android.systemui.animation.ActivityTransitionAnimator;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.qs.QSTile;
@@ -137,9 +137,9 @@
      *
      * Calls to the controller should be made here to set the new state of the device.
      *
-     * @param view The view that was clicked.
+     * @param expandable {@link Expandable} that was clicked.
      */
-    protected abstract void handleClick(@Nullable View view);
+    protected abstract void handleClick(@Nullable Expandable expandable);
 
     /**
      * Update state of the tile based on device state
@@ -282,7 +282,8 @@
         mHandler.sendEmptyMessage(H.REMOVE_CALLBACKS);
     }
 
-    public void click(@Nullable View view) {
+    @Override
+    public void click(@Nullable Expandable expandable) {
         mMetricsLogger.write(populate(new LogMaker(ACTION_QS_CLICK).setType(TYPE_ACTION)
                 .addTaggedData(FIELD_STATUS_BAR_STATE,
                         mStatusBarStateController.getState())));
@@ -292,11 +293,12 @@
         mQSLogger.logTileClick(mTileSpec, mStatusBarStateController.getState(), mState.state,
                 eventId);
         if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
-            mHandler.obtainMessage(H.CLICK, eventId, 0, view).sendToTarget();
+            mHandler.obtainMessage(H.CLICK, eventId, 0, expandable).sendToTarget();
         }
     }
 
-    public void secondaryClick(@Nullable View view) {
+    @Override
+    public void secondaryClick(@Nullable Expandable expandable) {
         mMetricsLogger.write(populate(new LogMaker(ACTION_QS_SECONDARY_CLICK).setType(TYPE_ACTION)
                 .addTaggedData(FIELD_STATUS_BAR_STATE,
                         mStatusBarStateController.getState())));
@@ -305,11 +307,11 @@
         final int eventId = mClickEventId++;
         mQSLogger.logTileSecondaryClick(mTileSpec, mStatusBarStateController.getState(),
                 mState.state, eventId);
-        mHandler.obtainMessage(H.SECONDARY_CLICK, eventId, 0, view).sendToTarget();
+        mHandler.obtainMessage(H.SECONDARY_CLICK, eventId, 0, expandable).sendToTarget();
     }
 
     @Override
-    public void longClick(@Nullable View view) {
+    public void longClick(@Nullable Expandable expandable) {
         mMetricsLogger.write(populate(new LogMaker(ACTION_QS_LONG_PRESS).setType(TYPE_ACTION)
                 .addTaggedData(FIELD_STATUS_BAR_STATE,
                         mStatusBarStateController.getState())));
@@ -319,7 +321,7 @@
         mQSLogger.logTileLongClick(mTileSpec, mStatusBarStateController.getState(), mState.state,
                 eventId);
         if (!mFalsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) {
-            mHandler.obtainMessage(H.LONG_CLICK, eventId, 0, view).sendToTarget();
+            mHandler.obtainMessage(H.LONG_CLICK, eventId, 0, expandable).sendToTarget();
         }
     }
 
@@ -397,22 +399,22 @@
      *
      * Defaults to {@link QSTileImpl#handleClick}
      *
-     * @param view The view that was clicked.
+     * @param expandable {@link Expandable} that was clicked.
      */
-    protected void handleSecondaryClick(@Nullable View view) {
+    protected void handleSecondaryClick(@Nullable Expandable expandable) {
         // Default to normal click.
-        handleClick(view);
+        handleClick(expandable);
     }
 
     /**
      * Handles long click on the tile by launching the {@link Intent} defined in
      * {@link QSTileImpl#getLongClickIntent}.
      *
-     * @param view The view from which the opening window will be animated.
+     * @param expandable {@link Expandable} from which the opening window will be animated.
      */
-    protected void handleLongClick(@Nullable View view) {
+    protected void handleLongClick(@Nullable Expandable expandable) {
         ActivityTransitionAnimator.Controller animationController =
-                view != null ? ActivityTransitionAnimator.Controller.fromView(view,
+                expandable != null ? expandable.activityTransitionController(
                         InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE) : null;
         mActivityStarter.postStartActivityDismissingKeyguard(getLongClickIntent(), 0,
                 animationController);
@@ -591,16 +593,16 @@
                         mActivityStarter.postStartActivityDismissingKeyguard(intent, 0);
                     } else {
                         mQSLogger.logHandleClick(mTileSpec, msg.arg1);
-                        handleClick((View) msg.obj);
+                        handleClick((Expandable) msg.obj);
                     }
                 } else if (msg.what == SECONDARY_CLICK) {
                     name = "handleSecondaryClick";
                     mQSLogger.logHandleSecondaryClick(mTileSpec, msg.arg1);
-                    handleSecondaryClick((View) msg.obj);
+                    handleSecondaryClick((Expandable) msg.obj);
                 } else if (msg.what == LONG_CLICK) {
                     name = "handleLongClick";
                     mQSLogger.logHandleLongClick(mTileSpec, msg.arg1);
-                    handleLongClick((View) msg.obj);
+                    handleLongClick((Expandable) msg.obj);
                 } else if (msg.what == REFRESH_STATE) {
                     name = "handleRefreshState";
                     handleRefreshState(msg.obj);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
index ca5b771..f3852a2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
@@ -53,6 +53,7 @@
 import com.android.systemui.Flags
 import com.android.systemui.Flags.quickSettingsVisualHapticsLongpress
 import com.android.systemui.FontSizeUtils
+import com.android.systemui.animation.Expandable
 import com.android.systemui.animation.LaunchableView
 import com.android.systemui.animation.LaunchableViewDelegate
 import com.android.systemui.haptics.qs.QSLongPressEffect
@@ -364,10 +365,11 @@
     }
 
     override fun init(tile: QSTile) {
+        val expandable = Expandable.fromView(this)
         init(
-                { v: View? -> tile.click(this) },
-                { view: View? ->
-                    tile.longClick(this)
+                { _: View? -> tile.click(expandable) },
+                { _: View? ->
+                    tile.longClick(expandable)
                     true
                 }
         )
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/AirplaneModeTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/AirplaneModeTile.java
index 17251c3..2068799 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/AirplaneModeTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/AirplaneModeTile.java
@@ -29,13 +29,13 @@
 import android.service.quicksettings.Tile;
 import android.sysprop.TelephonyProperties;
 import android.telephony.TelephonyManager;
-import android.view.View;
 import android.widget.Switch;
 
 import androidx.annotation.Nullable;
 
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
@@ -103,7 +103,7 @@
     }
 
     @Override
-    public void handleClick(@Nullable View view) {
+    public void handleClick(@Nullable Expandable expandable) {
         boolean airplaneModeEnabled = mState.value;
         MetricsLogger.action(mContext, getMetricsCategory(), !airplaneModeEnabled);
         if (!airplaneModeEnabled && TelephonyProperties.in_ecm_mode().orElse(false)) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/AlarmTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/AlarmTile.kt
index 688f3ca..73d991f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/AlarmTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/AlarmTile.kt
@@ -9,12 +9,10 @@
 import android.service.quicksettings.Tile
 import android.text.TextUtils
 import android.text.format.DateFormat
-import android.view.View
 import androidx.annotation.VisibleForTesting
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.internal.logging.MetricsLogger
-import com.android.systemui.res.R
-import com.android.systemui.animation.ActivityTransitionAnimator
+import com.android.systemui.animation.Expandable
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.plugins.ActivityStarter
@@ -25,12 +23,15 @@
 import com.android.systemui.qs.QsEventLogger
 import com.android.systemui.qs.logging.QSLogger
 import com.android.systemui.qs.tileimpl.QSTileImpl
+import com.android.systemui.res.R
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.policy.NextAlarmController
 import java.util.Locale
 import javax.inject.Inject
 
-class AlarmTile @Inject constructor(
+class AlarmTile
+@Inject
+constructor(
     host: QSHost,
     uiEventLogger: QsEventLogger,
     @Background backgroundLooper: Looper,
@@ -56,8 +57,7 @@
 
     private var lastAlarmInfo: AlarmManager.AlarmClockInfo? = null
     private val icon = ResourceIcon.get(R.drawable.ic_alarm)
-    @VisibleForTesting
-    internal val defaultIntent = Intent(AlarmClock.ACTION_SHOW_ALARMS)
+    @VisibleForTesting internal val defaultIntent = Intent(AlarmClock.ACTION_SHOW_ALARMS)
     private val callback = NextAlarmController.NextAlarmChangeCallback { nextAlarm ->
         lastAlarmInfo = nextAlarm
         refreshState()
@@ -73,11 +73,11 @@
         }
     }
 
-    override fun handleClick(view: View?) {
-        val animationController = view?.let {
-            ActivityTransitionAnimator.Controller.fromView(
-                    it, InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE)
-        }
+    override fun handleClick(expandable: Expandable?) {
+        val animationController =
+            expandable?.activityTransitionController(
+                InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE
+            )
         val pendingIntent = lastAlarmInfo?.showIntent
         if (pendingIntent != null) {
             mActivityStarter.postStartActivityDismissingKeyguard(pendingIntent, animationController)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/BatterySaverTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/BatterySaverTile.java
index 426aa55..7c0ce4c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/BatterySaverTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/BatterySaverTile.java
@@ -21,7 +21,6 @@
 import android.provider.Settings;
 import android.provider.Settings.Secure;
 import android.service.quicksettings.Tile;
-import android.view.View;
 import android.widget.Switch;
 
 import androidx.annotation.Nullable;
@@ -29,6 +28,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.ActivityStarter;
@@ -121,7 +121,7 @@
         if (!listening) {
             // If we stopped listening, it means that the tile is not visible. In that case, we
             // don't need to save the view anymore
-            mBatteryController.clearLastPowerSaverStartView();
+            mBatteryController.clearLastPowerSaverStartExpandable();
         }
     }
 
@@ -131,11 +131,11 @@
     }
 
     @Override
-    protected void handleClick(@Nullable View view) {
+    protected void handleClick(@Nullable Expandable expandable) {
         if (getState().state == Tile.STATE_UNAVAILABLE) {
             return;
         }
-        mBatteryController.setPowerSaveMode(!mPowerSave, view);
+        mBatteryController.setPowerSaveMode(!mPowerSave, expandable);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
index 6eae32a..9af34f6 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
@@ -31,7 +31,6 @@
 import android.service.quicksettings.Tile;
 import android.text.TextUtils;
 import android.util.Log;
-import android.view.View;
 import android.widget.Switch;
 
 import com.android.internal.logging.MetricsLogger;
@@ -39,6 +38,7 @@
 import com.android.settingslib.Utils;
 import com.android.settingslib.bluetooth.BluetoothUtils;
 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogViewModel;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
@@ -109,9 +109,9 @@
     }
 
     @Override
-    protected void handleClick(@Nullable View view) {
+    protected void handleClick(@Nullable Expandable expandable) {
         if (mFeatureFlags.isEnabled(Flags.BLUETOOTH_QS_TILE_DIALOG)) {
-            mDialogViewModel.showDialog(view);
+            mDialogViewModel.showDialog(expandable);
         } else {
             // Secondary clicks are header clicks, just toggle.
             final boolean isEnabled = mState.value;
@@ -127,7 +127,7 @@
     }
 
     @Override
-    protected void handleSecondaryClick(@Nullable View view) {
+    protected void handleSecondaryClick(@Nullable Expandable expandable) {
         if (!mController.canConfigBluetooth()) {
             mActivityStarter.postStartActivityDismissingKeyguard(
                     new Intent(Settings.ACTION_BLUETOOTH_SETTINGS), 0);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java
index b27b974..169cdc1 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java
@@ -29,7 +29,6 @@
 import android.provider.Settings;
 import android.service.quicksettings.Tile;
 import android.util.Log;
-import android.view.View;
 import android.widget.Button;
 
 import androidx.annotation.Nullable;
@@ -41,6 +40,7 @@
 import com.android.systemui.animation.ActivityTransitionAnimator;
 import com.android.systemui.animation.DialogCuj;
 import com.android.systemui.animation.DialogTransitionAnimator;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.flags.FeatureFlags;
@@ -161,12 +161,12 @@
     }
 
     @Override
-    protected void handleLongClick(@Nullable View view) {
-        handleClick(view);
+    protected void handleLongClick(@Nullable Expandable expandable) {
+        handleClick(expandable);
     }
 
     @Override
-    protected void handleClick(@Nullable View view) {
+    protected void handleClick(@Nullable Expandable expandable) {
         if (getState().state == Tile.STATE_UNAVAILABLE) {
             return;
         }
@@ -174,7 +174,7 @@
         List<CastDevice> activeDevices = getActiveDevices();
         if (willPopDialog()) {
             if (!mKeyguard.isShowing()) {
-                showDialog(view);
+                showDialog(expandable);
             } else {
                 mActivityStarter.postQSRunnableDismissingKeyguard(() -> {
                     // Dismissing the keyguard will collapse the shade, so we don't animate from the
@@ -216,7 +216,7 @@
         }
     }
 
-    private void showDialog(@Nullable View view) {
+    private void showDialog(@Nullable Expandable expandable) {
         mUiHandler.post(() -> {
             final DialogHolder holder = new DialogHolder();
             final Dialog dialog = MediaRouteDialogPresenter.createDialog(
@@ -241,17 +241,21 @@
             SystemUIDialog.setDialogSize(dialog);
 
             mUiHandler.post(() -> {
-                if (view != null) {
-                    mDialogTransitionAnimator.showFromView(dialog, view,
-                            new DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
-                                    INTERACTION_JANK_TAG));
-                } else {
-                    if (dialog.getWindow() != null) {
-                        DialogKt.registerAnimationOnBackInvoked(dialog,
-                                dialog.getWindow().getDecorView());
+                if (expandable != null) {
+                    DialogTransitionAnimator.Controller controller =
+                            expandable.dialogTransitionController(
+                                    new DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
+                                            INTERACTION_JANK_TAG));
+                    if (controller != null) {
+                        mDialogTransitionAnimator.show(dialog, controller);
+                        return;
                     }
-                    dialog.show();
                 }
+                if (dialog.getWindow() != null) {
+                    DialogKt.registerAnimationOnBackInvoked(dialog,
+                            dialog.getWindow().getDecorView());
+                }
+                dialog.show();
             });
         });
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorCorrectionTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorCorrectionTile.java
index c8adbfc..871973df 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorCorrectionTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorCorrectionTile.java
@@ -22,12 +22,12 @@
 import android.provider.Settings;
 import android.provider.Settings.Secure;
 import android.service.quicksettings.Tile;
-import android.view.View;
 import android.widget.Switch;
 
 import androidx.annotation.Nullable;
 
 import com.android.internal.logging.MetricsLogger;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.ActivityStarter;
@@ -109,7 +109,7 @@
     }
 
     @Override
-    protected void handleClick(@Nullable View view) {
+    protected void handleClick(@Nullable Expandable expandable) {
         mSetting.setValue(mState.value ? 0 : 1);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java
index c34a584..5896910 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java
@@ -22,13 +22,13 @@
 import android.provider.Settings;
 import android.provider.Settings.Secure;
 import android.service.quicksettings.Tile;
-import android.view.View;
 import android.widget.Switch;
 
 import androidx.annotation.Nullable;
 
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.ActivityStarter;
@@ -108,7 +108,7 @@
     }
 
     @Override
-    protected void handleClick(@Nullable View view) {
+    protected void handleClick(@Nullable Expandable expandable) {
         mSetting.setValue(mState.value ? 0 : 1);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java
index 58630a0..7760943 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java
@@ -19,7 +19,6 @@
 import android.os.Looper;
 import android.provider.Settings;
 import android.service.quicksettings.Tile;
-import android.view.View;
 import android.widget.Switch;
 
 import androidx.annotation.Nullable;
@@ -30,6 +29,7 @@
 import com.android.systemui.Prefs;
 import com.android.systemui.animation.DialogCuj;
 import com.android.systemui.animation.DialogTransitionAnimator;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.ActivityStarter;
@@ -47,7 +47,7 @@
 import javax.inject.Inject;
 
 public class DataSaverTile extends QSTileImpl<BooleanState> implements
-        DataSaverController.Listener{
+        DataSaverController.Listener {
 
     public static final String TILE_SPEC = "saver";
 
@@ -89,8 +89,9 @@
     public Intent getLongClickIntent() {
         return new Intent(Settings.ACTION_DATA_SAVER_SETTINGS);
     }
+
     @Override
-    protected void handleClick(@Nullable View view) {
+    protected void handleClick(@Nullable Expandable expandable) {
         if (mState.value
                 || Prefs.getBoolean(mContext, Prefs.Key.QS_DATA_SAVER_DIALOG_SHOWN, false)) {
             // Do it right away.
@@ -112,10 +113,16 @@
             dialog.setNeutralButton(com.android.internal.R.string.cancel, null);
             dialog.setShowForAllUsers(true);
 
-            if (view != null) {
-                mDialogTransitionAnimator.showFromView(dialog, view, new DialogCuj(
-                        InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
-                        INTERACTION_JANK_TAG));
+            if (expandable != null) {
+                DialogTransitionAnimator.Controller controller =
+                        expandable.dialogTransitionController(new DialogCuj(
+                                InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
+                                INTERACTION_JANK_TAG));
+                if (controller != null) {
+                    mDialogTransitionAnimator.show(dialog, controller);
+                } else {
+                    dialog.show();
+                }
             } else {
                 dialog.show();
             }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DeviceControlsTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/DeviceControlsTile.kt
index bb175e2..cc8a734 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DeviceControlsTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DeviceControlsTile.kt
@@ -1,3 +1,4 @@
+
 /*
  * Copyright (C) 2021 The Android Open Source Project
  *
@@ -21,12 +22,11 @@
 import android.os.Handler
 import android.os.Looper
 import android.service.quicksettings.Tile
-import android.view.View
 import androidx.annotation.VisibleForTesting
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.internal.logging.MetricsLogger
 import com.android.systemui.res.R
-import com.android.systemui.animation.ActivityTransitionAnimator
+import com.android.systemui.animation.Expandable
 import com.android.systemui.controls.ControlsServiceInfo
 import com.android.systemui.controls.dagger.ControlsComponent
 import com.android.systemui.controls.dagger.ControlsComponent.Visibility.AVAILABLE
@@ -100,26 +100,30 @@
         }
     }
 
-    override fun handleClick(view: View?) {
+    override fun handleClick(expandable: Expandable?) {
         if (state.state == Tile.STATE_UNAVAILABLE) {
             return
         }
 
         val intent = Intent().apply {
             component = ComponentName(mContext, controlsComponent.getControlsUiController().get()
-                .resolveActivity())
+                    .resolveActivity())
             addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
             putExtra(ControlsUiController.EXTRA_ANIMATE, true)
         }
-        val animationController = view?.let {
-            ActivityTransitionAnimator.Controller.fromView(
-                    it, InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE)
-        }
+        val animationController =
+            expandable?.activityTransitionController(
+                    InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE
+            )
 
         mUiHandler.post {
             val showOverLockscreenWhenLocked = state.state == Tile.STATE_ACTIVE
             mActivityStarter.startActivity(
-                intent, true /* dismissShade */, animationController, showOverLockscreenWhenLocked)
+                intent,
+                true /* dismissShade */,
+                animationController,
+                showOverLockscreenWhenLocked,
+            )
         }
     }
 
@@ -130,7 +134,7 @@
         if (controlsComponent.isEnabled() && hasControlsApps.get()) {
             if (controlsComponent.getVisibility() == AVAILABLE) {
                 val selection = controlsComponent
-                    .getControlsController().get().getPreferredSelection()
+                        .getControlsController().get().getPreferredSelection()
                 state.state = if (selection is SelectedItem.StructureItem &&
                         selection.structure.controls.isEmpty()) {
                     Tile.STATE_INACTIVE
@@ -157,7 +161,7 @@
         return null
     }
 
-    override fun handleLongClick(view: View?) {}
+    override fun handleLongClick(expandable: Expandable?) {}
 
     override fun getTileLabel(): CharSequence {
         return mContext.getText(controlsComponent.getTileTitleId())
@@ -166,4 +170,4 @@
     companion object {
         const val TILE_SPEC = "controls"
     }
-}
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java
index f62b60b..4ebebea 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java
@@ -35,7 +35,6 @@
 import android.service.quicksettings.Tile;
 import android.text.TextUtils;
 import android.util.Log;
-import android.view.View;
 import android.widget.Switch;
 
 import androidx.annotation.Nullable;
@@ -47,6 +46,7 @@
 import com.android.systemui.Prefs;
 import com.android.systemui.animation.DialogCuj;
 import com.android.systemui.animation.DialogTransitionAnimator;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.ActivityStarter;
@@ -147,12 +147,12 @@
     }
 
     @Override
-    protected void handleClick(@Nullable View view) {
+    protected void handleClick(@Nullable Expandable expandable) {
         // Zen is currently on
         if (mState.value) {
             mController.setZen(ZEN_MODE_OFF, null, TAG);
         } else {
-            enableZenMode(view);
+            enableZenMode(expandable);
         }
     }
 
@@ -162,7 +162,7 @@
         mSettingZenDuration.setUserId(newUserId);
     }
 
-    private void enableZenMode(@Nullable View view) {
+    private void enableZenMode(@Nullable Expandable expandable) {
         int zenDuration = mSettingZenDuration.getValue();
         boolean showOnboarding = Settings.Secure.getInt(mContext.getContentResolver(),
                 Settings.Secure.SHOW_ZEN_UPGRADE_NOTIFICATION, 0) != 0
@@ -183,11 +183,17 @@
                 case Settings.Secure.ZEN_DURATION_PROMPT:
                     mUiHandler.post(() -> {
                         Dialog dialog = makeZenModeDialog();
-                        if (view != null) {
-                            mDialogTransitionAnimator.showFromView(dialog, view, new DialogCuj(
+                        if (expandable != null) {
+                            DialogTransitionAnimator.Controller controller =
+                                    expandable.dialogTransitionController(new DialogCuj(
                                             InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
-                                            INTERACTION_JANK_TAG),
-                                    /* animateBackgroundBoundsChange= */ false);
+                                            INTERACTION_JANK_TAG));
+                            if (controller != null) {
+                                mDialogTransitionAnimator.show(dialog,
+                                        controller, /* animateBackgroundBoundsChange= */ false);
+                            } else {
+                                dialog.show();
+                            }
                         } else {
                             dialog.show();
                         }
@@ -217,8 +223,8 @@
     }
 
     @Override
-    protected void handleSecondaryClick(@Nullable View view) {
-        handleLongClick(view);
+    protected void handleSecondaryClick(@Nullable Expandable expandable) {
+        handleLongClick(expandable);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DreamTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DreamTile.java
index 4f0a63b..0d3d980 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DreamTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DreamTile.java
@@ -32,12 +32,12 @@
 import android.service.quicksettings.Tile;
 import android.text.TextUtils;
 import android.util.Log;
-import android.view.View;
 
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
 import com.android.internal.logging.MetricsLogger;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
@@ -153,7 +153,7 @@
     }
 
     @Override
-    protected void handleClick(@Nullable View view) {
+    protected void handleClick(@Nullable Expandable expandable) {
         try {
             if (mDreamManager.isDreaming()) {
                 mDreamManager.awaken();
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java
index f022981..848ff3c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java
@@ -22,13 +22,13 @@
 import android.os.Looper;
 import android.provider.MediaStore;
 import android.service.quicksettings.Tile;
-import android.view.View;
 import android.widget.Switch;
 
 import androidx.annotation.Nullable;
 
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.ActivityStarter;
@@ -99,7 +99,7 @@
     }
 
     @Override
-    protected void handleClick(@Nullable View view) {
+    protected void handleClick(@Nullable Expandable expandable) {
         if (ActivityManager.isUserAMonkey()) {
             return;
         }
@@ -114,8 +114,8 @@
     }
 
     @Override
-    protected void handleLongClick(@Nullable View view) {
-        handleClick(view);
+    protected void handleLongClick(@Nullable Expandable expandable) {
+        handleClick(expandable);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/FontScalingTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/FontScalingTile.kt
index f5018a2..078698c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/FontScalingTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/FontScalingTile.kt
@@ -19,12 +19,12 @@
 import android.os.Handler
 import android.os.Looper
 import android.provider.Settings
-import android.view.View
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.internal.logging.MetricsLogger
 import com.android.systemui.accessibility.fontscaling.FontScalingDialogDelegate
 import com.android.systemui.animation.DialogCuj
 import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.animation.Expandable
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.plugins.ActivityStarter
@@ -74,18 +74,23 @@
         return QSTile.State()
     }
 
-    override fun handleClick(view: View?) {
+    override fun handleClick(expandable: Expandable?) {
         // We animate from the touched view only if we are not on the keyguard
-        val animateFromView: Boolean = view != null && !keyguardStateController.isShowing
+        val animateFromExpandable: Boolean =
+            expandable != null && !keyguardStateController.isShowing
 
         val runnable = Runnable {
             val dialog: SystemUIDialog = fontScalingDialogDelegateProvider.get().createDialog()
-            if (animateFromView) {
-                dialogTransitionAnimator.showFromView(
-                    dialog,
-                    view!!,
-                    DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG)
-                )
+            if (animateFromExpandable) {
+                val controller =
+                    expandable?.dialogTransitionController(
+                        DialogCuj(
+                            InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
+                            INTERACTION_JANK_TAG
+                        )
+                    )
+                controller?.let { dialogTransitionAnimator.show(dialog, controller) }
+                    ?: dialog.show()
             } else {
                 dialog.show()
             }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/HearingDevicesTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/HearingDevicesTile.java
index 81a2026..183c1a4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/HearingDevicesTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/HearingDevicesTile.java
@@ -20,13 +20,13 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.provider.Settings;
-import android.view.View;
 
 import androidx.annotation.Nullable;
 
 import com.android.internal.logging.MetricsLogger;
 import com.android.systemui.Flags;
 import com.android.systemui.accessibility.hearingaid.HearingDevicesDialogManager;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.ActivityStarter;
@@ -72,8 +72,8 @@
     }
 
     @Override
-    protected void handleClick(@Nullable View view) {
-        mUiHandler.post(() -> mDialogManager.showDialog(view));
+    protected void handleClick(@Nullable Expandable expandable) {
+        mUiHandler.post(() -> mDialogManager.showDialog(expandable));
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java
index 4d0404d..ea3993e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java
@@ -25,7 +25,6 @@
 import android.provider.Settings;
 import android.service.quicksettings.Tile;
 import android.util.Log;
-import android.view.View;
 import android.widget.Switch;
 
 import androidx.annotation.Nullable;
@@ -33,6 +32,7 @@
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.settingslib.wifi.WifiEnterpriseRestrictionUtils;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.ActivityStarter;
@@ -112,7 +112,7 @@
     }
 
     @Override
-    protected void handleClick(@Nullable View view) {
+    protected void handleClick(@Nullable Expandable expandable) {
         final boolean isEnabled = mState.value;
         if (!isEnabled && mDataSaverController.isDataSaverEnabled()) {
             return;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
index 0f260e3..6d98da4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
@@ -30,7 +30,6 @@
 import android.text.Html;
 import android.text.TextUtils;
 import android.util.Log;
-import android.view.View;
 import android.widget.Switch;
 
 import androidx.annotation.Nullable;
@@ -41,6 +40,7 @@
 import com.android.settingslib.graph.SignalDrawable;
 import com.android.settingslib.mobile.TelephonyIcons;
 import com.android.settingslib.net.DataUsageController;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.ActivityStarter;
@@ -124,10 +124,10 @@
     }
 
     @Override
-    protected void handleClick(@Nullable View view) {
+    protected void handleClick(@Nullable Expandable expandable) {
         mHandler.post(() -> mInternetDialogManager.create(true,
                 mAccessPointController.canConfigMobileData(),
-                mAccessPointController.canConfigWifi(), view));
+                mAccessPointController.canConfigWifi(), expandable));
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt
index 357743b..932dec5 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt
@@ -20,9 +20,9 @@
 import android.os.Handler
 import android.os.Looper
 import android.provider.Settings
-import android.view.View
 import android.widget.Switch
 import com.android.internal.logging.MetricsLogger
+import com.android.systemui.animation.Expandable
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.plugins.ActivityStarter
@@ -44,18 +44,18 @@
 class InternetTileNewImpl
 @Inject
 constructor(
-        host: QSHost,
-        uiEventLogger: QsEventLogger,
-        @Background backgroundLooper: Looper,
-        @Main private val mainHandler: Handler,
-        falsingManager: FalsingManager,
-        metricsLogger: MetricsLogger,
-        statusBarStateController: StatusBarStateController,
-        activityStarter: ActivityStarter,
-        qsLogger: QSLogger,
-        viewModel: InternetTileViewModel,
-        private val internetDialogManager: InternetDialogManager,
-        private val accessPointController: AccessPointController,
+    host: QSHost,
+    uiEventLogger: QsEventLogger,
+    @Background backgroundLooper: Looper,
+    @Main private val mainHandler: Handler,
+    falsingManager: FalsingManager,
+    metricsLogger: MetricsLogger,
+    statusBarStateController: StatusBarStateController,
+    activityStarter: ActivityStarter,
+    qsLogger: QSLogger,
+    viewModel: InternetTileViewModel,
+    private val internetDialogManager: InternetDialogManager,
+    private val accessPointController: AccessPointController,
 ) :
     QSTileImpl<QSTile.BooleanState>(
         host,
@@ -84,13 +84,13 @@
         return QSTile.BooleanState().also { it.forceExpandIcon = true }
     }
 
-    override fun handleClick(view: View?) {
+    override fun handleClick(expandable: Expandable?) {
         mainHandler.post {
             internetDialogManager.create(
                 aboveStatusBar = true,
                 accessPointController.canConfigMobileData(),
                 accessPointController.canConfigWifi(),
-                view,
+                expandable,
             )
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java
index b3f0d8b..cad5c0d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java
@@ -22,13 +22,13 @@
 import android.os.UserManager;
 import android.provider.Settings;
 import android.service.quicksettings.Tile;
-import android.view.View;
 import android.widget.Switch;
 
 import androidx.annotation.Nullable;
 
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.ActivityStarter;
@@ -92,7 +92,7 @@
     }
 
     @Override
-    protected void handleClick(@Nullable View view) {
+    protected void handleClick(@Nullable Expandable expandable) {
         if (mKeyguard.isMethodSecure() && mKeyguard.isShowing()) {
             mActivityStarter.postQSRunnableDismissingKeyguard(() -> {
                 final boolean wasEnabled = mState.value;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/NfcTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/NfcTile.java
index d650f73..136eea8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/NfcTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/NfcTile.java
@@ -27,13 +27,13 @@
 import android.os.Looper;
 import android.provider.Settings;
 import android.service.quicksettings.Tile;
-import android.view.View;
 import android.widget.Switch;
 
 import androidx.annotation.Nullable;
 
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
@@ -119,7 +119,7 @@
     }
 
     @Override
-    protected void handleClick(@Nullable View view) {
+    protected void handleClick(@Nullable Expandable expandable) {
         if (getAdapter() == null) {
             return;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/NightDisplayTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/NightDisplayTile.java
index a1ea46d..ac762de 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/NightDisplayTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/NightDisplayTile.java
@@ -28,7 +28,6 @@
 import android.service.quicksettings.Tile;
 import android.text.TextUtils;
 import android.util.Log;
-import android.view.View;
 import android.widget.Switch;
 
 import androidx.annotation.Nullable;
@@ -36,6 +35,7 @@
 
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.dagger.NightDisplayListenerModule;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
@@ -112,7 +112,7 @@
     }
 
     @Override
-    protected void handleClick(@Nullable View view) {
+    protected void handleClick(@Nullable Expandable expandable) {
         // Enroll in forced auto mode if eligible.
         if ("1".equals(Settings.Global.getString(mContext.getContentResolver(),
                 Settings.Global.NIGHT_DISPLAY_FORCED_AUTO_MODE_AVAILABLE))
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/OneHandedModeTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/OneHandedModeTile.java
index b08e6a5..450c954 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/OneHandedModeTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/OneHandedModeTile.java
@@ -21,13 +21,13 @@
 import android.os.Looper;
 import android.provider.Settings;
 import android.service.quicksettings.Tile;
-import android.view.View;
 import android.widget.Switch;
 
 import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.MetricsLogger;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.ActivityStarter;
@@ -114,7 +114,7 @@
     }
 
     @Override
-    protected void handleClick(@Nullable View view) {
+    protected void handleClick(@Nullable Expandable expandable) {
         mSetting.setValue(mState.value ? 0 : 1);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/QRCodeScannerTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/QRCodeScannerTile.java
index de9a08e..9766fac 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/QRCodeScannerTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/QRCodeScannerTile.java
@@ -21,13 +21,13 @@
 import android.os.Looper;
 import android.service.quicksettings.Tile;
 import android.util.Log;
-import android.view.View;
 
 import androidx.annotation.Nullable;
 
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.logging.MetricsLogger;
 import com.android.systemui.animation.ActivityTransitionAnimator;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.ActivityStarter;
@@ -99,7 +99,7 @@
     }
 
     @Override
-    protected void handleClick(@Nullable View view) {
+    protected void handleClick(@Nullable Expandable expandable) {
         Intent intent = mQRCodeScannerController.getIntent();
         if (intent == null) {
             // This should never happen as the fact that we are handling clicks means that the
@@ -109,7 +109,7 @@
         }
 
         ActivityTransitionAnimator.Controller animationController =
-                view == null ? null : ActivityTransitionAnimator.Controller.fromView(view,
+                expandable == null ? null : expandable.activityTransitionController(
                         InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE);
         mActivityStarter.startActivity(intent, true /* dismissShade */,
                 animationController, true /* showOverLockscreenWhenLocked */);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java
index e1b742e..76aa146 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java
@@ -35,7 +35,6 @@
 import android.service.quickaccesswallet.WalletCard;
 import android.service.quicksettings.Tile;
 import android.util.Log;
-import android.view.View;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -44,6 +43,7 @@
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.logging.MetricsLogger;
 import com.android.systemui.animation.ActivityTransitionAnimator;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.ActivityStarter;
@@ -131,9 +131,9 @@
     }
 
     @Override
-    protected void handleClick(@Nullable View view) {
+    protected void handleClick(@Nullable Expandable expandable) {
         ActivityTransitionAnimator.Controller animationController =
-                view == null ? null : ActivityTransitionAnimator.Controller.fromView(view,
+                expandable == null ? null : expandable.activityTransitionController(
                         InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE);
 
         mUiHandler.post(
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt
index b418a17..9937ea4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt
@@ -24,7 +24,6 @@
 import android.os.Looper
 import android.service.quicksettings.Tile
 import android.text.TextUtils
-import android.view.View
 import android.widget.Switch
 import androidx.annotation.VisibleForTesting
 import com.android.internal.jank.InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN
@@ -32,6 +31,7 @@
 import com.android.systemui.Flags.recordIssueQsTile
 import com.android.systemui.animation.DialogCuj
 import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.animation.Expandable
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.plugins.ActivityStarter
@@ -113,11 +113,11 @@
         }
 
     @VisibleForTesting
-    public override fun handleClick(view: View?) {
+    public override fun handleClick(expandable: Expandable?) {
         if (issueRecordingState.isRecording) {
             stopIssueRecordingService()
         } else {
-            mUiHandler.post { showPrompt(view) }
+            mUiHandler.post { showPrompt(expandable) }
         }
     }
 
@@ -143,7 +143,7 @@
             )
             .send(BroadcastOptions.makeBasic().apply { isInteractive = true }.toBundle())
 
-    private fun showPrompt(view: View?) {
+    private fun showPrompt(expandable: Expandable?) {
         val dialog: AlertDialog =
             delegateFactory
                 .create {
@@ -156,12 +156,11 @@
             ActivityStarter.OnDismissAction {
                 // We animate from the touched view only if we are not on the keyguard, given
                 // that if we are we will dismiss it which will also collapse the shade.
-                if (view != null && !keyguardStateController.isShowing) {
-                    dialogTransitionAnimator.showFromView(
-                        dialog,
-                        view,
-                        DialogCuj(CUJ_SHADE_DIALOG_OPEN, TILE_SPEC)
-                    )
+                if (expandable != null && !keyguardStateController.isShowing) {
+                    expandable
+                        .dialogTransitionController(DialogCuj(CUJ_SHADE_DIALOG_OPEN, TILE_SPEC))
+                        ?.let { dialogTransitionAnimator.show(dialog, it) }
+                        ?: dialog.show()
                 } else {
                     dialog.show()
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ReduceBrightColorsTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ReduceBrightColorsTile.java
index 76ada10..3472352 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ReduceBrightColorsTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ReduceBrightColorsTile.java
@@ -23,13 +23,13 @@
 import android.os.Looper;
 import android.provider.Settings;
 import android.service.quicksettings.Tile;
-import android.view.View;
 import android.widget.Switch;
 
 import androidx.annotation.Nullable;
 
 import com.android.internal.R;
 import com.android.internal.logging.MetricsLogger;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.ActivityStarter;
@@ -97,7 +97,7 @@
     }
 
     @Override
-    protected void handleClick(@Nullable View view) {
+    protected void handleClick(@Nullable Expandable expandable) {
         mReduceBrightColorsController.setReduceBrightColorsActivated(!mState.value);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java
index f1d8f9f..35e43b6 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java
@@ -29,13 +29,13 @@
 import android.provider.Settings;
 import android.provider.Settings.Secure;
 import android.service.quicksettings.Tile;
-import android.view.View;
 import android.widget.Switch;
 
 import androidx.annotation.Nullable;
 
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.ActivityStarter;
@@ -131,7 +131,7 @@
     }
 
     @Override
-    protected void handleClick(@Nullable View view) {
+    protected void handleClick(@Nullable Expandable expandable) {
         final boolean newState = !mState.value;
         mController.setRotationLocked(!newState, /* caller= */ "RotationLockTile#handleClick");
         refreshState(newState);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
index 1a90d43..4715230 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
@@ -23,7 +23,6 @@
 import android.service.quicksettings.Tile;
 import android.text.TextUtils;
 import android.util.Log;
-import android.view.View;
 import android.widget.Switch;
 
 import androidx.annotation.Nullable;
@@ -32,6 +31,7 @@
 import com.android.internal.logging.MetricsLogger;
 import com.android.systemui.animation.DialogCuj;
 import com.android.systemui.animation.DialogTransitionAnimator;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.flags.FeatureFlags;
@@ -118,13 +118,13 @@
     }
 
     @Override
-    protected void handleClick(@Nullable View view) {
+    protected void handleClick(@Nullable Expandable expandable) {
         if (mController.isStarting()) {
             cancelCountdown();
         } else if (mController.isRecording()) {
             stopRecording();
         } else {
-            mUiHandler.post(() -> showPrompt(view));
+            mUiHandler.post(() -> showPrompt(expandable));
         }
         refreshState();
     }
@@ -174,10 +174,11 @@
         return mContext.getString(R.string.quick_settings_screen_record_label);
     }
 
-    private void showPrompt(@Nullable View view) {
+    private void showPrompt(@Nullable Expandable expandable) {
         // We animate from the touched view only if we are not on the keyguard, given that if we
         // are we will dismiss it which will also collapse the shade.
-        boolean shouldAnimateFromView = view != null && !mKeyguardStateController.isShowing();
+        boolean shouldAnimateFromExpandable =
+                expandable != null && !mKeyguardStateController.isShowing();
 
         // Create the recording dialog that will collapse the shade only if we start the recording.
         Runnable onStartRecordingClicked = () -> {
@@ -192,10 +193,17 @@
                 mDialogTransitionAnimator, mActivityStarter, onStartRecordingClicked);
 
         ActivityStarter.OnDismissAction dismissAction = () -> {
-            if (shouldAnimateFromView) {
-                mDialogTransitionAnimator.showFromView(dialog, view, new DialogCuj(
-                        InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG),
-                        /* animateBackgroundBoundsChange= */ true);
+            if (shouldAnimateFromExpandable) {
+                DialogTransitionAnimator.Controller controller =
+                        expandable.dialogTransitionController(new DialogCuj(
+                                InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
+                                INTERACTION_JANK_TAG));
+                if (controller != null) {
+                    mDialogTransitionAnimator.show(dialog,
+                            controller, /* animateBackgroundBoundsChange= */ true);
+                } else {
+                    dialog.show();
+                }
             } else {
                 dialog.show();
             }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/SensorPrivacyToggleTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/SensorPrivacyToggleTile.java
index 3eeb2a3..036ce08 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/SensorPrivacyToggleTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/SensorPrivacyToggleTile.java
@@ -26,13 +26,13 @@
 import android.safetycenter.SafetyCenterManager;
 import android.service.quicksettings.Tile;
 import android.text.TextUtils;
-import android.view.View;
 import android.widget.Switch;
 
 import androidx.annotation.DrawableRes;
 import androidx.annotation.Nullable;
 
 import com.android.internal.logging.MetricsLogger;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.ActivityStarter;
@@ -100,7 +100,7 @@
     }
 
     @Override
-    protected void handleClick(@Nullable View view) {
+    protected void handleClick(@Nullable Expandable expandable) {
         boolean blocked = mSensorPrivacyController.isSensorBlocked(getSensorId());
         if (mSensorPrivacyController.requiresAuthentication()
                 && mKeyguard.isMethodSecure()
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UiModeNightTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UiModeNightTile.java
index d92873ada..bec6581 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UiModeNightTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UiModeNightTile.java
@@ -24,13 +24,13 @@
 import android.provider.Settings;
 import android.service.quicksettings.Tile;
 import android.text.TextUtils;
-import android.view.View;
 import android.widget.Switch;
 
 import androidx.annotation.Nullable;
 
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.ActivityStarter;
@@ -107,7 +107,7 @@
     }
 
     @Override
-    protected void handleClick(@Nullable View view) {
+    protected void handleClick(@Nullable Expandable expandable) {
         if (getState().state == Tile.STATE_UNAVAILABLE) {
             return;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java
index abc4812..d9546ec 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java
@@ -24,7 +24,6 @@
 import android.os.Looper;
 import android.provider.Settings;
 import android.service.quicksettings.Tile;
-import android.view.View;
 import android.widget.Switch;
 
 import androidx.annotation.MainThread;
@@ -32,6 +31,7 @@
 
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.ActivityStarter;
@@ -88,7 +88,7 @@
     }
 
     @Override
-    public void handleClick(@Nullable View view) {
+    public void handleClick(@Nullable Expandable expandable) {
         mProfileController.setWorkModeEnabled(!mState.value);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandler.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandler.kt
index 7192f58..2d3120a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandler.kt
@@ -20,9 +20,9 @@
 import android.content.Intent
 import android.content.pm.PackageManager
 import android.os.UserHandle
-import android.view.View
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.systemui.animation.ActivityTransitionAnimator
+import com.android.systemui.animation.Expandable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.plugins.ActivityStarter
 import javax.inject.Inject
@@ -33,11 +33,11 @@
  */
 interface QSTileIntentUserInputHandler {
 
-    fun handle(view: View?, intent: Intent)
+    fun handle(expandable: Expandable?, intent: Intent)
 
     /** @param requestLaunchingDefaultActivity used in case !pendingIndent.isActivity */
     fun handle(
-        view: View?,
+        expandable: Expandable?,
         pendingIntent: PendingIntent,
         requestLaunchingDefaultActivity: Boolean = false
     )
@@ -52,31 +52,25 @@
     private val userHandle: UserHandle,
 ) : QSTileIntentUserInputHandler {
 
-    override fun handle(view: View?, intent: Intent) {
+    override fun handle(expandable: Expandable?, intent: Intent) {
         val animationController: ActivityTransitionAnimator.Controller? =
-            view?.let {
-                ActivityTransitionAnimator.Controller.fromView(
-                    it,
-                    InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE,
-                )
-            }
+            expandable?.activityTransitionController(
+                InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE
+            )
         activityStarter.postStartActivityDismissingKeyguard(intent, 0, animationController)
     }
 
     // TODO(b/249804373): make sure to allow showing activities over the lockscreen. See b/292112939
     override fun handle(
-        view: View?,
+        expandable: Expandable?,
         pendingIntent: PendingIntent,
         requestLaunchingDefaultActivity: Boolean
     ) {
         if (pendingIntent.isActivity) {
             val animationController: ActivityTransitionAnimator.Controller? =
-                view?.let {
-                    ActivityTransitionAnimator.Controller.fromView(
-                        it,
-                        InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE,
-                    )
-                }
+                expandable?.activityTransitionController(
+                    InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE
+                )
             activityStarter.postStartActivityDismissingKeyguard(pendingIntent, animationController)
         } else if (requestLaunchingDefaultActivity) {
             val intent =
@@ -97,7 +91,7 @@
                 ?.let { resolved ->
                     intent.setPackage(null)
                     intent.setComponent(resolved.activityInfo.componentName)
-                    handle(view, intent)
+                    handle(expandable, intent)
                 }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogManager.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogManager.kt
index 5aef950..246fe38 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogManager.kt
@@ -16,10 +16,10 @@
 package com.android.systemui.qs.tiles.dialog
 
 import android.util.Log
-import android.view.View
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.systemui.animation.DialogCuj
 import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.animation.Expandable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.statusbar.phone.SystemUIDialog
@@ -47,14 +47,14 @@
     }
 
     /**
-     * Creates a [InternetDialogDelegate]. The dialog will be animated from [view] if it is not
-     * null.
+     * Creates a [InternetDialogDelegate]. The dialog will be animated from [expandable] if it is
+     * not null.
      */
     fun create(
         aboveStatusBar: Boolean,
         canConfigMobileData: Boolean,
         canConfigWifi: Boolean,
-        view: View?
+        expandable: Expandable?
     ) {
         if (dialog != null) {
             if (DEBUG) {
@@ -67,20 +67,18 @@
                 dialogFactory
                     .create(aboveStatusBar, canConfigMobileData, canConfigWifi, coroutineScope)
                     .createDialog()
-            if (view != null) {
-                dialogTransitionAnimator.showFromView(
-                    dialog!!,
-                    view,
-                    animateBackgroundBoundsChange = true,
-                    cuj =
-                        DialogCuj(
-                            InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
-                            INTERACTION_JANK_TAG
-                        )
+            val controller =
+                expandable?.dialogTransitionController(
+                    DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG)
                 )
-            } else {
-                dialog!!.show()
+            controller?.let {
+                dialogTransitionAnimator.show(
+                    dialog!!,
+                    controller,
+                    animateBackgroundBoundsChange = true
+                )
             }
+                ?: dialog?.show()
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/airplane/domain/interactor/AirplaneModeTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/airplane/domain/interactor/AirplaneModeTileUserActionInteractor.kt
index 9e13a56..bf0f8f6 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/airplane/domain/interactor/AirplaneModeTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/airplane/domain/interactor/AirplaneModeTileUserActionInteractor.kt
@@ -45,7 +45,7 @@
                         }
                         AirplaneModeInteractor.SetResult.BLOCKED_BY_ECM -> {
                             qsTileIntentUserActionHandler.handle(
-                                action.view,
+                                action.expandable,
                                 Intent(TelephonyManager.ACTION_SHOW_NOTICE_ECM_BLOCK_OTHERS),
                             )
                         }
@@ -53,7 +53,7 @@
                 }
                 is QSTileUserAction.LongClick -> {
                     qsTileIntentUserActionHandler.handle(
-                        action.view,
+                        action.expandable,
                         Intent(Settings.ACTION_AIRPLANE_MODE_SETTINGS)
                     )
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/interactor/AlarmTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/interactor/AlarmTileUserActionInteractor.kt
index 0ad520b..14fc57c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/interactor/AlarmTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/interactor/AlarmTileUserActionInteractor.kt
@@ -40,9 +40,12 @@
                             data.alarmClockInfo.showIntent != null
                     ) {
                         val pendingIndent = data.alarmClockInfo.showIntent
-                        inputHandler.handle(action.view, pendingIndent, true)
+                        inputHandler.handle(action.expandable, pendingIndent, true)
                     } else {
-                        inputHandler.handle(action.view, Intent(AlarmClock.ACTION_SHOW_ALARMS))
+                        inputHandler.handle(
+                            action.expandable,
+                            Intent(AlarmClock.ACTION_SHOW_ALARMS)
+                        )
                     }
                 }
                 is QSTileUserAction.LongClick -> {}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/domain/interactor/BatterySaverTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/domain/interactor/BatterySaverTileUserActionInteractor.kt
index 1e4eb38..d4b4fe0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/domain/interactor/BatterySaverTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/domain/interactor/BatterySaverTileUserActionInteractor.kt
@@ -39,12 +39,12 @@
             when (action) {
                 is QSTileUserAction.Click -> {
                     if (!data.isPluggedIn) {
-                        batteryController.setPowerSaveMode(!data.isPowerSaving, action.view)
+                        batteryController.setPowerSaveMode(!data.isPowerSaving, action.expandable)
                     }
                 }
                 is QSTileUserAction.LongClick -> {
                     qsTileIntentUserActionHandler.handle(
-                        action.view,
+                        action.expandable,
                         Intent(Settings.ACTION_BATTERY_SAVER_SETTINGS)
                     )
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/interactor/ColorCorrectionUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/interactor/ColorCorrectionUserActionInteractor.kt
index d183802..534bd73 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/interactor/ColorCorrectionUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/interactor/ColorCorrectionUserActionInteractor.kt
@@ -45,7 +45,7 @@
                 }
                 is QSTileUserAction.LongClick -> {
                     qsTileIntentUserActionHandler.handle(
-                        action.view,
+                        action.expandable,
                         Intent(Settings.ACTION_COLOR_CORRECTION_SETTINGS)
                     )
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileUserActionInteractor.kt
index a16ac36..9bdf631 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileUserActionInteractor.kt
@@ -29,9 +29,9 @@
 import android.provider.Settings
 import android.service.quicksettings.TileService
 import android.view.IWindowManager
-import android.view.View
 import android.view.WindowManager
 import androidx.annotation.GuardedBy
+import com.android.systemui.animation.Expandable
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler
@@ -65,20 +65,21 @@
 
     @GuardedBy("token") private var isTokenGranted: Boolean = false
     @GuardedBy("token") private var isShowingDialog: Boolean = false
-    private val lastClickedView: AtomicReference<View> = AtomicReference<View>()
+    private val lastClickedExpandable: AtomicReference<Expandable> = AtomicReference<Expandable>()
 
     override suspend fun handleInput(input: QSTileInput<CustomTileDataModel>) =
         with(input) {
             when (action) {
-                is QSTileUserAction.Click -> click(action.view, data.tile.activityLaunchForClick)
+                is QSTileUserAction.Click ->
+                    click(action.expandable, data.tile.activityLaunchForClick)
                 is QSTileUserAction.LongClick ->
-                    longClick(user, action.view, data.componentName, data.tile.state)
+                    longClick(user, action.expandable, data.componentName, data.tile.state)
             }
             qsTileLogger.logCustomTileUserActionDelivered(tileSpec)
         }
 
     private suspend fun click(
-        view: View?,
+        expandable: Expandable?,
         activityLaunchForClick: PendingIntent?,
     ) {
         grantToken()
@@ -86,10 +87,10 @@
             // Bind active tile to deliver user action
             serviceInteractor.bindOnClick()
             if (activityLaunchForClick == null) {
-                lastClickedView.set(view)
+                lastClickedExpandable.set(expandable)
                 serviceInteractor.onClick(token)
             } else {
-                qsTileIntentUserInputHandler.handle(view, activityLaunchForClick)
+                qsTileIntentUserInputHandler.handle(expandable, activityLaunchForClick)
             }
         } catch (e: RemoteException) {
             qsTileLogger.logError(tileSpec, "Failed to deliver click", e)
@@ -117,10 +118,10 @@
         if (!isTokenGranted) {
             return
         }
-        qsTileIntentUserInputHandler.handle(lastClickedView.getAndSet(null), pendingIntent)
+        qsTileIntentUserInputHandler.handle(lastClickedExpandable.getAndSet(null), pendingIntent)
     }
 
-    fun clearLastClickedView() = lastClickedView.set(null)
+    fun clearLastClickedView() = lastClickedExpandable.set(null)
 
     private fun grantToken() {
         synchronized(token) {
@@ -142,7 +143,7 @@
 
     private suspend fun longClick(
         user: UserHandle,
-        view: View?,
+        expandable: Expandable?,
         componentName: ComponentName,
         state: Int
     ) {
@@ -159,14 +160,14 @@
                 }
         if (resolvedIntent == null) {
             qsTileIntentUserInputHandler.handle(
-                view,
+                expandable,
                 Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
                     .setData(
                         Uri.fromParts(IntentFilter.SCHEME_PACKAGE, componentName.packageName, null)
                     )
             )
         } else {
-            qsTileIntentUserInputHandler.handle(view, resolvedIntent)
+            qsTileIntentUserInputHandler.handle(expandable, resolvedIntent)
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileUserActionInteractor.kt
index db8b1a5..d308ec8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileUserActionInteractor.kt
@@ -52,21 +52,22 @@
         with(input) {
             when (action) {
                 is QSTileUserAction.Click -> {
-                    // We animate from the touched view only if we are not on the keyguard
-                    val animateFromView: Boolean =
-                        action.view != null && !keyguardStateController.isShowing
+                    // We animate from the touched expandable only if we are not on the keyguard
+                    val animateFromExpandable: Boolean =
+                        action.expandable != null && !keyguardStateController.isShowing
                     val runnable = Runnable {
                         val dialog: SystemUIDialog =
                             fontScalingDialogDelegateProvider.get().createDialog()
-                        if (animateFromView) {
-                            dialogTransitionAnimator.showFromView(
-                                dialog,
-                                action.view!!,
-                                DialogCuj(
-                                    InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
-                                    INTERACTION_JANK_TAG
+                        if (animateFromExpandable) {
+                            action.expandable
+                                ?.dialogTransitionController(
+                                    DialogCuj(
+                                        InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
+                                        INTERACTION_JANK_TAG
+                                    )
                                 )
-                            )
+                                ?.let { dialogTransitionAnimator.show(dialog, it) }
+                                ?: dialog.show()
                         } else {
                             dialog.show()
                         }
@@ -84,7 +85,7 @@
                 }
                 is QSTileUserAction.LongClick -> {
                     qsTileIntentUserActionHandler.handle(
-                        action.view,
+                        action.expandable,
                         Intent(Settings.ACTION_TEXT_READING_SETTINGS)
                     )
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt
index 2620cd5..c0b089d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt
@@ -49,13 +49,13 @@
                             aboveStatusBar = true,
                             accessPointController.canConfigMobileData(),
                             accessPointController.canConfigWifi(),
-                            action.view,
+                            action.expandable,
                         )
                     }
                 }
                 is QSTileUserAction.LongClick -> {
                     qsTileIntentUserActionHandler.handle(
-                        action.view,
+                        action.expandable,
                         Intent(Settings.ACTION_WIFI_SETTINGS)
                     )
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/inversion/domain/interactor/ColorInversionUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/inversion/domain/interactor/ColorInversionUserActionInteractor.kt
index 43b58c8..d643273 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/inversion/domain/interactor/ColorInversionUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/inversion/domain/interactor/ColorInversionUserActionInteractor.kt
@@ -45,7 +45,7 @@
                 }
                 is QSTileUserAction.LongClick -> {
                     qsTileIntentUserActionHandler.handle(
-                        action.view,
+                        action.expandable,
                         Intent(Settings.ACTION_COLOR_INVERSION_SETTINGS)
                     )
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/interactor/LocationTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/interactor/LocationTileUserActionInteractor.kt
index 66705ea..77404aa 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/interactor/LocationTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/interactor/LocationTileUserActionInteractor.kt
@@ -64,7 +64,7 @@
                 }
                 is QSTileUserAction.LongClick -> {
                     qsTileIntentUserActionHandler.handle(
-                        action.view,
+                        action.expandable,
                         Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
                     )
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileUserActionInteractor.kt
index 762f863..14dbe0e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileUserActionInteractor.kt
@@ -44,7 +44,7 @@
                 }
                 is QSTileUserAction.LongClick -> {
                     qsTileIntentUserActionHandler.handle(
-                        action.view,
+                        action.expandable,
                         Intent(Settings.ACTION_REDUCE_BRIGHT_COLORS_SETTINGS)
                     )
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/rotation/domain/interactor/RotationLockTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/rotation/domain/interactor/RotationLockTileUserActionInteractor.kt
index 8530926..34385ea 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/rotation/domain/interactor/RotationLockTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/rotation/domain/interactor/RotationLockTileUserActionInteractor.kt
@@ -42,7 +42,7 @@
                 }
                 is QSTileUserAction.LongClick -> {
                     qsTileIntentUserActionHandler.handle(
-                        action.view,
+                        action.expandable,
                         Intent(Settings.ACTION_AUTO_ROTATE_SETTINGS)
                     )
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileUserActionInteractor.kt
index 861faf5..a5dc66c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileUserActionInteractor.kt
@@ -75,34 +75,32 @@
                     // must be created and shown on the main thread, so we post it to the UI
                     // handler
                     withContext(coroutineContext) {
-                        val dialogContext = action.view?.context ?: context
                         val dialogDelegate =
                             DataSaverDialogDelegate(
                                 systemUIDialogFactory,
-                                dialogContext,
+                                context,
                                 backgroundContext,
                                 dataSaverController,
                                 sharedPreferences
                             )
-                        val dialog = systemUIDialogFactory.create(dialogDelegate, dialogContext)
+                        val dialog = systemUIDialogFactory.create(dialogDelegate, context)
 
-                        if (action.view != null) {
-                            dialogTransitionAnimator.showFromView(
-                                dialog,
-                                action.view!!,
+                        action.expandable
+                            ?.dialogTransitionController(
                                 DialogCuj(
                                     InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
                                     INTERACTION_JANK_TAG
                                 )
                             )
-                        } else {
-                            dialog.show()
-                        }
+                            ?.let { controller ->
+                                dialogTransitionAnimator.show(dialog, controller)
+                            }
+                            ?: dialog.show()
                     }
                 }
                 is QSTileUserAction.LongClick -> {
                     qsTileIntentUserActionHandler.handle(
-                        action.view,
+                        action.expandable,
                         Intent(Settings.ACTION_DATA_SAVER_SETTINGS)
                     )
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractor.kt
index d2bd09f..79766d6 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractor.kt
@@ -18,10 +18,10 @@
 
 import android.content.Context
 import android.util.Log
-import android.view.View
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.systemui.animation.DialogCuj
 import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.animation.Expandable
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
@@ -68,14 +68,16 @@
                         is ScreenRecordTileModel.Recording ->
                             withContext(backgroundContext) { recordingController.stopRecording() }
                         is ScreenRecordTileModel.DoingNothing ->
-                            withContext(mainContext) { showPrompt(action.view, user.identifier) }
+                            withContext(mainContext) {
+                                showPrompt(action.expandable, user.identifier)
+                            }
                     }
                 }
                 is QSTileUserAction.LongClick -> {} // no-op
             }
         }
 
-    private fun showPrompt(view: View?, userId: Int) {
+    private fun showPrompt(expandable: Expandable?, userId: Int) {
         // Create the recording dialog that will collapse the shade only if we start the recording.
         val onStartRecordingClicked = Runnable {
             // We dismiss the shade. Since starting the recording will also dismiss the dialog, we
@@ -99,21 +101,29 @@
             return
         }
 
-        // We animate from the touched view only if we are not on the keyguard, given that if we
+        // We animate from the touched expandable only if we are not on the keyguard, given that if
+        // we
         // are we will dismiss it which will also collapse the shade.
-        val shouldAnimateFromView = view != null && !keyguardInteractor.isKeyguardShowing()
+        val shouldAnimateFromExpandable =
+            expandable != null && !keyguardInteractor.isKeyguardShowing()
         val dismissAction =
             ActivityStarter.OnDismissAction {
-                if (shouldAnimateFromView) {
-                    dialogTransitionAnimator.showFromView(
-                        dialog,
-                        view!!,
-                        DialogCuj(
-                            InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
-                            INTERACTION_JANK_TAG
-                        ),
-                        animateBackgroundBoundsChange = true
-                    )
+                if (shouldAnimateFromExpandable) {
+                    val controller =
+                        expandable?.dialogTransitionController(
+                            DialogCuj(
+                                InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
+                                INTERACTION_JANK_TAG
+                            )
+                        )
+                    controller?.let {
+                        dialogTransitionAnimator.show(
+                            dialog,
+                            controller,
+                            animateBackgroundBoundsChange = true,
+                        )
+                    }
+                        ?: dialog.show()
                 } else {
                     dialog.show()
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/sensorprivacy/domain/SensorPrivacyToggleTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/sensorprivacy/domain/SensorPrivacyToggleTileUserActionInteractor.kt
index 9711cb8..f22a426 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/sensorprivacy/domain/SensorPrivacyToggleTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/sensorprivacy/domain/SensorPrivacyToggleTileUserActionInteractor.kt
@@ -80,7 +80,7 @@
                                 }
                             )
                     }
-                    qsTileIntentUserActionHandler.handle(action.view, longClickIntent)
+                    qsTileIntentUserActionHandler.handle(action.expandable, longClickIntent)
                 }
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/interactor/UiModeNightTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/interactor/UiModeNightTileUserActionInteractor.kt
index 00d7a62..f8dd1730 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/interactor/UiModeNightTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/interactor/UiModeNightTileUserActionInteractor.kt
@@ -50,7 +50,7 @@
                 }
                 is QSTileUserAction.LongClick -> {
                     qsTileIntentUserActionHandler.handle(
-                        action.view,
+                        action.expandable,
                         Intent(Settings.ACTION_DARK_THEME_SETTINGS)
                     )
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileUserActionInteractor.kt
index f765f8b..031e4d9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileUserActionInteractor.kt
@@ -44,7 +44,7 @@
                 is QSTileUserAction.LongClick -> {
                     if (data is WorkModeTileModel.HasActiveProfile) {
                         qsTileIntentUserActionHandler.handle(
-                            action.view,
+                            action.expandable,
                             Intent(Settings.ACTION_MANAGED_PROFILE_SETTINGS)
                         )
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileUserAction.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileUserAction.kt
index a145042..acb2936 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileUserAction.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileUserAction.kt
@@ -16,12 +16,12 @@
 
 package com.android.systemui.qs.tiles.viewmodel
 
-import android.view.View
+import com.android.systemui.animation.Expandable
 
 sealed interface QSTileUserAction {
 
-    val view: View?
+    val expandable: Expandable?
 
-    class Click(override val view: View?) : QSTileUserAction
-    class LongClick(override val view: View?) : QSTileUserAction
+    class Click(override val expandable: Expandable?) : QSTileUserAction
+    class LongClick(override val expandable: Expandable?) : QSTileUserAction
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
index 5a389f3..b88c1e5 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
@@ -19,10 +19,10 @@
 import android.content.Context
 import android.os.UserHandle
 import android.util.Log
-import android.view.View
 import androidx.annotation.GuardedBy
 import com.android.internal.logging.InstanceId
 import com.android.systemui.Dumpable
+import com.android.systemui.animation.Expandable
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.plugins.qs.QSTile
@@ -126,21 +126,21 @@
         synchronized(callbacks) { callbacks.clear() }
     }
 
-    override fun click(view: View?) {
+    override fun click(expandable: Expandable?) {
         if (isActionSupported(QSTileState.UserAction.CLICK)) {
-            qsTileViewModel.onActionPerformed(QSTileUserAction.Click(view))
+            qsTileViewModel.onActionPerformed(QSTileUserAction.Click(expandable))
         }
     }
 
-    override fun secondaryClick(view: View?) {
+    override fun secondaryClick(expandable: Expandable?) {
         if (isActionSupported(QSTileState.UserAction.CLICK)) {
-            qsTileViewModel.onActionPerformed(QSTileUserAction.Click(view))
+            qsTileViewModel.onActionPerformed(QSTileUserAction.Click(expandable))
         }
     }
 
-    override fun longClick(view: View?) {
+    override fun longClick(expandable: Expandable?) {
         if (isActionSupported(QSTileState.UserAction.LONG_CLICK)) {
-            qsTileViewModel.onActionPerformed(QSTileUserAction.LongClick(view))
+            qsTileViewModel.onActionPerformed(QSTileUserAction.LongClick(expandable))
         }
     }
 
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/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index b92e8eb..0673dcd 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -74,6 +74,7 @@
 
 import androidx.annotation.NonNull;
 
+import com.android.compose.animation.scene.SceneKey;
 import com.android.internal.accessibility.dialog.AccessibilityButtonChooserActivity;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.AssistUtils;
@@ -103,6 +104,8 @@
 import com.android.systemui.settings.DisplayTracker;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shade.ShadeViewController;
+import com.android.systemui.shade.domain.interactor.ShadeInteractor;
+import com.android.systemui.shade.shared.model.ShadeMode;
 import com.android.systemui.shared.recents.IOverviewProxy;
 import com.android.systemui.shared.recents.ISystemUiProxy;
 import com.android.systemui.shared.system.QuickStepContract;
@@ -155,6 +158,7 @@
     private final ScreenPinningRequest mScreenPinningRequest;
     private final NotificationShadeWindowController mStatusBarWinController;
     private final Provider<SceneInteractor> mSceneInteractor;
+    private final Provider<ShadeInteractor> mShadeInteractor;
 
     private final Runnable mConnectionRunnable = () ->
             internalConnectToCurrentUser("runnable: startConnectionToCurrentUser");
@@ -243,7 +247,9 @@
                             // Gesture was too short to be picked up by scene container touch
                             // handling; programmatically start the transition to shade scene.
                             mSceneInteractor.get().changeScene(
-                                    Scenes.Shade, "short launcher swipe");
+                                    getShadeSceneKey(),
+                                    "short launcher swipe"
+                            );
                         }
                     }
                     event.recycle();
@@ -261,7 +267,9 @@
                                 "trackpad swipe");
                     } else if (action == ACTION_UP) {
                         mSceneInteractor.get().changeScene(
-                                Scenes.Shade, "short trackpad swipe");
+                                getShadeSceneKey(),
+                                "short trackpad swipe"
+                        );
                     }
                     mStatusBarWinController.getWindowRootView().dispatchTouchEvent(event);
                 } else {
@@ -618,6 +626,7 @@
             NotificationShadeWindowController statusBarWinController,
             SysUiState sysUiState,
             Provider<SceneInteractor> sceneInteractor,
+            Provider<ShadeInteractor> shadeInteractor,
             UserTracker userTracker,
             WakefulnessLifecycle wakefulnessLifecycle,
             UiEventLogger uiEventLogger,
@@ -644,6 +653,7 @@
         mScreenPinningRequest = screenPinningRequest;
         mStatusBarWinController = statusBarWinController;
         mSceneInteractor = sceneInteractor;
+        mShadeInteractor = shadeInteractor;
         mUserTracker = userTracker;
         mConnectionBackoffAttempts = 0;
         mRecentsComponentName = ComponentName.unflattenFromString(context.getString(
@@ -909,6 +919,12 @@
         }
     }
 
+    private SceneKey getShadeSceneKey() {
+        return mShadeInteractor.get().getShadeMode().getValue() == ShadeMode.dual()
+                ? Scenes.NotificationsShade
+                : Scenes.Shade;
+    }
+
     private void notifyHomeRotationEnabled(boolean enabled) {
         for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) {
             mConnectionCallbacks.get(i).onHomeRotationEnabled(enabled);
diff --git a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
index 063a52c..aa8ecfc 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.scene.domain.startable.SceneContainerStartable
 import com.android.systemui.scene.shared.model.SceneContainerConfig
 import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shade.shared.flag.DualShade
 import dagger.Binds
 import dagger.Module
 import dagger.Provides
@@ -33,6 +34,7 @@
         [
             EmptySceneModule::class,
             GoneSceneModule::class,
+            NotificationsShadeSceneModule::class,
             QuickSettingsSceneModule::class,
             ShadeSceneModule::class,
         ],
@@ -59,18 +61,22 @@
                 // Note that this list is in z-order. The first one is the bottom-most and the
                 // last one is top-most.
                 sceneKeys =
-                    listOf(
+                    listOfNotNull(
                         Scenes.Gone,
                         Scenes.QuickSettings,
-                        Scenes.Shade,
+                        Scenes.NotificationsShade.takeIf { DualShade.isEnabled },
+                        Scenes.Shade.takeUnless { DualShade.isEnabled },
                     ),
                 initialSceneKey = Scenes.Gone,
                 navigationDistances =
                     mapOf(
-                        Scenes.Gone to 0,
-                        Scenes.Shade to 1,
-                        Scenes.QuickSettings to 2,
-                    ),
+                            Scenes.Gone to 0,
+                            Scenes.NotificationsShade to 1.takeIf { DualShade.isEnabled },
+                            Scenes.Shade to 1.takeUnless { DualShade.isEnabled },
+                            Scenes.QuickSettings to 2,
+                        )
+                        .filterValues { it != null }
+                        .mapValues { checkNotNull(it.value) }
             )
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
index cd1b965..551aa12 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.scene.domain.startable.SceneContainerStartable
 import com.android.systemui.scene.shared.model.SceneContainerConfig
 import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shade.shared.flag.DualShade
 import dagger.Binds
 import dagger.Module
 import dagger.Provides
@@ -40,6 +41,7 @@
             LockscreenSceneModule::class,
             QuickSettingsSceneModule::class,
             ShadeSceneModule::class,
+            NotificationsShadeSceneModule::class,
         ],
 )
 interface SceneContainerFrameworkModule {
@@ -61,27 +63,31 @@
         @Provides
         fun containerConfig(): SceneContainerConfig {
             return SceneContainerConfig(
-                // Note that this list is in z-order. The first one is the bottom-most and the
-                // last one is top-most.
+                // Note that this list is in z-order. The first one is the bottom-most and the last
+                // one is top-most.
                 sceneKeys =
-                    listOf(
+                    listOfNotNull(
                         Scenes.Gone,
                         Scenes.Communal,
                         Scenes.Lockscreen,
                         Scenes.Bouncer,
                         Scenes.QuickSettings,
-                        Scenes.Shade,
+                        Scenes.NotificationsShade.takeIf { DualShade.isEnabled },
+                        Scenes.Shade.takeUnless { DualShade.isEnabled },
                     ),
                 initialSceneKey = Scenes.Lockscreen,
                 navigationDistances =
                     mapOf(
-                        Scenes.Gone to 0,
-                        Scenes.Lockscreen to 0,
-                        Scenes.Communal to 1,
-                        Scenes.Shade to 2,
-                        Scenes.QuickSettings to 3,
-                        Scenes.Bouncer to 4,
-                    ),
+                            Scenes.Gone to 0,
+                            Scenes.Lockscreen to 0,
+                            Scenes.Communal to 1,
+                            Scenes.NotificationsShade to 2.takeIf { DualShade.isEnabled },
+                            Scenes.Shade to 2.takeUnless { DualShade.isEnabled },
+                            Scenes.QuickSettings to 3,
+                            Scenes.Bouncer to 4,
+                        )
+                        .filterValues { it != null }
+                        .mapValues { checkNotNull(it.value) }
             )
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt
index c59018b..ace4491 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt
@@ -125,6 +125,7 @@
                 Scenes.Communal -> true
                 Scenes.Gone -> true
                 Scenes.Lockscreen -> true
+                Scenes.NotificationsShade -> false
                 Scenes.QuickSettings -> false
                 Scenes.Shade -> false
                 else -> error("SceneKey \"$this\" doesn't have a mapping for canBeOccluded!")
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt
index e0b8b85..de3b87a 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt
@@ -51,7 +51,7 @@
     private val windowRootViewVisibilityRepository: WindowRootViewVisibilityRepository,
     private val keyguardRepository: KeyguardRepository,
     private val headsUpManager: HeadsUpManager,
-    private val powerInteractor: PowerInteractor,
+    powerInteractor: PowerInteractor,
     private val activeNotificationsInteractor: ActiveNotificationsInteractor,
     sceneInteractorProvider: Provider<SceneInteractor>,
 ) : CoreStartable {
@@ -77,11 +77,14 @@
                     when (state) {
                         is ObservableTransitionState.Idle ->
                             state.currentScene == Scenes.Shade ||
+                                state.currentScene == Scenes.NotificationsShade ||
                                 state.currentScene == Scenes.Lockscreen
                         is ObservableTransitionState.Transition ->
                             state.toScene == Scenes.Shade ||
+                                state.toScene == Scenes.NotificationsShade ||
                                 state.toScene == Scenes.Lockscreen ||
                                 state.fromScene == Scenes.Shade ||
+                                state.fromScene == Scenes.NotificationsShade ||
                                 state.fromScene == Scenes.Lockscreen
                     }
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
index f64e0af..9e57964 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
@@ -537,6 +537,7 @@
                                     Scenes.Lockscreen -> true
                                     Scenes.Bouncer -> false
                                     Scenes.Shade -> false
+                                    Scenes.NotificationsShade -> false
                                     else -> null
                                 }
                             }
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scenes.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scenes.kt
index 73fcca8..08f1be9 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scenes.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scenes.kt
@@ -42,11 +42,32 @@
     /** The lockscreen is the scene that shows when the device is locked. */
     @JvmField val Lockscreen = SceneKey("lockscreen")
 
+    /**
+     * The notifications shade scene primarily shows a scrollable list of notifications as an
+     * overlay UI.
+     *
+     * It's used only in the dual shade configuration, where there are two separate shades: one for
+     * notifications (this scene) and another for quick settings (where a separate scene is used).
+     *
+     * It's not used in the single/accordion configuration (swipe down once to reveal the shade,
+     * swipe down again the to expand quick settings) and for the "split" shade configuration (on
+     * large screens or unfolded foldables, where notifications and quick settings are shown
+     * side-by-side in their own columns).
+     */
+    @JvmField val NotificationsShade = SceneKey("notifications_shade")
+
     /** The quick settings scene shows the quick setting tiles. */
     @JvmField val QuickSettings = SceneKey("quick_settings")
 
     /**
-     * The shade is the scene whose primary purpose is to show a scrollable list of notifications.
+     * The shade is the scene that shows a scrollable list of notifications and the minimized
+     * version of quick settings (AKA "quick quick settings" or "QQS").
+     *
+     * This scene is used for single/accordion configuration (swipe down once to reveal the shade,
+     * swipe down again the to expand quick settings) and for the "split" shade configuration (on
+     * large screens or unfolded foldables, where notifications and quick settings are shown
+     * side-by-side in their own columns). For the dual shade configuration, where there are two
+     * separate shades: one for notifications and one for quick settings, other scenes are used.
      */
     @JvmField val Shade = SceneKey("shade")
 }
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt
index 451fd67..e4435cc 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt
@@ -51,7 +51,7 @@
 
     private fun destinationScenes(shadeMode: ShadeMode): Map<UserAction, UserActionResult> {
         return buildMap {
-            if (shadeMode == ShadeMode.Single) {
+            if (shadeMode is ShadeMode.Single) {
                 this[
                     Swipe(
                         pointerCount = 2,
@@ -60,7 +60,10 @@
                     )] = UserActionResult(Scenes.QuickSettings)
             }
 
-            this[Swipe(direction = SwipeDirection.Down)] = UserActionResult(Scenes.Shade)
+            this[Swipe(direction = SwipeDirection.Down)] =
+                UserActionResult(
+                    if (shadeMode is ShadeMode.Dual) Scenes.NotificationsShade else Scenes.Shade
+                )
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
index ef7829f..7fbede47 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
@@ -124,6 +124,7 @@
             when (toScene) {
                 Scenes.Bouncer -> Classifier.BOUNCER_UNLOCK
                 Scenes.Gone -> Classifier.UNLOCK
+                Scenes.NotificationsShade -> Classifier.NOTIFICATION_DRAG_DOWN
                 Scenes.Shade -> Classifier.NOTIFICATION_DRAG_DOWN
                 Scenes.QuickSettings -> Classifier.QUICK_SETTINGS
                 else -> null
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/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
index 8c15817..5cc30bd 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
@@ -32,6 +32,7 @@
 import com.android.systemui.scene.shared.model.TransitionKeys.SlightlyFasterShadeCollapse
 import com.android.systemui.shade.ShadeController.ShadeVisibilityListener
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.NotificationShadeWindowController
 import com.android.systemui.statusbar.VibratorHelper
@@ -194,7 +195,11 @@
     }
 
     override fun expandToNotifications() {
-        sceneInteractor.changeScene(Scenes.Shade, "ShadeController.animateExpandShade")
+        val shadeMode = shadeInteractor.shadeMode.value
+        sceneInteractor.changeScene(
+            if (shadeMode is ShadeMode.Dual) Scenes.NotificationsShade else Scenes.Shade,
+            "ShadeController.animateExpandShade"
+        )
     }
 
     override fun expandToQs() {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
index b934d63..7e1a310 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
@@ -16,6 +16,7 @@
 package com.android.systemui.shade.data.repository
 
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.shade.shared.flag.DualShade
 import com.android.systemui.shade.shared.model.ShadeMode
 import javax.inject.Inject
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -219,7 +220,7 @@
     @Deprecated("Use ShadeInteractor instead")
     override val legacyQsFullscreen: StateFlow<Boolean> = _legacyQsFullscreen.asStateFlow()
 
-    val _shadeMode = MutableStateFlow<ShadeMode>(ShadeMode.Single)
+    val _shadeMode = MutableStateFlow(if (DualShade.isEnabled) ShadeMode.Dual else ShadeMode.Single)
     override val shadeMode: StateFlow<ShadeMode> = _shadeMode.asStateFlow()
 
     override fun setShadeMode(shadeMode: ShadeMode) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt
index 3a8ba7a..c9949cd 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt
@@ -19,6 +19,7 @@
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
 import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shade.shared.model.ShadeMode
 import javax.inject.Inject
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 
@@ -34,7 +35,7 @@
     override fun animateCollapseQs(fullyCollapse: Boolean) {
         if (shadeInteractor.isQsExpanded.value) {
             val key =
-                if (fullyCollapse) {
+                if (fullyCollapse || shadeInteractor.shadeMode.value is ShadeMode.Dual) {
                     if (deviceEntryInteractor.isDeviceEntered.value) {
                         Scenes.Gone
                     } else {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt
index 6a8b9ee..9885fe4 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shade.data.repository.ShadeRepository
+import com.android.systemui.shade.shared.model.ShadeMode
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.delay
@@ -95,8 +96,9 @@
     }
 
     private fun changeToShadeScene() {
+        val shadeMode = shadeInteractor.shadeMode.value
         sceneInteractor.changeScene(
-            Scenes.Shade,
+            if (shadeMode is ShadeMode.Dual) Scenes.NotificationsShade else Scenes.Shade,
             "ShadeLockscreenInteractorImpl.expandToNotifications",
         )
     }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/startable/ShadeStartable.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/startable/ShadeStartable.kt
index f3802da..3f4bcba 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/startable/ShadeStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/startable/ShadeStartable.kt
@@ -29,6 +29,7 @@
 import com.android.systemui.shade.TouchLogger.Companion.logTouchesTo
 import com.android.systemui.shade.data.repository.ShadeRepository
 import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor
+import com.android.systemui.shade.shared.flag.DualShade
 import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.shade.transition.ScrimShadeTransitionController
 import com.android.systemui.statusbar.policy.SplitShadeStateController
@@ -67,19 +68,24 @@
     private fun hydrateShadeExpansionStateManager() {
         if (SceneContainerFlag.isEnabled) {
             combine(
-                panelExpansionInteractorProvider.get().legacyPanelExpansion,
-                sceneInteractorProvider.get().isTransitionUserInputOngoing,
-            ) { panelExpansion, tracking ->
-                shadeExpansionStateManager.onPanelExpansionChanged(
-                    fraction = panelExpansion,
-                    expanded = panelExpansion > 0f,
-                    tracking = tracking,
-                )
-            }.launchIn(applicationScope)
+                    panelExpansionInteractorProvider.get().legacyPanelExpansion,
+                    sceneInteractorProvider.get().isTransitionUserInputOngoing,
+                ) { panelExpansion, tracking ->
+                    shadeExpansionStateManager.onPanelExpansionChanged(
+                        fraction = panelExpansion,
+                        expanded = panelExpansion > 0f,
+                        tracking = tracking,
+                    )
+                }
+                .launchIn(applicationScope)
         }
     }
 
     private fun hydrateShadeMode() {
+        if (DualShade.isEnabled) {
+            shadeRepository.setShadeMode(ShadeMode.Dual)
+            return
+        }
         applicationScope.launch {
             configurationRepository.onAnyConfigurationChange
                 // Force initial collection.
@@ -90,11 +96,7 @@
                 }
                 .collect { isSplitShade ->
                     shadeRepository.setShadeMode(
-                        if (isSplitShade) {
-                            ShadeMode.Split
-                        } else {
-                            ShadeMode.Single
-                        }
+                        if (isSplitShade) ShadeMode.Split else ShadeMode.Single
                     )
                 }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/shared/model/ShadeMode.kt b/packages/SystemUI/src/com/android/systemui/shade/shared/model/ShadeMode.kt
index 3451eaf..8214a24 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/shared/model/ShadeMode.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/shared/model/ShadeMode.kt
@@ -38,4 +38,8 @@
      * a space on a small screen or folded device.
      */
     data object Dual : ShadeMode
+
+    companion object {
+        @JvmStatic fun dual(): Dual = Dual
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModel.kt
new file mode 100644
index 0000000..ba01776
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModel.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.shade.ui.viewmodel
+
+import com.android.compose.animation.scene.Back
+import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.Swipe
+import com.android.compose.animation.scene.UserAction
+import com.android.compose.animation.scene.UserActionResult
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+/** Models UI state and handles user input for the Notifications Shade scene. */
+@SysUISingleton
+class NotificationsShadeSceneViewModel
+@Inject
+constructor(
+    @Application private val applicationScope: CoroutineScope,
+    overlayShadeViewModel: OverlayShadeViewModel,
+) {
+    val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> =
+        overlayShadeViewModel.backgroundScene
+            .map(::destinationScenes)
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = destinationScenes(overlayShadeViewModel.backgroundScene.value),
+            )
+
+    private fun destinationScenes(backgroundScene: SceneKey): Map<UserAction, UserActionResult> {
+        return mapOf(
+            Swipe.Up to backgroundScene,
+            Back to backgroundScene,
+        )
+    }
+}
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/KeyboardShortcutListSearch.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
index 78e108d..0d8030f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
@@ -97,7 +97,7 @@
 public final class KeyboardShortcutListSearch {
     private static final String TAG = KeyboardShortcutListSearch.class.getSimpleName();
     private static final Object sLock = new Object();
-    @VisibleForTesting static KeyboardShortcutListSearch sInstance;
+    @VisibleForTesting public static KeyboardShortcutListSearch sInstance;
 
     private static int SHORTCUT_SYSTEM_INDEX = 0;
     private static int SHORTCUT_INPUT_INDEX = 1;
@@ -136,7 +136,7 @@
     };
 
     private final Handler mHandler = new Handler(Looper.getMainLooper());
-    @VisibleForTesting Context mContext;
+    @VisibleForTesting public Context mContext;
     private final IPackageManager mPackageManager;
 
     @VisibleForTesting BottomSheetDialog mKeyboardShortcutsBottomSheetDialog;
@@ -414,7 +414,7 @@
     private boolean mImeShortcutsReceived;
 
     @VisibleForTesting
-    void showKeyboardShortcuts(int deviceId) {
+    public void showKeyboardShortcuts(int deviceId) {
         retrieveKeyCharacterMap(deviceId);
         mAppShortcutsReceived = false;
         mImeShortcutsReceived = false;
@@ -502,7 +502,8 @@
         return keyboardShortcutMultiMappingGroups;
     }
 
-    private void dismissKeyboardShortcuts() {
+    @VisibleForTesting
+    public void dismissKeyboardShortcuts() {
         if (mKeyboardShortcutsBottomSheetDialog != null) {
             mKeyboardShortcutsBottomSheetDialog.dismiss();
             mKeyboardShortcutsBottomSheetDialog = null;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java
index 90567d8..21f608e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java
@@ -79,7 +79,7 @@
 public final class KeyboardShortcuts {
     private static final String TAG = KeyboardShortcuts.class.getSimpleName();
     private static final Object sLock = new Object();
-    @VisibleForTesting static KeyboardShortcuts sInstance;
+    @VisibleForTesting public static KeyboardShortcuts sInstance;
     private WindowManager mWindowManager;
 
     private final SparseArray<String> mSpecialCharacterNames = new SparseArray<>();
@@ -93,7 +93,7 @@
     };
 
     private final Handler mHandler = new Handler(Looper.getMainLooper());
-    @VisibleForTesting Context mContext;
+    @VisibleForTesting public Context mContext;
     private final IPackageManager mPackageManager;
     private final OnClickListener mDialogCloseListener = new DialogInterface.OnClickListener() {
         public void onClick(DialogInterface dialog, int id) {
@@ -373,7 +373,7 @@
     }
 
     @VisibleForTesting
-    void showKeyboardShortcuts(int deviceId) {
+    public void showKeyboardShortcuts(int deviceId) {
         retrieveKeyCharacterMap(deviceId);
         mReceivedAppShortcutGroups = null;
         mReceivedImeShortcutGroups = null;
@@ -407,7 +407,8 @@
         showKeyboardShortcutsDialog(shortcutGroups);
     }
 
-    private void dismissKeyboardShortcuts() {
+    @VisibleForTesting
+    public void dismissKeyboardShortcuts() {
         if (mKeyboardShortcutsDialog != null) {
             mKeyboardShortcutsDialog.dismiss();
             mKeyboardShortcutsDialog = null;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutsReceiver.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutsReceiver.java
index 1cfb400..815f1fc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutsReceiver.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutsReceiver.java
@@ -15,6 +15,8 @@
  */
 package com.android.systemui.statusbar;
 
+import static com.android.systemui.Flags.keyboardShortcutHelperRewrite;
+
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -25,21 +27,22 @@
 
 import javax.inject.Inject;
 
-/**
- * Receiver for the Keyboard Shortcuts Helper.
- */
+/** Receiver for the Keyboard Shortcuts Helper. */
 public class KeyboardShortcutsReceiver extends BroadcastReceiver {
 
-    private boolean mIsShortcutListSearchEnabled;
+    private final FeatureFlags mFeatureFlags;
 
     @Inject
     public KeyboardShortcutsReceiver(FeatureFlags featureFlags) {
-        mIsShortcutListSearchEnabled = featureFlags.isEnabled(Flags.SHORTCUT_LIST_SEARCH_LAYOUT);
+        mFeatureFlags = featureFlags;
     }
 
     @Override
     public void onReceive(Context context, Intent intent) {
-        if (mIsShortcutListSearchEnabled && Utilities.isLargeScreen(context)) {
+        if (keyboardShortcutHelperRewrite()) {
+            return;
+        }
+        if (isTabletLayoutFlagEnabled() && Utilities.isLargeScreen(context)) {
             if (Intent.ACTION_SHOW_KEYBOARD_SHORTCUTS.equals(intent.getAction())) {
                 KeyboardShortcutListSearch.show(context, -1 /* deviceId unknown */);
             } else if (Intent.ACTION_DISMISS_KEYBOARD_SHORTCUTS.equals(intent.getAction())) {
@@ -53,4 +56,8 @@
             }
         }
     }
+
+    private boolean isTabletLayoutFlagEnabled() {
+        return mFeatureFlags.isEnabled(Flags.SHORTCUT_LIST_SEARCH_LAYOUT);
+    }
 }
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/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
index 4f8c3caa..96a50f7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
@@ -677,6 +677,7 @@
             Scenes.Bouncer, StatusBarState.KEYGUARD,
             Scenes.Communal, StatusBarState.KEYGUARD,
             Scenes.Shade, StatusBarState.SHADE_LOCKED,
+            Scenes.NotificationsShade, StatusBarState.SHADE_LOCKED,
             Scenes.QuickSettings, StatusBarState.SHADE_LOCKED,
             Scenes.Gone, StatusBarState.SHADE
     );
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 7630d43..be6bef7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -26,10 +26,12 @@
 import static androidx.lifecycle.Lifecycle.State.RESUMED;
 
 import static com.android.systemui.Dependency.TIME_TICK_HANDLER_NAME;
+import static com.android.systemui.Flags.keyboardShortcutHelperRewrite;
 import static com.android.systemui.Flags.lightRevealMigration;
 import static com.android.systemui.Flags.newAodTransition;
 import static com.android.systemui.Flags.truncatedStatusBarIconsFix;
 import static com.android.systemui.charging.WirelessChargingAnimation.UNKNOWN_BATTERY_LEVEL;
+import static com.android.systemui.flags.Flags.SHORTCUT_LIST_SEARCH_LAYOUT;
 import static com.android.systemui.statusbar.NotificationLockscreenUserManager.PERMISSION_SELF;
 import static com.android.systemui.statusbar.StatusBarState.SHADE;
 
@@ -289,7 +291,6 @@
     private CentralSurfacesCommandQueueCallbacks mCommandQueueCallbacks;
     private float mTransitionToFullShadeProgress = 0f;
     private final NotificationListContainer mNotifListContainer;
-    private final boolean mIsShortcutListSearchEnabled;
 
     private final KeyguardStateController.Callback mKeyguardStateControllerCallback =
             new KeyguardStateController.Callback() {
@@ -789,7 +790,6 @@
         mStatusBarSignalPolicy = statusBarSignalPolicy;
         mStatusBarHideIconsForBouncerManager = statusBarHideIconsForBouncerManager;
         mFeatureFlags = featureFlags;
-        mIsShortcutListSearchEnabled = featureFlags.isEnabled(Flags.SHORTCUT_LIST_SEARCH_LAYOUT);
         mKeyguardUnlockAnimationController = keyguardUnlockAnimationController;
         mMainExecutor = delayableExecutor;
         mMessageRouter = messageRouter;
@@ -820,10 +820,13 @@
         // TODO(b/190746471): Find a better home for this.
         DateTimeView.setReceiverHandler(timeTickHandler);
 
-        mMessageRouter.subscribeTo(KeyboardShortcutsMessage.class,
-                data -> toggleKeyboardShortcuts(data.mDeviceId));
-        mMessageRouter.subscribeTo(MSG_DISMISS_KEYBOARD_SHORTCUTS_MENU,
-                id -> dismissKeyboardShortcuts());
+        if (!keyboardShortcutHelperRewrite()) {
+            mMessageRouter.subscribeTo(
+                    KeyboardShortcutsMessage.class,
+                    data -> toggleKeyboardShortcuts(data.mDeviceId));
+            mMessageRouter.subscribeTo(
+                    MSG_DISMISS_KEYBOARD_SHORTCUTS_MENU, id -> dismissKeyboardShortcuts());
+        }
         mMessageRouter.subscribeTo(AnimateExpandSettingsPanelMessage.class,
                 data -> mCommandQueueCallbacks.animateExpandSettingsPanel(data.mSubpanel));
         mMessageRouter.subscribeTo(MSG_LAUNCH_TRANSITION_TIMEOUT,
@@ -1872,10 +1875,12 @@
             String action = intent.getAction();
             String reason = intent.getStringExtra(SYSTEM_DIALOG_REASON_KEY);
             if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action)) {
-                if (mIsShortcutListSearchEnabled && Utilities.isLargeScreen(mContext)) {
-                    KeyboardShortcutListSearch.dismiss();
-                } else {
-                    KeyboardShortcuts.dismiss();
+                if (!keyboardShortcutHelperRewrite()) {
+                    if (shouldUseTabletKeyboardShortcuts()) {
+                        KeyboardShortcutListSearch.dismiss();
+                    } else {
+                        KeyboardShortcuts.dismiss();
+                    }
                 }
                 mRemoteInputManager.closeRemoteInputs();
                 if (mLockscreenUserManager.isCurrentProfile(getSendingUserId())) {
@@ -2345,6 +2350,7 @@
             } else if (mState == StatusBarState.KEYGUARD
                     && !mStatusBarKeyguardViewManager.primaryBouncerIsOrWillBeShowing()
                     && mStatusBarKeyguardViewManager.isSecure()) {
+                Log.d(TAG, "showBouncerOrLockScreenIfKeyguard, showingBouncer");
                 mStatusBarKeyguardViewManager.showBouncer(true /* scrimmed */);
             }
         }
@@ -2945,7 +2951,7 @@
     }
 
     protected void toggleKeyboardShortcuts(int deviceId) {
-        if (mIsShortcutListSearchEnabled && Utilities.isLargeScreen(mContext)) {
+        if (shouldUseTabletKeyboardShortcuts()) {
             KeyboardShortcutListSearch.toggle(mContext, deviceId);
         } else {
             KeyboardShortcuts.toggle(mContext, deviceId);
@@ -2953,13 +2959,18 @@
     }
 
     protected void dismissKeyboardShortcuts() {
-        if (mIsShortcutListSearchEnabled && Utilities.isLargeScreen(mContext)) {
+        if (shouldUseTabletKeyboardShortcuts()) {
             KeyboardShortcutListSearch.dismiss();
         } else {
             KeyboardShortcuts.dismiss();
         }
     }
 
+    private boolean shouldUseTabletKeyboardShortcuts() {
+        return mFeatureFlags.isEnabled(SHORTCUT_LIST_SEARCH_LAYOUT)
+                && Utilities.isLargeScreen(mContext);
+    }
+
     private void clearNotificationEffects() {
         try {
             mBarService.clearNotificationEffects();
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/src/com/android/systemui/statusbar/policy/BatteryController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java
index 37eda64..9273103 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java
@@ -17,11 +17,11 @@
 package com.android.systemui.statusbar.policy;
 
 import android.annotation.Nullable;
-import android.view.View;
 
 import androidx.annotation.NonNull;
 
 import com.android.systemui.Dumpable;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.demomode.DemoMode;
 import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback;
 
@@ -51,23 +51,23 @@
      *
      * Can pass the view that triggered the request.
      */
-    void setPowerSaveMode(boolean powerSave, @Nullable View view);
+    void setPowerSaveMode(boolean powerSave, @Nullable Expandable expandable);
 
     /**
      * Gets a reference to the last view used when called {@link #setPowerSaveMode}.
      */
     @Nullable
-    default WeakReference<View> getLastPowerSaverStartView() {
+    default WeakReference<Expandable> getLastPowerSaverStartExpandable() {
         return null;
     }
 
     /**
      * Clears the last view used when called {@link #setPowerSaveMode}.
      *
-     * Immediately after calling this, a call to {@link #getLastPowerSaverStartView()} should return
-     * {@code null}.
+     * Immediately after calling this, a call to {@link #getLastPowerSaverStartExpandable()} should
+     * return {@code null}.
      */
-    default void clearLastPowerSaverStartView() {}
+    default void clearLastPowerSaverStartExpandable() {}
 
     /**
      * Returns {@code true} if the device is currently plugged in.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
index dab27bb..6012ecd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
@@ -37,7 +37,6 @@
 import android.os.PowerManager;
 import android.os.PowerSaveState;
 import android.util.IndentingPrintWriter;
-import android.view.View;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -48,6 +47,7 @@
 import com.android.settingslib.fuelgauge.Estimate;
 import com.android.settingslib.utils.PowerUtil;
 import com.android.systemui.Dumpable;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
@@ -110,9 +110,10 @@
     private boolean mFetchingEstimate = false;
 
     // Use AtomicReference because we may request it from a different thread
-    // Use WeakReference because we are keeping a reference to a View that's not as long lived
-    // as this controller.
-    private AtomicReference<WeakReference<View>> mPowerSaverStartView = new AtomicReference<>();
+    // Use WeakReference because we are keeping a reference to an Expandable that's not as long
+    // lived as this controller.
+    private AtomicReference<WeakReference<Expandable>> mPowerSaverStartExpandable =
+            new AtomicReference<>();
 
     @VisibleForTesting
     public BatteryControllerImpl(
@@ -196,20 +197,20 @@
     }
 
     @Override
-    public void setPowerSaveMode(boolean powerSave, View view) {
-        if (powerSave) mPowerSaverStartView.set(new WeakReference<>(view));
+    public void setPowerSaveMode(boolean powerSave, Expandable expandable) {
+        if (powerSave) mPowerSaverStartExpandable.set(new WeakReference<>(expandable));
         BatterySaverUtils.setPowerSaveMode(mContext, powerSave, /*needFirstTimeWarning*/ true,
                 SAVER_ENABLED_QS);
     }
 
     @Override
-    public WeakReference<View> getLastPowerSaverStartView() {
-        return mPowerSaverStartView.get();
+    public WeakReference<Expandable> getLastPowerSaverStartExpandable() {
+        return mPowerSaverStartExpandable.get();
     }
 
     @Override
-    public void clearLastPowerSaverStartView() {
-        mPowerSaverStartView.set(null);
+    public void clearLastPowerSaverStartExpandable() {
+        mPowerSaverStartExpandable.set(null);
     }
 
     @Override
@@ -543,4 +544,4 @@
     public boolean isChargingSourceDock() {
         return mPluggedChargingSource == BatteryManager.BATTERY_PLUGGED_DOCK;
     }
-}
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManagerTest.java
index abc12ed..e9c742d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManagerTest.java
@@ -24,7 +24,6 @@
 import android.bluetooth.BluetoothDevice;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
-import android.view.View;
 
 import androidx.test.filters.SmallTest;
 
@@ -34,6 +33,7 @@
 import com.android.settingslib.bluetooth.LocalBluetoothManager;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.animation.DialogTransitionAnimator;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
 
 import org.junit.Before;
@@ -56,9 +56,10 @@
     @Rule
     public MockitoRule mockito = MockitoJUnit.rule();
 
-    private final View mView = new View(mContext);
     private final List<CachedBluetoothDevice> mCachedDevices = new ArrayList<>();
     @Mock
+    private Expandable mExpandable;
+    @Mock
     private DialogTransitionAnimator mDialogTransitionAnimator;
     @Mock
     private HearingDevicesDialogDelegate.Factory mDialogFactory;
@@ -97,7 +98,7 @@
     public void showDialog_bluetoothDisable_showPairNewDeviceTrue() {
         when(mLocalBluetoothAdapter.isEnabled()).thenReturn(false);
 
-        mManager.showDialog(mView);
+        mManager.showDialog(mExpandable);
 
         verify(mDialogFactory).create(eq(true));
     }
@@ -109,7 +110,7 @@
         when(mCachedDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
         mCachedDevices.add(mCachedDevice);
 
-        mManager.showDialog(mView);
+        mManager.showDialog(mExpandable);
 
         verify(mDialogFactory).create(eq(false));
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt
index fbe1184..e64df90 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt
@@ -28,10 +28,13 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
+import platform.test.motion.MotionTestRule
 import platform.test.motion.RecordedMotion
-import platform.test.motion.Sampling.Companion.evenlySampled
+import platform.test.motion.view.AnimationSampling.Companion.evenlySampled
 import platform.test.motion.view.DrawableFeatureCaptures
-import platform.test.motion.view.ViewMotionTestRule
+import platform.test.motion.view.ViewRecordingSpec.Companion.captureWithoutScreenshot
+import platform.test.motion.view.ViewToolkit
+import platform.test.motion.view.record
 import platform.test.screenshot.DeviceEmulationRule
 import platform.test.screenshot.DeviceEmulationSpec
 import platform.test.screenshot.DisplaySpec
@@ -64,7 +67,8 @@
 
     @get:Rule(order = 0) val deviceEmulationRule = DeviceEmulationRule(emulationSpec)
     @get:Rule(order = 1) val activityRule = ActivityScenarioRule(EmptyTestActivity::class.java)
-    @get:Rule(order = 2) val motionRule = ViewMotionTestRule(pathManager, { activityRule.scenario })
+    @get:Rule(order = 2)
+    val motionRule = MotionTestRule(ViewToolkit { activityRule.scenario }, pathManager)
 
     @Test
     fun backgroundAnimation_whenLaunching() {
@@ -151,15 +155,14 @@
         backgroundLayer: GradientDrawable,
         animator: AnimatorSet
     ): RecordedMotion {
-        return motionRule.checkThat(animator).record(
-            backgroundLayer,
-            evenlySampled(20),
-            visualCapture = null
-        ) {
-            capture(DrawableFeatureCaptures.bounds, "bounds")
-            capture(DrawableFeatureCaptures.cornerRadii, "corner_radii")
-            capture(DrawableFeatureCaptures.alpha, "alpha")
-        }
+        return motionRule.record(
+            animator,
+            backgroundLayer.captureWithoutScreenshot(evenlySampled(20)) {
+                feature(DrawableFeatureCaptures.bounds, "bounds")
+                feature(DrawableFeatureCaptures.cornerRadii, "corner_radii")
+                feature(DrawableFeatureCaptures.alpha, "alpha")
+            }
+        )
     }
 }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt
index b05d959..af1d315 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt
@@ -22,7 +22,6 @@
 import android.view.View
 import android.view.View.GONE
 import android.view.View.VISIBLE
-import android.widget.LinearLayout
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.UiEventLogger
 import com.android.settingslib.bluetooth.CachedBluetoothDevice
@@ -30,6 +29,7 @@
 import com.android.settingslib.flags.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.animation.Expandable
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.util.FakeSharedPreferences
@@ -100,6 +100,8 @@
     @Mock private lateinit var bluetoothTileDialogDelegate: BluetoothTileDialogDelegate
 
     @Mock private lateinit var sysuiDialog: SystemUIDialog
+    @Mock private lateinit var expandable: Expandable
+    @Mock private lateinit var controller: DialogTransitionAnimator.Controller
 
     private val sharedPreferences = FakeSharedPreferences()
 
@@ -157,6 +159,7 @@
             .thenReturn(getMutableStateFlow(false))
         whenever(audioSharingInteractor.audioSharingButtonStateUpdate)
             .thenReturn(getMutableStateFlow(AudioSharingButtonState.Gone))
+        whenever(expandable.dialogTransitionController(any())).thenReturn(controller)
     }
 
     @Test
@@ -164,16 +167,16 @@
         testScope.runTest {
             bluetoothTileDialogViewModel.showDialog(null)
 
-            verify(mDialogTransitionAnimator, never()).showFromView(any(), any(), any(), any())
+            verify(mDialogTransitionAnimator, never()).show(any(), any(), any())
         }
     }
 
     @Test
     fun testShowDialog_animated() {
         testScope.runTest {
-            bluetoothTileDialogViewModel.showDialog(LinearLayout(mContext))
+            bluetoothTileDialogViewModel.showDialog(expandable)
 
-            verify(mDialogTransitionAnimator).showFromView(any(), any(), nullable(), anyBoolean())
+            verify(mDialogTransitionAnimator).show(any(), any(), anyBoolean())
         }
     }
 
@@ -181,10 +184,9 @@
     fun testShowDialog_animated_callInBackgroundThread() {
         testScope.runTest {
             backgroundExecutor.execute {
-                bluetoothTileDialogViewModel.showDialog(LinearLayout(mContext))
+                bluetoothTileDialogViewModel.showDialog(expandable)
 
-                verify(mDialogTransitionAnimator)
-                    .showFromView(any(), any(), nullable(), anyBoolean())
+                verify(mDialogTransitionAnimator).show(any(), any(), anyBoolean())
             }
         }
     }
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/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index 325e7bf..6b1d39a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -241,7 +241,7 @@
                 .thenReturn(mock(Flow.class));
         when(mDreamViewModel.getTransitionEnded())
                 .thenReturn(mock(Flow.class));
-        when(mCommunalTransitionViewModel.getShowByDefault())
+        when(mCommunalTransitionViewModel.getShowCommunalFromOccluded())
                 .thenReturn(mock(Flow.class));
         when(mCommunalTransitionViewModel.getTransitionFromOccludedEnded())
                 .thenReturn(mock(Flow.class));
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/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
index 691d48f..1dc58d1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
@@ -26,8 +26,8 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
 import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.communal.domain.interactor.setCommunalAvailable
 import com.android.systemui.communal.shared.model.CommunalScenes
-import com.android.systemui.dock.DockManager
 import com.android.systemui.dock.fakeDockManager
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
@@ -1229,23 +1229,22 @@
         }
 
     @Test
-    fun occludedToGlanceableHubWhenDocked() =
+    fun occludedToGlanceableHubWhenInitiallyOnHub() =
         testScope.runTest {
-            // GIVEN a device on lockscreen
+            // GIVEN a device on lockscreen and communal is available
             keyguardRepository.setKeyguardShowing(true)
+            kosmos.setCommunalAvailable(true)
             runCurrent()
 
-            // GIVEN a prior transition has run to OCCLUDED
+            // GIVEN a prior transition has run to OCCLUDED from GLANCEABLE_HUB
             runTransitionAndSetWakefulness(KeyguardState.GLANCEABLE_HUB, KeyguardState.OCCLUDED)
             keyguardRepository.setKeyguardOccluded(true)
             runCurrent()
 
-            // GIVEN device is docked/communal is available
-            dockManager.setIsDocked(true)
-            dockManager.setDockEvent(DockManager.STATE_DOCKED)
+            // GIVEN on blank scene
             val idleTransitionState =
                 MutableStateFlow<ObservableTransitionState>(
-                    ObservableTransitionState.Idle(CommunalScenes.Communal)
+                    ObservableTransitionState.Idle(CommunalScenes.Blank)
                 )
             communalInteractor.setTransitionState(idleTransitionState)
             runCurrent()
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/power/PowerNotificationWarningsTest.java b/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java
index 9429725..b95d3aa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java
@@ -44,7 +44,6 @@
 import android.os.UserHandle;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
-import android.view.View;
 
 import androidx.test.filters.SmallTest;
 
@@ -53,6 +52,7 @@
 import com.android.settingslib.fuelgauge.BatterySaverUtils;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.animation.DialogTransitionAnimator;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.broadcast.BroadcastSender;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.settings.UserTracker;
@@ -88,7 +88,9 @@
     @Mock
     private UserTracker mUserTracker;
     @Mock
-    private View mView;
+    private Expandable mExpandable;
+    @Mock
+    private DialogTransitionAnimator.Controller mController;
     @Mock
     private SystemUIDialog.Factory mSystemUIDialogFactory;
     @Mock
@@ -234,32 +236,31 @@
 
     @Test
     public void testDialogStartedFromLauncher_viewVisible() {
-        when(mBatteryController.getLastPowerSaverStartView())
-                .thenReturn(new WeakReference<>(mView));
-        when(mView.isAggregatedVisible()).thenReturn(true);
+        when(mBatteryController.getLastPowerSaverStartExpandable())
+                .thenReturn(new WeakReference<>(mExpandable));
+        when(mExpandable.dialogTransitionController(any())).thenReturn(mController);
 
         Intent intent = new Intent(BatterySaverUtils.ACTION_SHOW_START_SAVER_CONFIRMATION);
         intent.putExtras(new Bundle());
 
         mReceiver.onReceive(mContext, intent);
 
-        verify(mDialogTransitionAnimator).showFromView(any(), eq(mView), any());
+        verify(mDialogTransitionAnimator).show(any(), eq(mController));
 
         mPowerNotificationWarnings.getSaverConfirmationDialog().dismiss();
     }
 
     @Test
     public void testDialogStartedNotFromLauncher_viewNotVisible() {
-        when(mBatteryController.getLastPowerSaverStartView())
-                .thenReturn(new WeakReference<>(mView));
-        when(mView.isAggregatedVisible()).thenReturn(false);
+        when(mBatteryController.getLastPowerSaverStartExpandable())
+                .thenReturn(new WeakReference<>(mExpandable));
 
         Intent intent = new Intent(BatterySaverUtils.ACTION_SHOW_START_SAVER_CONFIRMATION);
         intent.putExtras(new Bundle());
 
         mReceiver.onReceive(mContext, intent);
 
-        verify(mDialogTransitionAnimator, never()).showFromView(any(), any());
+        verify(mDialogTransitionAnimator, never()).show(any(), any());
 
         verify(mPowerNotificationWarnings.getSaverConfirmationDialog()).show();
         mPowerNotificationWarnings.getSaverConfirmationDialog().dismiss();
@@ -267,7 +268,7 @@
 
     @Test
     public void testDialogShownNotFromLauncher() {
-        when(mBatteryController.getLastPowerSaverStartView()).thenReturn(null);
+        when(mBatteryController.getLastPowerSaverStartExpandable()).thenReturn(null);
 
         Intent intent = new Intent(BatterySaverUtils.ACTION_SHOW_START_SAVER_CONFIRMATION);
         intent.putExtras(new Bundle());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
index ef7798e..5e14b1a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
@@ -43,7 +43,6 @@
 import android.os.UserHandle;
 import android.testing.AndroidTestingRunner;
 import android.util.SparseArray;
-import android.view.View;
 
 import androidx.annotation.Nullable;
 import androidx.test.filters.SmallTest;
@@ -51,6 +50,7 @@
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.util.CollectionUtils;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.classifier.FalsingManagerFake;
 import com.android.systemui.dump.nano.SystemUIProtoDump;
 import com.android.systemui.flags.FakeFeatureFlags;
@@ -734,7 +734,7 @@
         }
 
         @Override
-        protected void handleClick(@Nullable View view) {}
+        protected void handleClick(@Nullable Expandable expandable) {}
 
         @Override
         protected void handleUpdateState(State state, Object arg) {}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java
index df0ab34..8bf743884 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java
@@ -44,13 +44,13 @@
 import android.testing.TestableLooper;
 import android.text.TextUtils;
 import android.util.ArraySet;
-import android.view.View;
 
 import androidx.annotation.Nullable;
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.logging.InstanceId;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.qs.QSHost;
 import com.android.systemui.res.R;
@@ -395,13 +395,13 @@
         }
 
         @Override
-        public void click(@Nullable View view) {}
+        public void click(@Nullable Expandable expandable) {}
 
         @Override
-        public void secondaryClick(@Nullable View view) {}
+        public void secondaryClick(@Nullable Expandable expandable) {}
 
         @Override
-        public void longClick(@Nullable View view) {}
+        public void longClick(@Nullable Expandable expandable) {}
 
         @Override
         public void userSwitch(int currentUser) {}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt
index ef979d2..a8e9db5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt
@@ -35,11 +35,10 @@
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.view.IWindowManager
-import android.view.View
 import com.android.internal.logging.MetricsLogger
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.ActivityTransitionAnimator
-import com.android.systemui.animation.view.LaunchableFrameLayout
+import com.android.systemui.animation.Expandable
 import com.android.systemui.classifier.FalsingManagerFake
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.qs.QSTile
@@ -339,7 +338,7 @@
             tile.qsTile.activityLaunchForClick = pi
         }
 
-        tile.handleClick(mock(View::class.java))
+        tile.handleClick(mock(Expandable::class.java))
         testableLooper.processAllMessages()
 
         verify(activityStarter, never())
@@ -366,7 +365,7 @@
         val tile = CustomTile.create(customTileFactory, TILE_SPEC, mContext)
         tile.qsTile.activityLaunchForClick = pi
 
-        tile.handleClick(mock(LaunchableFrameLayout::class.java))
+        tile.handleClick(mock(Expandable::class.java))
 
         testableLooper.processAllMessages()
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java
index 22b1c7b..c706244 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java
@@ -51,7 +51,6 @@
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.testing.TestableLooper.RunWithLooper;
-import android.view.View;
 
 import androidx.annotation.Nullable;
 import androidx.test.filters.SmallTest;
@@ -63,6 +62,7 @@
 import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
 import com.android.systemui.InstanceIdSequenceFake;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.classifier.FalsingManagerFake;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.FalsingManager;
@@ -148,7 +148,7 @@
 
     @Test
     public void testClick_Metrics() {
-        mTile.click(null /* view */);
+        mTile.click(null /* expandable */);
         verify(mMetricsLogger).write(argThat(new TileLogMatcher(ACTION_QS_CLICK)));
         assertEquals(1, mUiEventLoggerFake.numLogs());
         UiEventLoggerFake.FakeUiEvent event = mUiEventLoggerFake.get(0);
@@ -159,7 +159,7 @@
     public void testClick_log() {
         when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE);
 
-        mTile.click(null /* view */);
+        mTile.click(null /* expandable */);
         verify(mQsLogger).logTileClick(eq(SPEC), eq(StatusBarState.SHADE), eq(Tile.STATE_ACTIVE),
                 anyInt());
     }
@@ -184,7 +184,7 @@
     @Test
     public void testClick_Metrics_Status_Bar_Status() {
         when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE);
-        mTile.click(null /* view */);
+        mTile.click(null /* expandable */);
         verify(mMetricsLogger).write(mLogCaptor.capture());
         assertEquals(StatusBarState.SHADE, mLogCaptor.getValue()
                 .getTaggedData(FIELD_STATUS_BAR_STATE));
@@ -193,12 +193,12 @@
     @Test
     public void testClick_falsing() {
         mFalsingManager.setFalseTap(true);
-        mTile.click(null /* view */);
+        mTile.click(null /* expandable */);
         mTestableLooper.processAllMessages();
         assertThat(mTile.mClicked).isFalse();
 
         mFalsingManager.setFalseTap(false);
-        mTile.click(null /* view */);
+        mTile.click(null /* expandable */);
         mTestableLooper.processAllMessages();
         assertThat(mTile.mClicked).isTrue();
     }
@@ -206,19 +206,19 @@
     @Test
     public void testLongClick_falsing() {
         mFalsingManager.setFalseLongTap(true);
-        mTile.longClick(null /* view */);
+        mTile.longClick(null /* expandable */);
         mTestableLooper.processAllMessages();
         assertThat(mTile.mLongClicked).isFalse();
 
         mFalsingManager.setFalseLongTap(false);
-        mTile.longClick(null /* view */);
+        mTile.longClick(null /* expandable */);
         mTestableLooper.processAllMessages();
         assertThat(mTile.mLongClicked).isTrue();
     }
 
     @Test
     public void testSecondaryClick_Metrics() {
-        mTile.secondaryClick(null /* view */);
+        mTile.secondaryClick(null /* expandable */);
         verify(mMetricsLogger).write(argThat(new TileLogMatcher(ACTION_QS_SECONDARY_CLICK)));
         assertEquals(1, mUiEventLoggerFake.numLogs());
         UiEventLoggerFake.FakeUiEvent event = mUiEventLoggerFake.get(0);
@@ -229,7 +229,7 @@
     public void testSecondaryClick_log() {
         when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE);
 
-        mTile.secondaryClick(null /* view */);
+        mTile.secondaryClick(null /* expandable */);
         verify(mQsLogger).logTileSecondaryClick(eq(SPEC), eq(StatusBarState.SHADE),
                 eq(Tile.STATE_ACTIVE), anyInt());
     }
@@ -254,7 +254,7 @@
     @Test
     public void testSecondaryClick_Metrics_Status_Bar_Status() {
         when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD);
-        mTile.secondaryClick(null /* view */);
+        mTile.secondaryClick(null /* expandable */);
         verify(mMetricsLogger).write(mLogCaptor.capture());
         assertEquals(StatusBarState.KEYGUARD, mLogCaptor.getValue()
                 .getTaggedData(FIELD_STATUS_BAR_STATE));
@@ -262,7 +262,7 @@
 
     @Test
     public void testLongClick_Metrics() {
-        mTile.longClick(null /* view */);
+        mTile.longClick(null /* expandable */);
         verify(mMetricsLogger).write(argThat(new TileLogMatcher(ACTION_QS_LONG_PRESS)));
         assertEquals(1, mUiEventLoggerFake.numLogs());
         UiEventLoggerFake.FakeUiEvent event = mUiEventLoggerFake.get(0);
@@ -274,7 +274,7 @@
     public void testLongClick_log() {
         when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE);
 
-        mTile.longClick(null /* view */);
+        mTile.longClick(null /* expandable */);
         verify(mQsLogger).logTileLongClick(eq(SPEC), eq(StatusBarState.SHADE),
                 eq(Tile.STATE_ACTIVE), anyInt());
     }
@@ -299,7 +299,7 @@
     @Test
     public void testLongClick_Metrics_Status_Bar_Status() {
         when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE_LOCKED);
-        mTile.click(null /* view */);
+        mTile.click(null /* expandable */);
         verify(mMetricsLogger).write(mLogCaptor.capture());
         assertEquals(StatusBarState.SHADE_LOCKED, mLogCaptor.getValue()
                 .getTaggedData(FIELD_STATUS_BAR_STATE));
@@ -560,12 +560,12 @@
         }
 
         @Override
-        protected void handleClick(@Nullable View view) {
+        protected void handleClick(@Nullable Expandable expandable) {
             mClicked = true;
         }
 
         @Override
-        protected void handleLongClick(@Nullable View view) {
+        protected void handleLongClick(@Nullable Expandable expandable) {
             mLongClicked = true;
         }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BatterySaverTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BatterySaverTileTest.kt
index 605dc14..2c49e92 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BatterySaverTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BatterySaverTileTest.kt
@@ -21,11 +21,10 @@
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.testing.TestableLooper.RunWithLooper
-import android.view.View
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.MetricsLogger
-import com.android.systemui.res.R
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.Expandable
 import com.android.systemui.classifier.FalsingManagerFake
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.qs.QSTile
@@ -34,6 +33,7 @@
 import com.android.systemui.qs.QsEventLogger
 import com.android.systemui.qs.logging.QSLogger
 import com.android.systemui.qs.tileimpl.QSTileImpl
+import com.android.systemui.res.R
 import com.android.systemui.statusbar.policy.BatteryController
 import com.android.systemui.util.settings.FakeSettings
 import com.android.systemui.util.settings.SecureSettings
@@ -59,24 +59,15 @@
         private const val USER = 10
     }
 
-    @Mock
-    private lateinit var userContext: Context
-    @Mock
-    private lateinit var qsHost: QSHost
-    @Mock
-    private lateinit var uiEventLogger: QsEventLogger
-    @Mock
-    private lateinit var metricsLogger: MetricsLogger
-    @Mock
-    private lateinit var statusBarStateController: StatusBarStateController
-    @Mock
-    private lateinit var activityStarter: ActivityStarter
-    @Mock
-    private lateinit var qsLogger: QSLogger
-    @Mock
-    private lateinit var batteryController: BatteryController
-    @Mock
-    private lateinit var view: View
+    @Mock private lateinit var userContext: Context
+    @Mock private lateinit var qsHost: QSHost
+    @Mock private lateinit var uiEventLogger: QsEventLogger
+    @Mock private lateinit var metricsLogger: MetricsLogger
+    @Mock private lateinit var statusBarStateController: StatusBarStateController
+    @Mock private lateinit var activityStarter: ActivityStarter
+    @Mock private lateinit var qsLogger: QSLogger
+    @Mock private lateinit var batteryController: BatteryController
+    @Mock private lateinit var expandable: Expandable
     private lateinit var secureSettings: SecureSettings
     private lateinit var testableLooper: TestableLooper
     private lateinit var tile: BatterySaverTile
@@ -91,7 +82,8 @@
 
         secureSettings = FakeSettings()
 
-        tile = BatterySaverTile(
+        tile =
+            BatterySaverTile(
                 qsHost,
                 uiEventLogger,
                 testableLooper.looper,
@@ -102,7 +94,8 @@
                 activityStarter,
                 qsLogger,
                 batteryController,
-                secureSettings)
+                secureSettings
+            )
 
         tile.initialize()
         testableLooper.processAllMessages()
@@ -131,23 +124,23 @@
     @Test
     fun testClickingPowerSavePassesView() {
         tile.onPowerSaveChanged(true)
-        tile.handleClick(view)
+        tile.handleClick(expandable)
 
         tile.onPowerSaveChanged(false)
-        tile.handleClick(view)
+        tile.handleClick(expandable)
 
-        verify(batteryController).setPowerSaveMode(true, view)
-        verify(batteryController).setPowerSaveMode(false, view)
+        verify(batteryController).setPowerSaveMode(true, expandable)
+        verify(batteryController).setPowerSaveMode(false, expandable)
     }
 
     @Test
     fun testStopListeningClearsViewInController() {
         clearInvocations(batteryController)
         tile.handleSetListening(true)
-        verify(batteryController, never()).clearLastPowerSaverStartView()
+        verify(batteryController, never()).clearLastPowerSaverStartExpandable()
 
         tile.handleSetListening(false)
-        verify(batteryController).clearLastPowerSaverStartView()
+        verify(batteryController).clearLastPowerSaverStartExpandable()
     }
 
     @Test
@@ -158,7 +151,7 @@
         tile.handleUpdateState(state, /* arg= */ null)
 
         assertThat(state.icon)
-                .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_battery_saver_icon_off))
+            .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_battery_saver_icon_off))
     }
 
     @Test
@@ -169,6 +162,6 @@
         tile.handleUpdateState(state, /* arg= */ null)
 
         assertThat(state.icon)
-                .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_battery_saver_icon_on))
+            .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_battery_saver_icon_on))
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt
index cca1344..1173fa3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt
@@ -26,12 +26,12 @@
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.view.ContextThemeWrapper
-import android.view.View
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.MetricsLogger
 import com.android.systemui.res.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.animation.Expandable
 import com.android.systemui.classifier.FalsingManagerFake
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.qs.QSTile
@@ -42,7 +42,6 @@
 import com.android.systemui.qs.tileimpl.QSTileImpl
 import com.android.systemui.statusbar.policy.ZenModeController
 import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.nullable
 import com.android.systemui.util.settings.FakeSettings
 import com.android.systemui.util.settings.SecureSettings
@@ -99,6 +98,12 @@
     @Mock
     private lateinit var hostDialog: Dialog
 
+    @Mock
+    private lateinit var expandable: Expandable
+
+    @Mock
+    private lateinit var controller: DialogTransitionAnimator.Controller
+
     private lateinit var secureSettings: SecureSettings
     private lateinit var testableLooper: TestableLooper
     private lateinit var tile: DndTile
@@ -119,6 +124,7 @@
             }
         }
         whenever(qsHost.context).thenReturn(wrappedContext)
+        whenever(expandable.dialogTransitionController(any())).thenReturn(controller)
 
         tile = DndTile(
             qsHost,
@@ -187,11 +193,10 @@
         secureSettings.putIntForUser(KEY, Settings.Secure.ZEN_DURATION_PROMPT, DEFAULT_USER)
         testableLooper.processAllMessages()
 
-        val view = View(context)
-        tile.handleClick(view)
+        tile.handleClick(expandable)
         testableLooper.processAllMessages()
 
-        verify(mDialogTransitionAnimator).showFromView(any(), eq(view), nullable(), anyBoolean())
+        verify(mDialogTransitionAnimator).show(any(), any(), anyBoolean())
     }
 
     @Test
@@ -201,8 +206,7 @@
         secureSettings.putIntForUser(KEY, 60, DEFAULT_USER)
         testableLooper.processAllMessages()
 
-        val view = View(context)
-        tile.handleClick(view)
+        tile.handleClick(expandable)
         testableLooper.processAllMessages()
 
         verify(mDialogTransitionAnimator, never())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/FontScalingTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/FontScalingTileTest.kt
index 1f5ebfe..1c42dd1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/FontScalingTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/FontScalingTileTest.kt
@@ -20,12 +20,12 @@
 import android.provider.Settings
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
-import android.view.View
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.MetricsLogger
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.accessibility.fontscaling.FontScalingDialogDelegate
 import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.animation.Expandable
 import com.android.systemui.classifier.FalsingManagerFake
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.statusbar.StatusBarStateController
@@ -37,7 +37,6 @@
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.nullable
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import org.junit.After
@@ -67,6 +66,8 @@
     @Mock private lateinit var keyguardStateController: KeyguardStateController
     @Mock private lateinit var fontScalingDialogDelegate: FontScalingDialogDelegate
     @Mock private lateinit var dialog: SystemUIDialog
+    @Mock private lateinit var expandable: Expandable
+    @Mock private lateinit var controller: DialogTransitionAnimator.Controller
 
     private lateinit var testableLooper: TestableLooper
     private lateinit var systemClock: FakeSystemClock
@@ -81,6 +82,7 @@
         testableLooper = TestableLooper.get(this)
         `when`(qsHost.getContext()).thenReturn(mContext)
         `when`(fontScalingDialogDelegate.createDialog()).thenReturn(dialog)
+        `when`(expandable.dialogTransitionController(any())).thenReturn(controller)
         systemClock = FakeSystemClock()
         backgroundDelayableExecutor = FakeExecutor(systemClock)
 
@@ -119,8 +121,7 @@
     @Test
     fun clickTile_screenUnlocked_showDialogAnimationFromView() {
         `when`(keyguardStateController.isShowing).thenReturn(false)
-        val view = View(context)
-        fontScalingTile.click(view)
+        fontScalingTile.click(expandable)
         testableLooper.processAllMessages()
 
         verify(activityStarter)
@@ -132,14 +133,13 @@
                 eq(false)
             )
         argumentCaptor.value.run()
-        verify(mDialogTransitionAnimator).showFromView(any(), eq(view), nullable(), anyBoolean())
+        verify(mDialogTransitionAnimator).show(any(), any(), anyBoolean())
     }
 
     @Test
     fun clickTile_onLockScreen_neverShowDialogAnimationFromView() {
         `when`(keyguardStateController.isShowing).thenReturn(true)
-        val view = View(context)
-        fontScalingTile.click(view)
+        fontScalingTile.click(expandable)
         testableLooper.processAllMessages()
 
         verify(activityStarter)
@@ -151,8 +151,7 @@
                 eq(false)
             )
         argumentCaptor.value.run()
-        verify(mDialogTransitionAnimator, never())
-            .showFromView(any(), eq(view), nullable(), anyBoolean())
+        verify(mDialogTransitionAnimator, never()).show(any(), any(), anyBoolean())
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HearingDevicesTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HearingDevicesTileTest.java
index 73aa54c..56671bf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HearingDevicesTileTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HearingDevicesTileTest.java
@@ -38,6 +38,7 @@
 import com.android.systemui.Flags;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.accessibility.hearingaid.HearingDevicesDialogManager;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.classifier.FalsingManagerFake;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -135,10 +136,10 @@
 
     @Test
     public void handleClick_dialogShown() {
-        View view = new View(mContext);
-        mTile.handleClick(view);
+        Expandable expandable = Expandable.fromView(new View(mContext));
+        mTile.handleClick(expandable);
         mTestableLooper.processAllMessages();
 
-        verify(mHearingDevicesDialogManager).showDialog(view);
+        verify(mHearingDevicesDialogManager).showDialog(expandable);
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
index 387f27d..effae5f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
@@ -241,6 +241,7 @@
             statusBarWinController,
             sysUiState,
             mock(),
+            mock(),
             userTracker,
             wakefulnessLifecycle,
             uiEventLogger,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionExecutorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionExecutorTest.kt
index 91f3912..5e7d8fb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionExecutorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionExecutorTest.kt
@@ -28,7 +28,6 @@
 import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.mock
 import com.google.common.truth.Truth.assertThat
-import kotlin.test.Ignore
 import kotlin.test.Test
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestCoroutineScheduler
@@ -56,7 +55,6 @@
 
     private lateinit var actionExecutor: ActionExecutor
 
-    @Ignore // Fixed with newer mockito version (in main)
     @Test
     fun startSharedTransition_callsLaunchIntent() = runTest {
         actionExecutor = createActionExecutor()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsReceiverTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsReceiverTest.java
index bedb2b3..e0eb99c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsReceiverTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsReceiverTest.java
@@ -16,120 +16,161 @@
 
 package com.android.systemui.statusbar;
 
+import static com.android.systemui.Flags.FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE;
+
 import static org.mockito.Mockito.anyInt;
 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.Intent;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.dx.mockito.inline.extended.StaticMockitoSession;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.flags.FakeFeatureFlags;
 import com.android.systemui.flags.Flags;
 import com.android.systemui.shared.recents.utilities.Utilities;
 
+import org.junit.After;
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
-import org.mockito.MockitoSession;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
+import org.mockito.MockitoAnnotations;
 import org.mockito.quality.Strictness;
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class KeyboardShortcutsReceiverTest extends SysuiTestCase {
 
-    @Rule public MockitoRule mockito = MockitoJUnit.rule();
+    private static final Intent SHOW_INTENT = new Intent(Intent.ACTION_SHOW_KEYBOARD_SHORTCUTS);
+    private static final Intent DISMISS_INTENT =
+            new Intent(Intent.ACTION_DISMISS_KEYBOARD_SHORTCUTS);
 
+    private StaticMockitoSession mockitoSession;
     private KeyboardShortcutsReceiver mKeyboardShortcutsReceiver;
-    private Intent mIntent;
-    private FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
+    private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
 
     @Mock private KeyboardShortcuts mKeyboardShortcuts;
     @Mock private KeyboardShortcutListSearch mKeyboardShortcutListSearch;
 
     @Before
     public void setUp() {
-        mIntent = new Intent(Intent.ACTION_SHOW_KEYBOARD_SHORTCUTS);
+        MockitoAnnotations.initMocks(this);
+        mSetFlagsRule.disableFlags(FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE);
         mKeyboardShortcuts.mContext = mContext;
         mKeyboardShortcutListSearch.mContext = mContext;
         KeyboardShortcuts.sInstance = mKeyboardShortcuts;
         KeyboardShortcutListSearch.sInstance = mKeyboardShortcutListSearch;
+
+        mKeyboardShortcutsReceiver = spy(new KeyboardShortcutsReceiver(mFeatureFlags));
+    }
+
+    @Before
+    public void startStaticMocking() {
+        mockitoSession =
+                ExtendedMockito.mockitoSession()
+                        .spyStatic(Utilities.class)
+                        .strictness(Strictness.LENIENT)
+                        .startMocking();
+    }
+
+    @After
+    public void endStaticMocking() {
+        mockitoSession.finishMocking();
     }
 
     @Test
     public void onReceive_whenFlagOffDeviceIsTablet_showKeyboardShortcuts() {
-        MockitoSession mockitoSession = ExtendedMockito.mockitoSession()
-                .spyStatic(Utilities.class)
-                .strictness(Strictness.LENIENT)
-                .startMocking();
         mFeatureFlags.set(Flags.SHORTCUT_LIST_SEARCH_LAYOUT, false);
-        mKeyboardShortcutsReceiver = spy(new KeyboardShortcutsReceiver(mFeatureFlags));
         when(Utilities.isLargeScreen(mContext)).thenReturn(true);
 
-        mKeyboardShortcutsReceiver.onReceive(mContext, mIntent);
+        mKeyboardShortcutsReceiver.onReceive(mContext, SHOW_INTENT);
 
         verify(mKeyboardShortcuts).showKeyboardShortcuts(anyInt());
         verify(mKeyboardShortcutListSearch, never()).showKeyboardShortcuts(anyInt());
-        mockitoSession.finishMocking();
     }
 
     @Test
     public void onReceive_whenFlagOffDeviceIsNotTablet_showKeyboardShortcuts() {
-        MockitoSession mockitoSession = ExtendedMockito.mockitoSession()
-                .spyStatic(Utilities.class)
-                .strictness(Strictness.LENIENT)
-                .startMocking();
         mFeatureFlags.set(Flags.SHORTCUT_LIST_SEARCH_LAYOUT, false);
-        mKeyboardShortcutsReceiver = spy(new KeyboardShortcutsReceiver(mFeatureFlags));
         when(Utilities.isLargeScreen(mContext)).thenReturn(false);
 
-        mKeyboardShortcutsReceiver.onReceive(mContext, mIntent);
+        mKeyboardShortcutsReceiver.onReceive(mContext, SHOW_INTENT);
 
         verify(mKeyboardShortcuts).showKeyboardShortcuts(anyInt());
         verify(mKeyboardShortcutListSearch, never()).showKeyboardShortcuts(anyInt());
-        mockitoSession.finishMocking();
     }
 
     @Test
     public void onReceive_whenFlagOnDeviceIsTablet_showKeyboardShortcutListSearch() {
-        MockitoSession mockitoSession = ExtendedMockito.mockitoSession()
-                .spyStatic(Utilities.class)
-                .strictness(Strictness.LENIENT)
-                .startMocking();
         mFeatureFlags.set(Flags.SHORTCUT_LIST_SEARCH_LAYOUT, true);
-        mKeyboardShortcutsReceiver = spy(new KeyboardShortcutsReceiver(mFeatureFlags));
         when(Utilities.isLargeScreen(mContext)).thenReturn(true);
 
-        mKeyboardShortcutsReceiver.onReceive(mContext, mIntent);
+        mKeyboardShortcutsReceiver.onReceive(mContext, SHOW_INTENT);
 
         verify(mKeyboardShortcuts, never()).showKeyboardShortcuts(anyInt());
         verify(mKeyboardShortcutListSearch).showKeyboardShortcuts(anyInt());
-        mockitoSession.finishMocking();
     }
 
     @Test
     public void onReceive_whenFlagOnDeviceIsNotTablet_showKeyboardShortcuts() {
-        MockitoSession mockitoSession = ExtendedMockito.mockitoSession()
-                .spyStatic(Utilities.class)
-                .strictness(Strictness.LENIENT)
-                .startMocking();
         mFeatureFlags.set(Flags.SHORTCUT_LIST_SEARCH_LAYOUT, true);
-        mKeyboardShortcutsReceiver = spy(new KeyboardShortcutsReceiver(mFeatureFlags));
         when(Utilities.isLargeScreen(mContext)).thenReturn(false);
 
-        mKeyboardShortcutsReceiver.onReceive(mContext, mIntent);
+        mKeyboardShortcutsReceiver.onReceive(mContext, SHOW_INTENT);
 
         verify(mKeyboardShortcuts).showKeyboardShortcuts(anyInt());
         verify(mKeyboardShortcutListSearch, never()).showKeyboardShortcuts(anyInt());
-        mockitoSession.finishMocking();
+    }
+
+    @Test
+    public void onShowIntent_rewriteFlagOn_oldFlagOn_isLargeScreen_doesNotLaunchOldVersions() {
+        mFeatureFlags.set(Flags.SHORTCUT_LIST_SEARCH_LAYOUT, true);
+        mSetFlagsRule.enableFlags(FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE);
+        when(Utilities.isLargeScreen(mContext)).thenReturn(true);
+
+        mKeyboardShortcutsReceiver.onReceive(mContext, SHOW_INTENT);
+
+        verifyZeroInteractions(mKeyboardShortcuts, mKeyboardShortcutListSearch);
+    }
+
+    @Test
+    public void onShowIntent_rewriteFlagOn_oldFlagOn_isSmallScreen_doesNotLaunchOldVersions() {
+        mFeatureFlags.set(Flags.SHORTCUT_LIST_SEARCH_LAYOUT, true);
+        mSetFlagsRule.enableFlags(FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE);
+        when(Utilities.isLargeScreen(mContext)).thenReturn(false);
+
+        mKeyboardShortcutsReceiver.onReceive(mContext, SHOW_INTENT);
+
+        verifyZeroInteractions(mKeyboardShortcuts, mKeyboardShortcutListSearch);
+    }
+
+    @Test
+    public void onDismissIntent_rewriteFlagOn_oldFlagOn_isLargeScreen_doesNotDismissOldVersions() {
+        mFeatureFlags.set(Flags.SHORTCUT_LIST_SEARCH_LAYOUT, true);
+        mSetFlagsRule.enableFlags(FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE);
+        when(Utilities.isLargeScreen(mContext)).thenReturn(true);
+
+        mKeyboardShortcutsReceiver.onReceive(mContext, DISMISS_INTENT);
+
+        verifyZeroInteractions(mKeyboardShortcuts, mKeyboardShortcutListSearch);
+    }
+
+    @Test
+    public void onDismissIntent_rewriteFlagOn_oldFlagOn_isSmallScreen_doesNotDismissOldVersions() {
+        mFeatureFlags.set(Flags.SHORTCUT_LIST_SEARCH_LAYOUT, true);
+        mSetFlagsRule.enableFlags(FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE);
+        when(Utilities.isLargeScreen(mContext)).thenReturn(false);
+
+        mKeyboardShortcutsReceiver.onReceive(mContext, DISMISS_INTENT);
+
+        verifyZeroInteractions(mKeyboardShortcuts, mKeyboardShortcutListSearch);
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index f666d8e..b9312d3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -21,9 +21,12 @@
 import static android.provider.Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED;
 import static android.provider.Settings.Global.HEADS_UP_ON;
 
+import static com.android.systemui.Flags.FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE;
 import static com.android.systemui.Flags.FLAG_LIGHT_REVEAL_MIGRATION;
+import static com.android.systemui.flags.Flags.SHORTCUT_LIST_SEARCH_LAYOUT;
 import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
 import static com.android.systemui.statusbar.StatusBarState.SHADE;
+import static com.android.systemui.statusbar.phone.CentralSurfaces.MSG_DISMISS_KEYBOARD_SHORTCUTS_MENU;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -42,6 +45,7 @@
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
 import static java.util.Collections.emptySet;
@@ -52,6 +56,8 @@
 import android.app.trust.TrustManager;
 import android.content.BroadcastReceiver;
 import android.content.IntentFilter;
+import android.content.res.Configuration;
+import android.graphics.Rect;
 import android.hardware.devicestate.DeviceState;
 import android.hardware.devicestate.DeviceStateManager;
 import android.hardware.display.AmbientDisplayConfiguration;
@@ -73,6 +79,8 @@
 import android.util.SparseArray;
 import android.view.ViewGroup;
 import android.view.ViewGroup.LayoutParams;
+import android.view.WindowManager;
+import android.view.WindowMetrics;
 
 import androidx.test.filters.SmallTest;
 
@@ -137,6 +145,8 @@
 import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.shade.ShadeLogger;
 import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.KeyboardShortcutListSearch;
+import com.android.systemui.statusbar.KeyboardShortcuts;
 import com.android.systemui.statusbar.KeyguardIndicationController;
 import com.android.systemui.statusbar.LightRevealScrim;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
@@ -202,6 +212,7 @@
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
+import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
 import java.io.ByteArrayOutputStream;
@@ -326,18 +337,21 @@
     @Mock IPowerManager mPowerManagerService;
     @Mock ActivityStarter mActivityStarter;
     @Mock private WindowRootViewVisibilityInteractor mWindowRootViewVisibilityInteractor;
+    @Mock private KeyboardShortcuts mKeyboardShortcuts;
+    @Mock private KeyboardShortcutListSearch mKeyboardShortcutListSearch;
 
     private ShadeController mShadeController;
     private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
     private final FakeGlobalSettings mFakeGlobalSettings = new FakeGlobalSettings();
     private final SystemSettings mSystemSettings = new FakeSettings();
     private final FakeEventLog mFakeEventLog = new FakeEventLog();
-    private final FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock);
+    private FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock);
     private final FakeExecutor mUiBgExecutor = new FakeExecutor(mFakeSystemClock);
     private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
     private final InitController mInitController = new InitController();
     private final DumpManager mDumpManager = new DumpManager();
     private final ScreenLifecycle mScreenLifecycle = new ScreenLifecycle(mDumpManager);
+    private MessageRouterImpl mMessageRouter = new MessageRouterImpl(mMainExecutor);
 
     private final BrightnessMirrorShowingInteractor mBrightnessMirrorShowingInteractor =
             mKosmos.getBrightnessMirrorShowingInteractor();
@@ -347,7 +361,7 @@
         MockitoAnnotations.initMocks(this);
 
         // Set default value to avoid IllegalStateException.
-        mFeatureFlags.set(Flags.SHORTCUT_LIST_SEARCH_LAYOUT, false);
+        mFeatureFlags.set(SHORTCUT_LIST_SEARCH_LAYOUT, false);
         mSetFlagsRule.enableFlags(FLAG_LIGHT_REVEAL_MIGRATION);
         // Turn AOD on and toggle feature flag for jank fixes
         mFeatureFlags.set(Flags.ZJ_285570694_LOCKSCREEN_TRANSITION_FROM_AOD, true);
@@ -462,6 +476,16 @@
     }
 
     private void createCentralSurfaces() {
+        mMainExecutor = new FakeExecutor(mFakeSystemClock);
+        mMessageRouter = new MessageRouterImpl(mMainExecutor);
+        mKeyboardShortcuts = mock(KeyboardShortcuts.class);
+        mKeyboardShortcutListSearch = mock(KeyboardShortcutListSearch.class);
+        // Test setup for legacy version
+        mKeyboardShortcuts.mContext = mContext;
+        mKeyboardShortcutListSearch.mContext = mContext;
+        KeyboardShortcuts.sInstance = mKeyboardShortcuts;
+        KeyboardShortcutListSearch.sInstance = mKeyboardShortcutListSearch;
+
         ConfigurationController configurationController = new ConfigurationControllerImpl(mContext);
         mCentralSurfaces = new CentralSurfacesImpl(
                 mContext,
@@ -554,7 +578,7 @@
                 mFeatureFlags,
                 mKeyguardUnlockAnimationController,
                 mMainExecutor,
-                new MessageRouterImpl(mMainExecutor),
+                mMessageRouter,
                 mWallpaperManager,
                 Optional.of(mStartingSurface),
                 mActivityTransitionAnimator,
@@ -1126,6 +1150,194 @@
         verify(mScrimController, never()).transitionTo(eq(ScrimState.BRIGHTNESS_MIRROR), any());
     }
 
+    @Test
+    public void dismissKeyboardShortcuts_largeScreen_bothFlagsEnabled_doesNotDismissAny() {
+        switchToLargeScreen();
+        mFeatureFlags.set(SHORTCUT_LIST_SEARCH_LAYOUT, true);
+        mSetFlagsRule.enableFlags(FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE);
+        createCentralSurfaces();
+
+        dismissKeyboardShortcuts();
+
+        verifyNoMoreInteractions(mKeyboardShortcuts, mKeyboardShortcutListSearch);
+    }
+
+    @Test
+    public void dismissKeyboardShortcuts_largeScreen_newFlagsDisabled_dismissesTabletVersion() {
+        switchToLargeScreen();
+        mFeatureFlags.set(SHORTCUT_LIST_SEARCH_LAYOUT, true);
+        mSetFlagsRule.disableFlags(FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE);
+        createCentralSurfaces();
+
+        dismissKeyboardShortcuts();
+
+        verify(mKeyboardShortcutListSearch).dismissKeyboardShortcuts();
+    }
+
+    @Test
+    public void dismissKeyboardShortcuts_largeScreen_bothFlagsDisabled_dismissesPhoneVersion() {
+        switchToLargeScreen();
+        mFeatureFlags.set(SHORTCUT_LIST_SEARCH_LAYOUT, false);
+        mSetFlagsRule.disableFlags(FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE);
+        createCentralSurfaces();
+
+        dismissKeyboardShortcuts();
+
+        verify(mKeyboardShortcuts).dismissKeyboardShortcuts();
+        verifyNoMoreInteractions(mKeyboardShortcutListSearch);
+    }
+
+    @Test
+    public void dismissKeyboardShortcuts_smallScreen_bothFlagsEnabled_doesNotDismissAny() {
+        switchToSmallScreen();
+        mFeatureFlags.set(SHORTCUT_LIST_SEARCH_LAYOUT, true);
+        mSetFlagsRule.enableFlags(FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE);
+        createCentralSurfaces();
+
+        dismissKeyboardShortcuts();
+
+        verifyNoMoreInteractions(mKeyboardShortcuts, mKeyboardShortcutListSearch);
+    }
+
+    @Test
+    public void dismissKeyboardShortcuts_smallScreen_newFlagsDisabled_dismissesPhoneVersion() {
+        switchToSmallScreen();
+        mFeatureFlags.set(SHORTCUT_LIST_SEARCH_LAYOUT, true);
+        mSetFlagsRule.disableFlags(FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE);
+        createCentralSurfaces();
+
+        dismissKeyboardShortcuts();
+
+        verify(mKeyboardShortcuts).dismissKeyboardShortcuts();
+        verifyNoMoreInteractions(mKeyboardShortcutListSearch);
+    }
+
+    @Test
+    public void dismissKeyboardShortcuts_smallScreen_bothFlagsDisabled_dismissesPhoneVersion() {
+        switchToSmallScreen();
+        mFeatureFlags.set(SHORTCUT_LIST_SEARCH_LAYOUT, false);
+        mSetFlagsRule.disableFlags(FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE);
+        createCentralSurfaces();
+
+        dismissKeyboardShortcuts();
+
+        verify(mKeyboardShortcuts).dismissKeyboardShortcuts();
+        verifyNoMoreInteractions(mKeyboardShortcutListSearch);
+    }
+
+    @Test
+    public void toggleKeyboardShortcuts_largeScreen_bothFlagsEnabled_doesNotTogglesAny() {
+        switchToLargeScreen();
+        mFeatureFlags.set(SHORTCUT_LIST_SEARCH_LAYOUT, true);
+        mSetFlagsRule.enableFlags(FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE);
+        createCentralSurfaces();
+
+        int deviceId = 321;
+        toggleKeyboardShortcuts(/* deviceId= */ deviceId);
+
+        verifyNoMoreInteractions(mKeyboardShortcuts, mKeyboardShortcutListSearch);
+    }
+
+    @Test
+    public void toggleKeyboardShortcuts_largeScreen_newFlagsDisabled_togglesTabletVersion() {
+        switchToLargeScreen();
+        mFeatureFlags.set(SHORTCUT_LIST_SEARCH_LAYOUT, true);
+        mSetFlagsRule.disableFlags(FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE);
+        createCentralSurfaces();
+
+        int deviceId = 654;
+        toggleKeyboardShortcuts(deviceId);
+
+        verify(mKeyboardShortcutListSearch).showKeyboardShortcuts(deviceId);
+        verifyNoMoreInteractions(mKeyboardShortcuts);
+    }
+
+    @Test
+    public void toggleKeyboardShortcuts_largeScreen_bothFlagsDisabled_togglesPhoneVersion() {
+        switchToLargeScreen();
+        mFeatureFlags.set(SHORTCUT_LIST_SEARCH_LAYOUT, false);
+        mSetFlagsRule.disableFlags(FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE);
+        createCentralSurfaces();
+
+        int deviceId = 987;
+        toggleKeyboardShortcuts(deviceId);
+
+        verify(mKeyboardShortcuts).showKeyboardShortcuts(deviceId);
+        verifyNoMoreInteractions(mKeyboardShortcutListSearch);
+    }
+
+    @Test
+    public void toggleKeyboardShortcuts_smallScreen_bothFlagsEnabled_doesNotToggleAny() {
+        switchToSmallScreen();
+        mFeatureFlags.set(SHORTCUT_LIST_SEARCH_LAYOUT, true);
+        mSetFlagsRule.enableFlags(FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE);
+        createCentralSurfaces();
+
+        int deviceId = 789;
+        toggleKeyboardShortcuts(/* deviceId= */ deviceId);
+
+        verifyNoMoreInteractions(mKeyboardShortcuts, mKeyboardShortcutListSearch);
+    }
+
+    @Test
+    public void toggleKeyboardShortcuts_smallScreen_newFlagsDisabled_togglesPhoneVersion() {
+        switchToSmallScreen();
+        mFeatureFlags.set(SHORTCUT_LIST_SEARCH_LAYOUT, true);
+        mSetFlagsRule.disableFlags(FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE);
+        createCentralSurfaces();
+
+        int deviceId = 456;
+        toggleKeyboardShortcuts(deviceId);
+
+        verify(mKeyboardShortcuts).showKeyboardShortcuts(deviceId);
+        verifyNoMoreInteractions(mKeyboardShortcutListSearch);
+    }
+
+    @Test
+    public void toggleKeyboardShortcuts_smallScreen_bothFlagsDisabled_togglesPhoneVersion() {
+        switchToSmallScreen();
+        mFeatureFlags.set(SHORTCUT_LIST_SEARCH_LAYOUT, false);
+        mSetFlagsRule.disableFlags(FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE);
+        createCentralSurfaces();
+
+        int deviceId = 123;
+        toggleKeyboardShortcuts(deviceId);
+
+        verify(mKeyboardShortcuts).showKeyboardShortcuts(deviceId);
+        verifyNoMoreInteractions(mKeyboardShortcutListSearch);
+    }
+
+    private void dismissKeyboardShortcuts() {
+        mMessageRouter.sendMessage(MSG_DISMISS_KEYBOARD_SHORTCUTS_MENU);
+        mMainExecutor.runAllReady();
+    }
+
+    private void toggleKeyboardShortcuts(int deviceId) {
+        mMessageRouter.sendMessage(new CentralSurfaces.KeyboardShortcutsMessage(deviceId));
+        mMainExecutor.runAllReady();
+    }
+
+    private void switchToLargeScreen() {
+        switchToScreenSize(1280, 800);
+    }
+
+    private void switchToSmallScreen() {
+        switchToScreenSize(504, 1122);
+    }
+
+    private void switchToScreenSize(int widthDp, int heightDp) {
+        WindowMetrics windowMetrics = Mockito.mock(WindowMetrics.class);
+        WindowManager windowManager = Mockito.mock(WindowManager.class);
+
+        Configuration configuration = new Configuration();
+        configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT;
+        mContext.getOrCreateTestableResources().overrideConfiguration(configuration);
+
+        when(windowMetrics.getBounds()).thenReturn(new Rect(0, 0, widthDp, heightDp));
+        when(windowManager.getCurrentWindowMetrics()).thenReturn(windowMetrics);
+        mContext.addMockSystemService(WindowManager.class, windowManager);
+    }
+
     /**
      * Configures the appropriate mocks and then calls {@link CentralSurfacesImpl#updateIsKeyguard}
      * to reconfigure the keyguard to reflect the requested showing/occluded states.
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/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java
index 9d4f1fc..ed8843b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java
@@ -38,13 +38,13 @@
 import android.os.PowerSaveState;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
-import android.view.View;
 
 import androidx.test.filters.SmallTest;
 
 import com.android.dx.mockito.inline.extended.StaticInOrder;
 import com.android.settingslib.fuelgauge.BatterySaverUtils;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.demomode.DemoModeController;
 import com.android.systemui.dump.DumpManager;
@@ -72,7 +72,7 @@
     @Mock private PowerManager mPowerManager;
     @Mock private BroadcastDispatcher mBroadcastDispatcher;
     @Mock private DemoModeController mDemoModeController;
-    @Mock private View mView;
+    @Mock private Expandable mExpandable;
     @Mock private UsbPort mUsbPort;
     @Mock private UsbManager mUsbManager;
     @Mock private UsbPortStatus mUsbPortStatus;
@@ -175,8 +175,8 @@
 
     @Test
     public void testBatteryUtilsCalledOnSetPowerSaveMode() {
-        mBatteryController.setPowerSaveMode(true, mView);
-        mBatteryController.setPowerSaveMode(false, mView);
+        mBatteryController.setPowerSaveMode(true, mExpandable);
+        mBatteryController.setPowerSaveMode(false, mExpandable);
 
         StaticInOrder inOrder = inOrder(staticMockMarker(BatterySaverUtils.class));
         inOrder.verify(() -> BatterySaverUtils.setPowerSaveMode(getContext(), true, true,
@@ -187,21 +187,21 @@
 
     @Test
     public void testSaveViewReferenceWhenSettingPowerSaveMode() {
-        mBatteryController.setPowerSaveMode(false, mView);
+        mBatteryController.setPowerSaveMode(false, mExpandable);
 
-        Assert.assertNull(mBatteryController.getLastPowerSaverStartView());
+        Assert.assertNull(mBatteryController.getLastPowerSaverStartExpandable());
 
-        mBatteryController.setPowerSaveMode(true, mView);
+        mBatteryController.setPowerSaveMode(true, mExpandable);
 
-        Assert.assertSame(mView, mBatteryController.getLastPowerSaverStartView().get());
+        Assert.assertSame(mExpandable, mBatteryController.getLastPowerSaverStartExpandable().get());
     }
 
     @Test
     public void testClearViewReference() {
-        mBatteryController.setPowerSaveMode(true, mView);
-        mBatteryController.clearLastPowerSaverStartView();
+        mBatteryController.setPowerSaveMode(true, mExpandable);
+        mBatteryController.clearLastPowerSaverStartExpandable();
 
-        Assert.assertNull(mBatteryController.getLastPowerSaverStartView());
+        Assert.assertNull(mBatteryController.getLastPowerSaverStartExpandable());
     }
 
     @Test
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
index 3dd382f..3fe6973 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
@@ -23,14 +23,15 @@
 import com.android.systemui.communal.data.repository.communalRepository
 import com.android.systemui.communal.data.repository.communalWidgetRepository
 import com.android.systemui.communal.widgets.EditWidgetsActivityStarter
-import com.android.systemui.dock.fakeDockManager
 import com.android.systemui.flags.Flags
 import com.android.systemui.flags.fakeFeatureFlagsClassic
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.plugins.activityStarter
 import com.android.systemui.scene.domain.interactor.sceneInteractor
@@ -42,6 +43,7 @@
 val Kosmos.communalInteractor by Fixture {
     CommunalInteractor(
         applicationScope = applicationCoroutineScope,
+        bgDispatcher = testDispatcher,
         broadcastDispatcher = broadcastDispatcher,
         communalRepository = communalRepository,
         widgetRepository = communalWidgetRepository,
@@ -49,13 +51,13 @@
         mediaRepository = communalMediaRepository,
         smartspaceRepository = smartspaceRepository,
         keyguardInteractor = keyguardInteractor,
+        keyguardTransitionInteractor = keyguardTransitionInteractor,
         communalSettingsInteractor = communalSettingsInteractor,
         appWidgetHost = mock(),
         editWidgetsActivityStarter = editWidgetsActivityStarter,
         userTracker = userTracker,
         activityStarter = activityStarter,
         userManager = userManager,
-        dockManager = fakeDockManager,
         sceneInteractor = sceneInteractor,
         logBuffer = logcatLogBuffer("CommunalInteractor"),
         tableLogBuffer = mock(),
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/actions/FakeQSTileIntentUserInputHandler.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/actions/FakeQSTileIntentUserInputHandler.kt
index 0307c41..c4bf8ff 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/actions/FakeQSTileIntentUserInputHandler.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/actions/FakeQSTileIntentUserInputHandler.kt
@@ -18,7 +18,7 @@
 
 import android.app.PendingIntent
 import android.content.Intent
-import android.view.View
+import com.android.systemui.animation.Expandable
 
 /**
  * Fake implementation of [QSTileIntentUserInputHandler] interface. Consider using this alongside
@@ -31,22 +31,24 @@
 
     private val mutableInputs = mutableListOf<Input>()
 
-    override fun handle(view: View?, intent: Intent) {
-        mutableInputs.add(Input.Intent(view, intent))
+    override fun handle(expandable: Expandable?, intent: Intent) {
+        mutableInputs.add(Input.Intent(expandable, intent))
     }
 
     override fun handle(
-        view: View?,
+        expandable: Expandable?,
         pendingIntent: PendingIntent,
         requestLaunchingDefaultActivity: Boolean
     ) {
-        mutableInputs.add(Input.PendingIntent(view, pendingIntent, requestLaunchingDefaultActivity))
+        mutableInputs.add(
+            Input.PendingIntent(expandable, pendingIntent, requestLaunchingDefaultActivity)
+        )
     }
 
     sealed interface Input {
-        data class Intent(val view: View?, val intent: android.content.Intent) : Input
+        data class Intent(val expandable: Expandable?, val intent: android.content.Intent) : Input
         data class PendingIntent(
-            val view: View?,
+            val expandable: Expandable?,
             val pendingIntent: android.app.PendingIntent,
             val requestLaunchingDefaultActivity: Boolean
         ) : Input
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/QSTileInputTestKtx.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/QSTileInputTestKtx.kt
index 832b07a..9cb76bb 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/QSTileInputTestKtx.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/QSTileInputTestKtx.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.qs.tiles.base.interactor
 
 import android.os.UserHandle
-import android.view.View
+import com.android.systemui.animation.Expandable
 import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
 
 object QSTileInputTestKtx {
@@ -25,12 +25,12 @@
     fun <T> click(
         data: T,
         user: UserHandle = UserHandle.CURRENT,
-        view: View? = null,
-    ): QSTileInput<T> = QSTileInput(user, QSTileUserAction.Click(view), data)
+        expandable: Expandable? = null,
+    ): QSTileInput<T> = QSTileInput(user, QSTileUserAction.Click(expandable), data)
 
     fun <T> longClick(
         data: T,
         user: UserHandle = UserHandle.CURRENT,
-        view: View? = null,
-    ): QSTileInput<T> = QSTileInput(user, QSTileUserAction.LongClick(view), data)
+        expandable: Expandable? = null,
+    ): QSTileInput<T> = QSTileInput(user, QSTileUserAction.LongClick(expandable), data)
 }
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/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelKosmos.kt
new file mode 100644
index 0000000..5e71227
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelKosmos.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.shade.ui.viewmodel
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+
+val Kosmos.notificationsShadeSceneViewModel: NotificationsShadeSceneViewModel by
+    Kosmos.Fixture {
+        NotificationsShadeSceneViewModel(
+            applicationScope = applicationCoroutineScope,
+            overlayShadeViewModel = overlayShadeViewModel,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeBatteryController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeBatteryController.java
index d798b3b..0364e05 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeBatteryController.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeBatteryController.java
@@ -16,8 +16,8 @@
 
 import android.os.Bundle;
 import android.testing.LeakCheck;
-import android.view.View;
 
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback;
 
@@ -61,7 +61,7 @@
      * Note: this method ignores the View argument
      */
     @Override
-    public void setPowerSaveMode(boolean powerSave, View view) {
+    public void setPowerSaveMode(boolean powerSave, Expandable expandable) {
         setPowerSaveMode(powerSave);
     }
 
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index de928b5..726a01c 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -1024,6 +1024,12 @@
                                     intent.getStringExtra(Intent.EXTRA_SETTING_PREVIOUS_VALUE),
                                     intent.getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE));
                         }
+                    } else if (Settings.Secure.ACCESSIBILITY_QS_TARGETS.equals(which)) {
+                        if (!android.view.accessibility.Flags.a11yQsShortcut()) {
+                            return;
+                        }
+                        restoreAccessibilityQsTargets(
+                                intent.getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE));
                     }
                 }
             }
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/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index c372b3f..4ca9e33 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -2415,7 +2415,9 @@
                                         != ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE) {
                         // Calling startForeground on a FGS type which has a time limit will only be
                         // allowed if the app is in a state where it can normally start another FGS
-                        // and it hasn't hit the time limit for that type in the past 24hrs.
+                        // and it hasn't hit its time limit in the past 24hrs, or it has been in the
+                        // foreground after it hit its time limit, or it is currently in the
+                        // TOP (or better) proc state.
 
                         // See if the app could start an FGS or not.
                         r.clearFgsAllowStart();
@@ -2441,11 +2443,13 @@
                                             SystemClock.elapsedRealtime() - (24 * 60 * 60 * 1000));
                                 final long lastTimeOutAt = fgsTypeInfo.getTimeLimitExceededAt();
                                 if (fgsTypeInfo.getFirstFgsStartRealtime() < before24Hr
+                                        || r.app.mState.getCurProcState() <= PROCESS_STATE_TOP
                                         || (lastTimeOutAt != Long.MIN_VALUE
                                             && r.app.mState.getLastTopTime() > lastTimeOutAt)) {
                                     // Reset the time limit info for this fgs type if it has been
-                                    // more than 24hrs since the first fgs start or if the app was
-                                    // in the TOP state after time limit was exhausted.
+                                    // more than 24hrs since the first fgs start or if the app is
+                                    // currently in the TOP state or was in the TOP state after
+                                    // the time limit was exhausted previously.
                                     fgsTypeInfo.reset();
                                 } else if (lastTimeOutAt > 0) {
                                     // Time limit was exhausted within the past 24 hours and the app
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 1b59c18..1b3b198 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -4342,6 +4342,7 @@
 
             mServices.bringDownDisabledPackageServicesLocked(
                     packageName, null, userId, false, true, true);
+            mServices.onUidRemovedLocked(uid);
 
             if (mBooted) {
                 mAtmInternal.resumeTopActivities(true);
@@ -4372,9 +4373,10 @@
             Slog.w(TAG, "Can't force stop all processes of all users, that is insane!");
         }
 
+        final int uid = getPackageManagerInternal().getPackageUid(packageName,
+                            MATCH_DEBUG_TRIAGED_MISSING | MATCH_ANY_USER, UserHandle.USER_SYSTEM);
         if (appId < 0 && packageName != null) {
-            appId = UserHandle.getAppId(getPackageManagerInternal().getPackageUid(packageName,
-                    MATCH_DEBUG_TRIAGED_MISSING | MATCH_ANY_USER, UserHandle.USER_SYSTEM));
+            appId = UserHandle.getAppId(uid);
         }
 
         boolean didSomething;
@@ -4418,6 +4420,7 @@
             }
             didSomething = true;
         }
+        mServices.onUidRemovedLocked(uid);
 
         if (packageName == null) {
             // Remove all sticky broadcasts from this user.
@@ -19950,6 +19953,20 @@
                 return !ActivityManagerService.this.mThemeOverlayReadyUsers.contains(userId);
             }
         }
+
+        @Override
+        public void addStartInfoTimestamp(int key, long timestampNs, int uid, int pid,
+                int userId) {
+            // For the simplification, we don't support USER_ALL nor USER_CURRENT here.
+            if (userId == UserHandle.USER_ALL || userId == UserHandle.USER_CURRENT) {
+                throw new IllegalArgumentException("Unsupported userId");
+            }
+
+            mUserController.handleIncomingUser(pid, uid, userId, true,
+                    ALLOW_NON_FULL, "addStartInfoTimestampSystem", null);
+
+            addStartInfoTimestampInternal(key, timestampNs, userId, uid);
+        }
     }
 
     long inputDispatchingTimedOut(int pid, final boolean aboveSystem, TimeoutRecord timeoutRecord) {
@@ -20266,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/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index da528a2..475334c 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -274,8 +274,11 @@
     }
 
     /*package*/ void setBluetoothA2dpOn_Async(boolean on, String source) {
-        mBluetoothA2dpEnabled.set(on);
-        sendLMsgNoDelay(MSG_L_SET_FORCE_BT_A2DP_USE, SENDMSG_REPLACE, source);
+        boolean wasOn = mBluetoothA2dpEnabled.getAndSet(on);
+        // do not mute music if we do not anticipate a change in A2DP ON state
+        sendLMsgNoDelay(wasOn == on
+                ? MSG_L_SET_FORCE_BT_A2DP_USE_NO_MUTE : MSG_L_SET_FORCE_BT_A2DP_USE,
+                SENDMSG_REPLACE, source);
     }
 
     /**
@@ -1803,6 +1806,7 @@
                     onSetForceUse(msg.arg1, msg.arg2, false, (String) msg.obj);
                     break;
                 case MSG_L_SET_FORCE_BT_A2DP_USE:
+                case MSG_L_SET_FORCE_BT_A2DP_USE_NO_MUTE:
                     int forcedUsage = mBluetoothA2dpEnabled.get()
                             ? AudioSystem.FORCE_NONE : AudioSystem.FORCE_NO_BT_A2DP;
                     onSetForceUse(AudioSystem.FOR_MEDIA, forcedUsage, true, (String) msg.obj);
@@ -2139,8 +2143,7 @@
     private static final int MSG_I_UPDATE_LE_AUDIO_GROUP_ADDRESSES = 57;
     private static final int MSG_L_SYNCHRONIZE_ADI_DEVICES_IN_INVENTORY = 58;
     private static final int MSG_IL_UPDATED_ADI_DEVICE_STATE = 59;
-
-
+    private static final int MSG_L_SET_FORCE_BT_A2DP_USE_NO_MUTE = 60;
 
     private static boolean isMessageHandledUnderWakelock(int msgId) {
         switch(msgId) {
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/wallpaper/WallpaperCropper.java b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
index b19bc7d..dd3d512 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
@@ -313,10 +313,14 @@
                 adjustedCrop.right -= widthToRemove / 2 + widthToRemove % 2;
             }
         } else {
-            // TODO (b/281648899) the third case is not always correct, fix that.
+            // Note: the third case when MODE == BALANCE, -W + sqrt(W * H * R), is the width to add
+            // so that, when removing the appropriate height, we get a bitmap of aspect ratio R and
+            // total surface of W * H. In other words it is the width to add to get the desired
+            // aspect ratio R, while preserving the total number of pixels W * H.
             int widthToAdd = mode == REMOVE ? 0
                     : mode == ADD ? (int) (0.5 + crop.height() * screenRatio - crop.width())
-                    : (int) (0.5 + crop.height() - crop.width());
+                    : (int) (0.5 - crop.width()
+                            + Math.sqrt(crop.width() * crop.height() * screenRatio));
             int availableWidth = bitmapSize.x - crop.width();
             if (availableWidth >= widthToAdd) {
                 int widthToAddLeft = widthToAdd / 2;
diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
index 6ec557a..b3208bf 100644
--- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
+++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
@@ -88,6 +88,7 @@
 import android.annotation.Nullable;
 import android.app.ActivityOptions;
 import android.app.ActivityOptions.SourceInfo;
+import android.app.ApplicationStartInfo;
 import android.app.CameraCompatTaskInfo.CameraCompatControlState;
 import android.app.WaitResult;
 import android.app.WindowConfiguration.WindowingMode;
@@ -845,6 +846,16 @@
                 && !r.mTransitionController.isCollecting(r))) {
             done(false /* abort */, info, "notifyWindowsDrawn", timestampNs);
         }
+
+        if (android.app.Flags.appStartInfoTimestamps()) {
+            // Log here to match StatsD for time to first frame.
+            mLoggerHandler.post(
+                    () -> mSupervisor.mService.mWindowManager.mAmInternal.addStartInfoTimestamp(
+                            ApplicationStartInfo.START_TIMESTAMP_FIRST_FRAME,
+                            timestampNs, r.getUid(), r.getPid(),
+                            info.mLastLaunchedActivity.mUserId));
+        }
+
         return infoSnapshot;
     }
 
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/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 85ab562..f553a5a 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -455,10 +455,10 @@
 import android.security.IKeyChainService;
 import android.security.KeyChain;
 import android.security.KeyChain.KeyChainConnection;
-import android.security.KeyStore;
 import android.security.keymaster.KeymasterCertificateChain;
 import android.security.keystore.AttestationUtils;
 import android.security.keystore.KeyGenParameterSpec;
+import android.security.keystore.KeyProperties;
 import android.security.keystore.ParcelableKeyGenParameterSpec;
 import android.stats.devicepolicy.DevicePolicyEnums;
 import android.telecom.TelecomManager;
@@ -6248,7 +6248,7 @@
             try (KeyChainConnection keyChainConnection =
                          KeyChain.bindAsUser(mContext, caller.getUserHandle())) {
                 IKeyChainService keyChain = keyChainConnection.getService();
-                if (!keyChain.installKeyPair(privKey, cert, chain, alias, KeyStore.UID_SELF)) {
+                if (!keyChain.installKeyPair(privKey, cert, chain, alias, KeyProperties.UID_SELF)) {
                     logInstallKeyPairFailure(caller, isCredentialManagementApp);
                     return false;
                 }
@@ -6583,7 +6583,7 @@
         }
         // As the caller will be granted access to the key, ensure no UID was specified, as
         // it will not have the desired effect.
-        if (keySpec.getUid() != KeyStore.UID_SELF) {
+        if (keySpec.getUid() != KeyProperties.UID_SELF) {
             Slogf.e(LOG_TAG, "Only the caller can be granted access to the generated keypair.");
             logGenerateKeyPairFailure(caller, isCredentialManagementApp);
             return false;
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/services/usb/java/com/android/server/usb/UsbDeviceManager.java b/services/usb/java/com/android/server/usb/UsbDeviceManager.java
index 9acda5f..e8bac66 100644
--- a/services/usb/java/com/android/server/usb/UsbDeviceManager.java
+++ b/services/usb/java/com/android/server/usb/UsbDeviceManager.java
@@ -37,6 +37,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.ServiceConnection;
 import android.content.SharedPreferences;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
@@ -62,6 +63,7 @@
 import android.os.FileUtils;
 import android.os.Handler;
 import android.os.HwBinder;
+import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
 import android.os.ParcelFileDescriptor;
@@ -89,6 +91,7 @@
 import com.android.internal.util.dump.DualDumpOutputStream;
 import com.android.server.FgThread;
 import com.android.server.LocalServices;
+import com.android.server.usb.flags.Flags;
 import com.android.server.usb.hal.gadget.UsbGadgetHal;
 import com.android.server.usb.hal.gadget.UsbGadgetHalInstance;
 import com.android.server.utils.EventLogger;
@@ -586,6 +589,22 @@
          */
         protected static final String USB_PERSISTENT_CONFIG_PROPERTY = "persist.sys.usb.config";
 
+        protected static final String MTP_PACKAGE_NAME = "com.android.mtp";
+        protected static final String MTP_SERVICE_CLASS_NAME = "com.android.mtp.MtpService";
+
+        private boolean mIsMtpServiceBound = false;
+
+        /**
+         * {@link ServiceConnection} for {@link MtpService}.
+         */
+        private ServiceConnection mMtpServiceConnection = new ServiceConnection() {
+            @Override
+            public void onServiceConnected(ComponentName className, IBinder service) {}
+
+            @Override
+            public void onServiceDisconnected(ComponentName arg0) {}
+        };
+
         UsbHandler(Looper looper, Context context, UsbDeviceManager deviceManager,
                 UsbAlsaManager alsaManager, UsbPermissionManager permissionManager) {
             super(looper);
@@ -915,6 +934,7 @@
 
         private void updateUsbFunctions() {
             updateMidiFunction();
+            updateMtpFunction();
         }
 
         private void updateMidiFunction() {
@@ -941,6 +961,67 @@
                     mMidiEnabled && mConfigured, mMidiCard, mMidiDevice);
         }
 
+        /**
+         * Bind to MtpService when MTP or PTP is enabled. This is done to prevent activity manager
+         * from freezing the corresponding process.
+         */
+        private void updateMtpFunction() {
+            if (!Flags.enableBindToMtpService()) {
+                return;
+            }
+
+            boolean mtpEnabled = ((mCurrentFunctions & UsbManager.FUNCTION_MTP) != 0);
+            boolean ptpEnabled = ((mCurrentFunctions & UsbManager.FUNCTION_PTP) != 0);
+
+            if (DEBUG) {
+                Slog.d(TAG, "updateMtpFunction "
+                        + ", mtpEnabled: " + mtpEnabled
+                        + ", ptpEnabled: " + ptpEnabled
+                        + ", mIsMtpServiceBound: " + mIsMtpServiceBound
+                );
+            }
+
+            if (mConfigured && (mtpEnabled || ptpEnabled)) {
+                bindToMtpService();
+            } else if (mIsMtpServiceBound) {
+                unbindMtpService();
+            }
+        }
+
+        private void bindToMtpService() {
+            Intent intent = new Intent();
+            intent.setComponent(new ComponentName(MTP_PACKAGE_NAME, MTP_SERVICE_CLASS_NAME));
+
+            if (DEBUG) Slog.d(TAG, "Binding to MtpService");
+
+            try {
+                mIsMtpServiceBound = mContext.bindServiceAsUser(
+                    intent,
+                    mMtpServiceConnection,
+                    Context.BIND_AUTO_CREATE,
+                    UserHandle.CURRENT
+                );
+            } catch (SecurityException exception) {
+                Slog.e(TAG, "Unable to bind to MtpService due to SecurityException", exception);
+            }
+
+            // Unbinding from the service if binding was not successful to release the connection.
+            // https://developer.android.com/reference/android/content/Context#bindService(android.content.Intent,%20android.content.ServiceConnection,%20int)
+            if (!mIsMtpServiceBound) {
+                unbindMtpService();
+                Slog.e(TAG, "Binding to MtpService failed");
+            }
+
+            if (DEBUG && mIsMtpServiceBound) Slog.d(TAG, "Successfully bound to MtpService");
+        }
+
+        private void unbindMtpService() {
+            if (DEBUG) Slog.d(TAG, "Unbinding from MtpService");
+
+            mContext.unbindService(mMtpServiceConnection);
+            mIsMtpServiceBound = false;
+        }
+
         private void setScreenUnlockedFunctions(int operationId) {
             setEnabledFunctions(mScreenUnlockedFunctions, false, operationId);
         }
diff --git a/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig b/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig
index a7c5ddb..cd96d76 100644
--- a/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig
+++ b/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig
@@ -7,3 +7,10 @@
     description: "This flag controls the restriction of usb overlay activities"
     bug: "307231174"
 }
+
+flag {
+    name: "enable_bind_to_mtp_service"
+    namespace: "usb"
+    description: "This flag enables binding to MtpService when in mtp/ptp modes"
+    bug: "332256525"
+}
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