Merge "Update default value of infrastructure_bitmask in javadoc." into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 98b62b3..2c78f93 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -60,6 +60,7 @@
     ":android.webkit.flags-aconfig-java{.generated_srcjars}",
     ":android.widget.flags-aconfig-java{.generated_srcjars}",
     ":audio-framework-aconfig",
+    ":backup_flags_lib{.generated_srcjars}",
     ":camera_platform_flags_core_java_lib{.generated_srcjars}",
     ":com.android.hardware.input-aconfig-java{.generated_srcjars}",
     ":com.android.input.flags-aconfig-java{.generated_srcjars}",
@@ -1092,4 +1093,11 @@
     name: "android.crashrecovery.flags-aconfig-java",
     aconfig_declarations: "android.crashrecovery.flags-aconfig",
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
-}
\ No newline at end of file
+}
+
+// Backup
+java_aconfig_library {
+    name: "backup_flags_lib",
+    aconfig_declarations: "backup_flags",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
diff --git a/apct-tests/perftests/core/src/android/input/MotionPredictorBenchmark.kt b/apct-tests/perftests/core/src/android/input/MotionPredictorBenchmark.kt
index aadbc23..add0a08 100644
--- a/apct-tests/perftests/core/src/android/input/MotionPredictorBenchmark.kt
+++ b/apct-tests/perftests/core/src/android/input/MotionPredictorBenchmark.kt
@@ -16,8 +16,6 @@
 
 package android.input
 
-import android.content.Context
-import android.content.res.Resources
 import android.os.SystemProperties
 import android.perftests.utils.PerfStatusReporter
 import android.view.InputDevice
@@ -38,8 +36,6 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.`when`
 
 import java.time.Duration
 
@@ -68,18 +64,6 @@
                 InputDevice.SOURCE_STYLUS, /*flags=*/0)
 }
 
-private fun getPredictionContext(offset: Duration, enablePrediction: Boolean): Context {
-    val context = mock(Context::class.java)
-    val resources: Resources = mock(Resources::class.java)
-    `when`(context.getResources()).thenReturn(resources)
-    `when`(resources.getInteger(
-            com.android.internal.R.integer.config_motionPredictionOffsetNanos)).thenReturn(
-                offset.toNanos().toInt())
-    `when`(resources.getBoolean(
-            com.android.internal.R.bool.config_enableMotionPrediction)).thenReturn(enablePrediction)
-    return context
-}
-
 @RunWith(AndroidJUnit4::class)
 @LargeTest
 class MotionPredictorBenchmark {
@@ -115,7 +99,7 @@
         var eventPosition = 0f
         val positionInterval = 10f
 
-        val predictor = MotionPredictor(getPredictionContext(offset, /*enablePrediction=*/true))
+        val predictor = MotionPredictor(/*isPredictionEnabled=*/true, offset.toNanos().toInt())
         // ACTION_DOWN t=0 x=0 y=0
         predictor.record(getStylusMotionEvent(
             eventTime, ACTION_DOWN, /*x=*/eventPosition, /*y=*/eventPosition))
@@ -141,12 +125,11 @@
      */
     @Test
     fun timeCreatePredictor() {
-        val context = getPredictionContext(
-                /*offset=*/Duration.ofMillis(20), /*enablePrediction=*/true)
+        val offsetNanos = Duration.ofMillis(20).toNanos().toInt()
 
         val state = perfStatusReporter.getBenchmarkState()
         while (state.keepRunning()) {
-            MotionPredictor(context)
+            MotionPredictor(/*isPredictionEnabled=*/true, offsetNanos)
         }
     }
 }
diff --git a/core/TEST_MAPPING b/core/TEST_MAPPING
index 24ba5c4..f1e4d0ee 100644
--- a/core/TEST_MAPPING
+++ b/core/TEST_MAPPING
@@ -23,7 +23,7 @@
   ],
   "postsubmit": [
       {
-        "name": "ContactKeysManagerTest",
+        "name": "CtsContactKeysManagerTestCases",
         "options": [
           {
             "include-filter": "android.provider.cts.contactkeys."
diff --git a/core/api/current.txt b/core/api/current.txt
index 55ea2f4..4efb3d3 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -12391,6 +12391,7 @@
     method public void registerCallback(android.content.pm.LauncherApps.Callback, android.os.Handler);
     method public void registerPackageInstallerSessionCallback(@NonNull java.util.concurrent.Executor, @NonNull android.content.pm.PackageInstaller.SessionCallback);
     method public android.content.pm.LauncherActivityInfo resolveActivity(android.content.Intent, android.os.UserHandle);
+    method @FlaggedApi("android.content.pm.archiving") public void setArchiveCompatibilityOptions(boolean, boolean);
     method public boolean shouldHideFromSuggestions(@NonNull String, @NonNull android.os.UserHandle);
     method public void startAppDetailsActivity(android.content.ComponentName, android.os.UserHandle, android.graphics.Rect, android.os.Bundle);
     method public void startMainActivity(android.content.ComponentName, android.os.UserHandle, android.graphics.Rect, android.os.Bundle);
@@ -25789,6 +25790,7 @@
     field public static final int FINAL_STATE_CANCELED = 2; // 0x2
     field public static final int FINAL_STATE_ERROR = 3; // 0x3
     field public static final int FINAL_STATE_SUCCEEDED = 1; // 0x1
+    field public static final int TIME_SINCE_CREATED_UNKNOWN = -1; // 0xffffffff
   }
 
   @FlaggedApi("com.android.media.editing.flags.add_media_metrics_editing") public static final class EditingEndedEvent.Builder {
@@ -25796,7 +25798,7 @@
     method @NonNull public android.media.metrics.EditingEndedEvent build();
     method @NonNull public android.media.metrics.EditingEndedEvent.Builder setErrorCode(int);
     method @NonNull public android.media.metrics.EditingEndedEvent.Builder setMetricsBundle(@NonNull android.os.Bundle);
-    method @NonNull public android.media.metrics.EditingEndedEvent.Builder setTimeSinceCreatedMillis(@IntRange(from=0xffffffff) long);
+    method @NonNull public android.media.metrics.EditingEndedEvent.Builder setTimeSinceCreatedMillis(@IntRange(from=android.media.metrics.EditingEndedEvent.TIME_SINCE_CREATED_UNKNOWN) long);
   }
 
   public final class EditingSession implements java.lang.AutoCloseable {
@@ -33144,7 +33146,7 @@
     method public static long getStartRequestedElapsedRealtime();
     method public static long getStartRequestedUptimeMillis();
     method public static long getStartUptimeMillis();
-    method public static final int getThreadPriority(int) throws java.lang.IllegalArgumentException;
+    method @IntRange(from=0xffffffec, to=android.os.Process.THREAD_PRIORITY_LOWEST) public static final int getThreadPriority(int) throws java.lang.IllegalArgumentException;
     method public static final int getUidForName(String);
     method public static final boolean is64Bit();
     method public static boolean isApplicationUid(int);
@@ -33159,8 +33161,8 @@
     method public static final int myUid();
     method public static android.os.UserHandle myUserHandle();
     method public static final void sendSignal(int, int);
-    method public static final void setThreadPriority(int, int) throws java.lang.IllegalArgumentException, java.lang.SecurityException;
-    method public static final void setThreadPriority(int) throws java.lang.IllegalArgumentException, java.lang.SecurityException;
+    method public static final void setThreadPriority(int, @IntRange(from=0xffffffec, to=android.os.Process.THREAD_PRIORITY_LOWEST) int) throws java.lang.IllegalArgumentException, java.lang.SecurityException;
+    method public static final void setThreadPriority(@IntRange(from=0xffffffec, to=android.os.Process.THREAD_PRIORITY_LOWEST) int) throws java.lang.IllegalArgumentException, java.lang.SecurityException;
     method @Deprecated public static final boolean supportsProcesses();
     field public static final int BLUETOOTH_UID = 1002; // 0x3ea
     field public static final int FIRST_APPLICATION_UID = 10000; // 0x2710
@@ -50648,6 +50650,7 @@
     field public static final int KEYCODE_DVR = 173; // 0xad
     field public static final int KEYCODE_E = 33; // 0x21
     field public static final int KEYCODE_EISU = 212; // 0xd4
+    field @FlaggedApi("com.android.hardware.input.emoji_and_screenshot_keycodes_available") public static final int KEYCODE_EMOJI_PICKER = 317; // 0x13d
     field public static final int KEYCODE_ENDCALL = 6; // 0x6
     field public static final int KEYCODE_ENTER = 66; // 0x42
     field public static final int KEYCODE_ENVELOPE = 65; // 0x41
@@ -50780,6 +50783,7 @@
     field public static final int KEYCODE_RIGHT_BRACKET = 72; // 0x48
     field public static final int KEYCODE_RO = 217; // 0xd9
     field public static final int KEYCODE_S = 47; // 0x2f
+    field @FlaggedApi("com.android.hardware.input.emoji_and_screenshot_keycodes_available") public static final int KEYCODE_SCREENSHOT = 318; // 0x13e
     field public static final int KEYCODE_SCROLL_LOCK = 116; // 0x74
     field public static final int KEYCODE_SEARCH = 84; // 0x54
     field public static final int KEYCODE_SEMICOLON = 74; // 0x4a
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 708f10e..f101161 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -14848,11 +14848,11 @@
     method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setDataEnabled(int, boolean);
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setDataRoamingEnabled(boolean);
     method @FlaggedApi("com.android.internal.telephony.flags.enable_identifier_disclosure_transparency") @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setEnableCellularIdentifierDisclosureNotifications(boolean);
-    method @FlaggedApi("com.android.internal.telephony.flags.enable_modem_cipher_transparency") @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setEnableNullCipherNotifications(boolean);
     method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public android.telephony.PinResult setIccLockEnabled(boolean, @NonNull String);
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setMobileDataPolicyEnabled(int, boolean);
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setMultiSimCarrierRestriction(boolean);
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public int setNrDualConnectivityState(int);
+    method @FlaggedApi("com.android.internal.telephony.flags.enable_modem_cipher_transparency") @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setNullCipherNotificationsEnabled(boolean);
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean setOpportunisticNetworkState(boolean);
     method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean setPreferredNetworkTypeBitmask(long);
     method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean setRadio(boolean);
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 1fa2b5c..77add41 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -2654,7 +2654,6 @@
     method @NonNull public static String convert(@NonNull java.util.UUID);
     method @Nullable public String getCloudMediaProvider();
     method public boolean isAppIoBlocked(@NonNull java.util.UUID, int, int, int);
-    method public static boolean isUserKeyUnlocked(int);
     field public static final String CACHE_RESERVE_PERCENT_HIGH_KEY = "cache_reserve_percent_high";
     field public static final String CACHE_RESERVE_PERCENT_LOW_KEY = "cache_reserve_percent_low";
     field public static final String STORAGE_THRESHOLD_PERCENT_HIGH_KEY = "storage_threshold_percent_high";
@@ -3617,7 +3616,7 @@
     method public final int getDisplayId();
     method public final void setDisplayId(int);
     field public static final int FLAG_IS_ACCESSIBILITY_EVENT = 2048; // 0x800
-    field public static final int LAST_KEYCODE = 316; // 0x13c
+    field public static final int LAST_KEYCODE = 318; // 0x13e
   }
 
   public final class KeyboardShortcutGroup implements android.os.Parcelable {
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 00c4b0f..bb666e6 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -1555,9 +1555,24 @@
      */
     public static final int OP_RUN_BACKUP_JOBS = AppProtoEnums.APP_OP_RUN_BACKUP_JOBS;
 
+    /**
+     * Whether the app has enabled to receive the icon overlay for fetching archived apps.
+     *
+     * @hide
+     */
+    public static final int OP_ARCHIVE_ICON_OVERLAY = AppProtoEnums.APP_OP_ARCHIVE_ICON_OVERLAY;
+
+    /**
+     * Whether the app has enabled compatibility support for unarchival.
+     *
+     * @hide
+     */
+    public static final int OP_UNARCHIVAL_CONFIRMATION =
+            AppProtoEnums.APP_OP_UNARCHIVAL_CONFIRMATION;
+
     /** @hide */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
-    public static final int _NUM_OP = 145;
+    public static final int _NUM_OP = 147;
 
     /**
      * All app ops represented as strings.
@@ -1708,6 +1723,8 @@
             OPSTR_RESERVED_FOR_TESTING,
             OPSTR_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER,
             OPSTR_RUN_BACKUP_JOBS,
+            OPSTR_ARCHIVE_ICON_OVERLAY,
+            OPSTR_UNARCHIVAL_CONFIRMATION,
     })
     public @interface AppOpString {}
 
@@ -2048,6 +2065,20 @@
     public static final String OPSTR_MEDIA_ROUTING_CONTROL = "android:media_routing_control";
 
     /**
+     * Whether the app has enabled to receive the icon overlay for fetching archived apps.
+     *
+     * @hide
+     */
+    public static final String OPSTR_ARCHIVE_ICON_OVERLAY = "android:archive_icon_overlay";
+
+    /**
+     * Whether the app has enabled compatibility support for unarchival.
+     *
+     * @hide
+     */
+    public static final String OPSTR_UNARCHIVAL_CONFIRMATION = "android:unarchival_support";
+
+    /**
      * AppOp granted to apps that we are started via {@code am instrument -e --no-isolated-storage}
      *
      * <p>MediaProvider is the only component (outside of system server) that should care about this
@@ -2520,6 +2551,8 @@
             OP_MEDIA_ROUTING_CONTROL,
             OP_READ_SYSTEM_GRAMMATICAL_GENDER,
             OP_RUN_BACKUP_JOBS,
+            OP_ARCHIVE_ICON_OVERLAY,
+            OP_UNARCHIVAL_CONFIRMATION,
     };
 
     static final AppOpInfo[] sAppOpInfos = new AppOpInfo[]{
@@ -2979,6 +3012,12 @@
                 .build(),
         new AppOpInfo.Builder(OP_RUN_BACKUP_JOBS, OPSTR_RUN_BACKUP_JOBS, "RUN_BACKUP_JOBS")
                 .setPermission(Manifest.permission.RUN_BACKUP_JOBS).build(),
+        new AppOpInfo.Builder(OP_ARCHIVE_ICON_OVERLAY, OPSTR_ARCHIVE_ICON_OVERLAY,
+                "ARCHIVE_ICON_OVERLAY")
+                .setDefaultMode(MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_UNARCHIVAL_CONFIRMATION, OPSTR_UNARCHIVAL_CONFIRMATION,
+                "UNARCHIVAL_CONFIRMATION")
+                .setDefaultMode(MODE_ALLOWED).build(),
     };
 
     // The number of longs needed to form a full bitmask of app ops
@@ -3113,7 +3152,7 @@
 
     /**
      * Retrieve the permission associated with an operation, or null if there is not one.
-     *
+
      * @param op The operation name.
      *
      * @hide
diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java
index 34c44f9..4f1db7d 100644
--- a/core/java/android/app/ApplicationPackageManager.java
+++ b/core/java/android/app/ApplicationPackageManager.java
@@ -4032,7 +4032,8 @@
     private Drawable getArchivedAppIcon(String packageName) {
         try {
             return new BitmapDrawable(null,
-                    mPM.getArchivedAppIcon(packageName, new UserHandle(getUserId())));
+                    mPM.getArchivedAppIcon(packageName, new UserHandle(getUserId()),
+                            mContext.getPackageName()));
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/core/java/android/app/OWNERS b/core/java/android/app/OWNERS
index 3b5bba2..729f92a 100644
--- a/core/java/android/app/OWNERS
+++ b/core/java/android/app/OWNERS
@@ -111,5 +111,9 @@
 per-file ConfigurationController.java = file:/services/core/java/com/android/server/wm/OWNERS
 per-file *ScreenCapture* = file:/services/core/java/com/android/server/wm/OWNERS
 
+# Multitasking
+per-file multitasking.aconfig = file:/services/core/java/com/android/server/wm/OWNERS
+per-file multitasking.aconfig = file:/libs/WindowManager/Shell/OWNERS
+
 # Zygote
 per-file *Zygote* = file:/ZYGOTE_OWNERS
diff --git a/core/java/android/app/background_install_control_manager.aconfig b/core/java/android/app/background_install_control_manager.aconfig
index 029b93a..4473b95 100644
--- a/core/java/android/app/background_install_control_manager.aconfig
+++ b/core/java/android/app/background_install_control_manager.aconfig
@@ -1,7 +1,7 @@
 package: "android.app"
 
 flag {
-     namespace: "background_install_control"
+     namespace: "preload_safety"
      name: "bic_client"
      description: "System API for background install control."
      is_fixed_read_only: true
diff --git a/core/java/android/app/backup/BackupAgent.java b/core/java/android/app/backup/BackupAgent.java
index 6b558d0..ffbd80c 100644
--- a/core/java/android/app/backup/BackupAgent.java
+++ b/core/java/android/app/backup/BackupAgent.java
@@ -43,12 +43,14 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.infra.AndroidFuture;
+import com.android.server.backup.Flags;
 
 import libcore.io.IoUtils;
 
 import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.lang.annotation.Retention;
@@ -1283,6 +1285,14 @@
                 // And bring live SharedPreferences instances up to date
                 reloadSharedPreferences();
 
+                // It's possible that onRestoreFile was overridden and that the agent did not
+                // consume all the data for this file from the pipe. We need to clear the pipe,
+                // otherwise the framework can get stuck trying to write to a full pipe or
+                // onRestoreFile could be called with the previous file's data left in the pipe.
+                if (Flags.enableClearPipeAfterRestoreFile()) {
+                    clearUnconsumedDataFromPipe(data, size);
+                }
+
                 Binder.restoreCallingIdentity(ident);
                 try {
                     callbackBinder.opCompleteForUser(getBackupUserId(), token, 0);
@@ -1296,6 +1306,16 @@
             }
         }
 
+        private static void clearUnconsumedDataFromPipe(ParcelFileDescriptor data, long size) {
+            try (FileInputStream in = new FileInputStream(data.getFileDescriptor())) {
+                if (in.available() > 0) {
+                    in.skip(size);
+                }
+            } catch (IOException e) {
+                Log.w(TAG, "Failed to clear unconsumed data from pipe.", e);
+            }
+        }
+
         @Override
         public void doRestoreFinished(int token, IBackupManager callbackBinder) {
             final long ident = Binder.clearCallingIdentity();
diff --git a/core/java/android/app/multitasking.aconfig b/core/java/android/app/multitasking.aconfig
new file mode 100644
index 0000000..ab00891
--- /dev/null
+++ b/core/java/android/app/multitasking.aconfig
@@ -0,0 +1,8 @@
+package: "android.app"
+
+flag {
+    name: "enable_pip_ui_state_callback_on_entering"
+    namespace: "multitasking"
+    description: "Enables PiP UI state callback on entering"
+    bug: "303718131"
+}
diff --git a/core/java/android/content/pm/ILauncherApps.aidl b/core/java/android/content/pm/ILauncherApps.aidl
index a97de63..62db65f 100644
--- a/core/java/android/content/pm/ILauncherApps.aidl
+++ b/core/java/android/content/pm/ILauncherApps.aidl
@@ -128,4 +128,6 @@
 
     /** Unregister a callback, so that it won't be called when LauncherApps dumps. */
     void unRegisterDumpCallback(IDumpCallback cb);
+
+    void setArchiveCompatibilityOptions(boolean enableIconOverlay, boolean enableUnarchivalConfirmation);
 }
diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl
index 6dc8d47..380de96 100644
--- a/core/java/android/content/pm/IPackageManager.aidl
+++ b/core/java/android/content/pm/IPackageManager.aidl
@@ -840,7 +840,7 @@
 
     ArchivedPackageParcel getArchivedPackage(in String packageName, int userId);
 
-    Bitmap getArchivedAppIcon(String packageName, in UserHandle user);
+    Bitmap getArchivedAppIcon(String packageName, in UserHandle user, String callingPackageName);
 
     boolean isAppArchivable(String packageName, in UserHandle user);
 }
diff --git a/core/java/android/content/pm/LauncherApps.java b/core/java/android/content/pm/LauncherApps.java
index 1d2b1af..50be983 100644
--- a/core/java/android/content/pm/LauncherApps.java
+++ b/core/java/android/content/pm/LauncherApps.java
@@ -1801,6 +1801,31 @@
         }
     }
 
+    /**
+     * Enable or disable different archive compatibility options of the launcher.
+     *
+     * @param enableIconOverlay Provides a cloud overlay for archived apps to ensure users are aware
+     * that a certain app is archived. True by default.
+     * Launchers might want to disable this operation if they want to provide custom user experience
+     * to differentiate archived apps.
+     * @param enableUnarchivalConfirmation If true, the user is shown a confirmation dialog when
+     * they click an archived app, which explains that the app will be downloaded and restored in
+     * the background. True by default.
+     * Launchers might want to disable this operation if they provide sufficient, alternative user
+     * guidance to highlight that an unarchival is starting and ongoing once an archived app is
+     * tapped. E.g., this could be achieved by showing the unarchival progress around the icon.
+     */
+    @FlaggedApi(android.content.pm.Flags.FLAG_ARCHIVING)
+    public void setArchiveCompatibilityOptions(boolean enableIconOverlay,
+            boolean enableUnarchivalConfirmation) {
+        try {
+            mService.setArchiveCompatibilityOptions(enableIconOverlay,
+                    enableUnarchivalConfirmation);
+        } catch (RemoteException re) {
+            throw re.rethrowFromSystemServer();
+        }
+    }
+
     /** @return position in mCallbacks for callback or -1 if not present. */
     private int findCallbackLocked(Callback callback) {
         if (callback == null) {
diff --git a/core/java/android/hardware/input/InputSettings.java b/core/java/android/hardware/input/InputSettings.java
index fdbd319..89fa5fb 100644
--- a/core/java/android/hardware/input/InputSettings.java
+++ b/core/java/android/hardware/input/InputSettings.java
@@ -19,6 +19,7 @@
 import static com.android.hardware.input.Flags.keyboardA11yBounceKeysFlag;
 import static com.android.hardware.input.Flags.keyboardA11ySlowKeysFlag;
 import static com.android.hardware.input.Flags.keyboardA11yStickyKeysFlag;
+import static com.android.hardware.input.Flags.touchpadTapDragging;
 import static com.android.input.flags.Flags.enableInputFilterRustImpl;
 
 import android.Manifest;
@@ -303,6 +304,53 @@
     }
 
     /**
+     * Returns true if the feature flag for touchpad tap dragging is enabled.
+     *
+     * @hide
+     */
+    public static boolean isTouchpadTapDraggingFeatureFlagEnabled() {
+        return touchpadTapDragging();
+    }
+
+    /**
+     * Returns true if the touchpad should allow tap dragging.
+     *
+     * The returned value only applies to gesture-compatible touchpads.
+     *
+     * @param context The application context.
+     * @return Whether the touchpad should allow tap dragging.
+     *
+     * @hide
+     */
+    public static boolean useTouchpadTapDragging(@NonNull Context context) {
+        if (!isTouchpadTapDraggingFeatureFlagEnabled()) {
+            return false;
+        }
+        return Settings.System.getIntForUser(context.getContentResolver(),
+                Settings.System.TOUCHPAD_TAP_DRAGGING, 0, UserHandle.USER_CURRENT) == 1;
+    }
+
+    /**
+     * Sets the tap dragging behavior for the touchpad.
+     *
+     * The new behavior is only applied to gesture-compatible touchpads.
+     *
+     * @param context The application context.
+     * @param enabled Will enable tap dragging if true, disable it if false
+     *
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.WRITE_SETTINGS)
+    public static void setTouchpadTapDragging(@NonNull Context context, boolean enabled) {
+        if (!isTouchpadTapDraggingFeatureFlagEnabled()) {
+            return;
+        }
+        Settings.System.putIntForUser(context.getContentResolver(),
+                Settings.System.TOUCHPAD_TAP_DRAGGING, enabled ? 1 : 0,
+                UserHandle.USER_CURRENT);
+    }
+
+    /**
      * Returns true if the touchpad should use the right click zone.
      *
      * The returned value only applies to gesture-compatible touchpads.
diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig
index 0ed6569..e070fe5 100644
--- a/core/java/android/hardware/input/input_framework.aconfig
+++ b/core/java/android/hardware/input/input_framework.aconfig
@@ -33,7 +33,21 @@
 
 flag {
     namespace: "input_native"
+    name: "emoji_and_screenshot_keycodes_available"
+    description: "Add new KeyEvent keycodes for opening Emoji Picker and Taking Screenshots"
+    bug: "315307777"
+}
+
+flag {
+    namespace: "input_native"
     name: "keyboard_a11y_slow_keys_flag"
     description: "Controls if the slow keys accessibility feature for physical keyboard is available to the user"
     bug: "294546335"
+}
+
+flag {
+    namespace: "input_native"
+    name: "touchpad_tap_dragging"
+    description: "Offers a setting to enable touchpad tap dragging"
+    bug: "321978150"
 }
\ No newline at end of file
diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java
index 05b7827f..b7556df 100644
--- a/core/java/android/os/Binder.java
+++ b/core/java/android/os/Binder.java
@@ -292,7 +292,7 @@
         sWarnOnBlockingOnCurrentThread.set(sWarnOnBlocking);
     }
 
-    private static ThreadLocal<SomeArgs> sIdentity$ravenwood;
+    private static volatile ThreadLocal<SomeArgs> sIdentity$ravenwood;
 
     @android.ravenwood.annotation.RavenwoodKeepWholeClass
     private static class IdentitySupplier implements Supplier<SomeArgs> {
diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java
index 1f3a162..7020a38 100644
--- a/core/java/android/os/Process.java
+++ b/core/java/android/os/Process.java
@@ -20,6 +20,7 @@
 
 import android.annotation.ElapsedRealtimeLong;
 import android.annotation.FlaggedApi;
+import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
@@ -844,7 +845,7 @@
         return "amd64".equals(System.getProperty("os.arch"));
     }
 
-    private static ThreadLocal<SomeArgs> sIdentity$ravenwood;
+    private static volatile ThreadLocal<SomeArgs> sIdentity$ravenwood;
 
     /** @hide */
     @android.ravenwood.annotation.RavenwoodKeep
@@ -1122,7 +1123,8 @@
      * priority.
      */
     @android.ravenwood.annotation.RavenwoodReplace
-    public static final native void setThreadPriority(int tid, int priority)
+    public static final native void setThreadPriority(int tid,
+            @IntRange(from = -20, to = THREAD_PRIORITY_LOWEST) int priority)
             throws IllegalArgumentException, SecurityException;
 
     /** @hide */
@@ -1288,7 +1290,8 @@
      * @see #setThreadPriority(int, int)
      */
     @android.ravenwood.annotation.RavenwoodReplace
-    public static final native void setThreadPriority(int priority)
+    public static final native void setThreadPriority(
+            @IntRange(from = -20, to = THREAD_PRIORITY_LOWEST) int priority)
             throws IllegalArgumentException, SecurityException;
 
     /** @hide */
@@ -1310,6 +1313,7 @@
      * <var>tid</var> does not exist.
      */
     @android.ravenwood.annotation.RavenwoodReplace
+    @IntRange(from = -20, to = THREAD_PRIORITY_LOWEST)
     public static final native int getThreadPriority(int tid)
             throws IllegalArgumentException;
 
diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java
index 9587db1..3a57e84 100644
--- a/core/java/android/os/storage/StorageManager.java
+++ b/core/java/android/os/storage/StorageManager.java
@@ -1669,12 +1669,6 @@
         }
     }
 
-    /** {@hide} */
-    @TestApi
-    public static boolean isUserKeyUnlocked(int userId) {
-        return isCeStorageUnlocked(userId);
-    }
-
     /**
      * Returns true if the user's credential-encrypted (CE) storage is unlocked.
      *
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index e238080..524b733 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -6049,6 +6049,13 @@
         public static final String TOUCHPAD_TAP_TO_CLICK = "touchpad_tap_to_click";
 
         /**
+         * Whether to enable tap dragging on touchpads.
+         *
+         * @hide
+         */
+        public static final String TOUCHPAD_TAP_DRAGGING = "touchpad_tap_dragging";
+
+        /**
          * Whether to enable a right-click zone on touchpads.
          *
          * When set to 1, pressing to click in a section on the right-hand side of the touchpad will
@@ -6270,6 +6277,7 @@
             PRIVATE_SETTINGS.add(TOUCHPAD_POINTER_SPEED);
             PRIVATE_SETTINGS.add(TOUCHPAD_NATURAL_SCROLLING);
             PRIVATE_SETTINGS.add(TOUCHPAD_TAP_TO_CLICK);
+            PRIVATE_SETTINGS.add(TOUCHPAD_TAP_DRAGGING);
             PRIVATE_SETTINGS.add(TOUCHPAD_RIGHT_CLICK_ZONE);
             PRIVATE_SETTINGS.add(CAMERA_FLASH_NOTIFICATION);
             PRIVATE_SETTINGS.add(SCREEN_FLASH_NOTIFICATION);
diff --git a/core/java/android/provider/Telephony.java b/core/java/android/provider/Telephony.java
index c7bfabc..2841dc0 100644
--- a/core/java/android/provider/Telephony.java
+++ b/core/java/android/provider/Telephony.java
@@ -4927,7 +4927,7 @@
         /**
          * TelephonyProvider column name for satellite attach enabled for carrier. The value of this
          * column is set based on user settings.
-         * By default, it's disabled.
+         * By default, it's enabled.
          *
          * @hide
          */
diff --git a/core/java/android/service/notification/flags.aconfig b/core/java/android/service/notification/flags.aconfig
index 3008b8d..446fe3d 100644
--- a/core/java/android/service/notification/flags.aconfig
+++ b/core/java/android/service/notification/flags.aconfig
@@ -8,14 +8,6 @@
 }
 
 flag {
-  name: "notification_lifetime_extension_refactor"
-    namespace: "systemui"
-    description: "Enables moving notification lifetime extension management from SystemUI to "
-        "Notification Manager Service"
-    bug: "299448097"
-}
-
-flag {
   name: "redact_sensitive_notifications_from_untrusted_listeners"
   namespace: "systemui"
   description: "This flag controls the redacting of sensitive notifications from untrusted NotificationListenerServices"
diff --git a/core/java/android/view/KeyEvent.java b/core/java/android/view/KeyEvent.java
index c6601e8d..1ee9509 100644
--- a/core/java/android/view/KeyEvent.java
+++ b/core/java/android/view/KeyEvent.java
@@ -19,6 +19,7 @@
 import static android.os.IInputConstants.INPUT_EVENT_FLAG_IS_ACCESSIBILITY_EVENT;
 import static android.view.Display.INVALID_DISPLAY;
 
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.TestApi;
@@ -31,6 +32,8 @@
 import android.util.SparseIntArray;
 import android.view.KeyCharacterMap.KeyData;
 
+import com.android.hardware.input.Flags;
+
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -384,7 +387,13 @@
     public static final int KEYCODE_META_RIGHT      = 118;
     /** Key code constant: Function modifier key. */
     public static final int KEYCODE_FUNCTION        = 119;
-    /** Key code constant: System Request / Print Screen key. */
+    /**
+     * Key code constant: System Request / Print Screen key.
+     *
+     * This key is sent to the app first and only if the app doesn't handle it, the framework
+     * handles it (to take a screenshot), unlike {@code KEYCODE_TAKE_SCREENSHOT} which is
+     * fully handled by the framework.
+     */
     public static final int KEYCODE_SYSRQ           = 120;
     /** Key code constant: Break / Pause key. */
     public static final int KEYCODE_BREAK           = 121;
@@ -921,14 +930,25 @@
      * User customizable key #4.
      */
     public static final int KEYCODE_MACRO_4 = 316;
-
+    /** Key code constant: To open emoji picker */
+    @FlaggedApi(Flags.FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE)
+    public static final int KEYCODE_EMOJI_PICKER = 317;
+    /**
+     * Key code constant: To take a screenshot
+     *
+     * This key is fully handled by the framework and will not be sent to the foreground app,
+     * unlike {@code KEYCODE_SYSRQ} which is sent to the app first and only if the app
+     * doesn't handle it, the framework handles it (to take a screenshot).
+     */
+    @FlaggedApi(Flags.FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE)
+    public static final int KEYCODE_SCREENSHOT = 318;
 
    /**
      * Integer value of the last KEYCODE. Increases as new keycodes are added to KeyEvent.
      * @hide
      */
     @TestApi
-    public static final int LAST_KEYCODE = KEYCODE_MACRO_4;
+    public static final int LAST_KEYCODE = KEYCODE_SCREENSHOT;
 
     // NOTE: If you add a new keycode here you must also add it to:
     //  isSystem()
diff --git a/core/java/android/view/MotionPredictor.java b/core/java/android/view/MotionPredictor.java
index 27af300..db2efaa 100644
--- a/core/java/android/view/MotionPredictor.java
+++ b/core/java/android/view/MotionPredictor.java
@@ -20,6 +20,8 @@
 import android.annotation.Nullable;
 import android.content.Context;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import libcore.util.NativeAllocationRegistry;
 
 /**
@@ -57,11 +59,21 @@
      * @param context The context for the predictions
      */
     public MotionPredictor(@NonNull Context context) {
-        mIsPredictionEnabled = context.getResources().getBoolean(
-                com.android.internal.R.bool.config_enableMotionPrediction);
-        final int offsetNanos = context.getResources().getInteger(
-                com.android.internal.R.integer.config_motionPredictionOffsetNanos);
-        mPtr = nativeInitialize(offsetNanos);
+        this(
+                context.getResources().getBoolean(
+                        com.android.internal.R.bool.config_enableMotionPrediction),
+                context.getResources().getInteger(
+                        com.android.internal.R.integer.config_motionPredictionOffsetNanos));
+    }
+
+    /**
+     * Internal constructor for testing.
+     * @hide
+     */
+    @VisibleForTesting
+    public MotionPredictor(boolean isPredictionEnabled, int motionPredictionOffsetNanos) {
+        mIsPredictionEnabled = isPredictionEnabled;
+        mPtr = nativeInitialize(motionPredictionOffsetNanos);
         RegistryHolder.REGISTRY.registerNativeAllocation(this, mPtr);
     }
 
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 7bc832e..f9abc8a 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -399,6 +399,8 @@
     @Nullable
     private ContentObserver mForceInvertObserver;
 
+    private static final int INVALID_VALUE = Integer.MIN_VALUE;
+    private int mForceInvertEnabled = INVALID_VALUE;
     /**
      * Callback for notifying about global configuration changes.
      */
@@ -1604,6 +1606,23 @@
         }
     }
 
+    private boolean isForceInvertEnabled() {
+        if (mForceInvertEnabled == INVALID_VALUE) {
+            reloadForceInvertEnabled();
+        }
+        return mForceInvertEnabled == 1;
+    }
+
+    private void reloadForceInvertEnabled() {
+        if (forceInvertColor()) {
+            mForceInvertEnabled = Settings.Secure.getIntForUser(
+                    mContext.getContentResolver(),
+                    Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED,
+                    /* def= */ 0,
+                    UserHandle.myUserId());
+        }
+    }
+
     /**
      * Register any kind of listeners if setView was success.
      */
@@ -1630,6 +1649,7 @@
                 mForceInvertObserver = new ContentObserver(mHandler) {
                     @Override
                     public void onChange(boolean selfChange) {
+                        reloadForceInvertEnabled();
                         updateForceDarkMode();
                     }
                 };
@@ -1850,16 +1870,11 @@
     @VisibleForTesting
     public @ForceDarkType.ForceDarkTypeDef int determineForceDarkType() {
         if (forceInvertColor()) {
-            boolean isForceInvertEnabled = Settings.Secure.getIntForUser(
-                    mContext.getContentResolver(),
-                    Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED,
-                    /* def= */ 0,
-                    UserHandle.myUserId()) == 1;
             // Force invert ignores all developer opt-outs.
             // We also ignore dark theme, since the app developer can override the user's preference
             // for dark mode in configuration.uiMode. Instead, we assume that the force invert
             // setting will be enabled at the same time dark theme is in the Settings app.
-            if (isForceInvertEnabled) {
+            if (isForceInvertEnabled()) {
                 return ForceDarkType.FORCE_INVERT_COLOR_DARK;
             }
         }
diff --git a/core/proto/android/providers/settings/system.proto b/core/proto/android/providers/settings/system.proto
index 48243f2..5fc2a59 100644
--- a/core/proto/android/providers/settings/system.proto
+++ b/core/proto/android/providers/settings/system.proto
@@ -207,6 +207,7 @@
         optional SettingProto pointer_speed = 2 [ (android.privacy).dest = DEST_AUTOMATIC ];
         optional SettingProto right_click_zone = 3 [ (android.privacy).dest = DEST_AUTOMATIC ];
         optional SettingProto tap_to_click = 4 [ (android.privacy).dest = DEST_AUTOMATIC ];
+        optional SettingProto tap_dragging = 5 [ (android.privacy).dest = DEST_AUTOMATIC ];
     }
     optional Touchpad touchpad = 36;
 
diff --git a/core/res/res/drawable-nodpi/platlogo.xml b/core/res/res/drawable-nodpi/platlogo.xml
index fe12f6e..f3acab0 100644
--- a/core/res/res/drawable-nodpi/platlogo.xml
+++ b/core/res/res/drawable-nodpi/platlogo.xml
@@ -1,5 +1,5 @@
 <!--
-Copyright (C) 2024 The Android Open Source Project
+Copyright (C) 2021 The Android Open Source Project
 
    Licensed under the Apache License, Version 2.0 (the "License");
     you may not use this file except in compliance with the License.
@@ -20,103 +20,179 @@
     android:viewportWidth="512"
     android:viewportHeight="512">
   <path
-      android:pathData="M256.22,437.7c-26.38,0 -43.81,-18.3 -61.2,-48.42 -17.39,-30.12 -103.26,-178.86 -120.65,-208.98s-24.52,-54.37 -11.33,-77.21c13.19,-22.84 37.75,-28.79 72.53,-28.79 34.78,0 206.53,0 241.31,0 34.78,0 59.35,5.95 72.53,28.79 13.19,22.84 6.06,47.09 -11.33,77.21s-103.26,178.86 -120.65,208.98c-17.39,30.12 -34.83,48.42 -61.2,48.42Z"
-      android:strokeWidth="0">
+      android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0">
     <aapt:attr name="android:fillColor">
       <gradient 
-          android:startX="56.22"
-          android:startY="256"
-          android:endX="456.22"
-          android:endY="256"
+          android:startX="256"
+          android:startY="21.81"
+          android:endX="256"
+          android:endY="350.42"
           android:type="linear">
         <item android:offset="0" android:color="#FF073042"/>
         <item android:offset="1" android:color="#FF073042"/>
       </gradient>
     </aapt:attr>
   </path>
+  <group>
+    <clip-path
+        android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+    <path
+        android:pathData="m195.27,187.64l2.25,-6.69c13.91,78.13 50.84,284.39 50.84,50.33 0,-0.97 0.72,-1.81 1.62,-1.81h12.69c0.9,0 1.62,0.83 1.62,1.8 -0.2,409.91 -69.03,-43.64 -69.03,-43.64Z"
+        android:fillColor="#3ddc84"/>
+  </group>
+  <group>
+    <clip-path
+        android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+    <path
+        android:pathData="m158.77,180.68l-33.17,57.45c-1.9,3.3 -0.77,7.52 2.53,9.42 3.3,1.9 7.52,0.77 9.42,-2.53l33.59,-58.17c54.27,24.33 116.34,24.33 170.61,0l33.59,58.17c1.97,3.26 6.21,4.3 9.47,2.33 3.17,-1.91 4.26,-5.99 2.47,-9.23l-33.16,-57.45c56.95,-30.97 95.91,-88.64 101.61,-156.76H57.17c5.7,68.12 44.65,125.79 101.61,156.76Z"
+        android:fillColor="#fff"/>
+  </group>
+  <group>
+    <clip-path
+        android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+    <path
+        android:pathData="M250.26,187.66L262.17,187.66A2.13,2.13 0,0 1,264.3 189.78L264.3,217.85A2.13,2.13 0,0 1,262.17 219.98L250.26,219.98A2.13,2.13 0,0 1,248.14 217.85L248.14,189.78A2.13,2.13 0,0 1,250.26 187.66z"
+        android:fillColor="#3ddc84"/>
+  </group>
+  <group>
+    <clip-path
+        android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+    <path
+        android:pathData="M250.12,170.29L262.32,170.29A1.98,1.98 0,0 1,264.3 172.26L264.3,176.39A1.98,1.98 0,0 1,262.32 178.37L250.12,178.37A1.98,1.98 0,0 1,248.14 176.39L248.14,172.26A1.98,1.98 0,0 1,250.12 170.29z"
+        android:fillColor="#3ddc84"/>
+  </group>
+  <group>
+    <clip-path
+        android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+    <path
+        android:pathData="M171.92,216.82h4.04v4.04h-4.04z"
+        android:fillColor="#fff"/>
+  </group>
+  <group>
+    <clip-path
+        android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+    <path
+        android:pathData="M188.8,275.73h4.04v4.04h-4.04z"
+        android:fillColor="#fff"/>
+  </group>
+  <group>
+    <clip-path
+        android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+    <path
+        android:pathData="M369.04,337.63h4.04v4.04h-4.04z"
+        android:fillColor="#fff"/>
+  </group>
+  <group>
+    <clip-path
+        android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+    <path
+        android:pathData="M285.93,252.22h4.04v4.04h-4.04z"
+        android:fillColor="#fff"/>
+  </group>
+  <group>
+    <clip-path
+        android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+    <path
+        android:pathData="M318.96,218.84h4.04v4.04h-4.04z"
+        android:fillColor="#fff"/>
+  </group>
+  <group>
+    <clip-path
+        android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+    <path
+        android:pathData="M294.05,288.55h4.04v4.04h-4.04z"
+        android:fillColor="#fff"/>
+  </group>
+  <group>
+    <clip-path
+        android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+    <path
+        android:pathData="M330.82,273.31h8.08v8.08h-8.08z"
+        android:fillColor="#fff"/>
+  </group>
+  <group>
+    <clip-path
+        android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+    <path
+        android:pathData="M188.8,298.95h4.04v4.04h-4.04z"
+        android:fillColor="#fff"/>
+  </group>
+  <group>
+    <clip-path
+        android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+    <path
+        android:pathData="M220.14,238.94h8.08v8.08h-8.08z"
+        android:fillColor="#fff"/>
+  </group>
+  <group>
+    <clip-path
+        android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+    <path
+        android:pathData="M272.1,318.9h8.08v8.08h-8.08z"
+        android:fillColor="#fff"/>
+  </group>
+  <group>
+    <clip-path
+        android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+    <path
+        android:pathData="M293.34,349.25h8.08v8.08h-8.08z"
+        android:fillColor="#fff"/>
+  </group>
+  <group>
+    <clip-path
+        android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+    <path
+        android:pathData="M161.05,254.24h8.08v8.08h-8.08z"
+        android:fillColor="#fff"/>
+  </group>
+  <group>
+    <clip-path
+        android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+    <path
+        android:pathData="M378.92,192h8.08v8.08h-8.08z"
+        android:fillColor="#fff"/>
+  </group>
+  <group>
+    <clip-path
+        android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/>
+    <path
+        android:pathData="M137.87,323.7h8.08v8.08h-8.08z"
+        android:fillColor="#fff"/>
+  </group>
   <path
-      android:pathData="M198.85,168.57l2.03,-6.03c12.55,70.48 45.87,256.56 45.87,45.41 0,-0.88 0.65,-1.63 1.46,-1.63h11.45c0.81,0 1.46,0.75 1.46,1.63 -0.18,369.8 -62.28,-39.37 -62.28,-39.37Z"
-      android:strokeWidth="0"
-      android:fillColor="#3ddc84"/>
-  <path
-      android:pathData="M186.69,167.97l-23.69,41.03c-1.36,2.36 -0.55,5.37 1.8,6.73 2.36,1.36 5.37,0.55 6.73,-1.8l23.99,-41.55c38.76,17.38 83.1,17.38 121.86,0l23.99,41.55c1.41,2.33 4.44,3.07 6.77,1.66 2.26,-1.37 3.04,-4.28 1.76,-6.59l-23.69,-41.03c40.68,-22.12 68.5,-63.31 72.57,-111.97H114.11c4.07,48.65 31.89,89.85 72.57,111.97Z"
-      android:strokeWidth="0"
-      android:fillColor="#fff"/>
-  <path
-      android:pathData="M248.46,168.59L259.2,168.59A1.92,1.92 0,0 1,261.12 170.5L261.12,195.83A1.92,1.92 0,0 1,259.2 197.75L248.46,197.75A1.92,1.92 0,0 1,246.54 195.83L246.54,170.5A1.92,1.92 0,0 1,248.46 168.59z"
-      android:strokeWidth="0"
-      android:fillColor="#3ddc84"/>
-  <path
-      android:pathData="M248.32,152.92L259.34,152.92A1.78,1.78 0,0 1,261.12 154.7L261.12,158.43A1.78,1.78 0,0 1,259.34 160.21L248.32,160.21A1.78,1.78 0,0 1,246.54 158.43L246.54,154.7A1.78,1.78 0,0 1,248.32 152.92z"
-      android:strokeWidth="0"
-      android:fillColor="#3ddc84"/>
-  <path
-      android:pathData="M159.03,176.91h4.04v4.04h-4.04z"
-      android:strokeWidth="0"
-      android:fillColor="#fff"/>
-  <path
-      android:pathData="M188.8,275.73h4.04v4.04h-4.04z"
-      android:strokeWidth="0"
-      android:fillColor="#fff"/>
-  <path
-      android:pathData="M373.41,158.93h4.04v4.04h-4.04z"
-      android:strokeWidth="0"
-      android:fillColor="#fff"/>
-  <path
-      android:pathData="M112.1,129.34h4.04v4.04h-4.04z"
-      android:strokeWidth="0"
-      android:fillColor="#fff"/>
-  <path
-      android:pathData="M285.93,252.22h4.04v4.04h-4.04z"
-      android:strokeWidth="0"
-      android:fillColor="#fff"/>
-  <path
-      android:pathData="M318.96,218.84h4.04v4.04h-4.04z"
-      android:strokeWidth="0"
-      android:fillColor="#fff"/>
-  <path
-      android:pathData="M294.05,288.55h4.04v4.04h-4.04z"
-      android:strokeWidth="0"
-      android:fillColor="#fff"/>
-  <path
-      android:pathData="M330.82,263.31h8.08v8.08h-8.08z"
-      android:strokeWidth="0"
-      android:fillColor="#fff"/>
-  <path
-      android:pathData="M188.8,298.95h4.04v4.04h-4.04z"
-      android:strokeWidth="0"
-      android:fillColor="#fff"/>
-  <path
-      android:pathData="M224.18,216.82h8.08v8.08h-8.08z"
-      android:strokeWidth="0"
-      android:fillColor="#fff"/>
-  <path
-      android:pathData="M272.1,318.9h8.08v8.08h-8.08z"
-      android:strokeWidth="0"
-      android:fillColor="#fff"/>
-  <path
-      android:pathData="M293.34,339.25h8.08v8.08h-8.08z"
-      android:strokeWidth="0"
-      android:fillColor="#fff"/>
-  <path
-      android:pathData="M165.09,236.28h8.08v8.08h-8.08z"
-      android:strokeWidth="0"
-      android:fillColor="#fff"/>
-  <path
-      android:pathData="M378.92,192h8.08v8.08h-8.08z"
-      android:strokeWidth="0"
-      android:fillColor="#fff"/>
-  <path
-      android:pathData="M204.28,314.86h8.08v8.08h-8.08z"
-      android:strokeWidth="0"
-      android:fillColor="#fff"/>
-  <path
-      android:pathData="M253.83,118.47c-6.04,0 -10.93,4.76 -10.93,10.62v13.1c0,1.13 0.92,2.05 2.05,2.05h0c1.13,0 2.05,-0.92 2.05,-2.05v-3.53c0,-2.27 1.84,-4.1 4.1,-4.1h5.47c2.27,0 4.1,1.84 4.1,4.1v3.53c0,1.13 0.92,2.05 2.05,2.05s2.05,-0.92 2.05,-2.05v-13.1c0,-5.86 -4.9,-10.62 -10.93,-10.62Z"
-      android:strokeWidth="0"
-      android:fillColor="#3ddc84"/>
-  <path
-      android:pathData="M256,437.7c-26.38,0 -43.81,-18.3 -61.2,-48.42 -17.39,-30.12 -103.26,-178.86 -120.65,-208.98 -17.39,-30.12 -24.52,-54.37 -11.33,-77.21 13.19,-22.84 37.75,-28.79 72.53,-28.79 34.78,0 206.53,0 241.31,0 34.78,0 59.35,5.95 72.53,28.79 13.19,22.84 6.06,47.09 -11.33,77.21 -17.39,30.12 -103.26,178.86 -120.65,208.98 -17.39,30.12 -34.83,48.42 -61.2,48.42Z"
-      android:strokeWidth="55"
+      android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"
+      android:strokeWidth="56.561"
       android:fillColor="#00000000"
-      android:strokeColor="#f86733"/>
+      android:strokeColor="#f86734"/>
+  <path
+      android:pathData="m256.22,126.57c-6.69,0 -12.12,5.27 -12.12,11.77v14.52c0,1.25 1.02,2.27 2.27,2.27h0c1.25,0 2.27,-1.02 2.27,-2.27v-3.91c0,-2.51 2.04,-4.55 4.55,-4.55h6.06c2.51,0 4.55,2.04 4.55,4.55v3.91c0,1.25 1.02,2.27 2.27,2.27s2.27,-1.02 2.27,-2.27v-14.52c0,-6.5 -5.43,-11.77 -12.12,-11.77Z"
+      android:fillColor="#3ddc84"/>
+  <path
+      android:pathData="m93.34,116.36l3.85,-4.36 29.64,9.76 -4.44,5.03 -6.23,-2.1 -7.86,8.91 2.86,5.92 -4.43,5.03 -13.39,-28.18ZM110.43,122.76l-8.86,-3.02 4.11,8.41 4.76,-5.39Z"
+      android:fillColor="#fff"/>
+  <path
+      android:pathData="m153.62,100.85l-21.71,-6.2 10.38,14.38 -5.21,3.76 -16.78,-23.26 4.49,-3.24 21.65,6.19 -10.35,-14.35 5.24,-3.78 16.78,23.26 -4.49,3.24Z"
+      android:fillColor="#fff"/>
+  <path
+      android:pathData="m161.46,63.15l8.99,-3.84c7.43,-3.18 15.96,0.12 19.09,7.44 3.13,7.32 -0.38,15.76 -7.81,18.94l-8.99,3.84 -11.28,-26.38ZM179.41,80.26c4.46,-1.91 5.96,-6.72 4.15,-10.96 -1.81,-4.24 -6.33,-6.48 -10.79,-4.57l-3.08,1.32 6.64,15.53 3.08,-1.32Z"
+      android:fillColor="#fff"/>
+  <path
+      android:pathData="m204.23,47.57l11.1,-2.2c5.31,-1.05 9.47,2.08 10.4,6.76 0.72,3.65 -0.76,6.37 -4.07,8.34l12.4,10.44 -7.57,1.5 -11.65,-9.76 -1.03,0.2 2.3,11.61 -6.3,1.25 -5.57,-28.14ZM216.78,56.7c1.86,-0.37 3,-1.71 2.68,-3.33 -0.34,-1.7 -1.88,-2.43 -3.74,-2.06l-4.04,0.8 1.07,5.39 4.04,-0.8Z"
+      android:fillColor="#fff"/>
+  <path
+      android:pathData="m244.29,55.6c0.13,-8.16 6.86,-14.72 15.06,-14.58 8.16,0.13 14.72,6.9 14.58,15.06s-6.91,14.72 -15.06,14.58c-8.2,-0.13 -14.71,-6.9 -14.58,-15.06ZM267.44,55.98c0.08,-4.64 -3.54,-8.66 -8.18,-8.74 -4.68,-0.08 -8.42,3.82 -8.5,8.47 -0.08,4.65 3.54,8.66 8.22,8.74 4.64,0.08 8.39,-3.82 8.46,-8.47Z"
+      android:fillColor="#fff"/>
+  <path
+      android:pathData="m294.39,44.84l6.31,1.23 -5.49,28.16 -6.31,-1.23 5.49,-28.16Z"
+      android:fillColor="#fff"/>
+  <path
+      android:pathData="m321.94,51.41l9.14,3.48c7.55,2.88 11.39,11.17 8.56,18.61 -2.83,7.44 -11.22,11.07 -18.77,8.19l-9.14,-3.48 10.22,-26.8ZM322.96,76.19c4.53,1.73 8.95,-0.69 10.6,-5 1.64,-4.3 -0.05,-9.06 -4.58,-10.78l-3.13,-1.19 -6.01,15.78 3.13,1.19Z"
+      android:fillColor="#fff"/>
+  <path
+      android:pathData="m381.41,89.24l-4.21,-3.21 3.65,-4.78 9.06,6.91 -17.4,22.81 -4.85,-3.7 13.75,-18.02Z"
+      android:fillColor="#fff"/>
+  <path
+      android:pathData="m397.96,126.37l-9.56,-10.26 3.61,-3.36 22.8,-1.25 3.74,4.02 -12.35,11.51 2.51,2.69 -4.08,3.8 -2.51,-2.69 -4.55,4.24 -4.16,-4.46 4.55,-4.24ZM407.83,117.17l-10.28,0.58 4.49,4.82 5.79,-5.4Z"
+      android:fillColor="#fff"/>
 </vector>
+
diff --git a/core/tests/coretests/src/android/app/backup/BackupAgentTest.java b/core/tests/coretests/src/android/app/backup/BackupAgentTest.java
index 9e8e2d6..cd5deb6 100644
--- a/core/tests/coretests/src/android/app/backup/BackupAgentTest.java
+++ b/core/tests/coretests/src/android/app/backup/BackupAgentTest.java
@@ -20,24 +20,33 @@
 
 import static org.mockito.Mockito.when;
 
+import android.app.IBackupAgent;
 import android.app.backup.BackupAgent.IncludeExcludeRules;
 import android.app.backup.BackupAnnotations.BackupDestination;
 import android.app.backup.BackupAnnotations.OperationType;
 import android.app.backup.FullBackup.BackupScheme.PathWithRequiredFlags;
+import android.content.Context;
 import android.os.ParcelFileDescriptor;
 import android.os.UserHandle;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.util.ArraySet;
 
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.server.backup.Flags;
+
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.util.Collections;
 import java.util.Map;
 import java.util.Set;
@@ -49,9 +58,12 @@
     private static final UserHandle USER_HANDLE = new UserHandle(15);
     private static final String DATA_TYPE_BACKED_UP = "test data type";
 
+    @Mock IBackupManager mIBackupManager;
     @Mock FullBackup.BackupScheme mBackupScheme;
+    @Mock Context mContext;
 
-    private BackupAgent mBackupAgent;
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 
     @Before
     public void setUp() {
@@ -67,11 +79,11 @@
         excludePaths.add(path);
         IncludeExcludeRules expectedRules = new IncludeExcludeRules(includePaths, excludePaths);
 
-        mBackupAgent = getAgentForBackupDestination(BackupDestination.CLOUD);
+        BackupAgent backupAgent = getAgentForBackupDestination(BackupDestination.CLOUD);
         when(mBackupScheme.maybeParseAndGetCanonicalExcludePaths()).thenReturn(excludePaths);
         when(mBackupScheme.maybeParseAndGetCanonicalIncludePaths()).thenReturn(includePaths);
 
-        IncludeExcludeRules rules = mBackupAgent.getIncludeExcludeRules(mBackupScheme);
+        IncludeExcludeRules rules = backupAgent.getIncludeExcludeRules(mBackupScheme);
         assertThat(rules).isEqualTo(expectedRules);
     }
 
@@ -137,6 +149,31 @@
                 0).getSuccessCount()).isEqualTo(1);
     }
 
+    @Test
+    public void doRestoreFile_agentOverrideIgnoresFile_consumesAllBytesInBuffer() throws Exception {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_CLEAR_PIPE_AFTER_RESTORE_FILE);
+        BackupAgent agent = new TestRestoreIgnoringFullBackupAgent();
+        agent.attach(mContext);
+        agent.onCreate(USER_HANDLE, BackupDestination.CLOUD, OperationType.RESTORE);
+        IBackupAgent agentBinder = (IBackupAgent) agent.onBind();
+
+        ParcelFileDescriptor[] pipes = ParcelFileDescriptor.createPipe();
+        FileOutputStream writeSide = new FileOutputStream(
+                pipes[1].getFileDescriptor());
+        writeSide.write("Hello".getBytes(StandardCharsets.UTF_8));
+
+        agentBinder.doRestoreFile(pipes[0], /* length= */ 5, BackupAgent.TYPE_FILE,
+                FullBackup.FILES_TREE_TOKEN, /* path= */ "hello_file", /* mode= */
+                0666, /* mtime= */ 12345, /* token= */ 6789, mIBackupManager);
+
+        try (FileInputStream in = new FileInputStream(pipes[0].getFileDescriptor())) {
+            assertThat(in.available()).isEqualTo(0);
+        } finally {
+            pipes[0].close();
+            pipes[1].close();
+        }
+    }
+
     private BackupAgent getAgentForBackupDestination(@BackupDestination int backupDestination) {
         BackupAgent agent = new TestFullBackupAgent();
         agent.onCreate(USER_HANDLE, backupDestination);
@@ -144,7 +181,6 @@
     }
 
     private static class TestFullBackupAgent extends BackupAgent {
-
         @Override
         public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
                 ParcelFileDescriptor newState) throws IOException {
@@ -162,4 +198,14 @@
             // Left empty as this is a full backup agent.
         }
     }
+
+    private static class TestRestoreIgnoringFullBackupAgent extends TestFullBackupAgent {
+
+        @Override
+        protected void onRestoreFile(ParcelFileDescriptor data, long size,
+                int type, String domain, String path, long mode, long mtime)
+                throws IOException {
+            // Ignore the file and don't consume any data.
+        }
+    }
 }
diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
index 0e04658..1cc8a8f 100644
--- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig
+++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
@@ -30,13 +30,6 @@
 }
 
 flag {
-    name: "enable_pip_ui_state_on_entering"
-    namespace: "multitasking"
-    description: "Enables PiP UI state callback on entering"
-    bug: "303718131"
-}
-
-flag {
     name: "enable_pip2_implementation"
     namespace: "multitasking"
     description: "Enables the new implementation of PiP (PiP2)"
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewTest.kt
index 4c76168..398fd55 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewTest.kt
@@ -22,7 +22,6 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.wm.shell.taskview.TaskView
-import com.android.wm.shell.taskview.TaskViewTaskController
 
 import com.google.common.truth.Truth.assertThat
 import com.google.common.util.concurrent.MoreExecutors.directExecutor
@@ -30,6 +29,8 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
@@ -37,10 +38,11 @@
 
     private lateinit var bubbleTaskView: BubbleTaskView
     private val context = ApplicationProvider.getApplicationContext<Context>()
+    private lateinit var taskView: TaskView
 
     @Before
     fun setUp() {
-        val taskView = TaskView(context, mock<TaskViewTaskController>())
+        taskView = mock()
         bubbleTaskView = BubbleTaskView(taskView, directExecutor())
     }
 
@@ -72,4 +74,19 @@
         assertThat(actualTaskId).isEqualTo(123)
         assertThat(actualComponentName).isEqualTo(componentName)
     }
+
+    @Test
+    fun cleanup_invalidTaskId_doesNotRemoveTask() {
+        bubbleTaskView.cleanup()
+        verify(taskView, never()).removeTask()
+    }
+
+    @Test
+    fun cleanup_validTaskId_removesTask() {
+        val componentName = ComponentName(context, "TestClass")
+        bubbleTaskView.listener.onTaskCreated(123, componentName)
+
+        bubbleTaskView.cleanup()
+        verify(taskView).removeTask()
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
index 87c8f52..f32f030 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
@@ -51,6 +51,8 @@
 import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView;
 import com.android.wm.shell.bubbles.bar.BubbleBarLayerView;
 import com.android.wm.shell.common.bubbles.BubbleInfo;
+import com.android.wm.shell.taskview.TaskView;
+import com.android.wm.shell.taskview.TaskViewTaskController;
 
 import java.io.PrintWriter;
 import java.util.List;
@@ -105,6 +107,8 @@
     private BubbleExpandedView mExpandedView;
     @Nullable
     private BubbleBarExpandedView mBubbleBarExpandedView;
+    @Nullable
+    private BubbleTaskView mBubbleTaskView;
 
     private BubbleViewInfoTask mInflationTask;
     private boolean mInflateSynchronously;
@@ -394,6 +398,21 @@
     }
 
     /**
+     * Returns the existing {@link #mBubbleTaskView} if it's not {@code null}. Otherwise a new
+     * instance of {@link BubbleTaskView} is created.
+     */
+    public BubbleTaskView getOrCreateBubbleTaskView(Context context, BubbleController controller) {
+        if (mBubbleTaskView == null) {
+            TaskViewTaskController taskViewTaskController = new TaskViewTaskController(context,
+                    controller.getTaskOrganizer(),
+                    controller.getTaskViewTransitions(), controller.getSyncTransactionQueue());
+            TaskView taskView = new TaskView(context, taskViewTaskController);
+            mBubbleTaskView = new BubbleTaskView(taskView, controller.getMainExecutor());
+        }
+        return mBubbleTaskView;
+    }
+
+    /**
      * @return the ShortcutInfo id if it exists, or the metadata shortcut id otherwise.
      */
     String getShortcutId() {
@@ -415,6 +434,10 @@
      * the bubble.
      */
     void cleanupExpandedView() {
+        cleanupExpandedView(true);
+    }
+
+    private void cleanupExpandedView(boolean cleanupTaskView) {
         if (mExpandedView != null) {
             mExpandedView.cleanUpExpandedState();
             mExpandedView = null;
@@ -423,17 +446,37 @@
             mBubbleBarExpandedView.cleanUpExpandedState();
             mBubbleBarExpandedView = null;
         }
+        if (cleanupTaskView) {
+            cleanupTaskView();
+        }
         if (mIntent != null) {
             mIntent.unregisterCancelListener(mIntentCancelListener);
         }
         mIntentActive = false;
     }
 
+    private void cleanupTaskView() {
+        if (mBubbleTaskView != null) {
+            mBubbleTaskView.cleanup();
+            mBubbleTaskView = null;
+        }
+    }
+
     /**
      * Call when all the views should be removed/cleaned up.
      */
     void cleanupViews() {
-        cleanupExpandedView();
+        cleanupViews(true);
+    }
+
+    /**
+     * Call when all the views should be removed/cleaned up.
+     *
+     * <p>If we're switching between bar and floating modes, pass {@code false} on
+     * {@code cleanupTaskView} to avoid recreating it in the new mode.
+     */
+    void cleanupViews(boolean cleanupTaskView) {
+        cleanupExpandedView(cleanupTaskView);
         mIconView = null;
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index a5f7880..1a6bf28 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -173,7 +173,7 @@
     private final Context mContext;
     private final BubblesImpl mImpl = new BubblesImpl();
     private Bubbles.BubbleExpandListener mExpandListener;
-    @Nullable private BubbleStackView.SurfaceSynchronizer mSurfaceSynchronizer;
+    @Nullable private final BubbleStackView.SurfaceSynchronizer mSurfaceSynchronizer;
     private final FloatingContentCoordinator mFloatingContentCoordinator;
     private final BubbleDataRepository mDataRepository;
     private final WindowManagerShellWrapper mWindowManagerShellWrapper;
@@ -197,12 +197,12 @@
     private final Handler mMainHandler;
     private final ShellExecutor mBackgroundExecutor;
 
-    private BubbleLogger mLogger;
-    private BubbleData mBubbleData;
+    private final BubbleLogger mLogger;
+    private final BubbleData mBubbleData;
     @Nullable private BubbleStackView mStackView;
     @Nullable private BubbleBarLayerView mLayerView;
     private BubbleIconFactory mBubbleIconFactory;
-    private BubblePositioner mBubblePositioner;
+    private final BubblePositioner mBubblePositioner;
     private Bubbles.SysuiProxy mSysuiProxy;
 
     // Tracks the id of the current (foreground) user.
@@ -232,13 +232,17 @@
     /** Whether or not the BubbleStackView has been added to the WindowManager. */
     private boolean mAddedToWindowManager = false;
 
-    /** Saved screen density, used to detect display size changes in {@link #onConfigChanged}. */
+    /**
+     * Saved screen density, used to detect display size changes in {@link #onConfigurationChanged}.
+     */
     private int mDensityDpi = Configuration.DENSITY_DPI_UNDEFINED;
 
-    /** Saved screen bounds, used to detect screen size changes in {@link #onConfigChanged}. **/
-    private Rect mScreenBounds = new Rect();
+    /**
+     * Saved screen bounds, used to detect screen size changes in {@link #onConfigurationChanged}.
+     */
+    private final Rect mScreenBounds = new Rect();
 
-    /** Saved font scale, used to detect font size changes in {@link #onConfigChanged}. */
+    /** Saved font scale, used to detect font size changes in {@link #onConfigurationChanged}. */
     private float mFontScale = 0;
 
     /** Saved direction, used to detect layout direction changes @link #onConfigChanged}. */
@@ -253,9 +257,9 @@
     private boolean mIsStatusBarShade = true;
 
     /** One handed mode controller to register transition listener. */
-    private Optional<OneHandedController> mOneHandedOptional;
+    private final Optional<OneHandedController> mOneHandedOptional;
     /** Drag and drop controller to register listener for onDragStarted. */
-    private Optional<DragAndDropController> mDragAndDropController;
+    private final Optional<DragAndDropController> mDragAndDropController;
     /** Used to send bubble events to launcher. */
     private Bubbles.BubbleStateListener mBubbleStateListener;
 
@@ -731,9 +735,11 @@
             }
         } else {
             if (mStackView == null) {
+                BubbleStackViewManager bubbleStackViewManager =
+                        BubbleStackViewManager.fromBubbleController(this);
                 mStackView = new BubbleStackView(
-                        mContext, this, mBubbleData, mSurfaceSynchronizer,
-                        mFloatingContentCoordinator, this, mMainExecutor);
+                        mContext, bubbleStackViewManager, mBubblePositioner, mBubbleData,
+                        mSurfaceSynchronizer, mFloatingContentCoordinator, this, mMainExecutor);
                 mStackView.onOrientationChanged();
                 if (mExpandListener != null) {
                     mStackView.setExpandListener(mExpandListener);
@@ -893,7 +899,6 @@
      * Called by the BubbleStackView and whenever all bubbles have animated out, and none have been
      * added in the meantime.
      */
-    @VisibleForTesting
     public void onAllBubblesAnimatedOut() {
         if (mStackView != null) {
             mStackView.setVisibility(INVISIBLE);
@@ -1047,7 +1052,6 @@
         return mBubbleData.hasBubbles() || mBubbleData.isShowingOverflow();
     }
 
-    @VisibleForTesting
     public boolean isStackExpanded() {
         return mBubbleData.isExpanded();
     }
@@ -1105,9 +1109,8 @@
      * <p>This is used by external callers (launcher).
      */
     @VisibleForTesting
-    public void expandStackAndSelectBubbleFromLauncher(String key, int bubbleBarOffsetX,
-            int bubbleBarOffsetY) {
-        mBubblePositioner.setBubbleBarPosition(bubbleBarOffsetX, bubbleBarOffsetY);
+    public void expandStackAndSelectBubbleFromLauncher(String key, Rect bubbleBarBounds) {
+        mBubblePositioner.setBubbleBarPosition(bubbleBarBounds);
 
         if (BubbleOverflow.KEY.equals(key)) {
             mBubbleData.setSelectedBubbleFromLauncher(mBubbleData.getOverflow());
@@ -1366,8 +1369,9 @@
             mStackView.resetOverflowView();
             mStackView.removeAllViews();
         }
-        // cleanup existing bubble views so they can be recreated later if needed.
-        mBubbleData.getBubbles().forEach(Bubble::cleanupViews);
+        // cleanup existing bubble views so they can be recreated later if needed, but retain
+        // TaskView.
+        mBubbleData.getBubbles().forEach(b -> b.cleanupViews(/* cleanupTaskView= */ false));
 
         // remove the current bubble container from window manager, null it out, and create a new
         // container based on the current mode.
@@ -1478,7 +1482,6 @@
      * <p>
      * Must be called from the main thread.
      */
-    @VisibleForTesting
     @MainThread
     public void removeBubble(String key, int reason) {
         if (mBubbleData.hasAnyBubbleWithKey(key)) {
@@ -1486,36 +1489,6 @@
         }
     }
 
-    // TODO(b/316358859): remove this method after task views are shared across modes
-    /**
-     * Removes the bubble with the given key after task removal, unless the task was removed as
-     * a result of mode switching, in which case, the bubble isn't removed because it will be
-     * re-inflated for the new mode.
-     */
-    @MainThread
-    public void removeFloatingBubbleAfterTaskRemoval(String key, int reason) {
-        // if we're floating remove the bubble. otherwise, we're here because the task was removed
-        // after switching modes. See b/316358859
-        if (!isShowingAsBubbleBar()) {
-            removeBubble(key, reason);
-        }
-    }
-
-    // TODO(b/316358859): remove this method after task views are shared across modes
-    /**
-     * Removes the bubble with the given key after task removal, unless the task was removed as
-     * a result of mode switching, in which case, the bubble isn't removed because it will be
-     * re-inflated for the new mode.
-     */
-    @MainThread
-    public void removeBarBubbleAfterTaskRemoval(String key, int reason) {
-        // if we're showing as bubble bar remove the bubble. otherwise, we're here because the task
-        // was removed after switching modes. See b/316358859
-        if (isShowingAsBubbleBar()) {
-            removeBubble(key, reason);
-        }
-    }
-
     /**
      * Removes all the bubbles.
      * <p>
@@ -2198,10 +2171,10 @@
         }
 
         @Override
-        public void showBubble(String key, int bubbleBarOffsetX, int bubbleBarOffsetY) {
+        public void showBubble(String key, Rect bubbleBarBounds) {
             mMainExecutor.execute(
                     () -> mController.expandStackAndSelectBubbleFromLauncher(
-                            key, bubbleBarOffsetX, bubbleBarOffsetY));
+                            key, bubbleBarBounds));
         }
 
         @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
index 9f7d0ac..efc4d8b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
@@ -27,12 +27,10 @@
 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.wm.shell.bubbles.BubblePositioner.MAX_HEIGHT;
-import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
 
 import android.annotation.NonNull;
 import android.annotation.SuppressLint;
 import android.app.ActivityOptions;
-import android.app.ActivityTaskManager;
 import android.app.PendingIntent;
 import android.content.ComponentName;
 import android.content.Context;
@@ -49,7 +47,6 @@
 import android.graphics.PorterDuff;
 import android.graphics.Rect;
 import android.graphics.drawable.ShapeDrawable;
-import android.os.RemoteException;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
 import android.util.IntProperty;
@@ -311,8 +308,7 @@
                         + " bubble=" + getBubbleKey());
             }
             if (mBubble != null) {
-                mController.removeFloatingBubbleAfterTaskRemoval(
-                        mBubble.getKey(), Bubbles.DISMISS_TASK_FINISHED);
+                mController.removeBubble(mBubble.getKey(), Bubbles.DISMISS_TASK_FINISHED);
             }
             if (mTaskView != null) {
                 // Release the surface
@@ -1105,32 +1101,11 @@
         return ((LinearLayout.LayoutParams) mManageButton.getLayoutParams()).getMarginStart();
     }
 
-    /**
-     * Cleans up anything related to the task. The TaskView itself is released after the task
-     * has been removed.
-     *
-     * If this view should be reused after this method is called, then
-     * {@link #initialize(BubbleController, BubbleStackView, boolean, BubbleTaskView)}
-     * must be invoked first.
-     */
+    /** Hide the task view. */
     public void cleanUpExpandedState() {
         if (DEBUG_BUBBLE_EXPANDED_VIEW) {
             Log.d(TAG, "cleanUpExpandedState: bubble=" + getBubbleKey() + " task=" + mTaskId);
         }
-        if (getTaskId() != INVALID_TASK_ID) {
-            // Ensure the task is removed from WM
-            if (ENABLE_SHELL_TRANSITIONS) {
-                if (mTaskView != null) {
-                    mTaskView.removeTask();
-                }
-            } else {
-                try {
-                    ActivityTaskManager.getService().removeTask(getTaskId());
-                } catch (RemoteException e) {
-                    Log.w(TAG, e.getMessage());
-                }
-            }
-        }
         if (mTaskView != null) {
             mTaskView.setVisibility(GONE);
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
index a76bd26..d62c86c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
@@ -19,7 +19,6 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Insets;
-import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.RectF;
@@ -97,7 +96,7 @@
     private PointF mRestingStackPosition;
 
     private boolean mShowingInBubbleBar;
-    private final Point mBubbleBarPosition = new Point();
+    private final Rect mBubbleBarBounds = new Rect();
 
     public BubblePositioner(Context context, WindowManager windowManager) {
         mContext = context;
@@ -791,15 +790,10 @@
     }
 
     /**
-     * Sets the position of the bubble bar in screen coordinates.
-     *
-     * @param offsetX the offset of the bubble bar from the edge of the screen on the X axis
-     * @param offsetY the offset of the bubble bar from the edge of the screen on the Y axis
+     * Sets the position of the bubble bar in display coordinates.
      */
-    public void setBubbleBarPosition(int offsetX, int offsetY) {
-        mBubbleBarPosition.set(
-                getAvailableRect().width() - offsetX,
-                getAvailableRect().height() + mInsets.top + mInsets.bottom - offsetY);
+    public void setBubbleBarPosition(Rect bubbleBarBounds) {
+        mBubbleBarBounds.set(bubbleBarBounds);
     }
 
     /**
@@ -820,7 +814,7 @@
 
     /** The bottom position of the expanded view when showing above the bubble bar. */
     public int getExpandedViewBottomForBubbleBar() {
-        return mBubbleBarPosition.y - mExpandedViewPadding;
+        return mBubbleBarBounds.top - mExpandedViewPadding;
     }
 
     /**
@@ -831,9 +825,9 @@
     }
 
     /**
-     * Returns the on screen co-ordinates of the bubble bar.
+     * Returns the display coordinates of the bubble bar.
      */
-    public Point getBubbleBarPosition() {
-        return mBubbleBarPosition;
+    public Rect getBubbleBarBounds() {
+        return mBubbleBarBounds;
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index a619401..9facef3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -204,7 +204,7 @@
                     Choreographer.getInstance().postFrameCallback(frameCallback);
                 }
             };
-    private final BubbleController mBubbleController;
+    private final BubbleStackViewManager mManager;
     private final BubbleData mBubbleData;
     private final Bubbles.SysuiProxy.Provider mSysuiProxyProvider;
     private StackViewState mStackViewState = new StackViewState();
@@ -858,6 +858,7 @@
 
     private BubbleOverflow mBubbleOverflow;
     private StackEducationView mStackEduView;
+    private StackEducationView.Manager mStackEducationViewManager;
     private ManageEducationView mManageEduView;
     private DismissView mDismissView;
 
@@ -873,15 +874,16 @@
     private BubblePositioner mPositioner;
 
     @SuppressLint("ClickableViewAccessibility")
-    public BubbleStackView(Context context, BubbleController bubbleController,
-            BubbleData data, @Nullable SurfaceSynchronizer synchronizer,
+    public BubbleStackView(Context context, BubbleStackViewManager bubbleStackViewManager,
+            BubblePositioner bubblePositioner, BubbleData data,
+            @Nullable SurfaceSynchronizer synchronizer,
             FloatingContentCoordinator floatingContentCoordinator,
             Bubbles.SysuiProxy.Provider sysuiProxyProvider,
             ShellExecutor mainExecutor) {
         super(context);
 
         mMainExecutor = mainExecutor;
-        mBubbleController = bubbleController;
+        mManager = bubbleStackViewManager;
         mBubbleData = data;
         mSysuiProxyProvider = sysuiProxyProvider;
 
@@ -893,7 +895,7 @@
         mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding);
         int elevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
 
-        mPositioner = mBubbleController.getPositioner();
+        mPositioner = bubblePositioner;
 
         final TypedArray ta = mContext.obtainStyledAttributes(
                 new int[]{android.R.attr.dialogCornerRadius});
@@ -903,7 +905,7 @@
         final Runnable onBubbleAnimatedOut = () -> {
             if (getBubbleCount() == 0) {
                 mExpandedViewTemporarilyHidden = false;
-                mBubbleController.onAllBubblesAnimatedOut();
+                mManager.onAllBubblesAnimatedOut();
             }
         };
         mStackAnimationController = new StackAnimationController(
@@ -1383,7 +1385,9 @@
             return false;
         }
         if (mStackEduView == null) {
-            mStackEduView = new StackEducationView(mContext, mPositioner, mBubbleController);
+            mStackEducationViewManager = mManager::updateWindowFlagsForBackpress;
+            mStackEduView =
+                    new StackEducationView(mContext, mPositioner, mStackEducationViewManager);
             addView(mStackEduView);
         }
         return showStackEdu();
@@ -1412,7 +1416,9 @@
     private void updateUserEdu() {
         if (isStackEduVisible() && !mStackEduView.isHiding()) {
             removeView(mStackEduView);
-            mStackEduView = new StackEducationView(mContext, mPositioner, mBubbleController);
+            mStackEducationViewManager = mManager::updateWindowFlagsForBackpress;
+            mStackEduView =
+                    new StackEducationView(mContext, mPositioner, mStackEducationViewManager);
             addView(mStackEduView);
             showStackEdu();
         }
@@ -2106,7 +2112,7 @@
             logBubbleEvent(mExpandedBubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED);
             logBubbleEvent(mExpandedBubble,
                     FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__STACK_EXPANDED);
-            mBubbleController.isNotificationPanelExpanded(notifPanelExpanded -> {
+            mManager.checkNotificationPanelExpandedState(notifPanelExpanded -> {
                 if (!notifPanelExpanded && mIsExpanded) {
                     startMonitoringSwipeUpGesture();
                 }
@@ -2227,7 +2233,7 @@
      */
     void hideCurrentInputMethod() {
         mPositioner.setImeVisible(false, 0);
-        mBubbleController.hideCurrentInputMethod();
+        mManager.hideCurrentInputMethod();
     }
 
     /** Set the stack position to whatever the positioner says. */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackViewManager.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackViewManager.kt
new file mode 100644
index 0000000..fb597a0
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackViewManager.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.wm.shell.bubbles
+
+import java.util.function.Consumer
+
+/** Defines callbacks from [BubbleStackView] to its manager. */
+interface BubbleStackViewManager {
+
+    /** Notifies that all bubbles animated out. */
+    fun onAllBubblesAnimatedOut()
+
+    /** Notifies whether backpress should be intercepted. */
+    fun updateWindowFlagsForBackpress(interceptBack: Boolean)
+
+    /**
+     * Checks the current expansion state of the notification panel, and invokes [callback] with the
+     * result.
+     */
+    fun checkNotificationPanelExpandedState(callback: Consumer<Boolean>)
+
+    /** Requests to hide the current input method. */
+    fun hideCurrentInputMethod()
+
+    companion object {
+
+        @JvmStatic
+        fun fromBubbleController(controller: BubbleController) = object : BubbleStackViewManager {
+            override fun onAllBubblesAnimatedOut() {
+                controller.onAllBubblesAnimatedOut()
+            }
+
+            override fun updateWindowFlagsForBackpress(interceptBack: Boolean) {
+                controller.updateWindowFlagsForBackpress(interceptBack)
+            }
+
+            override fun checkNotificationPanelExpandedState(callback: Consumer<Boolean>) {
+                controller.isNotificationPanelExpanded(callback)
+            }
+
+            override fun hideCurrentInputMethod() {
+                controller.hideCurrentInputMethod()
+            }
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskView.kt
index 2fcd133..65f8e48 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskView.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskView.kt
@@ -16,10 +16,14 @@
 
 package com.android.wm.shell.bubbles
 
+import android.app.ActivityTaskManager
 import android.app.ActivityTaskManager.INVALID_TASK_ID
 import android.content.ComponentName
+import android.os.RemoteException
+import android.util.Log
 import androidx.annotation.VisibleForTesting
 import com.android.wm.shell.taskview.TaskView
+import com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS
 import java.util.concurrent.Executor
 
 /**
@@ -78,4 +82,28 @@
     init {
         taskView.setListener(executor, listener)
     }
+
+    /**
+     * Removes the [TaskView] from window manager.
+     *
+     * This should be called after all other cleanup animations have finished.
+     */
+    fun cleanup() {
+        if (taskId != INVALID_TASK_ID) {
+            // Ensure the task is removed from WM
+            if (ENABLE_SHELL_TRANSITIONS) {
+                taskView.removeTask()
+            } else {
+                try {
+                    ActivityTaskManager.getService().removeTask(taskId)
+                } catch (e: RemoteException) {
+                    Log.w(TAG, e.message ?: "")
+                }
+            }
+        }
+    }
+
+    private companion object {
+        const val TAG = "BubbleTaskView"
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
index 5855a81..5fc67d7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
@@ -29,7 +29,6 @@
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Rect;
-import android.os.RemoteException;
 import android.util.Log;
 import android.view.View;
 
@@ -183,8 +182,11 @@
                         + " bubble=" + getBubbleKey());
             }
             if (mBubble != null) {
-                mController.removeBarBubbleAfterTaskRemoval(
-                        mBubble.getKey(), Bubbles.DISMISS_TASK_FINISHED);
+                mController.removeBubble(mBubble.getKey(), Bubbles.DISMISS_TASK_FINISHED);
+            }
+            if (mTaskView != null) {
+                mTaskView.release();
+                mTaskView = null;
             }
         }
 
@@ -228,24 +230,6 @@
         return false;
     }
 
-    /** Cleans up anything related to the task and {@code TaskView}. */
-    public void cleanUpTaskView() {
-        if (DEBUG_BUBBLE_EXPANDED_VIEW) {
-            Log.d(TAG, "cleanUpExpandedState: bubble=" + getBubbleKey() + " task=" + mTaskId);
-        }
-        if (mTaskId != INVALID_TASK_ID) {
-            try {
-                ActivityTaskManager.getService().removeTask(mTaskId);
-            } catch (RemoteException e) {
-                Log.w(TAG, e.getMessage());
-            }
-        }
-        if (mTaskView != null) {
-            mTaskView.release();
-            mTaskView = null;
-        }
-    }
-
     /** Returns the bubble key associated with this view. */
     @Nullable
     public String getBubbleKey() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java
index c3d899e..5fc10a9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java
@@ -46,8 +46,6 @@
 import com.android.wm.shell.R;
 import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView;
 import com.android.wm.shell.bubbles.bar.BubbleBarLayerView;
-import com.android.wm.shell.taskview.TaskView;
-import com.android.wm.shell.taskview.TaskViewTaskController;
 
 import java.lang.ref.WeakReference;
 import java.util.Objects;
@@ -175,7 +173,7 @@
             BubbleViewInfo info = new BubbleViewInfo();
 
             if (!skipInflation && !b.isInflated()) {
-                BubbleTaskView bubbleTaskView = createBubbleTaskView(c, controller);
+                BubbleTaskView bubbleTaskView = b.getOrCreateBubbleTaskView(c, controller);
                 LayoutInflater inflater = LayoutInflater.from(c);
                 info.bubbleBarExpandedView = (BubbleBarExpandedView) inflater.inflate(
                         R.layout.bubble_bar_expanded_view, layerView, false /* attachToRoot */);
@@ -205,7 +203,7 @@
                         R.layout.bubble_view, stackView, false /* attachToRoot */);
                 info.imageView.initialize(controller.getPositioner());
 
-                BubbleTaskView bubbleTaskView = createBubbleTaskView(c, controller);
+                BubbleTaskView bubbleTaskView = b.getOrCreateBubbleTaskView(c, controller);
                 info.expandedView = (BubbleExpandedView) inflater.inflate(
                         R.layout.bubble_expanded_view, stackView, false /* attachToRoot */);
                 info.expandedView.initialize(
@@ -225,15 +223,6 @@
             }
             return info;
         }
-
-        private static BubbleTaskView createBubbleTaskView(
-                Context context, BubbleController controller) {
-            TaskViewTaskController taskViewTaskController = new TaskViewTaskController(context,
-                    controller.getTaskOrganizer(),
-                    controller.getTaskViewTransitions(), controller.getSyncTransactionQueue());
-            TaskView taskView = new TaskView(context, taskViewTaskController);
-            return new BubbleTaskView(taskView, controller.getMainExecutor());
-        }
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl
index 5776ad1..7a5afec 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl
@@ -17,6 +17,7 @@
 package com.android.wm.shell.bubbles;
 
 import android.content.Intent;
+import android.graphics.Rect;
 import com.android.wm.shell.bubbles.IBubblesListener;
 
 /**
@@ -29,7 +30,7 @@
 
     oneway void unregisterBubbleListener(in IBubblesListener listener) = 2;
 
-    oneway void showBubble(in String key, in int bubbleBarOffsetX, in int bubbleBarOffsetY) = 3;
+    oneway void showBubble(in String key, in Rect bubbleBarBounds) = 3;
 
     oneway void removeBubble(in String key) = 4;
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt
index 95f1017..c4108c4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt
@@ -35,7 +35,7 @@
 class StackEducationView(
     context: Context,
     private val positioner: BubblePositioner,
-    private val controller: BubbleController
+    private val manager: Manager
 ) : LinearLayout(context) {
 
     companion object {
@@ -44,6 +44,12 @@
         private const val ANIMATE_DURATION_SHORT: Long = 40
     }
 
+    /** Callbacks to notify managers of [StackEducationView] about events. */
+    interface Manager {
+        /** Notifies whether backpress should be intercepted. */
+        fun updateWindowFlagsForBackpress(interceptBack: Boolean)
+    }
+
     private val view by lazy { requireViewById<View>(R.id.stack_education_layout) }
     private val titleTextView by lazy { requireViewById<TextView>(R.id.stack_education_title) }
     private val descTextView by lazy { requireViewById<TextView>(R.id.stack_education_description) }
@@ -93,7 +99,7 @@
     override fun onDetachedFromWindow() {
         super.onDetachedFromWindow()
         setOnKeyListener(null)
-        controller.updateWindowFlagsForBackpress(false /* interceptBack */)
+        manager.updateWindowFlagsForBackpress(false /* interceptBack */)
     }
 
     private fun setTextColor() {
@@ -124,7 +130,7 @@
         isHiding = false
         if (visibility == VISIBLE) return false
 
-        controller.updateWindowFlagsForBackpress(true /* interceptBack */)
+        manager.updateWindowFlagsForBackpress(true /* interceptBack */)
         layoutParams.width =
                 if (positioner.isLargeScreen || positioner.isLandscape)
                     context.resources.getDimensionPixelSize(R.dimen.bubbles_user_education_width)
@@ -185,7 +191,7 @@
         if (visibility != VISIBLE || isHiding) return
         isHiding = true
 
-        controller.updateWindowFlagsForBackpress(false /* interceptBack */)
+        manager.updateWindowFlagsForBackpress(false /* interceptBack */)
         animate()
             .alpha(0f)
             .setDuration(if (isExpanding) ANIMATE_DURATION_SHORT else ANIMATE_DURATION)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
index 893a87f..84a616f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
@@ -22,6 +22,7 @@
 import android.animation.ValueAnimator;
 import android.content.Context;
 import android.graphics.Point;
+import android.graphics.Rect;
 import android.util.Log;
 import android.util.Size;
 import android.view.View;
@@ -149,12 +150,12 @@
         bbev.setVisibility(VISIBLE);
 
         // Set the pivot point for the scale, so the view animates out from the bubble bar.
-        Point bubbleBarPosition = mPositioner.getBubbleBarPosition();
+        Rect bubbleBarBounds = mPositioner.getBubbleBarBounds();
         mExpandedViewContainerMatrix.setScale(
                 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
                 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
-                bubbleBarPosition.x,
-                bubbleBarPosition.y);
+                bubbleBarBounds.centerX(),
+                bubbleBarBounds.top);
 
         bbev.setAnimationMatrix(mExpandedViewContainerMatrix);
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
index 3cf23ac..00d683e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
@@ -272,7 +272,6 @@
             if (mTaskView != null) {
                 removeView(mTaskView);
             }
-            mBubbleTaskViewHelper.cleanUpTaskView();
         }
         mMenuViewController.hideMenu(false /* animated */);
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
index c0fc02fa..f82212d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
@@ -86,10 +86,10 @@
     ) {
         visibleTasksListeners[visibleTasksListener] = executor
         displayData.keyIterator().forEach { displayId ->
-            val visibleTasks = getVisibleTaskCount(displayId)
+            val visibleTasksCount = getVisibleTaskCount(displayId)
             val stashed = isStashed(displayId)
             executor.execute {
-                visibleTasksListener.onVisibilityChanged(displayId, visibleTasks > 0)
+                visibleTasksListener.onTasksVisibilityChanged(displayId, visibleTasksCount)
                 visibleTasksListener.onStashedChanged(displayId, stashed)
             }
         }
@@ -222,10 +222,8 @@
             val otherDisplays = displayData.keyIterator().asSequence().filter { it != displayId }
             for (otherDisplayId in otherDisplays) {
                 if (displayData[otherDisplayId].visibleTasks.remove(taskId)) {
-                    // Task removed from other display, check if we should notify listeners
-                    if (displayData[otherDisplayId].visibleTasks.isEmpty()) {
-                        notifyVisibleTaskListeners(otherDisplayId, hasVisibleFreeformTasks = false)
-                    }
+                    notifyVisibleTaskListeners(otherDisplayId,
+                        displayData[otherDisplayId].visibleTasks.size)
                 }
             }
         }
@@ -248,15 +246,15 @@
             )
         }
 
-        // Check if count changed and if there was no tasks or this is the first task
-        if (prevCount != newCount && (prevCount == 0 || newCount == 0)) {
-            notifyVisibleTaskListeners(displayId, newCount > 0)
+        // Check if count changed
+        if (prevCount != newCount) {
+            notifyVisibleTaskListeners(displayId, newCount)
         }
     }
 
-    private fun notifyVisibleTaskListeners(displayId: Int, hasVisibleFreeformTasks: Boolean) {
+    private fun notifyVisibleTaskListeners(displayId: Int, visibleTasksCount: Int) {
         visibleTasksListeners.forEach { (listener, executor) ->
-            executor.execute { listener.onVisibilityChanged(displayId, hasVisibleFreeformTasks) }
+            executor.execute { listener.onTasksVisibilityChanged(displayId, visibleTasksCount) }
         }
     }
 
@@ -379,9 +377,9 @@
      */
     interface VisibleTasksListener {
         /**
-         * Called when the desktop starts or stops showing freeform tasks.
+         * Called when the desktop changes the number of visible freeform tasks.
          */
-        fun onVisibilityChanged(displayId: Int, hasVisibleFreeformTasks: Boolean) {}
+        fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) {}
 
         /**
          * Called when the desktop stashed status changes.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 28c06a4..a089e81 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -106,8 +106,8 @@
         visualIndicator = null
     }
     private val taskVisibilityListener = object : VisibleTasksListener {
-        override fun onVisibilityChanged(displayId: Int, hasVisibleFreeformTasks: Boolean) {
-            launchAdjacentController.launchAdjacentEnabled = !hasVisibleFreeformTasks
+        override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) {
+            launchAdjacentController.launchAdjacentEnabled = visibleTasksCount == 0
         }
     }
     private val dragToDesktopStateListener = object : DragToDesktopStateListener {
@@ -1033,14 +1033,16 @@
                 SingleInstanceRemoteListener<DesktopTasksController, IDesktopTaskListener>
 
         private val listener: VisibleTasksListener = object : VisibleTasksListener {
-            override fun onVisibilityChanged(displayId: Int, visible: Boolean) {
+            override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) {
                 KtProtoLog.v(
                         WM_SHELL_DESKTOP_MODE,
-                        "IDesktopModeImpl: onVisibilityChanged display=%d visible=%b",
+                        "IDesktopModeImpl: onVisibilityChanged display=%d visible=%d",
                         displayId,
-                        visible
+                        visibleTasksCount
                 )
-                remoteListener.call { l -> l.onVisibilityChanged(displayId, visible) }
+                remoteListener.call {
+                    l -> l.onTasksVisibilityChanged(displayId, visibleTasksCount)
+                }
             }
 
             override fun onStashedChanged(displayId: Int, stashed: Boolean) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
index c3a82ce..731fec7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
@@ -239,7 +239,7 @@
                     show(change.leash)
                 }
             } else if (TransitionInfo.isIndependent(change, info)) {
-                // Root.
+                // Root(s).
                 when (state) {
                     is TransitionState.FromSplit -> {
                         state.splitRootChange = change
@@ -256,6 +256,9 @@
                         }
                     }
                     is TransitionState.FromFullscreen -> {
+                        // Most of the time we expect one change/task here, which should be the
+                        // same that initiated the drag and that should be layered on top of
+                        // everything.
                         if (change.taskInfo?.taskId == state.draggedTaskId) {
                             state.draggedTaskChange = change
                             val bounds = change.endAbsBounds
@@ -265,7 +268,18 @@
                                 show(change.leash)
                             }
                         } else {
-                            throw IllegalStateException("Expected root to be dragged task")
+                            // It's possible to see an additional change that isn't the dragged
+                            // task when the dragged task is translucent and so the task behind it
+                            // is included in the transition since it was visible and is now being
+                            // occluded by the Home task. Just layer it at the bottom and save it
+                            // in case we need to restore order if the drag is cancelled.
+                            state.otherRootChanges.add(change)
+                            val bounds = change.endAbsBounds
+                            startTransaction.apply {
+                                setLayer(change.leash, appLayers - i)
+                                setWindowCrop(change.leash, bounds.width(), bounds.height())
+                                show(change.leash)
+                            }
                         }
                     }
                 }
@@ -515,8 +529,18 @@
         val wct = WindowContainerTransaction()
         when (state) {
             is TransitionState.FromFullscreen -> {
+                // There may have been tasks sent behind home that are not the dragged task (like
+                // when the dragged task is translucent and that makes the task behind it visible).
+                // Restore the order of those first.
+                state.otherRootChanges.mapNotNull { it.container }.forEach { wc ->
+                    // TODO(b/322852244): investigate why even though these "other" tasks are
+                    //  reordered in front of home and behind the translucent dragged task, its
+                    //  surface is not visible on screen.
+                    wct.reorder(wc, true /* toTop */)
+                }
                 val wc = state.draggedTaskChange?.container
                         ?: error("Dragged task should be non-null before cancelling")
+                // Then the dragged task a the very top.
                 wct.reorder(wc, true /* toTop */)
             }
             is TransitionState.FromSplit -> {
@@ -574,6 +598,7 @@
                 override var draggedTaskChange: Change? = null,
                 override var cancelled: Boolean = false,
                 override var startAborted: Boolean = false,
+                var otherRootChanges: MutableList<Change> = mutableListOf()
         ) : TransitionState()
         data class FromSplit(
                 override val draggedTaskId: Int,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl
index 39128a8..8ed87f2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl
@@ -22,8 +22,8 @@
  */
 interface IDesktopTaskListener {
 
-    /** Desktop task visibility has change. Visible if at least 1 task is visible. */
-    oneway void onVisibilityChanged(int displayId, boolean visible);
+    /** Desktop tasks visibility has changed. Visible if at least 1 task is visible. */
+    oneway void onTasksVisibilityChanged(int displayId, int visibleTasksCount);
 
     /** Desktop task stashed status has changed. */
     oneway void onStashedChanged(int displayId, boolean stashed);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index 1f7cc5a..a8b39c41 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -843,7 +843,18 @@
 
     @Nullable
     private DesktopModeWindowDecoration getRelevantWindowDecor(MotionEvent ev) {
-        if (mSplitScreenController != null && mSplitScreenController.isSplitScreenVisible()) {
+        final DesktopModeWindowDecoration focusedDecor = getFocusedDecor();
+        if (focusedDecor == null) {
+            return null;
+        }
+        final boolean splitScreenVisible = mSplitScreenController != null
+                && mSplitScreenController.isSplitScreenVisible();
+        // It's possible that split tasks are visible but neither is focused, such as when there's
+        // a fullscreen translucent window on top of them. In that case, the relevant decor should
+        // just be that translucent focused window.
+        final boolean focusedTaskInSplit = mSplitScreenController != null
+                && mSplitScreenController.isTaskInSplitScreen(focusedDecor.mTaskInfo.taskId);
+        if (splitScreenVisible && focusedTaskInSplit) {
             // We can't look at focused task here as only one task will have focus.
             DesktopModeWindowDecoration splitTaskDecor = getSplitScreenDecor(ev);
             return splitTaskDecor == null ? getFocusedDecor() : splitTaskDecor;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt
index 4878df8..75965d6 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt
@@ -56,9 +56,7 @@
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.whenever
 
-/**
- * Tests for loading / inflating views & icons for a bubble.
- */
+/** Tests for loading / inflating views & icons for a bubble. */
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
 @RunWithLooper(setAsMainLooper = true)
@@ -76,25 +74,33 @@
     @Before
     fun setup() {
         metadataFlagListener = Bubbles.BubbleMetadataFlagListener {}
-        iconFactory = BubbleIconFactory(context,
+        iconFactory =
+            BubbleIconFactory(
+                context,
                 60,
                 30,
                 Color.RED,
-                mContext.resources.getDimensionPixelSize(
-                        R.dimen.importance_ring_stroke_width))
+                mContext.resources.getDimensionPixelSize(R.dimen.importance_ring_stroke_width)
+            )
 
         mainExecutor = TestShellExecutor()
         val windowManager = context.getSystemService(WindowManager::class.java)
         val shellInit = ShellInit(mainExecutor)
         val shellCommandHandler = ShellCommandHandler()
-        val shellController = ShellController(context, shellInit, shellCommandHandler,
-                mainExecutor)
+        val shellController = ShellController(context, shellInit, shellCommandHandler, mainExecutor)
         val bubblePositioner = BubblePositioner(context, windowManager)
-        val bubbleData = BubbleData(context, mock<BubbleLogger>(), bubblePositioner,
-                BubbleEducationController(context), mainExecutor)
+        val bubbleData =
+            BubbleData(
+                context,
+                mock<BubbleLogger>(),
+                bubblePositioner,
+                BubbleEducationController(context),
+                mainExecutor
+            )
         val surfaceSynchronizer = { obj: Runnable -> obj.run() }
 
-        bubbleController = BubbleController(
+        bubbleController =
+            BubbleController(
                 context,
                 shellInit,
                 shellCommandHandler,
@@ -122,18 +128,36 @@
                 mock<Transitions>(),
                 mock<SyncTransactionQueue>(),
                 mock<IWindowManager>(),
-                mock<BubbleProperties>())
+                mock<BubbleProperties>()
+            )
 
-        bubbleStackView = BubbleStackView(context, bubbleController, bubbleData,
-                surfaceSynchronizer, FloatingContentCoordinator(), bubbleController, mainExecutor)
+        val bubbleStackViewManager = BubbleStackViewManager.fromBubbleController(bubbleController)
+        bubbleStackView =
+            BubbleStackView(
+                context,
+                bubbleStackViewManager,
+                bubblePositioner,
+                bubbleData,
+                surfaceSynchronizer,
+                FloatingContentCoordinator(),
+                bubbleController,
+                mainExecutor
+            )
         bubbleBarLayerView = BubbleBarLayerView(context, bubbleController)
     }
 
     @Test
     fun testPopulate() {
         bubble = createBubbleWithShortcut()
-        val info = BubbleViewInfoTask.BubbleViewInfo.populate(context,
-                bubbleController, bubbleStackView, iconFactory, bubble, false /* skipInflation */)
+        val info =
+            BubbleViewInfoTask.BubbleViewInfo.populate(
+                context,
+                bubbleController,
+                bubbleStackView,
+                iconFactory,
+                bubble,
+                false /* skipInflation */
+            )
         assertThat(info!!).isNotNull()
 
         assertThat(info.imageView).isNotNull()
@@ -151,9 +175,15 @@
     @Test
     fun testPopulateForBubbleBar() {
         bubble = createBubbleWithShortcut()
-        val info = BubbleViewInfoTask.BubbleViewInfo.populateForBubbleBar(context,
-                bubbleController, bubbleBarLayerView, iconFactory, bubble,
-                false /* skipInflation */)
+        val info =
+            BubbleViewInfoTask.BubbleViewInfo.populateForBubbleBar(
+                context,
+                bubbleController,
+                bubbleBarLayerView,
+                iconFactory,
+                bubble,
+                false /* skipInflation */
+            )
         assertThat(info!!).isNotNull()
 
         assertThat(info.imageView).isNull()
@@ -176,12 +206,18 @@
         // exception here if the app has an issue loading the shortcut icon; we default to
         // the app icon in that case / none of the icons will be null.
         val mockIconFactory = mock<BubbleIconFactory>()
-        whenever(mockIconFactory.getBubbleDrawable(eq(context), eq(bubble.shortcutInfo),
-                any())).doThrow(RuntimeException())
+        whenever(mockIconFactory.getBubbleDrawable(eq(context), eq(bubble.shortcutInfo), any()))
+            .doThrow(RuntimeException())
 
-        val info = BubbleViewInfoTask.BubbleViewInfo.populateForBubbleBar(context,
-                bubbleController, bubbleBarLayerView, iconFactory, bubble,
-                true /* skipInflation */)
+        val info =
+            BubbleViewInfoTask.BubbleViewInfo.populateForBubbleBar(
+                context,
+                bubbleController,
+                bubbleBarLayerView,
+                iconFactory,
+                bubble,
+                true /* skipInflation */
+            )
         assertThat(info).isNotNull()
 
         assertThat(info?.shortcutInfo).isNotNull()
@@ -194,8 +230,17 @@
 
     private fun createBubbleWithShortcut(): Bubble {
         val shortcutInfo = ShortcutInfo.Builder(mContext, "mockShortcutId").build()
-        return Bubble("mockKey", shortcutInfo, 1000, Resources.ID_NULL,
-                "mockTitle", 0 /* taskId */, "mockLocus", true /* isDismissible */,
-                mainExecutor, metadataFlagListener)
+        return Bubble(
+            "mockKey",
+            shortcutInfo,
+            1000,
+            Resources.ID_NULL,
+            "mockTitle",
+            0 /* taskId */,
+            "mockLocus",
+            true /* isDismissible */,
+            mainExecutor,
+            metadataFlagListener
+        )
     }
-}
\ No newline at end of file
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
index 3fe78ef..445f74a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
@@ -124,7 +124,7 @@
         repo.addVisibleTasksListener(listener, executor)
         executor.flushAll()
 
-        assertThat(listener.hasVisibleTasksOnDefaultDisplay).isTrue()
+        assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(1)
         assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(1)
     }
 
@@ -148,7 +148,7 @@
         repo.addVisibleTasksListener(listener, executor)
         executor.flushAll()
 
-        assertThat(listener.hasVisibleTasksOnDefaultDisplay).isFalse()
+        assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(0)
         // One call as adding listener notifies it
         assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(0)
     }
@@ -162,8 +162,8 @@
         repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = true)
         executor.flushAll()
 
-        assertThat(listener.hasVisibleTasksOnDefaultDisplay).isTrue()
-        assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(1)
+        assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(2)
+        assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(2)
     }
 
     @Test
@@ -175,16 +175,16 @@
         repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
         executor.flushAll()
 
-        assertThat(listener.hasVisibleTasksOnDefaultDisplay).isTrue()
+        assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(1)
         assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(1)
-        assertThat(listener.hasVisibleTasksOnSecondaryDisplay).isFalse()
+        assertThat(listener.visibleTasksCountOnSecondaryDisplay).isEqualTo(0)
         assertThat(listener.visibleChangesOnSecondaryDisplay).isEqualTo(0)
 
         repo.updateVisibleFreeformTasks(displayId = 1, taskId = 2, visible = true)
         executor.flushAll()
 
         // Listener for secondary display is notified
-        assertThat(listener.hasVisibleTasksOnSecondaryDisplay).isTrue()
+        assertThat(listener.visibleTasksCountOnSecondaryDisplay).isEqualTo(1)
         assertThat(listener.visibleChangesOnSecondaryDisplay).isEqualTo(1)
         // No changes to listener for default display
         assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(1)
@@ -198,7 +198,7 @@
 
         repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
         executor.flushAll()
-        assertThat(listener.hasVisibleTasksOnDefaultDisplay).isTrue()
+        assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(1)
 
         // Mark task 1 visible on secondary display
         repo.updateVisibleFreeformTasks(displayId = 1, taskId = 1, visible = true)
@@ -208,11 +208,11 @@
         // 1 - visible task added
         // 2 - visible task removed
         assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(2)
-        assertThat(listener.hasVisibleTasksOnDefaultDisplay).isFalse()
+        assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(0)
 
         // Secondary display should have 1 call for visible task added
         assertThat(listener.visibleChangesOnSecondaryDisplay).isEqualTo(1)
-        assertThat(listener.hasVisibleTasksOnSecondaryDisplay).isTrue()
+        assertThat(listener.visibleTasksCountOnSecondaryDisplay).isEqualTo(1)
     }
 
     @Test
@@ -224,17 +224,17 @@
         repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = true)
         executor.flushAll()
 
-        assertThat(listener.hasVisibleTasksOnDefaultDisplay).isTrue()
+        assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(2)
         repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = false)
         executor.flushAll()
 
-        assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(1)
+        assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(3)
 
         repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = false)
         executor.flushAll()
 
-        assertThat(listener.hasVisibleTasksOnDefaultDisplay).isFalse()
-        assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(2)
+        assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(0)
+        assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(4)
     }
 
     @Test
@@ -397,8 +397,8 @@
     }
 
     class TestVisibilityListener : DesktopModeTaskRepository.VisibleTasksListener {
-        var hasVisibleTasksOnDefaultDisplay = false
-        var hasVisibleTasksOnSecondaryDisplay = false
+        var visibleTasksCountOnDefaultDisplay = 0
+        var visibleTasksCountOnSecondaryDisplay = 0
 
         var visibleChangesOnDefaultDisplay = 0
         var visibleChangesOnSecondaryDisplay = 0
@@ -409,14 +409,14 @@
         var stashedChangesOnDefaultDisplay = 0
         var stashedChangesOnSecondaryDisplay = 0
 
-        override fun onVisibilityChanged(displayId: Int, hasVisibleFreeformTasks: Boolean) {
+        override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) {
             when (displayId) {
                 DEFAULT_DISPLAY -> {
-                    hasVisibleTasksOnDefaultDisplay = hasVisibleFreeformTasks
+                    visibleTasksCountOnDefaultDisplay = visibleTasksCount
                     visibleChangesOnDefaultDisplay++
                 }
                 SECOND_DISPLAY -> {
-                    hasVisibleTasksOnSecondaryDisplay = hasVisibleFreeformTasks
+                    visibleTasksCountOnSecondaryDisplay = visibleTasksCount
                     visibleChangesOnSecondaryDisplay++
                 }
                 else -> fail("Visible task listener received unexpected display id: $displayId")
diff --git a/location/api/current.txt b/location/api/current.txt
index c55676b..c7954fe 100644
--- a/location/api/current.txt
+++ b/location/api/current.txt
@@ -682,6 +682,7 @@
   public final class AltitudeConverter {
     ctor public AltitudeConverter();
     method @WorkerThread public void addMslAltitudeToLocation(@NonNull android.content.Context, @NonNull android.location.Location) throws java.io.IOException;
+    method @FlaggedApi(Flags.FLAG_GEOID_HEIGHTS_VIA_ALTITUDE_HAL) public boolean addMslAltitudeToLocation(@NonNull android.location.Location);
   }
 
 }
diff --git a/location/java/android/location/altitude/AltitudeConverter.java b/location/java/android/location/altitude/AltitudeConverter.java
index 6f88912..461dafb 100644
--- a/location/java/android/location/altitude/AltitudeConverter.java
+++ b/location/java/android/location/altitude/AltitudeConverter.java
@@ -16,12 +16,14 @@
 
 package android.location.altitude;
 
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.WorkerThread;
 import android.content.Context;
 import android.frameworks.location.altitude.GetGeoidHeightRequest;
 import android.frameworks.location.altitude.GetGeoidHeightResponse;
 import android.location.Location;
+import android.location.flags.Flags;
 
 import com.android.internal.location.altitude.GeoidMap;
 import com.android.internal.location.altitude.S2CellIdUtils;
@@ -213,12 +215,12 @@
     }
 
     /**
-     * Same as {@link #addMslAltitudeToLocation(Context, Location)} except that data will not be
-     * loaded from raw assets. Returns true if a Mean Sea Level altitude is added to the
-     * {@code location}; otherwise, returns false and leaves the {@code location} unchanged.
-     *
-     * @hide
+     * Same as {@link #addMslAltitudeToLocation(Context, Location)} except that this method can be
+     * called on the main thread as data will not be loaded from raw assets. Returns true if a Mean
+     * Sea Level altitude is added to the {@code location}; otherwise, returns false and leaves the
+     * {@code location} unchanged.
      */
+    @FlaggedApi(Flags.FLAG_GEOID_HEIGHTS_VIA_ALTITUDE_HAL)
     public boolean addMslAltitudeToLocation(@NonNull Location location) {
         validate(location);
         MapParamsProto geoidHeightParams = GeoidMap.getGeoidHeightParams();
diff --git a/location/java/android/location/flags/location.aconfig b/location/java/android/location/flags/location.aconfig
index 32ad09c..a96fe47 100644
--- a/location/java/android/location/flags/location.aconfig
+++ b/location/java/android/location/flags/location.aconfig
@@ -8,6 +8,13 @@
 }
 
 flag {
+    name: "geoid_heights_via_altitude_hal"
+    namespace: "location"
+    description: "Flag for making geoid heights available via the Altitude HAL"
+    bug: "304375846"
+}
+
+flag {
     name: "gnss_api_navic_l1"
     namespace: "location"
     description: "Flag for GNSS API for NavIC L1"
diff --git a/media/java/android/media/metrics/EditingEndedEvent.java b/media/java/android/media/metrics/EditingEndedEvent.java
index 72e6db8..5ed8d40 100644
--- a/media/java/android/media/metrics/EditingEndedEvent.java
+++ b/media/java/android/media/metrics/EditingEndedEvent.java
@@ -86,7 +86,10 @@
      */
     public static final int ERROR_CODE_IO_NO_PERMISSION = 8;
 
-    /** */
+    /**
+     * Caused by failing to load data via cleartext HTTP, when the app's network security
+     * configuration does not permit it.
+     */
     public static final int ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED = 9;
 
     /** Caused by reading data out of the data bounds. */
@@ -146,6 +149,9 @@
     @Retention(java.lang.annotation.RetentionPolicy.SOURCE)
     public @interface ErrorCode {}
 
+    /** Special value for unknown {@linkplain #getTimeSinceCreatedMillis() time since creation}. */
+    public static final int TIME_SINCE_CREATED_UNKNOWN = -1;
+
     private final @ErrorCode int mErrorCode;
     @SuppressWarnings("HidingField") // Hiding field from superclass as for playback events.
     private final long mTimeSinceCreatedMillis;
@@ -174,16 +180,16 @@
     }
 
     /**
-     * Gets the elapsed time since creating of the editing session, in milliseconds, or -1 if
-     * unknown.
+     * Gets the elapsed time since creating of the editing session, in milliseconds, or {@link
+     * #TIME_SINCE_CREATED_UNKNOWN} if unknown.
      *
-     * @return The elapsed time since creating the editing session, in milliseconds, or -1 if
-     *     unknown.
+     * @return The elapsed time since creating the editing session, in milliseconds, or {@link
+     *     #TIME_SINCE_CREATED_UNKNOWN} if unknown.
      * @see LogSessionId
      * @see EditingSession
      */
     @Override
-    @IntRange(from = -1)
+    @IntRange(from = TIME_SINCE_CREATED_UNKNOWN)
     public long getTimeSinceCreatedMillis() {
         return mTimeSinceCreatedMillis;
     }
@@ -283,7 +289,7 @@
         public Builder(@FinalState int finalState) {
             mFinalState = finalState;
             mErrorCode = ERROR_CODE_NONE;
-            mTimeSinceCreatedMillis = -1;
+            mTimeSinceCreatedMillis = TIME_SINCE_CREATED_UNKNOWN;
             mMetricsBundle = new Bundle();
         }
 
@@ -291,11 +297,11 @@
          * Sets the elapsed time since creating the editing session, in milliseconds.
          *
          * @param timeSinceCreatedMillis The elapsed time since creating the editing session, in
-         *     milliseconds, or -1 if the value is unknown.
+         *     milliseconds, or {@link #TIME_SINCE_CREATED_UNKNOWN} if unknown.
          * @see #getTimeSinceCreatedMillis()
          */
         public @NonNull Builder setTimeSinceCreatedMillis(
-                @IntRange(from = -1) long timeSinceCreatedMillis) {
+                @IntRange(from = TIME_SINCE_CREATED_UNKNOWN) long timeSinceCreatedMillis) {
             mTimeSinceCreatedMillis = timeSinceCreatedMillis;
             return this;
         }
diff --git a/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml b/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml
index 929756c..b97e992 100644
--- a/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml
+++ b/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml
@@ -17,6 +17,7 @@
                 android:id="@android:id/content"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
+                android:minWidth="@dimen/dropdown_touch_target_min_width"
                 android:orientation="horizontal"
                 android:layout_marginEnd="@dimen/dropdown_layout_horizontal_margin"
                 android:elevation="3dp">
diff --git a/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml b/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml
index 1fe5e0e..261154f 100644
--- a/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml
+++ b/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml
@@ -17,6 +17,7 @@
                 android:id="@android:id/content"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
+                android:minWidth="@dimen/dropdown_touch_target_min_width"
                 android:layout_marginEnd="@dimen/dropdown_layout_horizontal_margin"
                 android:elevation="3dp">
 
@@ -24,7 +25,7 @@
             android:id="@android:id/icon1"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:contentDescription="@string/provider_icon_content_description"
+            android:contentDescription="@string/dropdown_presentation_more_sign_in_options_text"
             android:layout_centerVertical="true"
             android:layout_alignParentStart="true"
             android:background="@null"/>
diff --git a/packages/CredentialManager/res/values/dimens.xml b/packages/CredentialManager/res/values/dimens.xml
index 3a8c78f..53852cb 100644
--- a/packages/CredentialManager/res/values/dimens.xml
+++ b/packages/CredentialManager/res/values/dimens.xml
@@ -27,4 +27,5 @@
     <dimen name="autofill_dropdown_textview_max_width">230dp</dimen>
     <dimen name="dropdown_layout_horizontal_margin">24dp</dimen>
     <integer name="autofill_max_visible_datasets">3</integer>
+    <dimen name="dropdown_touch_target_min_width">48dp</dimen>
 </resources>
\ No newline at end of file
diff --git a/packages/PackageInstaller/AndroidManifest.xml b/packages/PackageInstaller/AndroidManifest.xml
index 5a21d59..09e0d61 100644
--- a/packages/PackageInstaller/AndroidManifest.xml
+++ b/packages/PackageInstaller/AndroidManifest.xml
@@ -75,19 +75,15 @@
             </intent-filter>
         </activity>
 
-        <!-- NOTE: the workaround to fix the screen flash problem. Remember to check the problem
-            is resolved for new implementation -->
         <activity android:name=".InstallStaging"
-                  android:theme="@style/Theme.AlertDialogActivity.NoDim"
-                  android:exported="false" />
+                android:exported="false" />
 
         <activity android:name=".DeleteStagedFileOnResult"
             android:theme="@style/Theme.AlertDialogActivity.NoActionBar"
             android:exported="false" />
 
         <activity android:name=".PackageInstallerActivity"
-                  android:theme="@style/Theme.AlertDialogActivity.NoAnimation"
-                  android:exported="false" />
+                android:exported="false" />
 
         <activity android:name=".InstallInstalling"
                 android:theme="@style/Theme.AlertDialogActivity.NoAnimation"
diff --git a/packages/PackageInstaller/res/values/themes.xml b/packages/PackageInstaller/res/values/themes.xml
index 811fa73..9a06229 100644
--- a/packages/PackageInstaller/res/values/themes.xml
+++ b/packages/PackageInstaller/res/values/themes.xml
@@ -32,9 +32,4 @@
         <item name="android:windowNoTitle">true</item>
     </style>
 
-    <style name="Theme.AlertDialogActivity.NoDim"
-        parent="@style/Theme.AlertDialogActivity.NoActionBar">
-        <item name="android:backgroundDimAmount">0</item>
-    </style>
-
 </resources>
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
index ceb580d..7dc157f 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
@@ -308,7 +308,6 @@
     }
 
     private void initiateInstall() {
-        bindUi();
         String pkgName = mPkgInfo.packageName;
         // Check if there is already a package on the device with this name
         // but it has been renamed to something else.
@@ -448,6 +447,7 @@
         if (mAppSnippet != null) {
             // load placeholder layout with OK button disabled until we override this layout in
             // startInstallConfirm
+            bindUi();
             checkIfAllowedAndInitiateInstall();
         }
 
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
index 38ec931..4c255a5 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
@@ -107,6 +107,7 @@
                 Settings.System.TOUCHPAD_POINTER_SPEED,
                 Settings.System.TOUCHPAD_NATURAL_SCROLLING,
                 Settings.System.TOUCHPAD_TAP_TO_CLICK,
+                Settings.System.TOUCHPAD_TAP_DRAGGING,
                 Settings.System.TOUCHPAD_RIGHT_CLICK_ZONE,
                 Settings.System.CAMERA_FLASH_NOTIFICATION,
                 Settings.System.SCREEN_FLASH_NOTIFICATION,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
index 98941c7..011b42f 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
@@ -209,6 +209,7 @@
         VALIDATORS.put(System.TOUCHPAD_POINTER_SPEED, new InclusiveIntegerRangeValidator(-7, 7));
         VALIDATORS.put(System.TOUCHPAD_NATURAL_SCROLLING, BOOLEAN_VALIDATOR);
         VALIDATORS.put(System.TOUCHPAD_TAP_TO_CLICK, BOOLEAN_VALIDATOR);
+        VALIDATORS.put(System.TOUCHPAD_TAP_DRAGGING, BOOLEAN_VALIDATOR);
         VALIDATORS.put(System.TOUCHPAD_RIGHT_CLICK_ZONE, BOOLEAN_VALIDATOR);
         VALIDATORS.put(System.LOCK_TO_APP_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
index 1670a70..1e146a5 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
@@ -3002,6 +3002,9 @@
         dumpSetting(s, p,
                 Settings.System.TOUCHPAD_TAP_TO_CLICK,
                 SystemSettingsProto.Touchpad.TAP_TO_CLICK);
+        dumpSetting(s, p,
+                Settings.System.TOUCHPAD_TAP_DRAGGING,
+                SystemSettingsProto.Touchpad.TAP_DRAGGING);
         p.end(touchpadToken);
 
         dumpSetting(s, p,
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 54ab5d1..0050676 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -1083,5 +1083,25 @@
         <!-- Allow SystemUI to listen for the capabilities defined in the linked xml -->
         <property android:name="android.net.PROPERTY_SELF_CERTIFIED_CAPABILITIES"
                   android:value="@xml/self_certified_network_capabilities_both" />
+
+
+        <service
+            android:name="com.android.systemui.dreams.homecontrols.HomeControlsDreamService"
+            android:exported="false"
+            android:enabled="false"
+            android:label="@string/home_controls_dream_label"
+            android:description="@string/home_controls_dream_description"
+            android:permission="android.permission.BIND_DREAM_SERVICE"
+            android:icon="@drawable/controls_icon"
+            >
+
+            <intent-filter>
+                <action android:name="android.service.dreams.DreamService" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <meta-data
+                android:name="android.service.dream"
+                android:resource="@xml/home_controls_dream_metadata" />
+        </service>
     </application>
 </manifest>
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 375fe13..0ea2b1f 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -59,14 +59,6 @@
 }
 
 flag {
-    name: "notification_lifetime_extension_refactor"
-    namespace: "systemui"
-    description: "Enables moving notification lifetime extension management from SystemUI to "
-        "Notification Manager Service"
-    bug: "299448097"
-}
-
-flag {
     name: "notifications_live_data_store_refactor"
     namespace: "systemui"
     description: "Replaces NotifLiveDataStore with ActiveNotificationListRepository, and updates consumers. "
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 f9b91cf..bd539a7 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
@@ -20,6 +20,7 @@
 
 import androidx.compose.foundation.text.KeyboardActions
 import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.IconButtonDefaults
 import androidx.compose.material3.LocalTextStyle
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.TextField
@@ -36,16 +37,21 @@
 import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.input.key.Key
 import androidx.compose.ui.input.key.key
 import androidx.compose.ui.input.key.onInterceptKeyBeforeSoftKeyboard
+import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.text.input.ImeAction
 import androidx.compose.ui.text.input.KeyboardType
 import androidx.compose.ui.text.input.PasswordVisualTransformation
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.dp
+import com.android.compose.PlatformIconButton
 import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel
+import com.android.systemui.res.R
 
 /** UI for the input part of a password-requiring version of the bouncer. */
 @Composable
@@ -64,6 +70,7 @@
     val password: String by viewModel.password.collectAsState()
     val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState()
     val animateFailure: Boolean by viewModel.animateFailure.collectAsState()
+    val isImeSwitcherButtonVisible by viewModel.isImeSwitcherButtonVisible.collectAsState()
 
     DisposableEffect(Unit) {
         viewModel.onShown()
@@ -116,5 +123,28 @@
                         false
                     }
                 },
+        trailingIcon =
+            if (isImeSwitcherButtonVisible) {
+                { ImeSwitcherButton(viewModel, color) }
+            } else null
+    )
+}
+
+/** Button for changing the password input method (IME). */
+@Composable
+private fun ImeSwitcherButton(
+    viewModel: PasswordBouncerViewModel,
+    color: Color,
+) {
+    val context = LocalContext.current
+    PlatformIconButton(
+        onClick = { viewModel.onImeSwitcherButtonClicked(context.displayId) },
+        iconResource = R.drawable.ic_lockscreen_ime,
+        contentDescription = stringResource(R.string.accessibility_ime_switch_button),
+        colors =
+            IconButtonDefaults.filledIconButtonColors(
+                contentColor = color,
+                containerColor = Color.Transparent,
+            )
     )
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
index ff5a698..92bc1f1 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
@@ -124,7 +124,7 @@
 
 // TODO(b/315490861): Remove these conversions once Compose can be used throughout SysUI.
 fun CommunalSceneKey.toTransitionSceneKey(): SceneKey {
-    return SceneKey(name = toString(), identity = this)
+    return SceneKey(debugName = toString(), identity = this)
 }
 
 /**
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
index 42fcd13..5e27d82 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
@@ -21,19 +21,27 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import com.android.compose.animation.scene.SceneScope
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
 import com.android.systemui.notifications.ui.composable.NotificationStack
 import com.android.systemui.scene.shared.flag.SceneContainerFlags
 import com.android.systemui.statusbar.notification.stack.AmbientState
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
+import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
 import com.android.systemui.statusbar.notification.stack.shared.flexiNotifsEnabled
 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
 import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackAppearanceViewBinder
+import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
 
+@SysUISingleton
 class NotificationSection
 @Inject
 constructor(
@@ -42,22 +50,41 @@
     controller: NotificationStackScrollLayoutController,
     sceneContainerFlags: SceneContainerFlags,
     sharedNotificationContainer: SharedNotificationContainer,
+    sharedNotificationContainerViewModel: SharedNotificationContainerViewModel,
     stackScrollLayout: NotificationStackScrollLayout,
     notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel,
     ambientState: AmbientState,
+    notificationStackSizeCalculator: NotificationStackSizeCalculator,
+    @Main mainDispatcher: CoroutineDispatcher,
 ) {
     init {
-        if (sceneContainerFlags.flexiNotifsEnabled()) {
+        if (!KeyguardShadeMigrationNssl.isUnexpectedlyInLegacyMode()) {
+            // This scene container section moves the NSSL to the SharedNotificationContainer. This
+            //  also requires that SharedNotificationContainer gets moved to the SceneWindowRootView
+            //  by the SceneWindowRootViewBinder.
+            // Prior to Scene Container, but when the KeyguardShadeMigrationNssl flag is enabled,
+            //  NSSL is moved into this container by the NotificationStackScrollLayoutSection.
             (stackScrollLayout.parent as? ViewGroup)?.removeView(stackScrollLayout)
             sharedNotificationContainer.addNotificationStackScrollLayout(stackScrollLayout)
 
-            NotificationStackAppearanceViewBinder.bind(
-                context,
+            SharedNotificationContainerBinder.bind(
                 sharedNotificationContainer,
-                notificationStackAppearanceViewModel,
-                ambientState,
+                sharedNotificationContainerViewModel,
+                sceneContainerFlags,
                 controller,
+                notificationStackSizeCalculator,
+                mainDispatcher,
             )
+
+            if (sceneContainerFlags.flexiNotifsEnabled()) {
+                NotificationStackAppearanceViewBinder.bind(
+                    context,
+                    sharedNotificationContainer,
+                    notificationStackAppearanceViewModel,
+                    ambientState,
+                    controller,
+                )
+            }
         }
     }
 
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt
new file mode 100644
index 0000000..2ba78cf
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.gestures.Orientation
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import com.android.compose.nestedscroll.PriorityNestedScrollConnection
+
+/**
+ * A [NestedScrollConnection] that listens for all vertical scroll events and responds in the
+ * following way:
+ * - If you **scroll up**, it **first brings the [scrimOffset]** back to the [minScrimOffset] and
+ *   then allows scrolling of the children (usually the content).
+ * - If you **scroll down**, it **first allows scrolling of the children** (usually the content) and
+ *   then resets the [scrimOffset] to [maxScrimOffset].
+ */
+fun NotificationScrimNestedScrollConnection(
+    scrimOffset: () -> Float,
+    onScrimOffsetChanged: (Float) -> Unit,
+    minScrimOffset: () -> Float,
+    maxScrimOffset: Float,
+    contentHeight: () -> Float,
+    minVisibleScrimHeight: () -> Float,
+): PriorityNestedScrollConnection {
+    return PriorityNestedScrollConnection(
+        orientation = Orientation.Vertical,
+        // scrolling up and inner content is taller than the scrim, so scrim needs to
+        // expand; content can scroll once scrim is at the minScrimOffset.
+        canStartPreScroll = { offsetAvailable, offsetBeforeStart ->
+            offsetAvailable < 0 &&
+                offsetBeforeStart == 0f &&
+                contentHeight() > minVisibleScrimHeight() &&
+                scrimOffset() > minScrimOffset()
+        },
+        // scrolling down and content is done scrolling to top. After that, the scrim
+        // needs to collapse; collapse the scrim until it is at the maxScrimOffset.
+        canStartPostScroll = { offsetAvailable, _ ->
+            offsetAvailable > 0 && scrimOffset() < maxScrimOffset
+        },
+        canStartPostFling = { false },
+        canContinueScroll = {
+            val currentHeight = scrimOffset()
+            minScrimOffset() < currentHeight && currentHeight < maxScrimOffset
+        },
+        canScrollOnFling = true,
+        onStart = { /* do nothing */},
+        onScroll = { offsetAvailable ->
+            val currentHeight = scrimOffset()
+            val amountConsumed =
+                if (offsetAvailable > 0) {
+                    val amountLeft = maxScrimOffset - currentHeight
+                    offsetAvailable.coerceAtMost(amountLeft)
+                } else {
+                    val amountLeft = minScrimOffset() - currentHeight
+                    offsetAvailable.coerceAtLeast(amountLeft)
+                }
+            onScrimOffsetChanged(currentHeight + amountConsumed)
+            amountConsumed
+        },
+        // Don't consume the velocity on pre/post fling
+        onStop = { 0f },
+    )
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
index e835d3e..0e08a19 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
@@ -20,32 +20,53 @@
 import android.util.Log
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.displayCutout
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.systemBars
+import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.snapshotFlow
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.graphics.BlendMode
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.layout.LayoutCoordinates
 import androidx.compose.ui.layout.boundsInWindow
 import androidx.compose.ui.layout.onPlaced
 import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.layout.positionInWindow
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.dp
 import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.NestedScrollBehavior
 import com.android.compose.animation.scene.SceneScope
 import com.android.compose.modifiers.height
 import com.android.systemui.notifications.ui.composable.Notifications.Form
+import com.android.systemui.scene.ui.composable.Gone
+import com.android.systemui.scene.ui.composable.Shade
+import com.android.systemui.shade.ui.composable.ShadeHeader
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
 import kotlin.math.roundToInt
 
@@ -100,33 +121,109 @@
 @Composable
 fun SceneScope.NotificationScrollingStack(
     viewModel: NotificationsPlaceholderViewModel,
+    maxScrimTop: () -> Float,
     modifier: Modifier = Modifier,
 ) {
+    val density = LocalDensity.current
     val cornerRadius by viewModel.cornerRadiusDp.collectAsState()
-
-    val contentHeight by viewModel.intrinsicContentHeight.collectAsState()
-
     val expansionFraction by viewModel.expandFraction.collectAsState(0f)
 
-    Box(
-        modifier =
-            modifier
-                .verticalNestedScrollToScene()
-                .fillMaxWidth()
-                .element(Notifications.Elements.NotificationScrim)
-                .graphicsLayer {
-                    shape = RoundedCornerShape(cornerRadius.dp)
-                    clip = true
-                    alpha = expansionFraction
-                }
-                .background(MaterialTheme.colorScheme.surface)
-                .debugBackground(viewModel, Color(0.5f, 0.5f, 0f, 0.2f))
-    ) {
-        NotificationPlaceholder(
-            viewModel = viewModel,
-            form = Form.Stack,
-            modifier = Modifier.fillMaxWidth().height { contentHeight.roundToInt() }
+    val navBarHeight =
+        with(density) { WindowInsets.systemBars.asPaddingValues().calculateBottomPadding().toPx() }
+    val statusBarHeight =
+        with(density) { WindowInsets.systemBars.asPaddingValues().calculateTopPadding().toPx() }
+    val displayCutoutHeight =
+        with(density) { WindowInsets.displayCutout.asPaddingValues().calculateTopPadding().toPx() }
+    val screenHeight =
+        with(density) { LocalConfiguration.current.screenHeightDp.dp.toPx() } +
+            navBarHeight +
+            maxOf(statusBarHeight, displayCutoutHeight)
+
+    val contentHeight = viewModel.intrinsicContentHeight.collectAsState()
+
+    // the offset for the notifications scrim. Its upper bound is 0, and its lower bound is
+    // calculated in minScrimOffset. The scrim is the same height as the screen minus the
+    // height of the Shade Header, and at rest (scrimOffset = 0) its top bound is at maxScrimStartY.
+    // When fully expanded (scrimOffset = minScrimOffset), its top bound is at minScrimStartY,
+    // which is equal to the height of the Shade Header. Thus, when the scrim is fully expanded, the
+    // entire height of the scrim is visible on screen.
+    val scrimOffset = remember { mutableStateOf(0f) }
+
+    val minScrimTop = with(density) { ShadeHeader.Dimensions.CollapsedHeight.toPx() }
+
+    // The minimum offset for the scrim. The scrim is considered fully expanded when it
+    // is at this offset.
+    val minScrimOffset: () -> Float = { minScrimTop - maxScrimTop() }
+
+    // The height of the scrim visible on screen when it is in its resting (collapsed) state.
+    val minVisibleScrimHeight: () -> Float = { screenHeight - maxScrimTop() }
+
+    // we are not scrolled to the top unless the scrim is at its maximum offset.
+    LaunchedEffect(viewModel, scrimOffset) {
+        snapshotFlow { scrimOffset.value >= 0f }
+            .collect { isScrolledToTop -> viewModel.setScrolledToTop(isScrolledToTop) }
+    }
+
+    // if contentHeight drops below minimum visible scrim height while scrim is
+    // expanded, reset scrim offset.
+    LaunchedEffect(contentHeight, screenHeight, maxScrimTop, scrimOffset) {
+        snapshotFlow { contentHeight.value < minVisibleScrimHeight() && scrimOffset.value < 0f }
+            .collect { shouldCollapse -> if (shouldCollapse) scrimOffset.value = 0f }
+    }
+
+    Box(modifier = modifier.element(Notifications.Elements.NotificationScrim)) {
+        Spacer(
+            modifier =
+                Modifier.fillMaxSize()
+                    .graphicsLayer {
+                        shape = RoundedCornerShape(cornerRadius.dp)
+                        clip = true
+                    }
+                    .drawBehind { drawRect(Color.Black, blendMode = BlendMode.DstOut) }
         )
+        Box(
+            modifier =
+                Modifier.fillMaxSize()
+                    .offset { IntOffset(0, scrimOffset.value.roundToInt()) }
+                    .graphicsLayer {
+                        shape = RoundedCornerShape(cornerRadius.dp)
+                        clip = true
+                        alpha =
+                            if (layoutState.isTransitioningBetween(Gone, Shade)) {
+                                (expansionFraction / 0.3f).coerceAtMost(1f)
+                            } else 1f
+                    }
+                    .background(MaterialTheme.colorScheme.surface)
+                    .debugBackground(viewModel, Color(0.5f, 0.5f, 0f, 0.2f))
+        ) {
+            NotificationPlaceholder(
+                viewModel = viewModel,
+                form = Form.Stack,
+                modifier =
+                    Modifier.verticalNestedScrollToScene(
+                            topBehavior = NestedScrollBehavior.EdgeWithPreview,
+                        )
+                        .nestedScroll(
+                            remember(
+                                scrimOffset,
+                                maxScrimTop,
+                                minScrimTop,
+                            ) {
+                                NotificationScrimNestedScrollConnection(
+                                    scrimOffset = { scrimOffset.value },
+                                    onScrimOffsetChanged = { scrimOffset.value = it },
+                                    minScrimOffset = minScrimOffset,
+                                    maxScrimOffset = 0f,
+                                    contentHeight = { contentHeight.value },
+                                    minVisibleScrimHeight = minVisibleScrimHeight,
+                                )
+                            }
+                        )
+                        .verticalScroll(rememberScrollState())
+                        .fillMaxWidth()
+                        .height { (contentHeight.value + navBarHeight).roundToInt() },
+            )
+        }
     }
 }
 
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 bbfe0fd..5531f9c 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
@@ -36,6 +36,7 @@
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.wrapContentHeight
 import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
@@ -46,6 +47,11 @@
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.boundsInWindow
+import androidx.compose.ui.layout.onPlaced
+import androidx.compose.ui.layout.positionInWindow
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.unit.dp
@@ -56,7 +62,7 @@
 import com.android.systemui.compose.modifiers.sysuiResTag
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.notifications.ui.composable.HeadsUpNotificationSpace
+import com.android.systemui.notifications.ui.composable.Notifications
 import com.android.systemui.qs.footer.ui.compose.FooterActions
 import com.android.systemui.qs.ui.viewmodel.QuickSettingsSceneViewModel
 import com.android.systemui.scene.shared.model.SceneKey
@@ -116,6 +122,8 @@
     statusBarIconController: StatusBarIconController,
     modifier: Modifier = Modifier,
 ) {
+    val cornerRadius by viewModel.notifications.cornerRadiusDp.collectAsState()
+
     // TODO(b/280887232): implement the real UI.
     Box(modifier = modifier.fillMaxSize()) {
         val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsState()
@@ -234,10 +242,32 @@
                 }
             }
         }
-        HeadsUpNotificationSpace(
-            viewModel = viewModel.notifications,
-            isPeekFromBottom = true,
-            modifier = Modifier.padding(16.dp).fillMaxSize(),
+        // Scrim with height 0 aligned to bottom of the screen to facilitate shared element
+        // transition from Shade scene.
+        Box(
+            modifier =
+                Modifier.element(Notifications.Elements.NotificationScrim)
+                    .fillMaxWidth()
+                    .height(0.dp)
+                    .graphicsLayer {
+                        shape = RoundedCornerShape(cornerRadius.dp)
+                        clip = true
+                        alpha = 1f
+                    }
+                    .background(MaterialTheme.colorScheme.surface)
+                    .align(Alignment.BottomCenter)
+                    .onPlaced { coordinates: LayoutCoordinates ->
+                        viewModel.notifications.onContentTopChanged(
+                            coordinates.positionInWindow().y
+                        )
+                        val boundsInWindow = coordinates.boundsInWindow()
+                        viewModel.notifications.onBoundsChanged(
+                            left = boundsInWindow.left,
+                            top = boundsInWindow.top,
+                            right = boundsInWindow.right,
+                            bottom = boundsInWindow.bottom,
+                        )
+                    }
         )
     }
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt
index 747faab..770d654 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt
@@ -18,13 +18,10 @@
 
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
 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.systemui.dagger.SysUISingleton
-import com.android.systemui.notifications.ui.composable.HeadsUpNotificationSpace
 import com.android.systemui.notifications.ui.composable.Notifications
 import com.android.systemui.scene.shared.model.Direction
 import com.android.systemui.scene.shared.model.Edge
@@ -66,12 +63,6 @@
     override fun SceneScope.Content(
         modifier: Modifier,
     ) {
-        Box(modifier = modifier) {
-            Box(modifier = Modifier.fillMaxSize().element(Notifications.Elements.NotificationScrim))
-            HeadsUpNotificationSpace(
-                viewModel = notificationsViewModel,
-                modifier = Modifier.padding(16.dp).fillMaxSize(),
-            )
-        }
+        Box(modifier = Modifier.fillMaxSize().element(Notifications.Elements.NotificationScrim))
     }
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/TransitionSceneKeys.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/TransitionSceneKeys.kt
index 5336bf6..0c66701 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/TransitionSceneKeys.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/TransitionSceneKeys.kt
@@ -12,5 +12,5 @@
 
 // TODO(b/293899074): Remove this file once we can use the scene keys from SceneTransitionLayout.
 fun SceneKey.toTransitionSceneKey(): SceneTransitionSceneKey {
-    return SceneTransitionSceneKey(name = toString(), identity = this)
+    return SceneTransitionSceneKey(debugName = toString(), identity = this)
 }
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 1545372..497fe87 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
@@ -32,6 +32,7 @@
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.layout.layout
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.res.dimensionResource
@@ -62,6 +63,7 @@
 import com.android.systemui.util.animation.MeasurementInput
 import javax.inject.Inject
 import javax.inject.Named
+import kotlin.math.roundToInt
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
@@ -154,62 +156,104 @@
     mediaHost: MediaHost,
     modifier: Modifier = Modifier,
 ) {
-    val localDensity = LocalDensity.current
+    val density = LocalDensity.current
     val layoutWidth = remember { mutableStateOf(0) }
+    val maxNotifScrimTop = remember { mutableStateOf(0f) }
 
     Box(
         modifier =
             modifier.element(Shade.Elements.Scrim).background(MaterialTheme.colorScheme.scrim),
     )
     Box {
-        Column(
-            horizontalAlignment = Alignment.CenterHorizontally,
-            modifier = Modifier.fillMaxWidth().clickable(onClick = { viewModel.onContentClicked() })
-        ) {
-            CollapsedShadeHeader(
-                viewModel = viewModel.shadeHeaderViewModel,
-                createTintedIconManager = createTintedIconManager,
-                createBatteryMeterViewController = createBatteryMeterViewController,
-                statusBarIconController = statusBarIconController,
-                modifier = Modifier.padding(horizontal = Shade.Dimensions.HorizontalPadding)
-            )
-            QuickSettings(
-                modifier = Modifier.height(130.dp),
-                viewModel.qsSceneAdapter,
-            )
-
-            if (viewModel.isMediaVisible()) {
-                val mediaHeight = dimensionResource(R.dimen.qs_media_session_height_expanded)
-                MediaCarousel(
-                    modifier =
-                        Modifier.height(mediaHeight).fillMaxWidth().layout { measurable, constraints
-                            ->
-                            val placeable = measurable.measure(constraints)
-
-                            // Notify controller to size the carousel for the current space
-                            mediaHost.measurementInput =
-                                MeasurementInput(placeable.width, placeable.height)
-                            mediaCarouselController.setSceneContainerSize(
-                                placeable.width,
-                                placeable.height
+        Layout(
+            contents =
+                listOf(
+                    {
+                        Column(
+                            horizontalAlignment = Alignment.CenterHorizontally,
+                            modifier =
+                                Modifier.fillMaxWidth()
+                                    .clickable(onClick = { viewModel.onContentClicked() })
+                        ) {
+                            CollapsedShadeHeader(
+                                viewModel = viewModel.shadeHeaderViewModel,
+                                createTintedIconManager = createTintedIconManager,
+                                createBatteryMeterViewController = createBatteryMeterViewController,
+                                statusBarIconController = statusBarIconController,
+                                modifier =
+                                    Modifier.padding(
+                                        horizontal = Shade.Dimensions.HorizontalPadding
+                                    )
+                            )
+                            QuickSettings(
+                                modifier = Modifier.height(130.dp),
+                                viewModel.qsSceneAdapter,
                             )
 
-                            layout(placeable.width, placeable.height) {
-                                placeable.placeRelative(0, 0)
-                            }
-                        },
-                    mediaHost = mediaHost,
-                    layoutWidth = layoutWidth.value,
-                    layoutHeight = with(localDensity) { mediaHeight.toPx() }.toInt(),
-                    carouselController = mediaCarouselController,
-                )
-            }
+                            if (viewModel.isMediaVisible()) {
+                                val mediaHeight =
+                                    dimensionResource(R.dimen.qs_media_session_height_expanded)
+                                MediaCarousel(
+                                    modifier =
+                                        Modifier.height(mediaHeight).fillMaxWidth().layout {
+                                            measurable,
+                                            constraints ->
+                                            val placeable = measurable.measure(constraints)
 
-            Spacer(modifier = Modifier.height(16.dp))
-            NotificationScrollingStack(
-                viewModel = viewModel.notifications,
-                modifier = Modifier.fillMaxWidth().weight(1f),
-            )
+                                            // Notify controller to size the carousel for the
+                                            // current space
+                                            mediaHost.measurementInput =
+                                                MeasurementInput(placeable.width, placeable.height)
+                                            mediaCarouselController.setSceneContainerSize(
+                                                placeable.width,
+                                                placeable.height
+                                            )
+
+                                            layout(placeable.width, placeable.height) {
+                                                placeable.placeRelative(0, 0)
+                                            }
+                                        },
+                                    mediaHost = mediaHost,
+                                    layoutWidth = layoutWidth.value,
+                                    layoutHeight = with(density) { mediaHeight.toPx() }.toInt(),
+                                    carouselController = mediaCarouselController,
+                                )
+                            }
+
+                            Spacer(modifier = Modifier.height(16.dp))
+                        }
+                    },
+                    {
+                        NotificationScrollingStack(
+                            viewModel = viewModel.notifications,
+                            maxScrimTop = { maxNotifScrimTop.value },
+                        )
+                    },
+                )
+        ) { measurables, constraints ->
+            check(measurables.size == 2)
+            check(measurables[0].size == 1)
+            check(measurables[1].size == 1)
+
+            val quickSettingsPlaceable = measurables[0][0].measure(constraints)
+
+            val notificationsMeasurable = measurables[1][0]
+            val notificationsScrimMaxHeight =
+                constraints.maxHeight - ShadeHeader.Dimensions.CollapsedHeight.roundToPx()
+            val notificationsPlaceable =
+                notificationsMeasurable.measure(
+                    constraints.copy(
+                        minHeight = notificationsScrimMaxHeight,
+                        maxHeight = notificationsScrimMaxHeight
+                    )
+                )
+
+            maxNotifScrimTop.value = quickSettingsPlaceable.height.toFloat()
+
+            layout(constraints.maxWidth, constraints.maxHeight) {
+                quickSettingsPlaceable.placeRelative(x = 0, y = 0)
+                notificationsPlaceable.placeRelative(x = 0, y = maxNotifScrimTop.value.roundToInt())
+            }
         }
     }
 }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt
index 7d3b0fb..7057545 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt
@@ -61,7 +61,7 @@
                     // The transition is already finished (progress ~= 1): no need to animate. We
                     // finish the current transition early to make sure that the current state
                     // change is committed.
-                    layoutState.finishTransition(transitionState, transitionState.currentScene)
+                    layoutState.finishTransition(transitionState, target)
                     null
                 } else {
                     // The transition is in progress: start the canned animation at the same
@@ -78,7 +78,7 @@
                 if (progress.absoluteValue < ProgressVisibilityThreshold) {
                     // The transition is at progress ~= 0: no need to animate.We finish the current
                     // transition early to make sure that the current state change is committed.
-                    layoutState.finishTransition(transitionState, transitionState.currentScene)
+                    layoutState.finishTransition(transitionState, target)
                     null
                 } else {
                     // TODO(b/290184746): Also take the current velocity into account.
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
index 9d4b69c..c6851954 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
@@ -42,16 +42,16 @@
 
 /** Key for a scene. */
 class SceneKey(
-    name: String,
+    debugName: String,
     identity: Any = Object(),
-) : Key(name, identity), UserActionResult {
+) : Key(debugName, identity), UserActionResult {
     @VisibleForTesting
     // TODO(b/240432457): Make internal once PlatformComposeSceneTransitionLayoutTestsUtils can
     // access internal members.
-    val testTag: String = "scene:$name"
+    val testTag: String = "scene:$debugName"
 
     /** The unique [ElementKey] identifying this scene's root element. */
-    val rootElementKey = ElementKey(name, identity)
+    val rootElementKey = ElementKey(debugName, identity)
 
     // Implementation of [UserActionResult].
     override val toScene: SceneKey = this
@@ -64,7 +64,7 @@
 
 /** Key for an element. */
 class ElementKey(
-    name: String,
+    debugName: String,
     identity: Any = Object(),
 
     /**
@@ -72,11 +72,11 @@
      * or compose MovableElements.
      */
     val scenePicker: ElementScenePicker = DefaultElementScenePicker,
-) : Key(name, identity), ElementMatcher {
+) : Key(debugName, identity), ElementMatcher {
     @VisibleForTesting
     // TODO(b/240432457): Make internal once PlatformComposeSceneTransitionLayoutTestsUtils can
     // access internal members.
-    val testTag: String = "element:$name"
+    val testTag: String = "element:$debugName"
 
     override fun matches(key: ElementKey, scene: SceneKey): Boolean {
         return key == this
@@ -99,7 +99,7 @@
 }
 
 /** Key for a shared value of an element. */
-class ValueKey(name: String, identity: Any = Object()) : Key(name, identity) {
+class ValueKey(debugName: String, identity: Any = Object()) : Key(debugName, identity) {
     override fun toString(): String {
         return "ValueKey(debugName=$debugName)"
     }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
index 5f615fd..529fc03 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
@@ -263,10 +263,10 @@
                     val deltaOffset = drag.position - initialDown.position
                     val delta =
                         when (orientation) {
-                            Orientation.Horizontal -> deltaOffset.y
+                            Orientation.Horizontal -> deltaOffset.x
                             Orientation.Vertical -> deltaOffset.y
                         }
-                    check(delta != 0f)
+                    check(delta != 0f) { "delta is equal to 0" }
                     overSlop = delta.sign
                 }
 
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
index 58c3be24..35754d6 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
@@ -146,9 +146,10 @@
         val fromScene = layoutImpl.scene(transitionState.currentScene)
         updateSwipes(fromScene, startedPosition, pointersDown)
 
-        val (targetScene, distance) =
-            findTargetSceneAndDistance(fromScene, overSlop, updateSwipesResults = true) ?: return
-        updateTransition(SwipeTransition(fromScene, targetScene, distance), force = true)
+        val result =
+            findUserActionResult(fromScene, directionOffset = overSlop, updateSwipesResults = true)
+                ?: return
+        updateTransition(SwipeTransition(fromScene, result), force = true)
     }
 
     private fun updateSwipes(fromScene: Scene, startedPosition: Offset?, pointersDown: Int) {
@@ -224,8 +225,8 @@
             computeFromSceneConsideringAcceleratedSwipe(swipeTransition)
 
         val isNewFromScene = fromScene.key != swipeTransition.fromScene
-        val (targetScene, distance) =
-            findTargetSceneAndDistance(
+        val result =
+            findUserActionResult(
                 fromScene,
                 swipeTransition.dragOffset,
                 updateSwipesResults = isNewFromScene,
@@ -236,9 +237,9 @@
                 }
         swipeTransition.dragOffset += acceleratedOffset
 
-        if (isNewFromScene || targetScene.key != swipeTransition.toScene) {
+        if (isNewFromScene || result.toScene != swipeTransition.toScene) {
             updateTransition(
-                SwipeTransition(fromScene, targetScene, distance).apply {
+                SwipeTransition(fromScene, result).apply {
                     this.dragOffset = swipeTransition.dragOffset
                 }
             )
@@ -306,7 +307,7 @@
     }
 
     /**
-     * Returns the target scene and distance from [fromScene] in the direction [directionOffset].
+     * Returns the [UserActionResult] from [fromScene] in the direction of [directionOffset].
      *
      * @param fromScene the scene from which we look for the target
      * @param directionOffset signed float that indicates the direction. Positive is down or right
@@ -322,60 +323,45 @@
      *   [directionOffset] is 0f and both direction are available, it will default to
      *   [upOrLeftResult].
      */
-    private inline fun findTargetSceneAndDistance(
+    private fun findUserActionResult(
         fromScene: Scene,
         directionOffset: Float,
         updateSwipesResults: Boolean,
-    ): Pair<Scene, Float>? {
+    ): UserActionResult? {
         if (updateSwipesResults) updateSwipesResults(fromScene)
 
-        // Compute the target scene depending on the current offset.
         return when {
             upOrLeftResult == null && downOrRightResult == null -> null
             (directionOffset < 0f && upOrLeftResult != null) || downOrRightResult == null ->
-                upOrLeftResult?.let { result ->
-                    Pair(
-                        layoutImpl.scene(result.toScene),
-                        -fromScene.getAbsoluteDistance(result.distance)
-                    )
-                }
-            else ->
-                downOrRightResult?.let { result ->
-                    Pair(
-                        layoutImpl.scene(result.toScene),
-                        fromScene.getAbsoluteDistance(result.distance)
-                    )
-                }
+                upOrLeftResult
+            else -> downOrRightResult
         }
     }
 
     /**
-     * A strict version of [findTargetSceneAndDistance] that will return null when there is no Scene
-     * in [directionOffset] direction
+     * A strict version of [findUserActionResult] that will return null when there is no Scene in
+     * [directionOffset] direction
      */
-    private inline fun findTargetSceneAndDistanceStrict(
-        fromScene: Scene,
-        directionOffset: Float,
-    ): Pair<Scene, Float>? {
+    private fun findUserActionResultStrict(directionOffset: Float): UserActionResult? {
         return when {
-            directionOffset > 0f ->
-                upOrLeftResult?.let { result ->
-                    Pair(
-                        layoutImpl.scene(result.toScene),
-                        -fromScene.getAbsoluteDistance(result.distance),
-                    )
-                }
-            directionOffset < 0f ->
-                downOrRightResult?.let { result ->
-                    Pair(
-                        layoutImpl.scene(result.toScene),
-                        fromScene.getAbsoluteDistance(result.distance),
-                    )
-                }
+            directionOffset > 0f -> upOrLeftResult
+            directionOffset < 0f -> downOrRightResult
             else -> null
         }
     }
 
+    private fun computeAbsoluteDistance(
+        fromScene: Scene,
+        result: UserActionResult,
+    ): Float {
+        return if (result == upOrLeftResult) {
+            -fromScene.getAbsoluteDistance(result.distance)
+        } else {
+            check(result == downOrRightResult)
+            fromScene.getAbsoluteDistance(result.distance)
+        }
+    }
+
     internal fun onDragStopped(velocity: Float, canChangeScene: Boolean) {
         // The state was changed since the drag started; don't do anything.
         if (!isDrivingTransition) {
@@ -440,8 +426,8 @@
 
             if (startFromIdlePosition) {
                 // If there is a target scene, we start the overscroll animation.
-                val (targetScene, distance) =
-                    findTargetSceneAndDistanceStrict(fromScene, velocity)
+                val result =
+                    findUserActionResultStrict(velocity)
                         ?: run {
                             // We will not animate
                             layoutState.finishTransition(swipeTransition, idleScene = fromScene.key)
@@ -449,7 +435,7 @@
                         }
 
                 updateTransition(
-                    SwipeTransition(fromScene, targetScene, distance).apply {
+                    SwipeTransition(fromScene, result).apply {
                         _currentScene = swipeTransition._currentScene
                     }
                 )
@@ -496,6 +482,14 @@
         }
     }
 
+    private fun SwipeTransition(fromScene: Scene, result: UserActionResult): SwipeTransition {
+        return SwipeTransition(
+            fromScene,
+            layoutImpl.scene(result.toScene),
+            computeAbsoluteDistance(fromScene, result),
+        )
+    }
+
     internal class SwipeTransition(
         val _fromScene: Scene,
         val _toScene: Scene,
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
index 48825fb..9a25d43 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
@@ -91,6 +91,16 @@
     }
 
     @Test
+    fun setTargetScene_transitionToOriginalScene() = runMonotonicClockTest {
+        val state = MutableSceneTransitionLayoutState(TestScenes.SceneA)
+        assertThat(state.setTargetScene(TestScenes.SceneB, coroutineScope = this)).isNotNull()
+
+        // Progress is 0f, so we don't animate at all and directly snap back to A.
+        assertThat(state.setTargetScene(TestScenes.SceneA, coroutineScope = this)).isNull()
+        assertThat(state.transitionState).isEqualTo(TransitionState.Idle(TestScenes.SceneA))
+    }
+
+    @Test
     fun setTargetScene_coroutineScopeCancelled() = runMonotonicClockTest {
         val state = MutableSceneTransitionLayoutState(TestScenes.SceneA)
 
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
index d81631a..af1a5b8 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
@@ -442,6 +442,23 @@
         transition = layoutState.currentTransition
         assertThat(transition).isNotNull()
         assertThat(transition?.toScene).isEqualTo(TestScenes.SceneB)
+
+        // Release the finger, animating back to scene A.
+        rule.onRoot().performTouchInput { up() }
+        rule.waitForIdle()
+        assertThat(layoutState.currentTransition).isNull()
+        assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+
+        // Swipe left by exactly touchSlop, so that the drag overSlop is 0f.
+        rule.onRoot().performTouchInput {
+            down(middle)
+            moveBy(Offset(-touchSlop, 0f), delayMillis = 1_000)
+        }
+
+        // We should still correctly compute that we are swiping down to scene B.
+        transition = layoutState.currentTransition
+        assertThat(transition).isNotNull()
+        assertThat(transition?.toScene).isEqualTo(TestScenes.SceneB)
     }
 
     @Test
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderClient.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderClient.kt
index e050604..34c4dfb 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderClient.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderClient.kt
@@ -33,6 +33,7 @@
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.withContext
@@ -518,6 +519,7 @@
                 awaitClose { context.contentResolver.unregisterContentObserver(observer) }
             }
             .onStart { emit(Unit) }
+            .flowOn(backgroundDispatcher)
     }
 
     private fun String.toIntent(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/SysuiTestCaseSelfTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/SysuiTestCaseSelfTest.kt
index be6bb9c..107293e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/SysuiTestCaseSelfTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/SysuiTestCaseSelfTest.kt
@@ -24,6 +24,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class SysuiTestCaseSelfTest : SysuiTestCase() {
     private val contextBeforeSetup = context
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepositoryImplTest.kt
index 9287edf..c2f0c6f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepositoryImplTest.kt
@@ -31,6 +31,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class AccessibilityQsShortcutsRepositoryImplTest : SysuiTestCase() {
     private val testDispatcher = StandardTestDispatcher()
     private val testScope = TestScope(testDispatcher)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt
index c86c747..b6605ed 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt
@@ -37,6 +37,7 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class ColorCorrectionRepositoryImplTest : SysuiTestCase() {
 
     private val testUser1 = UserHandle.of(1)!!
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt
index 4853529..30eb782 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt
@@ -37,6 +37,7 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class ColorInversionRepositoryImplTest : SysuiTestCase() {
 
     private val testUser1 = UserHandle.of(1)!!
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepositoryTest.kt
index ce22e28..ed3b4c0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepositoryTest.kt
@@ -31,6 +31,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class UserA11yQsShortcutsRepositoryTest : SysuiTestCase() {
     private val secureSettings = FakeSettings()
     private val testDispatcher = StandardTestDispatcher()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/FaceHelpMessageDeferralTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/FaceHelpMessageDeferralTest.kt
index 39f0d57..0596205 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/FaceHelpMessageDeferralTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/FaceHelpMessageDeferralTest.kt
@@ -34,6 +34,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class FaceHelpMessageDeferralTest : SysuiTestCase() {
     val threshold = .75f
     @Mock lateinit var logger: BiometricMessageDeferralLogger
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepositoryImplTest.kt
index a67b093..d317aeb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepositoryImplTest.kt
@@ -37,6 +37,7 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class EmergencyServicesRepositoryImplTest : SysuiTestCase() {
 
     private val kosmos = testKosmos()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerCallbackInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerCallbackInteractorTest.kt
index 4aea4f3..db83b3b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerCallbackInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerCallbackInteractorTest.kt
@@ -29,6 +29,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class PrimaryBouncerCallbackInteractorTest : SysuiTestCase() {
     private val mPrimaryBouncerCallbackInteractor = PrimaryBouncerCallbackInteractor()
     @Mock
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
index c193d14..fbb5415 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.bouncer.ui.viewmodel
 
+import android.content.pm.UserInfo
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -26,18 +27,27 @@
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
+import com.android.systemui.inputmethod.data.model.InputMethodModel
+import com.android.systemui.inputmethod.data.repository.fakeInputMethodRepository
+import com.android.systemui.inputmethod.domain.interactor.inputMethodInteractor
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.res.R
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.scene.shared.model.SceneModel
 import com.android.systemui.testKosmos
+import com.android.systemui.user.data.model.SelectedUserModel
+import com.android.systemui.user.data.model.SelectionStatus
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.user.domain.interactor.selectedUserInteractor
 import com.google.common.truth.Truth.assertThat
+import java.util.UUID
 import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -51,19 +61,22 @@
 
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
-    private val authenticationInteractor = kosmos.authenticationInteractor
+    private val authenticationInteractor by lazy { kosmos.authenticationInteractor }
     private val sceneInteractor by lazy { kosmos.sceneInteractor }
     private val bouncerInteractor by lazy { kosmos.bouncerInteractor }
+    private val selectedUserInteractor by lazy { kosmos.selectedUserInteractor }
+    private val inputMethodInteractor by lazy { kosmos.inputMethodInteractor }
     private val bouncerViewModel by lazy { kosmos.bouncerViewModel }
     private val isInputEnabled = MutableStateFlow(true)
 
-    private val underTest by lazy {
+    private val underTest =
         PasswordBouncerViewModel(
             viewModelScope = testScope.backgroundScope,
+            isInputEnabled = isInputEnabled.asStateFlow(),
             interactor = bouncerInteractor,
-            isInputEnabled.asStateFlow(),
+            inputMethodInteractor = inputMethodInteractor,
+            selectedUserInteractor = selectedUserInteractor,
         )
-    }
 
     @Before
     fun setUp() {
@@ -270,6 +283,52 @@
             assertThat(isTextFieldFocusRequested).isTrue()
         }
 
+    @Test
+    fun isImeSwitcherButtonVisible() =
+        testScope.runTest {
+            val selectedUserId by collectLastValue(selectedUserInteractor.selectedUser)
+            selectUser(USER_INFOS.first())
+
+            enableInputMethodsForUser(checkNotNull(selectedUserId))
+
+            // Assert initial value, before the UI subscribes.
+            assertThat(underTest.isImeSwitcherButtonVisible.value).isFalse()
+
+            // Subscription starts; verify a fresh value is fetched.
+            val isImeSwitcherButtonVisible by collectLastValue(underTest.isImeSwitcherButtonVisible)
+            assertThat(isImeSwitcherButtonVisible).isTrue()
+
+            // Change the user, verify a fresh value is fetched.
+            selectUser(USER_INFOS.last())
+
+            assertThat(
+                    inputMethodInteractor.hasMultipleEnabledImesOrSubtypes(
+                        checkNotNull(selectedUserId)
+                    )
+                )
+                .isFalse()
+            assertThat(isImeSwitcherButtonVisible).isFalse()
+
+            // Enable IMEs and add another subscriber; verify a fresh value is fetched.
+            enableInputMethodsForUser(checkNotNull(selectedUserId))
+            val collector2 by collectLastValue(underTest.isImeSwitcherButtonVisible)
+            assertThat(collector2).isTrue()
+        }
+
+    @Test
+    fun onImeSwitcherButtonClicked() =
+        testScope.runTest {
+            val displayId = 7
+            assertThat(kosmos.fakeInputMethodRepository.inputMethodPickerShownDisplayId)
+                .isNotEqualTo(displayId)
+
+            underTest.onImeSwitcherButtonClicked(displayId)
+            runCurrent()
+
+            assertThat(kosmos.fakeInputMethodRepository.inputMethodPickerShownDisplayId)
+                .isEqualTo(displayId)
+        }
+
     private fun TestScope.switchToScene(toScene: SceneKey) {
         val currentScene by collectLastValue(sceneInteractor.desiredScene)
         val bouncerShown = currentScene?.key != SceneKey.Bouncer && toScene == SceneKey.Bouncer
@@ -310,8 +369,45 @@
         runCurrent()
     }
 
+    private fun TestScope.selectUser(userInfo: UserInfo) {
+        kosmos.fakeUserRepository.selectedUser.value =
+            SelectedUserModel(
+                userInfo = userInfo,
+                selectionStatus = SelectionStatus.SELECTION_COMPLETE
+            )
+        advanceTimeBy(PasswordBouncerViewModel.DELAY_TO_FETCH_IMES)
+    }
+
+    private suspend fun enableInputMethodsForUser(userId: Int) {
+        kosmos.fakeInputMethodRepository.setEnabledInputMethods(
+            userId,
+            createInputMethodWithSubtypes(auxiliarySubtypes = 0, nonAuxiliarySubtypes = 0),
+            createInputMethodWithSubtypes(auxiliarySubtypes = 0, nonAuxiliarySubtypes = 1),
+        )
+        assertThat(inputMethodInteractor.hasMultipleEnabledImesOrSubtypes(userId)).isTrue()
+    }
+
+    private fun createInputMethodWithSubtypes(
+        auxiliarySubtypes: Int,
+        nonAuxiliarySubtypes: Int,
+    ): InputMethodModel {
+        return InputMethodModel(
+            imeId = UUID.randomUUID().toString(),
+            subtypes =
+                List(auxiliarySubtypes + nonAuxiliarySubtypes) {
+                    InputMethodModel.Subtype(subtypeId = it, isAuxiliary = it < auxiliarySubtypes)
+                }
+        )
+    }
+
     companion object {
         private const val ENTER_YOUR_PASSWORD = "Enter your password"
         private const val WRONG_PASSWORD = "Wrong password"
+
+        private val USER_INFOS =
+            listOf(
+                UserInfo(100, "First user", 0),
+                UserInfo(101, "Second user", 0),
+            )
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt
index 55016bb..25a287c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt
@@ -24,6 +24,7 @@
  */
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class PinInputViewModelTest : SysuiTestCase() {
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
new file mode 100644
index 0000000..a8fe16b
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
@@ -0,0 +1,252 @@
+/*
+ * 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.communal
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.communal.shared.model.CommunalSceneKey
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.dock.DockManager
+import com.android.systemui.dock.dockManager
+import com.android.systemui.dock.fakeDockManager
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
+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.advanceTimeBy
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CommunalSceneStartableTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+
+    private lateinit var underTest: CommunalSceneStartable
+
+    @Before
+    fun setUp() =
+        with(kosmos) {
+            underTest =
+                CommunalSceneStartable(
+                        dockManager = dockManager,
+                        communalInteractor = communalInteractor,
+                        keyguardTransitionInteractor = keyguardTransitionInteractor,
+                        applicationScope = applicationCoroutineScope,
+                        bgScope = applicationCoroutineScope,
+                    )
+                    .apply { start() }
+        }
+
+    @Test
+    fun keyguardGoesAway_forceBlankScene() =
+        with(kosmos) {
+            testScope.runTest {
+                val scene by collectLastValue(communalInteractor.desiredScene)
+
+                communalInteractor.onSceneChanged(CommunalSceneKey.Communal)
+                assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+
+                fakeKeyguardTransitionRepository.sendTransitionSteps(
+                    from = KeyguardState.PRIMARY_BOUNCER,
+                    to = KeyguardState.GONE,
+                    testScope = this
+                )
+
+                assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+            }
+        }
+
+    @Test
+    fun deviceDreaming_forceBlankScene() =
+        with(kosmos) {
+            testScope.runTest {
+                val scene by collectLastValue(communalInteractor.desiredScene)
+
+                communalInteractor.onSceneChanged(CommunalSceneKey.Communal)
+                assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+
+                fakeKeyguardTransitionRepository.sendTransitionSteps(
+                    from = KeyguardState.GLANCEABLE_HUB,
+                    to = KeyguardState.DREAMING,
+                    testScope = this
+                )
+
+                assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+            }
+        }
+
+    @Test
+    fun deviceDocked_forceCommunalScene() =
+        with(kosmos) {
+            testScope.runTest {
+                val scene by collectLastValue(communalInteractor.desiredScene)
+                assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+
+                updateDocked(true)
+                fakeKeyguardTransitionRepository.sendTransitionSteps(
+                    from = KeyguardState.GONE,
+                    to = KeyguardState.LOCKSCREEN,
+                    testScope = this
+                )
+                assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+
+                fakeKeyguardTransitionRepository.sendTransitionSteps(
+                    from = KeyguardState.GLANCEABLE_HUB,
+                    to = KeyguardState.DREAMING,
+                    testScope = this
+                )
+                assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+            }
+        }
+
+    @Test
+    fun deviceDocked_doesNotForceCommunalIfTransitioningFromCommunal() =
+        with(kosmos) {
+            testScope.runTest {
+                val scene by collectLastValue(communalInteractor.desiredScene)
+                assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+
+                updateDocked(true)
+                fakeKeyguardTransitionRepository.sendTransitionSteps(
+                    from = KeyguardState.GLANCEABLE_HUB,
+                    to = KeyguardState.LOCKSCREEN,
+                    testScope = this
+                )
+                assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+            }
+        }
+
+    @Test
+    fun deviceAsleep_forceBlankSceneAfterTimeout() =
+        with(kosmos) {
+            testScope.runTest {
+                val scene by collectLastValue(communalInteractor.desiredScene)
+                communalInteractor.onSceneChanged(CommunalSceneKey.Communal)
+                assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+
+                fakeKeyguardTransitionRepository.sendTransitionSteps(
+                    from = KeyguardState.GLANCEABLE_HUB,
+                    to = KeyguardState.OFF,
+                    testScope = this
+                )
+                assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+
+                advanceTimeBy(CommunalSceneStartable.AWAKE_DEBOUNCE_DELAY)
+
+                assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+            }
+        }
+
+    @Test
+    fun deviceAsleep_wakesUpBeforeTimeout_noChangeInScene() =
+        with(kosmos) {
+            testScope.runTest {
+                val scene by collectLastValue(communalInteractor.desiredScene)
+                communalInteractor.onSceneChanged(CommunalSceneKey.Communal)
+                assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+
+                fakeKeyguardTransitionRepository.sendTransitionSteps(
+                    from = KeyguardState.GLANCEABLE_HUB,
+                    to = KeyguardState.OFF,
+                    testScope = this
+                )
+                assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+                advanceTimeBy(CommunalSceneStartable.AWAKE_DEBOUNCE_DELAY / 2)
+                assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+
+                fakeKeyguardTransitionRepository.sendTransitionSteps(
+                    from = KeyguardState.OFF,
+                    to = KeyguardState.GLANCEABLE_HUB,
+                    testScope = this
+                )
+
+                advanceTimeBy(CommunalSceneStartable.AWAKE_DEBOUNCE_DELAY)
+                assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+            }
+        }
+
+    @Test
+    fun dockingOnLockscreen_forcesCommunal() =
+        with(kosmos) {
+            testScope.runTest {
+                communalInteractor.onSceneChanged(CommunalSceneKey.Blank)
+                val scene by collectLastValue(communalInteractor.desiredScene)
+
+                // device is docked while on the lockscreen
+                fakeKeyguardTransitionRepository.sendTransitionSteps(
+                    from = KeyguardState.GLANCEABLE_HUB,
+                    to = KeyguardState.LOCKSCREEN,
+                    testScope = this
+                )
+                updateDocked(true)
+
+                assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+                advanceTimeBy(CommunalSceneStartable.DOCK_DEBOUNCE_DELAY)
+                assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+            }
+        }
+
+    @Test
+    fun dockingOnLockscreen_doesNotForceCommunalIfDreamStarts() =
+        with(kosmos) {
+            testScope.runTest {
+                communalInteractor.onSceneChanged(CommunalSceneKey.Blank)
+                val scene by collectLastValue(communalInteractor.desiredScene)
+
+                // device is docked while on the lockscreen
+                fakeKeyguardTransitionRepository.sendTransitionSteps(
+                    from = KeyguardState.GLANCEABLE_HUB,
+                    to = KeyguardState.LOCKSCREEN,
+                    testScope = this
+                )
+                updateDocked(true)
+
+                assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+                advanceTimeBy(CommunalSceneStartable.DOCK_DEBOUNCE_DELAY / 2)
+                assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+
+                // dream starts shortly after docking
+                fakeKeyguardTransitionRepository.sendTransitionSteps(
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.DREAMING,
+                    testScope = this
+                )
+                advanceTimeBy(CommunalSceneStartable.DOCK_DEBOUNCE_DELAY)
+                assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+            }
+        }
+
+    private fun TestScope.updateDocked(docked: Boolean) =
+        with(kosmos) {
+            runCurrent()
+            fakeDockManager.setIsDocked(docked)
+            fakeDockManager.setDockEvent(DockManager.STATE_DOCKED)
+            runCurrent()
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt
index 92b75cb..76b0d4a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt
@@ -40,6 +40,7 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class CommunalMediaRepositoryImplTest : SysuiTestCase() {
     @Mock private lateinit var mediaDataManager: MediaDataManager
     @Mock private lateinit var mediaData: MediaData
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt
index 820bfbf..d15e15e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt
@@ -39,6 +39,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class CommunalPrefsRepositoryImplTest : SysuiTestCase() {
     private lateinit var underTest: CommunalPrefsRepositoryImpl
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt
index c4a8582..0c66bbb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt
@@ -38,6 +38,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class CommunalTutorialRepositoryImplTest : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt
index 3aa99c4..89a4c04 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt
@@ -23,16 +23,27 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.communal.widgets.CommunalAppWidgetHost
 import com.android.systemui.communal.widgets.CommunalAppWidgetHostView
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.log.logcatLogBuffer
+import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.mock
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWithLooper(setAsMainLooper = true)
 @RunWith(AndroidJUnit4::class)
 class CommunalAppWidgetHostTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
 
     private lateinit var testableLooper: TestableLooper
     private lateinit var underTest: CommunalAppWidgetHost
@@ -43,9 +54,11 @@
         underTest =
             CommunalAppWidgetHost(
                 context = context,
+                backgroundScope = kosmos.applicationCoroutineScope,
                 hostId = 116,
                 interactionHandler = mock(),
-                looper = testableLooper.looper
+                looper = testableLooper.looper,
+                logBuffer = logcatLogBuffer("CommunalAppWidgetHostTest"),
             )
     }
 
@@ -64,4 +77,23 @@
         assertThat(view).isNotNull()
         assertThat(view.appWidgetId).isEqualTo(appWidgetId)
     }
+
+    @Test
+    fun appWidgetIdToRemove_emit() =
+        testScope.runTest {
+            val appWidgetIdToRemove by collectLastValue(underTest.appWidgetIdToRemove)
+
+            // Nothing should be emitted yet
+            assertThat(appWidgetIdToRemove).isNull()
+
+            underTest.onAppWidgetRemoved(appWidgetId = 1)
+            runCurrent()
+
+            assertThat(appWidgetIdToRemove).isEqualTo(1)
+
+            underTest.onAppWidgetRemoved(appWidgetId = 2)
+            runCurrent()
+
+            assertThat(appWidgetIdToRemove).isEqualTo(2)
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
index a3654b6..032d76f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
@@ -21,14 +21,21 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.communal.data.repository.fakeCommunalRepository
+import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository
 import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
+import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.testKosmos
 import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -47,6 +54,8 @@
 
     @Mock private lateinit var appWidgetHost: CommunalAppWidgetHost
 
+    private lateinit var appWidgetIdToRemove: MutableSharedFlow<Int>
+
     private lateinit var underTest: CommunalAppWidgetHostStartable
 
     @Before
@@ -54,6 +63,9 @@
         MockitoAnnotations.initMocks(this)
         kosmos.fakeUserRepository.setUserInfos(listOf(MAIN_USER_INFO))
 
+        appWidgetIdToRemove = MutableSharedFlow()
+        whenever(appWidgetHost.appWidgetIdToRemove).thenReturn(appWidgetIdToRemove)
+
         underTest =
             CommunalAppWidgetHostStartable(
                 appWidgetHost,
@@ -120,6 +132,38 @@
             }
         }
 
+    @Test
+    fun removeAppWidgetReportedByHost() =
+        with(kosmos) {
+            testScope.runTest {
+                // Set up communal widgets
+                val widget1 =
+                    mock<CommunalWidgetContentModel> { whenever(this.appWidgetId).thenReturn(1) }
+                val widget2 =
+                    mock<CommunalWidgetContentModel> { whenever(this.appWidgetId).thenReturn(2) }
+                val widget3 =
+                    mock<CommunalWidgetContentModel> { whenever(this.appWidgetId).thenReturn(3) }
+                fakeCommunalWidgetRepository.setCommunalWidgets(listOf(widget1, widget2, widget3))
+
+                underTest.start()
+
+                // Assert communal widgets has 3
+                val communalWidgets by
+                    collectLastValue(fakeCommunalWidgetRepository.communalWidgets)
+                assertThat(communalWidgets).containsExactly(widget1, widget2, widget3)
+
+                // Report app widget 1 to remove and assert widget removed
+                appWidgetIdToRemove.emit(1)
+                runCurrent()
+                assertThat(communalWidgets).containsExactly(widget2, widget3)
+
+                // Report app widget 3 to remove and assert widget removed
+                appWidgetIdToRemove.emit(3)
+                runCurrent()
+                assertThat(communalWidgets).containsExactly(widget2)
+            }
+        }
+
     private suspend fun setCommunalAvailable(available: Boolean) =
         with(kosmos) {
             fakeKeyguardRepository.setIsEncryptedOrLockdown(false)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt
index b54c5bd..9536084 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt
@@ -32,6 +32,7 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class DeviceEntryRepositoryTest : SysuiTestCase() {
 
     @Mock private lateinit var lockPatternUtils: LockPatternUtils
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt
index 32943a1..51db451 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt
@@ -33,6 +33,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class DeviceUnlockedInteractorTest : SysuiTestCase() {
 
     private val kosmos = testKosmos()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/ui/binder/LiftToRunFaceAuthBinderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/ui/binder/LiftToRunFaceAuthBinderTest.kt
new file mode 100644
index 0000000..e9e85c9
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/ui/binder/LiftToRunFaceAuthBinderTest.kt
@@ -0,0 +1,200 @@
+/*
+ * 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.deviceentry.domain.ui.binder
+
+import android.content.packageManager
+import android.content.pm.PackageManager
+import android.hardware.Sensor
+import android.hardware.TriggerEventListener
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.data.repository.keyguardBouncerRepository
+import com.android.systemui.deviceentry.ui.binder.liftToRunFaceAuthBinder
+import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.power.data.repository.fakePowerRepository
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.sensors.asyncSensorManager
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class LiftToRunFaceAuthBinderTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val sensorManager = kosmos.asyncSensorManager
+    private val powerRepository = kosmos.fakePowerRepository
+    private val keyguardRepository = kosmos.fakeKeyguardRepository
+    private val bouncerRepository = kosmos.keyguardBouncerRepository
+    private val biometricSettingsRepository = kosmos.biometricSettingsRepository
+    private val packageManager = kosmos.packageManager
+
+    @Captor private lateinit var triggerEventListenerCaptor: ArgumentCaptor<TriggerEventListener>
+    @Mock private lateinit var mockSensor: Sensor
+
+    private val underTest = kosmos.liftToRunFaceAuthBinder
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        whenever(packageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true)
+        whenever(sensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE)).thenReturn(mockSensor)
+    }
+
+    @Test
+    fun doNotListenForGesture() =
+        testScope.runTest {
+            start()
+            verifyNeverRequestsTriggerSensor()
+        }
+
+    @Test
+    fun awakeKeyguard_listenForGesture() =
+        testScope.runTest {
+            start()
+            givenAwakeKeyguard(true)
+            runCurrent()
+            verifyRequestTriggerSensor()
+        }
+
+    @Test
+    fun faceNotEnrolled_listenForGesture() =
+        testScope.runTest {
+            start()
+            givenAwakeKeyguard(true)
+            biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false)
+            runCurrent()
+            verifyNeverRequestsTriggerSensor()
+        }
+
+    @Test
+    fun notInteractive_doNotListenForGesture() =
+        testScope.runTest {
+            start()
+            givenAwakeKeyguard(true)
+            powerRepository.setInteractive(false)
+            runCurrent()
+            verifyNeverRequestsTriggerSensor()
+        }
+
+    @Test
+    fun primaryBouncer_listenForGesture() =
+        testScope.runTest {
+            start()
+            givenAwakeKeyguard(false)
+            givenPrimaryBouncerShowing()
+            runCurrent()
+            verifyRequestTriggerSensor()
+        }
+
+    @Test
+    fun alternateBouncer_listenForGesture() =
+        testScope.runTest {
+            start()
+            givenAwakeKeyguard(false)
+            givenAlternateBouncerShowing()
+            runCurrent()
+            verifyRequestTriggerSensor()
+        }
+
+    @Test
+    fun restartListeningForGestureAfterSensorTrigger() =
+        testScope.runTest {
+            start()
+            givenAwakeKeyguard(true)
+            runCurrent()
+            verifyRequestTriggerSensor()
+            clearInvocations(sensorManager)
+
+            triggerEventListenerCaptor.value.onTrigger(null)
+            runCurrent()
+            verifyRequestTriggerSensor()
+        }
+
+    @Test
+    fun cancelTriggerSensor_keyguardNotAwakeAnymore() =
+        testScope.runTest {
+            start()
+            givenAwakeKeyguard(true)
+            runCurrent()
+            verifyRequestTriggerSensor()
+
+            givenAwakeKeyguard(false)
+            runCurrent()
+            verifyCancelTriggerSensor()
+        }
+
+    private fun start() {
+        underTest.start()
+        biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
+        givenAwakeKeyguard(false)
+        givenBouncerNotShowing()
+    }
+
+    private fun givenAwakeKeyguard(isAwake: Boolean) {
+        powerRepository.setInteractive(isAwake)
+        keyguardRepository.setKeyguardShowing(isAwake)
+        keyguardRepository.setKeyguardOccluded(false)
+    }
+
+    private fun givenPrimaryBouncerShowing() {
+        bouncerRepository.setPrimaryShow(true)
+        bouncerRepository.setAlternateVisible(false)
+    }
+
+    private fun givenBouncerNotShowing() {
+        bouncerRepository.setPrimaryShow(false)
+        bouncerRepository.setAlternateVisible(false)
+    }
+
+    private fun givenAlternateBouncerShowing() {
+        bouncerRepository.setPrimaryShow(false)
+        bouncerRepository.setAlternateVisible(true)
+    }
+
+    private fun verifyRequestTriggerSensor() {
+        verify(sensorManager).requestTriggerSensor(capture(triggerEventListenerCaptor), any())
+    }
+
+    private fun verifyNeverRequestsTriggerSensor() {
+        verify(sensorManager, never()).requestTriggerSensor(any(), any())
+    }
+
+    private fun verifyCancelTriggerSensor() {
+        verify(sensorManager).cancelTriggerSensor(any(), any())
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dock/DockManagerFakeKosmos.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dock/DockManagerFakeKosmos.kt
new file mode 100644
index 0000000..06275fa
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dock/DockManagerFakeKosmos.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.dock
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.dockManager: DockManager by Kosmos.Fixture { fakeDockManager }
+val Kosmos.fakeDockManager: DockManagerFake by Kosmos.Fixture { DockManagerFake() }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayCallbackControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayCallbackControllerTest.kt
index 2c6c793..d9dcfdc 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayCallbackControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayCallbackControllerTest.kt
@@ -31,6 +31,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class DreamOverlayCallbackControllerTest : SysuiTestCase() {
 
     @Mock private lateinit var callback: DreamOverlayCallbackController.Callback
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayNotificationCountProviderTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayNotificationCountProviderTest.java
index d379dc6..5ae8595 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayNotificationCountProviderTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayNotificationCountProviderTest.java
@@ -39,6 +39,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
+@android.platform.test.annotations.EnabledOnRavenwood
 public class DreamOverlayNotificationCountProviderTest extends SysuiTestCase {
     @Mock
     NotificationListener mNotificationListener;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
index 8bf878c..b46f2aa 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
@@ -50,6 +50,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
+@android.platform.test.annotations.EnabledOnRavenwood
 public class DreamOverlayStateControllerTest extends SysuiTestCase {
     @Mock
     DreamOverlayStateController.Callback mCallback;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayStatusBarItemsProviderTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayStatusBarItemsProviderTest.java
index 7ff345c..ad353ce 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayStatusBarItemsProviderTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayStatusBarItemsProviderTest.java
@@ -37,6 +37,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
+@android.platform.test.annotations.EnabledOnRavenwood
 public class DreamOverlayStatusBarItemsProviderTest extends SysuiTestCase {
     @Mock
     DreamOverlayStatusBarItemsProvider.Callback mCallback;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java
index e0c6ab2..cb5702ad 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java
@@ -42,6 +42,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
+@android.platform.test.annotations.EnabledOnRavenwood
 public class AssistantAttentionConditionTest extends SysuiTestCase {
     @Mock
     Condition.Callback mCallback;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/DreamConditionTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/DreamConditionTest.java
index 480754c..96d3c93 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/DreamConditionTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/DreamConditionTest.java
@@ -45,6 +45,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
+@android.platform.test.annotations.EnabledOnRavenwood
 public class DreamConditionTest extends SysuiTestCase {
     @Mock
     Condition.Callback mCallback;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorKosmos.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorKosmos.kt
new file mode 100644
index 0000000..efccf7a
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorKosmos.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.dreams.homecontrols
+
+import com.android.systemui.controls.dagger.ControlsComponent
+import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.controls.panels.AuthorizedPanelsRepository
+import com.android.systemui.controls.panels.selectedComponentRepository
+import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.util.mockito.mock
+
+val Kosmos.homeControlsComponentInteractor by
+    Kosmos.Fixture {
+        HomeControlsComponentInteractor(
+            selectedComponentRepository = selectedComponentRepository,
+            controlsComponent,
+            authorizedPanelsRepository = authorizedPanelsRepository,
+            userRepository = fakeUserRepository,
+            bgScope = applicationCoroutineScope,
+        )
+    }
+
+val Kosmos.controlsComponent by Kosmos.Fixture<ControlsComponent> { mock() }
+val Kosmos.controlsListingController by Kosmos.Fixture<ControlsListingController> { mock() }
+val Kosmos.authorizedPanelsRepository by Kosmos.Fixture<AuthorizedPanelsRepository> { mock() }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorTest.kt
new file mode 100644
index 0000000..ce74a90
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorTest.kt
@@ -0,0 +1,255 @@
+/*
+ * 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.dreams.homecontrols
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.ServiceInfo
+import android.content.pm.UserInfo
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.controls.ControlsServiceInfo
+import com.android.systemui.controls.dagger.ControlsComponent
+import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.controls.panels.AuthorizedPanelsRepository
+import com.android.systemui.controls.panels.FakeSelectedComponentRepository
+import com.android.systemui.controls.panels.SelectedComponentRepository
+import com.android.systemui.controls.panels.selectedComponentRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.mockito.withArgCaptor
+import com.google.common.truth.Truth.assertThat
+import java.util.Optional
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class HomeControlsComponentInteractorTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+
+    private lateinit var controlsComponent: ControlsComponent
+    private lateinit var controlsListingController: ControlsListingController
+    private lateinit var authorizedPanelsRepository: AuthorizedPanelsRepository
+    private lateinit var underTest: HomeControlsComponentInteractor
+    private lateinit var userRepository: FakeUserRepository
+    private lateinit var selectedComponentRepository: FakeSelectedComponentRepository
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        userRepository = kosmos.fakeUserRepository
+        userRepository.setUserInfos(listOf(PRIMARY_USER, ANOTHER_USER))
+
+        controlsComponent = kosmos.controlsComponent
+        authorizedPanelsRepository = kosmos.authorizedPanelsRepository
+        controlsListingController = kosmos.controlsListingController
+        selectedComponentRepository = kosmos.selectedComponentRepository
+
+        selectedComponentRepository.setCurrentUserHandle(PRIMARY_USER.userHandle)
+        whenever(controlsComponent.getControlsListingController())
+            .thenReturn(Optional.of(controlsListingController))
+
+        underTest =
+            HomeControlsComponentInteractor(
+                selectedComponentRepository,
+                controlsComponent,
+                authorizedPanelsRepository,
+                userRepository,
+                kosmos.applicationCoroutineScope,
+            )
+    }
+
+    @Test
+    fun testPanelComponentReturnsComponentNameForSelectedItemByUser() =
+        with(kosmos) {
+            testScope.runTest {
+                whenever(authorizedPanelsRepository.getAuthorizedPanels())
+                    .thenReturn(setOf(TEST_PACKAGE_PANEL))
+                userRepository.setSelectedUserInfo(PRIMARY_USER)
+                selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL)
+                val actualValue by collectLastValue(underTest.panelComponent)
+                assertThat(actualValue).isNull()
+                runServicesUpdate()
+                assertThat(actualValue).isEqualTo(TEST_COMPONENT_PANEL)
+            }
+        }
+
+    @Test
+    fun testPanelComponentReturnsComponentNameAsInitialValueWithoutServiceUpdate() =
+        with(kosmos) {
+            testScope.runTest {
+                whenever(authorizedPanelsRepository.getAuthorizedPanels())
+                    .thenReturn(setOf(TEST_PACKAGE_PANEL))
+                userRepository.setSelectedUserInfo(PRIMARY_USER)
+                selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL)
+                whenever(controlsListingController.getCurrentServices())
+                    .thenReturn(
+                        listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true))
+                    )
+                val actualValue by collectLastValue(underTest.panelComponent)
+                assertThat(actualValue).isEqualTo(TEST_COMPONENT_PANEL)
+            }
+        }
+
+    @Test
+    fun testPanelComponentReturnsNullForHomeControlsThatDoesNotSupportPanel() =
+        with(kosmos) {
+            testScope.runTest {
+                whenever(authorizedPanelsRepository.getAuthorizedPanels())
+                    .thenReturn(setOf(TEST_PACKAGE_PANEL))
+                userRepository.setSelectedUserInfo(PRIMARY_USER)
+                selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_NON_PANEL)
+                val actualValue by collectLastValue(underTest.panelComponent)
+                assertThat(actualValue).isNull()
+                runServicesUpdate(false)
+                assertThat(actualValue).isNull()
+            }
+        }
+
+    @Test
+    fun testPanelComponentReturnsNullWhenPanelIsUnauthorized() =
+        with(kosmos) {
+            testScope.runTest {
+                whenever(authorizedPanelsRepository.getAuthorizedPanels()).thenReturn(setOf())
+                userRepository.setSelectedUserInfo(PRIMARY_USER)
+                selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL)
+                val actualValue by collectLastValue(underTest.panelComponent)
+                assertThat(actualValue).isNull()
+                runServicesUpdate()
+                assertThat(actualValue).isNull()
+            }
+        }
+
+    @Test
+    fun testPanelComponentReturnsComponentNameForDifferentUsers() =
+        with(kosmos) {
+            testScope.runTest {
+                whenever(authorizedPanelsRepository.getAuthorizedPanels())
+                    .thenReturn(setOf(TEST_PACKAGE_PANEL))
+                userRepository.setSelectedUserInfo(ANOTHER_USER)
+                selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_NON_PANEL)
+                selectedComponentRepository.setCurrentUserHandle(ANOTHER_USER.userHandle)
+                selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL)
+
+                val actualValue by collectLastValue(underTest.panelComponent)
+                assertThat(actualValue).isNull()
+                runServicesUpdate()
+                assertThat(actualValue).isEqualTo(TEST_COMPONENT_PANEL)
+            }
+        }
+
+    @Test
+    fun testPanelComponentReturnsNullWhenControlsComponentReturnsNullForListingController() =
+        with(kosmos) {
+            testScope.runTest {
+                whenever(authorizedPanelsRepository.getAuthorizedPanels())
+                    .thenReturn(setOf(TEST_PACKAGE_PANEL))
+                whenever(controlsComponent.getControlsListingController())
+                    .thenReturn(Optional.empty())
+                userRepository.setSelectedUserInfo(PRIMARY_USER)
+                selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL)
+                val actualValue by collectLastValue(underTest.panelComponent)
+                assertThat(actualValue).isNull()
+            }
+        }
+
+    private fun runServicesUpdate(hasPanelBoolean: Boolean = true) {
+        val listings =
+            listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = hasPanelBoolean))
+        val callback = withArgCaptor { verify(controlsListingController).addCallback(capture()) }
+        callback.onServicesUpdated(listings)
+    }
+
+    private fun ControlsServiceInfo(
+        componentName: ComponentName,
+        label: CharSequence,
+        hasPanel: Boolean
+    ): ControlsServiceInfo {
+        val serviceInfo =
+            ServiceInfo().apply {
+                applicationInfo = ApplicationInfo()
+                packageName = componentName.packageName
+                name = componentName.className
+            }
+        return FakeControlsServiceInfo(context, serviceInfo, label, hasPanel)
+    }
+
+    private class FakeControlsServiceInfo(
+        context: Context,
+        serviceInfo: ServiceInfo,
+        private val label: CharSequence,
+        hasPanel: Boolean
+    ) : ControlsServiceInfo(context, serviceInfo) {
+
+        init {
+            if (hasPanel) {
+                panelActivity = serviceInfo.componentName
+            }
+        }
+
+        override fun loadLabel(): CharSequence {
+            return label
+        }
+    }
+
+    companion object {
+        private const val PRIMARY_USER_ID = 0
+        private val PRIMARY_USER =
+            UserInfo(
+                /* id= */ PRIMARY_USER_ID,
+                /* name= */ "primary user",
+                /* flags= */ UserInfo.FLAG_PRIMARY
+            )
+
+        private const val ANOTHER_USER_ID = 1
+        private val ANOTHER_USER =
+            UserInfo(
+                /* id= */ ANOTHER_USER_ID,
+                /* name= */ "another user",
+                /* flags= */ UserInfo.FLAG_PRIMARY
+            )
+        private const val TEST_PACKAGE = "pkg"
+        private val TEST_COMPONENT = ComponentName(TEST_PACKAGE, "service")
+        private const val TEST_PACKAGE_PANEL = "pkg.panel"
+        private val TEST_COMPONENT_PANEL = ComponentName(TEST_PACKAGE_PANEL, "service")
+        private val TEST_SELECTED_COMPONENT_PANEL =
+            SelectedComponentRepository.SelectedComponent(
+                TEST_PACKAGE_PANEL,
+                TEST_COMPONENT_PANEL,
+                true
+            )
+        private val TEST_SELECTED_COMPONENT_NON_PANEL =
+            SelectedComponentRepository.SelectedComponent(
+                TEST_PACKAGE_PANEL,
+                TEST_COMPONENT_PANEL,
+                false
+            )
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamServiceTest.kt
new file mode 100644
index 0000000..d28b6bf
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamServiceTest.kt
@@ -0,0 +1,120 @@
+/*
+ * 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.dreams.homecontrols
+
+import android.app.Activity
+import android.content.ComponentName
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.controls.dagger.ControlsComponent
+import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.controls.settings.FakeControlsSettingsRepository
+import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.FakeLogBuffer.Factory.Companion.create
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.whenever
+import java.util.Optional
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class HomeControlsDreamServiceTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+
+    private lateinit var controlsSettingsRepository: FakeControlsSettingsRepository
+    @Mock private lateinit var taskFragmentComponentFactory: TaskFragmentComponent.Factory
+    @Mock private lateinit var taskFragmentComponent: TaskFragmentComponent
+    @Mock private lateinit var activity: Activity
+    private val logBuffer: LogBuffer = create()
+
+    private lateinit var underTest: HomeControlsDreamService
+    private lateinit var homeControlsComponentInteractor: HomeControlsComponentInteractor
+    private lateinit var fakeDreamActivityProvider: DreamActivityProvider
+    private lateinit var controlsComponent: ControlsComponent
+    private lateinit var controlsListingController: ControlsListingController
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        whenever(taskFragmentComponentFactory.create(any(), any(), any(), any()))
+            .thenReturn(taskFragmentComponent)
+
+        controlsSettingsRepository = FakeControlsSettingsRepository()
+        controlsSettingsRepository.setAllowActionOnTrivialControlsInLockscreen(true)
+
+        controlsComponent = kosmos.controlsComponent
+        controlsListingController = kosmos.controlsListingController
+
+        whenever(controlsComponent.getControlsListingController())
+            .thenReturn(Optional.of(controlsListingController))
+
+        homeControlsComponentInteractor = kosmos.homeControlsComponentInteractor
+
+        fakeDreamActivityProvider = DreamActivityProvider { activity }
+        underTest =
+            HomeControlsDreamService(
+                controlsSettingsRepository,
+                taskFragmentComponentFactory,
+                homeControlsComponentInteractor,
+                fakeDreamActivityProvider,
+                logBuffer
+            )
+    }
+
+    @Test
+    fun testOnAttachedToWindowCreatesTaskFragmentComponent() {
+        underTest.onAttachedToWindow()
+        verify(taskFragmentComponentFactory).create(any(), any(), any(), any())
+    }
+
+    @Test
+    fun testOnDetachedFromWindowDestroyTaskFragmentComponent() {
+        underTest.onAttachedToWindow()
+        underTest.onDetachedFromWindow()
+        verify(taskFragmentComponent).destroy()
+    }
+
+    @Test
+    fun testNotCreatingTaskFragmentComponentWhenActivityIsNull() {
+        fakeDreamActivityProvider = DreamActivityProvider { null }
+        underTest =
+            HomeControlsDreamService(
+                controlsSettingsRepository,
+                taskFragmentComponentFactory,
+                homeControlsComponentInteractor,
+                fakeDreamActivityProvider,
+                logBuffer
+            )
+
+        underTest.onAttachedToWindow()
+        verify(taskFragmentComponentFactory, never()).create(any(), any(), any(), any())
+    }
+
+    companion object {
+        private const val TEST_PACKAGE_PANEL = "pkg.panel"
+        private val TEST_COMPONENT_PANEL = ComponentName(TEST_PACKAGE_PANEL, "service")
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartableTest.kt
new file mode 100644
index 0000000..6610e70
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartableTest.kt
@@ -0,0 +1,202 @@
+/*
+ * 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.dreams.homecontrols
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.content.pm.ServiceInfo
+import android.content.pm.UserInfo
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.service.controls.flags.Flags.FLAG_HOME_PANEL_DREAM
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.controls.ControlsServiceInfo
+import com.android.systemui.controls.dagger.ControlsComponent
+import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.controls.panels.AuthorizedPanelsRepository
+import com.android.systemui.controls.panels.SelectedComponentRepository
+import com.android.systemui.controls.panels.selectedComponentRepository
+import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.whenever
+import java.util.Optional
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class HomeControlsDreamStartableTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+
+    @Mock private lateinit var packageManager: PackageManager
+
+    private lateinit var homeControlsComponentInteractor: HomeControlsComponentInteractor
+    private lateinit var selectedComponentRepository: SelectedComponentRepository
+    private lateinit var authorizedPanelsRepository: AuthorizedPanelsRepository
+    private lateinit var userRepository: FakeUserRepository
+    private lateinit var controlsComponent: ControlsComponent
+    private lateinit var controlsListingController: ControlsListingController
+
+    private lateinit var startable: HomeControlsDreamStartable
+    private val componentName = ComponentName(context, HomeControlsDreamService::class.java)
+    private val testScope = kosmos.testScope
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        selectedComponentRepository = kosmos.selectedComponentRepository
+        authorizedPanelsRepository = kosmos.authorizedPanelsRepository
+        userRepository = kosmos.fakeUserRepository
+        controlsComponent = kosmos.controlsComponent
+        controlsListingController = kosmos.controlsListingController
+
+        userRepository.setUserInfos(listOf(PRIMARY_USER))
+
+        whenever(authorizedPanelsRepository.getAuthorizedPanels())
+            .thenReturn(setOf(TEST_PACKAGE_PANEL))
+
+        whenever(controlsComponent.getControlsListingController())
+            .thenReturn(Optional.of(controlsListingController))
+        whenever(controlsListingController.getCurrentServices())
+            .thenReturn(listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true)))
+
+        homeControlsComponentInteractor = kosmos.homeControlsComponentInteractor
+
+        startable =
+            HomeControlsDreamStartable(
+                mContext,
+                packageManager,
+                homeControlsComponentInteractor,
+                kosmos.applicationCoroutineScope
+            )
+    }
+
+    @Test
+    @EnableFlags(FLAG_HOME_PANEL_DREAM)
+    fun testStartEnablesHomeControlsDreamServiceWhenPanelComponentIsNotNull() =
+        testScope.runTest {
+            userRepository.setSelectedUserInfo(PRIMARY_USER)
+            selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL)
+            startable.start()
+            runCurrent()
+            verify(packageManager)
+                .setComponentEnabledSetting(
+                    eq(componentName),
+                    eq(PackageManager.COMPONENT_ENABLED_STATE_ENABLED),
+                    eq(PackageManager.DONT_KILL_APP)
+                )
+        }
+
+    @Test
+    @EnableFlags(FLAG_HOME_PANEL_DREAM)
+    fun testStartDisablesHomeControlsDreamServiceWhenPanelComponentIsNull() =
+        testScope.runTest {
+            selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_NON_PANEL)
+            startable.start()
+            runCurrent()
+            verify(packageManager)
+                .setComponentEnabledSetting(
+                    eq(componentName),
+                    eq(PackageManager.COMPONENT_ENABLED_STATE_DISABLED),
+                    eq(PackageManager.DONT_KILL_APP)
+                )
+        }
+
+    @Test
+    @DisableFlags(FLAG_HOME_PANEL_DREAM)
+    fun testStartDoesNotRunDreamServiceWhenFlagIsDisabled() =
+        testScope.runTest {
+            selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_NON_PANEL)
+            startable.start()
+            runCurrent()
+            verify(packageManager, never()).setComponentEnabledSetting(any(), any(), any())
+        }
+
+    private fun ControlsServiceInfo(
+        componentName: ComponentName,
+        label: CharSequence,
+        hasPanel: Boolean
+    ): ControlsServiceInfo {
+        val serviceInfo =
+            ServiceInfo().apply {
+                applicationInfo = ApplicationInfo()
+                packageName = componentName.packageName
+                name = componentName.className
+            }
+        return FakeControlsServiceInfo(context, serviceInfo, label, hasPanel)
+    }
+
+    private class FakeControlsServiceInfo(
+        context: Context,
+        serviceInfo: ServiceInfo,
+        private val label: CharSequence,
+        hasPanel: Boolean
+    ) : ControlsServiceInfo(context, serviceInfo) {
+
+        init {
+            if (hasPanel) {
+                panelActivity = serviceInfo.componentName
+            }
+        }
+
+        override fun loadLabel(): CharSequence {
+            return label
+        }
+    }
+
+    companion object {
+        private const val PRIMARY_USER_ID = 0
+        private val PRIMARY_USER =
+            UserInfo(
+                /* id= */ PRIMARY_USER_ID,
+                /* name= */ "primary user",
+                /* flags= */ UserInfo.FLAG_PRIMARY
+            )
+        private const val TEST_PACKAGE_PANEL = "pkg.panel"
+        private val TEST_COMPONENT_PANEL = ComponentName(TEST_PACKAGE_PANEL, "service")
+        private val TEST_SELECTED_COMPONENT_PANEL =
+            SelectedComponentRepository.SelectedComponent(
+                TEST_PACKAGE_PANEL,
+                TEST_COMPONENT_PANEL,
+                true
+            )
+        private val TEST_SELECTED_COMPONENT_NON_PANEL =
+            SelectedComponentRepository.SelectedComponent(
+                TEST_PACKAGE_PANEL,
+                TEST_COMPONENT_PANEL,
+                false
+            )
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/scrim/BouncerlessScrimControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/scrim/BouncerlessScrimControllerTest.java
index 017fdbe..97052a8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/scrim/BouncerlessScrimControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/scrim/BouncerlessScrimControllerTest.java
@@ -39,6 +39,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
+@android.platform.test.annotations.EnabledOnRavenwood
 public class BouncerlessScrimControllerTest extends SysuiTestCase {
     @Mock
     BouncerlessScrimController.Callback mCallback;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/scrim/ScrimManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/scrim/ScrimManagerTest.java
index 4ee4a60..ebbcf98 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/scrim/ScrimManagerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/scrim/ScrimManagerTest.java
@@ -39,6 +39,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
+@android.platform.test.annotations.EnabledOnRavenwood
 public class ScrimManagerTest extends SysuiTestCase {
     @Mock
     ScrimController mBouncerlessScrimController;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/fold/ui/helper/FoldPostureTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/fold/ui/helper/FoldPostureTest.kt
index 61b2057..db52a45 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/fold/ui/helper/FoldPostureTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/fold/ui/helper/FoldPostureTest.kt
@@ -28,6 +28,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class FoldPostureTest : SysuiTestCase() {
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/inputmethod/data/repository/InputMethodRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/inputmethod/data/repository/InputMethodRepositoryTest.kt
new file mode 100644
index 0000000..857cdce
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/inputmethod/data/repository/InputMethodRepositoryTest.kt
@@ -0,0 +1,124 @@
+/*
+ * 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.inputmethod.data.repository
+
+import android.os.UserHandle
+import android.view.inputmethod.InputMethodInfo
+import android.view.inputmethod.InputMethodManager
+import android.view.inputmethod.InputMethodSubtype
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+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.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.count
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class InputMethodRepositoryTest : SysuiTestCase() {
+
+    @Mock private lateinit var inputMethodManager: InputMethodManager
+
+    private val kosmos = Kosmos()
+    private val testScope = kosmos.testScope
+
+    private lateinit var underTest: InputMethodRepository
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        whenever(inputMethodManager.getEnabledInputMethodSubtypeList(eq(null), anyBoolean()))
+            .thenReturn(listOf())
+
+        underTest =
+            InputMethodRepositoryImpl(
+                backgroundDispatcher = kosmos.testDispatcher,
+                inputMethodManager = inputMethodManager,
+            )
+    }
+
+    @Test
+    fun enabledInputMethods_noImes_emptyFlow() =
+        testScope.runTest {
+            whenever(inputMethodManager.getEnabledInputMethodListAsUser(eq(USER_HANDLE)))
+                .thenReturn(listOf())
+            whenever(inputMethodManager.getEnabledInputMethodSubtypeList(any(), anyBoolean()))
+                .thenReturn(listOf())
+
+            assertThat(underTest.enabledInputMethods(USER_ID, fetchSubtypes = true).count())
+                .isEqualTo(0)
+        }
+
+    @Test
+    fun selectedInputMethodSubtypes_returnsSubtypeList() =
+        testScope.runTest {
+            val subtypeId = 123
+            val isAuxiliary = true
+            whenever(inputMethodManager.getEnabledInputMethodListAsUser(eq(USER_HANDLE)))
+                .thenReturn(listOf(mock<InputMethodInfo>()))
+            whenever(inputMethodManager.getEnabledInputMethodSubtypeList(any(), anyBoolean()))
+                .thenReturn(listOf())
+            whenever(inputMethodManager.getEnabledInputMethodSubtypeList(eq(null), anyBoolean()))
+                .thenReturn(
+                    listOf(
+                        InputMethodSubtype.InputMethodSubtypeBuilder()
+                            .setSubtypeId(subtypeId)
+                            .setIsAuxiliary(isAuxiliary)
+                            .build()
+                    )
+                )
+
+            val result = underTest.selectedInputMethodSubtypes()
+            assertThat(result).hasSize(1)
+            assertThat(result.first().subtypeId).isEqualTo(subtypeId)
+            assertThat(result.first().isAuxiliary).isEqualTo(isAuxiliary)
+        }
+
+    @Test
+    fun showImePicker_forwardsDisplayId() =
+        testScope.runTest {
+            val displayId = 7
+
+            underTest.showInputMethodPicker(displayId, /* showAuxiliarySubtypes = */ true)
+
+            verify(inputMethodManager)
+                .showInputMethodPickerFromSystem(
+                    /* showAuxiliarySubtypes = */ eq(true),
+                    /* displayId = */ eq(displayId)
+                )
+        }
+
+    companion object {
+        private const val USER_ID = 100
+        private val USER_HANDLE = UserHandle.of(USER_ID)
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/inputmethod/domain/interactor/InputMethodInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/inputmethod/domain/interactor/InputMethodInteractorTest.kt
new file mode 100644
index 0000000..d23ff2a
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/inputmethod/domain/interactor/InputMethodInteractorTest.kt
@@ -0,0 +1,157 @@
+/*
+ * 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.inputmethod.domain.interactor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.inputmethod.data.model.InputMethodModel
+import com.android.systemui.inputmethod.data.repository.fakeInputMethodRepository
+import com.android.systemui.inputmethod.data.repository.inputMethodRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.google.common.truth.Truth.assertThat
+import java.util.UUID
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class InputMethodInteractorTest : SysuiTestCase() {
+
+    private val kosmos = Kosmos()
+    private val testScope = kosmos.testScope
+    private val fakeInputMethodRepository = kosmos.fakeInputMethodRepository
+
+    private val underTest = InputMethodInteractor(repository = kosmos.inputMethodRepository)
+
+    @Test
+    fun hasMultipleEnabledImesOrSubtypes_noImes_returnsFalse() =
+        testScope.runTest {
+            fakeInputMethodRepository.setEnabledInputMethods(USER_ID)
+
+            assertThat(underTest.hasMultipleEnabledImesOrSubtypes(USER_ID)).isFalse()
+        }
+
+    @Test
+    fun hasMultipleEnabledImesOrSubtypes_noMatches_returnsFalse() =
+        testScope.runTest {
+            fakeInputMethodRepository.setEnabledInputMethods(
+                USER_ID,
+                createInputMethodWithSubtypes(auxiliarySubtypes = 1, nonAuxiliarySubtypes = 0),
+                createInputMethodWithSubtypes(auxiliarySubtypes = 3, nonAuxiliarySubtypes = 0),
+            )
+
+            assertThat(underTest.hasMultipleEnabledImesOrSubtypes(USER_ID)).isFalse()
+        }
+
+    @Test
+    fun hasMultipleEnabledImesOrSubtypes_oneMatch_returnsFalse() =
+        testScope.runTest {
+            fakeInputMethodRepository.setEnabledInputMethods(
+                USER_ID,
+                createInputMethodWithSubtypes(auxiliarySubtypes = 0, nonAuxiliarySubtypes = 0),
+            )
+
+            assertThat(underTest.hasMultipleEnabledImesOrSubtypes(USER_ID)).isFalse()
+        }
+
+    @Test
+    fun hasMultipleEnabledImesOrSubtypes_twoMatches_returnsTrue() =
+        testScope.runTest {
+            fakeInputMethodRepository.setEnabledInputMethods(
+                USER_ID,
+                createInputMethodWithSubtypes(auxiliarySubtypes = 0, nonAuxiliarySubtypes = 1),
+                createInputMethodWithSubtypes(auxiliarySubtypes = 0, nonAuxiliarySubtypes = 0),
+            )
+
+            assertThat(underTest.hasMultipleEnabledImesOrSubtypes(USER_ID)).isTrue()
+        }
+
+    @Test
+    fun hasMultipleEnabledImesOrSubtypes_oneWithNonAux_returnsFalse() =
+        testScope.runTest {
+            fakeInputMethodRepository.setEnabledInputMethods(
+                USER_ID,
+                createInputMethodWithSubtypes(auxiliarySubtypes = 0, nonAuxiliarySubtypes = 2),
+            )
+
+            assertThat(underTest.hasMultipleEnabledImesOrSubtypes(USER_ID)).isFalse()
+        }
+
+    @Test
+    fun hasMultipleEnabledImesOrSubtypes_twoWithAux_returnsFalse() =
+        testScope.runTest {
+            fakeInputMethodRepository.setEnabledInputMethods(
+                USER_ID,
+                createInputMethodWithSubtypes(auxiliarySubtypes = 3, nonAuxiliarySubtypes = 0),
+                createInputMethodWithSubtypes(auxiliarySubtypes = 5, nonAuxiliarySubtypes = 0),
+            )
+
+            assertThat(underTest.hasMultipleEnabledImesOrSubtypes(USER_ID)).isFalse()
+        }
+
+    @Test
+    fun hasMultipleEnabledImesOrSubtypes_selectedHasOneSubtype_returnsFalse() =
+        testScope.runTest {
+            fakeInputMethodRepository.selectedInputMethodSubtypes =
+                listOf(InputMethodModel.Subtype(1, isAuxiliary = false))
+
+            assertThat(underTest.hasMultipleEnabledImesOrSubtypes(USER_ID)).isFalse()
+        }
+
+    @Test
+    fun hasMultipleEnabledImesOrSubtypes_selectedHasTwoSubtypes_returnsTrue() =
+        testScope.runTest {
+            fakeInputMethodRepository.selectedInputMethodSubtypes =
+                listOf(
+                    InputMethodModel.Subtype(subtypeId = 1, isAuxiliary = false),
+                    InputMethodModel.Subtype(subtypeId = 2, isAuxiliary = false),
+                )
+
+            assertThat(underTest.hasMultipleEnabledImesOrSubtypes(USER_ID)).isTrue()
+        }
+
+    @Test
+    fun showImePicker_shownOnCorrectId() =
+        testScope.runTest {
+            val displayId = 7
+
+            underTest.showInputMethodPicker(displayId, showAuxiliarySubtypes = false)
+
+            assertThat(fakeInputMethodRepository.inputMethodPickerShownDisplayId)
+                .isEqualTo(displayId)
+        }
+
+    private fun createInputMethodWithSubtypes(
+        auxiliarySubtypes: Int,
+        nonAuxiliarySubtypes: Int,
+    ): InputMethodModel {
+        return InputMethodModel(
+            imeId = UUID.randomUUID().toString(),
+            subtypes =
+                List(auxiliarySubtypes + nonAuxiliarySubtypes) {
+                    InputMethodModel.Subtype(subtypeId = it, isAuxiliary = it < auxiliarySubtypes)
+                }
+        )
+    }
+
+    companion object {
+        private const val USER_ID = 100
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt
index e20d3af..ee3e241 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt
@@ -43,6 +43,7 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class CameraQuickAffordanceConfigTest : SysuiTestCase() {
 
     @Mock private lateinit var cameraGestureHelper: CameraGestureHelper
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfigTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfigTest.kt
index 4ae144c..77e0f4e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfigTest.kt
@@ -42,6 +42,7 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class FlashlightQuickAffordanceConfigTest : LeakCheckedTest() {
 
     @Mock private lateinit var context: Context
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt
index 521dea3..ca64cec 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt
@@ -40,6 +40,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class QrCodeScannerKeyguardQuickAffordanceConfigTest : SysuiTestCase() {
 
     @Mock private lateinit var controller: QRCodeScannerController
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt
index 6b7d263..558e7e6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt
@@ -50,6 +50,7 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class DeviceEntryFingerprintAuthRepositoryTest : SysuiTestCase() {
     @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
     @Mock private lateinit var authController: AuthController
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DevicePostureRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DevicePostureRepositoryTest.kt
index ae6c5b7..a0b8542 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DevicePostureRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DevicePostureRepositoryTest.kt
@@ -40,6 +40,7 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class DevicePostureRepositoryTest : SysuiTestCase() {
     private lateinit var underTest: DevicePostureRepository
     private lateinit var testScope: TestScope
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
index c01c79d..128b465 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
@@ -63,6 +63,7 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class KeyguardRepositoryImplTest : SysuiTestCase() {
 
     @Mock private lateinit var statusBarStateController: StatusBarStateController
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt
index ee47c58f4..5f0f24d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt
@@ -46,6 +46,7 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class TrustRepositoryTest : SysuiTestCase() {
     @Mock private lateinit var trustManager: TrustManager
     @Captor private lateinit var listener: ArgumentCaptor<TrustManager.TrustListener>
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
index 6828041..9368097 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
@@ -45,6 +45,7 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @kotlinx.coroutines.ExperimentalCoroutinesApi
+@android.platform.test.annotations.EnabledOnRavenwood
 class KeyguardTransitionInteractorTest : SysuiTestCase() {
 
     val kosmos = testKosmos()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManagerTest.kt
index e850456..4695ea4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManagerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManagerTest.kt
@@ -13,6 +13,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class KeyguardRemotePreviewManagerTest : SysuiTestCase() {
 
     private val testDispatcher = StandardTestDispatcher()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
index d4dd2ac..0b80ff8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
@@ -36,6 +36,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class LockscreenContentViewModelTest : SysuiTestCase() {
 
     private val kosmos: Kosmos = testKosmos()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt
index 30ac344..6fc5be1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt
@@ -19,7 +19,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.mockPrimaryBouncerInteractor
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.flags.Flags
 import com.android.systemui.flags.fakeFeatureFlagsClassic
@@ -52,11 +52,9 @@
     val testScope = kosmos.testScope
 
     val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
-    val primaryBouncerInteractor = kosmos.primaryBouncerInteractor
+    val primaryBouncerInteractor = kosmos.mockPrimaryBouncerInteractor
     val sysuiStatusBarStateController = kosmos.sysuiStatusBarStateController
-    val underTest by lazy {
-        kosmos.primaryBouncerToGoneTransitionViewModel
-    }
+    val underTest by lazy { kosmos.primaryBouncerToGoneTransitionViewModel }
 
     @Before
     fun setUp() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt
index b267720..2ad872c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt
@@ -40,6 +40,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class SceneContainerRepositoryTest : SysuiTestCase() {
 
     private val kosmos = testKosmos().apply { fakeSceneContainerFlags.enabled = true }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/WindowRootViewVisibilityRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/WindowRootViewVisibilityRepositoryTest.kt
index 7ae501d..13b5b54 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/WindowRootViewVisibilityRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/WindowRootViewVisibilityRepositoryTest.kt
@@ -30,6 +30,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class WindowRootViewVisibilityRepositoryTest : SysuiTestCase() {
     private val iStatusBarService = mock<IStatusBarService>()
     private val executor = FakeExecutor(FakeSystemClock())
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/BcSmartspaceConfigProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/BcSmartspaceConfigProviderTest.kt
index cb83e7c..bbbee90 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/BcSmartspaceConfigProviderTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/BcSmartspaceConfigProviderTest.kt
@@ -30,6 +30,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class BcSmartspaceConfigProviderTest : SysuiTestCase() {
     @Mock private lateinit var featureFlags: FeatureFlags
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/LockscreenPreconditionTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/LockscreenPreconditionTest.kt
index 0b5aea7..089eb43e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/LockscreenPreconditionTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/LockscreenPreconditionTest.kt
@@ -37,6 +37,7 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper
+@android.platform.test.annotations.EnabledOnRavenwood
 class LockscreenPreconditionTest : SysuiTestCase() {
     @Mock
     private lateinit var deviceProvisionedController: DeviceProvisionedController
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/LockscreenTargetFilterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/LockscreenTargetFilterTest.kt
index bf33010..6616786 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/LockscreenTargetFilterTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/LockscreenTargetFilterTest.kt
@@ -52,6 +52,7 @@
 @SmallTest
 @TestableLooper.RunWithLooper
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class LockscreenTargetFilterTest : SysuiTestCase() {
     @Mock private lateinit var secureSettings: SecureSettings
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/RemoteInputRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/RemoteInputRepositoryImplTest.kt
index 8a0400d..f7a8858 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/RemoteInputRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/RemoteInputRepositoryImplTest.kt
@@ -38,6 +38,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class RemoteInputRepositoryImplTest : SysuiTestCase() {
     @Mock private lateinit var remoteInputManager: NotificationRemoteInputManager
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractorTest.kt
index 12469dd..60da53c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractorTest.kt
@@ -34,6 +34,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class RemoteInputInteractorTest : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt
index 6a2e317..4d7d5d3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt
@@ -87,21 +87,21 @@
         }
 
     @Test
-    fun updateShadeExpansion() =
+    fun shadeExpansion_goneToShade() =
         testScope.runTest {
+            val transitionState =
+                MutableStateFlow<ObservableTransitionState>(
+                    ObservableTransitionState.Idle(scene = SceneKey.Gone)
+                )
+            sceneInteractor.setTransitionState(transitionState)
             val expandFraction by collectLastValue(appearanceViewModel.expandFraction)
             assertThat(expandFraction).isEqualTo(0f)
 
-            val transitionState =
-                MutableStateFlow<ObservableTransitionState>(
-                    ObservableTransitionState.Idle(scene = SceneKey.Lockscreen)
-                )
-            sceneInteractor.setTransitionState(transitionState)
             sceneInteractor.changeScene(SceneModel(SceneKey.Shade), "reason")
             val transitionProgress = MutableStateFlow(0f)
             transitionState.value =
                 ObservableTransitionState.Transition(
-                    fromScene = SceneKey.Lockscreen,
+                    fromScene = SceneKey.Gone,
                     toScene = SceneKey.Shade,
                     progress = transitionProgress,
                     isInitiatedByUserInput = false,
@@ -118,4 +118,49 @@
             sceneInteractor.onSceneChanged(SceneModel(SceneKey.Shade), "reason")
             assertThat(expandFraction).isWithin(0.01f).of(1f)
         }
+
+    @Test
+    fun shadeExpansion_idleOnLockscreen() =
+        testScope.runTest {
+            val transitionState =
+                MutableStateFlow<ObservableTransitionState>(
+                    ObservableTransitionState.Idle(scene = SceneKey.Lockscreen)
+                )
+            sceneInteractor.setTransitionState(transitionState)
+            val expandFraction by collectLastValue(appearanceViewModel.expandFraction)
+            assertThat(expandFraction).isEqualTo(1f)
+        }
+
+    @Test
+    fun shadeExpansion_shadeToQs() =
+        testScope.runTest {
+            val transitionState =
+                MutableStateFlow<ObservableTransitionState>(
+                    ObservableTransitionState.Idle(scene = SceneKey.Shade)
+                )
+            sceneInteractor.setTransitionState(transitionState)
+            val expandFraction by collectLastValue(appearanceViewModel.expandFraction)
+            assertThat(expandFraction).isEqualTo(1f)
+
+            sceneInteractor.changeScene(SceneModel(SceneKey.QuickSettings), "reason")
+            val transitionProgress = MutableStateFlow(0f)
+            transitionState.value =
+                ObservableTransitionState.Transition(
+                    fromScene = SceneKey.Shade,
+                    toScene = SceneKey.QuickSettings,
+                    progress = transitionProgress,
+                    isInitiatedByUserInput = false,
+                    isUserInputOngoing = flowOf(false),
+                )
+            val steps = 10
+            repeat(steps) { repetition ->
+                val progress = (1f / steps) * (repetition + 1)
+                transitionProgress.value = progress
+                runCurrent()
+                assertThat(expandFraction).isEqualTo(1f)
+            }
+
+            sceneInteractor.onSceneChanged(SceneModel(SceneKey.QuickSettings), "reason")
+            assertThat(expandFraction).isEqualTo(1f)
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt
index c7411cd..ffe6e6d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt
@@ -30,6 +30,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class NotificationStackAppearanceInteractorTest : SysuiTestCase() {
 
     private val kosmos = Kosmos()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt
index ce00250..18825a2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt
@@ -28,6 +28,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class DisabledWifiRepositoryTest : SysuiTestCase() {
 
     private lateinit var underTest: DisabledWifiRepository
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt
index 7fbbfc7..84c728c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt
@@ -43,6 +43,7 @@
 @Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class WifiInteractorImplTest : SysuiTestCase() {
 
     private lateinit var underTest: WifiInteractor
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/data/repository/UserSetupRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/data/repository/UserSetupRepositoryTest.kt
index ebc81be..4c8bbe0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/data/repository/UserSetupRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/data/repository/UserSetupRepositoryTest.kt
@@ -45,6 +45,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class UserSetupRepositoryTest : SysuiTestCase() {
 
     private val kosmos = testKosmos()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/UserSetupInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/UserSetupInteractorTest.kt
index 26c0f80..7ec0a61 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/UserSetupInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/UserSetupInteractorTest.kt
@@ -30,6 +30,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class UserSetupInteractorTest : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt
index b4a0a37..96d1c0d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt
@@ -34,6 +34,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
 class BooleanFlowOperatorsTest : SysuiTestCase() {
 
     val kosmos = testKosmos()
diff --git a/packages/SystemUI/res/drawable-nodpi/homecontrols_sq.png b/packages/SystemUI/res/drawable-nodpi/homecontrols_sq.png
new file mode 100644
index 0000000..00b461b
--- /dev/null
+++ b/packages/SystemUI/res/drawable-nodpi/homecontrols_sq.png
Binary files differ
diff --git a/packages/SystemUI/res/drawable/accessibility_window_magnification_drag_handle_background_change_inset.xml b/packages/SystemUI/res/drawable/accessibility_window_magnification_drag_handle_background_change_inset.xml
new file mode 100644
index 0000000..02486bf
--- /dev/null
+++ b/packages/SystemUI/res/drawable/accessibility_window_magnification_drag_handle_background_change_inset.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    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.
+-->
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+    android:drawable="@drawable/accessibility_window_magnification_drag_handle_background_change"
+    android:insetBottom="@dimen/magnification_inner_border_margin"
+    android:insetLeft="@dimen/magnification_inner_border_margin"
+    android:insetRight="@dimen/magnification_inner_border_margin"
+    android:insetTop="@dimen/magnification_inner_border_margin" />
diff --git a/packages/SystemUI/res/drawable/accessibility_window_magnification_drag_handle_background_inset.xml b/packages/SystemUI/res/drawable/accessibility_window_magnification_drag_handle_background_inset.xml
new file mode 100644
index 0000000..bfb7c47
--- /dev/null
+++ b/packages/SystemUI/res/drawable/accessibility_window_magnification_drag_handle_background_inset.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    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.
+-->
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+    android:drawable="@drawable/accessibility_window_magnification_drag_handle_background"
+    android:insetBottom="@dimen/magnification_inner_border_margin"
+    android:insetLeft="@dimen/magnification_inner_border_margin"
+    android:insetRight="@dimen/magnification_inner_border_margin"
+    android:insetTop="@dimen/magnification_inner_border_margin" />
diff --git a/packages/SystemUI/res/layout/window_magnifier_view.xml b/packages/SystemUI/res/layout/window_magnifier_view.xml
index a8a048d..6286f34 100644
--- a/packages/SystemUI/res/layout/window_magnifier_view.xml
+++ b/packages/SystemUI/res/layout/window_magnifier_view.xml
@@ -117,12 +117,11 @@
         android:id="@+id/drag_handle"
         android:layout_width="@dimen/magnification_drag_view_size"
         android:layout_height="@dimen/magnification_drag_view_size"
-        android:layout_margin="@dimen/magnification_inner_border_margin"
         android:layout_gravity="right|bottom"
         android:padding="@dimen/magnifier_drag_handle_padding"
         android:scaleType="center"
         android:src="@drawable/ic_move_magnification"
-        android:background="@drawable/accessibility_window_magnification_drag_handle_background"/>
+        android:background="@drawable/accessibility_window_magnification_drag_handle_background_inset"/>
 
     <ImageView
         android:id="@+id/close_button"
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 17719d1..7a83070 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -991,7 +991,7 @@
 
     <!-- Component name for Home Panel Dream -->
     <string name="config_homePanelDreamComponent" translatable="false">
-        @null
+        com.android.systemui/com.android.systemui.dreams.homecontrols.HomeControlsDreamService
     </string>
 
     <!--
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 4209c1f..fa89fcd 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1249,7 +1249,7 @@
     <dimen name="magnification_drag_corner_margin">8dp</dimen>
     <dimen name="magnification_frame_move_short">5dp</dimen>
     <dimen name="magnification_frame_move_long">25dp</dimen>
-    <dimen name="magnification_drag_view_size">36dp</dimen>
+    <dimen name="magnification_drag_view_size">70dp</dimen>
     <dimen name="magnification_controls_size">90dp</dimen>
     <dimen name="magnification_switch_button_size">56dp</dimen>
     <dimen name="magnification_switch_button_padding">6dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 7943588..8971859 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -3314,4 +3314,8 @@
     <string name="keyboard_backlight_dialog_title">Keyboard backlight</string>
     <!-- Content description for keyboard backlight brightness value [CHAR LIMIT=NONE] -->
     <string name="keyboard_backlight_value">Level %1$d of %2$d</string>
+    <!-- Label for home control panel [CHAR LIMIT=30] -->
+    <string name="home_controls_dream_label">Home Controls</string>
+    <!-- Description for home control panel [CHAR LIMIT=50] -->
+    <string name="home_controls_dream_description">Quickly access your home controls as a screensaver</string>
 </resources>
diff --git a/packages/SystemUI/res/xml/home_controls_dream_metadata.xml b/packages/SystemUI/res/xml/home_controls_dream_metadata.xml
new file mode 100644
index 0000000..eb7c79e
--- /dev/null
+++ b/packages/SystemUI/res/xml/home_controls_dream_metadata.xml
@@ -0,0 +1,19 @@
+<!--
+  ~ 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.
+  -->
+<dream xmlns:android="http://schemas.android.com/apk/res/android"
+       android:showClockAndComplications="false"
+       android:previewImage="@drawable/homecontrols_sq"
+    />
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
index bddf3b0..d2ad096 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
@@ -97,6 +97,26 @@
         )
     }
 
+    fun logUpdateLockScreenUserLockedMsg(
+        userId: Int,
+        userUnlocked: Boolean,
+        encryptedOrLockdown: Boolean,
+    ) {
+        buffer.log(
+            KeyguardIndicationController.TAG,
+            LogLevel.DEBUG,
+            {
+                int1 = userId
+                bool1 = userUnlocked
+                bool2 = encryptedOrLockdown
+            },
+            {
+                "updateLockScreenUserLockedMsg userId=$int1 " +
+                    "userUnlocked:$bool1 encryptedOrLockdown:$bool2"
+            }
+        )
+    }
+
     fun logUpdateBatteryIndication(
         powerIndication: String,
         pluggedIn: Boolean,
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
index 33728ac..d65cd5c 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
@@ -1698,8 +1698,8 @@
         mSettingsPanelVisibility = settingsPanelIsShown;
 
         mDragView.setBackground(mContext.getResources().getDrawable(settingsPanelIsShown
-                ? R.drawable.accessibility_window_magnification_drag_handle_background_change
-                : R.drawable.accessibility_window_magnification_drag_handle_background));
+                ? R.drawable.accessibility_window_magnification_drag_handle_background_change_inset
+                : R.drawable.accessibility_window_magnification_drag_handle_background_inset));
 
         PorterDuffColorFilter filter = new PorterDuffColorFilter(
                 mContext.getColor(settingsPanelIsShown
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
index 49f34f1..454ed27 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
@@ -39,7 +39,7 @@
 import com.android.systemui.scene.shared.flag.SceneContainerFlags
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
 import com.android.systemui.user.data.repository.UserRepository
-import com.android.systemui.util.kotlin.pairwise
+import com.android.systemui.util.kotlin.onSubscriberAdded
 import com.android.systemui.util.time.SystemClock
 import dagger.Binds
 import dagger.Module
@@ -54,7 +54,6 @@
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onStart
@@ -355,10 +354,7 @@
                     userRepository.selectedUserInfo.map { it.id }.distinctUntilChanged(),
                     // Emits a value only when the number of downstream subscribers of this flow
                     // increases.
-                    flow.subscriptionCount.pairwise(initialValue = 0).filter { (previous, current)
-                        ->
-                        current > previous
-                    },
+                    flow.onSubscriberAdded(),
                 ) { selectedUserId, _ ->
                     selectedUserId
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt
index 7265c0c..d849b3a 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt
@@ -21,8 +21,6 @@
 import com.android.systemui.flags.Flags
 import javax.inject.Inject
 import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
 
 /** Provides access to bouncer-related application state. */
 @SysUISingleton
@@ -31,15 +29,10 @@
 constructor(
     private val flags: FeatureFlagsClassic,
 ) {
-    private val _message = MutableStateFlow<String?>(null)
     /** The user-facing message to show in the bouncer. */
-    val message: StateFlow<String?> = _message.asStateFlow()
+    val message = MutableStateFlow<String?>(null)
 
     /** Whether the user switcher should be displayed within the bouncer UI on large screens. */
     val isUserSwitcherVisible: Boolean
         get() = flags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)
-
-    fun setMessage(message: String?) {
-        _message.value = message
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
index c8ce245..d8be1af 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
@@ -120,7 +120,7 @@
     }
 
     fun setMessage(message: String?) {
-        repository.setMessage(message)
+        repository.message.value = message
     }
 
     /**
@@ -129,13 +129,13 @@
      */
     fun resetMessage() {
         applicationScope.launch {
-            repository.setMessage(promptMessage(authenticationInteractor.getAuthenticationMethod()))
+            setMessage(promptMessage(authenticationInteractor.getAuthenticationMethod()))
         }
     }
 
     /** Removes the user-facing message. */
     fun clearMessage() {
-        repository.setMessage(null)
+        setMessage(null)
     }
 
     /**
@@ -196,7 +196,7 @@
      * message without having the attempt trigger lockout.
      */
     private suspend fun showWrongInputMessage() {
-        repository.setMessage(wrongInputMessage(authenticationInteractor.getAuthenticationMethod()))
+        setMessage(wrongInputMessage(authenticationInteractor.getAuthenticationMethod()))
     }
 
     /** Notifies that the input method editor (software keyboard) has been hidden by the user. */
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
index 4d686a1..4466cbb 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
@@ -34,7 +34,9 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.inputmethod.domain.interactor.InputMethodInteractor
 import com.android.systemui.scene.shared.flag.SceneContainerFlags
+import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import com.android.systemui.user.ui.viewmodel.UserActionViewModel
 import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
 import com.android.systemui.user.ui.viewmodel.UserViewModel
@@ -66,8 +68,10 @@
     @Application private val applicationScope: CoroutineScope,
     @Main private val mainDispatcher: CoroutineDispatcher,
     private val bouncerInteractor: BouncerInteractor,
+    private val inputMethodInteractor: InputMethodInteractor,
     private val simBouncerInteractor: SimBouncerInteractor,
     private val authenticationInteractor: AuthenticationInteractor,
+    private val selectedUserInteractor: SelectedUserInteractor,
     flags: SceneContainerFlags,
     selectedUser: Flow<UserViewModel>,
     users: Flow<List<UserViewModel>>,
@@ -346,8 +350,10 @@
             is AuthenticationMethodModel.Password ->
                 PasswordBouncerViewModel(
                     viewModelScope = newViewModelScope,
-                    interactor = bouncerInteractor,
                     isInputEnabled = isInputEnabled,
+                    interactor = bouncerInteractor,
+                    inputMethodInteractor = inputMethodInteractor,
+                    selectedUserInteractor = selectedUserInteractor,
                 )
             is AuthenticationMethodModel.Pattern ->
                 PatternBouncerViewModel(
@@ -467,11 +473,13 @@
         @Application applicationScope: CoroutineScope,
         @Main mainDispatcher: CoroutineDispatcher,
         bouncerInteractor: BouncerInteractor,
+        imeInteractor: InputMethodInteractor,
         simBouncerInteractor: SimBouncerInteractor,
+        actionButtonInteractor: BouncerActionButtonInteractor,
         authenticationInteractor: AuthenticationInteractor,
+        selectedUserInteractor: SelectedUserInteractor,
         flags: SceneContainerFlags,
         userSwitcherViewModel: UserSwitcherViewModel,
-        actionButtonInteractor: BouncerActionButtonInteractor,
         clock: SystemClock,
         devicePolicyManager: DevicePolicyManager,
     ): BouncerViewModel {
@@ -480,8 +488,10 @@
             applicationScope = applicationScope,
             mainDispatcher = mainDispatcher,
             bouncerInteractor = bouncerInteractor,
+            inputMethodInteractor = imeInteractor,
             simBouncerInteractor = simBouncerInteractor,
             authenticationInteractor = authenticationInteractor,
+            selectedUserInteractor = selectedUserInteractor,
             flags = flags,
             selectedUser = userSwitcherViewModel.selectedUser,
             users = userSwitcherViewModel.users,
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
index 5c9c997..1c8b84d 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
@@ -16,23 +16,32 @@
 
 package com.android.systemui.bouncer.ui.viewmodel
 
+import androidx.annotation.VisibleForTesting
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.inputmethod.domain.interactor.InputMethodInteractor
 import com.android.systemui.res.R
+import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import com.android.systemui.util.kotlin.onSubscriberAdded
+import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
 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.onEach
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
 
 /** Holds UI state and handles user input for the password bouncer UI. */
 class PasswordBouncerViewModel(
     viewModelScope: CoroutineScope,
-    interactor: BouncerInteractor,
     isInputEnabled: StateFlow<Boolean>,
+    interactor: BouncerInteractor,
+    private val inputMethodInteractor: InputMethodInteractor,
+    private val selectedUserInteractor: SelectedUserInteractor,
 ) :
     AuthMethodBouncerViewModel(
         viewModelScope = viewModelScope,
@@ -49,6 +58,9 @@
 
     override val lockoutMessageId = R.string.kg_too_many_failed_password_attempts_dialog_message
 
+    /** Informs the UI whether the input method switcher button should be visible. */
+    val isImeSwitcherButtonVisible: StateFlow<Boolean> = imeSwitcherRefreshingFlow()
+
     /** Whether the text field element currently has focus. */
     private val isTextFieldFocused = MutableStateFlow(false)
 
@@ -87,6 +99,13 @@
         _password.value = newPassword
     }
 
+    /** Notifies that the user clicked the button to change the input method. */
+    fun onImeSwitcherButtonClicked(displayId: Int) {
+        viewModelScope.launch {
+            inputMethodInteractor.showInputMethodPicker(displayId, showAuxiliarySubtypes = false)
+        }
+    }
+
     /** Notifies that the user has pressed the key for attempting to authenticate the password. */
     fun onAuthenticateKeyPressed() {
         if (_password.value.isNotEmpty()) {
@@ -103,4 +122,35 @@
     fun onTextFieldFocusChanged(isFocused: Boolean) {
         isTextFieldFocused.value = isFocused
     }
+
+    /**
+     * Whether the input method switcher button should be displayed in the password bouncer UI. The
+     * value may be stale at the moment of subscription to this flow, but it is guaranteed to be
+     * shortly updated with a fresh value.
+     *
+     * Note: Each added subscription triggers an IPC call in the background, so this should only be
+     * subscribed to by the UI once in its lifecycle (i.e. when the bouncer is shown).
+     */
+    private fun imeSwitcherRefreshingFlow(): StateFlow<Boolean> {
+        val isImeSwitcherButtonVisible = MutableStateFlow(value = false)
+        viewModelScope.launch {
+            // Re-fetch the currently-enabled IMEs whenever the selected user changes, and whenever
+            // the UI subscribes to the `isImeSwitcherButtonVisible` flow.
+            combine(
+                    // InputMethodManagerService sometimes takes some time to update its internal
+                    // state when the selected user changes. As a workaround, delay fetching the IME
+                    // info.
+                    selectedUserInteractor.selectedUser.onEach { delay(DELAY_TO_FETCH_IMES) },
+                    isImeSwitcherButtonVisible.onSubscriberAdded()
+                ) { selectedUserId, _ ->
+                    inputMethodInteractor.hasMultipleEnabledImesOrSubtypes(selectedUserId)
+                }
+                .collect { isImeSwitcherButtonVisible.value = it }
+        }
+        return isImeSwitcherButtonVisible.asStateFlow()
+    }
+
+    companion object {
+        @VisibleForTesting val DELAY_TO_FETCH_IMES = 300.milliseconds
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
new file mode 100644
index 0000000..f7ba5a4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
@@ -0,0 +1,110 @@
+/*
+ * 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.communal
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.communal.domain.interactor.CommunalInteractor
+import com.android.systemui.communal.shared.model.CommunalSceneKey
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dock.DockManager
+import com.android.systemui.dock.retrieveIsDocked
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.util.kotlin.sample
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.onEach
+
+/**
+ * A [CoreStartable] responsible for automatically navigating between communal scenes when certain
+ * conditions are met.
+ */
+@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
+@SysUISingleton
+class CommunalSceneStartable
+@Inject
+constructor(
+    private val dockManager: DockManager,
+    private val communalInteractor: CommunalInteractor,
+    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+    @Application private val applicationScope: CoroutineScope,
+    @Background private val bgScope: CoroutineScope,
+) : CoreStartable {
+    override fun start() {
+        // Handle automatically switching based on keyguard state.
+        keyguardTransitionInteractor.startedKeyguardTransitionStep
+            .mapLatest(::determineSceneAfterTransition)
+            .filterNotNull()
+            // TODO(b/322787129): Also set a custom transition animation here to avoid the regular
+            // slide-in animation when setting the scene programmatically
+            .onEach { nextScene -> communalInteractor.onSceneChanged(nextScene) }
+            .launchIn(applicationScope)
+
+        // Handle automatically switching to communal when docked.
+        dockManager
+            .retrieveIsDocked()
+            // Allow some time after docking to ensure the dream doesn't start. If the dream
+            // starts, then we don't want to automatically transition to glanceable hub.
+            .debounce(DOCK_DEBOUNCE_DELAY)
+            .sample(keyguardTransitionInteractor.startedKeyguardState, ::Pair)
+            .onEach { (docked, lastStartedState) ->
+                if (docked && lastStartedState == KeyguardState.LOCKSCREEN) {
+                    communalInteractor.onSceneChanged(CommunalSceneKey.Communal)
+                }
+            }
+            .launchIn(bgScope)
+    }
+
+    private suspend fun determineSceneAfterTransition(
+        lastStartedTransition: TransitionStep,
+    ): CommunalSceneKey? {
+        val to = lastStartedTransition.to
+        val from = lastStartedTransition.from
+        val docked = dockManager.isDocked
+
+        return when {
+            to == KeyguardState.DREAMING -> CommunalSceneKey.Blank
+            docked && to == KeyguardState.LOCKSCREEN && from != KeyguardState.GLANCEABLE_HUB -> {
+                CommunalSceneKey.Communal
+            }
+            to == KeyguardState.GONE -> CommunalSceneKey.Blank
+            !docked && !KeyguardState.deviceIsAwakeInState(to) -> {
+                // If the user taps the screen and wakes the device within this timeout, we don't
+                // want to dismiss the hub
+                delay(AWAKE_DEBOUNCE_DELAY)
+                CommunalSceneKey.Blank
+            }
+            else -> null
+        }
+    }
+
+    companion object {
+        val AWAKE_DEBOUNCE_DELAY = 5.seconds
+        val DOCK_DEBOUNCE_DELAY = 1.seconds
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt
index ab0a2d0d..d7f163b 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt
@@ -26,6 +26,7 @@
 import com.android.systemui.communal.widgets.WidgetInteractionHandler
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.dagger.CommunalLog
@@ -35,6 +36,7 @@
 import dagger.Provides
 import java.util.Optional
 import javax.inject.Named
+import kotlinx.coroutines.CoroutineScope
 
 @Module
 interface CommunalWidgetRepositoryModule {
@@ -52,10 +54,19 @@
         @Provides
         fun provideCommunalAppWidgetHost(
             @Application context: Context,
+            @Background backgroundScope: CoroutineScope,
             interactionHandler: WidgetInteractionHandler,
             @Main looper: Looper,
+            @CommunalLog logBuffer: LogBuffer,
         ): CommunalAppWidgetHost {
-            return CommunalAppWidgetHost(context, APP_WIDGET_HOST_ID, interactionHandler, looper)
+            return CommunalAppWidgetHost(
+                context,
+                backgroundScope,
+                APP_WIDGET_HOST_ID,
+                interactionHandler,
+                looper,
+                logBuffer,
+            )
         }
 
         @SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt
index 61db026..5f1d89e 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt
@@ -22,14 +22,31 @@
 import android.content.Context
 import android.os.Looper
 import android.widget.RemoteViews
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.Logger
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.launch
 
 /** Communal app widget host that creates a [CommunalAppWidgetHostView]. */
 class CommunalAppWidgetHost(
     context: Context,
+    private val backgroundScope: CoroutineScope,
     hostId: Int,
     interactionHandler: RemoteViews.InteractionHandler,
-    looper: Looper
+    looper: Looper,
+    logBuffer: LogBuffer,
 ) : AppWidgetHost(context, hostId, interactionHandler, looper) {
+
+    private val logger = Logger(logBuffer, TAG)
+
+    private val _appWidgetIdToRemove = MutableSharedFlow<Int>()
+
+    /** App widget ids that have been removed and no longer available. */
+    val appWidgetIdToRemove: SharedFlow<Int> = _appWidgetIdToRemove.asSharedFlow()
+
     override fun onCreateView(
         context: Context,
         appWidgetId: Int,
@@ -52,4 +69,15 @@
         // `createView`, but we are sure that the hostView is `CommunalAppWidgetHostView`
         return createView(context, appWidgetId, appWidget) as CommunalAppWidgetHostView
     }
+
+    override fun onAppWidgetRemoved(appWidgetId: Int) {
+        backgroundScope.launch {
+            logger.i({ "App widget removed from system: $int1" }) { int1 = appWidgetId }
+            _appWidgetIdToRemove.emit(appWidgetId)
+        }
+    }
+
+    companion object {
+        private const val TAG = "CommunalAppWidgetHost"
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt
index 586df32..fb9abeb 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt
@@ -40,6 +40,7 @@
     @Background private val bgScope: CoroutineScope,
     @Main private val uiDispatcher: CoroutineDispatcher
 ) : CoreStartable {
+
     override fun start() {
         or(communalInteractor.isCommunalAvailable, communalInteractor.editModeOpen)
             // Only trigger updates on state changes, ignoring the initial false value.
@@ -47,6 +48,10 @@
             .filter { (previous, new) -> previous != new }
             .onEach { (_, shouldListen) -> updateAppWidgetHostActive(shouldListen) }
             .launchIn(bgScope)
+
+        appWidgetHost.appWidgetIdToRemove
+            .onEach { appWidgetId -> communalInteractor.deleteWidget(appWidgetId) }
+            .launchIn(bgScope)
     }
 
     private suspend fun updateAppWidgetHostActive(active: Boolean) =
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
index ad1327e..54c709d 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
@@ -29,6 +29,7 @@
 import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
 import com.android.internal.logging.UiEventLogger
 import com.android.systemui.communal.shared.log.CommunalUiEvent
+import com.android.systemui.communal.shared.model.CommunalSceneKey
 import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
 import com.android.systemui.compose.ComposeFacade.setCommunalEditWidgetActivityContent
 import javax.inject.Inject
@@ -126,6 +127,7 @@
             },
             onEditDone = {
                 try {
+                    communalViewModel.onSceneChanged(CommunalSceneKey.Communal)
                     checkNotNull(windowManagerService).lockNow(/* options */ null)
                     finish()
                 } catch (e: RemoteException) {
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index 95233f7..5ee2045 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -24,12 +24,14 @@
 import com.android.systemui.back.domain.interactor.BackActionInteractor
 import com.android.systemui.biometrics.BiometricNotificationService
 import com.android.systemui.clipboardoverlay.ClipboardListener
+import com.android.systemui.communal.CommunalSceneStartable
 import com.android.systemui.communal.log.CommunalLoggerStartable
 import com.android.systemui.communal.widgets.CommunalAppWidgetHostStartable
 import com.android.systemui.controls.dagger.StartControlsStartableModule
 import com.android.systemui.dagger.qualifiers.PerUser
 import com.android.systemui.dreams.AssistantAttentionMonitor
 import com.android.systemui.dreams.DreamMonitor
+import com.android.systemui.dreams.homecontrols.HomeControlsDreamStartable
 import com.android.systemui.globalactions.GlobalActionsComponent
 import com.android.systemui.keyboard.KeyboardUI
 import com.android.systemui.keyboard.PhysicalKeyboardCoreStartable
@@ -50,7 +52,6 @@
 import com.android.systemui.statusbar.ImmersiveModeConfirmation
 import com.android.systemui.statusbar.gesture.GesturePointerEventListener
 import com.android.systemui.statusbar.notification.InstantAppNotifier
-import com.android.systemui.statusbar.phone.KeyguardLiftController
 import com.android.systemui.statusbar.phone.ScrimController
 import com.android.systemui.statusbar.phone.StatusBarHeadsUpChangeListener
 import com.android.systemui.stylus.StylusUsiPowerStartable
@@ -224,12 +225,6 @@
     @ClassKey(WMShell::class)
     abstract fun bindWMShell(sysui: WMShell): CoreStartable
 
-    /** Inject into KeyguardLiftController.  */
-    @Binds
-    @IntoMap
-    @ClassKey(KeyguardLiftController::class)
-    abstract fun bindKeyguardLiftController(sysui: KeyguardLiftController): CoreStartable
-
     /** Inject into MediaTttSenderCoordinator. */
     @Binds
     @IntoMap
@@ -328,8 +323,18 @@
 
     @Binds
     @IntoMap
+    @ClassKey(CommunalSceneStartable::class)
+    abstract fun bindCommunalSceneStartable(impl: CommunalSceneStartable): CoreStartable
+
+    @Binds
+    @IntoMap
     @ClassKey(CommunalAppWidgetHostStartable::class)
     abstract fun bindCommunalAppWidgetHostStartable(
         impl: CommunalAppWidgetHostStartable
     ): CoreStartable
+
+    @Binds
+    @IntoMap
+    @ClassKey(HomeControlsDreamStartable::class)
+    abstract fun bindHomeControlsDreamStartable(impl: HomeControlsDreamStartable): CoreStartable
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 2587e2d..efcbd47 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -57,6 +57,7 @@
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.FlagDependenciesModule;
 import com.android.systemui.flags.FlagsModule;
+import com.android.systemui.inputmethod.InputMethodModule;
 import com.android.systemui.keyboard.KeyboardModule;
 import com.android.systemui.keyevent.data.repository.KeyEventRepositoryModule;
 import com.android.systemui.keyguard.ui.view.layout.blueprints.KeyguardBlueprintModule;
@@ -193,6 +194,7 @@
         FlagsModule.class,
         FlagDependenciesModule.class,
         FooterActionsModule.class,
+        InputMethodModule.class,
         KeyEventRepositoryModule.class,
         KeyboardModule.class,
         KeyguardBlueprintModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/binder/LiftToRunFaceAuthBinder.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/binder/LiftToRunFaceAuthBinder.kt
new file mode 100644
index 0000000..1fd7d00
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/binder/LiftToRunFaceAuthBinder.kt
@@ -0,0 +1,143 @@
+/*
+ * 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.deviceentry.ui.binder
+
+import android.content.pm.PackageManager
+import android.hardware.Sensor
+import android.hardware.TriggerEvent
+import android.hardware.TriggerEventListener
+import com.android.keyguard.ActiveUnlockConfig
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.CoreStartable
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.util.Assert
+import com.android.systemui.util.sensors.AsyncSensorManager
+import java.io.PrintWriter
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filterNot
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+
+/**
+ * Triggers face auth and active unlock on lift when the device is showing the lock screen or
+ * bouncer. Only initialized if face auth is supported on the device. Not to be confused with the
+ * lift to wake gesture which is handled by {@link com.android.server.policy.PhoneWindowManager}.
+ */
+@SysUISingleton
+class LiftToRunFaceAuthBinder
+@Inject
+constructor(
+    @Application private val scope: CoroutineScope,
+    private val packageManager: PackageManager,
+    private val asyncSensorManager: AsyncSensorManager,
+    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+    keyguardInteractor: KeyguardInteractor,
+    primaryBouncerInteractor: PrimaryBouncerInteractor,
+    alternateBouncerInteractor: AlternateBouncerInteractor,
+    private val deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor,
+    powerInteractor: PowerInteractor,
+) : CoreStartable {
+
+    private var pickupSensor: Sensor? = null
+    private val isListening: MutableStateFlow<Boolean> = MutableStateFlow(false)
+    private val stoppedListening: Flow<Unit> = isListening.filterNot { it }.map {} // map to Unit
+
+    private val onAwakeKeyguard: Flow<Boolean> =
+        combine(
+            powerInteractor.isInteractive,
+            keyguardInteractor.isKeyguardVisible,
+        ) { isInteractive, isKeyguardVisible ->
+            isInteractive && isKeyguardVisible
+        }
+    private val bouncerShowing: Flow<Boolean> =
+        combine(
+            primaryBouncerInteractor.isShowing,
+            alternateBouncerInteractor.isVisible,
+        ) { primaryBouncerShowing, alternateBouncerShowing ->
+            primaryBouncerShowing || alternateBouncerShowing
+        }
+    private val listenForPickupSensor: Flow<Boolean> =
+        combine(
+            stoppedListening,
+            bouncerShowing,
+            onAwakeKeyguard,
+        ) { _, bouncerShowing, onAwakeKeyguard ->
+            (onAwakeKeyguard || bouncerShowing) &&
+                deviceEntryFaceAuthInteractor.isFaceAuthEnabledAndEnrolled()
+        }
+
+    override fun start() {
+        if (packageManager.hasSystemFeature(PackageManager.FEATURE_FACE)) {
+            init()
+        }
+    }
+
+    private fun init() {
+        pickupSensor = asyncSensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE)
+        scope.launch {
+            listenForPickupSensor.collect { listenForPickupSensor ->
+                updateListeningState(listenForPickupSensor)
+            }
+        }
+    }
+
+    private val listener: TriggerEventListener =
+        object : TriggerEventListener() {
+            override fun onTrigger(event: TriggerEvent?) {
+                Assert.isMainThread()
+                deviceEntryFaceAuthInteractor.onDeviceLifted()
+                keyguardUpdateMonitor.requestActiveUnlock(
+                    ActiveUnlockConfig.ActiveUnlockRequestOrigin.WAKE,
+                    "KeyguardLiftController"
+                )
+
+                // Not listening anymore since trigger events unregister themselves
+                isListening.value = false
+            }
+        }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        pw.println("LiftToRunFaceAuthBinder:")
+        pw.println("  pickupSensor: $pickupSensor")
+        pw.println("  isListening: ${isListening.value}")
+    }
+
+    private fun updateListeningState(shouldListen: Boolean) {
+        if (pickupSensor == null) {
+            return
+        }
+        if (shouldListen != isListening.value) {
+            isListening.value = shouldListen
+
+            if (shouldListen) {
+                asyncSensorManager.requestTriggerSensor(listener, pickupSensor)
+            } else {
+                asyncSensorManager.cancelTriggerSensor(listener, pickupSensor)
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
index 0656933..ba74742 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
@@ -17,6 +17,7 @@
 package com.android.systemui.dreams.dagger;
 
 import android.annotation.Nullable;
+import android.app.Service;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.PackageManager;
@@ -30,12 +31,18 @@
 import com.android.systemui.dreams.DreamOverlayNotificationCountProvider;
 import com.android.systemui.dreams.DreamOverlayService;
 import com.android.systemui.dreams.complication.dagger.ComplicationComponent;
+import com.android.systemui.dreams.homecontrols.DreamActivityProvider;
+import com.android.systemui.dreams.homecontrols.DreamActivityProviderImpl;
+import com.android.systemui.dreams.homecontrols.HomeControlsDreamService;
 import com.android.systemui.dreams.touch.scrim.dagger.ScrimModule;
 import com.android.systemui.res.R;
 import com.android.systemui.touch.TouchInsetManager;
 
+import dagger.Binds;
 import dagger.Module;
 import dagger.Provides;
+import dagger.multibindings.ClassKey;
+import dagger.multibindings.IntoMap;
 
 import java.util.Optional;
 import java.util.concurrent.Executor;
@@ -88,6 +95,15 @@
     }
 
     /**
+     * Provides Home Controls Dream Service
+     */
+    @Binds
+    @IntoMap
+    @ClassKey(HomeControlsDreamService.class)
+    Service bindHomeControlsDreamService(
+            HomeControlsDreamService service);
+
+    /**
      * Provides a touch inset manager for dreams.
      */
     @Provides
@@ -151,4 +167,9 @@
     static String providesDreamOverlayWindowTitle(@Main Resources resources) {
         return resources.getString(R.string.app_label);
     }
+
+    /** Provides activity for dream service */
+    @Binds
+    DreamActivityProvider bindActivityProvider(DreamActivityProviderImpl impl);
+
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProvider.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProvider.kt
new file mode 100644
index 0000000..b35b7f5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProvider.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.dreams.homecontrols
+
+import android.app.Activity
+import android.service.dreams.DreamService
+
+fun interface DreamActivityProvider {
+    /**
+     * Provides abstraction for getting the activity associated with a dream service, so that the
+     * activity can be mocked in tests.
+     */
+    fun getActivity(dreamService: DreamService): Activity?
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProviderImpl.kt
new file mode 100644
index 0000000..0854e93
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProviderImpl.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.dreams.homecontrols
+
+import android.app.Activity
+import android.service.dreams.DreamService
+import javax.inject.Inject
+
+class DreamActivityProviderImpl @Inject constructor() : DreamActivityProvider {
+    override fun getActivity(dreamService: DreamService): Activity {
+        return dreamService.activity
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt
new file mode 100644
index 0000000..e04a505
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt
@@ -0,0 +1,88 @@
+/*
+ * 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.dreams.homecontrols
+
+import android.content.Intent
+import android.service.controls.ControlsProviderService
+import android.service.dreams.DreamService
+import android.window.TaskFragmentInfo
+import com.android.systemui.controls.settings.ControlsSettingsRepository
+import com.android.systemui.dreams.DreamLogger
+import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.dagger.DreamLog
+import javax.inject.Inject
+
+class HomeControlsDreamService
+@Inject
+constructor(
+    private val controlsSettingsRepository: ControlsSettingsRepository,
+    private val taskFragmentFactory: TaskFragmentComponent.Factory,
+    private val homeControlsComponentInteractor: HomeControlsComponentInteractor,
+    private val dreamActivityProvider: DreamActivityProvider,
+    @DreamLog logBuffer: LogBuffer
+) : DreamService() {
+    private lateinit var taskFragmentComponent: TaskFragmentComponent
+
+    private val logger = DreamLogger(logBuffer, "HomeControlsDreamService")
+
+    override fun onAttachedToWindow() {
+        super.onAttachedToWindow()
+        val activity = dreamActivityProvider.getActivity(this)
+        if (activity == null) {
+            finish()
+            return
+        }
+        taskFragmentComponent =
+            taskFragmentFactory
+                .create(
+                    activity = activity,
+                    onCreateCallback = this::onTaskFragmentCreated,
+                    onInfoChangedCallback = this::onTaskFragmentInfoChanged,
+                    hide = { finish() }
+                )
+                .apply { createTaskFragment() }
+    }
+
+    private fun onTaskFragmentInfoChanged(taskFragmentInfo: TaskFragmentInfo) {
+        if (taskFragmentInfo.isEmpty) {
+            logger.d("Finishing dream due to TaskFragment being empty")
+            finish()
+        }
+    }
+
+    private fun onTaskFragmentCreated(taskFragmentInfo: TaskFragmentInfo) {
+        val setting = controlsSettingsRepository.allowActionOnTrivialControlsInLockscreen.value
+        val componentName = homeControlsComponentInteractor.panelComponent.value
+        logger.d("Starting embedding $componentName")
+        val intent =
+            Intent().apply {
+                component = componentName
+                putExtra(ControlsProviderService.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS, setting)
+                putExtra(
+                    ControlsProviderService.EXTRA_CONTROLS_SURFACE,
+                    ControlsProviderService.CONTROLS_SURFACE_DREAM
+                )
+            }
+        taskFragmentComponent.startActivityInTaskFragment(intent)
+    }
+
+    override fun onDetachedFromWindow() {
+        super.onDetachedFromWindow()
+        taskFragmentComponent.destroy()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartable.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartable.kt
new file mode 100644
index 0000000..6cd94c6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartable.kt
@@ -0,0 +1,63 @@
+/*
+ * 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.dreams.homecontrols
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.PackageManager
+import android.service.controls.flags.Flags.homePanelDream
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+class HomeControlsDreamStartable
+@Inject
+constructor(
+    private val context: Context,
+    private val packageManager: PackageManager,
+    private val homeControlsComponentInteractor: HomeControlsComponentInteractor,
+    @Background private val bgScope: CoroutineScope,
+) : CoreStartable {
+
+    private val componentName = ComponentName(context, HomeControlsDreamService::class.java)
+
+    override fun start() {
+        if (!homePanelDream()) return
+        bgScope.launch {
+            homeControlsComponentInteractor.panelComponent.collect { selectedPanelComponent ->
+                setEnableHomeControlPanel(selectedPanelComponent != null)
+            }
+        }
+    }
+
+    private fun setEnableHomeControlPanel(enabled: Boolean) {
+        val packageState =
+            if (enabled) {
+                PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+            } else {
+                PackageManager.COMPONENT_ENABLED_STATE_DISABLED
+            }
+        packageManager.setComponentEnabledSetting(
+            componentName,
+            packageState,
+            PackageManager.DONT_KILL_APP
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.kt
new file mode 100644
index 0000000..6f7dcb1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.kt
@@ -0,0 +1,166 @@
+/*
+ * 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.dreams.homecontrols
+
+import android.app.Activity
+import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
+import android.content.Intent
+import android.graphics.Rect
+import android.os.Binder
+import android.window.TaskFragmentCreationParams
+import android.window.TaskFragmentInfo
+import android.window.TaskFragmentOperation
+import android.window.TaskFragmentOperation.OP_TYPE_DELETE_TASK_FRAGMENT
+import android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_TOP_OF_TASK
+import android.window.TaskFragmentOrganizer
+import android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CHANGE
+import android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CLOSE
+import android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_OPEN
+import android.window.TaskFragmentTransaction
+import android.window.TaskFragmentTransaction.TYPE_ACTIVITY_REPARENTED_TO_TASK
+import android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_APPEARED
+import android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_ERROR
+import android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_INFO_CHANGED
+import android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED
+import android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_VANISHED
+import android.window.WindowContainerTransaction
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.util.concurrency.DelayableExecutor
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+typealias FragmentInfoCallback = (TaskFragmentInfo) -> Unit
+
+/** Wrapper around TaskFragmentOrganizer for managing a task fragment within an activity */
+class TaskFragmentComponent
+@AssistedInject
+constructor(
+    @Assisted private val activity: Activity,
+    @Assisted("onCreateCallback") private val onCreateCallback: FragmentInfoCallback,
+    @Assisted("onInfoChangedCallback") private val onInfoChangedCallback: FragmentInfoCallback,
+    @Assisted private val hide: () -> Unit,
+    @Main private val executor: DelayableExecutor,
+) {
+
+    @AssistedFactory
+    fun interface Factory {
+        fun create(
+            activity: Activity,
+            @Assisted("onCreateCallback") onCreateCallback: FragmentInfoCallback,
+            @Assisted("onInfoChangedCallback") onInfoChangedCallback: FragmentInfoCallback,
+            hide: () -> Unit
+        ): TaskFragmentComponent
+    }
+
+    private val fragmentToken = Binder()
+    private val organizer: TaskFragmentOrganizer =
+        object : TaskFragmentOrganizer(executor) {
+
+                override fun onTransactionReady(transaction: TaskFragmentTransaction) {
+                    handleTransactionReady(transaction)
+                }
+            }
+            .apply { registerOrganizer(true /* isSystemOrganizer */) }
+
+    private fun handleTransactionReady(transaction: TaskFragmentTransaction) {
+        val resultT = WindowContainerTransaction()
+
+        for (change in transaction.changes) {
+            change.taskFragmentInfo?.let { taskFragmentInfo ->
+                if (taskFragmentInfo.fragmentToken == fragmentToken) {
+                    when (change.type) {
+                        TYPE_TASK_FRAGMENT_APPEARED -> {
+                            resultT.addTaskFragmentOperation(
+                                fragmentToken,
+                                TaskFragmentOperation.Builder(OP_TYPE_REORDER_TO_TOP_OF_TASK)
+                                    .build()
+                            )
+
+                            onCreateCallback(taskFragmentInfo)
+                        }
+                        TYPE_TASK_FRAGMENT_INFO_CHANGED -> {
+                            onInfoChangedCallback(taskFragmentInfo)
+                        }
+                        TYPE_TASK_FRAGMENT_VANISHED -> {
+                            hide()
+                        }
+                        TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED -> {}
+                        TYPE_TASK_FRAGMENT_ERROR -> {
+                            hide()
+                        }
+                        TYPE_ACTIVITY_REPARENTED_TO_TASK -> {}
+                        else ->
+                            throw IllegalArgumentException(
+                                "Unknown TaskFragmentEvent=" + change.type
+                            )
+                    }
+                }
+            }
+        }
+        organizer.onTransactionHandled(
+            transaction.transactionToken,
+            resultT,
+            TASK_FRAGMENT_TRANSIT_CHANGE,
+            false
+        )
+    }
+
+    /** Creates the task fragment */
+    fun createTaskFragment() {
+        val taskBounds = Rect(activity.resources.configuration.windowConfiguration.bounds)
+        val fragmentOptions =
+            TaskFragmentCreationParams.Builder(
+                    organizer.organizerToken,
+                    fragmentToken,
+                    activity.activityToken!!
+                )
+                .setInitialRelativeBounds(taskBounds)
+                .setWindowingMode(WINDOWING_MODE_MULTI_WINDOW)
+                .build()
+        organizer.applyTransaction(
+            WindowContainerTransaction().createTaskFragment(fragmentOptions),
+            TASK_FRAGMENT_TRANSIT_CHANGE,
+            false
+        )
+    }
+
+    private fun WindowContainerTransaction.startActivity(intent: Intent) =
+        this.startActivityInTaskFragment(fragmentToken, activity.activityToken!!, intent, null)
+
+    /** Starts the provided activity in the fragment and move it to the background */
+    fun startActivityInTaskFragment(intent: Intent) {
+        organizer.applyTransaction(
+            WindowContainerTransaction().startActivity(intent),
+            TASK_FRAGMENT_TRANSIT_OPEN,
+            false
+        )
+    }
+
+    /** Destroys the task fragment */
+    fun destroy() {
+        organizer.applyTransaction(
+            WindowContainerTransaction()
+                .addTaskFragmentOperation(
+                    fragmentToken,
+                    TaskFragmentOperation.Builder(OP_TYPE_DELETE_TASK_FRAGMENT).build()
+                ),
+            TASK_FRAGMENT_TRANSIT_CLOSE,
+            false
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/domain/interactor/HomeControlsComponentInteractor.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/domain/interactor/HomeControlsComponentInteractor.kt
new file mode 100644
index 0000000..91e0547
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/domain/interactor/HomeControlsComponentInteractor.kt
@@ -0,0 +1,104 @@
+/*
+ * 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.dreams.homecontrols.domain.interactor
+
+import android.content.ComponentName
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.controls.ControlsServiceInfo
+import com.android.systemui.controls.dagger.ControlsComponent
+import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.controls.panels.AuthorizedPanelsRepository
+import com.android.systemui.controls.panels.SelectedComponentRepository
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.user.data.repository.UserRepository
+import com.android.systemui.util.kotlin.getOrNull
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.stateIn
+
+@SysUISingleton
+@OptIn(ExperimentalCoroutinesApi::class)
+class HomeControlsComponentInteractor
+@Inject
+constructor(
+    private val selectedComponentRepository: SelectedComponentRepository,
+    private val controlsComponent: ControlsComponent,
+    private val authorizedPanelsRepository: AuthorizedPanelsRepository,
+    userRepository: UserRepository,
+    @Background private val bgScope: CoroutineScope
+) {
+    private val controlsListingController =
+        controlsComponent.getControlsListingController().getOrNull()
+
+    /** Gets the current user's selected panel, or null if there isn't one */
+    private val selectedItem: Flow<SelectedComponentRepository.SelectedComponent?> =
+        userRepository.selectedUserInfo
+            .flatMapLatest { user ->
+                selectedComponentRepository.selectedComponentFlow(user.userHandle)
+            }
+            .map { if (it?.isPanel == true) it else null }
+
+    /** Gets all the available panels which are authorized by the user */
+    private fun allPanelItem(): Flow<List<PanelComponent>> {
+        if (controlsListingController == null) {
+            return emptyFlow()
+        }
+        return conflatedCallbackFlow {
+                val listener =
+                    object : ControlsListingController.ControlsListingCallback {
+                        override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) {
+                            trySend(serviceInfos)
+                        }
+                    }
+                controlsListingController.addCallback(listener)
+                awaitClose { controlsListingController.removeCallback(listener) }
+            }
+            .onStart { emit(controlsListingController.getCurrentServices()) }
+            .map { serviceInfos ->
+                val authorizedPanels = authorizedPanelsRepository.getAuthorizedPanels()
+                serviceInfos.mapNotNull {
+                    val panelActivity = it.panelActivity
+                    if (it.componentName.packageName in authorizedPanels && panelActivity != null) {
+                        PanelComponent(it.componentName, panelActivity)
+                    } else {
+                        null
+                    }
+                }
+            }
+    }
+    val panelComponent: StateFlow<ComponentName?> =
+        combine(allPanelItem(), selectedItem) { items, selected ->
+                val item =
+                    items.firstOrNull { it.componentName == selected?.componentName }
+                        ?: items.firstOrNull()
+                item?.panelActivity
+            }
+            .stateIn(bgScope, SharingStarted.WhileSubscribed(), null)
+
+    data class PanelComponent(val componentName: ComponentName, val panelActivity: ComponentName)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
index 1540423..df0566e 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -29,6 +29,8 @@
 import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
+import com.android.systemui.statusbar.notification.interruption.VisualInterruptionRefactor
+import com.android.systemui.statusbar.notification.shared.NotificationAvalancheSuppression
 import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
 import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
 import javax.inject.Inject
@@ -45,6 +47,7 @@
         // Internal notification frontend dependencies
         NotificationsLiveDataStoreRefactor.token dependsOn NotificationIconContainerRefactor.token
         FooterViewRefactor.token dependsOn NotificationIconContainerRefactor.token
+        NotificationAvalancheSuppression.token dependsOn VisualInterruptionRefactor.token
 
         // Internal keyguard dependencies
         KeyguardShadeMigrationNssl.token dependsOn keyguardBottomAreaRefactor
diff --git a/packages/SystemUI/src/com/android/systemui/inputmethod/InputMethodModule.kt b/packages/SystemUI/src/com/android/systemui/inputmethod/InputMethodModule.kt
new file mode 100644
index 0000000..bac48f1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/inputmethod/InputMethodModule.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.inputmethod
+
+import com.android.systemui.inputmethod.data.repository.InputMethodRepositoryModule
+import dagger.Module
+
+/** Module for providing objects exposed by the input method package. */
+@Module(
+    includes =
+        [
+            InputMethodRepositoryModule::class,
+        ],
+)
+object InputMethodModule
diff --git a/packages/SystemUI/src/com/android/systemui/inputmethod/data/model/InputMethodModel.kt b/packages/SystemUI/src/com/android/systemui/inputmethod/data/model/InputMethodModel.kt
new file mode 100644
index 0000000..bdc18b3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/inputmethod/data/model/InputMethodModel.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.inputmethod.data.model
+
+/**
+ * Models an input method editor (IME).
+ *
+ * @see android.view.inputmethod.InputMethodInfo
+ */
+data class InputMethodModel(
+    /** A unique ID for this input method. */
+    val imeId: String,
+    /** The subtypes of this IME (may be empty). */
+    val subtypes: List<Subtype>,
+) {
+    /**
+     * A Subtype can describe locale (e.g. en_US, fr_FR...) and mode (e.g. voice, keyboard), and is
+     * used for IME switch and settings.
+     *
+     * @see android.view.inputmethod.InputMethodSubtype
+     */
+    data class Subtype(
+        /** A unique ID for this IME subtype. */
+        val subtypeId: Int,
+        /**
+         * Whether this subtype is auxiliary. An auxiliary subtype will not be shown in the list of
+         * enabled IMEs for choosing the current IME in Settings, but it will be shown in the list
+         * of IMEs in the IME switcher to allow the user to tentatively switch to this subtype while
+         * an IME is shown.
+         *
+         * The intent of this flag is to allow for IMEs that are invoked in a one-shot way as
+         * auxiliary input mode, and return to the previous IME once it is finished (e.g. voice
+         * input).
+         */
+        val isAuxiliary: Boolean,
+    )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/inputmethod/data/repository/InputMethodRepository.kt b/packages/SystemUI/src/com/android/systemui/inputmethod/data/repository/InputMethodRepository.kt
new file mode 100644
index 0000000..5f316c4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/inputmethod/data/repository/InputMethodRepository.kt
@@ -0,0 +1,139 @@
+/*
+ * 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.inputmethod.data.repository
+
+import android.annotation.SuppressLint
+import android.os.UserHandle
+import android.view.inputmethod.InputMethodInfo
+import android.view.inputmethod.InputMethodManager
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.inputmethod.data.model.InputMethodModel
+import dagger.Binds
+import dagger.Module
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.asFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.withContext
+
+/** Provides access to input-method related application state in the bouncer. */
+interface InputMethodRepository {
+    /**
+     * Creates and returns a new `Flow` of installed input methods that are enabled for the
+     * specified user.
+     *
+     * @param fetchSubtypes Whether to fetch the IME Subtypes as well (requires an additional IPC
+     *   call for each IME, avoid if not needed).
+     * @see InputMethodManager.getEnabledInputMethodListAsUser
+     */
+    suspend fun enabledInputMethods(userId: Int, fetchSubtypes: Boolean): Flow<InputMethodModel>
+
+    /** Returns enabled subtypes for the currently selected input method. */
+    suspend fun selectedInputMethodSubtypes(): List<InputMethodModel.Subtype>
+
+    /**
+     * Shows the system's input method picker dialog.
+     *
+     * @param displayId The display ID on which to show the dialog.
+     * @param showAuxiliarySubtypes Whether to show auxiliary input method subtypes in the list of
+     *   enabled IMEs.
+     * @see InputMethodManager.showInputMethodPickerFromSystem
+     */
+    suspend fun showInputMethodPicker(displayId: Int, showAuxiliarySubtypes: Boolean)
+}
+
+@SysUISingleton
+class InputMethodRepositoryImpl
+@Inject
+constructor(
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+    private val inputMethodManager: InputMethodManager,
+) : InputMethodRepository {
+
+    override suspend fun enabledInputMethods(
+        userId: Int,
+        fetchSubtypes: Boolean
+    ): Flow<InputMethodModel> {
+        return withContext(backgroundDispatcher) {
+                inputMethodManager.getEnabledInputMethodListAsUser(UserHandle.of(userId))
+            }
+            .asFlow()
+            .map { inputMethodInfo ->
+                InputMethodModel(
+                    imeId = inputMethodInfo.id,
+                    subtypes =
+                        if (fetchSubtypes) {
+                            enabledInputMethodSubtypes(
+                                inputMethodInfo,
+                                allowsImplicitlyEnabledSubtypes = true
+                            )
+                        } else {
+                            listOf()
+                        }
+                )
+            }
+    }
+
+    override suspend fun selectedInputMethodSubtypes(): List<InputMethodModel.Subtype> {
+        return enabledInputMethodSubtypes(
+            inputMethodInfo = null, // Fetch subtypes for the currently-selected IME.
+            allowsImplicitlyEnabledSubtypes = false
+        )
+    }
+
+    @SuppressLint("MissingPermission")
+    override suspend fun showInputMethodPicker(displayId: Int, showAuxiliarySubtypes: Boolean) {
+        withContext(backgroundDispatcher) {
+            inputMethodManager.showInputMethodPickerFromSystem(showAuxiliarySubtypes, displayId)
+        }
+    }
+
+    /**
+     * Returns a list of enabled input method subtypes for the specified input method info.
+     *
+     * @param inputMethodInfo The [InputMethodInfo] whose subtypes list will be returned. If `null`,
+     *   returns enabled subtypes for the currently selected [InputMethodInfo].
+     * @param allowsImplicitlyEnabledSubtypes Whether to allow to return the implicitly enabled
+     *   subtypes. If an input method info doesn't have enabled subtypes, the framework will
+     *   implicitly enable subtypes according to the current system language.
+     * @see InputMethodManager.getEnabledInputMethodSubtypeList
+     */
+    private suspend fun enabledInputMethodSubtypes(
+        inputMethodInfo: InputMethodInfo?,
+        allowsImplicitlyEnabledSubtypes: Boolean
+    ): List<InputMethodModel.Subtype> {
+        return withContext(backgroundDispatcher) {
+                inputMethodManager.getEnabledInputMethodSubtypeList(
+                    inputMethodInfo,
+                    allowsImplicitlyEnabledSubtypes
+                )
+            }
+            .map {
+                InputMethodModel.Subtype(
+                    subtypeId = it.subtypeId,
+                    isAuxiliary = it.isAuxiliary,
+                )
+            }
+    }
+}
+
+@Module
+interface InputMethodRepositoryModule {
+    @Binds fun repository(impl: InputMethodRepositoryImpl): InputMethodRepository
+}
diff --git a/packages/SystemUI/src/com/android/systemui/inputmethod/domain/interactor/InputMethodInteractor.kt b/packages/SystemUI/src/com/android/systemui/inputmethod/domain/interactor/InputMethodInteractor.kt
new file mode 100644
index 0000000..c54aa7f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/inputmethod/domain/interactor/InputMethodInteractor.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.inputmethod.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.inputmethod.data.repository.InputMethodRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.count
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.take
+
+/** Hosts application business logic related to input methods (e.g. software keyboard). */
+@SysUISingleton
+class InputMethodInteractor
+@Inject
+constructor(
+    private val repository: InputMethodRepository,
+) {
+    /**
+     * Returns whether there are multiple enabled input methods to choose from for password input.
+     *
+     * Method adapted from `com.android.inputmethod.latin.Utils`.
+     */
+    suspend fun hasMultipleEnabledImesOrSubtypes(userId: Int): Boolean {
+        // Count IMEs that either have no subtypes, or have at least one non-auxiliary subtype.
+        val matchingInputMethods =
+            repository
+                .enabledInputMethods(userId, fetchSubtypes = true)
+                .filter { ime -> ime.subtypes.isEmpty() || ime.subtypes.any { !it.isAuxiliary } }
+                .take(2) // Short-circuit if we find at least 2 matching IMEs.
+
+        return matchingInputMethods.count() > 1 || repository.selectedInputMethodSubtypes().size > 1
+    }
+
+    /** Shows the system's input method picker dialog. */
+    suspend fun showInputMethodPicker(displayId: Int, showAuxiliarySubtypes: Boolean) {
+        repository.showInputMethodPicker(displayId, showAuxiliarySubtypes)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
index 13e3835..e16f8dc 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
@@ -51,7 +51,7 @@
 import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.keyguard.WindowManagerLockscreenVisibilityManager;
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardDataQuickAffordanceModule;
-import com.android.systemui.keyguard.data.repository.KeyguardFaceAuthModule;
+import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthModule;
 import com.android.systemui.keyguard.data.repository.KeyguardRepositoryModule;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.keyguard.domain.interactor.StartKeyguardTransitionModule;
@@ -104,7 +104,7 @@
             FalsingModule.class,
             KeyguardDataQuickAffordanceModule.class,
             KeyguardRepositoryModule.class,
-            KeyguardFaceAuthModule.class,
+            DeviceEntryFaceAuthModule.class,
             KeyguardDisplayModule.class,
             StartKeyguardTransitionModule.class,
             ResourceTrimmerModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthModule.kt
similarity index 85%
rename from packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthModule.kt
rename to packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthModule.kt
index fede479..4cd544f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthModule.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepositoryImpl
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
 import com.android.systemui.deviceentry.domain.interactor.SystemUIDeviceEntryFaceAuthInteractor
+import com.android.systemui.deviceentry.ui.binder.LiftToRunFaceAuthBinder
 import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.log.table.TableLogBufferFactory
 import dagger.Binds
@@ -32,7 +33,7 @@
 import dagger.multibindings.IntoMap
 
 @Module
-interface KeyguardFaceAuthModule {
+interface DeviceEntryFaceAuthModule {
     @Binds
     fun deviceEntryFaceAuthRepository(
         impl: DeviceEntryFaceAuthRepositoryImpl
@@ -41,13 +42,20 @@
     @Binds
     @IntoMap
     @ClassKey(SystemUIDeviceEntryFaceAuthInteractor::class)
-    fun bind(impl: SystemUIDeviceEntryFaceAuthInteractor): CoreStartable
+    fun bindSystemUIDeviceEntryFaceAuthInteractor(
+        impl: SystemUIDeviceEntryFaceAuthInteractor
+    ): CoreStartable
 
     @Binds
     fun keyguardFaceAuthInteractor(
         impl: SystemUIDeviceEntryFaceAuthInteractor
     ): DeviceEntryFaceAuthInteractor
 
+    @Binds
+    @IntoMap
+    @ClassKey(LiftToRunFaceAuthBinder::class)
+    fun bindLiftToRunFaceAuthBinder(impl: LiftToRunFaceAuthBinder): CoreStartable
+
     companion object {
         @Provides
         @SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
index 9d38be9..71d941a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
@@ -28,6 +28,7 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled
 import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleMultiple
 import com.android.systemui.util.kotlin.sample
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
@@ -64,6 +65,7 @@
         listenForHubToAlternateBouncer()
         listenForHubToOccluded()
         listenForHubToGone()
+        listenForHubToDreaming()
     }
 
     override fun getDefaultAnimatorForTransitionsToState(toState: KeyguardState): ValueAnimator {
@@ -131,6 +133,23 @@
         }
     }
 
+    private fun listenForHubToDreaming() {
+        val invalidFromStates = setOf(KeyguardState.AOD, KeyguardState.DOZING)
+        scope.launch("$TAG#listenForHubToDreaming") {
+            keyguardInteractor.isAbleToDream
+                .sampleMultiple(startedKeyguardTransitionStep, finishedKeyguardState)
+                .collect { (isAbleToDream, lastStartedTransition, finishedKeyguardState) ->
+                    val isOnHub = finishedKeyguardState == KeyguardState.GLANCEABLE_HUB
+                    val isTransitionInterruptible =
+                        lastStartedTransition.to == KeyguardState.GLANCEABLE_HUB &&
+                            !invalidFromStates.contains(lastStartedTransition.from)
+                    if (isAbleToDream && (isOnHub || isTransitionInterruptible)) {
+                        startTransitionTo(KeyguardState.DREAMING)
+                    }
+                }
+        }
+    }
+
     private fun listenForHubToOccluded() {
         scope.launch {
             keyguardInteractor.isKeyguardOccluded.sample(startedKeyguardState, ::Pair).collect {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
index 3965648..deb70b7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
@@ -405,7 +405,9 @@
             interpolator = Interpolators.LINEAR
             duration =
                 when (toState) {
-                    KeyguardState.DREAMING -> TO_DREAMING_DURATION
+                    // Adds 100ms to the overall delay to workaround legacy setOccluded calls
+                    // being delayed in KeyguardViewMediator
+                    KeyguardState.DREAMING -> TO_DREAMING_DURATION + 100.milliseconds
                     KeyguardState.OCCLUDED -> TO_OCCLUDED_DURATION
                     KeyguardState.AOD -> TO_AOD_DURATION
                     KeyguardState.DOZING -> TO_DOZING_DURATION
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/Edge.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/Edge.kt
new file mode 100644
index 0000000..a0f9be6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/Edge.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.keyguard.shared.model
+
+/** FROM -> TO keyguard transition. null values are allowed to signify FROM -> *, or * -> TO */
+data class Edge(
+    val from: KeyguardState?,
+    val to: KeyguardState?,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
index 4abda74..00b7989 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.Edge
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED
@@ -174,11 +175,6 @@
         }
     }
 
-    data class Edge(
-        val from: KeyguardState?,
-        val to: KeyguardState?,
-    )
-
     data class StateToValue(
         val transitionState: TransitionState,
         val value: Float?,
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 703bb87..873cc84 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
@@ -19,6 +19,7 @@
 
 import android.annotation.SuppressLint
 import android.content.res.ColorStateList
+import android.util.StateSet
 import android.view.HapticFeedbackConstants
 import android.view.View
 import androidx.lifecycle.Lifecycle
@@ -113,6 +114,8 @@
 
         fgIconView.repeatWhenAttached {
             repeatOnLifecycle(Lifecycle.State.STARTED) {
+                // Start with an empty state
+                fgIconView.setImageState(StateSet.NOTHING, /* merge */ false)
                 launch {
                     fgViewModel.viewModel.collect { viewModel ->
                         fgIconView.setImageState(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt
index 400d0dc..a651c10 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt
@@ -87,7 +87,8 @@
             return
         }
         // This moves the existing NSSL view to a different parent, as the controller is a
-        // singleton and recreating it has other bad side effects
+        // singleton and recreating it has other bad side effects.
+        // In the SceneContainer, this is done by the NotificationSection composable.
         notificationPanelView.findViewById<View?>(R.id.notification_stack_scroller)?.let {
             (it.parent as ViewGroup).removeView(it)
             sharedNotificationContainer.addNotificationStackScrollLayout(it)
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index 4e89fbf..7d86a6a 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -513,10 +513,6 @@
             notifySystemUiStateFlags(mSysUiState.getFlags());
 
             notifyConnectionChanged();
-            if (mDoneUserChanging != null) {
-                mDoneUserChanging.run();
-                mDoneUserChanging = null;
-            }
         }
 
         @Override
@@ -571,14 +567,11 @@
         }
     };
 
-    private Runnable mDoneUserChanging;
     private final UserTracker.Callback mUserChangedCallback =
             new UserTracker.Callback() {
                 @Override
-                public void onUserChanging(int newUser, @NonNull Context userContext,
-                        @NonNull Runnable resultCallback) {
+                public void onUserChanged(int newUser, @NonNull Context userContext) {
                     mConnectionBackoffAttempts = 0;
-                    mDoneUserChanging = resultCallback;
                     internalConnectToCurrentUser("User changed");
                 }
             };
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
index 93cfc5d..2b978b2 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
@@ -97,6 +97,10 @@
                     val legacyView = view.requireViewById<View>(R.id.legacy_window_root)
                     view.addView(createVisibilityToggleView(legacyView))
 
+                    // This moves the SharedNotificationContainer to the WindowRootView just after
+                    //  the SceneContainerView. This SharedNotificationContainer should contain NSSL
+                    //  due to the NotificationStackScrollLayoutSection (legacy) or
+                    //  NotificationSection (scene container) moving it there.
                     if (flags.flexiNotifsEnabled()) {
                         (sharedNotificationContainer.parent as? ViewGroup)?.removeView(
                             sharedNotificationContainer
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index 19a5840..ef820f3 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -18,6 +18,7 @@
 
 import static com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED;
 import static com.android.systemui.flags.Flags.TRACKPAD_GESTURE_COMMON;
+import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
 import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
 
 import android.app.StatusBarManager;
@@ -477,8 +478,9 @@
                         if (KeyguardShadeMigrationNssl.isEnabled()) {
                             // When on lockscreen, if the touch originates at the top of the screen
                             // go directly to QS and not the shade
-                            if (mQuickSettingsController.shouldQuickSettingsIntercept(
-                                    ev.getX(), ev.getY(), 0)) {
+                            if (mStatusBarStateController.getState() == KEYGUARD
+                                    && mQuickSettingsController.shouldQuickSettingsIntercept(
+                                        ev.getX(), ev.getY(), 0)) {
                                 mShadeLogger.d("NSWVC: QS intercepted");
                                 return true;
                             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index d6d3e67..04d9b0c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -591,8 +591,10 @@
     }
 
     private void updateLockScreenUserLockedMsg(int userId) {
-        if (!mKeyguardUpdateMonitor.isUserUnlocked(userId)
-                || mKeyguardUpdateMonitor.isEncryptedOrLockdown(userId)) {
+        boolean userUnlocked = mKeyguardUpdateMonitor.isUserUnlocked(userId);
+        boolean encryptedOrLockdown = mKeyguardUpdateMonitor.isEncryptedOrLockdown(userId);
+        mKeyguardLogger.logUpdateLockScreenUserLockedMsg(userId, userUnlocked, encryptedOrLockdown);
+        if (!userUnlocked || encryptedOrLockdown) {
             mRotateTextViewController.updateIndication(
                     INDICATION_TYPE_USER_LOCKED,
                     new KeyguardIndication.Builder()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index dd04531..b9afb14 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -617,7 +617,11 @@
     private final ScrollAdapter mScrollAdapter = new ScrollAdapter() {
         @Override
         public boolean isScrolledToTop() {
-            return mOwnScrollY == 0;
+            if (SceneContainerFlag.isEnabled()) {
+                return mController.isPlaceholderScrolledToTop();
+            } else {
+                return mOwnScrollY == 0;
+            }
         }
 
         @Override
@@ -1442,7 +1446,14 @@
             fraction = BouncerPanelExpansionCalculator.aboutToShowBouncerProgress(fraction);
         }
         final float stackY = MathUtils.lerp(0, endTopPosition, fraction);
-        mAmbientState.setStackY(stackY);
+        // TODO(b/322228881): Clean up scene container vs legacy behavior in NSSL
+        if (SceneContainerFlag.isEnabled()) {
+            // stackY should be driven by scene container, not NSSL
+            mAmbientState.setStackY(mTopPadding);
+        } else {
+            mAmbientState.setStackY(stackY);
+        }
+
         if (mOnStackYChanged != null) {
             mOnStackYChanged.accept(listenerNeedsAnimation);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 49fde39..ed26677 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -1144,6 +1144,14 @@
         return mStackAppearanceInteractor.getStackBounds().getValue().getTop();
     }
 
+    /**
+     * Returns whether the notification stack is scrolled to the top; i.e., it cannot be scrolled
+     * down any further.
+     */
+    public boolean isPlaceholderScrolledToTop() {
+        return mStackAppearanceInteractor.getScrolledToTop().getValue();
+    }
+
     /** Set the intrinsic height of the stack content without additional padding. */
     public void setIntrinsicContentHeight(float intrinsicContentHeight) {
         mStackAppearanceInteractor.setIntrinsicContentHeight(intrinsicContentHeight);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt
index aac3c28..0197264 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt
@@ -44,4 +44,10 @@
      * screen.
      */
     val contentTop = MutableStateFlow(0f)
+
+    /**
+     * Whether the notification stack is scrolled to the top; i.e., it cannot be scrolled down any
+     * further.
+     */
+    val scrolledToTop = MutableStateFlow(true)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
index 1dfde09..8307397 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
@@ -42,10 +42,16 @@
      * notifications, this can exceed the space available on screen to show notifications, at which
      * point the notification stack should become scrollable.
      */
-    val intrinsicContentHeight = repository.intrinsicContentHeight.asStateFlow()
+    val intrinsicContentHeight: StateFlow<Float> = repository.intrinsicContentHeight.asStateFlow()
 
     /** The y-coordinate in px of top of the contents of the notification stack. */
-    val contentTop = repository.contentTop.asStateFlow()
+    val contentTop: StateFlow<Float> = repository.contentTop.asStateFlow()
+
+    /**
+     * Whether the notification stack is scrolled to the top; i.e., it cannot be scrolled down any
+     * further.
+     */
+    val scrolledToTop: StateFlow<Boolean> = repository.scrolledToTop.asStateFlow()
 
     /** Sets the position of the notification stack in the current scene. */
     fun setStackBounds(bounds: NotificationContainerBounds) {
@@ -62,4 +68,9 @@
     fun setContentTop(startY: Float) {
         repository.contentTop.value = startY
     }
+
+    /** Sets whether the notification stack is scrolled to the top. */
+    fun setScrolledToTop(scrolledToTop: Boolean) {
+        repository.scrolledToTop.value = scrolledToTop
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt
index ed15f55..6c2cbbe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt
@@ -25,7 +25,6 @@
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel
-import kotlin.math.pow
 import kotlin.math.roundToInt
 import kotlinx.coroutines.launch
 
@@ -65,7 +64,9 @@
                     viewModel.expandFraction.collect { expandFraction ->
                         ambientState.expansionFraction = expandFraction
                         controller.expandedHeight = expandFraction * controller.view.height
-                        controller.setMaxAlphaForExpansion(expandFraction.pow(0.75f))
+                        controller.setMaxAlphaForExpansion(
+                            ((expandFraction - 0.5f) / 0.5f).coerceAtLeast(0f)
+                        )
                     }
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt
index 74db583..56ff7f9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt
@@ -19,11 +19,15 @@
 
 import com.android.systemui.common.shared.model.NotificationContainerBounds
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.ObservableTransitionState
+import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
 
 /** ViewModel which represents the state of the NSSL/Controller in the world of flexiglass */
 @SysUISingleton
@@ -32,9 +36,40 @@
 constructor(
     stackAppearanceInteractor: NotificationStackAppearanceInteractor,
     shadeInteractor: ShadeInteractor,
+    sceneInteractor: SceneInteractor,
 ) {
-    /** The expansion fraction from the top of the notification shade. */
-    val expandFraction: Flow<Float> = shadeInteractor.shadeExpansion
+    /**
+     * The expansion fraction of the notification stack. It should go from 0 to 1 when transitioning
+     * from Gone to Shade scenes, and remain at 1 when in Lockscreen or Shade scenes and while
+     * transitioning from Shade to QuickSettings scenes.
+     */
+    val expandFraction: Flow<Float> =
+        combine(
+            shadeInteractor.shadeExpansion,
+            sceneInteractor.transitionState,
+        ) { shadeExpansion, transitionState ->
+            when (transitionState) {
+                is ObservableTransitionState.Idle -> {
+                    if (transitionState.scene == SceneKey.Lockscreen) {
+                        1f
+                    } else {
+                        shadeExpansion
+                    }
+                }
+                is ObservableTransitionState.Transition -> {
+                    if (
+                        (transitionState.fromScene == SceneKey.Shade &&
+                            transitionState.toScene == SceneKey.QuickSettings) ||
+                            (transitionState.fromScene == SceneKey.QuickSettings &&
+                                transitionState.toScene == SceneKey.Shade)
+                    ) {
+                        1f
+                    } else {
+                        shadeExpansion
+                    }
+                }
+            }
+        }
 
     /** The bounds of the notification stack in the current scene. */
     val stackBounds: Flow<NotificationContainerBounds> = stackAppearanceInteractor.stackBounds
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
index 385f061..a436f17 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
@@ -87,4 +87,9 @@
     fun onContentTopChanged(padding: Float) {
         interactor.setContentTop(padding)
     }
+
+    /** Sets whether the notification stack is scrolled to the top. */
+    fun setScrolledToTop(scrolledToTop: Boolean) {
+        interactor.setScrolledToTop(scrolledToTop)
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
index 4617ce4..3915c376 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -24,13 +24,24 @@
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.Edge
 import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER
+import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
+import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING
+import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING
+import com.android.systemui.keyguard.shared.model.KeyguardState.GLANCEABLE_HUB
+import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
+import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED
+import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
 import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED
 import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
 import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
 import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel
 import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToLockscreenTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToGlanceableHubTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel
@@ -51,6 +62,7 @@
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.flow.onStart
@@ -69,43 +81,52 @@
     communalInteractor: CommunalInteractor,
     private val occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel,
     lockscreenToOccludedTransitionViewModel: LockscreenToOccludedTransitionViewModel,
+    dreamingToLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel,
+    lockscreenToDreamingTransitionViewModel: LockscreenToDreamingTransitionViewModel,
     glanceableHubToLockscreenTransitionViewModel: GlanceableHubToLockscreenTransitionViewModel,
     lockscreenToGlanceableHubTransitionViewModel: LockscreenToGlanceableHubTransitionViewModel,
     private val aodBurnInViewModel: AodBurnInViewModel,
 ) {
-    private val statesForConstrainedNotifications =
-        setOf(
-            KeyguardState.AOD,
-            KeyguardState.LOCKSCREEN,
-            KeyguardState.DOZING,
-            KeyguardState.ALTERNATE_BOUNCER,
-            KeyguardState.PRIMARY_BOUNCER
+    private val statesForConstrainedNotifications: Set<KeyguardState> =
+        setOf(AOD, LOCKSCREEN, DOZING, ALTERNATE_BOUNCER, PRIMARY_BOUNCER)
+
+    private val edgeToAlphaViewModel =
+        mapOf<Edge?, Flow<Float>>(
+            Edge(from = LOCKSCREEN, to = DREAMING) to
+                lockscreenToDreamingTransitionViewModel.lockscreenAlpha,
+            Edge(from = DREAMING, to = LOCKSCREEN) to
+                dreamingToLockscreenTransitionViewModel.lockscreenAlpha,
+            Edge(from = LOCKSCREEN, to = OCCLUDED) to
+                lockscreenToOccludedTransitionViewModel.lockscreenAlpha,
+            Edge(from = OCCLUDED, to = LOCKSCREEN) to
+                occludedToLockscreenTransitionViewModel.lockscreenAlpha,
         )
 
-    private val lockscreenToOccludedRunning =
-        keyguardTransitionInteractor
-            .transition(KeyguardState.LOCKSCREEN, KeyguardState.OCCLUDED)
-            .map { it.transitionState == STARTED || it.transitionState == RUNNING }
+    private val lockscreenTransitionInProgress: Flow<Edge?> =
+        keyguardTransitionInteractor.transitions
+            .map { step ->
+                if (
+                    (step.transitionState == STARTED || step.transitionState == RUNNING) &&
+                        (step.from == LOCKSCREEN || step.to == LOCKSCREEN)
+                ) {
+                    Edge(step.from, step.to)
+                } else {
+                    null
+                }
+            }
             .distinctUntilChanged()
-            .onStart { emit(false) }
-
-    private val occludedToLockscreenRunning =
-        keyguardTransitionInteractor
-            .transition(KeyguardState.OCCLUDED, KeyguardState.LOCKSCREEN)
-            .map { it.transitionState == STARTED || it.transitionState == RUNNING }
-            .distinctUntilChanged()
-            .onStart { emit(false) }
+            .onStart { emit(null) }
 
     private val lockscreenToGlanceableHubRunning =
         keyguardTransitionInteractor
-            .transition(KeyguardState.LOCKSCREEN, KeyguardState.GLANCEABLE_HUB)
+            .transition(LOCKSCREEN, GLANCEABLE_HUB)
             .map { it.transitionState == STARTED || it.transitionState == RUNNING }
             .distinctUntilChanged()
             .onStart { emit(false) }
 
     private val glanceableHubToLockscreenRunning =
         keyguardTransitionInteractor
-            .transition(KeyguardState.GLANCEABLE_HUB, KeyguardState.LOCKSCREEN)
+            .transition(GLANCEABLE_HUB, LOCKSCREEN)
             .map { it.transitionState == STARTED || it.transitionState == RUNNING }
             .distinctUntilChanged()
             .onStart { emit(false) }
@@ -141,7 +162,7 @@
                     statesForConstrainedNotifications.contains(it)
                 },
                 keyguardTransitionInteractor
-                    .transitionValue(KeyguardState.LOCKSCREEN)
+                    .transitionValue(LOCKSCREEN)
                     .onStart { emit(0f) }
                     .map { it > 0 }
             ) { constrainedNotificationState, transitioningToOrFromLockscreen ->
@@ -242,38 +263,46 @@
                 initialValue = NotificationContainerBounds(),
             )
 
+    /** As QS is expanding, fade out notifications unless in splitshade */
+    private val alphaForQsExpansion: Flow<Float> =
+        interactor.configurationBasedDimensions.flatMapLatest {
+            if (it.useSplitShade) {
+                flowOf(1f)
+            } else {
+                shadeInteractor.qsExpansion.map { 1f - it }
+            }
+        }
+
     val expansionAlpha: Flow<Float> =
         // Due to issues with the legacy shade, some shade expansion events are sent incorrectly,
-        // such as when the shade resets. This can happen while the LOCKSCREEN<->OCCLUDED transition
+        // such as when the shade resets. This can happen while the transition to/from LOCKSCREEN
         // is running. Therefore use a series of flatmaps to prevent unwanted interruptions while
         // those transitions are in progress. Without this, the alpha value will produce a visible
         // flicker.
-        lockscreenToOccludedRunning.flatMapLatest { isLockscreenToOccludedRunning ->
-            if (isLockscreenToOccludedRunning) {
-                lockscreenToOccludedTransitionViewModel.lockscreenAlpha
-            } else {
-                occludedToLockscreenRunning.flatMapLatest { isOccludedToLockscreenRunning ->
-                    if (isOccludedToLockscreenRunning) {
-                        occludedToLockscreenTransitionViewModel.lockscreenAlpha.onStart { emit(0f) }
-                    } else {
+        lockscreenTransitionInProgress
+            .flatMapLatest { edge ->
+                edgeToAlphaViewModel.getOrElse(
+                    edge,
+                    {
                         isOnLockscreenWithoutShade.flatMapLatest { isOnLockscreenWithoutShade ->
                             combineTransform(
                                 keyguardInteractor.keyguardAlpha,
                                 shadeCollpaseFadeIn,
-                            ) { alpha, shadeCollpaseFadeIn ->
+                                alphaForQsExpansion,
+                            ) { alpha, shadeCollpaseFadeIn, alphaForQsExpansion ->
                                 if (isOnLockscreenWithoutShade) {
                                     if (!shadeCollpaseFadeIn) {
                                         emit(alpha)
                                     }
                                 } else {
-                                    emit(1f)
+                                    emit(alphaForQsExpansion)
                                 }
                             }
                         }
                     }
-                }
+                )
             }
-        }
+            .distinctUntilChanged()
 
     /**
      * Returns a flow of the expected alpha while running a LOCKSCREEN<->GLANCEABLE_HUB transition
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt
deleted file mode 100644
index 9f08633..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.systemui.statusbar.phone
-
-import android.content.Context
-import android.content.pm.PackageManager
-import android.hardware.Sensor
-import android.hardware.TriggerEvent
-import android.hardware.TriggerEventListener
-import com.android.keyguard.ActiveUnlockConfig
-import com.android.keyguard.KeyguardUpdateMonitor
-import com.android.keyguard.KeyguardUpdateMonitorCallback
-import com.android.systemui.CoreStartable
-import com.android.systemui.Dumpable
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
-import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.user.domain.interactor.SelectedUserInteractor
-import com.android.systemui.util.Assert
-import com.android.systemui.util.sensors.AsyncSensorManager
-import java.io.PrintWriter
-import javax.inject.Inject
-
-/**
- * Triggers face auth on lift when the device is showing the lock screen. Only initialized
- * if face auth is supported on the device. Not to be confused with the lift to wake gesture
- * which is handled by {@link com.android.server.policy.PhoneWindowManager}.
- */
-@SysUISingleton
-class KeyguardLiftController @Inject constructor(
-        private val context: Context,
-        private val statusBarStateController: StatusBarStateController,
-        private val asyncSensorManager: AsyncSensorManager,
-        private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
-        private val deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor,
-        private val dumpManager: DumpManager,
-        private val selectedUserInteractor: SelectedUserInteractor,
-) : Dumpable, CoreStartable {
-
-    private val pickupSensor = asyncSensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE)
-    private var isListening = false
-    private var bouncerVisible = false
-
-    override fun start() {
-        if (context.packageManager.hasSystemFeature(PackageManager.FEATURE_FACE)) {
-            init()
-        }
-    }
-
-    private fun init() {
-        dumpManager.registerDumpable(this)
-        statusBarStateController.addCallback(statusBarStateListener)
-        keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
-        updateListeningState()
-    }
-
-    private val listener: TriggerEventListener = object : TriggerEventListener() {
-        override fun onTrigger(event: TriggerEvent?) {
-            Assert.isMainThread()
-            // Not listening anymore since trigger events unregister themselves
-            isListening = false
-            updateListeningState()
-            deviceEntryFaceAuthInteractor.onDeviceLifted()
-            keyguardUpdateMonitor.requestActiveUnlock(
-                ActiveUnlockConfig.ActiveUnlockRequestOrigin.WAKE,
-                "KeyguardLiftController")
-        }
-    }
-
-    private val keyguardUpdateMonitorCallback = object : KeyguardUpdateMonitorCallback() {
-        override fun onKeyguardBouncerFullyShowingChanged(bouncer: Boolean) {
-            bouncerVisible = bouncer
-            updateListeningState()
-        }
-
-        override fun onKeyguardVisibilityChanged(visible: Boolean) {
-            updateListeningState()
-        }
-    }
-
-    private val statusBarStateListener = object : StatusBarStateController.StateListener {
-        override fun onDozingChanged(isDozing: Boolean) {
-            updateListeningState()
-        }
-    }
-
-    override fun dump(pw: PrintWriter, args: Array<out String>) {
-        pw.println("KeyguardLiftController:")
-        pw.println("  pickupSensor: $pickupSensor")
-        pw.println("  isListening: $isListening")
-        pw.println("  bouncerVisible: $bouncerVisible")
-    }
-
-    private fun updateListeningState() {
-        if (pickupSensor == null) {
-            return
-        }
-        val onKeyguard = keyguardUpdateMonitor.isKeyguardVisible &&
-                !statusBarStateController.isDozing
-
-        val isFaceEnabled = deviceEntryFaceAuthInteractor.isFaceAuthEnabledAndEnrolled()
-        val shouldListen = (onKeyguard || bouncerVisible) && isFaceEnabled
-        if (shouldListen != isListening) {
-            isListening = shouldListen
-
-            if (shouldListen) {
-                asyncSensorManager.requestTriggerSensor(listener, pickupSensor)
-            } else {
-                asyncSensorManager.cancelTriggerSensor(listener, pickupSensor)
-            }
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
index 0051161..1c33d3f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
@@ -18,6 +18,7 @@
 
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository
 import com.android.systemui.statusbar.pipeline.satellite.domain.interactor.DeviceBasedSatelliteInteractor
 import com.android.systemui.statusbar.pipeline.satellite.ui.model.SatelliteIconModel
 import javax.inject.Inject
@@ -40,6 +41,7 @@
 constructor(
     interactor: DeviceBasedSatelliteInteractor,
     @Application scope: CoroutineScope,
+    airplaneModeRepository: AirplaneModeRepository,
 ) {
     private val shouldShowIcon: StateFlow<Boolean> =
         interactor.areAllConnectionsOutOfService
@@ -47,7 +49,11 @@
                 if (!allOos) {
                     flowOf(false)
                 } else {
-                    interactor.isSatelliteAllowed
+                    combine(interactor.isSatelliteAllowed, airplaneModeRepository.isAirplaneMode) {
+                        isSatelliteAllowed,
+                        isAirplaneMode ->
+                        isSatelliteAllowed && !isAirplaneMode
+                    }
                 }
             }
             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
index cf76c0d..37be1c6 100644
--- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
@@ -17,6 +17,7 @@
 
 package com.android.systemui.user.data.repository
 
+import android.annotation.SuppressLint
 import android.content.Context
 import android.content.pm.UserInfo
 import android.os.UserHandle
@@ -209,18 +210,15 @@
 
     override val selectedUserInfo: Flow<UserInfo> = selectedUser.map { it.userInfo }
 
+    @SuppressLint("MissingPermission")
     override fun refreshUsers() {
         applicationScope.launch {
-            val result = withContext(backgroundDispatcher) { manager.aliveUsers }
-
-            if (result != null) {
-                _userInfos.value =
-                    result
-                        // Users should be sorted by ascending creation time.
-                        .sortedBy { it.creationTime }
-                        // The guest user is always last, regardless of creation time.
-                        .sortedBy { it.isGuest }
-            }
+            _userInfos.value =
+                withContext(backgroundDispatcher) { manager.aliveUsers }
+                    // Users should be sorted by ascending creation time.
+                    .sortedBy { it.creationTime }
+                    // The guest user is always last, regardless of creation time.
+                    .sortedBy { it.isGuest }
 
             if (mainUserId == UserHandle.USER_NULL) {
                 val mainUser = withContext(backgroundDispatcher) { manager.mainUser }
diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/data/repository/AnimationStatusRepository.kt b/packages/SystemUI/src/com/android/systemui/util/animation/data/repository/AnimationStatusRepository.kt
index adae782..31a8d86 100644
--- a/packages/SystemUI/src/com/android/systemui/util/animation/data/repository/AnimationStatusRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/animation/data/repository/AnimationStatusRepository.kt
@@ -26,7 +26,7 @@
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.withContext
+import kotlinx.coroutines.flow.flowOn
 
 /** Utility class that could give information about if animation are enabled in the system */
 interface AnimationStatusRepository {
@@ -45,24 +45,26 @@
      * Emits true if animations are enabled in the system, after subscribing it immediately emits
      * the current state
      */
-    override fun areAnimationsEnabled(): Flow<Boolean> = conflatedCallbackFlow {
-        val initialValue = withContext(backgroundDispatcher) { resolver.areAnimationsEnabled() }
-        trySend(initialValue)
+    override fun areAnimationsEnabled(): Flow<Boolean> =
+        conflatedCallbackFlow {
+                val initialValue = resolver.areAnimationsEnabled()
+                trySend(initialValue)
 
-        val observer =
-            object : ContentObserver(backgroundHandler) {
-                override fun onChange(selfChange: Boolean) {
-                    val updatedValue = resolver.areAnimationsEnabled()
-                    trySend(updatedValue)
-                }
+                val observer =
+                    object : ContentObserver(backgroundHandler) {
+                        override fun onChange(selfChange: Boolean) {
+                            val updatedValue = resolver.areAnimationsEnabled()
+                            trySend(updatedValue)
+                        }
+                    }
+
+                resolver.registerContentObserver(
+                    Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE),
+                    /* notifyForDescendants= */ false,
+                    observer
+                )
+
+                awaitClose { resolver.unregisterContentObserver(observer) }
             }
-
-        resolver.registerContentObserver(
-            Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE),
-            /* notifyForDescendants= */ false,
-            observer
-        )
-
-        awaitClose { resolver.unregisterContentObserver(observer) }
-    }
+            .flowOn(backgroundDispatcher)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt
index 8fe57e11..d47413f 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt
@@ -20,21 +20,19 @@
 import com.android.systemui.util.time.SystemClockImpl
 import java.util.concurrent.atomic.AtomicReference
 import kotlin.math.max
-import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.CoroutineStart
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.channelFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
 
 /**
@@ -106,6 +104,14 @@
 /** Holds a [newValue] emitted from a [Flow], along with the [previousValue] emitted value. */
 data class WithPrev<out S, out T : S>(val previousValue: S, val newValue: T)
 
+/** Emits a [Unit] only when the number of downstream subscribers of this flow increases. */
+fun <T> MutableSharedFlow<T>.onSubscriberAdded(): Flow<Unit> {
+    return subscriptionCount
+        .pairwise(initialValue = 0)
+        .filter { (previous, current) -> current > previous }
+        .map {}
+}
+
 /**
  * Returns a new [Flow] that combines the [Set] changes between each emission from [this] using
  * [transform].
@@ -183,34 +189,6 @@
 
 /**
  * Returns a flow that mirrors the original flow, but delays values following emitted values for the
- * given [periodMs]. If the original flow emits more than one value during this period, only the
- * latest value is emitted.
- *
- * Example:
- * ```kotlin
- * flow {
- *     emit(1)     // t=0ms
- *     delay(90)
- *     emit(2)     // t=90ms
- *     delay(90)
- *     emit(3)     // t=180ms
- *     delay(1010)
- *     emit(4)     // t=1190ms
- *     delay(1010)
- *     emit(5)     // t=2200ms
- * }.throttle(1000)
- * ```
- *
- * produces the following emissions at the following times
- *
- * ```text
- * 1 (t=0ms), 3 (t=1000ms), 4 (t=2000ms), 5 (t=3000ms)
- * ```
- */
-fun <T> Flow<T>.throttle(periodMs: Long): Flow<T> = this.throttle(periodMs, SystemClockImpl())
-
-/**
- * Returns a flow that mirrors the original flow, but delays values following emitted values for the
  * given [periodMs] as reported by the given [clock]. If the original flow emits more than one value
  * during this period, only The latest value is emitted.
  *
@@ -235,70 +213,37 @@
  * 1 (t=0ms), 3 (t=1000ms), 4 (t=2000ms), 5 (t=3000ms)
  * ```
  */
-fun <T> Flow<T>.throttle(periodMs: Long, clock: SystemClock): Flow<T> = channelFlow {
-    coroutineScope {
-        var previousEmitTimeMs = 0L
-        var delayJob: Job? = null
-        var sendJob: Job? = null
-        val outerScope = this
+fun <T> Flow<T>.throttle(periodMs: Long, clock: SystemClock = SystemClockImpl()): Flow<T> =
+    channelFlow {
+        coroutineScope {
+            var previousEmitTimeMs = 0L
+            var delayJob: Job? = null
+            var sendJob: Job? = null
+            val outerScope = this
 
-        collect {
-            delayJob?.cancel()
-            sendJob?.join()
-            val currentTimeMs = clock.elapsedRealtime()
-            val timeSinceLastEmit = currentTimeMs - previousEmitTimeMs
-            val timeUntilNextEmit = max(0L, periodMs - timeSinceLastEmit)
-            if (timeUntilNextEmit > 0L) {
-                // We create delayJob to allow cancellation during the delay period
-                delayJob = launch {
-                    delay(timeUntilNextEmit)
-                    sendJob =
-                        outerScope.launch(start = CoroutineStart.UNDISPATCHED) {
-                            send(it)
-                            previousEmitTimeMs = clock.elapsedRealtime()
-                        }
+            collect {
+                delayJob?.cancel()
+                sendJob?.join()
+                val currentTimeMs = clock.elapsedRealtime()
+                val timeSinceLastEmit = currentTimeMs - previousEmitTimeMs
+                val timeUntilNextEmit = max(0L, periodMs - timeSinceLastEmit)
+                if (timeUntilNextEmit > 0L) {
+                    // We create delayJob to allow cancellation during the delay period
+                    delayJob = launch {
+                        delay(timeUntilNextEmit)
+                        sendJob =
+                            outerScope.launch(start = CoroutineStart.UNDISPATCHED) {
+                                send(it)
+                                previousEmitTimeMs = clock.elapsedRealtime()
+                            }
+                    }
+                } else {
+                    send(it)
+                    previousEmitTimeMs = currentTimeMs
                 }
-            } else {
-                send(it)
-                previousEmitTimeMs = currentTimeMs
             }
         }
     }
-}
-
-/**
- * Returns a [StateFlow] launched in the surrounding [CoroutineScope]. This [StateFlow] gets its
- * value by invoking [getValue] whenever an event is emitted from [changedSignals]. It will also
- * immediately invoke [getValue] to establish its initial value.
- */
-inline fun <T> CoroutineScope.stateFlow(
-    changedSignals: Flow<*>,
-    crossinline getValue: () -> T,
-): StateFlow<T> =
-    changedSignals.map { getValue() }.stateIn(this, SharingStarted.Eagerly, getValue())
-
-inline fun <T1, T2, T3, T4, T5, T6, R> combine(
-    flow: Flow<T1>,
-    flow2: Flow<T2>,
-    flow3: Flow<T3>,
-    flow4: Flow<T4>,
-    flow5: Flow<T5>,
-    flow6: Flow<T6>,
-    crossinline transform: suspend (T1, T2, T3, T4, T5, T6) -> R
-): Flow<R> {
-    return kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5, flow6) { args: Array<*>
-        ->
-        @Suppress("UNCHECKED_CAST")
-        transform(
-            args[0] as T1,
-            args[1] as T2,
-            args[2] as T3,
-            args[3] as T4,
-            args[4] as T5,
-            args[5] as T6
-        )
-    }
-}
 
 inline fun <T1, T2, T3, T4, T5, T6, T7, R> combine(
     flow: Flow<T1>,
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
index 1e801ae..e832506 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
@@ -347,10 +347,10 @@
         desktopMode.addVisibleTasksListener(
                 new DesktopModeTaskRepository.VisibleTasksListener() {
                     @Override
-                    public void onVisibilityChanged(int displayId, boolean hasFreeformTasks) {
+                    public void onTasksVisibilityChanged(int displayId, int visibleTasksCount) {
                         if (displayId == Display.DEFAULT_DISPLAY) {
                             mSysUiState.setFlag(SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE,
-                                            hasFreeformTasks)
+                                            visibleTasksCount > 0)
                                     .commitUpdate(mDisplayTracker.getDefaultDisplayId());
                         }
                         // TODO(b/278084491): update sysui state for changes on other displays
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 e93ad0be3..2812718 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
@@ -1895,6 +1895,37 @@
             coroutineContext.cancelChildren()
         }
 
+    @Test
+    fun glanceableHubToDreaming() =
+        testScope.runTest {
+            // GIVEN a device that is not dreaming or dozing
+            keyguardRepository.setDreamingWithOverlay(false)
+            keyguardRepository.setDozeTransitionModel(
+                DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH)
+            )
+            runCurrent()
+
+            // GIVEN a prior transition has run to GLANCEABLE_HUB
+            runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.GLANCEABLE_HUB)
+
+            // WHEN the device begins to dream
+            keyguardRepository.setDreamingWithOverlay(true)
+            advanceUntilIdle()
+
+            val info =
+                withArgCaptor<TransitionInfo> {
+                    verify(transitionRepository).startTransition(capture())
+                }
+            // THEN a transition to DREAMING should occur
+            assertThat(info.ownerName)
+                .isEqualTo(FromGlanceableHubTransitionInteractor::class.simpleName)
+            assertThat(info.from).isEqualTo(KeyguardState.GLANCEABLE_HUB)
+            assertThat(info.to).isEqualTo(KeyguardState.DREAMING)
+            assertThat(info.animator).isNotNull()
+
+            coroutineContext.cancelChildren()
+        }
+
     private fun createKeyguardInteractor(): KeyguardInteractor {
         return KeyguardInteractorFactory.create(
                 featureFlags = featureFlags,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsTest.kt
index 75994da..ad2ae8b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsTest.kt
@@ -19,7 +19,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.mockPrimaryBouncerInteractor
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.flags.Flags
 import com.android.systemui.flags.fakeFeatureFlagsClassic
@@ -65,7 +65,7 @@
     private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
     private val shadeRepository = kosmos.shadeRepository
     private val sysuiStatusBarStateController = kosmos.sysuiStatusBarStateController
-    private val primaryBouncerInteractor = kosmos.primaryBouncerInteractor
+    private val primaryBouncerInteractor = kosmos.mockPrimaryBouncerInteractor
     private val underTest = kosmos.bouncerToGoneFlows
 
     @Before
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
index 21c038a..f53fc46 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
@@ -20,6 +20,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor
 import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
 import com.android.systemui.statusbar.pipeline.satellite.data.prod.FakeDeviceBasedSatelliteRepository
@@ -36,6 +37,7 @@
 class DeviceBasedSatelliteViewModelTest : SysuiTestCase() {
     private lateinit var underTest: DeviceBasedSatelliteViewModel
     private lateinit var interactor: DeviceBasedSatelliteInteractor
+    private lateinit var airplaneModeRepository: FakeAirplaneModeRepository
 
     private val repo = FakeDeviceBasedSatelliteRepository()
     private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock())
@@ -45,6 +47,7 @@
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
+        airplaneModeRepository = FakeAirplaneModeRepository()
 
         interactor =
             DeviceBasedSatelliteInteractor(
@@ -57,6 +60,7 @@
             DeviceBasedSatelliteViewModel(
                 interactor,
                 testScope.backgroundScope,
+                airplaneModeRepository,
             )
     }
 
@@ -72,6 +76,9 @@
             val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
             i1.isInService.value = false
 
+            // GIVEN apm is disabled
+            airplaneModeRepository.setIsAirplaneMode(false)
+
             // THEN icon is null because we should not be showing it
             assertThat(latest).isNull()
         }
@@ -88,11 +95,33 @@
             val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
             i1.isInService.value = true
 
+            // GIVEN apm is disabled
+            airplaneModeRepository.setIsAirplaneMode(false)
+
             // THEN icon is null because we have service
             assertThat(latest).isNull()
         }
 
     @Test
+    fun icon_nullWhenShouldNotShow_apmIsEnabled() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.icon)
+
+            // GIVEN satellite is allowed
+            repo.isSatelliteAllowedForCurrentLocation.value = true
+
+            // GIVEN all icons are OOS
+            val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+            i1.isInService.value = false
+
+            // GIVEN apm is enabled
+            airplaneModeRepository.setIsAirplaneMode(true)
+
+            // THEN icon is null because we should not be showing it
+            assertThat(latest).isNull()
+        }
+
+    @Test
     fun icon_satelliteIsOff() =
         testScope.runTest {
             val latest by collectLastValue(underTest.icon)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
index abfff34..0669cb8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
@@ -31,6 +31,7 @@
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.cancel
 import kotlinx.coroutines.flow.launchIn
@@ -46,6 +47,7 @@
 import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(JUnit4::class)
 class UserRepositoryImplTest : SysuiTestCase() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 7eba3f0..0a3c2d9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -2217,7 +2217,8 @@
 
         FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
         mBubbleController.registerBubbleStateListener(bubbleStateListener);
-        mBubbleController.expandStackAndSelectBubbleFromLauncher(mBubbleEntry.getKey(), 500, 1000);
+        mBubbleController.expandStackAndSelectBubbleFromLauncher(mBubbleEntry.getKey(),
+                new Rect(500, 1000, 600, 1100));
 
         assertThat(mBubbleController.getLayerView().isExpanded()).isTrue();
 
diff --git a/packages/SystemUI/tests/utils/src/android/content/PackageManagerKosmos.kt b/packages/SystemUI/tests/utils/src/android/content/PackageManagerKosmos.kt
new file mode 100644
index 0000000..8901314
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/android/content/PackageManagerKosmos.kt
@@ -0,0 +1,22 @@
+/*
+ * 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 android.content
+
+import android.content.pm.PackageManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+val Kosmos.packageManager by Kosmos.Fixture { mock<PackageManager>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorKosmos.kt
index 06b6cda6..244ef8d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorKosmos.kt
@@ -16,7 +16,40 @@
 
 package com.android.systemui.bouncer.domain.interactor
 
+import android.content.applicationContext
+import com.android.keyguard.keyguardSecurityModel
+import com.android.keyguard.keyguardUpdateMonitor
+import com.android.systemui.bouncer.data.repository.keyguardBouncerRepository
+import com.android.systemui.bouncer.ui.BouncerView
+import com.android.systemui.classifier.falsingCollector
+import com.android.systemui.concurrency.fakeExecutor
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor
+import com.android.systemui.keyguard.DismissCallbackRegistry
+import com.android.systemui.keyguard.data.repository.trustRepository
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.statusbar.policy.KeyguardStateControllerImpl
+import com.android.systemui.user.domain.interactor.selectedUserInteractor
+import com.android.systemui.util.concurrency.mockExecutorHandler
 import com.android.systemui.util.mockito.mock
 
-var Kosmos.primaryBouncerInteractor by Kosmos.Fixture { mock<PrimaryBouncerInteractor>() }
+var Kosmos.mockPrimaryBouncerInteractor by Kosmos.Fixture { mock<PrimaryBouncerInteractor>() }
+var Kosmos.primaryBouncerInteractor by
+    Kosmos.Fixture {
+        PrimaryBouncerInteractor(
+            repository = keyguardBouncerRepository,
+            primaryBouncerView = mock<BouncerView>(),
+            mainHandler = mockExecutorHandler(executor = fakeExecutor),
+            keyguardStateController = mock<KeyguardStateControllerImpl>(),
+            keyguardSecurityModel = keyguardSecurityModel,
+            primaryBouncerCallbackInteractor = mock<PrimaryBouncerCallbackInteractor>(),
+            falsingCollector = falsingCollector,
+            dismissCallbackRegistry = mock<DismissCallbackRegistry>(),
+            context = applicationContext,
+            keyguardUpdateMonitor = keyguardUpdateMonitor,
+            trustRepository = trustRepository,
+            applicationScope = applicationCoroutineScope,
+            selectedUserInteractor = selectedUserInteractor,
+            deviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt
index d91c597..99dfe94 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt
@@ -21,11 +21,13 @@
 import com.android.systemui.bouncer.domain.interactor.bouncerActionButtonInteractor
 import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
 import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor
+import com.android.systemui.inputmethod.domain.interactor.inputMethodInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.scene.shared.flag.sceneContainerFlags
+import com.android.systemui.user.domain.interactor.selectedUserInteractor
 import com.android.systemui.user.ui.viewmodel.userSwitcherViewModel
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.time.systemClock
@@ -36,8 +38,10 @@
         applicationScope = testScope.backgroundScope,
         mainDispatcher = testDispatcher,
         bouncerInteractor = bouncerInteractor,
+        inputMethodInteractor = inputMethodInteractor,
         simBouncerInteractor = simBouncerInteractor,
         authenticationInteractor = authenticationInteractor,
+        selectedUserInteractor = selectedUserInteractor,
         flags = sceneContainerFlags,
         selectedUser = userSwitcherViewModel.selectedUser,
         users = userSwitcherViewModel.users,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
index bc7e7af..fab64e3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
@@ -36,6 +36,14 @@
         }
     }
 
+    override fun deleteWidget(widgetId: Int) {
+        if (_communalWidgets.value.none { it.appWidgetId == widgetId }) {
+            return
+        }
+
+        _communalWidgets.value = _communalWidgets.value.filter { it.appWidgetId != widgetId }
+    }
+
     private fun onConfigured(id: Int, providerInfo: AppWidgetProviderInfo, priority: Int) {
         _communalWidgets.value += listOf(CommunalWidgetContentModel(id, providerInfo, priority))
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt
similarity index 93%
rename from packages/SystemUI/tests/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt
index 002862e..a231212 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.controls.panels
 
 import android.os.UserHandle
+import com.android.systemui.kosmos.Kosmos
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 
@@ -68,3 +69,6 @@
         }
     }
 }
+
+val Kosmos.selectedComponentRepository by
+    Kosmos.Fixture<FakeSelectedComponentRepository> { FakeSelectedComponentRepository() }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/settings/FakeControlsSettingsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/controls/settings/FakeControlsSettingsRepository.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/controls/settings/FakeControlsSettingsRepository.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/controls/settings/FakeControlsSettingsRepository.kt
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt
index 0b1fb40..5575b05 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt
@@ -23,7 +23,7 @@
 import com.android.keyguard.trustManager
 import com.android.systemui.biometrics.data.repository.facePropertyRepository
 import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
-import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.mockPrimaryBouncerInteractor
 import com.android.systemui.deviceentry.data.repository.faceWakeUpTriggersConfig
 import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
 import com.android.systemui.keyguard.data.repository.deviceEntryFaceAuthRepository
@@ -46,7 +46,7 @@
             applicationScope = applicationCoroutineScope,
             mainDispatcher = testDispatcher,
             repository = deviceEntryFaceAuthRepository,
-            primaryBouncerInteractor = { primaryBouncerInteractor },
+            primaryBouncerInteractor = { mockPrimaryBouncerInteractor },
             alternateBouncerInteractor = alternateBouncerInteractor,
             keyguardTransitionInteractor = keyguardTransitionInteractor,
             faceAuthenticationLogger = faceAuthLogger,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/ui/binder/LiftToRunFaceAuthBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/ui/binder/LiftToRunFaceAuthBinderKosmos.kt
new file mode 100644
index 0000000..2fead91
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/ui/binder/LiftToRunFaceAuthBinderKosmos.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.deviceentry.ui.binder
+
+import android.content.packageManager
+import com.android.keyguard.keyguardUpdateMonitor
+import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.util.sensors.asyncSensorManager
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+@ExperimentalCoroutinesApi
+val Kosmos.liftToRunFaceAuthBinder by
+    Kosmos.Fixture {
+        LiftToRunFaceAuthBinder(
+            scope = applicationCoroutineScope,
+            packageManager = packageManager,
+            asyncSensorManager = asyncSensorManager,
+            keyguardUpdateMonitor = keyguardUpdateMonitor,
+            keyguardInteractor = keyguardInteractor,
+            primaryBouncerInteractor = primaryBouncerInteractor,
+            alternateBouncerInteractor = alternateBouncerInteractor,
+            deviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor,
+            powerInteractor = powerInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/inputmethod/data/repository/FakeInputMethodRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/inputmethod/data/repository/FakeInputMethodRepository.kt
new file mode 100644
index 0000000..8e4461d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/inputmethod/data/repository/FakeInputMethodRepository.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.inputmethod.data.repository
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.inputmethod.data.model.InputMethodModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.asFlow
+import kotlinx.coroutines.flow.flowOf
+
+@SysUISingleton
+class FakeInputMethodRepository : InputMethodRepository {
+
+    private var usersToEnabledInputMethods: MutableMap<Int, Flow<InputMethodModel>> = mutableMapOf()
+
+    var selectedInputMethodSubtypes = listOf<InputMethodModel.Subtype>()
+
+    /**
+     * The display ID on which the input method picker dialog was shown, or `null` if the dialog was
+     * not shown.
+     */
+    var inputMethodPickerShownDisplayId: Int? = null
+
+    fun setEnabledInputMethods(userId: Int, vararg enabledInputMethods: InputMethodModel) {
+        usersToEnabledInputMethods[userId] = enabledInputMethods.asFlow()
+    }
+
+    override suspend fun enabledInputMethods(
+        userId: Int,
+        fetchSubtypes: Boolean,
+    ): Flow<InputMethodModel> {
+        return usersToEnabledInputMethods[userId] ?: flowOf()
+    }
+
+    override suspend fun selectedInputMethodSubtypes(): List<InputMethodModel.Subtype> =
+        selectedInputMethodSubtypes
+
+    override suspend fun showInputMethodPicker(displayId: Int, showAuxiliarySubtypes: Boolean) {
+        inputMethodPickerShownDisplayId = displayId
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/inputmethod/data/repository/InputMethodRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/inputmethod/data/repository/InputMethodRepositoryKosmos.kt
new file mode 100644
index 0000000..b71b9d8
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/inputmethod/data/repository/InputMethodRepositoryKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.inputmethod.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+
+var Kosmos.inputMethodRepository: InputMethodRepository by
+    Kosmos.Fixture { fakeInputMethodRepository }
+val Kosmos.fakeInputMethodRepository by Kosmos.Fixture { FakeInputMethodRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/inputmethod/domain/interactor/InputMethodInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/inputmethod/domain/interactor/InputMethodInteractorKosmos.kt
new file mode 100644
index 0000000..da77575
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/inputmethod/domain/interactor/InputMethodInteractorKosmos.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.inputmethod.domain.interactor
+
+import com.android.systemui.inputmethod.data.repository.inputMethodRepository
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.inputMethodInteractor by
+    Kosmos.Fixture {
+        InputMethodInteractor(
+            repository = inputMethodRepository,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsKosmos.kt
index c71c1c3..ffa4133 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsKosmos.kt
@@ -18,7 +18,7 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
-import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.mockPrimaryBouncerInteractor
 import com.android.systemui.flags.featureFlagsClassic
 import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
 import com.android.systemui.kosmos.Kosmos
@@ -31,7 +31,7 @@
 val Kosmos.bouncerToGoneFlows by Fixture {
     BouncerToGoneFlows(
         statusBarStateController = sysuiStatusBarStateController,
-        primaryBouncerInteractor = primaryBouncerInteractor,
+        primaryBouncerInteractor = mockPrimaryBouncerInteractor,
         keyguardDismissActionInteractor = mock(),
         featureFlags = featureFlagsClassic,
         shadeInteractor = shadeInteractor,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelKosmos.kt
index ab28d0d6..4ecff73 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelKosmos.kt
@@ -18,7 +18,7 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
-import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.mockPrimaryBouncerInteractor
 import com.android.systemui.flags.featureFlagsClassic
 import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
 import com.android.systemui.kosmos.Kosmos
@@ -30,7 +30,7 @@
 val Kosmos.primaryBouncerToGoneTransitionViewModel by Fixture {
     PrimaryBouncerToGoneTransitionViewModel(
         statusBarStateController = sysuiStatusBarStateController,
-        primaryBouncerInteractor = primaryBouncerInteractor,
+        primaryBouncerInteractor = mockPrimaryBouncerInteractor,
         keyguardDismissActionInteractor = mock(),
         featureFlags = featureFlagsClassic,
         bouncerToGoneFlows = bouncerToGoneFlows,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModelKosmos.kt
index f2f3a5a..d79633a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModelKosmos.kt
@@ -18,6 +18,7 @@
 
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor
 
@@ -25,5 +26,6 @@
     NotificationStackAppearanceViewModel(
         stackAppearanceInteractor = notificationStackAppearanceInteractor,
         shadeInteractor = shadeInteractor,
+        sceneInteractor = sceneInteractor,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
index 7c398cd..549a775 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
@@ -20,7 +20,9 @@
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.keyguard.ui.viewmodel.aodBurnInViewModel
+import com.android.systemui.keyguard.ui.viewmodel.dreamingToLockscreenTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.glanceableHubToLockscreenTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.lockscreenToDreamingTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.lockscreenToGlanceableHubTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.lockscreenToOccludedTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.occludedToLockscreenTransitionViewModel
@@ -40,6 +42,8 @@
         communalInteractor = communalInteractor,
         occludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel,
         lockscreenToOccludedTransitionViewModel = lockscreenToOccludedTransitionViewModel,
+        dreamingToLockscreenTransitionViewModel = dreamingToLockscreenTransitionViewModel,
+        lockscreenToDreamingTransitionViewModel = lockscreenToDreamingTransitionViewModel,
         glanceableHubToLockscreenTransitionViewModel = glanceableHubToLockscreenTransitionViewModel,
         lockscreenToGlanceableHubTransitionViewModel = lockscreenToGlanceableHubTransitionViewModel,
         aodBurnInViewModel = aodBurnInViewModel,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/sensors/AsyncSensorManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/sensors/AsyncSensorManagerKosmos.kt
new file mode 100644
index 0000000..117ae8c
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/sensors/AsyncSensorManagerKosmos.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.util.sensors
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+val Kosmos.asyncSensorManager by Kosmos.Fixture { mock<AsyncSensorManager>() }
diff --git a/ravenwood/bulk_enable.py b/ravenwood/bulk_enable.py
new file mode 100644
index 0000000..36d398c
--- /dev/null
+++ b/ravenwood/bulk_enable.py
@@ -0,0 +1,75 @@
+#!/usr/bin/env python3
+#
+# 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.
+
+"""
+Tool to bulk-enable tests that are now passing on Ravenwood.
+
+Currently only offers to include classes which are fully passing; ignores
+classes that have partial success.
+
+Typical usage:
+$ ENABLE_PROBE_IGNORED=1 atest MyTestsRavenwood
+$ cd /path/to/tests/root
+$ python bulk_enable.py /path/to/atest/output/host_log.txt
+"""
+
+import collections
+import os
+import re
+import subprocess
+import sys
+
+re_result = re.compile("I/ModuleListener.+?null-device-0 (.+?)#(.+?) ([A-Z_]+)(.*)$")
+
+ANNOTATION = "@android.platform.test.annotations.EnabledOnRavenwood"
+SED_ARG = "s/^((public )?class )/%s\\n\\1/g" % (ANNOTATION)
+
+STATE_PASSED = "PASSED"
+STATE_FAILURE = "FAILURE"
+STATE_ASSUMPTION_FAILURE = "ASSUMPTION_FAILURE"
+STATE_CANDIDATE = "CANDIDATE"
+
+stats_total = collections.defaultdict(int)
+stats_class = collections.defaultdict(lambda: collections.defaultdict(int))
+stats_method = collections.defaultdict()
+
+with open(sys.argv[1]) as f:
+    for line in f.readlines():
+        result = re_result.search(line)
+        if result:
+            clazz, method, state, msg = result.groups()
+            if state == STATE_FAILURE and "actually passed under Ravenwood" in msg:
+                state = STATE_CANDIDATE
+            stats_total[state] += 1
+            stats_class[clazz][state] += 1
+            stats_method[(clazz, method)] = state
+
+# Find classes who are fully "candidates" (would be entirely green if enabled)
+num_enabled = 0
+for clazz in stats_class.keys():
+    stats = stats_class[clazz]
+    if STATE_CANDIDATE in stats and len(stats) == 1:
+        num_enabled += stats[STATE_CANDIDATE]
+        print("Enabling fully-passing class", clazz)
+        clazz_match = re.compile("%s\.(kt|java)" % (clazz.split(".")[-1]))
+        for root, dirs, files in os.walk("."):
+            for f in files:
+                if clazz_match.match(f):
+                    path = os.path.join(root, f)
+                    subprocess.run(["sed", "-i", "-E", SED_ARG, path])
+
+print("Overall stats", stats_total)
+print("Candidates actually enabled", num_enabled)
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
index a797b1d..5588f4f 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
@@ -20,9 +20,12 @@
 import android.os.Bundle;
 import android.os.HandlerThread;
 import android.os.Looper;
+import android.util.Log;
 
 import androidx.test.platform.app.InstrumentationRegistry;
 
+import org.junit.runner.Description;
+
 import java.io.PrintStream;
 import java.util.Map;
 import java.util.concurrent.Executors;
@@ -92,6 +95,13 @@
         android.os.Process.reset$ravenwood();
     }
 
+    public static void logTestRunner(String label, Description description) {
+        // This message string carefully matches the exact format emitted by on-device tests, to
+        // aid developers in debugging raw text logs
+        Log.e("TestRunner", label + ": " + description.getMethodName()
+                + "(" + description.getTestClass().getName() + ")");
+    }
+
     private static void dumpStacks() {
         final PrintStream out = System.err;
         out.println("-----BEGIN ALL THREAD STACKS-----");
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
index 1e7cbf6..0285b38 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
@@ -280,9 +280,14 @@
             public void evaluate() throws Throwable {
                 Assume.assumeTrue(shouldEnableOnRavenwood(description));
 
+                RavenwoodRuleImpl.logTestRunner("started", description);
                 RavenwoodRuleImpl.init(RavenwoodRule.this);
                 try {
                     base.evaluate();
+                    RavenwoodRuleImpl.logTestRunner("finished", description);
+                } catch (Throwable t) {
+                    RavenwoodRuleImpl.logTestRunner("failed", description);
+                    throw t;
                 } finally {
                     RavenwoodRuleImpl.reset(RavenwoodRule.this);
                 }
@@ -300,6 +305,8 @@
             @Override
             public void evaluate() throws Throwable {
                 Assume.assumeFalse(shouldStillIgnoreInProbeIgnoreMode(description));
+
+                RavenwoodRuleImpl.logTestRunner("started", description);
                 RavenwoodRuleImpl.init(RavenwoodRule.this);
                 try {
                     base.evaluate();
@@ -309,6 +316,7 @@
                     Assume.assumeTrue(shouldEnableOnRavenwood(description));
                     throw t;
                 } finally {
+                    RavenwoodRuleImpl.logTestRunner("finished", description);
                     RavenwoodRuleImpl.reset(RavenwoodRule.this);
                 }
 
diff --git a/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
index d0c2e18..7d172f2 100644
--- a/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
+++ b/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
@@ -16,6 +16,8 @@
 
 package android.platform.test.ravenwood;
 
+import org.junit.runner.Description;
+
 public class RavenwoodRuleImpl {
     public static boolean isOnRavenwood() {
         return false;
@@ -28,4 +30,8 @@
     public static void reset(RavenwoodRule rule) {
         // No-op when running on a real device
     }
+
+    public static void logTestRunner(String label, Description description) {
+        // No-op when running on a real device
+    }
 }
diff --git a/services/backup/Android.bp b/services/backup/Android.bp
index d08a97e..2a85eb6 100644
--- a/services/backup/Android.bp
+++ b/services/backup/Android.bp
@@ -21,7 +21,6 @@
     libs: ["services.core"],
     static_libs: [
         "app-compat-annotations",
-        "backup_flags_lib",
     ],
     lint: {
         baseline_filename: "lint-baseline.xml",
@@ -33,8 +32,3 @@
     package: "com.android.server.backup",
     srcs: ["flags.aconfig"],
 }
-
-java_aconfig_library {
-    name: "backup_flags_lib",
-    aconfig_declarations: "backup_flags",
-}
diff --git a/services/backup/flags.aconfig b/services/backup/flags.aconfig
index 1416c88..6a63b3a 100644
--- a/services/backup/flags.aconfig
+++ b/services/backup/flags.aconfig
@@ -24,4 +24,12 @@
     description: "Enables the write buffer to pipes to be of maximum size."
     bug: "265976737"
     is_fixed_read_only: true
+}
+
+flag {
+    name: "enable_clear_pipe_after_restore_file"
+    namespace: "onboarding"
+    description: "Enables clearing the pipe buffer after restoring a single file to a BackupAgent."
+    bug: "320633449"
+    is_fixed_read_only: true
 }
\ No newline at end of file
diff --git a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
index 0559708..d85dd87 100644
--- a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
+++ b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
@@ -732,6 +732,7 @@
                     BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
                     monitoringExtras);
             EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE);
+            mStatus = BackupTransport.TRANSPORT_ERROR;
             nextState = UnifiedRestoreState.FINAL;
         } finally {
             executeNextState(nextState);
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 8e35b74..89896c3 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -211,7 +211,6 @@
         "com_android_wm_shell_flags_lib",
         "com.android.server.utils_aconfig-java",
         "service-jobscheduler-deviceidle.flags-aconfig-java",
-        "backup_flags_lib",
         "policy_flags_lib",
         "net_flags_lib",
         "stats_flags_lib",
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 7aafda5..1207616 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -162,6 +162,7 @@
         "nfc",
         "pdf_viewer",
         "pixel_audio_android",
+        "pixel_biometrics_face",
         "pixel_bluetooth",
         "pixel_connectivity_gps",
         "pixel_system_sw_video",
diff --git a/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java b/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java
index aa6a0f1..fbd32a6 100644
--- a/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java
@@ -112,10 +112,8 @@
             getLogger().logOnError(getContext(), getOperationContext(),
                     errorCode, vendorCode, getTargetUserId());
             try {
-                if (getListener() != null) {
-                    mShouldSendErrorToClient = false;
-                    getListener().onError(getSensorId(), getCookie(), errorCode, vendorCode);
-                }
+                mShouldSendErrorToClient = false;
+                getListener().onError(getSensorId(), getCookie(), errorCode, vendorCode);
             } catch (RemoteException e) {
                 Slog.w(TAG, "Failed to invoke sendError", e);
             }
@@ -147,9 +145,7 @@
 
         final int errorCode = BiometricConstants.BIOMETRIC_ERROR_CANCELED;
         try {
-            if (getListener() != null) {
-                getListener().onError(getSensorId(), getCookie(), errorCode, 0 /* vendorCode */);
-            }
+            getListener().onError(getSensorId(), getCookie(), errorCode, 0 /* vendorCode */);
         } catch (RemoteException e) {
             Slog.w(TAG, "Failed to invoke sendError", e);
         }
@@ -181,7 +177,7 @@
         }
 
         try {
-            if (getListener() != null && shouldSend) {
+            if (shouldSend) {
                 getListener().onAcquired(getSensorId(), acquiredInfo, vendorCode);
             }
         } catch (RemoteException e) {
diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
index f9568ea..506b456 100644
--- a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
@@ -265,17 +265,13 @@
                 Slog.d(TAG, "Skipping addAuthToken");
             }
             try {
-                if (listener != null) {
-                    if (!mIsRestricted) {
-                        listener.onAuthenticationSucceeded(getSensorId(), identifier, byteToken,
-                                getTargetUserId(), mIsStrongBiometric);
-                    } else {
-                        listener.onAuthenticationSucceeded(getSensorId(), null /* identifier */,
-                                byteToken,
-                                getTargetUserId(), mIsStrongBiometric);
-                    }
+                if (!mIsRestricted) {
+                    listener.onAuthenticationSucceeded(getSensorId(), identifier, byteToken,
+                            getTargetUserId(), mIsStrongBiometric);
                 } else {
-                    Slog.e(TAG, "Received successful auth, but client was not listening");
+                    listener.onAuthenticationSucceeded(getSensorId(), null /* identifier */,
+                            byteToken,
+                            getTargetUserId(), mIsStrongBiometric);
                 }
             } catch (RemoteException e) {
                 Slog.e(TAG, "Unable to notify listener", e);
@@ -301,11 +297,7 @@
                 }
 
                 try {
-                    if (listener != null) {
-                        listener.onAuthenticationFailed(getSensorId());
-                    } else {
-                        Slog.e(TAG, "Received failed auth, but client was not listening");
-                    }
+                    listener.onAuthenticationFailed(getSensorId());
                 } catch (RemoteException e) {
                     Slog.e(TAG, "Unable to notify listener", e);
                     mCallback.onClientFinished(this, false);
diff --git a/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java b/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java
index 0216e49..a408852 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java
@@ -22,6 +22,7 @@
 import android.annotation.Nullable;
 import android.content.Context;
 import android.hardware.biometrics.BiometricConstants;
+import android.hardware.biometrics.IBiometricSensorReceiver;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.Slog;
@@ -55,7 +56,7 @@
 
     @Nullable private IBinder mToken;
     private long mRequestId;
-    @Nullable private ClientMonitorCallbackConverter mListener;
+    @NonNull private ClientMonitorCallbackConverter mListener;
     // Currently only used for authentication client. The cookie generated by BiometricService
     // is never 0.
     private final int mCookie;
@@ -95,7 +96,8 @@
         mContext = context;
         mToken = token;
         mRequestId = -1;
-        mListener = listener;
+        mListener = listener == null ? new ClientMonitorCallbackConverter(
+                new IBiometricSensorReceiver.Default()) : listener;
         mTargetUserId = userId;
         mOwner = owner;
         mCookie = cookie;
@@ -199,7 +201,7 @@
         }
         mToken = null;
         if (clearListener) {
-            mListener = null;
+            mListener = new ClientMonitorCallbackConverter(new IBiometricSensorReceiver.Default());
         }
     }
 
@@ -233,8 +235,8 @@
         return mOwner;
     }
 
-    @Nullable
-    public final ClientMonitorCallbackConverter getListener() {
+    @NonNull
+    protected ClientMonitorCallbackConverter getListener() {
         return mListener;
     }
 
@@ -312,9 +314,7 @@
         final int errorCode = BiometricConstants.BIOMETRIC_ERROR_CANCELED;
         try {
             ClientMonitorCallbackConverter listener = getListener();
-            if (listener != null) {
-                listener.onError(getSensorId(), getCookie(), errorCode, 0 /* vendorCode */);
-            }
+            listener.onError(getSensorId(), getCookie(), errorCode, 0 /* vendorCode */);
         } catch (RemoteException e) {
             Slog.w(TAG, "Failed to invoke sendError", e);
         }
diff --git a/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java
index 2c4d30b..8e7004d 100644
--- a/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java
@@ -82,9 +82,7 @@
 
         final ClientMonitorCallbackConverter listener = getListener();
         try {
-            if (listener != null) {
-                listener.onEnrollResult(identifier, remaining);
-            }
+            listener.onEnrollResult(identifier, remaining);
         } catch (RemoteException e) {
             Slog.e(TAG, "Remote exception", e);
         }
diff --git a/services/core/java/com/android/server/biometrics/sensors/RemovalClient.java b/services/core/java/com/android/server/biometrics/sensors/RemovalClient.java
index 45ffa23..d2ef278 100644
--- a/services/core/java/com/android/server/biometrics/sensors/RemovalClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/RemovalClient.java
@@ -74,13 +74,9 @@
         if (identifier == null) {
             Slog.e(TAG, "identifier was null, skipping onRemove()");
             try {
-                if (getListener() != null) {
-                    getListener().onError(getSensorId(), getCookie(),
-                            BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_REMOVE,
-                            0 /* vendorCode */);
-                } else {
-                    Slog.e(TAG, "Error, listener was null, not sending onError callback");
-                }
+                getListener().onError(getSensorId(), getCookie(),
+                        BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_REMOVE,
+                        0 /* vendorCode */);
             } catch (RemoteException e) {
                 Slog.w(TAG, "Failed to send error to client for onRemoved", e);
             }
@@ -93,9 +89,7 @@
                 identifier.getBiometricId());
 
         try {
-            if (getListener() != null) {
-                getListener().onRemoved(identifier, remaining);
-            }
+            getListener().onRemoved(identifier, remaining);
         } catch (RemoteException e) {
             Slog.w(TAG, "Failed to notify Removed:", e);
         }
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java
index f35de93..415d294 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java
@@ -337,7 +337,7 @@
         onAcquiredInternal(acquireInfo, vendorCode, false /* shouldSend */);
 
         final boolean shouldSend = shouldSendAcquiredMessage(acquireInfo, vendorCode);
-        if (shouldSend && getListener() != null) {
+        if (shouldSend) {
             try {
                 getListener().onAuthenticationFrame(frame);
             } catch (RemoteException e) {
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java
index f5c4529..5f370f2 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java
@@ -152,7 +152,7 @@
         onAcquiredInternal(acquireInfo, vendorCode, false /* shouldSend */);
 
         final boolean shouldSend = shouldSendAcquiredMessage(acquireInfo, vendorCode);
-        if (shouldSend && getListener() != null) {
+        if (shouldSend) {
             try {
                 getListener().onEnrollmentFrame(frame);
             } catch (RemoteException e) {
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClient.java
index e404bd2..cf45eb8 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClient.java
@@ -58,11 +58,6 @@
     void onChallengeGenerated(int sensorId, int userId, long challenge) {
         try {
             final ClientMonitorCallbackConverter listener = getListener();
-            if (listener == null) {
-                Slog.e(TAG, "Listener is null in onChallengeGenerated");
-                mCallback.onClientFinished(this, false /* success */);
-                return;
-            }
             listener.onChallengeGenerated(sensorId, userId, challenge);
             mCallback.onClientFinished(this, true /* success */);
         } catch (RemoteException e) {
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceGetFeatureClient.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceGetFeatureClient.java
index 9812536..47aaeec 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceGetFeatureClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceGetFeatureClient.java
@@ -61,9 +61,7 @@
     @Override
     public void unableToStart() {
         try {
-            if (getListener() != null) {
-                getListener().onFeatureGet(false /* success */, new int[0], new boolean[0]);
-            }
+            getListener().onFeatureGet(false /* success */, new int[0], new boolean[0]);
         } catch (RemoteException e) {
             Slog.e(TAG, "Unable to send error", e);
         }
@@ -85,9 +83,7 @@
             featureState[0] = result.value;
             mValue = result.value;
 
-            if (getListener() != null) {
-                getListener().onFeatureGet(result.status == Status.OK, features, featureState);
-            }
+            getListener().onFeatureGet(result.status == Status.OK, features, featureState);
             mCallback.onClientFinished(this, true /* success */);
         } catch (RemoteException e) {
             Slog.e(TAG, "Unable to getFeature", e);
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
index 6912961..e0fd44b 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
@@ -450,9 +450,7 @@
                         pc.major);
             }
 
-            if (getListener() != null) {
-                getListener().onUdfpsPointerDown(getSensorId());
-            }
+            getListener().onUdfpsPointerDown(getSensorId());
         } catch (RemoteException e) {
             Slog.e(TAG, "Remote exception", e);
         }
@@ -471,9 +469,7 @@
                 session.getSession().onPointerUp(pc.pointerId);
             }
 
-            if (getListener() != null) {
-                getListener().onUdfpsPointerUp(getSensorId());
-            }
+            getListener().onUdfpsPointerUp(getSensorId());
         } catch (RemoteException e) {
             Slog.e(TAG, "Remote exception", e);
         }
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java
index a7fb774..cb220b9e 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java
@@ -175,11 +175,7 @@
         vibrateSuccess();
 
         try {
-            if (getListener() != null) {
-                getListener().onDetected(getSensorId(), getTargetUserId(), mIsStrongBiometric);
-            } else {
-                Slog.e(TAG, "Listener is null!");
-            }
+            getListener().onDetected(getSensorId(), getTargetUserId(), mIsStrongBiometric);
             mCallback.onClientFinished(this, true /* success */);
         } catch (RemoteException e) {
             Slog.e(TAG, "Remote exception when sending onDetected", e);
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
index 3fb9223..225bd59 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
@@ -305,9 +305,7 @@
                         pc.major);
             }
 
-            if (getListener() != null) {
-                getListener().onUdfpsPointerDown(getSensorId());
-            }
+            getListener().onUdfpsPointerDown(getSensorId());
         } catch (RemoteException e) {
             Slog.e(TAG, "Unable to send pointer down", e);
         }
@@ -325,9 +323,7 @@
                 session.getSession().onPointerUp(pc.pointerId);
             }
 
-            if (getListener() != null) {
-                getListener().onUdfpsPointerUp(getSensorId());
-            }
+            getListener().onUdfpsPointerUp(getSensorId());
         } catch (RemoteException e) {
             Slog.e(TAG, "Unable to send pointer up", e);
         }
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGenerateChallengeClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGenerateChallengeClient.java
index ce693ff..d2f36ce 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGenerateChallengeClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGenerateChallengeClient.java
@@ -59,11 +59,6 @@
     void onChallengeGenerated(int sensorId, int userId, long challenge) {
         try {
             final ClientMonitorCallbackConverter listener = getListener();
-            if (listener == null) {
-                Slog.e(TAG, "Listener is null in onChallengeGenerated");
-                mCallback.onClientFinished(this, false /* success */);
-                return;
-            }
             listener.onChallengeGenerated(sensorId, userId, challenge);
             mCallback.onClientFinished(this, true /* success */);
         } catch (RemoteException e) {
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21UdfpsMock.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21UdfpsMock.java
index 9232e11..f857946 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21UdfpsMock.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21UdfpsMock.java
@@ -28,6 +28,7 @@
 import android.hardware.fingerprint.FingerprintManager.AuthenticationResult;
 import android.hardware.fingerprint.FingerprintSensorProperties;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
+import android.hardware.fingerprint.IFingerprintServiceReceiver;
 import android.hardware.fingerprint.IUdfpsOverlayController;
 import android.os.Handler;
 import android.os.IBinder;
@@ -367,7 +368,8 @@
             final IBinder token = client.getToken();
             final long operationId = authClient.getOperationId();
             final int cookie = client.getCookie();
-            final ClientMonitorCallbackConverter listener = client.getListener();
+            final ClientMonitorCallbackConverter listener = new ClientMonitorCallbackConverter(
+                    new IFingerprintServiceReceiver.Default());
             final boolean restricted = authClient.isRestricted();
             final int statsClient = client.getLogger().getStatsClient();
             final boolean isKeyguard = authClient.isKeyguard();
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java
index 7a329e9..60c532c 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java
@@ -279,12 +279,10 @@
         mALSProbeCallback.getProbe().enable();
         UdfpsHelper.onFingerDown(getFreshDaemon(), (int) pc.x, (int) pc.y, pc.minor, pc.major);
 
-        if (getListener() != null) {
-            try {
-                getListener().onUdfpsPointerDown(getSensorId());
-            } catch (RemoteException e) {
-                Slog.e(TAG, "Remote exception", e);
-            }
+        try {
+            getListener().onUdfpsPointerDown(getSensorId());
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Remote exception", e);
         }
     }
 
@@ -295,12 +293,10 @@
         mALSProbeCallback.getProbe().disable();
         UdfpsHelper.onFingerUp(getFreshDaemon());
 
-        if (getListener() != null) {
-            try {
-                getListener().onUdfpsPointerUp(getSensorId());
-            } catch (RemoteException e) {
-                Slog.e(TAG, "Remote exception", e);
-            }
+        try {
+            getListener().onUdfpsPointerUp(getSensorId());
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Remote exception", e);
         }
     }
 
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java
index 6e029d2..50e48fe 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java
@@ -153,12 +153,10 @@
         final PerformanceTracker pm = PerformanceTracker.getInstanceForSensorId(getSensorId());
         pm.incrementAuthForUser(getTargetUserId(), authenticated);
 
-        if (getListener() != null) {
-            try {
-                getListener().onDetected(getSensorId(), getTargetUserId(), mIsStrongBiometric);
-            } catch (RemoteException e) {
-                Slog.e(TAG, "Remote exception when sending onDetected", e);
-            }
+        try {
+            getListener().onDetected(getSensorId(), getTargetUserId(), mIsStrongBiometric);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Remote exception when sending onDetected", e);
         }
     }
 
diff --git a/services/core/java/com/android/server/input/InputSettingsObserver.java b/services/core/java/com/android/server/input/InputSettingsObserver.java
index 165dfe4..5ffc380 100644
--- a/services/core/java/com/android/server/input/InputSettingsObserver.java
+++ b/services/core/java/com/android/server/input/InputSettingsObserver.java
@@ -64,6 +64,8 @@
                         (reason) -> updateTouchpadNaturalScrollingEnabled()),
                 Map.entry(Settings.System.getUriFor(Settings.System.TOUCHPAD_TAP_TO_CLICK),
                         (reason) -> updateTouchpadTapToClickEnabled()),
+                Map.entry(Settings.System.getUriFor(Settings.System.TOUCHPAD_TAP_DRAGGING),
+                        (reason) -> updateTouchpadTapDraggingEnabled()),
                 Map.entry(Settings.System.getUriFor(Settings.System.TOUCHPAD_RIGHT_CLICK_ZONE),
                         (reason) -> updateTouchpadRightClickZoneEnabled()),
                 Map.entry(Settings.System.getUriFor(Settings.System.SHOW_TOUCHES),
@@ -158,6 +160,10 @@
         mNative.setTouchpadTapToClickEnabled(InputSettings.useTouchpadTapToClick(mContext));
     }
 
+    private void updateTouchpadTapDraggingEnabled() {
+        mNative.setTouchpadTapDraggingEnabled(InputSettings.useTouchpadTapDragging(mContext));
+    }
+
     private void updateTouchpadRightClickZoneEnabled() {
         mNative.setTouchpadRightClickZoneEnabled(InputSettings.useTouchpadRightClickZone(mContext));
     }
diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java
index bc82078..e5f3484 100644
--- a/services/core/java/com/android/server/input/NativeInputManagerService.java
+++ b/services/core/java/com/android/server/input/NativeInputManagerService.java
@@ -129,6 +129,8 @@
 
     void setTouchpadTapToClickEnabled(boolean enabled);
 
+    void setTouchpadTapDraggingEnabled(boolean enabled);
+
     void setTouchpadRightClickZoneEnabled(boolean enabled);
 
     void setShowTouches(boolean enabled);
@@ -377,6 +379,9 @@
         public native void setTouchpadTapToClickEnabled(boolean enabled);
 
         @Override
+        public native void setTouchpadTapDraggingEnabled(boolean enabled);
+
+        @Override
         public native void setTouchpadRightClickZoneEnabled(boolean enabled);
 
         @Override
diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java
index b5346a3..bc0173a 100644
--- a/services/core/java/com/android/server/pm/LauncherAppsService.java
+++ b/services/core/java/com/android/server/pm/LauncherAppsService.java
@@ -18,6 +18,10 @@
 
 import static android.Manifest.permission.READ_FRAME_BUFFER;
 import static android.app.ActivityOptions.KEY_SPLASH_SCREEN_THEME;
+import static android.app.AppOpsManager.MODE_ALLOWED;
+import static android.app.AppOpsManager.MODE_IGNORED;
+import static android.app.AppOpsManager.OP_ARCHIVE_ICON_OVERLAY;
+import static android.app.AppOpsManager.OP_UNARCHIVAL_CONFIRMATION;
 import static android.app.ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED;
 import static android.app.ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED;
 import static android.app.PendingIntent.FLAG_IMMUTABLE;
@@ -43,6 +47,7 @@
 import android.app.ActivityManagerInternal;
 import android.app.ActivityOptions;
 import android.app.AppGlobals;
+import android.app.AppOpsManager;
 import android.app.IApplicationThread;
 import android.app.PendingIntent;
 import android.app.admin.DevicePolicyCache;
@@ -213,6 +218,7 @@
         private final ActivityTaskManagerInternal mActivityTaskManagerInternal;
         private final ShortcutServiceInternal mShortcutServiceInternal;
         private final PackageManagerInternal mPackageManagerInternal;
+        private final AppOpsManager mAppOpsManager;
         private final PackageCallbackList<IOnAppsChangedListener> mListeners
                 = new PackageCallbackList<IOnAppsChangedListener>();
         private final DevicePolicyManager mDpm;
@@ -253,6 +259,7 @@
                     LocalServices.getService(ShortcutServiceInternal.class));
             mPackageManagerInternal = Objects.requireNonNull(
                     LocalServices.getService(PackageManagerInternal.class));
+            mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
             mShortcutServiceInternal.addListener(mPackageMonitor);
             mShortcutChangeHandler = new ShortcutChangeHandler(mUserManagerInternal);
             mShortcutServiceInternal.addShortcutChangeCallback(mShortcutChangeHandler);
@@ -1997,6 +2004,23 @@
             }
         }
 
+        @Override
+        public void setArchiveCompatibilityOptions(boolean enableIconOverlay,
+                boolean enableUnarchivalConfirmation) {
+            int callingUid = Binder.getCallingUid();
+            Binder.withCleanCallingIdentity(
+                    () -> {
+                        mAppOpsManager.setUidMode(
+                                OP_ARCHIVE_ICON_OVERLAY,
+                                callingUid,
+                                enableIconOverlay ? MODE_ALLOWED : MODE_IGNORED);
+                        mAppOpsManager.setUidMode(
+                                OP_UNARCHIVAL_CONFIRMATION,
+                                callingUid,
+                                enableUnarchivalConfirmation ? MODE_ALLOWED : MODE_IGNORED);
+                    });
+        }
+
         /** Checks if user is a profile of or same as listeningUser.
          * and the user is enabled. */
         private boolean isEnabledProfileOf(UserHandle listeningUser, UserHandle user,
diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java
index 32f5646..474b590 100644
--- a/services/core/java/com/android/server/pm/PackageArchiver.java
+++ b/services/core/java/com/android/server/pm/PackageArchiver.java
@@ -20,6 +20,7 @@
 import static android.app.ActivityManager.START_CLASS_NOT_FOUND;
 import static android.app.ActivityManager.START_PERMISSION_DENIED;
 import static android.app.ActivityManager.START_SUCCESS;
+import static android.app.AppOpsManager.MODE_ALLOWED;
 import static android.app.AppOpsManager.MODE_IGNORED;
 import static android.app.ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED;
 import static android.content.pm.ArchivedActivityInfo.bytesFromBitmap;
@@ -275,11 +276,12 @@
         Slog.i(TAG, TextUtils.formatSimple("Unarchival is starting for: %s", packageName));
 
         try {
-            // TODO(b/311709794) Make showUnarchivalConfirmation dependent on the compat options.
             requestUnarchive(packageName, callerPackageName,
                     getOrCreateLauncherListener(userId, packageName),
                     UserHandle.of(userId),
-                    false /* showUnarchivalConfirmation= */);
+                    getAppOpsManager().checkOp(
+                            AppOpsManager.OP_UNARCHIVAL_CONFIRMATION, callingUid, callerPackageName)
+                            == MODE_ALLOWED);
         } catch (Throwable t) {
             Slog.e(TAG, TextUtils.formatSimple(
                     "Unexpected error occurred while unarchiving package %s: %s.", packageName,
@@ -796,7 +798,8 @@
      * <p> The icon is returned without any treatment/overlay. In the rare case the app had multiple
      * launcher activities, only one of the icons is returned arbitrarily.
      */
-    public Bitmap getArchivedAppIcon(@NonNull String packageName, @NonNull UserHandle user) {
+    public Bitmap getArchivedAppIcon(@NonNull String packageName, @NonNull UserHandle user,
+            String callingPackageName) {
         Objects.requireNonNull(packageName);
         Objects.requireNonNull(user);
 
@@ -819,7 +822,13 @@
         // TODO(b/298452477) Handle monochrome icons.
         // In the rare case the archived app defined more than two launcher activities, we choose
         // the first one arbitrarily.
-        return includeCloudOverlay(decodeIcon(archiveState.getActivityInfos().get(0)));
+        Bitmap icon = decodeIcon(archiveState.getActivityInfos().get(0));
+        if (getAppOpsManager().checkOp(
+                AppOpsManager.OP_ARCHIVE_ICON_OVERLAY, callingUid, callingPackageName)
+                == MODE_ALLOWED) {
+            icon = includeCloudOverlay(icon);
+        }
+        return icon;
     }
 
     /**
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 9617098..dfe705a7 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -6414,8 +6414,10 @@
         }
 
         @Override
-        public Bitmap getArchivedAppIcon(@NonNull String packageName, @NonNull UserHandle user) {
-            return mInstallerService.mPackageArchiver.getArchivedAppIcon(packageName, user);
+        public Bitmap getArchivedAppIcon(@NonNull String packageName, @NonNull UserHandle user,
+                @NonNull String callingPackageName) {
+            return mInstallerService.mPackageArchiver.getArchivedAppIcon(packageName, user,
+                    callingPackageName);
         }
 
         @Override
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index 7cf1d33..1ec37f4 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -4537,6 +4537,8 @@
         t.traceBegin("createNewUser-" + userHandle);
         Installer.Batch batch = new Installer.Batch();
         final boolean skipPackageAllowList = userTypeInstallablePackages == null;
+        // Use the same timestamp for all system apps that are to be installed on the new user
+        final long currentTimeMillis = System.currentTimeMillis();
         synchronized (mLock) {
             final int size = mPackages.size();
             for (int i = 0; i < size; i++) {
@@ -4552,6 +4554,9 @@
                                 ps.getPackageName()));
                 // Only system apps are initially installed.
                 ps.setInstalled(shouldReallyInstall, userHandle);
+                if (Flags.fixSystemAppsFirstInstallTime() && shouldReallyInstall) {
+                    ps.setFirstInstallTime(currentTimeMillis, userHandle);
+                }
 
                 // Non-Apex system apps, that are not included in the allowlist in
                 // initialNonStoppedSystemPackages, should be marked as stopped by default.
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 1fdcc64..51790b8 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -75,6 +75,7 @@
 import static android.view.WindowManagerGlobal.ADD_PERMISSION_DENIED;
 import static android.view.contentprotection.flags.Flags.createAccessibilityOverlayAppOpEnabled;
 
+import static com.android.hardware.input.Flags.emojiAndScreenshotKeycodesAvailable;
 import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.SCREENSHOT_KEYCHORD_DELAY;
 import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.CAMERA_LENS_COVERED;
 import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.CAMERA_LENS_COVER_ABSENT;
@@ -3779,6 +3780,11 @@
                     sendSystemKeyToStatusBarAsync(event);
                     return true;
                 }
+            case KeyEvent.KEYCODE_SCREENSHOT:
+                if (emojiAndScreenshotKeycodesAvailable() && down && repeatCount == 0) {
+                    interceptScreenshotChord(SCREENSHOT_KEY_OTHER, 0 /*pressDelay*/);
+                }
+                return true;
         }
         if (isValidGlobalKey(keyCode)
                 && mGlobalKeyManager.handleGlobalKey(mContext, keyCode, event)) {
@@ -5022,6 +5028,12 @@
             case KeyEvent.KEYCODE_MACRO_4:
                 result &= ~ACTION_PASS_TO_USER;
                 break;
+            case KeyEvent.KEYCODE_EMOJI_PICKER:
+                if (!emojiAndScreenshotKeycodesAvailable()) {
+                    // Don't allow EMOJI_PICKER key to be dispatched until flag is released.
+                    result &= ~ACTION_PASS_TO_USER;
+                }
+                break;
         }
 
         if (useHapticFeedback) {
diff --git a/services/core/java/com/android/server/trust/TrustAgentWrapper.java b/services/core/java/com/android/server/trust/TrustAgentWrapper.java
index 3abebf8..d102054 100644
--- a/services/core/java/com/android/server/trust/TrustAgentWrapper.java
+++ b/services/core/java/com/android/server/trust/TrustAgentWrapper.java
@@ -443,6 +443,8 @@
                 mPendingSuccessfulUnlock = false;
             }
 
+            // It's okay to use the "Inner" version of isDeviceLocked since they differ only for
+            // profiles, which cannot be switched to and thus don't support trust agents anyway.
             if (mTrustManagerService.isDeviceLockedInner(mUserId)) {
                 onDeviceLocked();
             } else {
diff --git a/services/core/java/com/android/server/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java
index dbf777f..2b05993 100644
--- a/services/core/java/com/android/server/trust/TrustManagerService.java
+++ b/services/core/java/com/android/server/trust/TrustManagerService.java
@@ -188,25 +188,30 @@
             new SparseArray<>();
 
     /**
-     * Stores the locked state for users on the device. There are three different type of users
+     * Stores the locked state for users on the device. There are several different types of users
      * which are handled slightly differently:
      * <ul>
-     *  <li> Users with real keyguard
+     *  <li> Users with real keyguard:
      *  These are users who can be switched to ({@link UserInfo#supportsSwitchToByUser()}). Their
      *  locked state is derived by a combination of user secure state, keyguard state, trust agent
      *  decision and biometric authentication result. These are updated via
      *  {@link #refreshDeviceLockedForUser(int)} and result stored in {@link #mDeviceLockedForUser}.
-     *  <li> Managed profiles with unified challenge
-     *  Managed profile with unified challenge always shares the same locked state as their parent,
+     *  <li> Profiles with unified challenge:
+     *  Profiles with a unified challenge always share the same locked state as their parent,
      *  so their locked state is not recorded in  {@link #mDeviceLockedForUser}. Instead,
      *  {@link ITrustManager#isDeviceLocked(int)} always resolves their parent user handle and
      *  queries its locked state instead.
-     *  <li> Managed profiles with separate challenge
-     *  Locked state for profile with separate challenge is determined by other parts of the
-     *  framework (mostly PowerManager) and pushed to TrustManagerService via
-     *  {@link ITrustManager#setDeviceLockedForUser(int, boolean)}. Although in a corner case when
-     *  the profile has a separate but empty challenge, setting its {@link #mDeviceLockedForUser} to
-     *  {@code false} is actually done by {@link #refreshDeviceLockedForUser(int)}.
+     *  <li> Profiles without unified challenge:
+     *  The locked state for profiles that do not have a unified challenge (e.g. they have a
+     *  separate challenge from their parent, or they have no parent at all) is determined by other
+     *  parts of the framework (mostly PowerManager) and pushed to TrustManagerService via
+     *  {@link ITrustManager#setDeviceLockedForUser(int, boolean)}.
+     *  However, in the case where such a profile has an empty challenge, setting its
+     *  {@link #mDeviceLockedForUser} to {@code false} is actually done by
+     *  {@link #refreshDeviceLockedForUser(int)}.
+     *  (This serves as a corner case for managed profiles with a separate but empty challenge. It
+     *  is always currently the case for Communal profiles, for which having a non-empty challenge
+     *  is not currently supported.)
      * </ul>
      * TODO: Rename {@link ITrustManager#setDeviceLockedForUser(int, boolean)} to
      * {@code setDeviceLockedForProfile} to better reflect its purpose. Unifying
@@ -794,7 +799,7 @@
 
     /**
      * Update the user's locked state. Only applicable to users with a real keyguard
-     * ({@link UserInfo#supportsSwitchToByUser}) and unsecured managed profiles.
+     * ({@link UserInfo#supportsSwitchToByUser}) and unsecured profiles.
      *
      * If this is called due to an unlock operation set unlockedUser to prevent the lock from
      * being prematurely reset for that user while keyguard is still in the process of going away.
@@ -826,7 +831,11 @@
             boolean secure = mLockPatternUtils.isSecure(id);
 
             if (!info.supportsSwitchToByUser()) {
-                if (info.isManagedProfile() && !secure) {
+                if (info.isProfile() && !secure
+                        && !mLockPatternUtils.isProfileWithUnifiedChallenge(id)) {
+                    // Unsecured profiles need to be explicitly set to false.
+                    // However, Unified challenge profiles officially shouldn't have a presence in
+                    // mDeviceLockedForUser at all, since that's not how they're tracked.
                     setDeviceLockedForUser(id, false);
                 }
                 continue;
@@ -1853,6 +1862,7 @@
         }
     }
 
+    /** If the userId has a parent, returns that parent's userId. Otherwise userId is returned. */
     private int resolveProfileParent(int userId) {
         final long identity = Binder.clearCallingIdentity();
         try {
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 03d55d9..b1d04c9 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -545,9 +545,6 @@
     boolean launchFailed;   // set if a launched failed, to abort on 2nd try
     boolean delayedResume;  // not yet resumed because of stopped app switches?
     boolean finishing;      // activity in pending finish list?
-    boolean deferRelaunchUntilPaused;   // relaunch of activity is being deferred until pause is
-                                        // completed
-    boolean preserveWindowOnDeferredRelaunch; // activity windows are preserved on deferred relaunch
     int configChangeFlags;  // which config values have changed
     private boolean keysPaused;     // has key dispatching been paused for it?
     int launchMode;         // the launch mode activity attribute.
@@ -1277,10 +1274,8 @@
         if (mDeferHidingClient) {
             pw.println(prefix + "mDeferHidingClient=" + mDeferHidingClient);
         }
-        if (deferRelaunchUntilPaused || configChangeFlags != 0) {
-            pw.print(prefix); pw.print("deferRelaunchUntilPaused=");
-                    pw.print(deferRelaunchUntilPaused);
-                    pw.print(" configChangeFlags=");
+        if (configChangeFlags != 0) {
+            pw.print(prefix); pw.print(" configChangeFlags=");
                     pw.println(Integer.toHexString(configChangeFlags));
         }
         if (mServiceConnectionsHolder != null) {
@@ -2137,7 +2132,6 @@
         launchFailed = false;
         delayedResume = false;
         finishing = false;
-        deferRelaunchUntilPaused = false;
         keysPaused = false;
         inHistory = false;
         nowVisible = false;
@@ -4096,8 +4090,6 @@
         // Clean up the splash screen if it was still displayed.
         cleanUpSplashScreen();
 
-        deferRelaunchUntilPaused = false;
-
         if (setState) {
             setState(DESTROYED, "cleanUp");
             if (DEBUG_APP) Slog.v(TAG_APP, "Clearing app during cleanUp for activity " + this);
@@ -6481,9 +6473,6 @@
             mAppStopped = true;
             ProtoLog.v(WM_DEBUG_STATES, "Stop failed; moving to STOPPED: %s", this);
             setState(STOPPED, "stopIfPossible");
-            if (deferRelaunchUntilPaused) {
-                destroyImmediately("stop-except");
-            }
         }
     }
 
@@ -6539,12 +6528,7 @@
         if (finishing) {
             abortAndClearOptionsAnimation();
         } else {
-            if (deferRelaunchUntilPaused) {
-                destroyImmediately("stop-config");
-                mRootWindowContainer.resumeFocusedTasksTopActivities();
-            } else {
-                mAtmService.updatePreviousProcess(this);
-            }
+            mAtmService.updatePreviousProcess(this);
         }
         mTaskSupervisor.checkReadyForSleepLocked(true /* allowDelay */);
     }
@@ -9724,23 +9708,12 @@
             } else {
                 mRelaunchReason = RELAUNCH_REASON_NONE;
             }
-            if (mState == PAUSING) {
-                // A little annoying: we are waiting for this activity to finish pausing. Let's not
-                // do anything now, but just flag that it needs to be restarted when done pausing.
-                ProtoLog.v(WM_DEBUG_CONFIGURATION,
-                        "Config is skipping already pausing %s", this);
-                deferRelaunchUntilPaused = true;
-                preserveWindowOnDeferredRelaunch = preserveWindow;
-                return true;
-            } else {
-                ProtoLog.v(WM_DEBUG_CONFIGURATION, "Config is relaunching %s",
-                        this);
-                if (!mVisibleRequested) {
-                    ProtoLog.v(WM_DEBUG_STATES, "Config is relaunching invisible "
-                            + "activity %s called by %s", this, Debug.getCallers(4));
-                }
-                relaunchActivityLocked(preserveWindow);
+            ProtoLog.v(WM_DEBUG_CONFIGURATION, "Config is relaunching %s", this);
+            if (!mVisibleRequested) {
+                ProtoLog.v(WM_DEBUG_STATES, "Config is relaunching invisible "
+                        + "activity %s called by %s", this, Debug.getCallers(4));
             }
+            relaunchActivityLocked(preserveWindow);
 
             // All done...  tell the caller we weren't able to keep this activity around.
             return false;
@@ -9958,8 +9931,6 @@
         mTaskSupervisor.mStoppingActivities.remove(this);
 
         configChangeFlags = 0;
-        deferRelaunchUntilPaused = false;
-        preserveWindowOnDeferredRelaunch = false;
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index 83ccbdc..13f6a5f 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -1527,6 +1527,12 @@
                         setLaunchBehind(visibleOpenActivities[i]);
                     }
                 }
+                // Force update mLastSurfaceShowing for opening activity and its task.
+                if (mWindowManagerService.mRoot.mTransitionController.isShellTransitionsEnabled()) {
+                    for (int i = visibleOpenActivities.length - 1; i >= 0; --i) {
+                        WindowContainer.enforceSurfaceVisible(visibleOpenActivities[i]);
+                    }
+                }
             }
 
             @Nullable Runnable build() {
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index e2bc59b..a7bbc25 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -1913,12 +1913,9 @@
              */
             final Rect mConfigFrame = new Rect();
 
-            /** The count of insets sources when calculating this info. */
-            int mLastInsetsSourceCount;
-
             private boolean mNeedUpdate = true;
 
-            void update(DisplayContent dc, int rotation, int w, int h) {
+            InsetsState update(DisplayContent dc, int rotation, int w, int h) {
                 final DisplayFrames df = new DisplayFrames();
                 dc.updateDisplayFrames(df, rotation, w, h);
                 dc.getDisplayPolicy().simulateLayoutDisplay(df);
@@ -1935,8 +1932,8 @@
                 mNonDecorFrame.inset(mNonDecorInsets);
                 mConfigFrame.set(displayFrame);
                 mConfigFrame.inset(mConfigInsets);
-                mLastInsetsSourceCount = dc.getDisplayPolicy().mInsetsSourceWindowsExceptIme.size();
                 mNeedUpdate = false;
+                return insetsState;
             }
 
             void set(Info other) {
@@ -1944,7 +1941,6 @@
                 mConfigInsets.set(other.mConfigInsets);
                 mNonDecorFrame.set(other.mNonDecorFrame);
                 mConfigFrame.set(other.mConfigFrame);
-                mLastInsetsSourceCount = other.mLastInsetsSourceCount;
                 mNeedUpdate = false;
             }
 
@@ -1997,6 +1993,29 @@
             }
         }
 
+        static boolean hasInsetsFrameDiff(InsetsState s1, InsetsState s2, int insetsTypes) {
+            int insetsCount1 = 0;
+            for (int i = s1.sourceSize() - 1; i >= 0; i--) {
+                final InsetsSource source1 = s1.sourceAt(i);
+                if ((source1.getType() & insetsTypes) == 0) {
+                    continue;
+                }
+                insetsCount1++;
+                final InsetsSource source2 = s2.peekSource(source1.getId());
+                if (source2 == null || !source2.getFrame().equals(source1.getFrame())) {
+                    return true;
+                }
+            }
+            int insetsCount2 = 0;
+            for (int i = s2.sourceSize() - 1; i >= 0; i--) {
+                final InsetsSource source2 = s2.sourceAt(i);
+                if ((source2.getType() & insetsTypes) != 0) {
+                    insetsCount2++;
+                }
+            }
+            return insetsCount1 != insetsCount2;
+        }
+
         private static class Cache {
             /**
              * If {@link #mPreserveId} is this value, it is in the middle of updating display
@@ -2031,12 +2050,14 @@
         final int dw = displayFrames.mWidth;
         final int dh = displayFrames.mHeight;
         final DecorInsets.Info newInfo = mDecorInsets.mTmpInfo;
-        newInfo.update(mDisplayContent, rotation, dw, dh);
+        final InsetsState newInsetsState = newInfo.update(mDisplayContent, rotation, dw, dh);
         final DecorInsets.Info currentInfo = getDecorInsetsInfo(rotation, dw, dh);
         if (newInfo.mConfigFrame.equals(currentInfo.mConfigFrame)) {
             // Even if the config frame is not changed in current rotation, it may change the
-            // insets in other rotations if the source count is changed.
-            if (newInfo.mLastInsetsSourceCount != currentInfo.mLastInsetsSourceCount) {
+            // insets in other rotations if the frame of insets source is changed.
+            final InsetsState currentInsetsState = mDisplayContent.mDisplayFrames.mInsetsState;
+            if (DecorInsets.hasInsetsFrameDiff(
+                    newInsetsState, currentInsetsState, mService.mConfigTypes)) {
                 for (int i = mDecorInsets.mInfoForRotation.length - 1; i >= 0; i--) {
                     if (i != rotation) {
                         final boolean flipSize = (i + rotation) % 2 == 1;
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 11e7bb0..838ce86 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -1915,11 +1915,7 @@
                 ProtoLog.v(WM_DEBUG_STATES, "Enqueue pending stop if needed: %s "
                                 + "wasStopping=%b visibleRequested=%b",  prev,  wasStopping,
                         prev.isVisibleRequested());
-                if (prev.deferRelaunchUntilPaused) {
-                    // Complete the deferred relaunch that was waiting for pause to complete.
-                    ProtoLog.v(WM_DEBUG_STATES, "Re-launching after pause: %s", prev);
-                    prev.relaunchActivityLocked(prev.preserveWindowOnDeferredRelaunch);
-                } else if (wasStopping) {
+                if (wasStopping) {
                     // We are also stopping, the stop request must have gone soon after the pause.
                     // We can't clobber it, because the stop confirmation will not be handled.
                     // We don't need to schedule another stop, we only need to let it happen.
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index 25b5630f..70775530 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -994,39 +994,18 @@
         Slog.e(TAG, "Set visible without transition " + wc + " playing=" + isPlaying
                 + " caller=" + caller);
         if (!isPlaying) {
-            enforceSurfaceVisible(wc);
+            WindowContainer.enforceSurfaceVisible(wc);
             return;
         }
         // Update surface visibility after the playing transitions are finished, so the last
         // visibility won't be replaced by the finish transaction of transition.
         mStateValidators.add(() -> {
             if (wc.isVisibleRequested()) {
-                enforceSurfaceVisible(wc);
+                WindowContainer.enforceSurfaceVisible(wc);
             }
         });
     }
 
-    private void enforceSurfaceVisible(WindowContainer<?> wc) {
-        if (wc.mSurfaceControl == null) return;
-        wc.getSyncTransaction().show(wc.mSurfaceControl);
-        final ActivityRecord ar = wc.asActivityRecord();
-        if (ar != null) {
-            ar.mLastSurfaceShowing = true;
-        }
-        // Force showing the parents because they may be hidden by previous transition.
-        for (WindowContainer<?> p = wc.getParent(); p != null && p != wc.mDisplayContent;
-                p = p.getParent()) {
-            if (p.mSurfaceControl != null) {
-                p.getSyncTransaction().show(p.mSurfaceControl);
-                final Task task = p.asTask();
-                if (task != null) {
-                    task.mLastSurfaceShowing = true;
-                }
-            }
-        }
-        wc.scheduleAnimation();
-    }
-
     /**
      * Called when the transition has a complete set of participants for its operation. In other
      * words, it is when the transition is "ready" but is still waiting for participants to draw.
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 286182e..2d2857a 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -3625,6 +3625,29 @@
         return mSurfaceControl.getHeight();
     }
 
+    static void enforceSurfaceVisible(@NonNull WindowContainer<?> wc) {
+        if (wc.mSurfaceControl == null) {
+            return;
+        }
+        wc.getSyncTransaction().show(wc.mSurfaceControl);
+        final ActivityRecord ar = wc.asActivityRecord();
+        if (ar != null) {
+            ar.mLastSurfaceShowing = true;
+        }
+        // Force showing the parents because they may be hidden by previous transition.
+        for (WindowContainer<?> p = wc.getParent(); p != null && p != wc.mDisplayContent;
+                p = p.getParent()) {
+            if (p.mSurfaceControl != null) {
+                p.getSyncTransaction().show(p.mSurfaceControl);
+                final Task task = p.asTask();
+                if (task != null) {
+                    task.mLastSurfaceShowing = true;
+                }
+            }
+        }
+        wc.scheduleAnimation();
+    }
+
     @CallSuper
     void dump(PrintWriter pw, String prefix, boolean dumpAll) {
         if (mSurfaceAnimator.isAnimating()) {
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 3f889c0..8cd399f 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -1471,12 +1471,11 @@
                         final int index = task.mChildren.indexOf(topTaskFragment);
                         task.mChildren.remove(taskFragment);
                         task.mChildren.add(index, taskFragment);
-                        if (taskFragment.hasChild()) {
-                            effects |= TRANSACT_EFFECTS_LIFECYCLE;
-                        } else {
+                        if (!taskFragment.hasChild()) {
                             // Ensure that the child layers are updated if the TaskFragment is empty
                             task.assignChildLayers();
                         }
+                        effects |= TRANSACT_EFFECTS_LIFECYCLE;
                     }
                 }
                 break;
@@ -1491,12 +1490,11 @@
                 if (task != null) {
                     task.mChildren.remove(taskFragment);
                     task.mChildren.add(0, taskFragment);
-                    if (taskFragment.hasChild()) {
-                        effects |= TRANSACT_EFFECTS_LIFECYCLE;
-                    } else {
+                    if (!taskFragment.hasChild()) {
                         // Ensure that the child layers are updated if the TaskFragment is empty.
                         task.assignChildLayers();
                     }
+                    effects |= TRANSACT_EFFECTS_LIFECYCLE;
                 }
                 break;
             }
@@ -1505,12 +1503,11 @@
                 if (task != null) {
                     task.mChildren.remove(taskFragment);
                     task.mChildren.add(taskFragment);
-                    if (taskFragment.hasChild()) {
-                        effects |= TRANSACT_EFFECTS_LIFECYCLE;
-                    } else {
+                    if (!taskFragment.hasChild()) {
                         // Ensure that the child layers are updated if the TaskFragment is empty.
                         task.assignChildLayers();
                     }
+                    effects |= TRANSACT_EFFECTS_LIFECYCLE;
                 }
                 break;
             }
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index cbc301b..4a6b31c 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -290,6 +290,7 @@
     void setTouchpadPointerSpeed(int32_t speed);
     void setTouchpadNaturalScrollingEnabled(bool enabled);
     void setTouchpadTapToClickEnabled(bool enabled);
+    void setTouchpadTapDraggingEnabled(bool enabled);
     void setTouchpadRightClickZoneEnabled(bool enabled);
     void setInputDeviceEnabled(uint32_t deviceId, bool enabled);
     void setShowTouches(bool enabled);
@@ -440,6 +441,9 @@
         // True to enable tap-to-click on touchpads.
         bool touchpadTapToClickEnabled{true};
 
+        // True to enable tap dragging on touchpads.
+        bool touchpadTapDraggingEnabled{false};
+
         // True to enable a zone on the right-hand side of touchpads where clicks will be turned
         // into context (a.k.a. "right") clicks.
         bool touchpadRightClickZoneEnabled{false};
@@ -697,6 +701,7 @@
         outConfig->touchpadPointerSpeed = mLocked.touchpadPointerSpeed;
         outConfig->touchpadNaturalScrollingEnabled = mLocked.touchpadNaturalScrollingEnabled;
         outConfig->touchpadTapToClickEnabled = mLocked.touchpadTapToClickEnabled;
+        outConfig->touchpadTapDraggingEnabled = mLocked.touchpadTapDraggingEnabled;
         outConfig->touchpadRightClickZoneEnabled = mLocked.touchpadRightClickZoneEnabled;
 
         outConfig->disabledDevices = mLocked.disabledInputDevices;
@@ -1301,6 +1306,22 @@
             InputReaderConfiguration::Change::TOUCHPAD_SETTINGS);
 }
 
+void NativeInputManager::setTouchpadTapDraggingEnabled(bool enabled) {
+    { // acquire lock
+        std::scoped_lock _l(mLock);
+
+        if (mLocked.touchpadTapDraggingEnabled == enabled) {
+            return;
+        }
+
+        ALOGI("Setting touchpad tap dragging to %s.", toString(enabled));
+        mLocked.touchpadTapDraggingEnabled = enabled;
+    } // release lock
+
+    mInputManager->getReader().requestRefreshConfiguration(
+            InputReaderConfiguration::Change::TOUCHPAD_SETTINGS);
+}
+
 void NativeInputManager::setTouchpadRightClickZoneEnabled(bool enabled) {
     { // acquire lock
         std::scoped_lock _l(mLock);
@@ -2223,6 +2244,13 @@
     im->setTouchpadTapToClickEnabled(enabled);
 }
 
+static void nativeSetTouchpadTapDraggingEnabled(JNIEnv* env, jobject nativeImplObj,
+                                                jboolean enabled) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
+
+    im->setTouchpadTapDraggingEnabled(enabled);
+}
+
 static void nativeSetTouchpadRightClickZoneEnabled(JNIEnv* env, jobject nativeImplObj,
                                                    jboolean enabled) {
     NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
@@ -2844,6 +2872,7 @@
         {"setTouchpadNaturalScrollingEnabled", "(Z)V",
          (void*)nativeSetTouchpadNaturalScrollingEnabled},
         {"setTouchpadTapToClickEnabled", "(Z)V", (void*)nativeSetTouchpadTapToClickEnabled},
+        {"setTouchpadTapDraggingEnabled", "(Z)V", (void*)nativeSetTouchpadTapDraggingEnabled},
         {"setTouchpadRightClickZoneEnabled", "(Z)V", (void*)nativeSetTouchpadRightClickZoneEnabled},
         {"setShowTouches", "(Z)V", (void*)nativeSetShowTouches},
         {"setInteractive", "(Z)V", (void*)nativeSetInteractive},
diff --git a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt
index edacda0..15c9b9f 100644
--- a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt
@@ -20,7 +20,6 @@
 import android.os.Build
 import android.util.Slog
 import com.android.server.permission.access.MutateStateScope
-import com.android.server.permission.access.collection.* // ktlint-disable no-wildcard-imports
 import com.android.server.permission.access.immutable.* // ktlint-disable no-wildcard-imports
 import com.android.server.permission.access.util.andInv
 import com.android.server.permission.access.util.hasAnyBit
@@ -61,10 +60,11 @@
         if (version <= 12 /*&& SdkLevel.isAtLeastT()*/) {
             Slog.v(
                 LOG_TAG,
-                "Upgrading scoped permissions for package: $packageName" +
+                "Upgrading scoped media and body sensor permissions for package: $packageName" +
                     ", version: $version, user: $userId"
             )
             upgradeAuralVisualMediaPermissions(packageState, userId)
+            upgradeBodySensorPermissions(packageState, userId)
         }
         // TODO Enable isAtLeastU check, when moving subsystem to mainline.
         if (version <= 14 /*&& SdkLevel.isAtLeastU()*/) {
@@ -182,6 +182,50 @@
         }
     }
 
+    private fun MutateStateScope.upgradeBodySensorPermissions(
+        packageState: PackageState,
+        userId: Int
+    ) {
+        if (
+            Manifest.permission.BODY_SENSORS_BACKGROUND !in
+                packageState.androidPackage!!.requestedPermissions
+        ) {
+            return
+        }
+
+        // Should have been granted when first getting exempt as if the perm was just split
+        val appId = packageState.appId
+        val backgroundBodySensorsFlags =
+            with(policy) {
+                getPermissionFlags(appId, userId, Manifest.permission.BODY_SENSORS_BACKGROUND)
+            }
+        if (backgroundBodySensorsFlags.hasAnyBit(PermissionFlags.MASK_EXEMPT)) {
+            return
+        }
+
+        // Add Upgrade Exemption - BODY_SENSORS_BACKGROUND is a restricted permission
+        with(policy) {
+            updatePermissionFlags(
+                appId,
+                userId,
+                Manifest.permission.BODY_SENSORS_BACKGROUND,
+                PermissionFlags.UPGRADE_EXEMPT,
+                PermissionFlags.UPGRADE_EXEMPT,
+            )
+        }
+
+        val bodySensorsFlags =
+            with(policy) { getPermissionFlags(appId, userId, Manifest.permission.BODY_SENSORS) }
+        val isForegroundBodySensorsGranted = PermissionFlags.isAppOpGranted(bodySensorsFlags)
+        if (isForegroundBodySensorsGranted) {
+            grantRuntimePermission(
+                packageState,
+                userId,
+                Manifest.permission.BODY_SENSORS_BACKGROUND
+            )
+        }
+    }
+
     /** Upgrade permission based on the grant in [Manifest.permission_group.READ_MEDIA_VISUAL] */
     private fun MutateStateScope.upgradeUserSelectedVisualMediaPermission(
         packageState: PackageState,
diff --git a/services/tests/mockingservicestests/Android.bp b/services/tests/mockingservicestests/Android.bp
index 321d945..d928306 100644
--- a/services/tests/mockingservicestests/Android.bp
+++ b/services/tests/mockingservicestests/Android.bp
@@ -46,7 +46,6 @@
         "androidx.test.espresso.core",
         "androidx.test.espresso.contrib",
         "androidx.test.ext.truth",
-        "backup_flags_lib",
         "flag-junit",
         "frameworks-base-testutils",
         "hamcrest-library",
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java
index a65ef00..bf00b75 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java
@@ -552,20 +552,22 @@
         when(mComputer.getPackageStateFiltered(eq(PACKAGE), anyInt(), anyInt())).thenReturn(
                 null);
 
-        assertThat(mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT)).isNull();
+        assertThat(mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT,
+                CALLER_PACKAGE)).isNull();
     }
 
     @Test
     public void getArchivedAppIcon_notArchived() {
-        assertThat(mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT)).isNull();
+        assertThat(mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT,
+                CALLER_PACKAGE)).isNull();
     }
 
     @Test
     public void getArchivedAppIcon_success() {
         mUserState.setArchiveState(createArchiveState()).setInstalled(false);
 
-        assertThat(mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT)).isEqualTo(
-                mIcon);
+        assertThat(mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT,
+                CALLER_PACKAGE)).isEqualTo(mIcon);
     }
 
 
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BaseClientMonitorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BaseClientMonitorTest.java
index 3a9c0f0..a1f0dbd 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BaseClientMonitorTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BaseClientMonitorTest.java
@@ -70,7 +70,7 @@
         mClientMonitor.binderDied();
 
         assertThat(mClientMonitor.mCanceled).isTrue();
-        assertThat(mClientMonitor.getListener()).isNull();
+        assertThat(mClientMonitor.getListener()).isNotNull();
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClientTest.java
index c8bfaa9..5b81277 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClientTest.java
@@ -87,14 +87,6 @@
         verify(mCallback).onClientFinished(mClient, true);
     }
 
-    @Test
-    public void generateChallenge_nullListener() {
-        createClient(null);
-        mClient.start(mCallback);
-
-        verify(mCallback).onClientFinished(mClient, false);
-    }
-
     private void createClient(ClientMonitorCallbackConverter listener) {
         mClient = new FaceGenerateChallengeClient(mContext, () -> mAidlSession, mToken, listener,
                 USER_ID, TAG, SENSOR_ID, mBiometricLogger, mBiometricContext);
diff --git a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
index 0805485..81df597 100644
--- a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
@@ -157,6 +157,7 @@
                     return mMockDevicePolicyManager;
                 case Context.APP_SEARCH_SERVICE:
                 case Context.ROLE_SERVICE:
+                case Context.APP_OPS_SERVICE:
                     // RoleManager is final and cannot be mocked, so we only override the inject
                     // accessor methods in ShortcutService.
                     return getTestContext().getSystemService(name);
diff --git a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java
index 0382ca0..e21388e 100644
--- a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java
@@ -34,6 +34,7 @@
 import static android.view.KeyEvent.KEYCODE_SLASH;
 import static android.view.KeyEvent.KEYCODE_SPACE;
 import static android.view.KeyEvent.KEYCODE_TAB;
+import static android.view.KeyEvent.KEYCODE_SCREENSHOT;
 import static android.view.KeyEvent.KEYCODE_U;
 import static android.view.KeyEvent.KEYCODE_Z;
 
@@ -193,4 +194,26 @@
             mPhoneWindowManager.verifyNewBrightness(newBrightness[i]);
         }
     }
+
+    /**
+     * Sends a KEYCODE_SCREENSHOT and validates screenshot is taken if flag is enabled
+     */
+    @Test
+    public void testTakeScreenshot_flagEnabled() {
+        mSetFlagsRule.enableFlags(com.android.hardware.input.Flags
+                .FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE);
+        sendKeyCombination(new int[]{KEYCODE_SCREENSHOT}, 0);
+        mPhoneWindowManager.assertTakeScreenshotCalled();
+    }
+
+    /**
+     * Sends a KEYCODE_SCREENSHOT and validates screenshot is not taken if flag is disabled
+     */
+    @Test
+    public void testTakeScreenshot_flagDisabled() {
+        mSetFlagsRule.disableFlags(com.android.hardware.input.Flags
+                .FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE);
+        sendKeyCombination(new int[]{KEYCODE_SCREENSHOT}, 0);
+        mPhoneWindowManager.assertTakeScreenshotNotCalled();
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
index 157d162..fee6582 100644
--- a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
+++ b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
@@ -46,6 +46,7 @@
 import static java.util.Collections.unmodifiableMap;
 
 import android.content.Context;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.util.ArrayMap;
 import android.view.InputDevice;
 import android.view.KeyCharacterMap;
@@ -57,11 +58,17 @@
 
 import org.junit.After;
 import org.junit.Rule;
+import org.junit.rules.RuleChain;
 
 import java.util.Map;
 
 class ShortcutKeyTestBase {
-    @Rule public FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule();
+
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+    public final FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule();
+
+    @Rule
+    public RuleChain rules = RuleChain.outerRule(mSettingsProviderRule).around(mSetFlagsRule);
 
     TestPhoneWindowManager mPhoneWindowManager;
     DispatchedKeyHandler mDispatchedKeyHandler = event -> false;
diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
index c8abd8d..2904c03 100644
--- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
+++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
@@ -596,6 +596,11 @@
         verify(mDisplayPolicy).takeScreenshot(anyInt(), anyInt());
     }
 
+    void assertTakeScreenshotNotCalled() {
+        mTestLooper.dispatchAll();
+        verify(mDisplayPolicy, never()).takeScreenshot(anyInt(), anyInt());
+    }
+
     void assertShowGlobalActionsCalled() {
         mTestLooper.dispatchAll();
         verify(mPhoneWindowManager).showGlobalActions();
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
index 9e00f92..5d14334 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
@@ -417,6 +417,8 @@
                     di.logicalWidth, di.logicalHeight).mConfigInsets.top);
         }
 
+        // Flush the pending change (DecorInsets.Info#mNeedUpdate) for the rotation to be tested.
+        displayPolicy.getDecorInsetsInfo(Surface.ROTATION_90, di.logicalHeight, di.logicalWidth);
         // Add a window that provides the same insets in current rotation. But it specifies
         // different insets in other rotations.
         final WindowState bar2 = createWindow(null, navbar.mAttrs.type, "bar2");
@@ -446,6 +448,12 @@
         // The insets in other rotations should be still updated.
         assertEquals(doubleHeightFor90, displayPolicy.getDecorInsetsInfo(Surface.ROTATION_90,
                 di.logicalHeight, di.logicalWidth).mConfigInsets.bottom);
+        // Restore to previous height and the insets can still be updated.
+        bar2.mAttrs.paramsForRotation[Surface.ROTATION_90].providedInsets[0].setInsetsSize(
+                Insets.of(0, 0, 0, NAV_BAR_HEIGHT));
+        assertFalse(displayPolicy.updateDecorInsetsInfo());
+        assertEquals(NAV_BAR_HEIGHT, displayPolicy.getDecorInsetsInfo(Surface.ROTATION_90,
+                di.logicalHeight, di.logicalWidth).mConfigInsets.bottom);
 
         navbar.removeIfPossible();
         bar2.removeIfPossible();
diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
index 3b4b220..9930c88 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
@@ -19,6 +19,7 @@
 import static android.app.ActivityManager.RECENT_WITH_EXCLUDED;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
@@ -1207,6 +1208,29 @@
     }
 
     @Test
+    public void addTask_tasksAreAddedAccordingToZOrder() {
+        final Task firstTask = new TaskBuilder(mSupervisor).setTaskId(1)
+                .setWindowingMode(WINDOWING_MODE_FREEFORM).build();
+        final Task secondTask = new TaskBuilder(mSupervisor).setTaskId(2)
+                .setWindowingMode(WINDOWING_MODE_FREEFORM).build();
+
+        assertEquals(-1, firstTask.compareTo(secondTask));
+
+        // initial addition when tasks are created
+        mRecentTasks.add(firstTask);
+        mRecentTasks.add(secondTask);
+
+        assertRecentTasksOrder(secondTask, firstTask);
+
+        // Tasks are added in a different order
+        mRecentTasks.add(secondTask);
+        mRecentTasks.add(firstTask);
+
+        // order in recents don't change as first task has lower z-order
+        assertRecentTasksOrder(secondTask, firstTask);
+    }
+
+    @Test
     public void removeTask_callsTaskNotificationController() {
         final Task task = createTaskBuilder(".Task").build();
 
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index 1bf11df..eb7e67d 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -1125,7 +1125,7 @@
     /**
      * TelephonyProvider column name for satellite attach enabled for carrier. The value of this
      * column is set based on user settings.
-     * By default, it's disabled.
+     * By default, it's enabled.
      * <P>Type: INTEGER (int)</P>
      * @hide
      */
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index c1ceaef..a0f0338 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -18970,11 +18970,11 @@
     @FlaggedApi(Flags.FLAG_ENABLE_MODEM_CIPHER_TRANSPARENCY)
     @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE)
     @SystemApi
-    public void setEnableNullCipherNotifications(boolean enable) {
+    public void setNullCipherNotificationsEnabled(boolean enable) {
         try {
             ITelephony telephony = getITelephony();
             if (telephony != null) {
-                telephony.setEnableNullCipherNotifications(enable);
+                telephony.setNullCipherNotificationsEnabled(enable);
             } else {
                 throw new IllegalStateException("telephony service is null.");
             }
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 43eb111..a1fc064 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -3247,7 +3247,7 @@
      */
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission("
         + "android.Manifest.permission.MODIFY_PHONE_STATE)")
-    void setEnableNullCipherNotifications(boolean enable);
+    void setNullCipherNotificationsEnabled(boolean enable);
 
     /**
      * Get whether notifications are enabled for null cipher or integrity algorithms in use by the
diff --git a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
index 566e51a..cbec85e 100644
--- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
+++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
@@ -146,6 +146,7 @@
         verify(native).setTouchpadPointerSpeed(anyInt())
         verify(native).setTouchpadNaturalScrollingEnabled(anyBoolean())
         verify(native).setTouchpadTapToClickEnabled(anyBoolean())
+        verify(native).setTouchpadTapDraggingEnabled(anyBoolean())
         verify(native).setTouchpadRightClickZoneEnabled(anyBoolean())
         verify(native).setShowTouches(anyBoolean())
         verify(native).setMotionClassifierEnabled(anyBoolean())
diff --git a/tools/hoststubgen/hoststubgen/helper-runtime-src/com/android/hoststubgen/hosthelper/HostTestUtils.java b/tools/hoststubgen/hoststubgen/helper-runtime-src/com/android/hoststubgen/hosthelper/HostTestUtils.java
index 7c6aa25..60eb47ee 100644
--- a/tools/hoststubgen/hoststubgen/helper-runtime-src/com/android/hoststubgen/hosthelper/HostTestUtils.java
+++ b/tools/hoststubgen/hoststubgen/helper-runtime-src/com/android/hoststubgen/hosthelper/HostTestUtils.java
@@ -63,7 +63,10 @@
      */
     public static void onThrowMethodCalled() {
         // TODO: Maybe add call tracking?
-        throw new RuntimeException("This method is not supported on the host side");
+        throw new RuntimeException(
+                "This method is not yet supported under the Ravenwood deviceless testing "
+                        + "environment; consider requesting support from the API owner or "
+                        + "consider using Mockito; more details at go/ravenwood-docs");
     }
 
     /**
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java
index fc6b862..ba17c75 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java
@@ -68,7 +68,7 @@
         TinyFrameworkForTextPolicy tfc = new TinyFrameworkForTextPolicy();
 
         thrown.expect(RuntimeException.class);
-        thrown.expectMessage("This method is not supported on the host side");
+        thrown.expectMessage("not yet supported");
         tfc.visibleButUsesUnsupportedMethod();
     }
 
@@ -182,7 +182,7 @@
 
         } catch (java.lang.reflect.InvocationTargetException e) {
             var inner = e.getCause();
-            assertThat(inner.getMessage()).contains("not supported on the host side");
+            assertThat(inner.getMessage()).contains("not yet supported");
         }
     }
 
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassWithAnnotTest.java b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassWithAnnotTest.java
index 20cc2ec..288c716 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassWithAnnotTest.java
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassWithAnnotTest.java
@@ -60,7 +60,7 @@
         TinyFrameworkClassAnnotations tfc = new TinyFrameworkClassAnnotations();
 
         thrown.expect(RuntimeException.class);
-        thrown.expectMessage("This method is not supported on the host side");
+        thrown.expectMessage("not yet supported");
         tfc.visibleButUsesUnsupportedMethod();
     }
 }