Merge "Allow cross user package suspension" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 6ecd38f..3391698 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -335,6 +335,11 @@
     aconfig_declarations: "android.os.flags-aconfig",
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
     mode: "exported",
+    min_sdk_version: "30",
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.mediaprovider",
+    ],
 }
 
 cc_aconfig_library {
@@ -716,6 +721,7 @@
     name: "android.credentials.flags-aconfig",
     package: "android.credentials.flags",
     srcs: ["core/java/android/credentials/flags.aconfig"],
+    exportable: true,
 }
 
 java_aconfig_library {
@@ -724,6 +730,13 @@
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
 }
 
+java_aconfig_library {
+    name: "android.credentials.flags-aconfig-java-export",
+    aconfig_declarations: "android.credentials.flags-aconfig",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+    mode: "exported",
+}
+
 // Content Protection
 aconfig_declarations {
     name: "android.view.contentprotection.flags-aconfig",
diff --git a/TEST_MAPPING b/TEST_MAPPING
index c904eb4..49384cd 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -232,30 +232,5 @@
        }
      ]
    }
- ],
- "auto-features-postsubmit": [
-   // Test tag for automotive feature targets. These are only running in postsubmit.
-   // This tag is used in targeted test features testing to limit resource use.
-   // TODO(b/256932212): this tag to be removed once the above is no longer in use.
-   {
-     "name": "FrameworksMockingServicesTests",
-     "options": [
-       {
-         "include-filter": "com.android.server.pm.UserVisibilityMediatorSUSDTest"
-       },
-       {
-         "include-filter": "com.android.server.pm.UserVisibilityMediatorMUMDTest"
-       },
-       {
-         "include-filter": "com.android.server.pm.UserVisibilityMediatorMUPANDTest"
-       },
-       {
-         "exclude-annotation": "androidx.test.filters.FlakyTest"
-       },
-       {
-         "exclude-annotation": "org.junit.Ignore"
-       }
-     ]
-   }
  ]
 }
diff --git a/core/api/current.txt b/core/api/current.txt
index 4d3ca13..8a61f4a 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -10764,6 +10764,7 @@
     field public static final String OVERLAY_SERVICE = "overlay";
     field public static final String PEOPLE_SERVICE = "people";
     field public static final String PERFORMANCE_HINT_SERVICE = "performance_hint";
+    field @FlaggedApi("android.security.frp_enforcement") public static final String PERSISTENT_DATA_BLOCK_SERVICE = "persistent_data_block";
     field public static final String POWER_SERVICE = "power";
     field public static final String PRINT_SERVICE = "print";
     field @FlaggedApi("android.os.telemetry_apis_framework_initialization") public static final String PROFILING_SERVICE = "profiling";
@@ -20235,10 +20236,10 @@
     method public android.hardware.camera2.CaptureRequest getSessionParameters();
     method public int getSessionType();
     method public android.hardware.camera2.CameraCaptureSession.StateCallback getStateCallback();
-    method @FlaggedApi("com.android.internal.camera.flags.camera_device_setup") public void setCallback(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraCaptureSession.StateCallback);
     method public void setColorSpace(@NonNull android.graphics.ColorSpace.Named);
     method public void setInputConfiguration(@NonNull android.hardware.camera2.params.InputConfiguration);
     method public void setSessionParameters(android.hardware.camera2.CaptureRequest);
+    method @FlaggedApi("com.android.internal.camera.flags.camera_device_setup") public void setStateCallback(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraCaptureSession.StateCallback);
     method public void writeToParcel(android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.hardware.camera2.params.SessionConfiguration> CREATOR;
     field public static final int SESSION_HIGH_SPEED = 1; // 0x1
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 8ceda62..5ead3e1 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -598,7 +598,6 @@
     field public static final int FOREGROUND_SERVICE_API_TYPE_MICROPHONE = 6; // 0x6
     field public static final int FOREGROUND_SERVICE_API_TYPE_PHONE_CALL = 7; // 0x7
     field public static final int FOREGROUND_SERVICE_API_TYPE_USB = 8; // 0x8
-    field @FlaggedApi("android.media.audio.foreground_audio_control") public static final int PROCESS_CAPABILITY_FOREGROUND_AUDIO_CONTROL = 64; // 0x40
     field public static final int PROCESS_CAPABILITY_FOREGROUND_CAMERA = 2; // 0x2
     field public static final int PROCESS_CAPABILITY_FOREGROUND_LOCATION = 1; // 0x1
     field public static final int PROCESS_CAPABILITY_FOREGROUND_MICROPHONE = 4; // 0x4
@@ -3797,7 +3796,6 @@
     field @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public static final String ON_DEVICE_INTELLIGENCE_SERVICE = "on_device_intelligence";
     field public static final String PERMISSION_CONTROLLER_SERVICE = "permission_controller";
     field public static final String PERMISSION_SERVICE = "permission";
-    field public static final String PERSISTENT_DATA_BLOCK_SERVICE = "persistent_data_block";
     field public static final String REBOOT_READINESS_SERVICE = "reboot_readiness";
     field public static final String ROLLBACK_SERVICE = "rollback";
     field public static final String SAFETY_CENTER_SERVICE = "safety_center";
@@ -4356,7 +4354,7 @@
     field @Deprecated public static final int INTENT_FILTER_VERIFICATION_SUCCESS = 1; // 0x1
     field @Deprecated public static final int MASK_PERMISSION_FLAGS = 255; // 0xff
     field public static final int MATCH_ANY_USER = 4194304; // 0x400000
-    field public static final int MATCH_CLONE_PROFILE = 536870912; // 0x20000000
+    field @Deprecated public static final int MATCH_CLONE_PROFILE = 536870912; // 0x20000000
     field @FlaggedApi("android.content.pm.fix_duplicated_flags") public static final long MATCH_CLONE_PROFILE_LONG = 17179869184L; // 0x400000000L
     field public static final int MATCH_FACTORY_ONLY = 2097152; // 0x200000
     field public static final int MATCH_HIDDEN_UNTIL_INSTALLED_COMPONENTS = 536870912; // 0x20000000
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 0a26490..a2b847e 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -2460,6 +2460,7 @@
   }
 
   public class UserManager {
+    method @FlaggedApi("android.os.allow_private_profile") @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}, conditional=true) public boolean canAddPrivateProfile();
     method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public android.content.pm.UserInfo createProfileForUser(@Nullable String, @NonNull String, int, int, @Nullable String[]);
     method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public android.content.pm.UserInfo createRestrictedProfile(@Nullable String);
     method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public android.content.pm.UserInfo createUser(@Nullable String, @NonNull String, int);
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index fae4348..0c54351 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -20,7 +20,6 @@
 import static android.app.WindowConfiguration.windowingModeToString;
 import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
 import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE;
-import static android.media.audio.Flags.FLAG_FOREGROUND_AUDIO_CONTROL;
 
 import android.Manifest;
 import android.annotation.ColorInt;
@@ -948,8 +947,6 @@
      * @hide
      * Process can access volume APIs and can request audio focus with GAIN.
      */
-    @FlaggedApi(FLAG_FOREGROUND_AUDIO_CONTROL)
-    @SystemApi
     public static final int PROCESS_CAPABILITY_FOREGROUND_AUDIO_CONTROL = 1 << 6;
 
     /**
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index ae5cacd..fa9346e 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -712,16 +712,22 @@
             stopped = false;
             hideForNow = false;
             activityConfigCallback = new ViewRootImpl.ActivityConfigCallback() {
+
                 @Override
-                public void onConfigurationChanged(Configuration overrideConfig,
-                        int newDisplayId) {
+                public void onConfigurationChanged(@NonNull Configuration overrideConfig,
+                        int newDisplayId, @Nullable ActivityWindowInfo activityWindowInfo) {
                     if (activity == null) {
                         throw new IllegalStateException(
                                 "Received config update for non-existing activity");
                     }
+                    if (activityWindowInfoFlag() && activityWindowInfo == null) {
+                        Log.w(TAG, "Received empty ActivityWindowInfo update for r=" + activity);
+                        activityWindowInfo = mActivityWindowInfo;
+                    }
                     activity.mMainThread.handleActivityConfigurationChanged(
                             ActivityClientRecord.this, overrideConfig, newDisplayId,
-                            mActivityWindowInfo, false /* alwaysReportChange */);
+                            activityWindowInfo,
+                            false /* alwaysReportChange */);
                 }
 
                 @Override
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index a8352fa..ff713d0 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -1581,6 +1581,10 @@
      * Allows an app to access location without the traditional location permissions and while the
      * user location setting is off, but only during pre-defined emergency sessions.
      *
+     * <p>This op is only used for tracking, not for permissions, so it is still the client's
+     * responsibility to check the {@link Manifest.permission.LOCATION_BYPASS} permission
+     * appropriately.
+     *
      * @hide
      */
     public static final int OP_EMERGENCY_LOCATION = AppProtoEnums.APP_OP_EMERGENCY_LOCATION;
@@ -2459,6 +2463,10 @@
      * Allows an app to access location without the traditional location permissions and while the
      * user location setting is off, but only during pre-defined emergency sessions.
      *
+     * <p>This op is only used for tracking, not for permissions, so it is still the client's
+     * responsibility to check the {@link Manifest.permission.LOCATION_BYPASS} permission
+     * appropriately.
+     *
      * @hide
      */
     @SystemApi
@@ -3047,8 +3055,10 @@
         new AppOpInfo.Builder(OP_UNARCHIVAL_CONFIRMATION, OPSTR_UNARCHIVAL_CONFIRMATION,
                 "UNARCHIVAL_CONFIRMATION")
                 .setDefaultMode(MODE_ALLOWED).build(),
-        // TODO(b/301150056): STOPSHIP determine how this appop should work with the permission
         new AppOpInfo.Builder(OP_EMERGENCY_LOCATION, OPSTR_EMERGENCY_LOCATION, "EMERGENCY_LOCATION")
+                .setDefaultMode(MODE_ALLOWED)
+                // even though this has a permission associated, this op is only used for tracking,
+                // and the client is responsible for checking the LOCATION_BYPASS permission.
                 .setPermission(Manifest.permission.LOCATION_BYPASS).build(),
     };
 
diff --git a/core/java/android/app/ApplicationExitInfo.java b/core/java/android/app/ApplicationExitInfo.java
index 24cb9ea..cac10f5 100644
--- a/core/java/android/app/ApplicationExitInfo.java
+++ b/core/java/android/app/ApplicationExitInfo.java
@@ -487,6 +487,15 @@
      */
     public static final int SUBREASON_FREEZER_BINDER_ASYNC_FULL = 31;
 
+    /**
+     * The process was killed because it was sending too many broadcasts while it is in the
+     * Cached state. This would be set only when the reason is {@link #REASON_OTHER}.
+     *
+     * For internal use only.
+     * @hide
+     */
+    public static final int SUBREASON_EXCESSIVE_OUTGOING_BROADCASTS_WHILE_CACHED = 32;
+
     // If there is any OEM code which involves additional app kill reasons, it should
     // be categorized in {@link #REASON_OTHER}, with subreason code starting from 1000.
 
@@ -665,6 +674,7 @@
         SUBREASON_EXCESSIVE_BINDER_OBJECTS,
         SUBREASON_OOM_KILL,
         SUBREASON_FREEZER_BINDER_ASYNC_FULL,
+        SUBREASON_EXCESSIVE_OUTGOING_BROADCASTS_WHILE_CACHED,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface SubReason {}
@@ -1396,6 +1406,8 @@
                 return "OOM KILL";
             case SUBREASON_FREEZER_BINDER_ASYNC_FULL:
                 return "FREEZER BINDER ASYNC FULL";
+            case SUBREASON_EXCESSIVE_OUTGOING_BROADCASTS_WHILE_CACHED:
+                return "EXCESSIVE_OUTGOING_BROADCASTS_WHILE_CACHED";
             default:
                 return "UNKNOWN";
         }
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index 6f6e091..716dee4 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -344,23 +344,37 @@
      */
     private boolean mOwnsToken = false;
 
-    private final Object mDirsLock = new Object();
-    @GuardedBy("mDirsLock")
+    private final Object mDatabasesDirLock = new Object();
+    @GuardedBy("mDatabasesDirLock")
     private File mDatabasesDir;
-    @GuardedBy("mDirsLock")
+
+    private final Object mPreferencesDirLock = new Object();
     @UnsupportedAppUsage
+    @GuardedBy("mPreferencesDirLock")
     private File mPreferencesDir;
-    @GuardedBy("mDirsLock")
+
+    private final Object mFilesDirLock = new Object();
+    @GuardedBy("mFilesDirLock")
     private File mFilesDir;
-    @GuardedBy("mDirsLock")
+
+    private final Object mCratesDirLock = new Object();
+    @GuardedBy("mCratesDirLock")
     private File mCratesDir;
-    @GuardedBy("mDirsLock")
+
+    private final Object mNoBackupFilesDirLock = new Object();
+    @GuardedBy("mNoBackupFilesDirLock")
     private File mNoBackupFilesDir;
-    @GuardedBy("mDirsLock")
+
+    private final Object mCacheDirLock = new Object();
+    @GuardedBy("mCacheDirLock")
     private File mCacheDir;
-    @GuardedBy("mDirsLock")
+
+    private final Object mCodeCacheDirLock = new Object();
+    @GuardedBy("mCodeCacheDirLock")
     private File mCodeCacheDir;
 
+    private final Object mMiscDirsLock = new Object();
+
     // The system service cache for the system services that are cached per-ContextImpl.
     @UnsupportedAppUsage
     final Object[] mServiceCache = SystemServiceRegistry.createServiceCache();
@@ -742,7 +756,7 @@
 
     @UnsupportedAppUsage
     private File getPreferencesDir() {
-        synchronized (mDirsLock) {
+        synchronized (mPreferencesDirLock) {
             if (mPreferencesDir == null) {
                 mPreferencesDir = new File(getDataDir(), "shared_prefs");
             }
@@ -831,7 +845,7 @@
 
     @Override
     public File getFilesDir() {
-        synchronized (mDirsLock) {
+        synchronized (mFilesDirLock) {
             if (mFilesDir == null) {
                 mFilesDir = new File(getDataDir(), "files");
             }
@@ -846,7 +860,7 @@
         final Path absoluteNormalizedCratePath = cratesRootPath.resolve(crateId)
                 .toAbsolutePath().normalize();
 
-        synchronized (mDirsLock) {
+        synchronized (mCratesDirLock) {
             if (mCratesDir == null) {
                 mCratesDir = cratesRootPath.toFile();
             }
@@ -859,7 +873,7 @@
 
     @Override
     public File getNoBackupFilesDir() {
-        synchronized (mDirsLock) {
+        synchronized (mNoBackupFilesDirLock) {
             if (mNoBackupFilesDir == null) {
                 mNoBackupFilesDir = new File(getDataDir(), "no_backup");
             }
@@ -876,7 +890,7 @@
 
     @Override
     public File[] getExternalFilesDirs(String type) {
-        synchronized (mDirsLock) {
+        synchronized (mMiscDirsLock) {
             File[] dirs = Environment.buildExternalStorageAppFilesDirs(getPackageName());
             if (type != null) {
                 dirs = Environment.buildPaths(dirs, type);
@@ -894,7 +908,7 @@
 
     @Override
     public File[] getObbDirs() {
-        synchronized (mDirsLock) {
+        synchronized (mMiscDirsLock) {
             File[] dirs = Environment.buildExternalStorageAppObbDirs(getPackageName());
             return ensureExternalDirsExistOrFilter(dirs, true /* tryCreateInProcess */);
         }
@@ -902,7 +916,7 @@
 
     @Override
     public File getCacheDir() {
-        synchronized (mDirsLock) {
+        synchronized (mCacheDirLock) {
             if (mCacheDir == null) {
                 mCacheDir = new File(getDataDir(), "cache");
             }
@@ -912,7 +926,7 @@
 
     @Override
     public File getCodeCacheDir() {
-        synchronized (mDirsLock) {
+        synchronized (mCodeCacheDirLock) {
             if (mCodeCacheDir == null) {
                 mCodeCacheDir = getCodeCacheDirBeforeBind(getDataDir());
             }
@@ -938,7 +952,7 @@
 
     @Override
     public File[] getExternalCacheDirs() {
-        synchronized (mDirsLock) {
+        synchronized (mMiscDirsLock) {
             File[] dirs = Environment.buildExternalStorageAppCacheDirs(getPackageName());
             // We don't try to create cache directories in-process, because they need special
             // setup for accurate quota tracking. This ensures the cache dirs are always
@@ -949,7 +963,7 @@
 
     @Override
     public File[] getExternalMediaDirs() {
-        synchronized (mDirsLock) {
+        synchronized (mMiscDirsLock) {
             File[] dirs = Environment.buildExternalStorageAppMediaDirs(getPackageName());
             return ensureExternalDirsExistOrFilter(dirs, true /* tryCreateInProcess */);
         }
@@ -1051,7 +1065,7 @@
     }
 
     private File getDatabasesDir() {
-        synchronized (mDirsLock) {
+        synchronized (mDatabasesDirLock) {
             if (mDatabasesDir == null) {
                 if ("android".equals(getPackageName())) {
                     mDatabasesDir = new File("/data/system");
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index 1cbec31..66ec865 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -450,6 +450,11 @@
                 new CachedServiceFetcher<VcnManager>() {
             @Override
             public VcnManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+                if (!ctx.getPackageManager().hasSystemFeature(
+                        PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)) {
+                    return null;
+                }
+
                 IBinder b = ServiceManager.getService(Context.VCN_MANAGEMENT_SERVICE);
                 IVcnManagementService service = IVcnManagementService.Stub.asInterface(b);
                 return new VcnManager(ctx, service);
@@ -1736,6 +1741,13 @@
         return fetcher;
     }
 
+    private static boolean hasSystemFeatureOpportunistic(@NonNull ContextImpl ctx,
+            @NonNull String featureName) {
+        PackageManager manager = ctx.getPackageManager();
+        if (manager == null) return true;
+        return manager.hasSystemFeature(featureName);
+    }
+
     /**
      * Gets a system service from a given context.
      * @hide
@@ -1758,12 +1770,18 @@
                 case Context.VIRTUALIZATION_SERVICE:
                 case Context.VIRTUAL_DEVICE_SERVICE:
                     return null;
-                case Context.SEARCH_SERVICE:
-                    // Wear device does not support SEARCH_SERVICE so we do not print WTF here
-                    PackageManager manager = ctx.getPackageManager();
-                    if (manager != null && manager.hasSystemFeature(PackageManager.FEATURE_WATCH)) {
+                case Context.VCN_MANAGEMENT_SERVICE:
+                    if (!hasSystemFeatureOpportunistic(ctx,
+                            PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)) {
                         return null;
                     }
+                    break;
+                case Context.SEARCH_SERVICE:
+                    // Wear device does not support SEARCH_SERVICE so we do not print WTF here
+                    if (hasSystemFeatureOpportunistic(ctx, PackageManager.FEATURE_WATCH)) {
+                        return null;
+                    }
+                    break;
             }
             Slog.wtf(TAG, "Manager wrapper not available: " + name);
             return null;
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index a075ac5..60dffbd 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -6545,8 +6545,10 @@
     }
 
     /**
-     * Flag for {@link #wipeData(int)}: also erase the device's external
-     * storage (such as SD cards).
+     * Flag for {@link #wipeData(int)}: also erase the device's adopted external storage (such as
+     * adopted SD cards).
+     * @see <a href="{@docRoot}about/versions/marshmallow/android-6.0.html#adoptable-storage">
+     *     Adoptable Storage Devices</a>
      */
     public static final int WIPE_EXTERNAL_STORAGE = 0x0001;
 
diff --git a/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl b/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl
index 0dbe181..8bf288a 100644
--- a/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl
+++ b/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl
@@ -53,19 +53,22 @@
       void getFeatureDetails(in Feature feature, in IFeatureDetailsCallback featureDetailsCallback) = 4;
 
       @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)")
-      void requestFeatureDownload(in Feature feature, in ICancellationSignal signal, in IDownloadCallback callback) = 5;
+      void requestFeatureDownload(in Feature feature, in  AndroidFuture cancellationSignalFuture, in IDownloadCallback callback) = 5;
 
       @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)")
-      void requestTokenInfo(in Feature feature, in Bundle requestBundle, in  ICancellationSignal signal,
+      void requestTokenInfo(in Feature feature, in Bundle requestBundle, in  AndroidFuture cancellationSignalFuture,
                                                         in ITokenInfoCallback tokenInfocallback) = 6;
 
       @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)")
-      void processRequest(in Feature feature, in Bundle requestBundle, int requestType, in  ICancellationSignal cancellationSignal,
-                                                in IProcessingSignal signal, in IResponseCallback responseCallback) = 7;
+      void processRequest(in Feature feature, in Bundle requestBundle, int requestType,
+                                                in  AndroidFuture cancellationSignalFuture,
+                                                in AndroidFuture processingSignalFuture,
+                                                in IResponseCallback responseCallback) = 7;
 
       @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)")
       void processRequestStreaming(in Feature feature,
-                    in Bundle requestBundle, int requestType, in  ICancellationSignal cancellationSignal, in  IProcessingSignal signal,
+                    in Bundle requestBundle, int requestType, in  AndroidFuture cancellationSignalFuture,
+                    in  AndroidFuture processingSignalFuture,
                     in IStreamingResponseCallback streamingCallback) = 8;
 
       String getRemoteServicePackageName() = 9;
diff --git a/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java b/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java
index a465e3c..bc50d2e4 100644
--- a/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java
+++ b/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java
@@ -26,22 +26,23 @@
 import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
-import android.content.ComponentName;
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.CancellationSignal;
+import android.os.IBinder;
 import android.os.ICancellationSignal;
 import android.os.OutcomeReceiver;
 import android.os.PersistableBundle;
 import android.os.RemoteCallback;
 import android.os.RemoteException;
 import android.system.OsConstants;
+import android.util.Log;
 
 import androidx.annotation.IntDef;
 
-import com.android.internal.R;
+import com.android.internal.infra.AndroidFuture;
 
 import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
@@ -76,6 +77,8 @@
      */
     public static final String AUGMENT_REQUEST_CONTENT_BUNDLE_KEY =
             "AugmentRequestContentBundleKey";
+
+    private static final String TAG = "OnDeviceIntelligence";
     private final Context mContext;
     private final IOnDeviceIntelligenceManager mService;
 
@@ -121,9 +124,9 @@
     @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)
     public String getRemoteServicePackageName() {
         String result;
-        try{
-           result = mService.getRemoteServicePackageName();
-        } catch (RemoteException e){
+        try {
+            result = mService.getRemoteServicePackageName();
+        } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
         return result;
@@ -288,18 +291,15 @@
                 }
             };
 
-            ICancellationSignal transport = null;
-            if (cancellationSignal != null) {
-                transport = CancellationSignal.createTransport();
-                cancellationSignal.setRemote(transport);
-            }
-
-            mService.requestFeatureDownload(feature, transport, downloadCallback);
+            mService.requestFeatureDownload(feature,
+                    configureRemoteCancellationFuture(cancellationSignal, callbackExecutor),
+                    downloadCallback);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
     }
 
+
     /**
      * The methods computes the token related information for a given request payload using the
      * provided {@link Feature}.
@@ -337,13 +337,9 @@
                 }
             };
 
-            ICancellationSignal transport = null;
-            if (cancellationSignal != null) {
-                transport = CancellationSignal.createTransport();
-                cancellationSignal.setRemote(transport);
-            }
-
-            mService.requestTokenInfo(feature, request, transport, callback);
+            mService.requestTokenInfo(feature, request,
+                    configureRemoteCancellationFuture(cancellationSignal, callbackExecutor),
+                    callback);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -407,19 +403,9 @@
             };
 
 
-            IProcessingSignal transport = null;
-            if (processingSignal != null) {
-                transport = ProcessingSignal.createTransport();
-                processingSignal.setRemote(transport);
-            }
-
-            ICancellationSignal cancellationTransport = null;
-            if (cancellationSignal != null) {
-                cancellationTransport = CancellationSignal.createTransport();
-                cancellationSignal.setRemote(cancellationTransport);
-            }
-
-            mService.processRequest(feature, request, requestType, cancellationTransport, transport,
+            mService.processRequest(feature, request, requestType,
+                    configureRemoteCancellationFuture(cancellationSignal, callbackExecutor),
+                    configureRemoteProcessingSignalFuture(processingSignal, callbackExecutor),
                     callback);
 
         } catch (RemoteException e) {
@@ -449,7 +435,8 @@
      * @param callbackExecutor          executor to run the callback on.
      */
     @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)
-    public void processRequestStreaming(@NonNull Feature feature, @NonNull @InferenceParams Bundle request,
+    public void processRequestStreaming(@NonNull Feature feature,
+            @NonNull @InferenceParams Bundle request,
             @RequestType int requestType,
             @Nullable CancellationSignal cancellationSignal,
             @Nullable ProcessingSignal processingSignal,
@@ -500,20 +487,11 @@
                 }
             };
 
-            IProcessingSignal transport = null;
-            if (processingSignal != null) {
-                transport = ProcessingSignal.createTransport();
-                processingSignal.setRemote(transport);
-            }
-
-            ICancellationSignal cancellationTransport = null;
-            if (cancellationSignal != null) {
-                cancellationTransport = CancellationSignal.createTransport();
-                cancellationSignal.setRemote(cancellationTransport);
-            }
-
             mService.processRequestStreaming(
-                    feature, request, requestType, cancellationTransport, transport, callback);
+                    feature, request, requestType,
+                    configureRemoteCancellationFuture(cancellationSignal, callbackExecutor),
+                    configureRemoteProcessingSignalFuture(processingSignal, callbackExecutor),
+                    callback);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -574,4 +552,45 @@
     @Target({ElementType.PARAMETER, ElementType.FIELD})
     public @interface InferenceParams {
     }
+
+
+    @Nullable
+    private static AndroidFuture<IBinder> configureRemoteCancellationFuture(
+            @Nullable CancellationSignal cancellationSignal,
+            @NonNull Executor callbackExecutor) {
+        if (cancellationSignal == null) {
+            return null;
+        }
+        AndroidFuture<IBinder> cancellationFuture = new AndroidFuture<>();
+        cancellationFuture.whenCompleteAsync(
+                (cancellationTransport, error) -> {
+                    if (error != null || cancellationTransport == null) {
+                        Log.e(TAG, "Unable to receive the remote cancellation signal.", error);
+                    } else {
+                        cancellationSignal.setRemote(
+                                ICancellationSignal.Stub.asInterface(cancellationTransport));
+                    }
+                }, callbackExecutor);
+        return cancellationFuture;
+    }
+
+    @Nullable
+    private static AndroidFuture<IBinder> configureRemoteProcessingSignalFuture(
+            ProcessingSignal processingSignal, Executor executor) {
+        if (processingSignal == null) {
+            return null;
+        }
+        AndroidFuture<IBinder> processingSignalFuture = new AndroidFuture<>();
+        processingSignalFuture.whenCompleteAsync(
+                (transport, error) -> {
+                    if (error != null || transport == null) {
+                        Log.e(TAG, "Unable to receive the remote processing signal.", error);
+                    } else {
+                        processingSignal.setRemote(IProcessingSignal.Stub.asInterface(transport));
+                    }
+                }, executor);
+        return processingSignalFuture;
+    }
+
+
 }
diff --git a/core/java/android/app/ondeviceintelligence/ProcessingSignal.java b/core/java/android/app/ondeviceintelligence/ProcessingSignal.java
index c275cc7..733f4fa 100644
--- a/core/java/android/app/ondeviceintelligence/ProcessingSignal.java
+++ b/core/java/android/app/ondeviceintelligence/ProcessingSignal.java
@@ -123,10 +123,10 @@
      * Sets the processing signal callback to be called when signals are received.
      *
      * This method is intended to be used by the recipient of a processing signal
-     * such as the remote implementation for {@link OnDeviceIntelligenceManager} to handle
-     * cancellation requests while performing a long-running operation.  This method is not
-     * intended
-     * to be used by applications themselves.
+     * such as the remote implementation in
+     * {@link android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService} to handle
+     * processing signals while performing a long-running operation.  This method is not
+     * intended to be used by the caller themselves.
      *
      * If {@link ProcessingSignal#sendSignal} has already been called, then the provided callback
      * is invoked immediately and all previously queued actions are passed to remote signal.
@@ -200,7 +200,7 @@
     }
 
     /**
-     * Given a locally created transport, returns its associated cancellation signal.
+     * Given a locally created transport, returns its associated processing signal.
      *
      * @param transport The locally created transport, or null if none.
      * @return The associated processing signal, or null if none.
diff --git a/core/java/android/app/servertransaction/WindowStateResizeItem.java b/core/java/android/app/servertransaction/WindowStateResizeItem.java
index fedffe1..1817c5e 100644
--- a/core/java/android/app/servertransaction/WindowStateResizeItem.java
+++ b/core/java/android/app/servertransaction/WindowStateResizeItem.java
@@ -25,6 +25,7 @@
 import android.app.ActivityThread;
 import android.app.ClientTransactionHandler;
 import android.content.Context;
+import android.os.IBinder;
 import android.os.Parcel;
 import android.os.RemoteException;
 import android.os.Trace;
@@ -32,6 +33,7 @@
 import android.util.MergedConfiguration;
 import android.view.IWindow;
 import android.view.InsetsState;
+import android.window.ActivityWindowInfo;
 import android.window.ClientWindowFrames;
 
 import java.util.Objects;
@@ -55,6 +57,14 @@
     private int mSyncSeqId;
     private boolean mDragResizing;
 
+    /** {@code null} if this is not an Activity window. */
+    @Nullable
+    private IBinder mActivityToken;
+
+    /** {@code null} if this is not an Activity window. */
+    @Nullable
+    private ActivityWindowInfo mActivityWindowInfo;
+
     @Override
     public void execute(@NonNull ClientTransactionHandler client,
             @NonNull PendingTransactionActions pendingActions) {
@@ -65,7 +75,8 @@
         }
         try {
             mWindow.resized(mFrames, mReportDraw, mConfiguration, mInsetsState, mForceLayout,
-                    mAlwaysConsumeSystemBars, mDisplayId, mSyncSeqId, mDragResizing);
+                    mAlwaysConsumeSystemBars, mDisplayId, mSyncSeqId, mDragResizing,
+                    mActivityWindowInfo);
         } catch (RemoteException e) {
             // Should be a local call.
             // An exception could happen if the process is restarted. It is safe to ignore since
@@ -78,6 +89,7 @@
     @Nullable
     @Override
     public Context getContextToUpdate(@NonNull ClientTransactionHandler client) {
+        // TODO(b/260873529): dispatch for mActivityToken as well.
         // WindowStateResizeItem may update the global config with #mConfiguration.
         return ActivityThread.currentApplication();
     }
@@ -91,7 +103,8 @@
             @NonNull ClientWindowFrames frames, boolean reportDraw,
             @NonNull MergedConfiguration configuration, @NonNull InsetsState insetsState,
             boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId, int syncSeqId,
-            boolean dragResizing) {
+            boolean dragResizing, @Nullable IBinder activityToken,
+            @Nullable ActivityWindowInfo activityWindowInfo) {
         WindowStateResizeItem instance =
                 ObjectPool.obtain(WindowStateResizeItem.class);
         if (instance == null) {
@@ -107,6 +120,10 @@
         instance.mDisplayId = displayId;
         instance.mSyncSeqId = syncSeqId;
         instance.mDragResizing = dragResizing;
+        instance.mActivityToken = activityToken;
+        instance.mActivityWindowInfo = activityWindowInfo != null
+                ? new ActivityWindowInfo(activityWindowInfo)
+                : null;
 
         return instance;
     }
@@ -123,6 +140,8 @@
         mDisplayId = INVALID_DISPLAY;
         mSyncSeqId = -1;
         mDragResizing = false;
+        mActivityToken = null;
+        mActivityWindowInfo = null;
         ObjectPool.recycle(this);
     }
 
@@ -141,6 +160,8 @@
         dest.writeInt(mDisplayId);
         dest.writeInt(mSyncSeqId);
         dest.writeBoolean(mDragResizing);
+        dest.writeStrongBinder(mActivityToken);
+        dest.writeTypedObject(mActivityWindowInfo, flags);
     }
 
     /** Reads from Parcel. */
@@ -155,6 +176,8 @@
         mDisplayId = in.readInt();
         mSyncSeqId = in.readInt();
         mDragResizing = in.readBoolean();
+        mActivityToken = in.readStrongBinder();
+        mActivityWindowInfo = in.readTypedObject(ActivityWindowInfo.CREATOR);
     }
 
     public static final @NonNull Creator<WindowStateResizeItem> CREATOR = new Creator<>() {
@@ -185,7 +208,9 @@
                 && mAlwaysConsumeSystemBars == other.mAlwaysConsumeSystemBars
                 && mDisplayId == other.mDisplayId
                 && mSyncSeqId == other.mSyncSeqId
-                && mDragResizing == other.mDragResizing;
+                && mDragResizing == other.mDragResizing
+                && Objects.equals(mActivityToken, other.mActivityToken)
+                && Objects.equals(mActivityWindowInfo, other.mActivityWindowInfo);
     }
 
     @Override
@@ -201,6 +226,8 @@
         result = 31 * result + mDisplayId;
         result = 31 * result + mSyncSeqId;
         result = 31 * result + (mDragResizing ? 1 : 0);
+        result = 31 * result + Objects.hashCode(mActivityToken);
+        result = 31 * result + Objects.hashCode(mActivityWindowInfo);
         return result;
     }
 
@@ -209,6 +236,8 @@
         return "WindowStateResizeItem{window=" + mWindow
                 + ", reportDrawn=" + mReportDraw
                 + ", configuration=" + mConfiguration
+                + ", activityToken=" + mActivityToken
+                + ", activityWindowInfo=" + mActivityWindowInfo
                 + "}";
     }
 
diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl
index 6eab363..30a1135 100644
--- a/core/java/android/companion/virtual/IVirtualDevice.aidl
+++ b/core/java/android/companion/virtual/IVirtualDevice.aidl
@@ -79,6 +79,11 @@
     int getDevicePolicy(int policyType);
 
     /**
+    * Returns whether the device has a valid microphone.
+    */
+    boolean hasCustomAudioInputSupport();
+
+    /**
      * Closes the virtual device and frees all associated resources.
      */
     @EnforcePermission("CREATE_VIRTUAL_DEVICE")
diff --git a/core/java/android/companion/virtual/VirtualDevice.java b/core/java/android/companion/virtual/VirtualDevice.java
index 97fa2ba..b9e9afe 100644
--- a/core/java/android/companion/virtual/VirtualDevice.java
+++ b/core/java/android/companion/virtual/VirtualDevice.java
@@ -17,7 +17,6 @@
 package android.companion.virtual;
 
 import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM;
-import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO;
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CAMERA;
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_SENSORS;
 
@@ -176,8 +175,7 @@
     @FlaggedApi(Flags.FLAG_VDM_PUBLIC_APIS)
     public boolean hasCustomAudioInputSupport() {
         try {
-            return mVirtualDevice.getDevicePolicy(POLICY_TYPE_AUDIO) == DEVICE_POLICY_CUSTOM;
-            // TODO(b/291735254): also check for a custom audio injection mix for this device id.
+            return mVirtualDevice.hasCustomAudioInputSupport();
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index 3304475..ec59cf6 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -972,6 +972,7 @@
          *
          * @param config camera configuration.
          * @return newly created camera.
+         * @throws UnsupportedOperationException if virtual camera isn't supported on this device.
          * @see VirtualDeviceParams#POLICY_TYPE_CAMERA
          */
         @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 89300e3..284e318 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -4208,7 +4208,7 @@
             MEDIA_COMMUNICATION_SERVICE,
             BATTERY_SERVICE,
             JOB_SCHEDULER_SERVICE,
-            //@hide: PERSISTENT_DATA_BLOCK_SERVICE,
+            PERSISTENT_DATA_BLOCK_SERVICE,
             //@hide: OEM_LOCK_SERVICE,
             MEDIA_PROJECTION_SERVICE,
             MIDI_SERVICE,
@@ -5930,9 +5930,8 @@
      *
      * @see #getSystemService(String)
      * @see android.service.persistentdata.PersistentDataBlockManager
-     * @hide
      */
-    @SystemApi
+    @FlaggedApi(android.security.Flags.FLAG_FRP_ENFORCEMENT)
     public static final String PERSISTENT_DATA_BLOCK_SERVICE = "persistent_data_block";
 
     /**
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index 9f2f74b..b5809cf 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -895,7 +895,7 @@
             GET_DISABLED_COMPONENTS,
             GET_DISABLED_UNTIL_USED_COMPONENTS,
             GET_UNINSTALLED_PACKAGES,
-            MATCH_CLONE_PROFILE,
+            MATCH_CLONE_PROFILE_LONG,
             MATCH_QUARANTINED_COMPONENTS,
     })
     @Retention(RetentionPolicy.SOURCE)
@@ -1235,10 +1235,11 @@
     public static final int MATCH_DEBUG_TRIAGED_MISSING = MATCH_DIRECT_BOOT_AUTO;
 
     /**
-     * Use {@link #MATCH_CLONE_PROFILE_LONG} instead.
+     * @deprecated Use {@link #MATCH_CLONE_PROFILE_LONG} instead.
      *
      * @hide
      */
+    @Deprecated
     @SystemApi
     public static final int MATCH_CLONE_PROFILE = 0x20000000;
 
diff --git a/core/java/android/content/res/Configuration.java b/core/java/android/content/res/Configuration.java
index d274792..885f4c5 100644
--- a/core/java/android/content/res/Configuration.java
+++ b/core/java/android/content/res/Configuration.java
@@ -802,14 +802,20 @@
     public static final int SCREEN_WIDTH_DP_UNDEFINED = 0;
 
     /**
-     * The width of the available screen space in dp units excluding the area
-     * occupied by {@link android.view.WindowInsets window insets}.
+     * The width of the available screen space in dp units.
      *
-     * <aside class="note"><b>Note:</b> The width measurement excludes window
-     * insets even when the app is displayed edge to edge using
-     * {@link android.view.Window#setDecorFitsSystemWindows(boolean)
+     * <aside class="note"><b>Note:</b> If the app targets
+     * {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}
+     * or after, The width measurement reflects the window size without excluding insets.
+     * Otherwise, the measurement excludes window insets even when the app is displayed edge to edge
+     * using {@link android.view.Window#setDecorFitsSystemWindows(boolean)
      * Window#setDecorFitsSystemWindows(boolean)}.</aside>
      *
+     * Use {@link android.view.WindowMetrics#getBounds()} to always obtain the horizontal
+     * display area available to an app or embedded activity including the area
+     * occupied by window insets. A version of the API is also available for use on older platforms
+     * through {@link androidx.window.layout.WindowMetrics}.
+     *
      * <p>Corresponds to the
      * <a href="{@docRoot}guide/topics/resources/providing-resources.html#AvailableWidthHeightQualifier">
      * available width</a> resource qualifier. Defaults to
@@ -831,14 +837,15 @@
      * environment, {@code screenWidthDp} is the width of the screen on which
      * the app is displayed excluding window insets.
      *
-     * <p>Differs from {@link android.view.WindowMetrics} by not including
+     * <p>If the app targets {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM} or after,
+     * it is the same as {@link android.view.WindowMetrics}, but is expressed rounded to the nearest
+     * dp rather than px.
+     *
+     * <p>Otherwise, differs from {@link android.view.WindowMetrics} by not including
      * window insets in the width measurement and by expressing the measurement
      * in dp rather than px. Use {@code screenWidthDp} to obtain the width of
      * the display area available to an app or embedded activity excluding the
-     * area occupied by window insets. Use
-     * {@link android.view.WindowMetrics#getBounds()} to obtain the horizontal
-     * display area available to an app or embedded activity including the area
-     * occupied by window insets.
+     * area occupied by window insets.
      */
     public int screenWidthDp;
 
@@ -849,15 +856,20 @@
     public static final int SCREEN_HEIGHT_DP_UNDEFINED = 0;
 
     /**
-     * The height of the available screen space in dp units excluding the area
-     * occupied by {@link android.view.WindowInsets window insets}, such as the
-     * status bar, navigation bar, and cutouts.
+     * The height of the available screen space in dp units.
      *
-     * <aside class="note"><b>Note:</b> The height measurement excludes window
-     * insets even when the app is displayed edge to edge using
-     * {@link android.view.Window#setDecorFitsSystemWindows(boolean)
+     * <aside class="note"><b>Note:</b> If the app targets
+     * {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}
+     * or after, the height measurement reflects the window size without excluding insets.
+     * Otherwise, the measurement excludes window insets even when the app is displayed edge to edge
+     * using {@link android.view.Window#setDecorFitsSystemWindows(boolean)
      * Window#setDecorFitsSystemWindows(boolean)}.</aside>
      *
+     * Use {@link android.view.WindowMetrics#getBounds()} to always obtain the vertical
+     * display area available to an app or embedded activity including the area
+     * occupied by window insets. A version of the API is also available for use on older platforms
+     * through {@link androidx.window.layout.WindowMetrics}.
+     *
      * <p>Corresponds to the
      * <a href="{@docRoot}guide/topics/resources/providing-resources.html#AvailableWidthHeightQualifier">
      * available height</a> resource qualifier. Defaults to
@@ -879,14 +891,15 @@
      * multiple-screen environment, {@code screenHeightDp} is the height of the
      * screen on which the app is displayed excluding window insets.
      *
-     * <p>Differs from {@link android.view.WindowMetrics} by not including
+     * <p>If the app targets {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM} or after,
+     * it is the same as {@link android.view.WindowMetrics}, but is expressed rounded to the nearest
+     * dp rather than px.
+     *
+     * <p>Otherwise, differs from {@link android.view.WindowMetrics} by not including
      * window insets in the height measurement and by expressing the measurement
      * in dp rather than px. Use {@code screenHeightDp} to obtain the height of
      * the display area available to an app or embedded activity excluding the
-     * area occupied by window insets. Use
-     * {@link android.view.WindowMetrics#getBounds()} to obtain the vertical
-     * display area available to an app or embedded activity including the area
-     * occupied by window insets.
+     * area occupied by window insets.
      */
     public int screenHeightDp;
 
diff --git a/core/java/android/credentials/flags.aconfig b/core/java/android/credentials/flags.aconfig
index 47edba6..16ca31f 100644
--- a/core/java/android/credentials/flags.aconfig
+++ b/core/java/android/credentials/flags.aconfig
@@ -47,6 +47,7 @@
     name: "configurable_selector_ui_enabled"
     description: "Enables OEM configurable Credential Selector UI"
     bug: "319448437"
+    is_exported: true
 }
 
 flag {
diff --git a/core/java/android/credentials/selection/IntentCreationResult.java b/core/java/android/credentials/selection/IntentCreationResult.java
new file mode 100644
index 0000000..189ff7b
--- /dev/null
+++ b/core/java/android/credentials/selection/IntentCreationResult.java
@@ -0,0 +1,155 @@
+/*
+ * 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.credentials.selection;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Intent;
+
+/**
+ * Result of creating a Credential Manager UI intent.
+ *
+ * @hide
+ */
+public final class IntentCreationResult {
+    @NonNull
+    private final Intent mIntent;
+    @Nullable
+    private final String mFallbackUiPackageName;
+    @Nullable
+    private final String mOemUiPackageName;
+    @NonNull
+    private final OemUiUsageStatus mOemUiUsageStatus;
+
+    private IntentCreationResult(@NonNull Intent intent, @Nullable String fallbackUiPackageName,
+            @Nullable String oemUiPackageName, OemUiUsageStatus oemUiUsageStatus) {
+        mIntent = intent;
+        mFallbackUiPackageName = fallbackUiPackageName;
+        mOemUiPackageName = oemUiPackageName;
+        mOemUiUsageStatus = oemUiUsageStatus;
+    }
+
+    /** Returns the UI intent. */
+    @NonNull
+    public Intent getIntent() {
+        return mIntent;
+    }
+
+    /**
+     * Returns the result of attempting to use the config_oemCredentialManagerDialogComponent
+     * as the Credential Manager UI.
+     */
+    @NonNull
+    public OemUiUsageStatus getOemUiUsageStatus() {
+        return mOemUiUsageStatus;
+    }
+
+    /**
+     * Returns the package name of the ui component specified in
+     * config_fallbackCredentialManagerDialogComponent, or null if unspecified / not parsable
+     * successfully.
+     */
+    @Nullable
+    public String getFallbackUiPackageName() {
+        return mFallbackUiPackageName;
+    }
+
+    /**
+     * Returns the package name of the oem ui component specified in
+     * config_oemCredentialManagerDialogComponent, or null if unspecified / not parsable.
+     */
+    @Nullable
+    public String getOemUiPackageName() {
+        return mOemUiPackageName;
+    }
+
+    /**
+     * Result of attempting to use the config_oemCredentialManagerDialogComponent as the Credential
+     * Manager UI.
+     */
+    public enum OemUiUsageStatus {
+        UNKNOWN,
+        // Success: the UI specified in config_oemCredentialManagerDialogComponent was used to
+        // fulfill the request.
+        SUCCESS,
+        // The config value was not specified (e.g. left empty).
+        OEM_UI_CONFIG_NOT_SPECIFIED,
+        // The config value component was specified but not found (e.g. component doesn't exist or
+        // component isn't a system app).
+        OEM_UI_CONFIG_SPECIFIED_BUT_NOT_FOUND,
+        // The config value component was found but not enabled.
+        OEM_UI_CONFIG_SPECIFIED_FOUND_BUT_NOT_ENABLED,
+    }
+
+    /**
+     * Builder for {@link IntentCreationResult}.
+     *
+     * @hide
+     */
+    public static final class Builder {
+        @NonNull
+        private Intent mIntent;
+        @Nullable
+        private String mFallbackUiPackageName = null;
+        @Nullable
+        private String mOemUiPackageName = null;
+        @NonNull
+        private OemUiUsageStatus mOemUiUsageStatus = OemUiUsageStatus.UNKNOWN;
+
+        public Builder(Intent intent) {
+            mIntent = intent;
+        }
+
+        /**
+         * Sets the package name of the ui component specified in
+         * config_fallbackCredentialManagerDialogComponent, or null if unspecified / not parsable
+         * successfully.
+         */
+        @NonNull
+        public Builder setFallbackUiPackageName(@Nullable String fallbackUiPackageName) {
+            mFallbackUiPackageName = fallbackUiPackageName;
+            return this;
+        }
+
+        /**
+         * Sets the package name of the oem ui component specified in
+         * config_oemCredentialManagerDialogComponent, or null if unspecified / not parsable.
+         */
+        @NonNull
+        public Builder setOemUiPackageName(@Nullable String oemUiPackageName) {
+            mOemUiPackageName = oemUiPackageName;
+            return this;
+        }
+
+        /**
+         * Sets the result of attempting to use the config_oemCredentialManagerDialogComponent
+         * as the Credential Manager UI.
+         */
+        @NonNull
+        public Builder setOemUiUsageStatus(OemUiUsageStatus oemUiUsageStatus) {
+            mOemUiUsageStatus = oemUiUsageStatus;
+            return this;
+        }
+
+        /** Builds a {@link IntentCreationResult}. */
+        @NonNull
+        public IntentCreationResult build() {
+            return new IntentCreationResult(mIntent, mFallbackUiPackageName, mOemUiPackageName,
+                    mOemUiUsageStatus);
+        }
+    }
+}
diff --git a/core/java/android/credentials/selection/IntentFactory.java b/core/java/android/credentials/selection/IntentFactory.java
index 79fba9b..b98a0d8 100644
--- a/core/java/android/credentials/selection/IntentFactory.java
+++ b/core/java/android/credentials/selection/IntentFactory.java
@@ -36,6 +36,8 @@
 import android.text.TextUtils;
 import android.util.Slog;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import java.util.ArrayList;
 
 /**
@@ -57,98 +59,48 @@
      * @hide
      */
     @NonNull
-    public static Intent createCredentialSelectorIntentForAutofill(
+    public static IntentCreationResult createCredentialSelectorIntentForAutofill(
             @NonNull Context context,
             @NonNull RequestInfo requestInfo,
             @SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling.
             @NonNull
             ArrayList<DisabledProviderData> disabledProviderDataList,
             @NonNull ResultReceiver resultReceiver) {
-        return createCredentialSelectorIntent(context, requestInfo,
+        return createCredentialSelectorIntentInternal(context, requestInfo,
                 disabledProviderDataList, resultReceiver);
     }
 
     /**
      * Generate a new launch intent to the Credential Selector UI.
+     *
+     * @param context                  the CredentialManager system service (only expected caller)
+     *                                 context that may be used to query existence of the key UI
+     *                                 application
+     * @param disabledProviderDataList the list of disabled provider data that when non-empty the
+     *                                 UI should accordingly generate an entry suggesting the user
+     *                                 to navigate to settings and enable them
+     * @param enabledProviderDataList  the list of enabled provider that contain options for this
+     *                                 request; the UI should render each option to the user for
+     *                                 selection
+     * @param requestInfo              the display information about the given app request
+     * @param resultReceiver           used by the UI to send the UI selection result back
+     * @hide
      */
     @NonNull
-    private static Intent createCredentialSelectorIntent(
+    public static IntentCreationResult createCredentialSelectorIntentForCredMan(
             @NonNull Context context,
             @NonNull RequestInfo requestInfo,
             @SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling.
             @NonNull
+            ArrayList<ProviderData> enabledProviderDataList,
+            @SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling.
+            @NonNull
             ArrayList<DisabledProviderData> disabledProviderDataList,
             @NonNull ResultReceiver resultReceiver) {
-        Intent intent = new Intent();
-        setCredentialSelectorUiComponentName(context, intent);
-        intent.putParcelableArrayListExtra(
-                ProviderData.EXTRA_DISABLED_PROVIDER_DATA_LIST, disabledProviderDataList);
-        intent.putExtra(RequestInfo.EXTRA_REQUEST_INFO, requestInfo);
-        intent.putExtra(
-                Constants.EXTRA_RESULT_RECEIVER, toIpcFriendlyResultReceiver(resultReceiver));
-
-        return intent;
-    }
-
-    private static void setCredentialSelectorUiComponentName(@NonNull Context context,
-            @NonNull Intent intent) {
-        if (configurableSelectorUiEnabled()) {
-            ComponentName componentName = getOemOverrideComponentName(context);
-            if (componentName == null) {
-                componentName = ComponentName.unflattenFromString(Resources.getSystem().getString(
-                        com.android.internal.R.string
-                                .config_fallbackCredentialManagerDialogComponent));
-            }
-            intent.setComponent(componentName);
-        } else {
-            ComponentName componentName = ComponentName.unflattenFromString(Resources.getSystem()
-                    .getString(com.android.internal.R.string
-                            .config_fallbackCredentialManagerDialogComponent));
-            intent.setComponent(componentName);
-        }
-    }
-
-    /**
-     * Returns null if there is not an enabled and valid oem override component. It means the
-     * default platform UI component name should be used instead.
-     */
-    @Nullable
-    private static ComponentName getOemOverrideComponentName(@NonNull Context context) {
-        ComponentName result = null;
-        String oemComponentString =
-                Resources.getSystem()
-                        .getString(
-                                com.android.internal.R.string
-                                        .config_oemCredentialManagerDialogComponent);
-        if (!TextUtils.isEmpty(oemComponentString)) {
-            ComponentName oemComponentName = ComponentName.unflattenFromString(
-                    oemComponentString);
-            if (oemComponentName != null) {
-                try {
-                    ActivityInfo info = context.getPackageManager().getActivityInfo(
-                            oemComponentName,
-                            PackageManager.ComponentInfoFlags.of(
-                                    PackageManager.MATCH_SYSTEM_ONLY));
-                    if (info.enabled && info.exported) {
-                        Slog.i(TAG,
-                                "Found enabled oem CredMan UI component."
-                                        + oemComponentString);
-                        result = oemComponentName;
-                    } else {
-                        Slog.i(TAG,
-                                "Found enabled oem CredMan UI component but it was not "
-                                        + "enabled.");
-                    }
-                } catch (PackageManager.NameNotFoundException e) {
-                    Slog.i(TAG, "Unable to find oem CredMan UI component: "
-                            + oemComponentString + ".");
-                }
-            } else {
-                Slog.i(TAG, "Invalid OEM ComponentName format.");
-            }
-        } else {
-            Slog.i(TAG, "Invalid empty OEM component name.");
-        }
+        IntentCreationResult result = createCredentialSelectorIntentInternal(context, requestInfo,
+                disabledProviderDataList, resultReceiver);
+        result.getIntent().putParcelableArrayListExtra(
+                ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, enabledProviderDataList);
         return result;
     }
 
@@ -167,6 +119,7 @@
      * @param requestInfo              the display information about the given app request
      * @param resultReceiver           used by the UI to send the UI selection result back
      */
+    @VisibleForTesting
     @NonNull
     public static Intent createCredentialSelectorIntent(
             @NonNull Context context,
@@ -178,22 +131,21 @@
             @NonNull
             ArrayList<DisabledProviderData> disabledProviderDataList,
             @NonNull ResultReceiver resultReceiver) {
-        Intent intent = createCredentialSelectorIntent(context, requestInfo,
-                disabledProviderDataList, resultReceiver);
-        intent.putParcelableArrayListExtra(
-                ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, enabledProviderDataList);
-        return intent;
+        return createCredentialSelectorIntentForCredMan(context, requestInfo,
+                enabledProviderDataList, disabledProviderDataList, resultReceiver).getIntent();
     }
 
     /**
      * Creates an Intent that cancels any UI matching the given request token id.
      */
+    @VisibleForTesting
     @NonNull
     public static Intent createCancelUiIntent(@NonNull Context context,
             @NonNull IBinder requestToken, boolean shouldShowCancellationUi,
             @NonNull String appPackageName) {
         Intent intent = new Intent();
-        setCredentialSelectorUiComponentName(context, intent);
+        IntentCreationResult.Builder intentResultBuilder = new IntentCreationResult.Builder(intent);
+        setCredentialSelectorUiComponentName(context, intent, intentResultBuilder);
         intent.putExtra(CancelSelectionRequest.EXTRA_CANCEL_UI_REQUEST,
                 new CancelSelectionRequest(new RequestToken(requestToken), shouldShowCancellationUi,
                         appPackageName));
@@ -201,6 +153,119 @@
     }
 
     /**
+     * Generate a new launch intent to the Credential Selector UI.
+     */
+    @NonNull
+    private static IntentCreationResult createCredentialSelectorIntentInternal(
+            @NonNull Context context,
+            @NonNull RequestInfo requestInfo,
+            @SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling.
+            @NonNull
+            ArrayList<DisabledProviderData> disabledProviderDataList,
+            @NonNull ResultReceiver resultReceiver) {
+        Intent intent = new Intent();
+        IntentCreationResult.Builder intentResultBuilder = new IntentCreationResult.Builder(intent);
+        setCredentialSelectorUiComponentName(context, intent, intentResultBuilder);
+        intent.putParcelableArrayListExtra(
+                ProviderData.EXTRA_DISABLED_PROVIDER_DATA_LIST, disabledProviderDataList);
+        intent.putExtra(RequestInfo.EXTRA_REQUEST_INFO, requestInfo);
+        intent.putExtra(
+                Constants.EXTRA_RESULT_RECEIVER, toIpcFriendlyResultReceiver(resultReceiver));
+        return intentResultBuilder.build();
+    }
+
+    private static void setCredentialSelectorUiComponentName(@NonNull Context context,
+            @NonNull Intent intent, @NonNull IntentCreationResult.Builder intentResultBuilder) {
+        if (configurableSelectorUiEnabled()) {
+            ComponentName componentName = getOemOverrideComponentName(context, intentResultBuilder);
+
+            ComponentName fallbackUiComponentName = null;
+            try {
+                fallbackUiComponentName = ComponentName.unflattenFromString(
+                        Resources.getSystem().getString(
+                                com.android.internal.R.string
+                                        .config_fallbackCredentialManagerDialogComponent));
+                intentResultBuilder.setFallbackUiPackageName(
+                        fallbackUiComponentName.getPackageName());
+            } catch (Exception e) {
+                Slog.w(TAG, "Fallback CredMan IU not found: " + e);
+            }
+
+            if (componentName == null) {
+                componentName = fallbackUiComponentName;
+            }
+
+            intent.setComponent(componentName);
+        } else {
+            ComponentName componentName = ComponentName.unflattenFromString(Resources.getSystem()
+                    .getString(com.android.internal.R.string
+                            .config_fallbackCredentialManagerDialogComponent));
+            intent.setComponent(componentName);
+        }
+    }
+
+    /**
+     * Returns null if there is not an enabled and valid oem override component. It means the
+     * default platform UI component name should be used instead.
+     */
+    @Nullable
+    private static ComponentName getOemOverrideComponentName(@NonNull Context context,
+            @NonNull IntentCreationResult.Builder intentResultBuilder) {
+        ComponentName result = null;
+        String oemComponentString =
+                Resources.getSystem()
+                        .getString(
+                                com.android.internal.R.string
+                                        .config_oemCredentialManagerDialogComponent);
+        if (!TextUtils.isEmpty(oemComponentString)) {
+            ComponentName oemComponentName = null;
+            try {
+                oemComponentName = ComponentName.unflattenFromString(
+                        oemComponentString);
+            } catch (Exception e) {
+                Slog.i(TAG, "Failed to parse OEM component name " + oemComponentString + ": " + e);
+            }
+            if (oemComponentName != null) {
+                try {
+                    intentResultBuilder.setOemUiPackageName(oemComponentName.getPackageName());
+                    ActivityInfo info = context.getPackageManager().getActivityInfo(
+                            oemComponentName,
+                            PackageManager.ComponentInfoFlags.of(
+                                    PackageManager.MATCH_SYSTEM_ONLY));
+                    if (info.enabled && info.exported) {
+                        intentResultBuilder.setOemUiUsageStatus(IntentCreationResult
+                                .OemUiUsageStatus.SUCCESS);
+                        Slog.i(TAG,
+                                "Found enabled oem CredMan UI component."
+                                        + oemComponentString);
+                        result = oemComponentName;
+                    } else {
+                        intentResultBuilder.setOemUiUsageStatus(IntentCreationResult
+                                .OemUiUsageStatus.OEM_UI_CONFIG_SPECIFIED_FOUND_BUT_NOT_ENABLED);
+                        Slog.i(TAG,
+                                "Found enabled oem CredMan UI component but it was not "
+                                        + "enabled.");
+                    }
+                } catch (PackageManager.NameNotFoundException e) {
+                    intentResultBuilder.setOemUiUsageStatus(IntentCreationResult.OemUiUsageStatus
+                            .OEM_UI_CONFIG_SPECIFIED_BUT_NOT_FOUND);
+                    Slog.i(TAG, "Unable to find oem CredMan UI component: "
+                            + oemComponentString + ".");
+                }
+            } else {
+                intentResultBuilder.setOemUiUsageStatus(IntentCreationResult.OemUiUsageStatus
+                        .OEM_UI_CONFIG_SPECIFIED_BUT_NOT_FOUND);
+                Slog.i(TAG, "Invalid OEM ComponentName format.");
+            }
+        } else {
+            intentResultBuilder.setOemUiUsageStatus(
+                    IntentCreationResult.OemUiUsageStatus.OEM_UI_CONFIG_NOT_SPECIFIED);
+            Slog.i(TAG, "Invalid empty OEM component name.");
+        }
+        return result;
+    }
+
+    /**
      * Convert an instance of a "locally-defined" ResultReceiver to an instance of {@link
      * android.os.ResultReceiver} itself, which the receiving process will be able to unmarshall.
      */
diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java
index 13d5c7e..6f901d7 100644
--- a/core/java/android/hardware/camera2/CaptureRequest.java
+++ b/core/java/android/hardware/camera2/CaptureRequest.java
@@ -2800,7 +2800,9 @@
      * upright.</p>
      * <p>Camera devices may either encode this value into the JPEG EXIF header, or
      * rotate the image data to match this orientation. When the image data is rotated,
-     * the thumbnail data will also be rotated.</p>
+     * the thumbnail data will also be rotated. Additionally, in the case where the image data
+     * is rotated, {@link android.media.Image#getWidth } and {@link android.media.Image#getHeight }
+     * will not be updated to reflect the height and width of the rotated image.</p>
      * <p>Note that this orientation is relative to the orientation of the camera sensor, given
      * by {@link CameraCharacteristics#SENSOR_ORIENTATION android.sensor.orientation}.</p>
      * <p>To translate from the device orientation given by the Android sensor APIs for camera
diff --git a/core/java/android/hardware/camera2/CaptureResult.java b/core/java/android/hardware/camera2/CaptureResult.java
index 7145501..69b1c34 100644
--- a/core/java/android/hardware/camera2/CaptureResult.java
+++ b/core/java/android/hardware/camera2/CaptureResult.java
@@ -3091,7 +3091,9 @@
      * upright.</p>
      * <p>Camera devices may either encode this value into the JPEG EXIF header, or
      * rotate the image data to match this orientation. When the image data is rotated,
-     * the thumbnail data will also be rotated.</p>
+     * the thumbnail data will also be rotated. Additionally, in the case where the image data
+     * is rotated, {@link android.media.Image#getWidth } and {@link android.media.Image#getHeight }
+     * will not be updated to reflect the height and width of the rotated image.</p>
      * <p>Note that this orientation is relative to the orientation of the camera sensor, given
      * by {@link CameraCharacteristics#SENSOR_ORIENTATION android.sensor.orientation}.</p>
      * <p>To translate from the device orientation given by the Android sensor APIs for camera
diff --git a/core/java/android/hardware/camera2/params/SessionConfiguration.java b/core/java/android/hardware/camera2/params/SessionConfiguration.java
index b0f354f..3b2913c 100644
--- a/core/java/android/hardware/camera2/params/SessionConfiguration.java
+++ b/core/java/android/hardware/camera2/params/SessionConfiguration.java
@@ -133,7 +133,7 @@
      * {@link CameraDeviceSetup.isSessionConfigurationSupported} and {@link
      * CameraDeviceSetup.getSessionCharacteristics} to query a camera device's feature
      * combination support and session specific characteristics. For the SessionConfiguration
-     * object to be used to create a capture session, {@link #setCallback} must be called to
+     * object to be used to create a capture session, {@link #setStateCallback} must be called to
      * specify the state callback function, and any incomplete OutputConfigurations must be
      * completed via {@link OutputConfiguration#addSurface} or
      * {@link OutputConfiguration#setSurfacesForMultiResolutionOutput} as appropriate.</p>
@@ -419,7 +419,7 @@
      * @param cb A state callback interface implementation.
      */
     @FlaggedApi(Flags.FLAG_CAMERA_DEVICE_SETUP)
-    public void setCallback(
+    public void setStateCallback(
             @NonNull @CallbackExecutor Executor executor,
             @NonNull CameraCaptureSession.StateCallback cb) {
         mStateCallback = cb;
diff --git a/core/java/android/hardware/camera2/params/StreamConfigurationMap.java b/core/java/android/hardware/camera2/params/StreamConfigurationMap.java
index b067095..978a8f9 100644
--- a/core/java/android/hardware/camera2/params/StreamConfigurationMap.java
+++ b/core/java/android/hardware/camera2/params/StreamConfigurationMap.java
@@ -1473,6 +1473,11 @@
      * <li>ImageFormat.DEPTH_JPEG => HAL_DATASPACE_DYNAMIC_DEPTH
      * <li>ImageFormat.HEIC => HAL_DATASPACE_HEIF
      * <li>ImageFormat.JPEG_R => HAL_DATASPACE_JPEG_R
+     * <li>ImageFormat.YUV_420_888 => HAL_DATASPACE_JFIF
+     * <li>ImageFormat.RAW_SENSOR => HAL_DATASPACE_ARBITRARY
+     * <li>ImageFormat.RAW_OPAQUE => HAL_DATASPACE_ARBITRARY
+     * <li>ImageFormat.RAW10 => HAL_DATASPACE_ARBITRARY
+     * <li>ImageFormat.RAW12 => HAL_DATASPACE_ARBITRARY
      * <li>others => HAL_DATASPACE_UNKNOWN
      * </ul>
      * </p>
@@ -1511,6 +1516,11 @@
                 return HAL_DATASPACE_JPEG_R;
             case ImageFormat.YUV_420_888:
                 return HAL_DATASPACE_JFIF;
+            case ImageFormat.RAW_SENSOR:
+            case ImageFormat.RAW_PRIVATE:
+            case ImageFormat.RAW10:
+            case ImageFormat.RAW12:
+                return HAL_DATASPACE_ARBITRARY;
             default:
                 return HAL_DATASPACE_UNKNOWN;
         }
@@ -2005,6 +2015,12 @@
     private static final int HAL_DATASPACE_RANGE_SHIFT = 27;
 
     private static final int HAL_DATASPACE_UNKNOWN = 0x0;
+
+    /**
+     * @hide
+     */
+    public static final int HAL_DATASPACE_ARBITRARY = 0x1;
+
     /** @hide */
     public static final int HAL_DATASPACE_V0_JFIF =
             (2 << HAL_DATASPACE_STANDARD_SHIFT) |
diff --git a/core/java/android/hardware/devicestate/DeviceState.java b/core/java/android/hardware/devicestate/DeviceState.java
index b214da2..689e343 100644
--- a/core/java/android/hardware/devicestate/DeviceState.java
+++ b/core/java/android/hardware/devicestate/DeviceState.java
@@ -173,7 +173,7 @@
     public static final int PROPERTY_FEATURE_DUAL_DISPLAY_INTERNAL_DEFAULT = 17;
 
     /** @hide */
-    @IntDef(prefix = {"PROPERTY_"}, flag = true, value = {
+    @IntDef(prefix = {"PROPERTY_"}, flag = false, value = {
             PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_CLOSED,
             PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_HALF_OPEN,
             PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_OPEN,
@@ -197,7 +197,7 @@
     public @interface DeviceStateProperties {}
 
     /** @hide */
-    @IntDef(prefix = {"PROPERTY_"}, flag = true, value = {
+    @IntDef(prefix = {"PROPERTY_"}, flag = false, value = {
             PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_CLOSED,
             PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_HALF_OPEN,
             PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_OPEN
@@ -207,7 +207,7 @@
     public @interface PhysicalDeviceStateProperties {}
 
     /** @hide */
-    @IntDef(prefix = {"PROPERTY_"}, flag = true, value = {
+    @IntDef(prefix = {"PROPERTY_"}, flag = false, value = {
             PROPERTY_POLICY_CANCEL_OVERRIDE_REQUESTS,
             PROPERTY_POLICY_CANCEL_WHEN_REQUESTER_NOT_ON_TOP,
             PROPERTY_POLICY_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL,
diff --git a/core/java/android/net/vcn/flags.aconfig b/core/java/android/net/vcn/flags.aconfig
index 97b773e..e64823a 100644
--- a/core/java/android/net/vcn/flags.aconfig
+++ b/core/java/android/net/vcn/flags.aconfig
@@ -20,4 +20,18 @@
     namespace: "vcn"
     description: "Feature flag for enabling network metric monitor"
     bug: "282996138"
+}
+
+flag{
+    name: "validate_network_on_ipsec_loss"
+    namespace: "vcn"
+    description: "Trigger network validation when IPsec packet loss exceeds the threshold"
+    bug: "329139898"
+}
+
+flag{
+    name: "evaluate_ipsec_loss_on_lp_nc_change"
+    namespace: "vcn"
+    description: "Re-evaluate IPsec packet loss on LinkProperties or NetworkCapabilities change"
+    bug: "323238888"
 }
\ No newline at end of file
diff --git a/core/java/android/os/Bundle.java b/core/java/android/os/Bundle.java
index 387eebe..ed4037c 100644
--- a/core/java/android/os/Bundle.java
+++ b/core/java/android/os/Bundle.java
@@ -18,6 +18,7 @@
 
 import static java.util.Objects.requireNonNull;
 
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
@@ -31,6 +32,8 @@
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.io.Serializable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -53,6 +56,53 @@
     @VisibleForTesting
     static final int FLAG_ALLOW_FDS = 1 << 10;
 
+    @VisibleForTesting
+    static final int FLAG_HAS_BINDERS_KNOWN = 1 << 11;
+
+    @VisibleForTesting
+    static final int FLAG_HAS_BINDERS = 1 << 12;
+
+
+    /**
+     * Status when the Bundle can <b>assert</b> that the underlying Parcel DOES NOT contain
+     * Binder object(s).
+     *
+     * @hide
+     */
+    public static final int STATUS_BINDERS_NOT_PRESENT = 0;
+
+    /**
+     * Status when the Bundle can <b>assert</b> that there are Binder object(s) in the Parcel.
+     *
+     * @hide
+     */
+    public static final int STATUS_BINDERS_PRESENT = 1;
+
+    /**
+     * Status when the Bundle cannot be checked for Binders and there is no parcelled data
+     * available to check either.
+     * <p> This could happen when a Bundle is unparcelled or was never parcelled, and modified such
+     * that it is not possible to assert if the Bundle has any Binder objects in the current state.
+     *
+     * For e.g. calling {@link #putParcelable} or {@link #putBinder} could have added a Binder
+     * object to the Bundle but it is not possible to assert this fact unless the Bundle is written
+     * to a Parcel.
+     * </p>
+     *
+     * @hide
+     */
+    public static final int STATUS_BINDERS_UNKNOWN = 2;
+
+    /** @hide */
+    @IntDef(flag = true, prefix = {"STATUS_BINDERS_"}, value = {
+            STATUS_BINDERS_PRESENT,
+            STATUS_BINDERS_UNKNOWN,
+            STATUS_BINDERS_NOT_PRESENT
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface HasBinderStatus {
+    }
+
     /** An unmodifiable {@code Bundle} that is always {@link #isEmpty() empty}. */
     public static final Bundle EMPTY;
 
@@ -75,7 +125,7 @@
      */
     public Bundle() {
         super();
-        mFlags = FLAG_HAS_FDS_KNOWN | FLAG_ALLOW_FDS;
+        mFlags = FLAG_HAS_FDS_KNOWN | FLAG_HAS_BINDERS_KNOWN | FLAG_ALLOW_FDS;
     }
 
     /**
@@ -111,7 +161,6 @@
      *
      * @param from The bundle to be copied.
      * @param deep Whether is a deep or shallow copy.
-     *
      * @hide
      */
     Bundle(Bundle from, boolean deep) {
@@ -143,7 +192,7 @@
      */
     public Bundle(ClassLoader loader) {
         super(loader);
-        mFlags = FLAG_HAS_FDS_KNOWN | FLAG_ALLOW_FDS;
+        mFlags = FLAG_HAS_FDS_KNOWN | FLAG_HAS_BINDERS_KNOWN | FLAG_ALLOW_FDS;
     }
 
     /**
@@ -154,7 +203,7 @@
      */
     public Bundle(int capacity) {
         super(capacity);
-        mFlags = FLAG_HAS_FDS_KNOWN | FLAG_ALLOW_FDS;
+        mFlags = FLAG_HAS_FDS_KNOWN | FLAG_HAS_BINDERS_KNOWN | FLAG_ALLOW_FDS;
     }
 
     /**
@@ -180,7 +229,7 @@
      */
     public Bundle(PersistableBundle b) {
         super(b);
-        mFlags = FLAG_HAS_FDS_KNOWN | FLAG_ALLOW_FDS;
+        mFlags = FLAG_HAS_FDS_KNOWN | FLAG_HAS_BINDERS_KNOWN | FLAG_ALLOW_FDS;
     }
 
     /**
@@ -292,6 +341,9 @@
         if ((mFlags & FLAG_HAS_FDS) != 0) {
             mFlags &= ~FLAG_HAS_FDS_KNOWN;
         }
+        if ((mFlags & FLAG_HAS_BINDERS) != 0) {
+            mFlags &= ~FLAG_HAS_BINDERS_KNOWN;
+        }
     }
 
     /**
@@ -306,13 +358,20 @@
         bundle.mOwnsLazyValues = false;
         mMap.putAll(bundle.mMap);
 
-        // FD state is now known if and only if both bundles already knew
+        // FD and Binders state is now known if and only if both bundles already knew
         if ((bundle.mFlags & FLAG_HAS_FDS) != 0) {
             mFlags |= FLAG_HAS_FDS;
         }
         if ((bundle.mFlags & FLAG_HAS_FDS_KNOWN) == 0) {
             mFlags &= ~FLAG_HAS_FDS_KNOWN;
         }
+
+        if ((bundle.mFlags & FLAG_HAS_BINDERS) != 0) {
+            mFlags |= FLAG_HAS_BINDERS;
+        }
+        if ((bundle.mFlags & FLAG_HAS_BINDERS_KNOWN) == 0) {
+            mFlags &= ~FLAG_HAS_BINDERS_KNOWN;
+        }
     }
 
     /**
@@ -343,6 +402,33 @@
         return (mFlags & FLAG_HAS_FDS) != 0;
     }
 
+    /**
+     * Returns a status indicating whether the bundle contains any parcelled Binder objects.
+     * @hide
+     */
+    public @HasBinderStatus int hasBinders() {
+        if ((mFlags & FLAG_HAS_BINDERS_KNOWN) != 0) {
+            if ((mFlags & FLAG_HAS_BINDERS) != 0) {
+                return STATUS_BINDERS_PRESENT;
+            } else {
+                return STATUS_BINDERS_NOT_PRESENT;
+            }
+        }
+
+        final Parcel p = mParcelledData;
+        if (p == null) {
+            return STATUS_BINDERS_UNKNOWN;
+        }
+        if (p.hasBinders()) {
+            mFlags = mFlags | FLAG_HAS_BINDERS | FLAG_HAS_BINDERS_KNOWN;
+            return STATUS_BINDERS_PRESENT;
+        } else {
+            mFlags = mFlags & ~FLAG_HAS_BINDERS;
+            mFlags |= FLAG_HAS_BINDERS_KNOWN;
+            return STATUS_BINDERS_NOT_PRESENT;
+        }
+    }
+
     /** {@hide} */
     @Override
     public void putObject(@Nullable String key, @Nullable Object value) {
@@ -464,6 +550,7 @@
         unparcel();
         mMap.put(key, value);
         mFlags &= ~FLAG_HAS_FDS_KNOWN;
+        mFlags &= ~FLAG_HAS_BINDERS_KNOWN;
     }
 
     /**
@@ -502,6 +589,7 @@
         unparcel();
         mMap.put(key, value);
         mFlags &= ~FLAG_HAS_FDS_KNOWN;
+        mFlags &= ~FLAG_HAS_BINDERS_KNOWN;
     }
 
     /**
@@ -517,6 +605,7 @@
         unparcel();
         mMap.put(key, value);
         mFlags &= ~FLAG_HAS_FDS_KNOWN;
+        mFlags &= ~FLAG_HAS_BINDERS_KNOWN;
     }
 
     /** {@hide} */
@@ -525,6 +614,7 @@
         unparcel();
         mMap.put(key, value);
         mFlags &= ~FLAG_HAS_FDS_KNOWN;
+        mFlags &= ~FLAG_HAS_BINDERS_KNOWN;
     }
 
     /**
@@ -540,6 +630,7 @@
         unparcel();
         mMap.put(key, value);
         mFlags &= ~FLAG_HAS_FDS_KNOWN;
+        mFlags &= ~FLAG_HAS_BINDERS_KNOWN;
     }
 
     /**
@@ -680,6 +771,7 @@
     public void putBinder(@Nullable String key, @Nullable IBinder value) {
         unparcel();
         mMap.put(key, value);
+        mFlags &= ~FLAG_HAS_BINDERS_KNOWN;
     }
 
     /**
@@ -697,6 +789,7 @@
     public void putIBinder(@Nullable String key, @Nullable IBinder value) {
         unparcel();
         mMap.put(key, value);
+        mFlags &= ~FLAG_HAS_BINDERS_KNOWN;
     }
 
     /**
diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java
index ccfb632..bcef815 100644
--- a/core/java/android/os/Parcel.java
+++ b/core/java/android/os/Parcel.java
@@ -475,6 +475,10 @@
     private static native boolean nativeHasFileDescriptors(long nativePtr);
     private static native boolean nativeHasFileDescriptorsInRange(
             long nativePtr, int offset, int length);
+
+    private static native boolean nativeHasBinders(long nativePtr);
+    private static native boolean nativeHasBindersInRange(
+            long nativePtr, int offset, int length);
     @RavenwoodThrow
     private static native void nativeWriteInterfaceToken(long nativePtr, String interfaceName);
     @RavenwoodThrow
@@ -970,6 +974,34 @@
     }
 
     /**
+     * Report whether the parcel contains any marshalled IBinder objects.
+     *
+     * @throws UnsupportedOperationException if binder kernel driver was disabled or if method was
+     *                                       invoked in case of Binder RPC protocol.
+     * @hide
+     */
+    public boolean hasBinders() {
+        return nativeHasBinders(mNativePtr);
+    }
+
+    /**
+     * Report whether the parcel contains any marshalled {@link IBinder} objects in the range
+     * defined by {@code offset} and {@code length}.
+     *
+     * @param offset The offset from which the range starts. Should be between 0 and
+     *               {@link #dataSize()}.
+     * @param length The length of the range. Should be between 0 and {@link #dataSize()} - {@code
+     *               offset}.
+     * @return whether there are binders in the range or not.
+     * @throws IllegalArgumentException if the parameters are out of the permitted ranges.
+     *
+     * @hide
+     */
+    public boolean hasBinders(int offset, int length) {
+        return nativeHasBindersInRange(mNativePtr, offset, length);
+    }
+
+    /**
      * Store or read an IBinder interface token in the parcel at the current
      * {@link #dataPosition}. This is used to validate that the marshalled
      * transaction is intended for the target interface. This is typically written
diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java
index 7020a38..db06a6b 100644
--- a/core/java/android/os/Process.java
+++ b/core/java/android/os/Process.java
@@ -48,6 +48,7 @@
 import java.io.FileDescriptor;
 import java.io.IOException;
 import java.util.Map;
+import java.util.NoSuchElementException;
 import java.util.concurrent.TimeoutException;
 
 /**
@@ -588,6 +589,8 @@
      **/
     public static final int THREAD_GROUP_RESTRICTED = 7;
 
+    /** @hide */
+    public static final int SIGNAL_DEFAULT = 0;
     public static final int SIGNAL_QUIT = 3;
     public static final int SIGNAL_KILL = 9;
     public static final int SIGNAL_USR1 = 10;
@@ -1437,6 +1440,49 @@
         sendSignal(pid, SIGNAL_KILL);
     }
 
+    /**
+     * Check the tgid and tid pair to see if the tid still exists and belong to the tgid.
+     *
+     * TOCTOU warning: the status of the tid can change at the time this method returns. This should
+     * be used in very rare cases such as checking if a (tid, tgid) pair that is known to exist
+     * recently no longer exists now. As the possibility of the same tid to be reused under the same
+     * tgid during a short window is rare. And even if it happens the caller logic should be robust
+     * to handle it without error.
+     *
+     * @throws IllegalArgumentException if tgid or tid is not positive.
+     * @throws SecurityException if the caller doesn't have the permission, this method is expected
+     *                           to be used by system process with {@link #SYSTEM_UID} because it
+     *                           internally uses tkill(2).
+     * @throws NoSuchElementException if the Linux process with pid as the tid has exited or it
+     *                                doesn't belong to the tgid.
+     * @hide
+     */
+    public static final void checkTid(int tgid, int tid)
+            throws IllegalArgumentException, SecurityException, NoSuchElementException {
+        sendTgSignalThrows(tgid, tid, SIGNAL_DEFAULT);
+    }
+
+    /**
+     * Check if the pid still exists.
+     *
+     * TOCTOU warning: the status of the pid can change at the time this method returns. This should
+     * be used in very rare cases such as checking if a pid that belongs to an isolated process of a
+     * uid known to exist recently no longer exists now. As the possibility of the same pid to be
+     * reused again under the same uid during a short window is rare. And even if it happens the
+     * caller logic should be robust to handle it without error.
+     *
+     * @throws IllegalArgumentException if pid is not positive.
+     * @throws SecurityException if the caller doesn't have the permission, this method is expected
+     *                           to be used by system process with {@link #SYSTEM_UID} because it
+     *                           internally uses kill(2).
+     * @throws NoSuchElementException if the Linux process with the pid has exited.
+     * @hide
+     */
+    public static final void checkPid(int pid)
+            throws IllegalArgumentException, SecurityException, NoSuchElementException {
+        sendSignalThrows(pid, SIGNAL_DEFAULT);
+    }
+
     /** @hide */
     public static final native int setUid(int uid);
 
@@ -1451,6 +1497,12 @@
      */
     public static final native void sendSignal(int pid, int signal);
 
+    private static native void sendSignalThrows(int pid, int signal)
+            throws IllegalArgumentException, SecurityException, NoSuchElementException;
+
+    private static native void sendTgSignalThrows(int pid, int tgid, int signal)
+            throws IllegalArgumentException, SecurityException, NoSuchElementException;
+
     /**
      * @hide
      * Private impl for avoiding a log message...  DO NOT USE without doing
diff --git a/core/java/android/os/Trace.java b/core/java/android/os/Trace.java
index bebb912..edb3a64 100644
--- a/core/java/android/os/Trace.java
+++ b/core/java/android/os/Trace.java
@@ -125,15 +125,15 @@
     @UnsupportedAppUsage
     @CriticalNative
     @android.ravenwood.annotation.RavenwoodReplace
-    private static native long nativeGetEnabledTags();
+    private static native boolean nativeIsTagEnabled(long tag);
     @android.ravenwood.annotation.RavenwoodReplace
     private static native void nativeSetAppTracingAllowed(boolean allowed);
     @android.ravenwood.annotation.RavenwoodReplace
     private static native void nativeSetTracingEnabled(boolean allowed);
 
-    private static long nativeGetEnabledTags$ravenwood() {
+    private static boolean nativeIsTagEnabled$ravenwood(long traceTag) {
         // Tracing currently completely disabled under Ravenwood
-        return 0;
+        return false;
     }
 
     private static void nativeSetAppTracingAllowed$ravenwood(boolean allowed) {
@@ -181,8 +181,7 @@
     @UnsupportedAppUsage
     @SystemApi(client = MODULE_LIBRARIES)
     public static boolean isTagEnabled(long traceTag) {
-        long tags = nativeGetEnabledTags();
-        return (tags & traceTag) != 0;
+        return nativeIsTagEnabled(traceTag);
     }
 
     /**
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 84619a0..f172c3e 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -3188,6 +3188,8 @@
      * @return whether the context user can add a private profile.
      * @hide
      */
+    @TestApi
+    @FlaggedApi(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE)
     @RequiresPermission(anyOf = {
             Manifest.permission.MANAGE_USERS,
             Manifest.permission.CREATE_USERS},
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index e26dc73..25c2b0e 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -11090,21 +11090,12 @@
                 "assist_long_press_home_enabled";
 
         /**
-         * Whether press and hold on nav handle can trigger search.
+         * Whether all entrypoints can trigger search. Replaces individual settings.
          *
          * @hide
          */
-        public static final String SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED =
-                "search_press_hold_nav_handle_enabled";
-
-        /**
-         * Whether long-pressing on the home button can trigger search.
-         *
-         * @hide
-         */
-        public static final String SEARCH_LONG_PRESS_HOME_ENABLED =
-                "search_long_press_home_enabled";
-
+        public static final String SEARCH_ALL_ENTRYPOINTS_ENABLED =
+                "search_all_entrypoints_enabled";
 
         /**
          * Whether or not the accessibility data streaming is enbled for the
@@ -12395,6 +12386,13 @@
          */
         public static final String HIDE_PRIVATESPACE_ENTRY_POINT = "hide_privatespace_entry_point";
 
+        /**
+         * Whether or not secure windows should be disabled. This only works on debuggable builds.
+         *
+         * @hide
+         */
+        public static final String DISABLE_SECURE_WINDOWS = "disable_secure_windows";
+
         /** @hide */
         public static final int PRIVATE_SPACE_AUTO_LOCK_ON_DEVICE_LOCK = 0;
         /** @hide */
diff --git a/core/java/android/service/chooser/flags.aconfig b/core/java/android/service/chooser/flags.aconfig
index d72441f..00236df 100644
--- a/core/java/android/service/chooser/flags.aconfig
+++ b/core/java/android/service/chooser/flags.aconfig
@@ -27,14 +27,3 @@
   description: "Provides additional callbacks with information about user actions in ChooserResult"
   bug: "263474465"
 }
-
-flag {
-  name: "legacy_chooser_pinning_removal"
-  namespace: "intentresolver"
-  description: "Removing pinning functionality from the legacy chooser (used by partial screenshare)"
-  bug: "301068735"
-  metadata {
-    purpose: PURPOSE_BUGFIX
-  }
-}
-
diff --git a/core/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl b/core/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl
index 6dbff71..908ab5f 100644
--- a/core/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl
+++ b/core/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl
@@ -41,7 +41,9 @@
     void getFeatureDetails(int callerUid, in Feature feature, in IFeatureDetailsCallback featureDetailsCallback);
     void getReadOnlyFileDescriptor(in String fileName, in AndroidFuture<ParcelFileDescriptor> future);
     void getReadOnlyFeatureFileDescriptorMap(in Feature feature, in RemoteCallback remoteCallback);
-    void requestFeatureDownload(int callerUid, in Feature feature, in ICancellationSignal cancellationSignal, in IDownloadCallback downloadCallback);
+    void requestFeatureDownload(int callerUid, in Feature feature,
+                                in AndroidFuture<ICancellationSignal> cancellationSignal,
+                                in IDownloadCallback downloadCallback);
     void registerRemoteServices(in IRemoteProcessingService remoteProcessingService);
     void notifyInferenceServiceConnected();
     void notifyInferenceServiceDisconnected();
diff --git a/core/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl b/core/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl
index 799c7545..4213a09 100644
--- a/core/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl
+++ b/core/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl
@@ -24,6 +24,7 @@
 import android.os.ICancellationSignal;
 import android.os.PersistableBundle;
 import android.os.Bundle;
+import com.android.internal.infra.AndroidFuture;
 import android.service.ondeviceintelligence.IRemoteStorageService;
 import android.service.ondeviceintelligence.IProcessingUpdateStatusCallback;
 
@@ -34,13 +35,16 @@
  */
 oneway interface IOnDeviceSandboxedInferenceService {
     void registerRemoteStorageService(in IRemoteStorageService storageService);
-    void requestTokenInfo(int callerUid, in Feature feature, in Bundle request, in ICancellationSignal cancellationSignal,
+    void requestTokenInfo(int callerUid, in Feature feature, in Bundle request,
+                            in AndroidFuture<ICancellationSignal> cancellationSignal,
                             in ITokenInfoCallback tokenInfoCallback);
     void processRequest(int callerUid, in Feature feature, in Bundle request, in int requestType,
-                        in ICancellationSignal cancellationSignal, in IProcessingSignal processingSignal,
+                        in AndroidFuture<ICancellationSignal> cancellationSignal,
+                        in AndroidFuture<IProcessingSignal> processingSignal,
                         in IResponseCallback callback);
     void processRequestStreaming(int callerUid, in Feature feature, in Bundle request, in int requestType,
-                                in ICancellationSignal cancellationSignal, in IProcessingSignal processingSignal,
+                                in AndroidFuture<ICancellationSignal> cancellationSignal,
+                                in AndroidFuture<IProcessingSignal> processingSignal,
                                 in IStreamingResponseCallback callback);
     void updateProcessingState(in Bundle processingState,
                                      in IProcessingUpdateStatusCallback callback);
diff --git a/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java b/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java
index 9321318..86320b8 100644
--- a/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java
+++ b/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java
@@ -148,14 +148,18 @@
 
                 @Override
                 public void requestFeatureDownload(int callerUid, Feature feature,
-                        ICancellationSignal cancellationSignal,
+                        AndroidFuture cancellationSignalFuture,
                         IDownloadCallback downloadCallback) {
                     Objects.requireNonNull(feature);
                     Objects.requireNonNull(downloadCallback);
-
+                    ICancellationSignal transport = null;
+                    if (cancellationSignalFuture != null) {
+                        transport = CancellationSignal.createTransport();
+                        cancellationSignalFuture.complete(transport);
+                    }
                     OnDeviceIntelligenceService.this.onDownloadFeature(callerUid,
                             feature,
-                            CancellationSignal.fromTransport(cancellationSignal),
+                            CancellationSignal.fromTransport(transport),
                             wrapDownloadCallback(downloadCallback));
                 }
 
diff --git a/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java b/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java
index fc7a4c8..96c45ee 100644
--- a/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java
+++ b/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java
@@ -122,46 +122,72 @@
 
                 @Override
                 public void requestTokenInfo(int callerUid, Feature feature, Bundle request,
-                        ICancellationSignal cancellationSignal,
+                        AndroidFuture cancellationSignalFuture,
                         ITokenInfoCallback tokenInfoCallback) {
                     Objects.requireNonNull(feature);
                     Objects.requireNonNull(tokenInfoCallback);
+                    ICancellationSignal transport = null;
+                    if (cancellationSignalFuture != null) {
+                        transport = CancellationSignal.createTransport();
+                        cancellationSignalFuture.complete(transport);
+                    }
                     OnDeviceSandboxedInferenceService.this.onTokenInfoRequest(callerUid,
                             feature,
                             request,
-                            CancellationSignal.fromTransport(cancellationSignal),
+                            CancellationSignal.fromTransport(transport),
                             wrapTokenInfoCallback(tokenInfoCallback));
                 }
 
                 @Override
                 public void processRequestStreaming(int callerUid, Feature feature, Bundle request,
-                        int requestType, ICancellationSignal cancellationSignal,
-                        IProcessingSignal processingSignal,
+                        int requestType,
+                        AndroidFuture cancellationSignalFuture,
+                        AndroidFuture processingSignalFuture,
                         IStreamingResponseCallback callback) {
                     Objects.requireNonNull(feature);
                     Objects.requireNonNull(callback);
 
+                    ICancellationSignal transport = null;
+                    if (cancellationSignalFuture != null) {
+                        transport = CancellationSignal.createTransport();
+                        cancellationSignalFuture.complete(transport);
+                    }
+                    IProcessingSignal processingSignalTransport = null;
+                    if (processingSignalFuture != null) {
+                        processingSignalTransport = ProcessingSignal.createTransport();
+                        processingSignalFuture.complete(processingSignalTransport);
+                    }
                     OnDeviceSandboxedInferenceService.this.onProcessRequestStreaming(callerUid,
                             feature,
                             request,
                             requestType,
-                            CancellationSignal.fromTransport(cancellationSignal),
-                            ProcessingSignal.fromTransport(processingSignal),
+                            CancellationSignal.fromTransport(transport),
+                            ProcessingSignal.fromTransport(processingSignalTransport),
                             wrapStreamingResponseCallback(callback));
                 }
 
                 @Override
                 public void processRequest(int callerUid, Feature feature, Bundle request,
-                        int requestType, ICancellationSignal cancellationSignal,
-                        IProcessingSignal processingSignal,
+                        int requestType,
+                        AndroidFuture cancellationSignalFuture,
+                        AndroidFuture processingSignalFuture,
                         IResponseCallback callback) {
                     Objects.requireNonNull(feature);
                     Objects.requireNonNull(callback);
-
+                    ICancellationSignal transport = null;
+                    if (cancellationSignalFuture != null) {
+                        transport = CancellationSignal.createTransport();
+                        cancellationSignalFuture.complete(transport);
+                    }
+                    IProcessingSignal processingSignalTransport = null;
+                    if (processingSignalFuture != null) {
+                        processingSignalTransport = ProcessingSignal.createTransport();
+                        processingSignalFuture.complete(processingSignalTransport);
+                    }
                     OnDeviceSandboxedInferenceService.this.onProcessRequest(callerUid, feature,
                             request, requestType,
-                            CancellationSignal.fromTransport(cancellationSignal),
-                            ProcessingSignal.fromTransport(processingSignal),
+                            CancellationSignal.fromTransport(transport),
+                            ProcessingSignal.fromTransport(processingSignalTransport),
                             wrapResponseCallback(callback));
                 }
 
@@ -206,7 +232,8 @@
      * Invoked when caller provides a request for a particular feature to be processed in a
      * streaming manner. The expectation from the implementation is that when processing the
      * request,
-     * it periodically populates the {@link StreamingProcessingCallback#onPartialResult} to continuously
+     * it periodically populates the {@link StreamingProcessingCallback#onPartialResult} to
+     * continuously
      * provide partial Bundle results for the caller to utilize. Optionally the implementation can
      * provide the complete response in the {@link StreamingProcessingCallback#onResult} upon
      * processing completion.
diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java
index bbda068..f6d197c 100644
--- a/core/java/android/service/wallpaper/WallpaperService.java
+++ b/core/java/android/service/wallpaper/WallpaperService.java
@@ -102,6 +102,7 @@
 import android.view.WindowLayout;
 import android.view.WindowManager;
 import android.view.WindowManagerGlobal;
+import android.window.ActivityWindowInfo;
 import android.window.ClientWindowFrames;
 import android.window.ScreenCapture;
 
@@ -211,7 +212,7 @@
      * @hide
      */
     @ChangeId
-    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
     public static final long WEAROS_WALLPAPER_HANDLES_SCALING = 272527315L;
 
     static final class WallpaperCommand {
@@ -459,7 +460,8 @@
             public void resized(ClientWindowFrames frames, boolean reportDraw,
                     MergedConfiguration mergedConfiguration, InsetsState insetsState,
                     boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId,
-                    int syncSeqId, boolean dragResizing) {
+                    int syncSeqId, boolean dragResizing,
+                    @Nullable ActivityWindowInfo activityWindowInfo) {
                 Message msg = mCaller.obtainMessageIO(MSG_WINDOW_RESIZED,
                         reportDraw ? 1 : 0,
                         mergedConfiguration);
diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig
index aff1d4a..30b1a2e 100644
--- a/core/java/android/text/flags/flags.aconfig
+++ b/core/java/android/text/flags/flags.aconfig
@@ -121,8 +121,22 @@
 }
 
 flag {
+  name: "handwriting_end_of_line_tap"
+  namespace: "text"
+  description: "Initiate handwriting when stylus taps at the end of a line in a focused non-empty TextView with the cursor at the end of that line"
+  bug: "323376217"
+}
+
+flag {
   name: "handwriting_cursor_position"
   namespace: "text"
   description: "When handwriting is initiated in an unfocused TextView, cursor is placed at the end of the closest paragraph."
   bug: "323376217"
 }
+
+flag {
+  name: "handwriting_unsupported_message"
+  namespace: "text"
+  description: "Feature flag for showing error message when user tries stylus handwriting on a text field which doesn't support it"
+  bug: "297962571"
+}
diff --git a/core/java/android/view/HandwritingInitiator.java b/core/java/android/view/HandwritingInitiator.java
index 29c8350..f4dadbb 100644
--- a/core/java/android/view/HandwritingInitiator.java
+++ b/core/java/android/view/HandwritingInitiator.java
@@ -34,7 +34,9 @@
 import android.widget.EditText;
 import android.widget.Editor;
 import android.widget.TextView;
+import android.widget.Toast;
 
+import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.lang.ref.WeakReference;
@@ -223,24 +225,43 @@
                     View candidateView = findBestCandidateView(mState.mStylusDownX,
                             mState.mStylusDownY, /* isHover */ false);
                     if (candidateView != null && candidateView.isEnabled()) {
-                        if (candidateView == getConnectedOrFocusedView()) {
-                            if (!mInitiateWithoutConnection && !candidateView.hasFocus()) {
+                        boolean candidateHasFocus = candidateView.hasFocus();
+                        if (shouldShowHandwritingUnavailableMessageForView(candidateView)) {
+                            int messagesResId = (candidateView instanceof TextView tv
+                                    && tv.isAnyPasswordInputType())
+                                    ? R.string.error_handwriting_unsupported_password
+                                    : R.string.error_handwriting_unsupported;
+                            Toast.makeText(candidateView.getContext(), messagesResId,
+                                    Toast.LENGTH_SHORT).show();
+                            if (!candidateView.hasFocus()) {
+                                requestFocusWithoutReveal(candidateView);
+                            }
+                            mImm.showSoftInput(candidateView, 0);
+                            mState.mHandled = true;
+                            mState.mShouldInitHandwriting = false;
+                            motionEvent.setAction((motionEvent.getAction()
+                                    & MotionEvent.ACTION_POINTER_INDEX_MASK)
+                                    | MotionEvent.ACTION_CANCEL);
+                            candidateView.getRootView().dispatchTouchEvent(motionEvent);
+                        } else if (candidateView == getConnectedOrFocusedView()) {
+                            if (!candidateHasFocus) {
                                 requestFocusWithoutReveal(candidateView);
                             }
                             startHandwriting(candidateView);
                         } else if (candidateView.getHandwritingDelegatorCallback() != null) {
                             prepareDelegation(candidateView);
                         } else {
-                            if (!mInitiateWithoutConnection) {
+                            if (mInitiateWithoutConnection) {
+                                if (!candidateHasFocus) {
+                                    // schedule for view focus.
+                                    mState.mPendingFocusedView = new WeakReference<>(candidateView);
+                                    requestFocusWithoutReveal(candidateView);
+                                }
+                            } else {
                                 mState.mPendingConnectedView = new WeakReference<>(candidateView);
-                            }
-                            if (!candidateView.hasFocus()) {
-                                requestFocusWithoutReveal(candidateView);
-                            }
-                            if (mInitiateWithoutConnection
-                                    && updateFocusedView(candidateView,
-                                            /* fromTouchEvent */ true)) {
-                                startHandwriting(candidateView);
+                                if (!candidateHasFocus) {
+                                    requestFocusWithoutReveal(candidateView);
+                                }
                             }
                         }
                     }
@@ -266,6 +287,9 @@
      * gained focus.
      */
     public void onDelegateViewFocused(@NonNull View view) {
+        if (mInitiateWithoutConnection) {
+            onEditorFocused(view);
+        }
         if (view == getConnectedView()) {
             tryAcceptStylusHandwritingDelegation(view);
         }
@@ -313,6 +337,33 @@
     }
 
     /**
+     * Notify HandwritingInitiator that a new editor is focused.
+     * @param view the view that received focus.
+     */
+    @VisibleForTesting
+    public void onEditorFocused(@NonNull View view) {
+        if (!mInitiateWithoutConnection) {
+            return;
+        }
+
+        if (!view.isAutoHandwritingEnabled()) {
+            clearFocusedView(view);
+            return;
+        }
+
+        final View focusedView = getFocusedView();
+        if (focusedView == view) {
+            return;
+        }
+        updateFocusedView(view);
+
+        if (mState != null && mState.mPendingFocusedView != null
+                && mState.mPendingFocusedView.get() == view) {
+            startHandwriting(view);
+        }
+    }
+
+    /**
      * Notify HandwritingInitiator that the InputConnection has closed for the given view.
      * The caller of this method should guarantee that each onInputConnectionClosed call
      * is paired with a onInputConnectionCreated call.
@@ -359,7 +410,7 @@
      * @return {@code true} if handwriting can initiate for given view.
      */
     @VisibleForTesting
-    public boolean updateFocusedView(@NonNull View view, boolean fromTouchEvent) {
+    public boolean updateFocusedView(@NonNull View view) {
         if (!view.shouldInitiateHandwriting()) {
             mFocusedView = null;
             return false;
@@ -371,9 +422,7 @@
             // A new view just gain focus. By default, we should show hover icon for it.
             mShowHoverIconForConnectedView = true;
         }
-        if (!fromTouchEvent && view.isHandwritingDelegate()) {
-            tryAcceptStylusHandwritingDelegation(view);
-        }
+
         return true;
     }
 
@@ -484,6 +533,15 @@
         return view.isStylusHandwritingAvailable();
     }
 
+    private static boolean shouldShowHandwritingUnavailableMessageForView(@NonNull View view) {
+        return (view instanceof TextView) && !shouldTriggerStylusHandwritingForView(view);
+    }
+
+    private static boolean shouldTriggerHandwritingOrShowUnavailableMessageForView(
+            @NonNull View view) {
+        return (view instanceof TextView) || shouldTriggerStylusHandwritingForView(view);
+    }
+
     /**
      * Returns the pointer icon for the motion event, or null if it doesn't specify the icon.
      * This gives HandwritingInitiator a chance to show the stylus handwriting icon over a
@@ -491,7 +549,7 @@
      */
     public PointerIcon onResolvePointerIcon(Context context, MotionEvent event) {
         final View hoverView = findHoverView(event);
-        if (hoverView == null) {
+        if (hoverView == null || !shouldTriggerStylusHandwritingForView(hoverView)) {
             return null;
         }
 
@@ -594,7 +652,7 @@
 
     /**
      * Given the location of the stylus event, return the best candidate view to initialize
-     * handwriting mode.
+     * handwriting mode or show the handwriting unavailable error message.
      *
      * @param x the x coordinates of the stylus event, in the coordinates of the window.
      * @param y the y coordinates of the stylus event, in the coordinates of the window.
@@ -610,7 +668,8 @@
             Rect handwritingArea = mTempRect;
             if (getViewHandwritingArea(connectedOrFocusedView, handwritingArea)
                     && isInHandwritingArea(handwritingArea, x, y, connectedOrFocusedView, isHover)
-                    && shouldTriggerStylusHandwritingForView(connectedOrFocusedView)) {
+                    && shouldTriggerHandwritingOrShowUnavailableMessageForView(
+                            connectedOrFocusedView)) {
                 if (!isHover && mState != null) {
                     mState.mStylusDownWithinEditorBounds =
                             contains(handwritingArea, x, y, 0f, 0f, 0f, 0f);
@@ -628,7 +687,7 @@
             final View view = viewInfo.getView();
             final Rect handwritingArea = viewInfo.getHandwritingArea();
             if (!isInHandwritingArea(handwritingArea, x, y, view, isHover)
-                    || !shouldTriggerStylusHandwritingForView(view)) {
+                    || !shouldTriggerHandwritingOrShowUnavailableMessageForView(view)) {
                 continue;
             }
 
@@ -832,6 +891,12 @@
          */
         private WeakReference<View> mPendingConnectedView = null;
 
+        /**
+         * A view which has requested focus and is yet to receive it.
+         * When view receives focus, a handwriting session should be started for the view.
+         */
+        private WeakReference<View> mPendingFocusedView = null;
+
         /** The pointer id of the stylus pointer that is being tracked. */
         private final int mStylusPointerId;
         /** The time stamp when the stylus pointer goes down. */
@@ -856,7 +921,7 @@
     /** The helper method to check if the given view is still active for handwriting. */
     private static boolean isViewActive(@Nullable View view) {
         return view != null && view.isAttachedToWindow() && view.isAggregatedVisible()
-                && view.shouldInitiateHandwriting();
+                && view.shouldTrackHandwritingArea();
     }
 
     private CursorAnchorInfo getCursorAnchorInfoForConnectionless(View view) {
diff --git a/core/java/android/view/IWindow.aidl b/core/java/android/view/IWindow.aidl
index 5ee526e..1c0834f 100644
--- a/core/java/android/view/IWindow.aidl
+++ b/core/java/android/view/IWindow.aidl
@@ -30,6 +30,7 @@
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.inputmethod.ImeTracker;
+import android.window.ActivityWindowInfo;
 import android.window.ClientWindowFrames;
 
 import com.android.internal.os.IResultReceiver;
@@ -61,7 +62,8 @@
     void resized(in ClientWindowFrames frames, boolean reportDraw,
             in MergedConfiguration newMergedConfiguration, in InsetsState insetsState,
             boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId,
-            int syncSeqId, boolean dragResizing);
+            int syncSeqId, boolean dragResizing,
+            in @nullable ActivityWindowInfo activityWindowInfo);
 
     /**
      * Called when this window retrieved control over a specified set of insets sources.
diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl
index e126836..3a90841 100644
--- a/core/java/android/view/IWindowSession.aidl
+++ b/core/java/android/view/IWindowSession.aidl
@@ -47,6 +47,19 @@
  * {@hide}
  */
 interface IWindowSession {
+
+    /**
+     * Bundle key to store the latest sync seq id for the relayout configuration.
+     * @see #relayout
+     */
+    const String KEY_RELAYOUT_BUNDLE_SEQID = "seqid";
+    /**
+     * Bundle key to store the latest ActivityWindowInfo associated with the relayout configuration.
+     * Will only be set if the relayout window is an activity window.
+     * @see #relayout
+     */
+    const String KEY_RELAYOUT_BUNDLE_ACTIVITY_WINDOW_INFO = "activity_window_info";
+
     int addToDisplay(IWindow window, in WindowManager.LayoutParams attrs,
             in int viewVisibility, in int layerStackId, int requestedVisibleTypes,
             out InputChannel outInputChannel, out InsetsState insetsState,
@@ -92,7 +105,7 @@
      * @param outSurfaceControl Object in which is placed the new display surface.
      * @param insetsState The current insets state in the system.
      * @param activeControls Objects which allow controlling {@link InsetsSource}s.
-     * @param bundle A temporary object to obtain the latest SyncSeqId.
+     * @param bundle A Bundle to contain the latest SyncSeqId and any extra relayout optional infos.
      * @return int Result flags, defined in {@link WindowManagerGlobal}.
      */
     int relayout(IWindow window, in WindowManager.LayoutParams attrs,
diff --git a/core/java/android/view/OWNERS b/core/java/android/view/OWNERS
index a2f767d..07d05a4 100644
--- a/core/java/android/view/OWNERS
+++ b/core/java/android/view/OWNERS
@@ -75,12 +75,14 @@
 per-file View.java = file:/services/core/java/com/android/server/input/OWNERS
 per-file View.java = file:/services/core/java/com/android/server/wm/OWNERS
 per-file View.java = file:/core/java/android/view/inputmethod/OWNERS
+per-file View.java = file:/core/java/android/text/OWNERS
 per-file ViewRootImpl.java = file:/services/accessibility/OWNERS
 per-file ViewRootImpl.java = file:/core/java/android/service/autofill/OWNERS
 per-file ViewRootImpl.java = file:/graphics/java/android/graphics/OWNERS
 per-file ViewRootImpl.java = file:/services/core/java/com/android/server/input/OWNERS
 per-file ViewRootImpl.java = file:/services/core/java/com/android/server/wm/OWNERS
 per-file ViewRootImpl.java = file:/core/java/android/view/inputmethod/OWNERS
+per-file ViewRootImpl.java = file:/core/java/android/text/OWNERS
 per-file AccessibilityInteractionController.java = file:/services/accessibility/OWNERS
 per-file OnReceiveContentListener.java = file:/core/java/android/service/autofill/OWNERS
 per-file OnReceiveContentListener.java = file:/core/java/android/widget/OWNERS
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 0a75f4e..0a9ac2f 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -44,6 +44,7 @@
 import static android.view.flags.Flags.toolkitSetFrameRateReadOnly;
 import static android.view.flags.Flags.viewVelocityApi;
 import static android.view.inputmethod.Flags.FLAG_HOME_SCREEN_HANDWRITING_DELEGATOR;
+import static android.view.inputmethod.Flags.initiationWithoutInputConnection;
 
 import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS;
 import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS;
@@ -8496,8 +8497,9 @@
      *            hierarchy
      * @param refocus when propagate is true, specifies whether to request the
      *            root view place new focus
+     * @hide
      */
-    void clearFocusInternal(View focused, boolean propagate, boolean refocus) {
+    public void clearFocusInternal(View focused, boolean propagate, boolean refocus) {
         if ((mPrivateFlags & PFLAG_FOCUSED) != 0) {
             mPrivateFlags &= ~PFLAG_FOCUSED;
             clearParentsWantFocus();
@@ -8668,11 +8670,12 @@
             onFocusLost();
         } else if (hasWindowFocus()) {
             notifyFocusChangeToImeFocusController(true /* hasFocus */);
-
-            if (mIsHandwritingDelegate) {
-                ViewRootImpl viewRoot = getViewRootImpl();
-                if (viewRoot != null) {
+            ViewRootImpl viewRoot = getViewRootImpl();
+            if (viewRoot != null) {
+                if (mIsHandwritingDelegate) {
                     viewRoot.getHandwritingInitiator().onDelegateViewFocused(this);
+                } else if (initiationWithoutInputConnection() && onCheckIsTextEditor()) {
+                    viewRoot.getHandwritingInitiator().onEditorFocused(this);
                 }
             }
         }
@@ -12695,7 +12698,7 @@
         if (getSystemGestureExclusionRects().isEmpty()
                 && collectPreferKeepClearRects().isEmpty()
                 && collectUnrestrictedPreferKeepClearRects().isEmpty()
-                && (info.mHandwritingArea == null || !shouldInitiateHandwriting())) {
+                && (info.mHandwritingArea == null || !shouldTrackHandwritingArea())) {
             if (info.mPositionUpdateListener != null) {
                 mRenderNode.removePositionUpdateListener(info.mPositionUpdateListener);
                 info.mPositionUpdateListener = null;
@@ -13062,7 +13065,7 @@
 
     void updateHandwritingArea() {
         // If autoHandwritingArea is not enabled, do nothing.
-        if (!shouldInitiateHandwriting()) return;
+        if (!shouldTrackHandwritingArea()) return;
         final AttachInfo ai = mAttachInfo;
         if (ai != null) {
             ai.mViewRootImpl.getHandwritingInitiator().updateHandwritingAreasForView(this);
@@ -13080,6 +13083,16 @@
     }
 
     /**
+     * Returns whether the handwriting initiator should track the handwriting area for this view,
+     * either to initiate handwriting mode, or to prepare handwriting delegation, or to show the
+     * handwriting unsupported message.
+     * @hide
+     */
+    public boolean shouldTrackHandwritingArea() {
+        return shouldInitiateHandwriting();
+    }
+
+    /**
      * Sets a callback which should be called when a stylus {@link MotionEvent} occurs within this
      * view's bounds. The callback will be called from the UI thread.
      *
@@ -16691,6 +16704,10 @@
             onFocusLost();
         } else if ((mPrivateFlags & PFLAG_FOCUSED) != 0) {
             notifyFocusChangeToImeFocusController(true /* hasFocus */);
+            ViewRootImpl viewRoot = getViewRootImpl();
+            if (viewRoot != null && initiationWithoutInputConnection() && onCheckIsTextEditor()) {
+                viewRoot.getHandwritingInitiator().onEditorFocused(this);
+            }
         }
 
         refreshDrawableState();
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index cae6672..5fe8c00 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -114,6 +114,7 @@
 import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.INSETS_CONTROLLER;
 
 import static com.android.input.flags.Flags.enablePointerChoreographer;
+import static com.android.window.flags.Flags.activityWindowInfoFlag;
 import static com.android.window.flags.Flags.enableBufferTransformHintFromDisplay;
 
 import android.Manifest;
@@ -233,6 +234,7 @@
 import android.view.inputmethod.ImeTracker;
 import android.view.inputmethod.InputMethodManager;
 import android.widget.Scroller;
+import android.window.ActivityWindowInfo;
 import android.window.BackEvent;
 import android.window.ClientWindowFrames;
 import android.window.CompatOnBackInvokedCallback;
@@ -435,13 +437,27 @@
      * Callback for notifying activities.
      */
     public interface ActivityConfigCallback {
-
         /**
          * Notifies about override config change and/or move to different display.
          * @param overrideConfig New override config to apply to activity.
          * @param newDisplayId New display id, {@link Display#INVALID_DISPLAY} if not changed.
          */
-        void onConfigurationChanged(Configuration overrideConfig, int newDisplayId);
+        default void onConfigurationChanged(@NonNull Configuration overrideConfig,
+                int newDisplayId) {
+            // Must override one of the #onConfigurationChanged.
+            throw new IllegalStateException("Not implemented");
+        }
+
+        /**
+         * Notifies about override config change and/or move to different display.
+         * @param overrideConfig New override config to apply to activity.
+         * @param newDisplayId New display id, {@link Display#INVALID_DISPLAY} if not changed.
+         * @param activityWindowInfo New ActivityWindowInfo to apply to activity.
+         */
+        default void onConfigurationChanged(@NonNull Configuration overrideConfig,
+                int newDisplayId, @Nullable ActivityWindowInfo activityWindowInfo) {
+            onConfigurationChanged(overrideConfig, newDisplayId);
+        }
 
         /**
          * Notify the corresponding activity about the request to show or hide a camera compat
@@ -467,7 +483,7 @@
      * In that case we receive a call back from {@link ActivityThread} and this flag is used to
      * preserve the initial value.
      *
-     * @see #performConfigurationChange(MergedConfiguration, boolean, int)
+     * @see #performConfigurationChange
      */
     private boolean mForceNextConfigUpdate;
 
@@ -814,6 +830,13 @@
     /** Configurations waiting to be applied. */
     private final MergedConfiguration mPendingMergedConfiguration = new MergedConfiguration();
 
+    /** Non-{@code null} if {@link #mActivityConfigCallback} is not {@code null}. */
+    @Nullable
+    private ActivityWindowInfo mPendingActivityWindowInfo;
+    /** Non-{@code null} if {@link #mActivityConfigCallback} is not {@code null}. */
+    @Nullable
+    private ActivityWindowInfo mLastReportedActivityWindowInfo;
+
     boolean mScrollMayChange;
     @SoftInputModeFlags
     int mSoftInputMode;
@@ -1260,8 +1283,18 @@
      * Add activity config callback to be notified about override config changes and camera
      * compat control state updates.
      */
-    public void setActivityConfigCallback(ActivityConfigCallback callback) {
+    public void setActivityConfigCallback(@Nullable ActivityConfigCallback callback) {
         mActivityConfigCallback = callback;
+        if (!activityWindowInfoFlag()) {
+            return;
+        }
+        if (callback == null) {
+            mPendingActivityWindowInfo = null;
+            mLastReportedActivityWindowInfo = null;
+        } else {
+            mPendingActivityWindowInfo = new ActivityWindowInfo();
+            mLastReportedActivityWindowInfo = new ActivityWindowInfo();
+        }
     }
 
     public void setOnContentApplyWindowInsetsListener(OnContentApplyWindowInsetsListener listener) {
@@ -2096,7 +2129,8 @@
     /** Handles messages {@link #MSG_RESIZED} and {@link #MSG_RESIZED_REPORT}. */
     private void handleResized(ClientWindowFrames frames, boolean reportDraw,
             MergedConfiguration mergedConfiguration, InsetsState insetsState, boolean forceLayout,
-            boolean alwaysConsumeSystemBars, int displayId, int syncSeqId, boolean dragResizing) {
+            boolean alwaysConsumeSystemBars, int displayId, int syncSeqId, boolean dragResizing,
+            @Nullable ActivityWindowInfo activityWindowInfo) {
         if (!mAdded) {
             return;
         }
@@ -2114,7 +2148,14 @@
         mInsetsController.onStateChanged(insetsState);
         final float compatScale = frames.compatScale;
         final boolean frameChanged = !mWinFrame.equals(frame);
-        final boolean configChanged = !mLastReportedMergedConfiguration.equals(mergedConfiguration);
+        final boolean shouldReportActivityWindowInfoChanged =
+                // Can be null if callbacks is not set
+                mLastReportedActivityWindowInfo != null
+                        // Can be null if not activity window
+                        && activityWindowInfo != null
+                        && !mLastReportedActivityWindowInfo.equals(activityWindowInfo);
+        final boolean configChanged = !mLastReportedMergedConfiguration.equals(mergedConfiguration)
+                || shouldReportActivityWindowInfoChanged;
         final boolean attachedFrameChanged =
                 !Objects.equals(mTmpFrames.attachedFrame, attachedFrame);
         final boolean displayChanged = mDisplay.getDisplayId() != displayId;
@@ -2133,7 +2174,8 @@
         if (configChanged) {
             // If configuration changed - notify about that and, maybe, about move to display.
             performConfigurationChange(mergedConfiguration, false /* force */,
-                    displayChanged ? displayId : INVALID_DISPLAY /* same display */);
+                    displayChanged ? displayId : INVALID_DISPLAY /* same display */,
+                    activityWindowInfo);
         } else if (displayChanged) {
             // Moved to display without config change - report last applied one.
             onMovedToDisplay(displayId, mLastConfigurationFromResources);
@@ -3532,12 +3574,18 @@
                 // WindowManagerService has reported back a frame from a configuration not yet
                 // handled by the client. In this case, we need to accept the configuration so we
                 // do not lay out and draw with the wrong configuration.
-                if (mRelayoutRequested
-                        && !mPendingMergedConfiguration.equals(mLastReportedMergedConfiguration)) {
+                boolean shouldPerformConfigurationUpdate =
+                        !mPendingMergedConfiguration.equals(mLastReportedMergedConfiguration)
+                                || !Objects.equals(mPendingActivityWindowInfo,
+                                mLastReportedActivityWindowInfo);
+                if (mRelayoutRequested && shouldPerformConfigurationUpdate) {
                     if (DEBUG_CONFIGURATION) Log.v(mTag, "Visible with new config: "
                             + mPendingMergedConfiguration.getMergedConfiguration());
                     performConfigurationChange(new MergedConfiguration(mPendingMergedConfiguration),
-                            !mFirst, INVALID_DISPLAY /* same display */);
+                            !mFirst, INVALID_DISPLAY /* same display */,
+                            mPendingActivityWindowInfo != null
+                                    ? new ActivityWindowInfo(mPendingActivityWindowInfo)
+                                    : null);
                     updatedConfiguration = true;
                 }
                 final boolean updateSurfaceNeeded = mUpdateSurfaceNeeded;
@@ -6063,9 +6111,11 @@
      * @param force Flag indicating if we should force apply the config.
      * @param newDisplayId Id of new display if moved, {@link Display#INVALID_DISPLAY} if not
      *                     changed.
+     * @param activityWindowInfo New activity window info. {@code null} if it is non-app window, or
+     *                           this is not a Configuration change to the activity window (global).
      */
-    private void performConfigurationChange(MergedConfiguration mergedConfiguration, boolean force,
-            int newDisplayId) {
+    private void performConfigurationChange(@NonNull MergedConfiguration mergedConfiguration,
+            boolean force, int newDisplayId, @Nullable ActivityWindowInfo activityWindowInfo) {
         if (mergedConfiguration == null) {
             throw new IllegalArgumentException("No merged config provided.");
         }
@@ -6105,6 +6155,9 @@
         }
 
         mLastReportedMergedConfiguration.setConfiguration(globalConfig, overrideConfig);
+        if (mLastReportedActivityWindowInfo != null && activityWindowInfo != null) {
+            mLastReportedActivityWindowInfo.set(activityWindowInfo);
+        }
 
         mForceNextConfigUpdate = force;
         if (mActivityConfigCallback != null) {
@@ -6112,7 +6165,8 @@
             // This basically initiates a round trip to ActivityThread and back, which will ensure
             // that corresponding activity and resources are updated before updating inner state of
             // ViewRootImpl. Eventually it will call #updateConfiguration().
-            mActivityConfigCallback.onConfigurationChanged(overrideConfig, newDisplayId);
+            mActivityConfigCallback.onConfigurationChanged(overrideConfig, newDisplayId,
+                    activityWindowInfo);
         } else {
             // There is no activity callback - update the configuration right away.
             updateConfiguration(newDisplayId);
@@ -6354,13 +6408,15 @@
                     final boolean reportDraw = msg.what == MSG_RESIZED_REPORT;
                     final MergedConfiguration mergedConfiguration = (MergedConfiguration) args.arg2;
                     final InsetsState insetsState = (InsetsState) args.arg3;
+                    final ActivityWindowInfo activityWindowInfo = (ActivityWindowInfo) args.arg4;
                     final boolean forceLayout = args.argi1 != 0;
                     final boolean alwaysConsumeSystemBars = args.argi2 != 0;
                     final int displayId = args.argi3;
                     final int syncSeqId = args.argi4;
                     final boolean dragResizing = args.argi5 != 0;
                     handleResized(frames, reportDraw, mergedConfiguration, insetsState, forceLayout,
-                            alwaysConsumeSystemBars, displayId, syncSeqId, dragResizing);
+                            alwaysConsumeSystemBars, displayId, syncSeqId, dragResizing,
+                            activityWindowInfo);
                     args.recycle();
                     break;
                 }
@@ -6504,7 +6560,8 @@
                             mLastReportedMergedConfiguration.getOverrideConfiguration());
 
                     performConfigurationChange(new MergedConfiguration(mPendingMergedConfiguration),
-                            false /* force */, INVALID_DISPLAY /* same display */);
+                            false /* force */, INVALID_DISPLAY /* same display */,
+                            mLastReportedActivityWindowInfo);
                 } break;
                 case MSG_CLEAR_ACCESSIBILITY_FOCUS_HOST: {
                     setAccessibilityFocus(null, null);
@@ -8933,10 +8990,19 @@
                     mTempInsets, mTempControls, mRelayoutBundle);
             mRelayoutRequested = true;
 
-            final int maybeSyncSeqId = mRelayoutBundle.getInt("seqid");
+            final int maybeSyncSeqId = mRelayoutBundle.getInt(
+                    IWindowSession.KEY_RELAYOUT_BUNDLE_SEQID);
             if (maybeSyncSeqId > 0) {
                 mSyncSeqId = maybeSyncSeqId;
             }
+            if (activityWindowInfoFlag() && mPendingActivityWindowInfo != null) {
+                final ActivityWindowInfo outInfo = mRelayoutBundle.getParcelable(
+                        IWindowSession.KEY_RELAYOUT_BUNDLE_ACTIVITY_WINDOW_INFO,
+                        ActivityWindowInfo.class);
+                if (outInfo != null) {
+                    mPendingActivityWindowInfo.set(outInfo);
+                }
+            }
             mWinFrameInScreen.set(mTmpFrames.frame);
             if (mTranslator != null) {
                 mTranslator.translateRectInScreenToAppWindow(mTmpFrames.frame);
@@ -9357,6 +9423,10 @@
                 + mLastReportedMergedConfiguration);
         writer.println(innerPrefix + "mLastConfigurationFromResources="
                 + mLastConfigurationFromResources);
+        if (mLastReportedActivityWindowInfo != null) {
+            writer.println(innerPrefix + "mLastReportedActivityWindowInfo="
+                    + mLastReportedActivityWindowInfo);
+        }
         writer.println(innerPrefix + "mIsAmbientMode="  + mIsAmbientMode);
         writer.println(innerPrefix + "mUnbufferedInputSource="
                 + Integer.toHexString(mUnbufferedInputSource));
@@ -9570,12 +9640,14 @@
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     private void dispatchResized(ClientWindowFrames frames, boolean reportDraw,
             MergedConfiguration mergedConfiguration, InsetsState insetsState, boolean forceLayout,
-            boolean alwaysConsumeSystemBars, int displayId, int syncSeqId, boolean dragResizing) {
+            boolean alwaysConsumeSystemBars, int displayId, int syncSeqId, boolean dragResizing,
+            @Nullable ActivityWindowInfo activityWindowInfo) {
         Message msg = mHandler.obtainMessage(reportDraw ? MSG_RESIZED_REPORT : MSG_RESIZED);
         SomeArgs args = SomeArgs.obtain();
         args.arg1 = frames;
         args.arg2 = mergedConfiguration;
         args.arg3 = insetsState;
+        args.arg4 = activityWindowInfo;
         args.argi1 = forceLayout ? 1 : 0;
         args.argi2 = alwaysConsumeSystemBars ? 1 : 0;
         args.argi3 = displayId;
@@ -11028,7 +11100,7 @@
         public void resized(ClientWindowFrames frames, boolean reportDraw,
                 MergedConfiguration mergedConfiguration, InsetsState insetsState,
                 boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId, int syncSeqId,
-                boolean dragResizing) {
+                boolean dragResizing, @Nullable ActivityWindowInfo activityWindowInfo) {
             final boolean isFromResizeItem = mIsFromResizeItem;
             mIsFromResizeItem = false;
             // Although this is a AIDL method, it will only be triggered in local process through
@@ -11047,7 +11119,8 @@
             if (isFromResizeItem && viewAncestor.mHandler.getLooper()
                     == ActivityThread.currentActivityThread().getLooper()) {
                 viewAncestor.handleResized(frames, reportDraw, mergedConfiguration, insetsState,
-                        forceLayout, alwaysConsumeSystemBars, displayId, syncSeqId, dragResizing);
+                        forceLayout, alwaysConsumeSystemBars, displayId, syncSeqId, dragResizing,
+                        activityWindowInfo);
                 return;
             }
             // The the parameters from WindowStateResizeItem are already copied.
@@ -11059,7 +11132,8 @@
                 mergedConfiguration = new MergedConfiguration(mergedConfiguration);
             }
             viewAncestor.dispatchResized(frames, reportDraw, mergedConfiguration, insetsState,
-                    forceLayout, alwaysConsumeSystemBars, displayId, syncSeqId, dragResizing);
+                    forceLayout, alwaysConsumeSystemBars, displayId, syncSeqId, dragResizing,
+                    activityWindowInfo);
         }
 
         @Override
@@ -12715,7 +12789,10 @@
     }
 
     private boolean shouldEnableDvrr() {
-        return sToolkitSetFrameRateReadOnlyFlagValue && mIsFrameRatePowerSavingsBalanced;
+        // uncomment this when we are ready for enabling dVRR
+        // return sToolkitSetFrameRateReadOnlyFlagValue && mIsFrameRatePowerSavingsBalanced;
+        return false;
+
     }
 
     private void checkIdleness() {
diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java
index 22d8ed9..7309080 100644
--- a/core/java/android/view/WindowlessWindowManager.java
+++ b/core/java/android/view/WindowlessWindowManager.java
@@ -638,7 +638,7 @@
                 mTmpConfig.setConfiguration(mConfiguration, mConfiguration);
                 s.mClient.resized(mTmpFrames, false /* reportDraw */, mTmpConfig, state,
                         false /* forceLayout */, false /* alwaysConsumeSystemBars */, s.mDisplayId,
-                        Integer.MAX_VALUE, false /* dragResizing */);
+                        Integer.MAX_VALUE, false /* dragResizing */, null /* activityWindowInfo */);
             } catch (RemoteException e) {
                 // Too bad
             }
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index 985f542..8efb201 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -3499,10 +3499,6 @@
             return false;
         }
         mServedView = mNextServedView;
-        if (initiationWithoutInputConnection() && mServedView.isHandwritingDelegate()) {
-            mServedView.getViewRootImpl().getHandwritingInitiator().onDelegateViewFocused(
-                    mServedView);
-        }
         if (mServedInputConnection != null) {
             mServedInputConnection.finishComposingTextFromImm();
         }
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 0373539..fbb5116 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -9733,7 +9733,7 @@
                         return KEY_EVENT_HANDLED;
                     }
                     if (hasFocus()) {
-                        clearFocus();
+                        clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
                         InputMethodManager imm = getInputMethodManager();
                         if (imm != null) {
                             imm.hideSoftInputFromView(this, 0);
@@ -13118,6 +13118,16 @@
             return superResult;
         }
 
+        // At this point, the event is not a long press, otherwise it would be handled above.
+        if (Flags.handwritingEndOfLineTap() && action == MotionEvent.ACTION_UP
+                && shouldStartHandwritingForEndOfLineTap(event)) {
+            InputMethodManager imm = getInputMethodManager();
+            if (imm != null) {
+                imm.startStylusHandwriting(this);
+                return true;
+            }
+        }
+
         final boolean touchIsFinished = (action == MotionEvent.ACTION_UP)
                 && (mEditor == null || !mEditor.mIgnoreActionUpEvent) && isFocused();
 
@@ -13167,6 +13177,46 @@
     }
 
     /**
+     * If handwriting is supported, the TextView is already focused and not empty, and the cursor is
+     * at the end of a line, a stylus tap after the end of the line will trigger handwriting.
+     */
+    private boolean shouldStartHandwritingForEndOfLineTap(MotionEvent actionUpEvent) {
+        if (!onCheckIsTextEditor()
+                || !isEnabled()
+                || !isAutoHandwritingEnabled()
+                || TextUtils.isEmpty(mText)
+                || didTouchFocusSelect()
+                || mLayout == null
+                || !actionUpEvent.isStylusPointer()) {
+            return false;
+        }
+        int cursorOffset = getSelectionStart();
+        if (cursorOffset < 0 || getSelectionEnd() != cursorOffset) {
+            return false;
+        }
+        int cursorLine = mLayout.getLineForOffset(cursorOffset);
+        int cursorLineEnd = mLayout.getLineEnd(cursorLine);
+        if (cursorLine != mLayout.getLineCount() - 1) {
+            cursorLineEnd--;
+        }
+        if (cursorLineEnd != cursorOffset) {
+            return false;
+        }
+        // Check that the stylus down point is within the same line as the cursor.
+        if (getLineAtCoordinate(actionUpEvent.getY()) != cursorLine) {
+            return false;
+        }
+        // Check that the stylus down point is after the end of the line.
+        float localX = convertToLocalHorizontalCoordinate(actionUpEvent.getX());
+        if (mLayout.getParagraphDirection(cursorLine) == Layout.DIR_RIGHT_TO_LEFT
+                ? localX >= mLayout.getLineLeft(cursorLine)
+                : localX <= mLayout.getLineRight(cursorLine)) {
+            return false;
+        }
+        return isStylusHandwritingAvailable();
+    }
+
+    /**
      * Returns true when need to show UIs, e.g. floating toolbar, etc, for finger based interaction.
      *
      * @return true if UIs need to show for finger interaciton. false if UIs are not necessary.
@@ -13565,6 +13615,15 @@
 
     /** @hide */
     @Override
+    public boolean shouldTrackHandwritingArea() {
+        // The handwriting initiator tracks all editable TextViews regardless of whether handwriting
+        // is supported, so that it can show an error message for unsupported editable TextViews.
+        return super.shouldTrackHandwritingArea()
+                || (Flags.handwritingUnsupportedMessage() && onCheckIsTextEditor());
+    }
+
+    /** @hide */
+    @Override
     public boolean isStylusHandwritingAvailable() {
         if (mTextOperationUser == null) {
             return super.isStylusHandwritingAvailable();
diff --git a/core/java/android/window/TaskFragmentOperation.java b/core/java/android/window/TaskFragmentOperation.java
index 7e77f15..43df4f9 100644
--- a/core/java/android/window/TaskFragmentOperation.java
+++ b/core/java/android/window/TaskFragmentOperation.java
@@ -112,10 +112,13 @@
     /**
      * Creates a decor surface in the parent Task of the TaskFragment. The created decor surface
      * will be provided in {@link TaskFragmentTransaction#TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED}
-     * event callback. The decor surface can be used to draw the divider between TaskFragments or
-     * other decorations.
+     * event callback. If a decor surface already exists in the parent Task, the current
+     * TaskFragment will become the new owner of the decor surface and the decor surface will be
+     * moved above the TaskFragment.
+     *
+     * The decor surface can be used to draw the divider between TaskFragments or other decorations.
      */
-    public static final int OP_TYPE_CREATE_TASK_FRAGMENT_DECOR_SURFACE = 14;
+    public static final int OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE = 14;
 
     /**
      * Removes the decor surface in the parent Task of the TaskFragment.
@@ -162,7 +165,7 @@
             OP_TYPE_SET_ISOLATED_NAVIGATION,
             OP_TYPE_REORDER_TO_BOTTOM_OF_TASK,
             OP_TYPE_REORDER_TO_TOP_OF_TASK,
-            OP_TYPE_CREATE_TASK_FRAGMENT_DECOR_SURFACE,
+            OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE,
             OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE,
             OP_TYPE_SET_DIM_ON_TASK,
             OP_TYPE_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH,
diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java
index 3685bba..5227724 100644
--- a/core/java/android/window/TransitionInfo.java
+++ b/core/java/android/window/TransitionInfo.java
@@ -542,6 +542,9 @@
         // independent either.
         if (change.getMode() == TRANSIT_CHANGE) return false;
 
+        // Always fold the activity embedding change into the parent change.
+        if (change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY)) return false;
+
         TransitionInfo.Change parentChg = info.getChange(change.getParent());
         while (parentChg != null) {
             // If the parent is a visibility change, it will include the results of all child
diff --git a/core/java/android/window/WindowTokenClient.java b/core/java/android/window/WindowTokenClient.java
index 7f5331b..4a3aba1 100644
--- a/core/java/android/window/WindowTokenClient.java
+++ b/core/java/android/window/WindowTokenClient.java
@@ -165,7 +165,8 @@
             Log.d(TAG, "Configuration not dispatch to IME because configuration is up"
                     + " to date. Current config=" + context.getResources().getConfiguration()
                     + ", reported config=" + currentConfig
-                    + ", updated config=" + newConfig);
+                    + ", updated config=" + newConfig
+                    + ", updated display ID=" + newDisplayId);
         }
         // Update display first. In case callers want to obtain display information(
         // ex: DisplayMetrics) in #onConfigurationChanged callback.
@@ -190,13 +191,18 @@
             if (mShouldDumpConfigForIme) {
                 if (!shouldReportConfigChange) {
                     Log.d(TAG, "Only apply configuration update to Resources because "
-                            + "shouldReportConfigChange is false.\n" + Debug.getCallers(5));
+                            + "shouldReportConfigChange is false. "
+                            + "context=" + context
+                            + ", config=" + context.getResources().getConfiguration()
+                            + ", display ID=" + context.getDisplayId() + "\n"
+                            + Debug.getCallers(5));
                 } else if (diff == 0) {
                     Log.d(TAG, "Configuration not dispatch to IME because configuration has no "
                             + " public difference with updated config. "
                             + " Current config=" + context.getResources().getConfiguration()
                             + ", reported config=" + currentConfig
-                            + ", updated config=" + newConfig);
+                            + ", updated config=" + newConfig
+                            + ", display ID=" + context.getDisplayId());
                 }
             }
         }
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index 14fb17c..65bf241 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -38,6 +38,17 @@
 }
 
 flag {
+  name: "skip_sleeping_when_switching_display"
+  namespace: "windowing_frontend"
+  description: "Reduce unnecessary visibility or lifecycle changes when changing fold state"
+  bug: "303241079"
+  is_fixed_read_only: true
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
+flag {
   name: "introduce_smoother_dimmer"
   namespace: "windowing_frontend"
   description: "Refactor dim to fix flickers"
diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java
index 2e80b7e..c70febb 100644
--- a/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java
+++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java
@@ -20,7 +20,6 @@
 import static android.view.accessibility.AccessibilityManager.ShortcutType;
 
 import static com.android.internal.accessibility.common.ShortcutConstants.ShortcutMenuMode;
-import static com.android.internal.accessibility.dialog.AccessibilityTargetHelper.createEnableDialogContentView;
 import static com.android.internal.accessibility.dialog.AccessibilityTargetHelper.getInstalledTargets;
 import static com.android.internal.accessibility.dialog.AccessibilityTargetHelper.getTargets;
 import static com.android.internal.accessibility.util.AccessibilityUtils.isUserSetupCompleted;
@@ -115,39 +114,22 @@
     private void onTargetChecked(AdapterView<?> parent, View view, int position, long id) {
         final AccessibilityTarget target = mTargets.get(position);
 
-        if (Flags.cleanupAccessibilityWarningDialog()) {
-            if (target instanceof AccessibilityServiceTarget serviceTarget) {
-                if (sendRestrictedDialogIntentIfNeeded(target)) {
-                    return;
-                }
-                final AccessibilityManager am = getSystemService(AccessibilityManager.class);
-                if (am.isAccessibilityServiceWarningRequired(
-                        serviceTarget.getAccessibilityServiceInfo())) {
-                    showPermissionDialogIfNeeded(this, (AccessibilityServiceTarget) target,
-                            position, mTargetAdapter);
-                    return;
-                }
+        if (target instanceof AccessibilityServiceTarget serviceTarget) {
+            if (sendRestrictedDialogIntentIfNeeded(target)) {
+                return;
             }
-            if (target instanceof AccessibilityActivityTarget activityTarget) {
-                if (!activityTarget.isShortcutEnabled()
-                        && sendRestrictedDialogIntentIfNeeded(activityTarget)) {
-                    return;
-                }
+            final AccessibilityManager am = getSystemService(AccessibilityManager.class);
+            if (am.isAccessibilityServiceWarningRequired(
+                    serviceTarget.getAccessibilityServiceInfo())) {
+                showPermissionDialogIfNeeded(this, (AccessibilityServiceTarget) target,
+                        position, mTargetAdapter);
+                return;
             }
-        } else {
-            if (!target.isShortcutEnabled()) {
-                if (target instanceof AccessibilityServiceTarget
-                        || target instanceof AccessibilityActivityTarget) {
-                    if (sendRestrictedDialogIntentIfNeeded(target)) {
-                        return;
-                    }
-                }
-
-                if (target instanceof AccessibilityServiceTarget) {
-                    showPermissionDialogIfNeeded(this, (AccessibilityServiceTarget) target,
-                            position, mTargetAdapter);
-                    return;
-                }
+        }
+        if (target instanceof AccessibilityActivityTarget activityTarget) {
+            if (!activityTarget.isShortcutEnabled()
+                    && sendRestrictedDialogIntentIfNeeded(activityTarget)) {
+                return;
             }
         }
 
@@ -178,37 +160,25 @@
             return;
         }
 
-        if (Flags.cleanupAccessibilityWarningDialog()) {
-            mPermissionDialog = AccessibilityServiceWarning
-                    .createAccessibilityServiceWarningDialog(context,
-                            serviceTarget.getAccessibilityServiceInfo(),
-                            v -> {
-                                serviceTarget.onCheckedChanged(true);
-                                targetAdapter.notifyDataSetChanged();
-                                mPermissionDialog.dismiss();
-                            }, v -> {
-                                serviceTarget.onCheckedChanged(false);
-                                mPermissionDialog.dismiss();
-                            },
-                            v -> {
-                                mTargets.remove(position);
-                                context.getPackageManager().getPackageInstaller().uninstall(
-                                        serviceTarget.getComponentName().getPackageName(), null);
-                                targetAdapter.notifyDataSetChanged();
-                                mPermissionDialog.dismiss();
-                            });
-            mPermissionDialog.setOnDismissListener(dialog -> mPermissionDialog = null);
-        } else {
-            mPermissionDialog = new AlertDialog.Builder(context)
-                    .setView(createEnableDialogContentView(context, serviceTarget,
-                            v -> {
-                                mPermissionDialog.dismiss();
-                                targetAdapter.notifyDataSetChanged();
-                            },
-                            v -> mPermissionDialog.dismiss()))
-                    .setOnDismissListener(dialog -> mPermissionDialog = null)
-                    .create();
-        }
+        mPermissionDialog = AccessibilityServiceWarning
+                .createAccessibilityServiceWarningDialog(context,
+                        serviceTarget.getAccessibilityServiceInfo(),
+                        v -> {
+                            serviceTarget.onCheckedChanged(true);
+                            targetAdapter.notifyDataSetChanged();
+                            mPermissionDialog.dismiss();
+                        }, v -> {
+                            serviceTarget.onCheckedChanged(false);
+                            mPermissionDialog.dismiss();
+                        },
+                        v -> {
+                            mTargets.remove(position);
+                            context.getPackageManager().getPackageInstaller().uninstall(
+                                    serviceTarget.getComponentName().getPackageName(), null);
+                            targetAdapter.notifyDataSetChanged();
+                            mPermissionDialog.dismiss();
+                        });
+        mPermissionDialog.setOnDismissListener(dialog -> mPermissionDialog = null);
         mPermissionDialog.show();
     }
 
diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java
index 3d3db47..0d82d63 100644
--- a/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java
+++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java
@@ -37,14 +37,8 @@
 import android.os.Build;
 import android.os.UserHandle;
 import android.provider.Settings;
-import android.text.BidiFormatter;
-import android.view.LayoutInflater;
-import android.view.View;
 import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.AccessibilityManager.ShortcutType;
-import android.widget.Button;
-import android.widget.ImageView;
-import android.widget.TextView;
 
 import com.android.internal.R;
 import com.android.internal.accessibility.common.ShortcutConstants.AccessibilityFragmentType;
@@ -52,7 +46,6 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.Locale;
 
 /**
  * Collection of utilities for accessibility target.
@@ -298,50 +291,6 @@
     }
 
     /**
-     * @deprecated Use {@link AccessibilityServiceWarning}.
-     */
-    @Deprecated
-    static View createEnableDialogContentView(Context context,
-            AccessibilityServiceTarget target, View.OnClickListener allowListener,
-            View.OnClickListener denyListener) {
-        final LayoutInflater inflater = (LayoutInflater) context.getSystemService(
-                Context.LAYOUT_INFLATER_SERVICE);
-
-        final View content = inflater.inflate(
-                R.layout.accessibility_enable_service_warning, /* root= */ null);
-
-        final ImageView dialogIcon = content.findViewById(
-                R.id.accessibility_permissionDialog_icon);
-        dialogIcon.setImageDrawable(target.getIcon());
-
-        final TextView dialogTitle = content.findViewById(
-                R.id.accessibility_permissionDialog_title);
-        dialogTitle.setText(context.getString(R.string.accessibility_enable_service_title,
-                getServiceName(context, target.getLabel())));
-
-        final Button allowButton = content.findViewById(
-                R.id.accessibility_permission_enable_allow_button);
-        final Button denyButton = content.findViewById(
-                R.id.accessibility_permission_enable_deny_button);
-        allowButton.setOnClickListener((view) -> {
-            target.onCheckedChanged(/* isChecked= */ true);
-            allowListener.onClick(view);
-        });
-        denyButton.setOnClickListener((view) -> {
-            target.onCheckedChanged(/* isChecked= */ false);
-            denyListener.onClick(view);
-        });
-
-        return content;
-    }
-
-    // Gets the service name and bidi wrap it to protect from bidi side effects.
-    private static CharSequence getServiceName(Context context, CharSequence label) {
-        final Locale locale = context.getResources().getConfiguration().getLocales().get(0);
-        return BidiFormatter.getInstance(locale).unicodeWrap(label);
-    }
-
-    /**
      * Determines if the{@link AccessibilityTarget} is allowed.
      */
     public static boolean isAccessibilityTargetAllowed(Context context, String packageName,
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index 29669d3..ab456a8 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -96,7 +96,6 @@
 import android.provider.OpenableColumns;
 import android.provider.Settings;
 import android.service.chooser.ChooserTarget;
-import android.service.chooser.Flags;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.util.HashedStringCache;
@@ -1801,54 +1800,6 @@
         return getIntent().getBooleanExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, true);
     }
 
-    private void showTargetDetails(TargetInfo targetInfo) {
-        if (targetInfo == null) return;
-
-        ArrayList<DisplayResolveInfo> targetList;
-        ChooserTargetActionsDialogFragment fragment = new ChooserTargetActionsDialogFragment();
-        Bundle bundle = new Bundle();
-
-        if (targetInfo instanceof SelectableTargetInfo) {
-            SelectableTargetInfo selectableTargetInfo = (SelectableTargetInfo) targetInfo;
-            if (selectableTargetInfo.getDisplayResolveInfo() == null
-                    || selectableTargetInfo.getChooserTarget() == null) {
-                Log.e(TAG, "displayResolveInfo or chooserTarget in selectableTargetInfo are null");
-                return;
-            }
-            targetList = new ArrayList<>();
-            targetList.add(selectableTargetInfo.getDisplayResolveInfo());
-            bundle.putString(ChooserTargetActionsDialogFragment.SHORTCUT_ID_KEY,
-                    selectableTargetInfo.getChooserTarget().getIntentExtras().getString(
-                            Intent.EXTRA_SHORTCUT_ID));
-            bundle.putBoolean(ChooserTargetActionsDialogFragment.IS_SHORTCUT_PINNED_KEY,
-                    selectableTargetInfo.isPinned());
-            bundle.putParcelable(ChooserTargetActionsDialogFragment.INTENT_FILTER_KEY,
-                    getTargetIntentFilter());
-            if (selectableTargetInfo.getDisplayLabel() != null) {
-                bundle.putString(ChooserTargetActionsDialogFragment.SHORTCUT_TITLE_KEY,
-                        selectableTargetInfo.getDisplayLabel().toString());
-            }
-        } else if (targetInfo instanceof MultiDisplayResolveInfo) {
-            // For multiple targets, include info on all targets
-            MultiDisplayResolveInfo mti = (MultiDisplayResolveInfo) targetInfo;
-            targetList = mti.getTargets();
-        } else {
-            targetList = new ArrayList<DisplayResolveInfo>();
-            targetList.add((DisplayResolveInfo) targetInfo);
-        }
-        // Adding userHandle from ResolveInfo allows the app icon in Dialog Box to be
-        // resolved correctly.
-        bundle.putParcelable(ChooserTargetActionsDialogFragment.USER_HANDLE_KEY,
-                getResolveInfoUserHandle(
-                        targetInfo.getResolveInfo(),
-                        mChooserMultiProfilePagerAdapter.getCurrentUserHandle()));
-        bundle.putParcelableArrayList(ChooserTargetActionsDialogFragment.TARGET_INFOS_KEY,
-                targetList);
-        fragment.setArguments(bundle);
-
-        fragment.show(getFragmentManager(), TARGET_DETAILS_FRAGMENT_TAG);
-    }
-
     private void modifyTargetIntent(Intent in) {
         if (isSendAction(in)) {
             in.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT |
@@ -2544,10 +2495,7 @@
 
         @Override
         public boolean isComponentPinned(ComponentName name) {
-            if (Flags.legacyChooserPinningRemoval()) {
-                return false;
-            }
-            return mPinnedSharedPrefs.getBoolean(name.flattenToString(), false);
+            return false;
         }
 
         @Override
@@ -3135,34 +3083,10 @@
             if (isClickable) {
                 itemView.setOnClickListener(v -> startSelected(mListPosition,
                         false/* always */, true/* filterd */));
-
-                itemView.setOnLongClickListener(v -> {
-                    final TargetInfo ti = mChooserMultiProfilePagerAdapter.getActiveListAdapter()
-                            .targetInfoForPosition(mListPosition, /* filtered */ true);
-
-                    // This should always be the case for ItemViewHolder, check for validity
-                    if (ti instanceof DisplayResolveInfo && shouldShowTargetDetails(ti)) {
-                        showTargetDetails((DisplayResolveInfo) ti);
-                    }
-                    return true;
-                });
             }
         }
     }
 
-    private boolean shouldShowTargetDetails(TargetInfo ti) {
-        if (Flags.legacyChooserPinningRemoval()) {
-            // Never show the long press menu if we've removed pinning.
-            return false;
-        }
-        ComponentName nearbyShare = getNearbySharingComponent();
-        //  Suppress target details for nearby share to hide pin/unpin action
-        boolean isNearbyShare = nearbyShare != null && nearbyShare.equals(
-                ti.getResolvedComponentName()) && shouldNearbyShareBeFirstInRankedRow();
-        return ti instanceof SelectableTargetInfo
-                || (ti instanceof DisplayResolveInfo && !isNearbyShare);
-    }
-
     /**
      * Add a footer to the list, to support scrolling behavior below the navbar.
      */
@@ -3517,16 +3441,6 @@
                     }
                 });
 
-                // Show menu for both direct share and app share targets after long click.
-                v.setOnLongClickListener(v1 -> {
-                    TargetInfo ti = mChooserListAdapter.targetInfoForPosition(
-                            holder.getItemIndex(column), true);
-                    if (shouldShowTargetDetails(ti)) {
-                        showTargetDetails(ti);
-                    }
-                    return true;
-                });
-
                 holder.addView(i, v);
 
                 // Force Direct Share to be 2 lines and auto-wrap to second line via hoz scroll =
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index 78f06b6..84715aa 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -217,6 +217,12 @@
     public static final String EXTRA_IS_AUDIO_CAPTURE_DEVICE = "is_audio_capture_device";
 
     /**
+     * Boolean extra to indicate if Resolver Sheet needs to be started in single user mode.
+     */
+    protected static final String EXTRA_RESTRICT_TO_SINGLE_USER =
+            "com.android.internal.app.ResolverActivity.EXTRA_RESTRICT_TO_SINGLE_USER";
+
+    /**
      * Integer extra to indicate which profile should be automatically selected.
      * <p>Can only be used if there is a work profile.
      * <p>Possible values can be either {@link #PROFILE_PERSONAL} or {@link #PROFILE_WORK}.
@@ -750,8 +756,10 @@
     }
 
     protected UserHandle getPersonalProfileUserHandle() {
-        if (privateSpaceEnabled() && isLaunchedAsPrivateProfile()){
-            return mPrivateProfileUserHandle;
+        // When launched in single user mode, only personal tab is populated, so we use
+        // tabOwnerUserHandleForLaunch as personal tab's user handle.
+        if (privateSpaceEnabled() && isLaunchedInSingleUserMode()) {
+            return getTabOwnerUserHandleForLaunch();
         }
         return mPersonalProfileUserHandle;
     }
@@ -822,11 +830,11 @@
         // If we are in work or private profile's process, return WorkProfile/PrivateProfile user
         // as owner, otherwise we always return PersonalProfile user as owner
         if (UserHandle.of(UserHandle.myUserId()).equals(getWorkProfileUserHandle())) {
-            return getWorkProfileUserHandle();
+            return mWorkProfileUserHandle;
         } else if (privateSpaceEnabled() && isLaunchedAsPrivateProfile()) {
-            return getPrivateProfileUserHandle();
+            return mPrivateProfileUserHandle;
         }
-        return getPersonalProfileUserHandle();
+        return mPersonalProfileUserHandle;
     }
 
     private boolean hasWorkProfile() {
@@ -847,8 +855,18 @@
                 && (UserHandle.myUserId() == getPrivateProfileUserHandle().getIdentifier());
     }
 
+    protected final boolean isLaunchedInSingleUserMode() {
+        // When launched from Private Profile, return true
+        if (isLaunchedAsPrivateProfile()) {
+            return true;
+        }
+        return getIntent()
+                .getBooleanExtra(EXTRA_RESTRICT_TO_SINGLE_USER, /* defaultValue = */ false);
+    }
+
     protected boolean shouldShowTabs() {
-        if (privateSpaceEnabled() && isLaunchedAsPrivateProfile()) {
+        // No Tabs are shown when launched in single user mode.
+        if (privateSpaceEnabled() && isLaunchedInSingleUserMode()) {
             return false;
         }
         return hasWorkProfile() && ENABLE_TABBED_VIEW;
diff --git a/core/java/com/android/internal/jank/Cuj.java b/core/java/com/android/internal/jank/Cuj.java
index 3662d69..d2a533c 100644
--- a/core/java/com/android/internal/jank/Cuj.java
+++ b/core/java/com/android/internal/jank/Cuj.java
@@ -124,10 +124,13 @@
     public static final int CUJ_BACK_PANEL_ARROW = 88;
     public static final int CUJ_LAUNCHER_CLOSE_ALL_APPS_BACK = 89;
     public static final int CUJ_LAUNCHER_SEARCH_QSB_WEB_SEARCH = 90;
+    public static final int CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE = 91;
+    public static final int CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR = 92;
+    public static final int CUJ_LAUNCHER_SAVE_APP_PAIR = 93;
 
     // When adding a CUJ, update this and make sure to also update CUJ_TO_STATSD_INTERACTION_TYPE.
     @VisibleForTesting
-    static final int LAST_CUJ = CUJ_LAUNCHER_SEARCH_QSB_WEB_SEARCH;
+    static final int LAST_CUJ = CUJ_LAUNCHER_SAVE_APP_PAIR;
 
     /** @hide */
     @IntDef({
@@ -212,6 +215,9 @@
             CUJ_BACK_PANEL_ARROW,
             CUJ_LAUNCHER_CLOSE_ALL_APPS_BACK,
             CUJ_LAUNCHER_SEARCH_QSB_WEB_SEARCH,
+            CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE,
+            CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR,
+            CUJ_LAUNCHER_SAVE_APP_PAIR
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface CujType {
@@ -306,6 +312,9 @@
         CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_BACK_PANEL_ARROW] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__BACK_PANEL_ARROW;
         CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_CLOSE_ALL_APPS_BACK] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_CLOSE_ALL_APPS_BACK;
         CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_SEARCH_QSB_WEB_SEARCH] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_SEARCH_QSB_WEB_SEARCH;
+        CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE;
+        CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR;
+        CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_SAVE_APP_PAIR] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_SAVE_APP_PAIR;
     }
 
     private Cuj() {
@@ -484,6 +493,12 @@
                 return "LAUNCHER_CLOSE_ALL_APPS_BACK";
             case CUJ_LAUNCHER_SEARCH_QSB_WEB_SEARCH:
                 return "LAUNCHER_SEARCH_QSB_WEB_SEARCH";
+            case CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE:
+                return "LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE";
+            case CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR:
+                return "LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR";
+            case CUJ_LAUNCHER_SAVE_APP_PAIR:
+                return "LAUNCHER_SAVE_APP_PAIR";
         }
         return "UNKNOWN";
     }
diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java
index 0ec8b74..a288fb7 100644
--- a/core/java/com/android/internal/jank/InteractionJankMonitor.java
+++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java
@@ -165,6 +165,9 @@
     @Deprecated public static final int CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY = Cuj.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY;
     @Deprecated public static final int CUJ_PREDICTIVE_BACK_CROSS_TASK = Cuj.CUJ_PREDICTIVE_BACK_CROSS_TASK;
     @Deprecated public static final int CUJ_PREDICTIVE_BACK_HOME = Cuj.CUJ_PREDICTIVE_BACK_HOME;
+    @Deprecated public static final int CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE = Cuj.CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE;
+    @Deprecated public static final int CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR = Cuj.CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR;
+    @Deprecated public static final int CUJ_LAUNCHER_SAVE_APP_PAIR = Cuj.CUJ_LAUNCHER_SAVE_APP_PAIR;
 
     private static class InstanceHolder {
         public static final InteractionJankMonitor INSTANCE =
diff --git a/core/java/com/android/internal/net/ConnectivityBlobStore.java b/core/java/com/android/internal/net/ConnectivityBlobStore.java
new file mode 100644
index 0000000..1b18485
--- /dev/null
+++ b/core/java/com/android/internal/net/ConnectivityBlobStore.java
@@ -0,0 +1,173 @@
+/*
+ * 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.internal.net;
+
+import android.annotation.NonNull;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.Binder;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Database for storing blobs with a key of name strings.
+ * @hide
+ */
+public class ConnectivityBlobStore {
+    private static final String TAG = ConnectivityBlobStore.class.getSimpleName();
+    private static final String TABLENAME = "blob_table";
+    private static final String ROOT_DIR = "/data/misc/connectivityblobdb/";
+
+    private static final String CREATE_TABLE =
+            "CREATE TABLE IF NOT EXISTS " + TABLENAME + " ("
+            + "owner INTEGER,"
+            + "name BLOB,"
+            + "blob BLOB,"
+            + "UNIQUE(owner, name));";
+
+    private final SQLiteDatabase mDb;
+
+    /**
+     * Construct a ConnectivityBlobStore object.
+     *
+     * @param dbName the filename of the database to create/access.
+     */
+    public ConnectivityBlobStore(String dbName) {
+        this(new File(ROOT_DIR + dbName));
+    }
+
+    @VisibleForTesting
+    public ConnectivityBlobStore(File file) {
+        final SQLiteDatabase.OpenParams params = new SQLiteDatabase.OpenParams.Builder()
+                .addOpenFlags(SQLiteDatabase.CREATE_IF_NECESSARY)
+                .addOpenFlags(SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING)
+                .build();
+        mDb = SQLiteDatabase.openDatabase(file, params);
+        mDb.execSQL(CREATE_TABLE);
+    }
+
+    /**
+     * Stores the blob under the name in the database. Existing blobs by the same name will be
+     * replaced.
+     *
+     * @param name The name of the blob
+     * @param blob The blob.
+     * @return true if the blob was successfully added. False otherwise.
+     * @hide
+     */
+    public boolean put(@NonNull String name, @NonNull byte[] blob) {
+        final int ownerUid = Binder.getCallingUid();
+        final ContentValues values = new ContentValues();
+        values.put("owner", ownerUid);
+        values.put("name", name);
+        values.put("blob", blob);
+
+        // No need for try-catch since it is done within db.replace
+        // nullColumnHack is for the case where values may be empty since SQL does not allow
+        // inserting a completely empty row. Since values is never empty, set this to null.
+        final long res = mDb.replace(TABLENAME, null /* nullColumnHack */, values);
+        return res > 0;
+    }
+
+    /**
+     * Retrieves a blob by the name from the database.
+     *
+     * @param name Name of the blob to retrieve.
+     * @return The unstructured blob, that is the blob that was stored using
+     *         {@link com.android.internal.net.ConnectivityBlobStore#put}.
+     *         Returns null if no blob was found.
+     * @hide
+     */
+    public byte[] get(@NonNull String name) {
+        final int ownerUid = Binder.getCallingUid();
+        try (Cursor cursor = mDb.query(TABLENAME,
+                new String[] {"blob"} /* columns */,
+                "owner=? AND name=?" /* selection */,
+                new String[] {Integer.toString(ownerUid), name} /* selectionArgs */,
+                null /* groupBy */,
+                null /* having */,
+                null /* orderBy */)) {
+            if (cursor.moveToFirst()) {
+                return cursor.getBlob(0);
+            }
+        } catch (SQLException e) {
+            Log.e(TAG, "Error in getting " + name + ": " + e);
+        }
+
+        return null;
+    }
+
+    /**
+     * Removes a blob by the name from the database.
+     *
+     * @param name Name of the blob to be removed.
+     * @return True if a blob was removed. False if no such name was found.
+     * @hide
+     */
+    public boolean remove(@NonNull String name) {
+        final int ownerUid = Binder.getCallingUid();
+        try {
+            final int res = mDb.delete(TABLENAME,
+                    "owner=? AND name=?" /* whereClause */,
+                    new String[] {Integer.toString(ownerUid), name} /* whereArgs */);
+            return res > 0;
+        } catch (SQLException e) {
+            Log.e(TAG, "Error in removing " + name + ": " + e);
+            return false;
+        }
+    }
+
+    /**
+     * Lists the name suffixes stored in the database matching the given prefix, sorted in
+     * ascending order.
+     *
+     * @param prefix String of prefix to list from the stored names.
+     * @return An array of strings representing the name suffixes stored in the database
+     *         matching the given prefix, sorted in ascending order.
+     *         The return value may be empty but never null.
+     * @hide
+     */
+    public String[] list(@NonNull String prefix) {
+        final int ownerUid = Binder.getCallingUid();
+        final List<String> names = new ArrayList<String>();
+        try (Cursor cursor = mDb.query(TABLENAME,
+                new String[] {"name"} /* columns */,
+                "owner=? AND name LIKE ?" /* selection */,
+                new String[] {Integer.toString(ownerUid), prefix + "%"} /* selectionArgs */,
+                null /* groupBy */,
+                null /* having */,
+                "name ASC" /* orderBy */)) {
+            if (cursor.moveToFirst()) {
+                do {
+                    final String name = cursor.getString(0);
+                    names.add(name.substring(prefix.length()));
+                } while (cursor.moveToNext());
+            }
+        } catch (SQLException e) {
+            Log.e(TAG, "Error in listing " + prefix + ": " + e);
+        }
+
+        return names.toArray(new String[names.size()]);
+    }
+}
diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl
index a22232a..f5b1a47 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -388,9 +388,9 @@
      */
     void showMediaOutputSwitcher(String packageName);
 
-    /** Enters desktop mode.
+    /** Enters desktop mode from the current focused app.
     *
     * @param displayId the id of the current display.
     */
-    void enterDesktop(int displayId);
+    void moveFocusedTaskToDesktop(int displayId);
 }
diff --git a/core/java/com/android/internal/view/BaseIWindow.java b/core/java/com/android/internal/view/BaseIWindow.java
index 600058e..e33704b 100644
--- a/core/java/com/android/internal/view/BaseIWindow.java
+++ b/core/java/com/android/internal/view/BaseIWindow.java
@@ -33,6 +33,7 @@
 import android.view.ScrollCaptureResponse;
 import android.view.WindowInsets.Type.InsetsType;
 import android.view.inputmethod.ImeTracker;
+import android.window.ActivityWindowInfo;
 import android.window.ClientWindowFrames;
 
 import com.android.internal.os.IResultReceiver;
@@ -53,7 +54,8 @@
     @Override
     public void resized(ClientWindowFrames frames, boolean reportDraw,
             MergedConfiguration mergedConfiguration, InsetsState insetsState, boolean forceLayout,
-            boolean alwaysConsumeSystemBars, int displayId, int seqId, boolean dragResizing) {
+            boolean alwaysConsumeSystemBars, int displayId, int seqId, boolean dragResizing,
+            @Nullable ActivityWindowInfo activityWindowInfo) {
         if (reportDraw) {
             try {
                 mSession.finishDrawing(this, null /* postDrawTransaction */, seqId);
diff --git a/core/java/com/android/internal/widget/ConversationAvatarData.java b/core/java/com/android/internal/widget/ConversationAvatarData.java
index e04772f..bc9cd40 100644
--- a/core/java/com/android/internal/widget/ConversationAvatarData.java
+++ b/core/java/com/android/internal/widget/ConversationAvatarData.java
@@ -21,9 +21,9 @@
 /**
  * @hide
  */
-interface ConversationAvatarData {
+public interface ConversationAvatarData {
     final class OneToOneConversationAvatarData implements ConversationAvatarData {
-        final Drawable mDrawable;
+        public final Drawable mDrawable;
 
         OneToOneConversationAvatarData(Drawable drawable) {
             mDrawable = drawable;
diff --git a/core/java/com/android/internal/widget/ConversationHeaderData.java b/core/java/com/android/internal/widget/ConversationHeaderData.java
index 0953b39..ea92155 100644
--- a/core/java/com/android/internal/widget/ConversationHeaderData.java
+++ b/core/java/com/android/internal/widget/ConversationHeaderData.java
@@ -21,7 +21,7 @@
 /**
  * @hide
  */
-final class ConversationHeaderData {
+public final class ConversationHeaderData {
     private final CharSequence mConversationText;
 
     private final ConversationAvatarData mConversationAvatarData;
@@ -38,7 +38,7 @@
     }
 
     @Nullable
-    ConversationAvatarData getConversationAvatar() {
+    public ConversationAvatarData getConversationAvatar() {
         return mConversationAvatarData;
     }
 }
diff --git a/core/java/com/android/internal/widget/ConversationLayout.java b/core/java/com/android/internal/widget/ConversationLayout.java
index 6d5a96a..b6066ba 100644
--- a/core/java/com/android/internal/widget/ConversationLayout.java
+++ b/core/java/com/android/internal/widget/ConversationLayout.java
@@ -162,6 +162,8 @@
     private TouchDelegateComposite mTouchDelegate = new TouchDelegateComposite(this);
     private ArrayList<MessagingLinearLayout.MessagingChild> mToRecycle = new ArrayList<>();
     private boolean mPrecomputedTextEnabled = false;
+    @Nullable
+    private ConversationHeaderData mConversationHeaderData;
 
     public ConversationLayout(@NonNull Context context) {
         super(context);
@@ -651,6 +653,7 @@
 
     private void setConversationAvatarAndNameFromData(
             ConversationHeaderData conversationHeaderData) {
+        mConversationHeaderData = conversationHeaderData;
         final OneToOneConversationAvatarData oneToOneConversationDrawable;
         final GroupConversationAvatarData groupConversationAvatarData;
         final ConversationAvatarData conversationAvatar =
@@ -804,7 +807,10 @@
         bottomBackground.setLayoutParams(layoutParams);
     }
 
-    private void bindFacePileWithDrawable(ImageView bottomBackground, ImageView bottomView,
+    /**
+     * Binds group avatar drawables to face pile.
+     */
+    public void bindFacePileWithDrawable(ImageView bottomBackground, ImageView bottomView,
             ImageView topView, GroupConversationAvatarData groupConversationAvatarData) {
         applyNotificationBackgroundColor(bottomBackground);
         bottomView.setImageDrawable(groupConversationAvatarData.mLastIcon);
@@ -1573,6 +1579,11 @@
         return mConversationIcon;
     }
 
+    @Nullable
+    public ConversationHeaderData getConversationHeaderData() {
+        return mConversationHeaderData;
+    }
+
     private static class TouchDelegateComposite extends TouchDelegate {
         private final ArrayList<TouchDelegate> mDelegates = new ArrayList<>();
 
diff --git a/core/java/com/android/internal/widget/EmphasizedNotificationButton.java b/core/java/com/android/internal/widget/EmphasizedNotificationButton.java
index 01b4569..c07e624 100644
--- a/core/java/com/android/internal/widget/EmphasizedNotificationButton.java
+++ b/core/java/com/android/internal/widget/EmphasizedNotificationButton.java
@@ -278,11 +278,6 @@
         // be ready to glue. This can only happen if the button is initialized and displayed and
         // *then* someone calls glueIcon or glueLabel.
 
-        if (mIconToGlue == null) {
-            Log.w(TAG, "glueIconAndLabelIfNeeded: label glued without icon; doing nothing");
-            return;
-        }
-
         if (mLabelToGlue == null) {
             Log.w(TAG, "glueIconAndLabelIfNeeded: icon glued without label; doing nothing");
             return;
@@ -318,6 +313,14 @@
     private static final String POP_DIRECTIONAL_ISOLATE = "\u2069";
 
     private void glueIconAndLabel(int layoutDirection) {
+        if (mIconToGlue == null) {
+            if (DEBUG_NEW_ACTION_LAYOUT) {
+                Log.d(TAG, "glueIconAndLabel: null icon, setting text to label");
+            }
+            setText(mLabelToGlue);
+            return;
+        }
+
         final boolean rtlLayout = layoutDirection == LAYOUT_DIRECTION_RTL;
 
         if (DEBUG_NEW_ACTION_LAYOUT) {
diff --git a/core/jni/OWNERS b/core/jni/OWNERS
index 3aca751..2a4f062 100644
--- a/core/jni/OWNERS
+++ b/core/jni/OWNERS
@@ -27,6 +27,7 @@
 # WindowManager
 per-file android_graphics_BLASTBufferQueue.cpp = file:/services/core/java/com/android/server/wm/OWNERS
 per-file android_view_Surface* = file:/services/core/java/com/android/server/wm/OWNERS
+per-file android_view_WindowManagerGlobal.cpp = file:/services/core/java/com/android/server/wm/OWNERS
 per-file android_window_* = file:/services/core/java/com/android/server/wm/OWNERS
 
 # Resources
diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp
index 5223798..d48cdc4 100644
--- a/core/jni/android_media_AudioSystem.cpp
+++ b/core/jni/android_media_AudioSystem.cpp
@@ -161,6 +161,7 @@
     jfieldID mMixType;
     jfieldID mCallbackFlags;
     jfieldID mToken;
+    jfieldID mVirtualDeviceId;
 } gAudioMixFields;
 
 static jclass gAudioFormatClass;
@@ -2312,7 +2313,7 @@
     jstring deviceAddress = env->NewStringUTF(nAudioMix.mDeviceAddress.c_str());
     *jAudioMix = env->NewObject(gAudioMixClass, gAudioMixCstor, jAudioMixingRule, jAudioFormat,
                                 nAudioMix.mRouteFlags, nAudioMix.mCbFlags, nAudioMix.mDeviceType,
-                                deviceAddress, jBinderToken);
+                                deviceAddress, jBinderToken, nAudioMix.mVirtualDeviceId);
     return AUDIO_JAVA_SUCCESS;
 }
 
@@ -2347,6 +2348,7 @@
             aiBinder(AIBinder_fromJavaBinder(env, jToken), &AIBinder_decStrong);
     nAudioMix->mToken = AIBinder_toPlatformBinder(aiBinder.get());
 
+    nAudioMix->mVirtualDeviceId = env->GetIntField(jAudioMix, gAudioMixFields.mVirtualDeviceId);
     jint status = convertAudioMixingRuleToNative(env, jRule, &(nAudioMix->mCriteria));
 
     env->DeleteLocalRef(jRule);
@@ -3676,7 +3678,7 @@
         gAudioMixCstor =
                 GetMethodIDOrDie(env, audioMixClass, "<init>",
                                  "(Landroid/media/audiopolicy/AudioMixingRule;Landroid/"
-                                 "media/AudioFormat;IIILjava/lang/String;Landroid/os/IBinder;)V");
+                                 "media/AudioFormat;IIILjava/lang/String;Landroid/os/IBinder;I)V");
     }
     gAudioMixFields.mRule = GetFieldIDOrDie(env, audioMixClass, "mRule",
                                                 "Landroid/media/audiopolicy/AudioMixingRule;");
@@ -3689,6 +3691,7 @@
     gAudioMixFields.mMixType = GetFieldIDOrDie(env, audioMixClass, "mMixType", "I");
     gAudioMixFields.mCallbackFlags = GetFieldIDOrDie(env, audioMixClass, "mCallbackFlags", "I");
     gAudioMixFields.mToken = GetFieldIDOrDie(env, audioMixClass, "mToken", "Landroid/os/IBinder;");
+    gAudioMixFields.mVirtualDeviceId = GetFieldIDOrDie(env, audioMixClass, "mVirtualDeviceId", "I");
 
     jclass audioFormatClass = FindClassOrDie(env, "android/media/AudioFormat");
     gAudioFormatClass = MakeGlobalRefOrDie(env, audioFormatClass);
diff --git a/core/jni/android_os_Parcel.cpp b/core/jni/android_os_Parcel.cpp
index 3539476..584ebaa 100644
--- a/core/jni/android_os_Parcel.cpp
+++ b/core/jni/android_os_Parcel.cpp
@@ -661,6 +661,35 @@
     return;
 }
 
+static jboolean android_os_Parcel_hasBinders(JNIEnv* env, jclass clazz, jlong nativePtr) {
+    Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
+    if (parcel != NULL) {
+        bool result;
+        status_t err = parcel->hasBinders(&result);
+        if (err != NO_ERROR) {
+            signalExceptionForError(env, clazz, err);
+            return JNI_FALSE;
+        }
+        return result ? JNI_TRUE : JNI_FALSE;
+    }
+    return JNI_FALSE;
+}
+
+static jboolean android_os_Parcel_hasBindersInRange(JNIEnv* env, jclass clazz, jlong nativePtr,
+                                                    jint offset, jint length) {
+    Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
+    if (parcel != NULL) {
+        bool result;
+        status_t err = parcel->hasBindersInRange(offset, length, &result);
+        if (err != NO_ERROR) {
+            signalExceptionForError(env, clazz, err);
+            return JNI_FALSE;
+        }
+        return result ? JNI_TRUE : JNI_FALSE;
+    }
+    return JNI_FALSE;
+}
+
 static jboolean android_os_Parcel_hasFileDescriptors(jlong nativePtr)
 {
     jboolean ret = JNI_FALSE;
@@ -806,7 +835,7 @@
 }
 
 // ----------------------------------------------------------------------------
-
+// clang-format off
 static const JNINativeMethod gParcelMethods[] = {
     // @CriticalNative
     {"nativeMarkSensitive",       "(J)V", (void*)android_os_Parcel_markSensitive},
@@ -886,6 +915,9 @@
     // @CriticalNative
     {"nativeHasFileDescriptors",  "(J)Z", (void*)android_os_Parcel_hasFileDescriptors},
     {"nativeHasFileDescriptorsInRange",  "(JII)Z", (void*)android_os_Parcel_hasFileDescriptorsInRange},
+
+    {"nativeHasBinders",  "(J)Z", (void*)android_os_Parcel_hasBinders},
+    {"nativeHasBindersInRange",  "(JII)Z", (void*)android_os_Parcel_hasBindersInRange},
     {"nativeWriteInterfaceToken", "(JLjava/lang/String;)V", (void*)android_os_Parcel_writeInterfaceToken},
     {"nativeEnforceInterface",    "(JLjava/lang/String;)V", (void*)android_os_Parcel_enforceInterface},
 
@@ -900,6 +932,7 @@
     // @CriticalNative
     {"nativeReplaceCallingWorkSourceUid", "(JI)Z", (void*)android_os_Parcel_replaceCallingWorkSourceUid},
 };
+// clang-format on
 
 const char* const kParcelPathName = "android/os/Parcel";
 
diff --git a/core/jni/android_os_Trace.cpp b/core/jni/android_os_Trace.cpp
index b579daf..4387a4c 100644
--- a/core/jni/android_os_Trace.cpp
+++ b/core/jni/android_os_Trace.cpp
@@ -124,8 +124,8 @@
     });
 }
 
-static jlong android_os_Trace_nativeGetEnabledTags(JNIEnv* env) {
-    return tracing_perfetto::getEnabledCategories();
+static jboolean android_os_Trace_nativeIsTagEnabled(jlong tag) {
+    return tracing_perfetto::isTagEnabled(tag);
 }
 
 static void android_os_Trace_nativeRegisterWithPerfetto(JNIEnv* env) {
@@ -157,7 +157,7 @@
         {"nativeRegisterWithPerfetto", "()V", (void*)android_os_Trace_nativeRegisterWithPerfetto},
 
         // ----------- @CriticalNative  ----------------
-        {"nativeGetEnabledTags", "()J", (void*)android_os_Trace_nativeGetEnabledTags},
+        {"nativeIsTagEnabled", "(J)Z", (void*)android_os_Trace_nativeIsTagEnabled},
 };
 
 int register_android_os_Trace(JNIEnv* env) {
diff --git a/core/jni/android_util_Process.cpp b/core/jni/android_util_Process.cpp
index d2e58bb..982189e 100644
--- a/core/jni/android_util_Process.cpp
+++ b/core/jni/android_util_Process.cpp
@@ -1137,6 +1137,41 @@
     }
 }
 
+void android_os_Process_sendSignalThrows(JNIEnv* env, jobject clazz, jint pid, jint sig) {
+    if (pid <= 0) {
+        jniThrowExceptionFmt(env, "java/lang/IllegalArgumentException", "Invalid argument: pid(%d)",
+                             pid);
+        return;
+    }
+    int ret = kill(pid, sig);
+    if (ret < 0) {
+        if (errno == ESRCH) {
+            jniThrowExceptionFmt(env, "java/util/NoSuchElementException",
+                                 "Process with pid %d not found", pid);
+        } else {
+            signalExceptionForError(env, errno, pid);
+        }
+    }
+}
+
+void android_os_Process_sendTgSignalThrows(JNIEnv* env, jobject clazz, jint tgid, jint tid,
+                                           jint sig) {
+    if (tgid <= 0 || tid <= 0) {
+        jniThrowExceptionFmt(env, "java/lang/IllegalArgumentException",
+                             "Invalid argument: tgid(%d), tid(%d)", tid, tgid);
+        return;
+    }
+    int ret = tgkill(tgid, tid, sig);
+    if (ret < 0) {
+        if (errno == ESRCH) {
+            jniThrowExceptionFmt(env, "java/util/NoSuchElementException",
+                                 "Process with tid %d and tgid %d not found", tid, tgid);
+        } else {
+            signalExceptionForError(env, errno, tid);
+        }
+    }
+}
+
 static jlong android_os_Process_getElapsedCpuTime(JNIEnv* env, jobject clazz)
 {
     struct timespec ts;
@@ -1357,6 +1392,8 @@
         {"setGid", "(I)I", (void*)android_os_Process_setGid},
         {"sendSignal", "(II)V", (void*)android_os_Process_sendSignal},
         {"sendSignalQuiet", "(II)V", (void*)android_os_Process_sendSignalQuiet},
+        {"sendSignalThrows", "(II)V", (void*)android_os_Process_sendSignalThrows},
+        {"sendTgSignalThrows", "(III)V", (void*)android_os_Process_sendTgSignalThrows},
         {"setProcessFrozen", "(IIZ)V", (void*)android_os_Process_setProcessFrozen},
         {"getFreeMemory", "()J", (void*)android_os_Process_getFreeMemory},
         {"getTotalMemory", "()J", (void*)android_os_Process_getTotalMemory},
diff --git a/core/jni/android_view_WindowManagerGlobal.cpp b/core/jni/android_view_WindowManagerGlobal.cpp
index b03ac88..abc621d 100644
--- a/core/jni/android_view_WindowManagerGlobal.cpp
+++ b/core/jni/android_view_WindowManagerGlobal.cpp
@@ -48,7 +48,7 @@
             surfaceControlObj(env,
                               android_view_SurfaceControl_getJavaSurfaceControl(env,
                                                                                 surfaceControl));
-    jobject clientTokenObj = javaObjectForIBinder(env, clientToken);
+    ScopedLocalRef<jobject> clientTokenObj(env, javaObjectForIBinder(env, clientToken));
     ScopedLocalRef<jobject> clientInputTransferTokenObj(
             env,
             android_window_InputTransferToken_getJavaInputTransferToken(env,
@@ -57,7 +57,7 @@
             inputChannelObj(env,
                             env->CallStaticObjectMethod(gWindowManagerGlobal.clazz,
                                                         gWindowManagerGlobal.createInputChannel,
-                                                        clientTokenObj,
+                                                        clientTokenObj.get(),
                                                         hostInputTransferTokenObj.get(),
                                                         surfaceControlObj.get(),
                                                         clientInputTransferTokenObj.get()));
@@ -68,9 +68,9 @@
 void removeInputChannel(const sp<IBinder>& clientToken) {
     JNIEnv* env = AndroidRuntime::getJNIEnv();
 
-    jobject clientTokenObj(javaObjectForIBinder(env, clientToken));
+    ScopedLocalRef<jobject> clientTokenObj(env, javaObjectForIBinder(env, clientToken));
     env->CallStaticObjectMethod(gWindowManagerGlobal.clazz, gWindowManagerGlobal.removeInputChannel,
-                                clientTokenObj);
+                                clientTokenObj.get());
 }
 
 int register_android_view_WindowManagerGlobal(JNIEnv* env) {
diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto
index 763d9ce..6b0c2d2 100644
--- a/core/proto/android/providers/settings/secure.proto
+++ b/core/proto/android/providers/settings/secure.proto
@@ -143,9 +143,11 @@
         optional SettingProto gesture_setup_complete = 9 [ (android.privacy).dest = DEST_AUTOMATIC ];
         optional SettingProto touch_gesture_enabled = 10 [ (android.privacy).dest = DEST_AUTOMATIC ];
         optional SettingProto long_press_home_enabled = 11 [ (android.privacy).dest = DEST_AUTOMATIC ];
-        optional SettingProto search_press_hold_nav_handle_enabled = 12 [ (android.privacy).dest = DEST_AUTOMATIC ];
-        optional SettingProto search_long_press_home_enabled = 13 [ (android.privacy).dest = DEST_AUTOMATIC ];
+        // Deprecated - use search_all_entrypoints_enabled instead
+        optional SettingProto search_press_hold_nav_handle_enabled = 12 [ (android.privacy).dest = DEST_AUTOMATIC, deprecated = true  ];
+        optional SettingProto search_long_press_home_enabled = 13 [ (android.privacy).dest = DEST_AUTOMATIC, deprecated = true  ];
         optional SettingProto visual_query_accessibility_detection_enabled = 14 [ (android.privacy).dest = DEST_AUTOMATIC ];
+        optional SettingProto search_all_entrypoints_enabled = 15 [ (android.privacy).dest = DEST_AUTOMATIC ];
     }
     optional Assist assist = 7;
 
diff --git a/core/res/res/drawable/activity_embedding_divider_handle.xml b/core/res/res/drawable/activity_embedding_divider_handle.xml
new file mode 100644
index 0000000..d9f363c
--- /dev/null
+++ b/core/res/res/drawable/activity_embedding_divider_handle.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 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.
+  -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_pressed="true"
+        android:drawable="@drawable/activity_embedding_divider_handle_pressed" />
+    <item android:drawable="@drawable/activity_embedding_divider_handle_default" />
+</selector>
\ No newline at end of file
diff --git a/core/res/res/drawable/activity_embedding_divider_handle_default.xml b/core/res/res/drawable/activity_embedding_divider_handle_default.xml
new file mode 100644
index 0000000..565f671
--- /dev/null
+++ b/core/res/res/drawable/activity_embedding_divider_handle_default.xml
@@ -0,0 +1,23 @@
+<!--
+  Copyright 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.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <corners android:radius="@dimen/activity_embedding_divider_handle_radius" />
+    <size
+        android:width="@dimen/activity_embedding_divider_handle_width"
+        android:height="@dimen/activity_embedding_divider_handle_height" />
+    <solid android:color="@color/activity_embedding_divider_color" />
+</shape>
\ No newline at end of file
diff --git a/core/res/res/drawable/activity_embedding_divider_handle_pressed.xml b/core/res/res/drawable/activity_embedding_divider_handle_pressed.xml
new file mode 100644
index 0000000..e5cca239
--- /dev/null
+++ b/core/res/res/drawable/activity_embedding_divider_handle_pressed.xml
@@ -0,0 +1,23 @@
+<!--
+  Copyright 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.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <corners android:radius="@dimen/activity_embedding_divider_handle_radius_pressed" />
+    <size
+        android:width="@dimen/activity_embedding_divider_handle_width_pressed"
+        android:height="@dimen/activity_embedding_divider_handle_height_pressed" />
+    <solid android:color="@color/activity_embedding_divider_color_pressed" />
+</shape>
\ No newline at end of file
diff --git a/core/res/res/drawable/autofill_dataset_picker_background.xml b/core/res/res/drawable/autofill_dataset_picker_background.xml
index d574970..6c4ef11 100644
--- a/core/res/res/drawable/autofill_dataset_picker_background.xml
+++ b/core/res/res/drawable/autofill_dataset_picker_background.xml
@@ -16,7 +16,7 @@
 
 <inset xmlns:android="http://schemas.android.com/apk/res/android">
     <shape android:shape="rectangle">
-        <corners android:radius="@dimen/config_bottomDialogCornerRadius" />
+        <corners android:radius="@dimen/config_buttonCornerRadius" />
         <solid android:color="?attr/colorBackground" />
     </shape>
 </inset>
diff --git a/core/res/res/layout/transient_notification_with_icon.xml b/core/res/res/layout/transient_notification_with_icon.xml
index 0dfb3ad..04518b2 100644
--- a/core/res/res/layout/transient_notification_with_icon.xml
+++ b/core/res/res/layout/transient_notification_with_icon.xml
@@ -22,7 +22,7 @@
     android:orientation="horizontal"
     android:gravity="center_vertical"
     android:maxWidth="@dimen/toast_width"
-    android:background="?android:attr/colorBackground"
+    android:background="@android:drawable/toast_frame"
     android:elevation="@dimen/toast_elevation"
     android:layout_marginEnd="16dp"
     android:layout_marginStart="16dp"
@@ -31,8 +31,11 @@
 
     <ImageView
         android:id="@android:id/icon"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content" />
+        android:layout_width="24dp"
+        android:layout_height="24dp"
+        android:layout_marginTop="10dp"
+        android:layout_marginBottom="10dp"
+        android:layout_marginEnd="10dp" />
 
     <TextView
         android:id="@android:id/message"
diff --git a/core/res/res/values/colors.xml b/core/res/res/values/colors.xml
index 417c6df..e671919 100644
--- a/core/res/res/values/colors.xml
+++ b/core/res/res/values/colors.xml
@@ -593,6 +593,10 @@
     <color name="accessibility_magnification_thumbnail_container_background_color">#99000000</color>
     <color name="accessibility_magnification_thumbnail_container_stroke_color">#FFFFFF</color>
 
+    <!-- Activity Embedding divider -->
+    <color name="activity_embedding_divider_color">#8e918f</color>
+    <color name="activity_embedding_divider_color_pressed">#e3e3e3</color>
+
     <!-- Lily Language Picker language item view colors -->
     <color name="language_picker_item_text_color">#202124</color>
     <color name="language_picker_item_text_color_secondary">#5F6368</color>
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index efba709..89ac81e 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -6419,10 +6419,8 @@
     <!-- Default value for Settings.ASSIST_TOUCH_GESTURE_ENABLED -->
     <bool name="config_assistTouchGestureEnabledDefault">true</bool>
 
-    <!-- Default value for Settings.SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED -->
-    <bool name="config_searchPressHoldNavHandleEnabledDefault">true</bool>
-    <!-- Default value for Settings.ASSIST_LONG_PRESS_HOME_ENABLED for search overlay -->
-    <bool name="config_searchLongPressHomeEnabledDefault">true</bool>
+    <!-- Default value for Settings.SEARCH_ALL_ENTRYPOINTS_ENABLED -->
+    <bool name="config_searchAllEntrypointsEnabledDefault">true</bool>
 
     <!-- The maximum byte size of the information contained in the bundle of
     HotwordDetectedResult. -->
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index 291a593..4aa741d 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -1028,6 +1028,16 @@
     <dimen name="popup_enter_animation_from_y_delta">20dp</dimen>
     <dimen name="popup_exit_animation_to_y_delta">-10dp</dimen>
 
+    <!-- Dimensions for the activity embedding divider. -->
+    <dimen name="activity_embedding_divider_handle_width">4dp</dimen>
+    <dimen name="activity_embedding_divider_handle_height">48dp</dimen>
+    <dimen name="activity_embedding_divider_handle_radius">2dp</dimen>
+    <dimen name="activity_embedding_divider_handle_width_pressed">12dp</dimen>
+    <dimen name="activity_embedding_divider_handle_height_pressed">53dp</dimen>
+    <dimen name="activity_embedding_divider_handle_radius_pressed">6dp</dimen>
+    <dimen name="activity_embedding_divider_touch_target_width">24dp</dimen>
+    <dimen name="activity_embedding_divider_touch_target_height">64dp</dimen>
+
     <!-- Default handwriting bounds offsets for editors. -->
     <dimen name="handwriting_bounds_offset_left">10dp</dimen>
     <dimen name="handwriting_bounds_offset_top">40dp</dimen>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index f915f03..a3dba48 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -231,8 +231,10 @@
     <string name="NetworkPreferenceSwitchSummary">Try changing preferred network. Tap to change.</string>
     <!-- Displayed to tell the user that emergency calls might not be available. -->
     <string name="EmergencyCallWarningTitle">Emergency calling unavailable</string>
-    <!-- Displayed to tell the user that emergency calls might not be available. -->
-    <string name="EmergencyCallWarningSummary">Can\u2019t make emergency calls over Wi\u2011Fi</string>
+    <!-- Displayed to tell the user that emergency calls might not be available; this is shown to
+         the user when only WiFi calling is available and the carrier does not support emergency
+         calls over WiFi calling. -->
+    <string name="EmergencyCallWarningSummary">Emergency calls require a mobile network</string>
 
     <!-- Telephony notification channel name for a channel containing network alert notifications. -->
     <string name="notification_channel_network_alert">Alerts</string>
@@ -3247,6 +3249,12 @@
     <!-- Title for EditText context menu [CHAR LIMIT=20] -->
     <string name="editTextMenuTitle">Text actions</string>
 
+    <!-- Error shown when a user uses a stylus to try handwriting on a text field which doesn't support stylus handwriting. [CHAR LIMIT=TOAST] -->
+    <string name="error_handwriting_unsupported">Handwriting is not supported in this field</string>
+
+    <!-- Error shown when a user uses a stylus to try handwriting on a password text field which doesn't support stylus handwriting. [CHAR LIMIT=TOAST] -->
+    <string name="error_handwriting_unsupported_password">Handwriting is not supported in password fields</string>
+
     <!-- Content description of the back button for accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
     <string name="input_method_nav_back_button_desc">Back</string>
     <!-- Content description of the switch input method button for accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 668a88c..2e029b2 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3122,6 +3122,8 @@
   <!-- TextView -->
   <java-symbol type="bool" name="config_textShareSupported" />
   <java-symbol type="string" name="failed_to_copy_to_clipboard" />
+  <java-symbol type="string" name="error_handwriting_unsupported" />
+  <java-symbol type="string" name="error_handwriting_unsupported_password" />
 
   <java-symbol type="id" name="notification_material_reply_container" />
   <java-symbol type="id" name="notification_material_reply_text_1" />
@@ -5017,8 +5019,7 @@
   <java-symbol type="bool" name="config_assistLongPressHomeEnabledDefault" />
   <java-symbol type="bool" name="config_assistTouchGestureEnabledDefault" />
 
-  <java-symbol type="bool" name="config_searchPressHoldNavHandleEnabledDefault" />
-  <java-symbol type="bool" name="config_searchLongPressHomeEnabledDefault" />
+  <java-symbol type="bool" name="config_searchAllEntrypointsEnabledDefault" />
 
   <java-symbol type="integer" name="config_hotwordDetectedResultMaxBundleSize" />
 
@@ -5336,6 +5337,11 @@
 
   <java-symbol type="raw" name="default_ringtone_vibration_effect" />
 
+  <!-- For activity embedding divider -->
+  <java-symbol type="drawable" name="activity_embedding_divider_handle" />
+  <java-symbol type="dimen" name="activity_embedding_divider_touch_target_width" />
+  <java-symbol type="dimen" name="activity_embedding_divider_touch_target_height" />
+
   <!-- Whether we order unlocking and waking -->
   <java-symbol type="bool" name="config_orderUnlockAndWake" />
 
diff --git a/core/res/res/xml/sms_short_codes.xml b/core/res/res/xml/sms_short_codes.xml
index 7d740ef..c8625b9 100644
--- a/core/res/res/xml/sms_short_codes.xml
+++ b/core/res/res/xml/sms_short_codes.xml
@@ -42,8 +42,8 @@
     <!-- Argentina: 5 digits, known short codes listed -->
     <shortcode country="ar" pattern="\\d{5}" free="11711|28291|44077|78887" />
 
-    <!-- Armenia: 3-4 digits, emergency numbers 10[123] -->
-    <shortcode country="am" pattern="\\d{3,4}" premium="11[2456]1|3024" free="10[123]" />
+    <!-- Armenia: 3-5 digits, emergency numbers 10[123] -->
+    <shortcode country="am" pattern="\\d{3,5}" premium="11[2456]1|3024" free="10[123]|71522|71512|71502" />
 
     <!-- Austria: 10 digits, premium prefix 09xx, plus EU -->
     <shortcode country="at" pattern="11\\d{4}" premium="09.*" free="116\\d{3}" />
@@ -111,7 +111,7 @@
     <shortcode country="do" pattern="\\d{1,6}" free="912892" />
 
     <!-- Ecuador: 1-6 digits (standard system default, not country specific) -->
-    <shortcode country="ec" pattern="\\d{1,6}" free="466453" />
+    <shortcode country="ec" pattern="\\d{1,6}" free="466453|18512" />
 
     <!-- Estonia: short codes 3-5 digits starting with 1, plus premium 7 digit numbers starting with 90, plus EU.
          http://www.tja.ee/public/documents/Elektrooniline_side/Oigusaktid/ENG/Estonian_Numbering_Plan_annex_06_09_2010.mht -->
@@ -137,11 +137,11 @@
          visual voicemail code for EE: 887 -->
     <shortcode country="gb" pattern="\\d{4,6}" premium="[5-8]\\d{4}" free="116\\d{3}|2020|35890|61002|61202|887|83669|34664|40406|60174|7726|37726|88555|9017|9018" />
 
-    <!-- Georgia: 4 digits, known premium codes listed -->
-    <shortcode country="ge" pattern="\\d{4}" premium="801[234]|888[239]" />
+    <!-- Georgia: 1-5 digits, known premium codes listed -->
+    <shortcode country="ge" pattern="\\d{1,5}" premium="801[234]|888[239]" free="95201|95202|95203" />
 
     <!-- Ghana: 4 digits, known premium codes listed -->
-    <shortcode country="gh" pattern="\\d{4}" free="5041" />
+    <shortcode country="gh" pattern="\\d{4}" free="5041|3777" />
 
     <!-- Greece: 5 digits (54xxx, 19yxx, x=0-9, y=0-5): http://www.cmtelecom.com/premium-sms/greece -->
     <shortcode country="gr" pattern="\\d{5}" premium="54\\d{3}|19[0-5]\\d{2}" free="116\\d{3}|12115" />
@@ -210,6 +210,9 @@
     <!-- Macedonia: 1-6 digits (not confirmed), known premium codes listed -->
     <shortcode country="mk" pattern="\\d{1,6}" free="129005|122" />
 
+    <!-- Mongolia : 1-6 digits (standard system default, not country specific) -->
+    <shortcode country="mn" pattern="\\d{1,6}" free="44444|45678|445566" />
+
     <!-- Malawi: 1-5 digits (standard system default, not country specific) -->
     <shortcode country="mw" pattern="\\d{1,5}" free="4276" />
 
@@ -247,7 +250,7 @@
     <shortcode country="ph" pattern="\\d{1,5}" free="2147|5495|5496" />
 
     <!-- Pakistan -->
-    <shortcode country="pk" pattern="\\d{1,5}" free="2057|9092" />
+    <shortcode country="pk" pattern="\\d{1,6}" free="2057|9092|909203" />
 
     <!-- Palestine: 5 digits, known premium codes listed -->
     <shortcode country="ps" pattern="\\d{1,5}" free="37477|6681" />
@@ -291,7 +294,7 @@
     <shortcode country="sk" premium="\\d{4}" free="116\\d{3}|8000" />
 
     <!-- Senegal(SN): 1-5 digits (standard system default, not country specific) -->
-    <shortcode country="sn" pattern="\\d{1,5}" free="21215" />
+    <shortcode country="sn" pattern="\\d{1,5}" free="21215|21098" />
 
     <!-- El Salvador(SV): 1-5 digits (standard system default, not country specific) -->
     <shortcode country="sv" pattern="\\d{4,6}" free="466453" />
@@ -321,14 +324,17 @@
          visual voicemail code for T-Mobile: 122 -->
     <shortcode country="us" pattern="\\d{5,6}" premium="20433|21(?:344|472)|22715|23(?:333|847)|24(?:15|28)0|25209|27(?:449|606|663)|28498|305(?:00|83)|32(?:340|941)|33(?:166|786|849)|34746|35(?:182|564)|37975|38(?:135|146|254)|41(?:366|463)|42335|43(?:355|500)|44(?:578|711|811)|45814|46(?:157|173|327)|46666|47553|48(?:221|277|669)|50(?:844|920)|51(?:062|368)|52944|54(?:723|892)|55928|56483|57370|59(?:182|187|252|342)|60339|61(?:266|982)|62478|64(?:219|898)|65(?:108|500)|69(?:208|388)|70877|71851|72(?:078|087|465)|73(?:288|588|882|909|997)|74(?:034|332|815)|76426|79213|81946|83177|84(?:103|685)|85797|86(?:234|236|666)|89616|90(?:715|842|938)|91(?:362|958)|94719|95297|96(?:040|666|835|969)|97(?:142|294|688)|99(?:689|796|807)" standard="44567|244444" free="122|87902|21696|24614|28003|30356|33669|40196|41064|41270|43753|44034|46645|52413|56139|57969|61785|66975|75136|76227|81398|83952|85140|86566|86799|95737|96684|99245|611611|96831" />
 
+    <!--Uruguay : 1-5 digits (standard system default, not country specific) -->
+    <shortcode country="uy" pattern="\\d{1,5}" free="55002" />
+
     <!-- Vietnam: 1-5 digits (standard system default, not country specific) -->
-    <shortcode country="vn" pattern="\\d{1,5}" free="5001|9055" />
+    <shortcode country="vn" pattern="\\d{1,5}" free="5001|9055|8079" />
 
     <!-- Mayotte (French Territory): 1-5 digits (not confirmed) -->
     <shortcode country="yt" pattern="\\d{1,5}" free="38600,36300,36303,959" />
 
     <!-- South Africa -->
-    <shortcode country="za" pattern="\\d{1,5}" free="44136|30791|36056" />
+    <shortcode country="za" pattern="\\d{1,5}" free="44136|30791|36056|33009" />
 
     <!-- Zimbabwe -->
     <shortcode country="zw" pattern="\\d{1,5}" free="33679" />
diff --git a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java
index 9907397..2ce7a7d 100644
--- a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java
+++ b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java
@@ -88,6 +88,7 @@
     private InsetsState mInsetsState;
     private ClientWindowFrames mFrames;
     private MergedConfiguration mMergedConfiguration;
+    private ActivityWindowInfo mActivityWindowInfo;
 
     @Before
     public void setup() {
@@ -99,6 +100,7 @@
         mInsetsState = new InsetsState();
         mFrames = new ClientWindowFrames();
         mMergedConfiguration = new MergedConfiguration(mGlobalConfig, mConfiguration);
+        mActivityWindowInfo = new ActivityWindowInfo();
 
         doReturn(mActivity).when(mHandler).getActivity(mActivityToken);
         doReturn(mActivitiesToBeDestroyed).when(mHandler).getActivitiesToBeDestroyed();
@@ -107,7 +109,7 @@
     @Test
     public void testActivityConfigurationChangeItem_getContextToUpdate() {
         final ActivityConfigurationChangeItem item = ActivityConfigurationChangeItem
-                .obtain(mActivityToken, mConfiguration, new ActivityWindowInfo());
+                .obtain(mActivityToken, mConfiguration, mActivityWindowInfo);
         final Context context = item.getContextToUpdate(mHandler);
 
         assertEquals(mActivity, context);
@@ -118,7 +120,7 @@
         final ActivityRelaunchItem item = ActivityRelaunchItem
                 .obtain(mActivityToken, null /* pendingResults */, null  /* pendingNewIntents */,
                         0 /* configChange */, mMergedConfiguration, false /* preserveWindow */,
-                        new ActivityWindowInfo());
+                        mActivityWindowInfo);
         final Context context = item.getContextToUpdate(mHandler);
 
         assertEquals(mActivity, context);
@@ -177,7 +179,7 @@
     @Test
     public void testMoveToDisplayItem_getContextToUpdate() {
         final MoveToDisplayItem item = MoveToDisplayItem
-                .obtain(mActivityToken, DEFAULT_DISPLAY, mConfiguration, new ActivityWindowInfo());
+                .obtain(mActivityToken, DEFAULT_DISPLAY, mConfiguration, mActivityWindowInfo);
         final Context context = item.getContextToUpdate(mHandler);
 
         assertEquals(mActivity, context);
@@ -218,13 +220,13 @@
         final WindowStateResizeItem item = WindowStateResizeItem.obtain(mWindow, mFrames,
                 true /* reportDraw */, mMergedConfiguration, mInsetsState, true /* forceLayout */,
                 true /* alwaysConsumeSystemBars */, 123 /* displayId */, 321 /* syncSeqId */,
-                true /* dragResizing */);
+                true /* dragResizing */, mActivityToken, mActivityWindowInfo);
         item.execute(mHandler, mPendingActions);
 
         verify(mWindow).resized(mFrames,
                 true /* reportDraw */, mMergedConfiguration, mInsetsState, true /* forceLayout */,
                 true /* alwaysConsumeSystemBars */, 123 /* displayId */, 321 /* syncSeqId */,
-                true /* dragResizing */);
+                true /* dragResizing */, mActivityWindowInfo);
     }
 
     @Test
@@ -232,7 +234,7 @@
         final WindowStateResizeItem item = WindowStateResizeItem.obtain(mWindow, mFrames,
                 true /* reportDraw */, mMergedConfiguration, mInsetsState, true /* forceLayout */,
                 true /* alwaysConsumeSystemBars */, 123 /* displayId */, 321 /* syncSeqId */,
-                true /* dragResizing */);
+                true /* dragResizing */, mActivityToken, mActivityWindowInfo);
         final Context context = item.getContextToUpdate(mHandler);
 
         assertEquals(ActivityThread.currentApplication(), context);
diff --git a/core/tests/coretests/src/android/os/BundleTest.java b/core/tests/coretests/src/android/os/BundleTest.java
index 93c2e0e..40e79ad 100644
--- a/core/tests/coretests/src/android/os/BundleTest.java
+++ b/core/tests/coretests/src/android/os/BundleTest.java
@@ -24,6 +24,7 @@
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 
+import android.platform.test.annotations.DisabledOnRavenwood;
 import android.platform.test.annotations.IgnoreUnderRavenwood;
 import android.platform.test.annotations.Presubmit;
 import android.platform.test.ravenwood.RavenwoodRule;
@@ -445,6 +446,42 @@
         assertThat(bundle.size()).isEqualTo(0);
     }
 
+    @Test
+    @DisabledOnRavenwood(blockedBy = Parcel.class)
+    public void parcelledBundleWithBinder_shouldReturnHasBindersTrue() throws Exception {
+        Bundle bundle = new Bundle();
+        bundle.putParcelable("test", new CustomParcelable(13, "Tiramisu"));
+        bundle.putBinder("test_binder",
+                new IBinderWorkSourceNestedService.Stub() {
+
+                    public int[] nestedCallWithWorkSourceToSet(int uidToBlame) {
+                        return new int[0];
+                    }
+
+                    public int[] nestedCall() {
+                        return new int[0];
+                    }
+                });
+        Bundle bundle2 = new Bundle(getParcelledBundle(bundle));
+        assertEquals(bundle2.hasBinders(), Bundle.STATUS_BINDERS_PRESENT);
+
+        bundle2.putParcelable("test2", new CustomParcelable(13, "Tiramisu"));
+        assertEquals(bundle2.hasBinders(), Bundle.STATUS_BINDERS_UNKNOWN);
+    }
+
+    @Test
+    @DisabledOnRavenwood(blockedBy = Parcel.class)
+    public void parcelledBundleWithoutBinder_shouldReturnHasBindersFalse() throws Exception {
+        Bundle bundle = new Bundle();
+        bundle.putParcelable("test", new CustomParcelable(13, "Tiramisu"));
+        Bundle bundle2 = new Bundle(getParcelledBundle(bundle));
+        //Should fail to load with framework classloader.
+        assertEquals(bundle2.hasBinders(), Bundle.STATUS_BINDERS_NOT_PRESENT);
+
+        bundle2.putParcelable("test2", new CustomParcelable(13, "Tiramisu"));
+        assertEquals(bundle2.hasBinders(), Bundle.STATUS_BINDERS_UNKNOWN);
+    }
+
     private Bundle getMalformedBundle() {
         Parcel p = Parcel.obtain();
         p.writeInt(BaseBundle.BUNDLE_MAGIC);
@@ -520,6 +557,7 @@
             public CustomParcelable createFromParcel(Parcel in) {
                 return new CustomParcelable(in);
             }
+
             @Override
             public CustomParcelable[] newArray(int size) {
                 return new CustomParcelable[size];
diff --git a/core/tests/coretests/src/android/os/ParcelTest.java b/core/tests/coretests/src/android/os/ParcelTest.java
index 26f6d69..442394e3 100644
--- a/core/tests/coretests/src/android/os/ParcelTest.java
+++ b/core/tests/coretests/src/android/os/ParcelTest.java
@@ -347,4 +347,30 @@
         p.recycle();
         Binder.setIsDirectlyHandlingTransactionOverride(false);
     }
+
+    @Test
+    @IgnoreUnderRavenwood(blockedBy = Parcel.class)
+    public void testHasBinders_AfterWritingBinderToParcel() {
+        Binder binder = new Binder();
+        Parcel pA = Parcel.obtain();
+        int iA = pA.dataPosition();
+        pA.writeInt(13);
+        assertFalse(pA.hasBinders());
+        pA.writeStrongBinder(binder);
+        assertTrue(pA.hasBinders());
+    }
+
+
+    @Test
+    @IgnoreUnderRavenwood(blockedBy = Parcel.class)
+    public void testHasBindersInRange_AfterWritingBinderToParcel() {
+        Binder binder = new Binder();
+        Parcel pA = Parcel.obtain();
+        pA.writeInt(13);
+
+        int binderStartPos = pA.dataPosition();
+        pA.writeStrongBinder(binder);
+        int binderEndPos = pA.dataPosition();
+        assertTrue(pA.hasBinders(binderStartPos, binderEndPos - binderStartPos));
+    }
 }
diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java
index 652011b..41b67ce 100644
--- a/core/tests/coretests/src/android/view/ViewRootImplTest.java
+++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java
@@ -81,6 +81,7 @@
 import org.junit.AfterClass;
 import org.junit.Before;
 import org.junit.BeforeClass;
+import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -462,6 +463,7 @@
      */
     @UiThreadTest
     @Test
+    @Ignore("Can be enabled only after b/330596920 is ready")
     @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
     public void votePreferredFrameRate_getDefaultValues() {
         ViewRootImpl viewRootImpl = new ViewRootImpl(sContext,
@@ -478,6 +480,7 @@
      * Also, mIsFrameRateBoosting should be true when the visibility becomes visible
      */
     @Test
+    @Ignore("Can be enabled only after b/330596920 is ready")
     @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY,
             FLAG_TOOLKIT_FRAME_RATE_BY_SIZE_READ_ONLY})
     public void votePreferredFrameRate_voteFrameRateCategory_visibility_bySize() {
@@ -511,6 +514,7 @@
      * <7%: FRAME_RATE_CATEGORY_LOW
      */
     @Test
+    @Ignore("Can be enabled only after b/330596920 is ready")
     @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY,
             FLAG_TOOLKIT_FRAME_RATE_BY_SIZE_READ_ONLY})
     public void votePreferredFrameRate_voteFrameRateCategory_smallSize_bySize() {
@@ -539,6 +543,7 @@
      * >=7% : FRAME_RATE_CATEGORY_NORMAL
      */
     @Test
+    @Ignore("Can be enabled only after b/330596920 is ready")
     @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY,
             FLAG_TOOLKIT_FRAME_RATE_BY_SIZE_READ_ONLY})
     public void votePreferredFrameRate_voteFrameRateCategory_normalSize_bySize() {
@@ -571,6 +576,7 @@
      * Also, mIsFrameRateBoosting should be true when the visibility becomes visible
      */
     @Test
+    @Ignore("Can be enabled only after b/330596920 is ready")
     @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
     public void votePreferredFrameRate_voteFrameRateCategory_visibility_defaultHigh() {
         View view = new View(sContext);
@@ -603,6 +609,7 @@
      * <7%: FRAME_RATE_CATEGORY_NORMAL
      */
     @Test
+    @Ignore("Can be enabled only after b/330596920 is ready")
     @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
     public void votePreferredFrameRate_voteFrameRateCategory_smallSize_defaultHigh() {
         View view = new View(sContext);
@@ -630,6 +637,7 @@
      * >=7% : FRAME_RATE_CATEGORY_HIGH
      */
     @Test
+    @Ignore("Can be enabled only after b/330596920 is ready")
     @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
     public void votePreferredFrameRate_voteFrameRateCategory_normalSize_defaultHigh() {
         View view = new View(sContext);
@@ -659,6 +667,7 @@
      * It should take the max value among all of the voted categories per frame.
      */
     @Test
+    @Ignore("Can be enabled only after b/330596920 is ready")
     @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
     public void votePreferredFrameRate_voteFrameRateCategory_aggregate() {
         View view = new View(sContext);
@@ -704,6 +713,7 @@
      * prioritize 60Hz..
      */
     @Test
+    @Ignore("Can be enabled only after b/330596920 is ready")
     @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
     public void votePreferredFrameRate_voteFrameRate_aggregate() {
         View view = new View(sContext);
@@ -762,6 +772,7 @@
      * submit your preferred choice to the ViewRootImpl.
      */
     @Test
+    @Ignore("Can be enabled only after b/330596920 is ready")
     @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
     public void votePreferredFrameRate_voteFrameRate_category() {
         View view = new View(sContext);
@@ -801,6 +812,7 @@
      * Also, we shouldn't call setFrameRate.
      */
     @Test
+    @Ignore("Can be enabled only after b/330596920 is ready")
     @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY, FLAG_VIEW_VELOCITY_API})
     public void votePreferredFrameRate_voteFrameRateCategory_velocityToHigh() {
         View view = new View(sContext);
@@ -832,6 +844,7 @@
      * We should boost the frame rate if the value of mInsetsAnimationRunning is true.
      */
     @Test
+    @Ignore("Can be enabled only after b/330596920 is ready")
     @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
     public void votePreferredFrameRate_insetsAnimation() {
         View view = new View(sContext);
@@ -868,6 +881,7 @@
      * Test FrameRateBoostOnTouchEnabled API
      */
     @Test
+    @Ignore("Can be enabled only after b/330596920 is ready")
     @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
     public void votePreferredFrameRate_frameRateBoostOnTouch() {
         View view = new View(sContext);
@@ -900,6 +914,7 @@
      * mPreferredFrameRate should be set to 0.
      */
     @Test
+    @Ignore("Can be enabled only after b/330596920 is ready")
     @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
     public void votePreferredFrameRate_voteFrameRateTimeOut() throws InterruptedException {
         final long delay = 200L;
@@ -937,6 +952,7 @@
      * A View should either vote a frame rate or a frame rate category instead of both.
      */
     @Test
+    @Ignore("Can be enabled only after b/330596920 is ready")
     @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
     public void votePreferredFrameRate_voteFrameRateOnly() {
         View view = new View(sContext);
@@ -979,6 +995,7 @@
      * - otherwise, use the previous category value.
      */
     @Test
+    @Ignore("Can be enabled only after b/330596920 is ready")
     @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
     public void votePreferredFrameRate_infrequentLayer_defaultHigh() throws InterruptedException {
         final long delay = 200L;
@@ -1039,6 +1056,7 @@
      */
     @UiThreadTest
     @Test
+    @Ignore("Can be enabled only after b/330596920 is ready")
     @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
     public void votePreferredFrameRate_isFrameRatePowerSavingsBalanced() {
         ViewRootImpl viewRootImpl = new ViewRootImpl(sContext,
@@ -1056,6 +1074,7 @@
      * 2. If FT2-FT1 > 15ms && FT3-FT2 > 15ms -> vote for NORMAL category
      */
     @Test
+    @Ignore("Can be enabled only after b/330596920 is ready")
     @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
     public void votePreferredFrameRate_applyTextureViewHeuristic() throws InterruptedException {
         final long delay = 30L;
diff --git a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java
index a5c9624..faad472 100644
--- a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java
+++ b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java
@@ -52,9 +52,11 @@
 import android.view.View;
 import android.view.ViewConfiguration;
 import android.view.ViewGroup;
+import android.view.inputmethod.Flags;
 import android.view.inputmethod.InputMethodManager;
 import android.widget.EditText;
 
+import androidx.test.annotation.UiThreadTest;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 import androidx.test.platform.app.InstrumentationRegistry;
@@ -72,6 +74,7 @@
  */
 @Presubmit
 @SmallTest
+@UiThreadTest
 @RunWith(AndroidJUnit4.class)
 public class HandwritingInitiatorTest {
     private static final long TIMEOUT = ViewConfiguration.getLongPressTimeout();
@@ -133,7 +136,7 @@
         when(mTestView1.getOffsetForPosition(anyFloat(), anyFloat())).thenReturn(4);
         when(mTestView1.getLineAtCoordinate(anyFloat())).thenReturn(0);
 
-        mHandwritingInitiator.onInputConnectionCreated(mTestView1);
+        onEditorFocusedOrConnectionCreated(mTestView1);
         final int x1 = (sHwArea1.left + sHwArea1.right) / 2;
         final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2;
         MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
@@ -170,7 +173,7 @@
         when(mTestView1.getOffsetForPosition(anyFloat(), anyFloat())).thenReturn(4);
         when(mTestView1.getLineAtCoordinate(anyFloat())).thenReturn(2);
 
-        mHandwritingInitiator.onInputConnectionCreated(mTestView1);
+        onEditorFocusedOrConnectionCreated(mTestView1);
         final int x1 = (sHwArea1.left + sHwArea1.right) / 2;
         final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2;
         MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
@@ -200,7 +203,7 @@
 
     @Test
     public void onTouchEvent_startHandwritingOnce_when_stylusMoveMultiTimes_withinHWArea() {
-        mHandwritingInitiator.onInputConnectionCreated(mTestView1);
+        onEditorFocusedOrConnectionCreated(mTestView1);
         final int x1 = (sHwArea1.left + sHwArea1.right) / 2;
         final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2;
         MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
@@ -244,9 +247,7 @@
         when(mTestView1.getOffsetForPosition(anyFloat(), anyFloat())).thenReturn(4);
         when(mTestView1.getLineAtCoordinate(anyFloat())).thenReturn(0);
 
-        if (!mInitiateWithoutConnection) {
-            mHandwritingInitiator.onInputConnectionCreated(mTestView1);
-        }
+        onEditorFocusedOrConnectionCreated(mTestView1);
         final int x1 = sHwArea1.left - HW_BOUNDS_OFFSETS_LEFT_PX / 2;
         final int y1 = sHwArea1.top - HW_BOUNDS_OFFSETS_TOP_PX / 2;
         MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
@@ -282,13 +283,7 @@
         MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0);
         mHandwritingInitiator.onTouchEvent(stylusEvent2);
 
-        if (mInitiateWithoutConnection) {
-            // Focus is changed after stylus movement.
-            mHandwritingInitiator.updateFocusedView(mTestView1, /*fromTouchEvent*/ true);
-        } else {
-            // InputConnection is created after stylus movement.
-            mHandwritingInitiator.onInputConnectionCreated(mTestView1);
-        }
+        onEditorFocusedOrConnectionCreated(mTestView1);
 
         verify(mHandwritingInitiator, times(1)).startHandwriting(mTestView1);
     }
@@ -310,24 +305,11 @@
         final int y2 = y1;
         MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0);
         mHandwritingInitiator.onTouchEvent(stylusEvent2);
-
-        if (!mInitiateWithoutConnection) {
-            // First create InputConnection for mTestView2 and verify that handwriting is not
-            // started.
-            mHandwritingInitiator.onInputConnectionCreated(mTestView2);
-        }
-
+        onEditorFocusedOrConnectionCreated(mTestView2);
         // Note: mTestView2 receives focus when initiationWithoutInputConnection() is enabled.
         //  verify that handwriting is not started.
         verify(mHandwritingInitiator, never()).startHandwriting(mTestView2);
-        if (mInitiateWithoutConnection) {
-            // Focus is changed after stylus movement.
-            mHandwritingInitiator.updateFocusedView(mTestView1, /*fromTouchEvent*/ true);
-        } else {
-            // Next create InputConnection for mTextView1. Handwriting is started for this view
-            // since the stylus down point is closest to this view.
-            mHandwritingInitiator.onInputConnectionCreated(mTestView1);
-        }
+        onEditorFocusedOrConnectionCreated(mTestView1);
         // Handwriting is started for this view since  the stylus down point is closest to this
         // view.
         verify(mHandwritingInitiator).startHandwriting(mTestView1);
@@ -349,7 +331,7 @@
         delegateView.setIsHandwritingDelegate(true);
 
         mTestView1.setHandwritingDelegatorCallback(
-                () -> mHandwritingInitiator.onInputConnectionCreated(delegateView));
+                () -> onEditorFocusedOrConnectionCreated(delegateView));
 
         final int x1 = (sHwArea1.left + sHwArea1.right) / 2;
         final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2;
@@ -369,17 +351,15 @@
     public void onTouchEvent_tryAcceptDelegation_delegatorCallbackFocusesDelegate() {
         View delegateView = new EditText(mContext);
         delegateView.setIsHandwritingDelegate(true);
+        if (mInitiateWithoutConnection) {
+            mHandwritingInitiator.onEditorFocused(delegateView);
+        }
         mHandwritingInitiator.onInputConnectionCreated(delegateView);
         reset(mHandwritingInitiator);
 
-        if (mInitiateWithoutConnection) {
-            mTestView1.setHandwritingDelegatorCallback(
-                    () -> mHandwritingInitiator.updateFocusedView(
-                            delegateView, /*fromTouchEvent*/ false));
-        } else  {
-            mTestView1.setHandwritingDelegatorCallback(
-                    () -> mHandwritingInitiator.onDelegateViewFocused(delegateView));
-        }
+
+        mTestView1.setHandwritingDelegatorCallback(
+                () -> mHandwritingInitiator.onDelegateViewFocused(delegateView));
 
         final int x1 = (sHwArea1.left + sHwArea1.right) / 2;
         final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2;
@@ -391,7 +371,7 @@
         MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0);
         mHandwritingInitiator.onTouchEvent(stylusEvent2);
 
-        verify(mHandwritingInitiator, times(1)).tryAcceptStylusHandwritingDelegation(delegateView);
+        verify(mHandwritingInitiator, times(1)).tryAcceptStylusHandwritingDelegation(any());
     }
 
     @Test
@@ -429,14 +409,6 @@
         assertThat(onTouchEventResult4).isTrue();
     }
 
-    private void callOnInputConnectionOrUpdateViewFocus(View view) {
-        if (mInitiateWithoutConnection) {
-            mHandwritingInitiator.updateFocusedView(view, /*fromTouchEvent*/ true);
-        } else {
-            mHandwritingInitiator.onInputConnectionCreated(view);
-        }
-    }
-
     @Test
     public void onTouchEvent_notStartHandwriting_whenHandwritingNotAvailable() {
         final Rect rect = new Rect(600, 600, 900, 900);
@@ -444,7 +416,7 @@
                 false /* isStylusHandwritingAvailable */);
         mHandwritingInitiator.updateHandwritingAreasForView(testView);
 
-        callOnInputConnectionOrUpdateViewFocus(testView);
+        onEditorFocusedOrConnectionCreated(testView);
         final int x1 = (rect.left + rect.right) / 2;
         final int y1 = (rect.top + rect.bottom) / 2;
         MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
@@ -463,7 +435,7 @@
 
     @Test
     public void onTouchEvent_notStartHandwriting_when_stylusTap_withinHWArea() {
-        callOnInputConnectionOrUpdateViewFocus(mTestView1);
+        onEditorFocusedOrConnectionCreated(mTestView1);
         final int x1 = 200;
         final int y1 = 200;
         MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
@@ -479,7 +451,7 @@
 
     @Test
     public void onTouchEvent_notStartHandwriting_when_stylusMove_outOfHWArea() {
-        callOnInputConnectionOrUpdateViewFocus(mTestView1);
+        onEditorFocusedOrConnectionCreated(mTestView1);
         final int x1 = 10;
         final int y1 = 10;
         MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
@@ -495,7 +467,7 @@
 
     @Test
     public void onTouchEvent_notStartHandwriting_when_stylusMove_afterTimeOut() {
-        callOnInputConnectionOrUpdateViewFocus(mTestView1);
+        onEditorFocusedOrConnectionCreated(mTestView1);
         final int x1 = 10;
         final int y1 = 10;
         final long time1 = 10L;
@@ -551,9 +523,7 @@
 
     @Test
     public void onTouchEvent_focusView_inputConnectionAlreadyBuilt_stylusMoveOnce_withinHWArea() {
-        if (!mInitiateWithoutConnection) {
-            mHandwritingInitiator.onInputConnectionCreated(mTestView1);
-        }
+        onEditorFocusedOrConnectionCreated(mTestView1);
         final int x1 = (sHwArea1.left + sHwArea1.right) / 2;
         final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2;
         MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
@@ -606,14 +576,14 @@
 
         verify(mTestView2, times(1)).requestFocus();
 
-        callOnInputConnectionOrUpdateViewFocus(mTestView2);
+        onEditorFocusedOrConnectionCreated(mTestView2);
         verify(mHandwritingInitiator, times(1)).startHandwriting(mTestView2);
     }
 
     @Test
     public void onTouchEvent_handwritingAreaOverlapped_focusedViewHasPriority() {
         // Simulate the case where mTestView1 is focused.
-        callOnInputConnectionOrUpdateViewFocus(mTestView1);
+        onEditorFocusedOrConnectionCreated(mTestView1);
         // The ACTION_DOWN location is within the handwriting bounds of both mTestView1 and
         // mTestView2. Although it's closer to mTestView2's handwriting bounds, handwriting is
         // initiated for mTestView1 because it's focused.
@@ -651,7 +621,7 @@
     @Test
     public void onResolvePointerIcon_afterHandwriting_hidePointerIconForConnectedView() {
         // simulate the case where sTestView1 is focused.
-        mHandwritingInitiator.onInputConnectionCreated(mTestView1);
+        onEditorFocusedOrConnectionCreated(mTestView1);
         injectStylusEvent(mHandwritingInitiator, sHwArea1.centerX(), sHwArea1.centerY(),
                 /* exceedsHWSlop */ true);
         // Verify that handwriting started for sTestView1.
@@ -677,15 +647,14 @@
     public void onResolvePointerIcon_afterHandwriting_hidePointerIconForDelegatorView() {
         // Set mTextView2 to be the delegate of mTestView1.
         mTestView2.setIsHandwritingDelegate(true);
+        mTestView1.setHandwritingDelegatorCallback(
+                () -> {
+                    if (mInitiateWithoutConnection) {
+                        mHandwritingInitiator.updateFocusedView(mTestView2);
+                    }
+                    mHandwritingInitiator.onInputConnectionCreated(mTestView2);
+                });
 
-        if (mInitiateWithoutConnection) {
-            mTestView1.setHandwritingDelegatorCallback(
-                    () -> mHandwritingInitiator.updateFocusedView(
-                            mTestView2, /*fromTouchEvent*/ false));
-        } else {
-            mTestView1.setHandwritingDelegatorCallback(
-                    () -> mHandwritingInitiator.onInputConnectionCreated(mTestView2));
-        }
         injectStylusEvent(mHandwritingInitiator, sHwArea1.centerX(), sHwArea1.centerY(),
                 /* exceedsHWSlop */ true);
         // Prerequisite check, verify that handwriting started for delegateView.
@@ -700,7 +669,7 @@
     @Test
     public void onResolvePointerIcon_showHoverIconAfterTap() {
         // Simulate the case where sTestView1 is focused.
-        mHandwritingInitiator.onInputConnectionCreated(mTestView1);
+        onEditorFocusedOrConnectionCreated(mTestView1);
         injectStylusEvent(mHandwritingInitiator, sHwArea1.centerX(), sHwArea1.centerY(),
                 /* exceedsHWSlop */ true);
         // Verify that handwriting started for sTestView1.
@@ -722,7 +691,7 @@
     @Test
     public void onResolvePointerIcon_showHoverIconAfterFocusChange() {
         // Simulate the case where sTestView1 is focused.
-        mHandwritingInitiator.onInputConnectionCreated(mTestView1);
+        onEditorFocusedOrConnectionCreated(mTestView1);
         injectStylusEvent(mHandwritingInitiator, sHwArea1.centerX(), sHwArea1.centerY(),
                 /* exceedsHWSlop */ true);
         // Verify that handwriting started for sTestView1.
@@ -733,14 +702,8 @@
         // After handwriting is initiated for the connected view, hide the hover icon.
         assertThat(icon1).isNull();
 
-        // Simulate that focus is switched to mTestView2 first and then switched back.
-        if (mInitiateWithoutConnection) {
-            mHandwritingInitiator.updateFocusedView(mTestView2, /*fromTouchEvent*/ true);
-            mHandwritingInitiator.updateFocusedView(mTestView1, /*fromTouchEvent*/ true);
-        } else {
-            mHandwritingInitiator.onInputConnectionCreated(mTestView2);
-            mHandwritingInitiator.onInputConnectionCreated(mTestView1);
-        }
+        onEditorFocusedOrConnectionCreated(mTestView2);
+        onEditorFocusedOrConnectionCreated(mTestView1);
 
         PointerIcon icon2 = mHandwritingInitiator.onResolvePointerIcon(mContext, hoverEvent1);
         // After the change of focus, hover icon shows again.
@@ -752,11 +715,11 @@
         if (mInitiateWithoutConnection) {
             mTestView1.setAutoHandwritingEnabled(false);
             mTestView1.setHandwritingDelegatorCallback(null);
-            mHandwritingInitiator.updateFocusedView(mTestView1, /*fromTouchEvent*/ true);
+            onEditorFocusedOrConnectionCreated(mTestView1);
         } else {
             View mockView = createView(sHwArea1, false /* autoHandwritingEnabled */,
                     true /* isStylusHandwritingAvailable */);
-            mHandwritingInitiator.onInputConnectionCreated(mockView);
+            onEditorFocusedOrConnectionCreated(mockView);
         }
         final int x1 = (sHwArea1.left + sHwArea1.right) / 2;
         final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2;
@@ -972,4 +935,12 @@
                 1 /* yPrecision */, 0 /* deviceId */, 0 /* edgeFlags */,
                 InputDevice.SOURCE_STYLUS, 0 /* flags */);
     }
+
+    private void onEditorFocusedOrConnectionCreated(View testView) {
+        if (Flags.initiationWithoutInputConnection()) {
+            mHandwritingInitiator.onEditorFocused(testView);
+        } else {
+            mHandwritingInitiator.onInputConnectionCreated(testView);
+        }
+    }
 }
diff --git a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java
index 60a436e..745390d 100644
--- a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java
+++ b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java
@@ -25,7 +25,6 @@
 import static androidx.test.espresso.matcher.RootMatchers.isDialog;
 import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
 import static androidx.test.espresso.matcher.ViewMatchers.withClassName;
-import static androidx.test.espresso.matcher.ViewMatchers.withId;
 import static androidx.test.espresso.matcher.ViewMatchers.withText;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -54,7 +53,6 @@
 import android.content.pm.ServiceInfo;
 import android.os.Bundle;
 import android.os.Handler;
-import android.platform.test.annotations.RequiresFlagsDisabled;
 import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.platform.test.flag.junit.CheckFlagsRule;
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
@@ -176,21 +174,6 @@
     }
 
     @Test
-    @RequiresFlagsDisabled(Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG)
-    public void selectTestService_oldPermissionDialog_deny_dialogIsHidden() {
-        launchActivity();
-        openShortcutsList();
-
-        mDevice.findObject(By.text(TEST_LABEL)).clickAndWait(Until.newWindow(), UI_TIMEOUT_MS);
-        onView(withText(DENY_LABEL)).perform(scrollTo(), click());
-        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
-
-        onView(withId(R.id.accessibility_permissionDialog_title)).inRoot(isDialog()).check(
-                doesNotExist());
-    }
-
-    @Test
-    @RequiresFlagsEnabled(Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG)
     public void selectTestService_permissionDialog_allow_rowChecked() {
         launchActivity();
         openShortcutsList();
@@ -202,7 +185,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG)
     public void selectTestService_permissionDialog_deny_rowNotChecked() {
         launchActivity();
         openShortcutsList();
@@ -214,7 +196,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG)
     public void selectTestService_permissionDialog_uninstall_callsUninstaller_rowRemoved() {
         launchActivity();
         openShortcutsList();
@@ -228,7 +209,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG)
     public void selectTestService_permissionDialog_notShownWhenNotRequired() throws Exception {
         when(mAccessibilityManagerService.isAccessibilityServiceWarningRequired(any()))
                 .thenReturn(false);
@@ -243,7 +223,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG)
     public void selectTestService_notPermittedByAdmin_blockedEvenIfNoWarningRequired()
             throws Exception {
         when(mAccessibilityManagerService.isAccessibilityServiceWarningRequired(any()))
@@ -380,11 +359,9 @@
         @Override
         public void onCreate(Bundle savedInstanceState) {
             super.onCreate(savedInstanceState);
-            if (Flags.cleanupAccessibilityWarningDialog()) {
-                // Setting the Theme is necessary here for the dialog to use the proper style
-                // resources as designated in its layout XML.
-                setTheme(R.style.Theme_DeviceDefault_DayNight);
-            }
+            // Setting the Theme is necessary here for the dialog to use the proper style
+            // resources as designated in its layout XML.
+            setTheme(R.style.Theme_DeviceDefault_DayNight);
         }
 
         @Override
diff --git a/core/tests/coretests/src/com/android/internal/accessibility/dialog/AccessibilityServiceWarningTest.java b/core/tests/coretests/src/com/android/internal/accessibility/dialog/AccessibilityServiceWarningTest.java
index 24aab61..362eeea 100644
--- a/core/tests/coretests/src/com/android/internal/accessibility/dialog/AccessibilityServiceWarningTest.java
+++ b/core/tests/coretests/src/com/android/internal/accessibility/dialog/AccessibilityServiceWarningTest.java
@@ -25,7 +25,6 @@
 import android.app.AlertDialog;
 import android.content.Context;
 import android.os.RemoteException;
-import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.platform.test.flag.junit.CheckFlagsRule;
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.testing.AndroidTestingRunner;
@@ -57,8 +56,6 @@
  */
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper
-@RequiresFlagsEnabled(
-        android.view.accessibility.Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG)
 public class AccessibilityServiceWarningTest {
     private static final String A11Y_SERVICE_PACKAGE_LABEL = "TestA11yService";
     private static final String A11Y_SERVICE_SUMMARY = "TestA11yService summary";
diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java b/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java
index cb8754a..488f017 100644
--- a/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java
+++ b/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java
@@ -27,6 +27,7 @@
 import static androidx.test.espresso.matcher.ViewMatchers.withText;
 
 import static com.android.internal.app.MatcherUtils.first;
+import static com.android.internal.app.ResolverActivity.EXTRA_RESTRICT_TO_SINGLE_USER;
 import static com.android.internal.app.ResolverDataProvider.createPackageManagerMockedInfo;
 import static com.android.internal.app.ResolverWrapperActivity.sOverrides;
 
@@ -1254,6 +1255,51 @@
         }
     }
 
+    @Test
+    public void testTriggerFromMainProfile_inSingleUserMode_withWorkProfilePresent() {
+        mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
+                android.multiuser.Flags.FLAG_ALLOW_RESOLVER_SHEET_FOR_PRIVATE_SPACE);
+        markWorkProfileUserAvailable();
+        setTabOwnerUserHandleForLaunch(PERSONAL_USER_HANDLE);
+        Intent sendIntent = createSendImageIntent();
+        sendIntent.putExtra(EXTRA_RESTRICT_TO_SINGLE_USER, true);
+        List<ResolvedComponentInfo> personalResolvedComponentInfos =
+                createResolvedComponentsForTestWithOtherProfile(3, PERSONAL_USER_HANDLE);
+        List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4,
+                sOverrides.workProfileUserHandle);
+        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+        waitForIdle();
+        assertThat(activity.getPersonalListAdapter().getCount(), is(2));
+        onView(withId(R.id.tabs)).check(matches(not(isDisplayed())));
+        assertEquals(activity.getMultiProfilePagerAdapterCount(), 1);
+        for (ResolvedComponentInfo resolvedInfo : personalResolvedComponentInfos) {
+            assertEquals(resolvedInfo.getResolveInfoAt(0).userHandle, PERSONAL_USER_HANDLE);
+        }
+    }
+
+    @Test
+    public void testTriggerFromWorkProfile_inSingleUserMode() {
+        mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
+                android.multiuser.Flags.FLAG_ALLOW_RESOLVER_SHEET_FOR_PRIVATE_SPACE);
+        markWorkProfileUserAvailable();
+        setTabOwnerUserHandleForLaunch(sOverrides.workProfileUserHandle);
+        Intent sendIntent = createSendImageIntent();
+        sendIntent.putExtra(EXTRA_RESTRICT_TO_SINGLE_USER, true);
+        List<ResolvedComponentInfo> personalResolvedComponentInfos =
+                createResolvedComponentsForTest(3, sOverrides.workProfileUserHandle);
+        setupResolverControllers(personalResolvedComponentInfos);
+        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+        waitForIdle();
+        assertThat(activity.getPersonalListAdapter().getCount(), is(3));
+        onView(withId(R.id.tabs)).check(matches(not(isDisplayed())));
+        assertEquals(activity.getMultiProfilePagerAdapterCount(), 1);
+        for (ResolvedComponentInfo resolvedInfo : personalResolvedComponentInfos) {
+            assertEquals(resolvedInfo.getResolveInfoAt(0).userHandle,
+                    sOverrides.workProfileUserHandle);
+        }
+    }
+
     private Intent createSendImageIntent() {
         Intent sendIntent = new Intent();
         sendIntent.setAction(Intent.ACTION_SEND);
@@ -1339,6 +1385,10 @@
         ResolverWrapperActivity.sOverrides.privateProfileUserHandle = UserHandle.of(12);
     }
 
+    private void setTabOwnerUserHandleForLaunch(UserHandle tabOwnerUserHandleForLaunch) {
+        sOverrides.tabOwnerUserHandleForLaunch = tabOwnerUserHandleForLaunch;
+    }
+
     private void setupResolverControllers(
             List<ResolvedComponentInfo> personalResolvedComponentInfos,
             List<ResolvedComponentInfo> workResolvedComponentInfos) {
diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java
index 862cbd5..4604b01 100644
--- a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java
+++ b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java
@@ -116,6 +116,10 @@
             when(sOverrides.resolverListController.getUserHandle()).thenReturn(UserHandle.SYSTEM);
             return sOverrides.resolverListController;
         }
+        if (isLaunchedInSingleUserMode()) {
+            when(sOverrides.resolverListController.getUserHandle()).thenReturn(userHandle);
+            return sOverrides.resolverListController;
+        }
         when(sOverrides.workResolverListController.getUserHandle()).thenReturn(userHandle);
         return sOverrides.workResolverListController;
     }
diff --git a/core/tests/coretests/src/com/android/internal/net/ConnectivityBlobStoreTest.java b/core/tests/coretests/src/com/android/internal/net/ConnectivityBlobStoreTest.java
new file mode 100644
index 0000000..68545cf
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/net/ConnectivityBlobStoreTest.java
@@ -0,0 +1,156 @@
+/*
+ * 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.internal.net;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ConnectivityBlobStoreTest {
+    private static final String DATABASE_FILENAME = "ConnectivityBlobStore.db";
+    private static final String TEST_NAME = "TEST_NAME";
+    private static final byte[] TEST_BLOB = new byte[] {(byte) 10, (byte) 90, (byte) 45, (byte) 12};
+
+    private Context mContext;
+    private File mFile;
+
+    private ConnectivityBlobStore createConnectivityBlobStore() {
+        return new ConnectivityBlobStore(mFile);
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = InstrumentationRegistry.getContext();
+        mFile = mContext.getDatabasePath(DATABASE_FILENAME);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mContext.deleteDatabase(DATABASE_FILENAME);
+    }
+
+    @Test
+    public void testFileCreateDelete() {
+        assertFalse(mFile.exists());
+        createConnectivityBlobStore();
+        assertTrue(mFile.exists());
+
+        assertTrue(mContext.deleteDatabase(DATABASE_FILENAME));
+        assertFalse(mFile.exists());
+    }
+
+    @Test
+    public void testPutAndGet() throws Exception {
+        final ConnectivityBlobStore connectivityBlobStore = createConnectivityBlobStore();
+        assertNull(connectivityBlobStore.get(TEST_NAME));
+
+        assertTrue(connectivityBlobStore.put(TEST_NAME, TEST_BLOB));
+        assertArrayEquals(TEST_BLOB, connectivityBlobStore.get(TEST_NAME));
+
+        // Test replacement
+        final byte[] newBlob = new byte[] {(byte) 15, (byte) 20};
+        assertTrue(connectivityBlobStore.put(TEST_NAME, newBlob));
+        assertArrayEquals(newBlob, connectivityBlobStore.get(TEST_NAME));
+    }
+
+    @Test
+    public void testRemove() throws Exception {
+        final ConnectivityBlobStore connectivityBlobStore = createConnectivityBlobStore();
+        assertNull(connectivityBlobStore.get(TEST_NAME));
+        assertFalse(connectivityBlobStore.remove(TEST_NAME));
+
+        assertTrue(connectivityBlobStore.put(TEST_NAME, TEST_BLOB));
+        assertArrayEquals(TEST_BLOB, connectivityBlobStore.get(TEST_NAME));
+
+        assertTrue(connectivityBlobStore.remove(TEST_NAME));
+        assertNull(connectivityBlobStore.get(TEST_NAME));
+
+        // Removing again returns false
+        assertFalse(connectivityBlobStore.remove(TEST_NAME));
+    }
+
+    @Test
+    public void testMultipleNames() throws Exception {
+        final String name1 = TEST_NAME + "1";
+        final String name2 = TEST_NAME + "2";
+        final ConnectivityBlobStore connectivityBlobStore = createConnectivityBlobStore();
+
+        assertNull(connectivityBlobStore.get(name1));
+        assertNull(connectivityBlobStore.get(name2));
+        assertFalse(connectivityBlobStore.remove(name1));
+        assertFalse(connectivityBlobStore.remove(name2));
+
+        assertTrue(connectivityBlobStore.put(name1, TEST_BLOB));
+        assertTrue(connectivityBlobStore.put(name2, TEST_BLOB));
+        assertArrayEquals(TEST_BLOB, connectivityBlobStore.get(name1));
+        assertArrayEquals(TEST_BLOB, connectivityBlobStore.get(name2));
+
+        // Replace the blob for name1 only.
+        final byte[] newBlob = new byte[] {(byte) 16, (byte) 21};
+        assertTrue(connectivityBlobStore.put(name1, newBlob));
+        assertArrayEquals(newBlob, connectivityBlobStore.get(name1));
+
+        assertTrue(connectivityBlobStore.remove(name1));
+        assertNull(connectivityBlobStore.get(name1));
+        assertArrayEquals(TEST_BLOB, connectivityBlobStore.get(name2));
+
+        assertFalse(connectivityBlobStore.remove(name1));
+        assertTrue(connectivityBlobStore.remove(name2));
+        assertNull(connectivityBlobStore.get(name2));
+        assertFalse(connectivityBlobStore.remove(name2));
+    }
+
+    @Test
+    public void testList() throws Exception {
+        final String[] unsortedNames = new String[] {
+                TEST_NAME + "1",
+                TEST_NAME + "2",
+                TEST_NAME + "0",
+                "NON_MATCHING_PREFIX",
+                "MATCHING_SUFFIX_" + TEST_NAME
+        };
+        // Expected to match and discard the prefix and be in increasing sorted order.
+        final String[] expected = new String[] {
+                "0",
+                "1",
+                "2"
+        };
+        final ConnectivityBlobStore connectivityBlobStore = createConnectivityBlobStore();
+
+        for (int i = 0; i < unsortedNames.length; i++) {
+            assertTrue(connectivityBlobStore.put(unsortedNames[i], TEST_BLOB));
+        }
+        final String[] actual = connectivityBlobStore.list(TEST_NAME /* prefix */);
+        assertArrayEquals(expected, actual);
+    }
+}
diff --git a/core/tests/coretests/src/com/android/internal/net/OWNERS b/core/tests/coretests/src/com/android/internal/net/OWNERS
new file mode 100644
index 0000000..f51ba47
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/net/OWNERS
@@ -0,0 +1 @@
+include /core/java/com/android/internal/net/OWNERS
diff --git a/data/etc/com.android.settings.xml b/data/etc/com.android.settings.xml
index fbe1b8e..6bdd291 100644
--- a/data/etc/com.android.settings.xml
+++ b/data/etc/com.android.settings.xml
@@ -49,6 +49,7 @@
         <permission name="android.permission.READ_SEARCH_INDEXABLES"/>
         <permission name="android.permission.REBOOT"/>
         <permission name="android.permission.RECOVERY"/>
+        <permission name="android.permission.SCHEDULE_EXACT_ALARM"/>
         <permission name="android.permission.STATUS_BAR"/>
         <permission name="android.permission.SUGGEST_MANUAL_TIME_AND_ZONE"/>
         <permission name="android.permission.TETHER_PRIVILEGED"/>
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 9c1c7006..ea3235b 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -588,6 +588,8 @@
         <permission name="android.permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW" />
         <!-- Permission required for CTS test - PackageManagerShellCommandInstallTest -->
         <permission name="android.permission.EMERGENCY_INSTALL_PACKAGES" />
+        <!-- Permission required for Cts test - CtsSettingsTestCases -->
+        <permission name="android.permission.PREPARE_FACTORY_RESET" />
     </privapp-permissions>
 
     <privapp-permissions package="com.android.statementservice">
diff --git a/data/keyboards/Vendor_054c_Product_05c4.idc b/data/keyboards/Vendor_054c_Product_05c4.idc
index 9576e8d..2da6227 100644
--- a/data/keyboards/Vendor_054c_Product_05c4.idc
+++ b/data/keyboards/Vendor_054c_Product_05c4.idc
@@ -45,14 +45,15 @@
 # This uneven timing causes the apparent speed of a finger (calculated using
 # time deltas between received reports) to vary dramatically even if it's
 # actually moving smoothly across the touchpad, triggering the touchpad stack's
-# drumroll detection logic, which causes the finger's single smooth movement to
-# be treated as many small movements of consecutive touches, which are then
-# inhibited by the click wiggle filter.
+# drumroll detection logic. For moving fingers, the drumroll detection logic
+# splits the finger's single movement into many small movements of consecutive
+# touches, which are then inhibited by the click wiggle filter. For tapping
+# fingers, it prevents tapping to click because it thinks the finger's moving
+# too fast.
 #
-# Since this touchpad does not seem vulnerable to click wiggle, we can safely
-# disable drumroll detection due to speed changes (by setting the speed change
-# threshold very high, since there's no boolean control property).
-gestureProp.Drumroll_Max_Speed_Change_Factor = 1000000000
+# Since this touchpad doesn't seem to have to drumroll issues, we can safely
+# disable drumroll detection.
+gestureProp.Drumroll_Suppression_Enable = 0
 
 # Because of the way this touchpad is positioned, touches around the edges are
 # no more likely to be palms than ones in the middle, so remove the edge zones
diff --git a/data/keyboards/Vendor_054c_Product_09cc.idc b/data/keyboards/Vendor_054c_Product_09cc.idc
index 9576e8d..2a1a4fc 100644
--- a/data/keyboards/Vendor_054c_Product_09cc.idc
+++ b/data/keyboards/Vendor_054c_Product_09cc.idc
@@ -45,14 +45,15 @@
 # This uneven timing causes the apparent speed of a finger (calculated using
 # time deltas between received reports) to vary dramatically even if it's
 # actually moving smoothly across the touchpad, triggering the touchpad stack's
-# drumroll detection logic, which causes the finger's single smooth movement to
-# be treated as many small movements of consecutive touches, which are then
-# inhibited by the click wiggle filter.
+# drumroll detection logic. For moving fingers, the drumroll detection logic
+# splits the finger's single movement into many small movements of consecutive
+# touches, which are then inhibited by the click wiggle filter. For tapping
+# fingers, it prevents tapping to click because it thinks the finger's moving
+# too fast.
 #
-# Since this touchpad does not seem vulnerable to click wiggle, we can safely
-# disable drumroll detection due to speed changes (by setting the speed change
-# threshold very high, since there's no boolean control property).
-gestureProp.Drumroll_Max_Speed_Change_Factor = 1000000000
+# Since this touchpad doesn't seem to have drumroll issues, we can safely
+# disable drumroll detection.
+gestureProp.Drumroll_Suppression_Enable = 0
 
 # Because of the way this touchpad is positioned, touches around the edges are
 # no more likely to be palms than ones in the middle, so remove the edge zones
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
index 9756278..16c77d0 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
@@ -53,7 +53,7 @@
      * The min version of the WM Extensions that must be supported in the current platform version.
      */
     @VisibleForTesting
-    static final int EXTENSIONS_VERSION_CURRENT_PLATFORM = 5;
+    static final int EXTENSIONS_VERSION_CURRENT_PLATFORM = 6;
 
     private final Object mLock = new Object();
     private volatile DeviceStateManagerFoldingFeatureProducer mFoldingFeatureProducer;
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
index 100185b..cae232e 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
@@ -17,6 +17,12 @@
 package androidx.window.extensions.embedding;
 
 import static android.util.TypedValue.COMPLEX_UNIT_DIP;
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
+import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
+import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE;
+import static android.window.TaskFragmentOperation.OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE;
 
 import static androidx.window.extensions.embedding.DividerAttributes.RATIO_UNSET;
 import static androidx.window.extensions.embedding.DividerAttributes.WIDTH_UNSET;
@@ -28,34 +34,253 @@
 import android.annotation.Nullable;
 import android.app.ActivityThread;
 import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
+import android.graphics.Color;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.RotateDrawable;
+import android.hardware.display.DisplayManager;
+import android.os.IBinder;
 import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.SurfaceControl;
+import android.view.SurfaceControlViewHost;
+import android.view.WindowManager;
+import android.view.WindowlessWindowManager;
+import android.widget.FrameLayout;
+import android.widget.ImageButton;
+import android.window.InputTransferToken;
+import android.window.TaskFragmentOperation;
+import android.window.TaskFragmentParentInfo;
+import android.window.WindowContainerTransaction;
 
+import androidx.annotation.IdRes;
 import androidx.annotation.NonNull;
 
+import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.window.flags.Flags;
 
+import java.util.Objects;
+
 /**
  * Manages the rendering and interaction of the divider.
  */
 class DividerPresenter {
+    private static final String WINDOW_NAME = "AE Divider";
+
     // TODO(b/327067596) Update based on UX guidance.
-    @VisibleForTesting static final float DEFAULT_MIN_RATIO = 0.35f;
-    @VisibleForTesting static final float DEFAULT_MAX_RATIO = 0.65f;
-    @VisibleForTesting static final int DEFAULT_DIVIDER_WIDTH_DP = 24;
+    private static final Color DEFAULT_DIVIDER_COLOR = Color.valueOf(Color.BLACK);
+    @VisibleForTesting
+    static final float DEFAULT_MIN_RATIO = 0.35f;
+    @VisibleForTesting
+    static final float DEFAULT_MAX_RATIO = 0.65f;
+    @VisibleForTesting
+    static final int DEFAULT_DIVIDER_WIDTH_DP = 24;
 
-    static int getDividerWidthPx(@NonNull DividerAttributes dividerAttributes) {
+    /**
+     * The {@link Properties} of the divider. This field is {@code null} when no divider should be
+     * drawn, e.g. when the split doesn't have {@link DividerAttributes} or when the decor surface
+     * is not available.
+     */
+    @Nullable
+    @VisibleForTesting
+    Properties mProperties;
+
+    /**
+     * The {@link Renderer} of the divider. This field is {@code null} when no divider should be
+     * drawn, i.e. when {@link #mProperties} is {@code null}. The {@link Renderer} is recreated or
+     * updated when {@link #mProperties} is changed.
+     */
+    @Nullable
+    @VisibleForTesting
+    Renderer mRenderer;
+
+    /**
+     * The owner TaskFragment token of the decor surface. The decor surface is placed right above
+     * the owner TaskFragment surface and is removed if the owner TaskFragment is destroyed.
+     */
+    @Nullable
+    @VisibleForTesting
+    IBinder mDecorSurfaceOwner;
+
+    /** Updates the divider when external conditions are changed. */
+    void updateDivider(
+            @NonNull WindowContainerTransaction wct,
+            @NonNull TaskFragmentParentInfo parentInfo,
+            @Nullable SplitContainer topSplitContainer) {
+        if (!Flags.activityEmbeddingInteractiveDividerFlag()) {
+            return;
+        }
+
+        // Clean up the decor surface if top SplitContainer is null.
+        if (topSplitContainer == null) {
+            removeDecorSurfaceAndDivider(wct);
+            return;
+        }
+
+        // Clean up the decor surface if DividerAttributes is null.
+        final DividerAttributes dividerAttributes =
+                topSplitContainer.getCurrentSplitAttributes().getDividerAttributes();
+        if (dividerAttributes == null) {
+            removeDecorSurfaceAndDivider(wct);
+            return;
+        }
+
+        if (topSplitContainer.getCurrentSplitAttributes().getSplitType()
+                instanceof SplitAttributes.SplitType.ExpandContainersSplitType) {
+            // No divider is needed for ExpandContainersSplitType.
+            removeDivider();
+            return;
+        }
+
+        // Skip updating when the TFs have not been updated to match the SplitAttributes.
+        if (topSplitContainer.getPrimaryContainer().getLastRequestedBounds().isEmpty()
+                || topSplitContainer.getSecondaryContainer().getLastRequestedBounds().isEmpty()) {
+            return;
+        }
+
+        final SurfaceControl decorSurface = parentInfo.getDecorSurface();
+        if (decorSurface == null) {
+            // Clean up when the decor surface is currently unavailable.
+            removeDivider();
+            // Request to create the decor surface
+            createOrMoveDecorSurface(wct, topSplitContainer.getPrimaryContainer());
+            return;
+        }
+
+        // make the top primary container the owner of the decor surface.
+        if (!Objects.equals(mDecorSurfaceOwner,
+                topSplitContainer.getPrimaryContainer().getTaskFragmentToken())) {
+            createOrMoveDecorSurface(wct, topSplitContainer.getPrimaryContainer());
+        }
+
+        updateProperties(
+                new Properties(
+                        parentInfo.getConfiguration(),
+                        dividerAttributes,
+                        decorSurface,
+                        getInitialDividerPosition(topSplitContainer),
+                        isVerticalSplit(topSplitContainer),
+                        parentInfo.getDisplayId()));
+    }
+
+    private void updateProperties(@NonNull Properties properties) {
+        if (Properties.equalsForDivider(mProperties, properties)) {
+            return;
+        }
+        final Properties previousProperties = mProperties;
+        mProperties = properties;
+
+        if (mRenderer == null) {
+            // Create a new renderer when a renderer doesn't exist yet.
+            mRenderer = new Renderer();
+        } else if (!Properties.areSameSurfaces(
+                previousProperties.mDecorSurface, mProperties.mDecorSurface)
+                || previousProperties.mDisplayId != mProperties.mDisplayId) {
+            // Release and recreate the renderer if the decor surface or the display has changed.
+            mRenderer.release();
+            mRenderer = new Renderer();
+        } else {
+            // Otherwise, update the renderer for the new properties.
+            mRenderer.update();
+        }
+    }
+
+    /**
+     * Creates a decor surface for the TaskFragment if no decor surface exists, or changes the owner
+     * of the existing decor surface to be the specified TaskFragment.
+     *
+     * See {@link TaskFragmentOperation#OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE}.
+     */
+    private void createOrMoveDecorSurface(
+            @NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container) {
+        final TaskFragmentOperation operation = new TaskFragmentOperation.Builder(
+                OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE)
+                .build();
+        wct.addTaskFragmentOperation(container.getTaskFragmentToken(), operation);
+        mDecorSurfaceOwner = container.getTaskFragmentToken();
+    }
+
+    private void removeDecorSurfaceAndDivider(@NonNull WindowContainerTransaction wct) {
+        if (mDecorSurfaceOwner != null) {
+            final TaskFragmentOperation operation = new TaskFragmentOperation.Builder(
+                    OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE)
+                    .build();
+            wct.addTaskFragmentOperation(mDecorSurfaceOwner, operation);
+            mDecorSurfaceOwner = null;
+        }
+        removeDivider();
+    }
+
+    private void removeDivider() {
+        if (mRenderer != null) {
+            mRenderer.release();
+        }
+        mProperties = null;
+        mRenderer = null;
+    }
+
+    @VisibleForTesting
+    static int getInitialDividerPosition(@NonNull SplitContainer splitContainer) {
+        final Rect primaryBounds =
+                splitContainer.getPrimaryContainer().getLastRequestedBounds();
+        final Rect secondaryBounds =
+                splitContainer.getSecondaryContainer().getLastRequestedBounds();
+        if (isVerticalSplit(splitContainer)) {
+            return Math.min(primaryBounds.right, secondaryBounds.right);
+        } else {
+            return Math.min(primaryBounds.bottom, secondaryBounds.bottom);
+        }
+    }
+
+    private static boolean isVerticalSplit(@NonNull SplitContainer splitContainer) {
+        final int layoutDirection = splitContainer.getCurrentSplitAttributes().getLayoutDirection();
+        switch(layoutDirection) {
+            case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT:
+            case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT:
+            case SplitAttributes.LayoutDirection.LOCALE:
+                return true;
+            case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM:
+            case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP:
+                return false;
+            default:
+                throw new IllegalArgumentException("Invalid layout direction:" + layoutDirection);
+        }
+    }
+
+    private static void safeReleaseSurfaceControl(@Nullable SurfaceControl sc) {
+        if (sc != null) {
+            sc.release();
+        }
+    }
+
+    private static int getDividerWidthPx(@NonNull DividerAttributes dividerAttributes) {
         int dividerWidthDp = dividerAttributes.getWidthDp();
+        return convertDpToPixel(dividerWidthDp);
+    }
 
+    private static int convertDpToPixel(int dp) {
         // TODO(b/329193115) support divider on secondary display
         final Context applicationContext = ActivityThread.currentActivityThread().getApplication();
 
         return (int) TypedValue.applyDimension(
                 COMPLEX_UNIT_DIP,
-                dividerWidthDp,
+                dp,
                 applicationContext.getResources().getDisplayMetrics());
     }
 
+    private static int getDimensionDp(@IdRes int resId) {
+        final Context context = ActivityThread.currentActivityThread().getApplication();
+        final int px = context.getResources().getDimensionPixelSize(resId);
+        return (int) TypedValue.convertPixelsToDimension(
+                COMPLEX_UNIT_DIP,
+                px,
+                context.getResources().getDisplayMetrics());
+    }
+
     /**
      * Returns the container bound offset that is a result of the presence of a divider.
      *
@@ -140,6 +365,12 @@
             widthDp = DEFAULT_DIVIDER_WIDTH_DP;
         }
 
+        if (dividerAttributes.getDividerType() == DividerAttributes.DIVIDER_TYPE_DRAGGABLE) {
+            // Draggable divider width must be larger than the drag handle size.
+            widthDp = Math.max(widthDp,
+                    getDimensionDp(R.dimen.activity_embedding_divider_touch_target_width));
+        }
+
         float minRatio = dividerAttributes.getPrimaryMinRatio();
         if (minRatio == RATIO_UNSET) {
             minRatio = DEFAULT_MIN_RATIO;
@@ -156,4 +387,231 @@
                 .setPrimaryMaxRatio(maxRatio)
                 .build();
     }
+
+    /**
+     * Properties for the {@link DividerPresenter}. The rendering of the divider solely depends on
+     * these properties. When any value is updated, the divider is re-rendered. The Properties
+     * instance is created only when all the pre-conditions of drawing a divider are met.
+     */
+    @VisibleForTesting
+    static class Properties {
+        private static final int CONFIGURATION_MASK_FOR_DIVIDER =
+                ActivityInfo.CONFIG_DENSITY | ActivityInfo.CONFIG_WINDOW_CONFIGURATION;
+        @NonNull
+        private final Configuration mConfiguration;
+        @NonNull
+        private final DividerAttributes mDividerAttributes;
+        @NonNull
+        private final SurfaceControl mDecorSurface;
+
+        /** The initial position of the divider calculated based on container bounds. */
+        private final int mInitialDividerPosition;
+
+        /** Whether the split is vertical, such as left-to-right or right-to-left split. */
+        private final boolean mIsVerticalSplit;
+
+        private final int mDisplayId;
+
+        @VisibleForTesting
+        Properties(
+                @NonNull Configuration configuration,
+                @NonNull DividerAttributes dividerAttributes,
+                @NonNull SurfaceControl decorSurface,
+                int initialDividerPosition,
+                boolean isVerticalSplit,
+                int displayId) {
+            mConfiguration = configuration;
+            mDividerAttributes = dividerAttributes;
+            mDecorSurface = decorSurface;
+            mInitialDividerPosition = initialDividerPosition;
+            mIsVerticalSplit = isVerticalSplit;
+            mDisplayId = displayId;
+        }
+
+        /**
+         * Compares whether two Properties objects are equal for rendering the divider. The
+         * Configuration is checked for rendering related fields, and other fields are checked for
+         * regular equality.
+         */
+        private static boolean equalsForDivider(@Nullable Properties a, @Nullable Properties b) {
+            if (a == b) {
+                return true;
+            }
+            if (a == null || b == null) {
+                return false;
+            }
+            return areSameSurfaces(a.mDecorSurface, b.mDecorSurface)
+                    && Objects.equals(a.mDividerAttributes, b.mDividerAttributes)
+                    && areConfigurationsEqualForDivider(a.mConfiguration, b.mConfiguration)
+                    && a.mInitialDividerPosition == b.mInitialDividerPosition
+                    && a.mIsVerticalSplit == b.mIsVerticalSplit
+                    && a.mDisplayId == b.mDisplayId;
+        }
+
+        private static boolean areSameSurfaces(
+                @Nullable SurfaceControl sc1, @Nullable SurfaceControl sc2) {
+            if (sc1 == sc2) {
+                // If both are null or both refer to the same object.
+                return true;
+            }
+            if (sc1 == null || sc2 == null) {
+                return false;
+            }
+            return sc1.isSameSurface(sc2);
+        }
+
+        private static boolean areConfigurationsEqualForDivider(
+                @NonNull Configuration a, @NonNull Configuration b) {
+            final int diff = a.diff(b);
+            return (diff & CONFIGURATION_MASK_FOR_DIVIDER) == 0;
+        }
+    }
+
+    /**
+     * Handles the rendering of the divider. When the decor surface is updated, the renderer is
+     * recreated. When other fields in the Properties are changed, the renderer is updated.
+     */
+    @VisibleForTesting
+    class Renderer {
+        @NonNull
+        private final SurfaceControl mDividerSurface;
+        @NonNull
+        private final WindowlessWindowManager mWindowlessWindowManager;
+        @NonNull
+        private final SurfaceControlViewHost mViewHost;
+        @NonNull
+        private final FrameLayout mDividerLayout;
+        private final int mDividerWidthPx;
+
+        private Renderer() {
+            mDividerWidthPx = getDividerWidthPx(mProperties.mDividerAttributes);
+
+            mDividerSurface = createChildSurface("DividerSurface", true /* visible */);
+            mWindowlessWindowManager = new WindowlessWindowManager(
+                    mProperties.mConfiguration,
+                    mDividerSurface,
+                    new InputTransferToken());
+
+            final Context context = ActivityThread.currentActivityThread().getApplication();
+            final DisplayManager displayManager = context.getSystemService(DisplayManager.class);
+            mViewHost = new SurfaceControlViewHost(
+                    context, displayManager.getDisplay(mProperties.mDisplayId),
+                    mWindowlessWindowManager, "DividerContainer");
+            mDividerLayout = new FrameLayout(context);
+
+            update();
+        }
+
+        /** Updates the divider when properties are changed */
+        @VisibleForTesting
+        void update() {
+            mWindowlessWindowManager.setConfiguration(mProperties.mConfiguration);
+            updateSurface();
+            updateLayout();
+            updateDivider();
+        }
+
+        @VisibleForTesting
+        void release() {
+            mViewHost.release();
+            // TODO handle synchronization between surface transactions and WCT.
+            new SurfaceControl.Transaction().remove(mDividerSurface).apply();
+            safeReleaseSurfaceControl(mDividerSurface);
+        }
+
+        private void updateSurface() {
+            final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds();
+            // TODO handle synchronization between surface transactions and WCT.
+            final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+            if (mProperties.mIsVerticalSplit) {
+                t.setPosition(mDividerSurface, mProperties.mInitialDividerPosition, 0.0f);
+                t.setWindowCrop(mDividerSurface, mDividerWidthPx, taskBounds.height());
+            } else {
+                t.setPosition(mDividerSurface, 0.0f, mProperties.mInitialDividerPosition);
+                t.setWindowCrop(mDividerSurface, taskBounds.width(), mDividerWidthPx);
+            }
+            t.apply();
+        }
+
+        private void updateLayout() {
+            final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds();
+            final WindowManager.LayoutParams lp = mProperties.mIsVerticalSplit
+                    ? new WindowManager.LayoutParams(
+                            mDividerWidthPx,
+                            taskBounds.height(),
+                            TYPE_APPLICATION_PANEL,
+                            FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL | FLAG_SLIPPERY,
+                            PixelFormat.TRANSLUCENT)
+                    : new WindowManager.LayoutParams(
+                            taskBounds.width(),
+                            mDividerWidthPx,
+                            TYPE_APPLICATION_PANEL,
+                            FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL | FLAG_SLIPPERY,
+                            PixelFormat.TRANSLUCENT);
+            lp.setTitle(WINDOW_NAME);
+            mViewHost.setView(mDividerLayout, lp);
+        }
+
+        private void updateDivider() {
+            mDividerLayout.removeAllViews();
+            mDividerLayout.setBackgroundColor(DEFAULT_DIVIDER_COLOR.toArgb());
+            if (mProperties.mDividerAttributes.getDividerType()
+                    == DividerAttributes.DIVIDER_TYPE_DRAGGABLE) {
+                drawDragHandle();
+            }
+            mViewHost.getView().invalidate();
+        }
+
+        private void drawDragHandle() {
+            final Context context = mDividerLayout.getContext();
+            final ImageButton button = new ImageButton(context);
+            final FrameLayout.LayoutParams params = mProperties.mIsVerticalSplit
+                    ? new FrameLayout.LayoutParams(
+                            context.getResources().getDimensionPixelSize(
+                                    R.dimen.activity_embedding_divider_touch_target_width),
+                            context.getResources().getDimensionPixelSize(
+                                    R.dimen.activity_embedding_divider_touch_target_height))
+                    : new FrameLayout.LayoutParams(
+                            context.getResources().getDimensionPixelSize(
+                                    R.dimen.activity_embedding_divider_touch_target_height),
+                            context.getResources().getDimensionPixelSize(
+                                    R.dimen.activity_embedding_divider_touch_target_width));
+            params.gravity = Gravity.CENTER;
+            button.setLayoutParams(params);
+            button.setBackgroundColor(R.color.transparent);
+
+            final Drawable handle =  context.getResources().getDrawable(
+                    R.drawable.activity_embedding_divider_handle, context.getTheme());
+            if (mProperties.mIsVerticalSplit) {
+                button.setImageDrawable(handle);
+            } else {
+                // Rotate the handle drawable
+                RotateDrawable rotatedHandle = new RotateDrawable();
+                rotatedHandle.setFromDegrees(90f);
+                rotatedHandle.setToDegrees(90f);
+                rotatedHandle.setPivotXRelative(true);
+                rotatedHandle.setPivotYRelative(true);
+                rotatedHandle.setPivotX(0.5f);
+                rotatedHandle.setPivotY(0.5f);
+                rotatedHandle.setLevel(1);
+                rotatedHandle.setDrawable(handle);
+
+                button.setImageDrawable(rotatedHandle);
+            }
+            mDividerLayout.addView(button);
+        }
+
+        @NonNull
+        private SurfaceControl createChildSurface(@NonNull String name, boolean visible) {
+            final Rect bounds = mProperties.mConfiguration.windowConfiguration.getBounds();
+            return new SurfaceControl.Builder()
+                    .setParent(mProperties.mDecorSurface)
+                    .setName(name)
+                    .setHidden(!visible)
+                    .setCallsite("DividerManager.createChildSurface")
+                    .setBufferSize(bounds.width(), bounds.height())
+                    .setColorLayer()
+                    .build();
+        }
+    }
 }
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
index 80afb16d..3f4dddf 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
@@ -168,11 +168,14 @@
      * @param fragmentToken token of an existing TaskFragment.
      */
     void expandTaskFragment(@NonNull WindowContainerTransaction wct,
-            @NonNull IBinder fragmentToken) {
+            @NonNull TaskFragmentContainer container) {
+        final IBinder fragmentToken = container.getTaskFragmentToken();
         resizeTaskFragment(wct, fragmentToken, new Rect());
         clearAdjacentTaskFragments(wct, fragmentToken);
         updateWindowingMode(wct, fragmentToken, WINDOWING_MODE_UNDEFINED);
         updateAnimationParams(wct, fragmentToken, TaskFragmentAnimationParams.DEFAULT);
+
+        container.getTaskContainer().updateDivider(wct);
     }
 
     /**
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index 0cc4b1f..1bc8264 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -844,6 +844,7 @@
         // Checks if container should be updated before apply new parentInfo.
         final boolean shouldUpdateContainer = taskContainer.shouldUpdateContainer(parentInfo);
         taskContainer.updateTaskFragmentParentInfo(parentInfo);
+        taskContainer.updateDivider(wct);
 
         // If the last direct activity of the host task is dismissed and the overlay container is
         // the only taskFragment, the overlay container should also be dismissed.
@@ -1224,7 +1225,7 @@
         final TaskFragmentContainer container = getContainerWithActivity(activity);
         if (shouldContainerBeExpanded(container)) {
             // Make sure that the existing container is expanded.
-            mPresenter.expandTaskFragment(wct, container.getTaskFragmentToken());
+            mPresenter.expandTaskFragment(wct, container);
         } else {
             // Put activity into a new expanded container.
             final TaskFragmentContainer newContainer = newContainer(activity, getTaskId(activity));
@@ -1928,7 +1929,7 @@
         }
         if (shouldContainerBeExpanded(container)) {
             if (container.getInfo() != null) {
-                mPresenter.expandTaskFragment(wct, container.getTaskFragmentToken());
+                mPresenter.expandTaskFragment(wct, container);
             }
             // If the info is not available yet the task fragment will be expanded when it's ready
             return;
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
index f680694..20bc820 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -368,6 +368,7 @@
         updateTaskFragmentWindowingModeIfRegistered(wct, secondaryContainer, windowingMode);
         updateAnimationParams(wct, primaryContainer.getTaskFragmentToken(), splitAttributes);
         updateAnimationParams(wct, secondaryContainer.getTaskFragmentToken(), splitAttributes);
+        taskContainer.updateDivider(wct);
     }
 
     private void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct,
@@ -686,8 +687,8 @@
                     splitContainer.getPrimaryContainer().getTaskFragmentToken();
             final IBinder secondaryToken =
                     splitContainer.getSecondaryContainer().getTaskFragmentToken();
-            expandTaskFragment(wct, primaryToken);
-            expandTaskFragment(wct, secondaryToken);
+            expandTaskFragment(wct, splitContainer.getPrimaryContainer());
+            expandTaskFragment(wct, splitContainer.getSecondaryContainer());
             // Set the companion TaskFragment when the two containers stacked.
             setCompanionTaskFragment(wct, primaryToken, secondaryToken,
                     splitContainer.getSplitRule(), true /* isStacked */);
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
index 73109e2..e75a317 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
@@ -77,6 +77,9 @@
 
     private boolean mHasDirectActivity;
 
+    @Nullable
+    private TaskFragmentParentInfo mTaskFragmentParentInfo;
+
     /**
      * TaskFragments that the organizer has requested to be closed. They should be removed when
      * the organizer receives
@@ -85,14 +88,17 @@
      */
     final Set<IBinder> mFinishedContainer = new ArraySet<>();
 
+    // TODO(b/293654166): move DividerPresenter to SplitController.
+    @NonNull
+    final DividerPresenter mDividerPresenter;
+
     /**
      * The {@link TaskContainer} constructor
      *
-     * @param taskId The ID of the Task, which must match {@link Activity#getTaskId()} with
-     *               {@code activityInTask}.
+     * @param taskId         The ID of the Task, which must match {@link Activity#getTaskId()} with
+     *                       {@code activityInTask}.
      * @param activityInTask The {@link Activity} in the Task with {@code taskId}. It is used to
      *                       initialize the {@link TaskContainer} properties.
-     *
      */
     TaskContainer(int taskId, @NonNull Activity activityInTask) {
         if (taskId == INVALID_TASK_ID) {
@@ -107,6 +113,7 @@
         // the host task is visible and has an activity in the task.
         mIsVisible = true;
         mHasDirectActivity = true;
+        mDividerPresenter = new DividerPresenter();
     }
 
     int getTaskId() {
@@ -136,10 +143,12 @@
     }
 
     void updateTaskFragmentParentInfo(@NonNull TaskFragmentParentInfo info) {
+        // TODO(b/293654166): cache the TaskFragmentParentInfo and remove these fields.
         mConfiguration.setTo(info.getConfiguration());
         mDisplayId = info.getDisplayId();
         mIsVisible = info.isVisible();
         mHasDirectActivity = info.hasDirectActivity();
+        mTaskFragmentParentInfo = info;
     }
 
     /**
@@ -161,8 +170,8 @@
      * Returns the windowing mode for the TaskFragments below this Task, which should be split with
      * other TaskFragments.
      *
-     * @param taskFragmentBounds    Requested bounds for the TaskFragment. It will be empty when
-     *                              the pair of TaskFragments are stacked due to the limited space.
+     * @param taskFragmentBounds Requested bounds for the TaskFragment. It will be empty when
+     *                           the pair of TaskFragments are stacked due to the limited space.
      */
     @WindowingMode
     int getWindowingModeForTaskFragment(@Nullable Rect taskFragmentBounds) {
@@ -228,7 +237,7 @@
 
     @Nullable
     TaskFragmentContainer getTopNonFinishingTaskFragmentContainer(boolean includePin,
-                                                                  boolean includeOverlay) {
+            boolean includeOverlay) {
         for (int i = mContainers.size() - 1; i >= 0; i--) {
             final TaskFragmentContainer container = mContainers.get(i);
             if (!includePin && isTaskFragmentContainerPinned(container)) {
@@ -283,7 +292,7 @@
         return mContainers.indexOf(child);
     }
 
-    /** Whether the Task is in an intermediate state waiting for the server update.*/
+    /** Whether the Task is in an intermediate state waiting for the server update. */
     boolean isInIntermediateState() {
         for (TaskFragmentContainer container : mContainers) {
             if (container.isInIntermediateState()) {
@@ -389,6 +398,26 @@
         return mContainers;
     }
 
+    void updateDivider(@NonNull WindowContainerTransaction wct) {
+        if (mTaskFragmentParentInfo != null) {
+            // Update divider only if TaskFragmentParentInfo is available.
+            mDividerPresenter.updateDivider(
+                    wct, mTaskFragmentParentInfo, getTopNonFinishingSplitContainer());
+        }
+    }
+
+    @Nullable
+    private SplitContainer getTopNonFinishingSplitContainer() {
+        for (int i = mSplitContainers.size() - 1; i >= 0; i--) {
+            final SplitContainer splitContainer = mSplitContainers.get(i);
+            if (!splitContainer.getPrimaryContainer().isFinished()
+                    && !splitContainer.getSecondaryContainer().isFinished()) {
+                return splitContainer;
+            }
+        }
+        return null;
+    }
+
     private void onTaskFragmentContainerUpdated() {
         // TODO(b/300211704): Find a better mechanism to handle the z-order in case we introduce
         //  another special container that should also be on top in the future.
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
index a6bf99d..e20a3e0 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
@@ -748,6 +748,10 @@
         }
     }
 
+    @NonNull Rect getLastRequestedBounds() {
+        return mLastRequestedBounds;
+    }
+
     /**
      * Checks if last requested windowing mode is equal to the provided value.
      * @see WindowContainerTransaction#setWindowingMode
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java
index 2a277f4..4d1d807 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java
@@ -16,22 +16,49 @@
 
 package androidx.window.extensions.embedding;
 
+import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE;
+import static android.window.TaskFragmentOperation.OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE;
+
 import static androidx.window.extensions.embedding.DividerPresenter.getBoundsOffsetForDivider;
+import static androidx.window.extensions.embedding.DividerPresenter.getInitialDividerPosition;
 import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_BOTTOM;
 import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_LEFT;
 import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_RIGHT;
 import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_TOP;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.os.Binder;
+import android.os.IBinder;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.view.Display;
+import android.view.SurfaceControl;
+import android.window.TaskFragmentOperation;
+import android.window.TaskFragmentParentInfo;
+import android.window.WindowContainerTransaction;
 
 import androidx.annotation.NonNull;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
+import com.android.window.flags.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;
 
 /**
  * Test class for {@link DividerPresenter}.
@@ -43,6 +70,167 @@
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class DividerPresenterTest {
+    @Rule
+    public final SetFlagsRule mSetFlagRule = new SetFlagsRule();
+
+    @Mock
+    private DividerPresenter.Renderer mRenderer;
+
+    @Mock
+    private WindowContainerTransaction mTransaction;
+
+    @Mock
+    private TaskFragmentParentInfo mParentInfo;
+
+    @Mock
+    private SplitContainer mSplitContainer;
+
+    @Mock
+    private SurfaceControl mSurfaceControl;
+
+    private DividerPresenter mDividerPresenter;
+
+    private final IBinder mPrimaryContainerToken = new Binder();
+
+    private final IBinder mSecondaryContainerToken = new Binder();
+
+    private final IBinder mAnotherContainerToken = new Binder();
+
+    private DividerPresenter.Properties mProperties;
+
+    private static final DividerAttributes DEFAULT_DIVIDER_ATTRIBUTES =
+            new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_DRAGGABLE).build();
+
+    private static final DividerAttributes ANOTHER_DIVIDER_ATTRIBUTES =
+            new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_DRAGGABLE)
+                    .setWidthDp(10).build();
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mSetFlagRule.enableFlags(Flags.FLAG_ACTIVITY_EMBEDDING_INTERACTIVE_DIVIDER_FLAG);
+
+        when(mParentInfo.getDisplayId()).thenReturn(Display.DEFAULT_DISPLAY);
+        when(mParentInfo.getConfiguration()).thenReturn(new Configuration());
+        when(mParentInfo.getDecorSurface()).thenReturn(mSurfaceControl);
+
+        when(mSplitContainer.getCurrentSplitAttributes()).thenReturn(
+                new SplitAttributes.Builder()
+                        .setDividerAttributes(DEFAULT_DIVIDER_ATTRIBUTES)
+                        .build());
+        final TaskFragmentContainer mockPrimaryContainer =
+                createMockTaskFragmentContainer(
+                        mPrimaryContainerToken, new Rect(0, 0, 950, 1000));
+        final TaskFragmentContainer mockSecondaryContainer =
+                createMockTaskFragmentContainer(
+                        mSecondaryContainerToken, new Rect(1000, 0, 2000, 1000));
+        when(mSplitContainer.getPrimaryContainer()).thenReturn(mockPrimaryContainer);
+        when(mSplitContainer.getSecondaryContainer()).thenReturn(mockSecondaryContainer);
+
+        mProperties = new DividerPresenter.Properties(
+                new Configuration(),
+                DEFAULT_DIVIDER_ATTRIBUTES,
+                mSurfaceControl,
+                getInitialDividerPosition(mSplitContainer),
+                true /* isVerticalSplit */,
+                Display.DEFAULT_DISPLAY);
+
+        mDividerPresenter = new DividerPresenter();
+        mDividerPresenter.mProperties = mProperties;
+        mDividerPresenter.mRenderer = mRenderer;
+        mDividerPresenter.mDecorSurfaceOwner = mPrimaryContainerToken;
+    }
+
+    @Test
+    public void testUpdateDivider() {
+        when(mSplitContainer.getCurrentSplitAttributes()).thenReturn(
+                new SplitAttributes.Builder()
+                        .setDividerAttributes(ANOTHER_DIVIDER_ATTRIBUTES)
+                        .build());
+        mDividerPresenter.updateDivider(
+                mTransaction,
+                mParentInfo,
+                mSplitContainer);
+
+        assertNotEquals(mProperties, mDividerPresenter.mProperties);
+        verify(mRenderer).update();
+        verify(mTransaction, never()).addTaskFragmentOperation(any(), any());
+    }
+
+    @Test
+    public void testUpdateDivider_updateDecorSurfaceOwnerIfPrimaryContainerChanged() {
+        final TaskFragmentContainer mockPrimaryContainer =
+                createMockTaskFragmentContainer(
+                        mAnotherContainerToken, new Rect(0, 0, 750, 1000));
+        final TaskFragmentContainer mockSecondaryContainer =
+                createMockTaskFragmentContainer(
+                        mSecondaryContainerToken, new Rect(800, 0, 2000, 1000));
+        when(mSplitContainer.getPrimaryContainer()).thenReturn(mockPrimaryContainer);
+        when(mSplitContainer.getSecondaryContainer()).thenReturn(mockSecondaryContainer);
+        mDividerPresenter.updateDivider(
+                mTransaction,
+                mParentInfo,
+                mSplitContainer);
+
+        assertNotEquals(mProperties, mDividerPresenter.mProperties);
+        verify(mRenderer).update();
+        final TaskFragmentOperation operation = new TaskFragmentOperation.Builder(
+                OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE)
+                .build();
+        assertEquals(mAnotherContainerToken, mDividerPresenter.mDecorSurfaceOwner);
+        verify(mTransaction).addTaskFragmentOperation(mAnotherContainerToken, operation);
+    }
+
+    @Test
+    public void testUpdateDivider_noChangeIfPropertiesIdentical() {
+        mDividerPresenter.updateDivider(
+                mTransaction,
+                mParentInfo,
+                mSplitContainer);
+
+        assertEquals(mProperties, mDividerPresenter.mProperties);
+        verify(mRenderer, never()).update();
+        verify(mTransaction, never()).addTaskFragmentOperation(any(), any());
+    }
+
+    @Test
+    public void testUpdateDivider_dividerRemovedWhenSplitContainerIsNull() {
+        mDividerPresenter.updateDivider(
+                mTransaction,
+                mParentInfo,
+                null /* splitContainer */);
+        final TaskFragmentOperation taskFragmentOperation = new TaskFragmentOperation.Builder(
+                OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE)
+                .build();
+
+        verify(mTransaction).addTaskFragmentOperation(
+                mPrimaryContainerToken, taskFragmentOperation);
+        verify(mRenderer).release();
+        assertNull(mDividerPresenter.mRenderer);
+        assertNull(mDividerPresenter.mProperties);
+        assertNull(mDividerPresenter.mDecorSurfaceOwner);
+    }
+
+    @Test
+    public void testUpdateDivider_dividerRemovedWhenDividerAttributesIsNull() {
+        when(mSplitContainer.getCurrentSplitAttributes()).thenReturn(
+                new SplitAttributes.Builder().setDividerAttributes(null).build());
+        mDividerPresenter.updateDivider(
+                mTransaction,
+                mParentInfo,
+                mSplitContainer);
+        final TaskFragmentOperation taskFragmentOperation = new TaskFragmentOperation.Builder(
+                OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE)
+                .build();
+
+        verify(mTransaction).addTaskFragmentOperation(
+                mPrimaryContainerToken, taskFragmentOperation);
+        verify(mRenderer).release();
+        assertNull(mDividerPresenter.mRenderer);
+        assertNull(mDividerPresenter.mProperties);
+        assertNull(mDividerPresenter.mDecorSurfaceOwner);
+    }
+
     @Test
     public void testSanitizeDividerAttributes_setDefaultValues() {
         DividerAttributes attributes =
@@ -61,7 +249,7 @@
     public void testSanitizeDividerAttributes_notChangingValidValues() {
         DividerAttributes attributes =
                 new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_DRAGGABLE)
-                        .setWidthDp(10)
+                        .setWidthDp(24)
                         .setPrimaryMinRatio(0.3f)
                         .setPrimaryMaxRatio(0.7f)
                         .build();
@@ -123,6 +311,14 @@
                 dividerWidthPx, splitType, expectedTopLeftOffset, expectedBottomRightOffset);
     }
 
+    private TaskFragmentContainer createMockTaskFragmentContainer(
+            @NonNull IBinder token, @NonNull Rect bounds) {
+        final TaskFragmentContainer container = mock(TaskFragmentContainer.class);
+        when(container.getTaskFragmentToken()).thenReturn(token);
+        when(container.getLastRequestedBounds()).thenReturn(bounds);
+        return container;
+    }
+
     private void assertDividerOffsetEquals(
             int dividerWidthPx,
             @NonNull SplitAttributes.SplitType splitType,
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java
index dd087e8..6f37e9c 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java
@@ -107,7 +107,7 @@
         mOrganizer.mFragmentInfos.put(container.getTaskFragmentToken(), info);
         container.setInfo(mTransaction, info);
 
-        mOrganizer.expandTaskFragment(mTransaction, container.getTaskFragmentToken());
+        mOrganizer.expandTaskFragment(mTransaction, container);
 
         verify(mTransaction).setWindowingMode(container.getInfo().getToken(),
                 WINDOWING_MODE_UNDEFINED);
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
index cdb37ac..c246a19 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
@@ -642,7 +642,7 @@
                 false /* isOnReparent */);
 
         assertTrue(result);
-        verify(mSplitPresenter).expandTaskFragment(mTransaction, container.getTaskFragmentToken());
+        verify(mSplitPresenter).expandTaskFragment(mTransaction, container);
     }
 
     @Test
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
index 941b4e1..62d8aa3 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
@@ -665,8 +665,8 @@
 
         assertEquals(RESULT_EXPANDED, mPresenter.expandSplitContainerIfNeeded(mTransaction,
                 splitContainer, mActivity, secondaryActivity, null /* secondaryIntent */));
-        verify(mPresenter).expandTaskFragment(mTransaction, primaryTf.getTaskFragmentToken());
-        verify(mPresenter).expandTaskFragment(mTransaction, secondaryTf.getTaskFragmentToken());
+        verify(mPresenter).expandTaskFragment(mTransaction, primaryTf);
+        verify(mPresenter).expandTaskFragment(mTransaction, secondaryTf);
 
         splitContainer.updateCurrentSplitAttributes(SPLIT_ATTRIBUTES);
         clearInvocations(mPresenter);
@@ -675,8 +675,8 @@
                 splitContainer, mActivity, null /* secondaryActivity */,
                 new Intent(ApplicationProvider.getApplicationContext(),
                         MinimumDimensionActivity.class)));
-        verify(mPresenter).expandTaskFragment(mTransaction, primaryTf.getTaskFragmentToken());
-        verify(mPresenter).expandTaskFragment(mTransaction, secondaryTf.getTaskFragmentToken());
+        verify(mPresenter).expandTaskFragment(mTransaction, primaryTf);
+        verify(mPresenter).expandTaskFragment(mTransaction, secondaryTf);
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/multivalentTests/Android.bp b/libs/WindowManager/Shell/multivalentTests/Android.bp
index 1686d0d..1ad19c9 100644
--- a/libs/WindowManager/Shell/multivalentTests/Android.bp
+++ b/libs/WindowManager/Shell/multivalentTests/Android.bp
@@ -46,6 +46,7 @@
     exclude_srcs: ["src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt"],
     static_libs: [
         "junit",
+        "androidx.core_core-animation-testing",
         "androidx.test.runner",
         "androidx.test.rules",
         "androidx.test.ext.junit",
@@ -64,6 +65,7 @@
     static_libs: [
         "WindowManager-Shell",
         "junit",
+        "androidx.core_core-animation-testing",
         "androidx.test.runner",
         "androidx.test.rules",
         "androidx.test.ext.junit",
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarDropTargetControllerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarDropTargetControllerTest.kt
new file mode 100644
index 0000000..2ac7791
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarDropTargetControllerTest.kt
@@ -0,0 +1,180 @@
+/*
+ * 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.bar
+
+import android.content.Context
+import android.graphics.Insets
+import android.graphics.Rect
+import android.view.View
+import android.view.WindowManager
+import android.widget.FrameLayout
+import androidx.core.animation.AnimatorTestRule
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.internal.protolog.common.ProtoLog
+import com.android.wm.shell.R
+import com.android.wm.shell.bubbles.BubblePositioner
+import com.android.wm.shell.bubbles.DeviceConfig
+import com.android.wm.shell.bubbles.bar.BubbleBarDropTargetController.Companion.DROP_TARGET_ALPHA_IN_DURATION
+import com.android.wm.shell.bubbles.bar.BubbleBarDropTargetController.Companion.DROP_TARGET_ALPHA_OUT_DURATION
+import com.android.wm.shell.bubbles.bar.BubbleBarDropTargetController.Companion.DROP_TARGET_SCALE
+import com.android.wm.shell.common.bubbles.BubbleBarLocation
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.ClassRule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Tests for [BubbleBarDropTargetController] */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class BubbleBarDropTargetControllerTest {
+
+    companion object {
+        @JvmField @ClassRule val animatorTestRule: AnimatorTestRule = AnimatorTestRule()
+    }
+
+    private val context = ApplicationProvider.getApplicationContext<Context>()
+    private lateinit var controller: BubbleBarDropTargetController
+    private lateinit var positioner: BubblePositioner
+    private lateinit var container: FrameLayout
+
+    @Before
+    fun setUp() {
+        ProtoLog.REQUIRE_PROTOLOGTOOL = false
+        container = FrameLayout(context)
+        val windowManager = context.getSystemService(WindowManager::class.java)
+        positioner = BubblePositioner(context, windowManager)
+        positioner.setShowingInBubbleBar(true)
+        val deviceConfig =
+            DeviceConfig(
+                windowBounds = Rect(0, 0, 2000, 2600),
+                isLargeScreen = true,
+                isSmallTablet = false,
+                isLandscape = true,
+                isRtl = false,
+                insets = Insets.of(10, 20, 30, 40)
+            )
+        positioner.update(deviceConfig)
+        positioner.bubbleBarBounds = Rect(1800, 2400, 1970, 2560)
+
+        controller = BubbleBarDropTargetController(context, container, positioner)
+    }
+
+    @Test
+    fun show_moveLeftToRight_isVisibleWithExpectedBounds() {
+        val expectedBoundsOnLeft = getExpectedDropTargetBounds(onLeft = true)
+        val expectedBoundsOnRight = getExpectedDropTargetBounds(onLeft = false)
+
+        runOnMainSync { controller.show(BubbleBarLocation.LEFT) }
+        waitForAnimateIn()
+        val viewOnLeft = getDropTargetView()
+        assertThat(viewOnLeft).isNotNull()
+        assertThat(viewOnLeft!!.alpha).isEqualTo(1f)
+        assertThat(viewOnLeft.layoutParams.width).isEqualTo(expectedBoundsOnLeft.width())
+        assertThat(viewOnLeft.layoutParams.height).isEqualTo(expectedBoundsOnLeft.height())
+        assertThat(viewOnLeft.x).isEqualTo(expectedBoundsOnLeft.left)
+        assertThat(viewOnLeft.y).isEqualTo(expectedBoundsOnLeft.top)
+
+        runOnMainSync { controller.show(BubbleBarLocation.RIGHT) }
+        waitForAnimateOut()
+        waitForAnimateIn()
+        val viewOnRight = getDropTargetView()
+        assertThat(viewOnRight).isNotNull()
+        assertThat(viewOnRight!!.alpha).isEqualTo(1f)
+        assertThat(viewOnRight.layoutParams.width).isEqualTo(expectedBoundsOnRight.width())
+        assertThat(viewOnRight.layoutParams.height).isEqualTo(expectedBoundsOnRight.height())
+        assertThat(viewOnRight.x).isEqualTo(expectedBoundsOnRight.left)
+        assertThat(viewOnRight.y).isEqualTo(expectedBoundsOnRight.top)
+    }
+
+    @Test
+    fun toggleSetHidden_dropTargetShown_updatesAlpha() {
+        runOnMainSync { controller.show(BubbleBarLocation.RIGHT) }
+        waitForAnimateIn()
+        val view = getDropTargetView()
+        assertThat(view).isNotNull()
+        assertThat(view!!.alpha).isEqualTo(1f)
+
+        runOnMainSync { controller.setHidden(true) }
+        waitForAnimateOut()
+        val hiddenView = getDropTargetView()
+        assertThat(hiddenView).isNotNull()
+        assertThat(hiddenView!!.alpha).isEqualTo(0f)
+
+        runOnMainSync { controller.setHidden(false) }
+        waitForAnimateIn()
+        val shownView = getDropTargetView()
+        assertThat(shownView).isNotNull()
+        assertThat(shownView!!.alpha).isEqualTo(1f)
+    }
+
+    @Test
+    fun toggleSetHidden_dropTargetNotShown_viewNotCreated() {
+        runOnMainSync { controller.setHidden(true) }
+        waitForAnimateOut()
+        assertThat(getDropTargetView()).isNull()
+        runOnMainSync { controller.setHidden(false) }
+        waitForAnimateIn()
+        assertThat(getDropTargetView()).isNull()
+    }
+
+    @Test
+    fun dismiss_dropTargetShown_viewRemoved() {
+        runOnMainSync { controller.show(BubbleBarLocation.LEFT) }
+        waitForAnimateIn()
+        assertThat(getDropTargetView()).isNotNull()
+        runOnMainSync { controller.dismiss() }
+        waitForAnimateOut()
+        assertThat(getDropTargetView()).isNull()
+    }
+
+    @Test
+    fun dismiss_dropTargetNotShown_doesNothing() {
+        runOnMainSync { controller.dismiss() }
+        waitForAnimateOut()
+        assertThat(getDropTargetView()).isNull()
+    }
+
+    private fun getDropTargetView(): View? = container.findViewById(R.id.bubble_bar_drop_target)
+
+    private fun getExpectedDropTargetBounds(onLeft: Boolean): Rect {
+        val rect = Rect()
+        positioner.getBubbleBarExpandedViewBounds(onLeft, false /* isOveflowExpanded */, rect)
+        // Scale the rect to expected size, but keep the center point the same
+        val centerX = rect.centerX()
+        val centerY = rect.centerY()
+        rect.scale(DROP_TARGET_SCALE)
+        rect.offset(centerX - rect.centerX(), centerY - rect.centerY())
+        return rect
+    }
+
+    private fun runOnMainSync(runnable: Runnable) {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable)
+    }
+
+    private fun waitForAnimateIn() {
+        // Advance animator for on-device test
+        runOnMainSync { animatorTestRule.advanceTimeBy(DROP_TARGET_ALPHA_IN_DURATION) }
+    }
+
+    private fun waitForAnimateOut() {
+        // Advance animator for on-device test
+        runOnMainSync { animatorTestRule.advanceTimeBy(DROP_TARGET_ALPHA_OUT_DURATION) }
+    }
+}
diff --git a/libs/WindowManager/Shell/res/color/bubble_drop_target_background_color.xml b/libs/WindowManager/Shell/res/color/bubble_drop_target_background_color.xml
new file mode 100644
index 0000000..ab1ab98
--- /dev/null
+++ b/libs/WindowManager/Shell/res/color/bubble_drop_target_background_color.xml
@@ -0,0 +1,20 @@
+<?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.
+  -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+    <item android:alpha="0.35" android:color="?androidprv:attr/materialColorPrimaryContainer" />
+</selector>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/bubble_drop_target_background.xml b/libs/WindowManager/Shell/res/drawable/bubble_drop_target_background.xml
index 468b5c2..9dcde3b 100644
--- a/libs/WindowManager/Shell/res/drawable/bubble_drop_target_background.xml
+++ b/libs/WindowManager/Shell/res/drawable/bubble_drop_target_background.xml
@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
+<?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");
@@ -14,9 +13,12 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<shape android:shape="rectangle"
-       xmlns:android="http://schemas.android.com/apk/res/android">
-    <solid android:color="#bf309fb5" />
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    android:shape="rectangle">
     <corners android:radius="@dimen/bubble_bar_expanded_view_corner_radius" />
-    <stroke android:width="1dp" android:color="#A00080FF"/>
+    <solid android:color="@color/bubble_drop_target_background_color" />
+    <stroke
+        android:width="1dp"
+        android:color="?androidprv:attr/materialColorPrimaryContainer" />
 </shape>
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 43ce166..c032a81 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -213,7 +213,7 @@
     <dimen name="bubble_swap_animation_offset">15dp</dimen>
     <!-- How far offscreen the bubble stack rests. There's some padding around the bubble so
          add 3dp to the desired overhang. -->
-    <dimen name="bubble_stack_offscreen">3dp</dimen>
+    <dimen name="bubble_stack_offscreen">2.5dp</dimen>
     <!-- How far down the screen the stack starts. -->
     <dimen name="bubble_stack_starting_offset_y">120dp</dimen>
     <!-- Space between the pointer triangle and the bubble expanded view -->
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDropTargetController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDropTargetController.kt
index 55ec6cd..f6b4653 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDropTargetController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDropTargetController.kt
@@ -21,6 +21,10 @@
 import android.view.View
 import android.widget.FrameLayout
 import android.widget.FrameLayout.LayoutParams
+import androidx.annotation.VisibleForTesting
+import androidx.core.animation.Animator
+import androidx.core.animation.AnimatorListenerAdapter
+import androidx.core.animation.ObjectAnimator
 import com.android.wm.shell.R
 import com.android.wm.shell.bubbles.BubblePositioner
 import com.android.wm.shell.common.bubbles.BubbleBarLocation
@@ -33,6 +37,7 @@
 ) {
 
     private var dropTargetView: View? = null
+    private var animator: ObjectAnimator? = null
     private val tempRect: Rect by lazy(LazyThreadSafetyMode.NONE) { Rect() }
 
     /**
@@ -57,7 +62,8 @@
     /**
      * Set the view hidden or not
      *
-     * Requires the drop target to be first shown by calling [show]. Otherwise does not do anything.
+     * Requires the drop target to be first shown by calling [animateIn]. Otherwise does not do
+     * anything.
      */
     fun setHidden(hidden: Boolean) {
         val targetView = dropTargetView ?: return
@@ -106,20 +112,40 @@
     }
 
     private fun View.animateIn() {
-        animate().alpha(1f).setDuration(DROP_TARGET_ALPHA_IN_DURATION).start()
+        animator?.cancel()
+        animator =
+            ObjectAnimator.ofFloat(this, View.ALPHA, 1f)
+                .setDuration(DROP_TARGET_ALPHA_IN_DURATION)
+                .addEndAction { animator = null }
+        animator?.start()
     }
 
     private fun View.animateOut(endAction: Runnable? = null) {
-        animate()
-            .alpha(0f)
-            .setDuration(DROP_TARGET_ALPHA_OUT_DURATION)
-            .withEndAction(endAction)
-            .start()
+        animator?.cancel()
+        animator =
+            ObjectAnimator.ofFloat(this, View.ALPHA, 0f)
+                .setDuration(DROP_TARGET_ALPHA_OUT_DURATION)
+                .addEndAction {
+                    endAction?.run()
+                    animator = null
+                }
+        animator?.start()
+    }
+
+    private fun <T : Animator> T.addEndAction(runnable: Runnable): T {
+        addListener(
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationEnd(animation: Animator) {
+                    runnable.run()
+                }
+            }
+        )
+        return this
     }
 
     companion object {
-        private const val DROP_TARGET_ALPHA_IN_DURATION = 150L
-        private const val DROP_TARGET_ALPHA_OUT_DURATION = 100L
-        private const val DROP_TARGET_SCALE = 0.9f
+        @VisibleForTesting const val DROP_TARGET_ALPHA_IN_DURATION = 150L
+        @VisibleForTesting const val DROP_TARGET_ALPHA_OUT_DURATION = 100L
+        @VisibleForTesting const val DROP_TARGET_SCALE = 0.9f
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
index e4cf6d1..98dccbb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
@@ -48,6 +48,7 @@
 import android.view.WindowManager;
 import android.view.WindowlessWindowManager;
 import android.view.inputmethod.ImeTracker;
+import android.window.ActivityWindowInfo;
 import android.window.ClientWindowFrames;
 import android.window.InputTransferToken;
 
@@ -348,7 +349,7 @@
         public void resized(ClientWindowFrames frames, boolean reportDraw,
                 MergedConfiguration newMergedConfiguration, InsetsState insetsState,
                 boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId, int syncSeqId,
-                boolean dragResizing) {}
+                boolean dragResizing, @Nullable ActivityWindowInfo activityWindowInfo) {}
 
         @Override
         public void insetsControlChanged(InsetsState insetsState,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
index 838603f..5889da1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
@@ -49,7 +49,7 @@
 
 
     /** Called when requested to go to desktop mode from the current focused app. */
-    void enterDesktop(int displayId);
+    void moveFocusedTaskToDesktop(int displayId);
 
     /** Called when requested to go to fullscreen from the current focused desktop app. */
     void moveFocusedTaskToFullscreen(int displayId);
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 992e5ae..cdef4fd 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
@@ -263,7 +263,7 @@
     }
 
     /** Enter desktop by using the focused task in given `displayId` */
-    fun enterDesktop(displayId: Int) {
+    fun moveFocusedTaskToDesktop(displayId: Int) {
         val allFocusedTasks =
             shellTaskOrganizer.getRunningTasks(displayId).filter { taskInfo ->
                 taskInfo.isFocused &&
@@ -1212,9 +1212,9 @@
             }
         }
 
-        override fun enterDesktop(displayId: Int) {
+        override fun moveFocusedTaskToDesktop(displayId: Int) {
             mainExecutor.execute {
-                this@DesktopTasksController.enterDesktop(displayId)
+                this@DesktopTasksController.moveFocusedTaskToDesktop(displayId)
             }
         }
 
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 af26e29..b830a41 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
@@ -15,6 +15,7 @@
 import android.content.Intent
 import android.content.Intent.FILL_IN_COMPONENT
 import android.graphics.Rect
+import android.os.Bundle
 import android.os.IBinder
 import android.os.SystemClock
 import android.view.SurfaceControl
@@ -124,7 +125,7 @@
                 options.toBundle()
         )
         val wct = WindowContainerTransaction()
-        wct.sendPendingIntent(pendingIntent, launchHomeIntent, options.toBundle())
+        wct.sendPendingIntent(pendingIntent, launchHomeIntent, Bundle())
         val startTransitionToken = transitions
                 .startTransition(TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, wct, this)
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java
index 1a0c011..ceac40d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java
@@ -23,6 +23,7 @@
 
 import android.annotation.BinderThread;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.ActivityManager.TaskDescription;
 import android.graphics.Paint;
@@ -42,6 +43,7 @@
 import android.view.View;
 import android.view.WindowManager;
 import android.view.WindowManagerGlobal;
+import android.window.ActivityWindowInfo;
 import android.window.ClientWindowFrames;
 import android.window.SnapshotDrawerUtils;
 import android.window.StartingWindowInfo;
@@ -214,7 +216,7 @@
         public void resized(ClientWindowFrames frames, boolean reportDraw,
                 MergedConfiguration mergedConfiguration, InsetsState insetsState,
                 boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId, int seqId,
-                boolean dragResizing) {
+                boolean dragResizing, @Nullable ActivityWindowInfo activityWindowInfo) {
             final TaskSnapshotWindow snapshot = mOuter.get();
             if (snapshot == null) {
                 return;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index 9130edf..74e85f8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -334,6 +334,7 @@
         boolean isDisplayRotationAnimationStarted = false;
         final boolean isDreamTransition = isDreamTransition(info);
         final boolean isOnlyTranslucent = isOnlyTranslucent(info);
+        final boolean isActivityLevel = isActivityLevelOnly(info);
 
         for (int i = info.getChanges().size() - 1; i >= 0; --i) {
             final TransitionInfo.Change change = info.getChanges().get(i);
@@ -502,8 +503,35 @@
                         : new Rect(change.getEndAbsBounds());
                 clipRect.offsetTo(0, 0);
 
+                final TransitionInfo.Root animRoot = TransitionUtil.getRootFor(change, info);
+                final Point animRelOffset = new Point(
+                        change.getEndAbsBounds().left - animRoot.getOffset().x,
+                        change.getEndAbsBounds().top - animRoot.getOffset().y);
+                if (change.getActivityComponent() != null && !isActivityLevel) {
+                    // At this point, this is an independent activity change in a non-activity
+                    // transition. This means that an activity transition got erroneously combined
+                    // with another ongoing transition. This then means that the animation root may
+                    // not tightly fit the activities, so we have to put them in a separate crop.
+                    final int layer = Transitions.calculateAnimLayer(change, i,
+                            info.getChanges().size(), info.getType());
+                    final SurfaceControl leash = new SurfaceControl.Builder()
+                            .setName("Transition ActivityWrap: "
+                                    + change.getActivityComponent().toShortString())
+                            .setParent(animRoot.getLeash())
+                            .setContainerLayer().build();
+                    startTransaction.setCrop(leash, clipRect);
+                    startTransaction.setPosition(leash, animRelOffset.x, animRelOffset.y);
+                    startTransaction.setLayer(leash, layer);
+                    startTransaction.show(leash);
+                    startTransaction.reparent(change.getLeash(), leash);
+                    startTransaction.setPosition(change.getLeash(), 0, 0);
+                    animRelOffset.set(0, 0);
+                    finishTransaction.reparent(leash, null);
+                    leash.release();
+                }
+
                 buildSurfaceAnimation(animations, a, change.getLeash(), onAnimFinish,
-                        mTransactionPool, mMainExecutor, change.getEndRelOffset(), cornerRadius,
+                        mTransactionPool, mMainExecutor, animRelOffset, cornerRadius,
                         clipRect);
 
                 if (info.getAnimationOptions() != null) {
@@ -612,6 +640,18 @@
         return (translucentOpen + translucentClose) > 0;
     }
 
+    /**
+     * Does `info` only contain activity-level changes? This kinda assumes that if so, they are
+     * all in one task.
+     */
+    private static boolean isActivityLevelOnly(@NonNull TransitionInfo info) {
+        for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+            final TransitionInfo.Change change = info.getChanges().get(i);
+            if (change.getActivityComponent() == null) return false;
+        }
+        return true;
+    }
+
     @Override
     public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
             @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index ccd0b2d..a77602b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -31,7 +31,6 @@
 import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED;
 import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW;
 import static android.window.TransitionInfo.FLAG_IS_OCCLUDED;
-import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
 import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP;
 import static android.window.TransitionInfo.FLAG_NO_ANIMATION;
 import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
@@ -496,6 +495,7 @@
             if (mode == TRANSIT_TO_FRONT) {
                 // When the window is moved to front, make sure the crop is updated to prevent it
                 // from using the old crop.
+                t.setPosition(leash, change.getEndRelOffset().x, change.getEndRelOffset().y);
                 t.setWindowCrop(leash, change.getEndAbsBounds().width(),
                         change.getEndAbsBounds().height());
             }
@@ -507,6 +507,8 @@
                     t.setMatrix(leash, 1, 0, 0, 1);
                     t.setAlpha(leash, 1.f);
                     t.setPosition(leash, change.getEndRelOffset().x, change.getEndRelOffset().y);
+                    t.setWindowCrop(leash, change.getEndAbsBounds().width(),
+                            change.getEndAbsBounds().height());
                 }
                 continue;
             }
@@ -530,6 +532,44 @@
         }
     }
 
+    static int calculateAnimLayer(@NonNull TransitionInfo.Change change, int i,
+            int numChanges, @WindowManager.TransitionType int transitType) {
+        // Put animating stuff above this line and put static stuff below it.
+        final int zSplitLine = numChanges + 1;
+        final boolean isOpening = isOpeningType(transitType);
+        final boolean isClosing = isClosingType(transitType);
+        final int mode = change.getMode();
+        // Put all the OPEN/SHOW on top
+        if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) {
+            if (isOpening
+                    // This is for when an activity launches while a different transition is
+                    // collecting.
+                    || change.hasFlags(FLAG_MOVED_TO_TOP)) {
+                // put on top
+                return zSplitLine + numChanges - i;
+            } else {
+                // put on bottom
+                return zSplitLine - i;
+            }
+        } else if (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK) {
+            if (isOpening) {
+                // put on bottom and leave visible
+                return zSplitLine - i;
+            } else {
+                // put on top
+                return zSplitLine + numChanges - i;
+            }
+        } else { // CHANGE or other
+            if (isClosing || TransitionUtil.isOrderOnly(change)) {
+                // Put below CLOSE mode (in the "static" section).
+                return zSplitLine - i;
+            } else {
+                // Put above CLOSE mode.
+                return zSplitLine + numChanges - i;
+            }
+        }
+    }
+
     /**
      * Reparents all participants into a shared parent and orders them based on: the global transit
      * type, their transit mode, and their destination z-order.
@@ -537,19 +577,14 @@
     private static void setupAnimHierarchy(@NonNull TransitionInfo info,
             @NonNull SurfaceControl.Transaction t, @NonNull SurfaceControl.Transaction finishT) {
         final int type = info.getType();
-        final boolean isOpening = isOpeningType(type);
-        final boolean isClosing = isClosingType(type);
         for (int i = 0; i < info.getRootCount(); ++i) {
             t.show(info.getRoot(i).getLeash());
         }
         final int numChanges = info.getChanges().size();
-        // Put animating stuff above this line and put static stuff below it.
-        final int zSplitLine = numChanges + 1;
         // changes should be ordered top-to-bottom in z
         for (int i = numChanges - 1; i >= 0; --i) {
             final TransitionInfo.Change change = info.getChanges().get(i);
             final SurfaceControl leash = change.getLeash();
-            final int mode = change.getMode();
 
             // Don't reparent anything that isn't independent within its parents
             if (!TransitionInfo.isIndependent(change, info)) {
@@ -558,50 +593,14 @@
 
             boolean hasParent = change.getParent() != null;
 
-            final int rootIdx = TransitionUtil.rootIndexFor(change, info);
+            final TransitionInfo.Root root = TransitionUtil.getRootFor(change, info);
             if (!hasParent) {
-                t.reparent(leash, info.getRoot(rootIdx).getLeash());
+                t.reparent(leash, root.getLeash());
                 t.setPosition(leash,
-                        change.getStartAbsBounds().left - info.getRoot(rootIdx).getOffset().x,
-                        change.getStartAbsBounds().top - info.getRoot(rootIdx).getOffset().y);
+                        change.getStartAbsBounds().left - root.getOffset().x,
+                        change.getStartAbsBounds().top - root.getOffset().y);
             }
-            final int layer;
-            // Put all the OPEN/SHOW on top
-            if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) {
-                // Wallpaper is always at the bottom, opening wallpaper on top of closing one.
-                if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) {
-                    layer = -zSplitLine + numChanges - i;
-                } else {
-                    layer = -zSplitLine - i;
-                }
-            } else if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) {
-                if (isOpening
-                        // This is for when an activity launches while a different transition is
-                        // collecting.
-                        || change.hasFlags(FLAG_MOVED_TO_TOP)) {
-                    // put on top
-                    layer = zSplitLine + numChanges - i;
-                } else {
-                    // put on bottom
-                    layer = zSplitLine - i;
-                }
-            } else if (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK) {
-                if (isOpening) {
-                    // put on bottom and leave visible
-                    layer = zSplitLine - i;
-                } else {
-                    // put on top
-                    layer = zSplitLine + numChanges - i;
-                }
-            } else { // CHANGE or other
-                if (isClosing || TransitionUtil.isOrderOnly(change)) {
-                    // Put below CLOSE mode (in the "static" section).
-                    layer = zSplitLine - i;
-                } else {
-                    // Put above CLOSE mode.
-                    layer = zSplitLine + numChanges - i;
-                }
-            }
+            final int layer = calculateAnimLayer(change, i, numChanges, type);
             t.setLayer(leash, layer);
         }
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
index 6f8b3d5..76096b0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
@@ -18,6 +18,7 @@
 
 import static android.view.WindowManager.TRANSIT_CHANGE;
 
+import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.os.IBinder;
@@ -178,10 +179,11 @@
         for (TransitionInfo.Change change: info.getChanges()) {
             final SurfaceControl sc = change.getLeash();
             final Rect endBounds = change.getEndAbsBounds();
+            final Point endPosition = change.getEndRelOffset();
             startTransaction.setWindowCrop(sc, endBounds.width(), endBounds.height())
-                    .setPosition(sc, endBounds.left, endBounds.top);
+                    .setPosition(sc, endPosition.x, endPosition.y);
             finishTransaction.setWindowCrop(sc, endBounds.width(), endBounds.height())
-                    .setPosition(sc, endBounds.left, endBounds.top);
+                    .setPosition(sc, endPosition.x, endPosition.y);
         }
 
         startTransaction.apply();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
index c12a93e..5fce5d2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
@@ -18,6 +18,7 @@
 
 import static android.view.WindowManager.TRANSIT_CHANGE;
 
+import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.os.IBinder;
@@ -179,10 +180,11 @@
         for (TransitionInfo.Change change: info.getChanges()) {
             final SurfaceControl sc = change.getLeash();
             final Rect endBounds = change.getEndAbsBounds();
+            final Point endPosition = change.getEndRelOffset();
             startTransaction.setWindowCrop(sc, endBounds.width(), endBounds.height())
-                    .setPosition(sc, endBounds.left, endBounds.top);
+                    .setPosition(sc, endPosition.x, endPosition.y);
             finishTransaction.setWindowCrop(sc, endBounds.width(), endBounds.height())
-                    .setPosition(sc, endBounds.left, endBounds.top);
+                    .setPosition(sc, endPosition.x, endPosition.y);
         }
 
         startTransaction.apply();
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt
index 1ccc7d8..5f25d70 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt
@@ -24,6 +24,7 @@
 import android.tools.flicker.legacy.LegacyFlickerTestFactory
 import android.tools.helpers.WindowUtils
 import android.tools.traces.parsers.toFlickerComponent
+import androidx.test.filters.FlakyTest
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
 import com.android.server.wm.flicker.testapp.ActivityOptions
@@ -181,6 +182,12 @@
         }
     }
 
+    /** {@inheritDoc} */
+    @FlakyTest(bugId = 312446524)
+    @Test
+    override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+        super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index 254bf7d..4fbf2bd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -833,7 +833,7 @@
         verify(launchAdjacentController).launchAdjacentEnabled = true
     }
     @Test
-    fun enterDesktop_fullscreenTaskIsMovedToDesktop() {
+    fun moveFocusedTaskToDesktop_fullscreenTaskIsMovedToDesktop() {
         val task1 = setUpFullscreenTask()
         val task2 = setUpFullscreenTask()
         val task3 = setUpFullscreenTask()
@@ -842,7 +842,7 @@
         task2.isFocused = false
         task3.isFocused = false
 
-        controller.enterDesktop(DEFAULT_DISPLAY)
+        controller.moveFocusedTaskToDesktop(DEFAULT_DISPLAY)
 
         val wct = getLatestMoveToDesktopWct()
         assertThat(wct.changes[task1.token.asBinder()]?.windowingMode)
@@ -850,7 +850,7 @@
     }
 
     @Test
-    fun enterDesktop_splitScreenTaskIsMovedToDesktop() {
+    fun moveFocusedTaskToDesktop_splitScreenTaskIsMovedToDesktop() {
         val task1 = setUpSplitScreenTask()
         val task2 = setUpFullscreenTask()
         val task3 = setUpFullscreenTask()
@@ -863,7 +863,7 @@
 
         task4.parentTaskId = task1.taskId
 
-        controller.enterDesktop(DEFAULT_DISPLAY)
+        controller.moveFocusedTaskToDesktop(DEFAULT_DISPLAY)
 
         val wct = getLatestMoveToDesktopWct()
         assertThat(wct.changes[task4.token.asBinder()]?.windowingMode)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
index ce7b633..9174556 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
@@ -2,6 +2,7 @@
 
 import android.app.ActivityManager
 import android.app.WindowConfiguration
+import android.graphics.Point
 import android.graphics.Rect
 import android.os.IBinder
 import android.testing.AndroidTestingRunner
@@ -11,6 +12,7 @@
 import android.view.Surface.ROTATION_90
 import android.view.SurfaceControl
 import android.view.WindowManager
+import android.window.TransitionInfo
 import android.window.WindowContainerToken
 import android.window.WindowContainerTransaction
 import android.window.WindowContainerTransaction.Change.CHANGE_DRAG_RESIZING
@@ -41,6 +43,8 @@
 import org.mockito.MockitoAnnotations
 import org.mockito.kotlin.doReturn
 import java.util.function.Supplier
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.mock
 import org.mockito.Mockito.`when` as whenever
 
 /**
@@ -575,6 +579,32 @@
         })
     }
 
+    @Test
+    fun testStartAnimation_useEndRelOffset() {
+        val mockTransitionInfo = mock(TransitionInfo::class.java)
+        val changeMock = mock(TransitionInfo.Change::class.java)
+        val startTransaction = mock(SurfaceControl.Transaction::class.java)
+        val finishTransaction = mock(SurfaceControl.Transaction::class.java)
+        val point = Point(10, 20)
+        val bounds = Rect(1, 2, 3, 4)
+        `when`(changeMock.endRelOffset).thenReturn(point)
+        `when`(changeMock.endAbsBounds).thenReturn(bounds)
+        `when`(mockTransitionInfo.changes).thenReturn(listOf(changeMock))
+        `when`(startTransaction.setWindowCrop(any(),
+            eq(bounds.width()),
+            eq(bounds.height()))).thenReturn(startTransaction)
+        `when`(finishTransaction.setWindowCrop(any(),
+            eq(bounds.width()),
+            eq(bounds.height()))).thenReturn(finishTransaction)
+
+        taskPositioner.startAnimation(mockTransitionBinder, mockTransitionInfo, startTransaction,
+            finishTransaction, { _ -> })
+
+        verify(startTransaction).setPosition(any(), eq(point.x.toFloat()), eq(point.y.toFloat()))
+        verify(finishTransaction).setPosition(any(), eq(point.x.toFloat()), eq(point.y.toFloat()))
+        verify(changeMock).endRelOffset
+    }
+
     private fun WindowContainerTransaction.Change.ofBounds(bounds: Rect): Boolean {
         return ((windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0) &&
                 bounds == configuration.windowConfiguration.bounds
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
index 7f6e538..a9f4492 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
@@ -17,6 +17,7 @@
 
 import android.app.ActivityManager
 import android.app.WindowConfiguration
+import android.graphics.Point
 import android.graphics.Rect
 import android.os.IBinder
 import android.testing.AndroidTestingRunner
@@ -25,6 +26,7 @@
 import android.view.Surface.ROTATION_270
 import android.view.Surface.ROTATION_90
 import android.view.SurfaceControl
+import android.view.SurfaceControl.Transaction
 import android.view.WindowManager.TRANSIT_CHANGE
 import android.window.TransitionInfo
 import android.window.WindowContainerToken
@@ -39,6 +41,7 @@
 import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT
 import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP
 import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_UNDEFINED
+import java.util.function.Supplier
 import junit.framework.Assert
 import org.junit.Before
 import org.junit.Test
@@ -47,13 +50,13 @@
 import org.mockito.Mockito.any
 import org.mockito.Mockito.argThat
 import org.mockito.Mockito.eq
+import org.mockito.Mockito.mock
 import org.mockito.Mockito.never
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
-import org.mockito.MockitoAnnotations
-import java.util.function.Supplier
 import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
 
 /**
  * Tests for [VeiledResizeTaskPositioner].
@@ -439,6 +442,40 @@
         Assert.assertFalse(taskPositioner.isResizingOrAnimating)
     }
 
+    @Test
+    fun testStartAnimation_useEndRelOffset() {
+        val changeMock = mock(TransitionInfo.Change::class.java)
+        val startTransaction = mock(Transaction::class.java)
+        val finishTransaction = mock(Transaction::class.java)
+        val point = Point(10, 20)
+        val bounds = Rect(1, 2, 3, 4)
+        `when`(changeMock.endRelOffset).thenReturn(point)
+        `when`(changeMock.endAbsBounds).thenReturn(bounds)
+        `when`(mockTransitionInfo.changes).thenReturn(listOf(changeMock))
+        `when`(startTransaction.setWindowCrop(
+            any(),
+            eq(bounds.width()),
+            eq(bounds.height())
+        )).thenReturn(startTransaction)
+        `when`(finishTransaction.setWindowCrop(
+            any(),
+            eq(bounds.width()),
+            eq(bounds.height())
+        )).thenReturn(finishTransaction)
+
+        taskPositioner.startAnimation(
+            mockTransitionBinder,
+            mockTransitionInfo,
+            startTransaction,
+            finishTransaction,
+            mockFinishCallback
+        )
+
+        verify(startTransaction).setPosition(any(), eq(point.x.toFloat()), eq(point.y.toFloat()))
+        verify(finishTransaction).setPosition(any(), eq(point.x.toFloat()), eq(point.y.toFloat()))
+        verify(changeMock).endRelOffset
+    }
+
     private fun performDrag(
         startX: Float,
         startY: Float,
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 6f7024a..1fe3c2e 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -5453,7 +5453,8 @@
             String regId = service.registerAudioPolicy(policy.getConfig(), policy.cb(),
                     policy.hasFocusListener(), policy.isFocusPolicy(), policy.isTestFocusPolicy(),
                     policy.isVolumeController(),
-                    projection == null ? null : projection.getProjection());
+                    projection == null ? null : projection.getProjection(),
+                    policy.getAttributionSource());
             if (regId == null) {
                 return ERROR;
             } else {
diff --git a/media/java/android/media/AudioRecord.java b/media/java/android/media/AudioRecord.java
index 447d3bb..80e5719 100644
--- a/media/java/android/media/AudioRecord.java
+++ b/media/java/android/media/AudioRecord.java
@@ -789,7 +789,7 @@
         private @NonNull AudioRecord buildAudioPlaybackCaptureRecord() {
             AudioMix audioMix = mAudioPlaybackCaptureConfiguration.createAudioMix(mFormat);
             MediaProjection projection = mAudioPlaybackCaptureConfiguration.getMediaProjection();
-            AudioPolicy audioPolicy = new AudioPolicy.Builder(/*context=*/ null)
+            AudioPolicy audioPolicy = new AudioPolicy.Builder(/*context=*/ mContext)
                     .setMediaProjection(projection)
                     .addMix(audioMix).build();
 
@@ -853,7 +853,7 @@
                     .setFormat(mFormat)
                     .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK)
                     .build();
-            AudioPolicy audioPolicy = new AudioPolicy.Builder(null).addMix(audioMix).build();
+            AudioPolicy audioPolicy = new AudioPolicy.Builder(mContext).addMix(audioMix).build();
             if (AudioManager.registerAudioPolicyStatic(audioPolicy) != 0) {
                 throw new UnsupportedOperationException("Error: could not register audio policy");
             }
diff --git a/media/java/android/media/AudioTrack.java b/media/java/android/media/AudioTrack.java
index 194da21..73deb17 100644
--- a/media/java/android/media/AudioTrack.java
+++ b/media/java/android/media/AudioTrack.java
@@ -1353,7 +1353,8 @@
                     .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK)
                     .build();
             AudioPolicy audioPolicy =
-                    new AudioPolicy.Builder(/*context=*/ null).addMix(audioMix).build();
+                    new AudioPolicy.Builder(/*context=*/ mContext).addMix(audioMix).build();
+
             if (AudioManager.registerAudioPolicyStatic(audioPolicy) != 0) {
                 throw new UnsupportedOperationException("Error: could not register audio policy");
             }
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index 98bd3ca..e612645 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -18,6 +18,7 @@
 
 import android.bluetooth.BluetoothDevice;
 import android.content.ComponentName;
+import android.content.AttributionSource;
 import android.media.AudioAttributes;
 import android.media.AudioDeviceAttributes;
 import android.media.AudioFormat;
@@ -361,7 +362,8 @@
     String registerAudioPolicy(in AudioPolicyConfig policyConfig,
             in IAudioPolicyCallback pcb, boolean hasFocusListener, boolean isFocusPolicy,
             boolean isTestFocusPolicy,
-            boolean isVolumeController, in IMediaProjection projection);
+            boolean isVolumeController, in IMediaProjection projection,
+            in AttributionSource attributionSource);
 
     oneway void unregisterAudioPolicyAsync(in IAudioPolicyCallback pcb);
 
diff --git a/media/java/android/media/MediaCas.java b/media/java/android/media/MediaCas.java
index ab7c27f..2d7db5e 100644
--- a/media/java/android/media/MediaCas.java
+++ b/media/java/android/media/MediaCas.java
@@ -35,6 +35,7 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.os.IBinder;
 import android.os.IHwBinder;
 import android.os.Looper;
 import android.os.Message;
@@ -43,7 +44,6 @@
 import android.os.ServiceManager;
 import android.os.ServiceSpecificException;
 import android.util.Log;
-import android.util.Singleton;
 
 import com.android.internal.util.FrameworkStatsLog;
 
@@ -264,71 +264,107 @@
     public static final int PLUGIN_STATUS_SESSION_NUMBER_CHANGED =
             android.hardware.cas.StatusEvent.PLUGIN_SESSION_NUMBER_CHANGED;
 
-    private static final Singleton<IMediaCasService> sService =
-            new Singleton<IMediaCasService>() {
+    private static IMediaCasService sService = null;
+    private static Object sAidlLock = new Object();
+
+    /** DeathListener for AIDL service */
+    private static IBinder.DeathRecipient sDeathListener =
+            new IBinder.DeathRecipient() {
                 @Override
-                protected IMediaCasService create() {
-                    try {
-                        Log.d(TAG, "Trying to get AIDL service");
-                        IMediaCasService serviceAidl =
-                                IMediaCasService.Stub.asInterface(
-                                        ServiceManager.waitForDeclaredService(
-                                                IMediaCasService.DESCRIPTOR + "/default"));
-                        if (serviceAidl != null) {
-                            return serviceAidl;
-                        }
-                    } catch (Exception eAidl) {
-                        Log.d(TAG, "Failed to get cas AIDL service");
+                public void binderDied() {
+                    synchronized (sAidlLock) {
+                        Log.d(TAG, "The service is dead");
+                        sService.asBinder().unlinkToDeath(sDeathListener, 0);
+                        sService = null;
                     }
-                    return null;
-                }
-            };
-
-    private static final Singleton<android.hardware.cas.V1_0.IMediaCasService> sServiceHidl =
-            new Singleton<android.hardware.cas.V1_0.IMediaCasService>() {
-                @Override
-                protected android.hardware.cas.V1_0.IMediaCasService create() {
-                    try {
-                        Log.d(TAG, "Trying to get cas@1.2 service");
-                        android.hardware.cas.V1_2.IMediaCasService serviceV12 =
-                                android.hardware.cas.V1_2.IMediaCasService.getService(
-                                        true /*wait*/);
-                        if (serviceV12 != null) {
-                            return serviceV12;
-                        }
-                    } catch (Exception eV1_2) {
-                        Log.d(TAG, "Failed to get cas@1.2 service");
-                    }
-
-                    try {
-                        Log.d(TAG, "Trying to get cas@1.1 service");
-                        android.hardware.cas.V1_1.IMediaCasService serviceV11 =
-                                android.hardware.cas.V1_1.IMediaCasService.getService(
-                                        true /*wait*/);
-                        if (serviceV11 != null) {
-                            return serviceV11;
-                        }
-                    } catch (Exception eV1_1) {
-                        Log.d(TAG, "Failed to get cas@1.1 service");
-                    }
-
-                    try {
-                        Log.d(TAG, "Trying to get cas@1.0 service");
-                        return android.hardware.cas.V1_0.IMediaCasService.getService(true /*wait*/);
-                    } catch (Exception eV1_0) {
-                        Log.d(TAG, "Failed to get cas@1.0 service");
-                    }
-
-                    return null;
                 }
             };
 
     static IMediaCasService getService() {
-        return sService.get();
+        synchronized (sAidlLock) {
+            if (sService == null || !sService.asBinder().isBinderAlive()) {
+                try {
+                    Log.d(TAG, "Trying to get AIDL service");
+                    sService =
+                            IMediaCasService.Stub.asInterface(
+                                    ServiceManager.waitForDeclaredService(
+                                            IMediaCasService.DESCRIPTOR + "/default"));
+                    if (sService != null) {
+                        sService.asBinder().linkToDeath(sDeathListener, 0);
+                    }
+                } catch (Exception eAidl) {
+                    Log.d(TAG, "Failed to get cas AIDL service");
+                }
+            }
+            return sService;
+        }
     }
 
+    private static android.hardware.cas.V1_0.IMediaCasService sServiceHidl = null;
+    private static Object sHidlLock = new Object();
+
+    /** Used to indicate the right end-point to handle the serviceDied method */
+    private static final long MEDIA_CAS_HIDL_COOKIE = 394;
+
+    /** DeathListener for HIDL service */
+    private static IHwBinder.DeathRecipient sDeathListenerHidl =
+            new IHwBinder.DeathRecipient() {
+                @Override
+                public void serviceDied(long cookie) {
+                    if (cookie == MEDIA_CAS_HIDL_COOKIE) {
+                        synchronized (sHidlLock) {
+                            sServiceHidl = null;
+                        }
+                    }
+                }
+            };
+
     static android.hardware.cas.V1_0.IMediaCasService getServiceHidl() {
-        return sServiceHidl.get();
+        synchronized (sHidlLock) {
+            if (sServiceHidl != null) {
+                return sServiceHidl;
+            } else {
+                try {
+                    Log.d(TAG, "Trying to get cas@1.2 service");
+                    android.hardware.cas.V1_2.IMediaCasService serviceV12 =
+                            android.hardware.cas.V1_2.IMediaCasService.getService(true /*wait*/);
+                    if (serviceV12 != null) {
+                        sServiceHidl = serviceV12;
+                        sServiceHidl.linkToDeath(sDeathListenerHidl, MEDIA_CAS_HIDL_COOKIE);
+                        return sServiceHidl;
+                    }
+                } catch (Exception eV1_2) {
+                    Log.d(TAG, "Failed to get cas@1.2 service");
+                }
+
+                try {
+                    Log.d(TAG, "Trying to get cas@1.1 service");
+                    android.hardware.cas.V1_1.IMediaCasService serviceV11 =
+                            android.hardware.cas.V1_1.IMediaCasService.getService(true /*wait*/);
+                    if (serviceV11 != null) {
+                        sServiceHidl = serviceV11;
+                        sServiceHidl.linkToDeath(sDeathListenerHidl, MEDIA_CAS_HIDL_COOKIE);
+                        return sServiceHidl;
+                    }
+                } catch (Exception eV1_1) {
+                    Log.d(TAG, "Failed to get cas@1.1 service");
+                }
+
+                try {
+                    Log.d(TAG, "Trying to get cas@1.0 service");
+                    sServiceHidl =
+                            android.hardware.cas.V1_0.IMediaCasService.getService(true /*wait*/);
+                    if (sServiceHidl != null) {
+                        sServiceHidl.linkToDeath(sDeathListenerHidl, MEDIA_CAS_HIDL_COOKIE);
+                    }
+                    return sServiceHidl;
+                } catch (Exception eV1_0) {
+                    Log.d(TAG, "Failed to get cas@1.0 service");
+                }
+            }
+        }
+        // Couldn't find an HIDL service, returning null.
+        return null;
     }
 
     private void validateInternalStates() {
@@ -756,7 +792,7 @@
      * @return Whether the specified CA system is supported on this device.
      */
     public static boolean isSystemIdSupported(int CA_system_id) {
-        IMediaCasService service = sService.get();
+        IMediaCasService service = getService();
         if (service != null) {
             try {
                 return service.isSystemIdSupported(CA_system_id);
@@ -765,7 +801,7 @@
             }
         }
 
-        android.hardware.cas.V1_0.IMediaCasService serviceHidl = sServiceHidl.get();
+        android.hardware.cas.V1_0.IMediaCasService serviceHidl = getServiceHidl();
         if (serviceHidl != null) {
             try {
                 return serviceHidl.isSystemIdSupported(CA_system_id);
@@ -781,7 +817,7 @@
      * @return an array of descriptors for the available CA plugins.
      */
     public static PluginDescriptor[] enumeratePlugins() {
-        IMediaCasService service = sService.get();
+        IMediaCasService service = getService();
         if (service != null) {
             try {
                 AidlCasPluginDescriptor[] descriptors = service.enumeratePlugins();
@@ -794,10 +830,11 @@
                 }
                 return results;
             } catch (RemoteException e) {
+                Log.e(TAG, "Some exception while enumerating plugins");
             }
         }
 
-        android.hardware.cas.V1_0.IMediaCasService serviceHidl = sServiceHidl.get();
+        android.hardware.cas.V1_0.IMediaCasService serviceHidl = getServiceHidl();
         if (serviceHidl != null) {
             try {
                 ArrayList<HidlCasPluginDescriptor> descriptors = serviceHidl.enumeratePlugins();
diff --git a/media/java/android/media/audiopolicy/AudioMix.java b/media/java/android/media/audiopolicy/AudioMix.java
index a53a8ce..e4eaaa3 100644
--- a/media/java/android/media/audiopolicy/AudioMix.java
+++ b/media/java/android/media/audiopolicy/AudioMix.java
@@ -24,6 +24,7 @@
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
 import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
 import android.media.AudioDeviceInfo;
 import android.media.AudioFormat;
 import android.media.AudioSystem;
@@ -67,12 +68,19 @@
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     final int mDeviceSystemType; // an AudioSystem.DEVICE_* value, not AudioDeviceInfo.TYPE_*
 
+    // The (virtual) device ID that this AudioMix was registered for. This value is overwritten
+    // when registering this AudioMix with an AudioPolicy or attaching this AudioMix to an
+    // AudioPolicy to match the AudioPolicy attribution. Does not imply that it only modifies
+    // audio routing for this device ID.
+    private int mVirtualDeviceId;
+
     /**
      * All parameters are guaranteed valid through the Builder.
      */
     private AudioMix(@NonNull AudioMixingRule rule, @NonNull AudioFormat format,
             int routeFlags, int callbackFlags,
-            int deviceType, @Nullable String deviceAddress, IBinder token) {
+            int deviceType, @Nullable String deviceAddress, IBinder token,
+            int virtualDeviceId) {
         mRule = Objects.requireNonNull(rule);
         mFormat = Objects.requireNonNull(format);
         mRouteFlags = routeFlags;
@@ -81,6 +89,7 @@
         mDeviceSystemType = deviceType;
         mDeviceAddress = (deviceAddress == null) ? new String("") : deviceAddress;
         mToken = token;
+        mVirtualDeviceId = virtualDeviceId;
     }
 
     // CALLBACK_FLAG_* values: keep in sync with AudioMix::kCbFlag* values defined
@@ -269,6 +278,11 @@
     }
 
     /** @hide */
+    public boolean matchesVirtualDeviceId(int deviceId) {
+        return mVirtualDeviceId == deviceId;
+    }
+
+    /** @hide */
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;
@@ -311,6 +325,7 @@
         mFormat.writeToParcel(dest, flags);
         mRule.writeToParcel(dest, flags);
         dest.writeStrongBinder(mToken);
+        dest.writeInt(mVirtualDeviceId);
     }
 
     public static final @NonNull Parcelable.Creator<AudioMix> CREATOR = new Parcelable.Creator<>() {
@@ -331,6 +346,7 @@
             mixBuilder.setFormat(AudioFormat.CREATOR.createFromParcel(p));
             mixBuilder.setMixingRule(AudioMixingRule.CREATOR.createFromParcel(p));
             mixBuilder.setToken(p.readStrongBinder());
+            mixBuilder.setVirtualDeviceId(p.readInt());
             return mixBuilder.build();
         }
 
@@ -339,6 +355,15 @@
         }
     };
 
+    /**
+     * Updates the deviceId of the AudioMix to match with the AudioPolicy the mix is registered
+     * through.
+     * @hide
+     */
+    public void setVirtualDeviceId(int virtualDeviceId) {
+        mVirtualDeviceId = virtualDeviceId;
+    }
+
     /** @hide */
     @IntDef(flag = true,
             value = { ROUTE_FLAG_RENDER, ROUTE_FLAG_LOOP_BACK } )
@@ -354,6 +379,7 @@
         private int mRouteFlags = 0;
         private int mCallbackFlags = 0;
         private IBinder mToken = null;
+        private int mVirtualDeviceId = Context.DEVICE_ID_DEFAULT;
         // an AudioSystem.DEVICE_* value, not AudioDeviceInfo.TYPE_*
         private int mDeviceSystemType = AudioSystem.DEVICE_NONE;
         private String mDeviceAddress = null;
@@ -404,6 +430,15 @@
 
         /**
          * @hide
+         * Only used by AudioMix internally.
+         */
+        Builder setVirtualDeviceId(int virtualDeviceId) {
+            mVirtualDeviceId = virtualDeviceId;
+            return this;
+        }
+
+        /**
+         * @hide
          * Only used by AudioPolicyConfig, not a public API.
          * @param callbackFlags which callbacks are called from native
          * @return the same Builder instance.
@@ -570,7 +605,7 @@
             }
 
             return new AudioMix(mRule, mFormat, mRouteFlags, mCallbackFlags, mDeviceSystemType,
-                    mDeviceAddress, mToken);
+                    mDeviceAddress, mToken, mVirtualDeviceId);
         }
 
         private int getLoopbackDeviceSystemTypeForAudioMixingRule(AudioMixingRule rule) {
diff --git a/media/java/android/media/audiopolicy/AudioPolicy.java b/media/java/android/media/audiopolicy/AudioPolicy.java
index 508c0a2b..293a8f8 100644
--- a/media/java/android/media/audiopolicy/AudioPolicy.java
+++ b/media/java/android/media/audiopolicy/AudioPolicy.java
@@ -27,6 +27,7 @@
 import android.annotation.TestApi;
 import android.annotation.UserIdInt;
 import android.app.ActivityManager;
+import android.content.AttributionSource;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.media.AudioAttributes;
@@ -146,6 +147,16 @@
         return mProjection;
     }
 
+    /** @hide */
+    public AttributionSource getAttributionSource() {
+        return getAttributionSource(mContext);
+    }
+
+    private static AttributionSource getAttributionSource(Context context) {
+        return context == null
+                ? AttributionSource.myAttributionSource() : context.getAttributionSource();
+    }
+
     /**
      * The parameters are guaranteed non-null through the Builder
      */
@@ -208,6 +219,9 @@
             if (mix == null) {
                 throw new IllegalArgumentException("Illegal null AudioMix argument");
             }
+            if (android.permission.flags.Flags.deviceAwarePermissionApisEnabled()) {
+                mix.setVirtualDeviceId(getAttributionSource(mContext).getDeviceId());
+            }
             mMixes.add(mix);
             return this;
         }
@@ -358,6 +372,9 @@
                 if (mix == null) {
                     throw new IllegalArgumentException("Illegal null AudioMix in attachMixes");
                 } else {
+                    if (android.permission.flags.Flags.deviceAwarePermissionApisEnabled()) {
+                        mix.setVirtualDeviceId(getAttributionSource(mContext).getDeviceId());
+                    }
                     zeMixes.add(mix);
                 }
             }
@@ -400,6 +417,9 @@
                 if (mix == null) {
                     throw new IllegalArgumentException("Illegal null AudioMix in detachMixes");
                 } else {
+                    if (android.permission.flags.Flags.deviceAwarePermissionApisEnabled()) {
+                        mix.setVirtualDeviceId(getAttributionSource(mContext).getDeviceId());
+                    }
                     zeMixes.add(mix);
                 }
             }
diff --git a/native/android/OWNERS b/native/android/OWNERS
index 0b86909..9a3527d 100644
--- a/native/android/OWNERS
+++ b/native/android/OWNERS
@@ -16,6 +16,8 @@
 per-file native_window_jni.cpp = file:/services/core/java/com/android/server/wm/OWNERS
 per-file native_activity.cpp = file:/services/core/java/com/android/server/wm/OWNERS
 per-file surface_control.cpp = file:/services/core/java/com/android/server/wm/OWNERS
+per-file surface_control_input_receiver.cpp = file:/services/core/java/com/android/server/wm/OWNERS
+per-file input_transfer_token.cpp = file:/services/core/java/com/android/server/wm/OWNERS
 
 # Graphics
 per-file choreographer.cpp = file:/graphics/java/android/graphics/OWNERS
diff --git a/native/android/surface_control_input_receiver.cpp b/native/android/surface_control_input_receiver.cpp
index da0defd..a84ec73 100644
--- a/native/android/surface_control_input_receiver.cpp
+++ b/native/android/surface_control_input_receiver.cpp
@@ -45,6 +45,8 @@
             mClientToken(clientToken),
             mInputTransferToken(inputTransferToken) {}
 
+    // The InputConsumer does not keep the InputReceiver alive so the receiver is cleared once the
+    // owner releases it.
     ~InputReceiver() {
         remove();
     }
@@ -190,7 +192,9 @@
 
 void AInputReceiver_release(AInputReceiver* aInputReceiver) {
     InputReceiver* inputReceiver = AInputReceiver_to_InputReceiver(aInputReceiver);
-    inputReceiver->remove();
+    if (inputReceiver != nullptr) {
+        inputReceiver->remove();
+    }
     delete inputReceiver;
 }
 
diff --git a/nfc/api/current.txt b/nfc/api/current.txt
index da292a81..80b2be2 100644
--- a/nfc/api/current.txt
+++ b/nfc/api/current.txt
@@ -268,10 +268,9 @@
   }
 
   @FlaggedApi("android.nfc.nfc_read_polling_loop") public final class PollingFrame implements android.os.Parcelable {
-    ctor public PollingFrame(int, @Nullable byte[], int, int, boolean);
     method public int describeContents();
     method @NonNull public byte[] getData();
-    method public int getTimestamp();
+    method public long getTimestamp();
     method public boolean getTriggeredAutoTransact();
     method public int getType();
     method public int getVendorSpecificGain();
diff --git a/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java b/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java
index be3c248..a353df7 100644
--- a/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java
+++ b/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java
@@ -723,6 +723,7 @@
      * delivered to {@link HostApduService#processPollingFrames(List)}. Adding a key with this
      * multiple times will cause the value to be overwritten each time.
      * @param pollingLoopFilter the polling loop filter to add, must be a valid hexadecimal string
+     * @param autoTransact whether Observe Mode should be disabled when this filter matches or not
      */
     @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP)
     public void addPollingLoopFilter(@NonNull String pollingLoopFilter,
@@ -747,6 +748,7 @@
      * multiple times will cause the value to be overwritten each time.
      * @param pollingLoopPatternFilter the polling loop pattern filter to add, must be a valid
      *                                regex to match a hexadecimal string
+     * @param autoTransact whether Observe Mode should be disabled when this filter matches or not
      */
     @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP)
     public void addPollingLoopPatternFilter(@NonNull String pollingLoopPatternFilter,
diff --git a/nfc/java/android/nfc/cardemulation/PollingFrame.java b/nfc/java/android/nfc/cardemulation/PollingFrame.java
index af63a6e..654e8cc 100644
--- a/nfc/java/android/nfc/cardemulation/PollingFrame.java
+++ b/nfc/java/android/nfc/cardemulation/PollingFrame.java
@@ -16,6 +16,7 @@
 
 package android.nfc.cardemulation;
 
+import android.annotation.DurationMillisLong;
 import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
@@ -148,7 +149,8 @@
     private final int mType;
     private final byte[] mData;
     private final int mGain;
-    private final int mTimestamp;
+    @DurationMillisLong
+    private final long mTimestamp;
     private final boolean mTriggeredAutoTransact;
 
     public static final @NonNull Parcelable.Creator<PollingFrame> CREATOR =
@@ -180,16 +182,18 @@
      * @param type the type of the frame
      * @param data a byte array of the data contained in the frame
      * @param gain the vendor-specific gain of the field
-     * @param timestamp the timestamp in millisecones
+     * @param timestampMillis the timestamp in millisecones
      * @param triggeredAutoTransact whether or not this frame triggered the device to start a
      * transaction automatically
+     *
+     * @hide
      */
     public PollingFrame(@PollingFrameType int type, @Nullable byte[] data,
-            int gain, int timestamp, boolean triggeredAutoTransact) {
+            int gain, @DurationMillisLong long timestampMillis, boolean triggeredAutoTransact) {
         mType = type;
         mData = data == null ? new byte[0] : data;
         mGain = gain;
-        mTimestamp = timestamp;
+        mTimestamp = timestampMillis;
         mTriggeredAutoTransact = triggeredAutoTransact;
     }
 
@@ -230,7 +234,7 @@
      * frames relative to each other.
      * @return the timestamp in milliseconds
      */
-    public int getTimestamp() {
+    public @DurationMillisLong long getTimestamp() {
         return mTimestamp;
     }
 
@@ -264,7 +268,7 @@
             frame.putInt(KEY_POLLING_LOOP_GAIN, (byte) getVendorSpecificGain());
         }
         frame.putByteArray(KEY_POLLING_LOOP_DATA, getData());
-        frame.putInt(KEY_POLLING_LOOP_TIMESTAMP, getTimestamp());
+        frame.putLong(KEY_POLLING_LOOP_TIMESTAMP, getTimestamp());
         frame.putBoolean(KEY_POLLING_LOOP_TRIGGERED_AUTOTRANSACT, getTriggeredAutoTransact());
         return frame;
     }
@@ -273,7 +277,7 @@
     public String toString() {
         return "PollingFrame { Type: " + (char) getType()
                 + ", gain: " + getVendorSpecificGain()
-                + ", timestamp: " + Integer.toUnsignedString(getTimestamp())
+                + ", timestamp: " + Long.toUnsignedString(getTimestamp())
                 + ", data: [" + HexFormat.ofDelimiter(" ").formatHex(getData()) + "] }";
     }
 }
diff --git a/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java b/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java
index 37b5d40..a8d8f9a 100644
--- a/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java
+++ b/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java
@@ -26,6 +26,7 @@
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.VersionedPackage;
+import android.crashrecovery.flags.Flags;
 import android.net.ConnectivityModuleConnector;
 import android.os.Environment;
 import android.os.Handler;
@@ -57,16 +58,20 @@
 import java.io.BufferedReader;
 import java.io.BufferedWriter;
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.FileReader;
 import java.io.FileWriter;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
@@ -130,8 +135,25 @@
 
     @VisibleForTesting
     static final int DEFAULT_BOOT_LOOP_TRIGGER_COUNT = 5;
-    @VisibleForTesting
+
     static final long DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS = TimeUnit.MINUTES.toMillis(10);
+    // Boot loop at which packageWatchdog starts first mitigation
+    private static final String BOOT_LOOP_THRESHOLD =
+            "persist.device_config.configuration.boot_loop_threshold";
+    @VisibleForTesting
+    static final int DEFAULT_BOOT_LOOP_THRESHOLD = 15;
+    // Once boot_loop_threshold is surpassed next mitigation would be triggered after
+    // specified number of reboots.
+    private static final String BOOT_LOOP_MITIGATION_INCREMENT =
+            "persist.device_config.configuration..boot_loop_mitigation_increment";
+    @VisibleForTesting
+    static final int DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT = 2;
+
+    // Threshold level at which or above user might experience significant disruption.
+    private static final String MAJOR_USER_IMPACT_LEVEL_THRESHOLD =
+            "persist.device_config.configuration.major_user_impact_level_threshold";
+    private static final int DEFAULT_MAJOR_USER_IMPACT_LEVEL_THRESHOLD =
+            PackageHealthObserverImpact.USER_IMPACT_LEVEL_71;
 
     private long mNumberOfNativeCrashPollsRemaining;
 
@@ -145,6 +167,7 @@
     private static final String ATTR_EXPLICIT_HEALTH_CHECK_DURATION = "health-check-duration";
     private static final String ATTR_PASSED_HEALTH_CHECK = "passed-health-check";
     private static final String ATTR_MITIGATION_CALLS = "mitigation-calls";
+    private static final String ATTR_MITIGATION_COUNT = "mitigation-count";
 
     // A file containing information about the current mitigation count in the case of a boot loop.
     // This allows boot loop information to persist in the case of an fs-checkpoint being
@@ -230,8 +253,16 @@
         mConnectivityModuleConnector = connectivityModuleConnector;
         mSystemClock = clock;
         mNumberOfNativeCrashPollsRemaining = NUMBER_OF_NATIVE_CRASH_POLLS;
-        mBootThreshold = new BootThreshold(DEFAULT_BOOT_LOOP_TRIGGER_COUNT,
-                DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS);
+        if (Flags.recoverabilityDetection()) {
+            mBootThreshold = new BootThreshold(DEFAULT_BOOT_LOOP_TRIGGER_COUNT,
+                    DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS,
+                    SystemProperties.getInt(BOOT_LOOP_MITIGATION_INCREMENT,
+                            DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT));
+        } else {
+            mBootThreshold = new BootThreshold(DEFAULT_BOOT_LOOP_TRIGGER_COUNT,
+                    DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS);
+        }
+
         loadFromFile();
         sPackageWatchdog = this;
     }
@@ -436,8 +467,13 @@
                                 mitigationCount =
                                         currentMonitoredPackage.getMitigationCountLocked();
                             }
-                            currentObserverToNotify.execute(versionedPackage,
-                                    failureReason, mitigationCount);
+                            if (Flags.recoverabilityDetection()) {
+                                maybeExecute(currentObserverToNotify, versionedPackage,
+                                        failureReason, currentObserverImpact, mitigationCount);
+                            } else {
+                                currentObserverToNotify.execute(versionedPackage,
+                                        failureReason, mitigationCount);
+                            }
                         }
                     }
                 }
@@ -467,37 +503,76 @@
             }
         }
         if (currentObserverToNotify != null) {
-            currentObserverToNotify.execute(failingPackage,  failureReason, 1);
+            if (Flags.recoverabilityDetection()) {
+                maybeExecute(currentObserverToNotify, failingPackage, failureReason,
+                        currentObserverImpact, /*mitigationCount=*/ 1);
+            } else {
+                currentObserverToNotify.execute(failingPackage,  failureReason, 1);
+            }
         }
     }
 
+    private void maybeExecute(PackageHealthObserver currentObserverToNotify,
+                              VersionedPackage versionedPackage,
+                              @FailureReasons int failureReason,
+                              int currentObserverImpact,
+                              int mitigationCount) {
+        if (currentObserverImpact < getUserImpactLevelLimit()) {
+            currentObserverToNotify.execute(versionedPackage, failureReason, mitigationCount);
+        }
+    }
+
+
     /**
      * Called when the system server boots. If the system server is detected to be in a boot loop,
      * query each observer and perform the mitigation action with the lowest user impact.
      */
+    @SuppressWarnings("GuardedBy")
     public void noteBoot() {
         synchronized (mLock) {
-            if (mBootThreshold.incrementAndTest()) {
-                mBootThreshold.reset();
+            boolean mitigate = mBootThreshold.incrementAndTest();
+            if (mitigate) {
+                if (!Flags.recoverabilityDetection()) {
+                    mBootThreshold.reset();
+                }
                 int mitigationCount = mBootThreshold.getMitigationCount() + 1;
                 PackageHealthObserver currentObserverToNotify = null;
+                ObserverInternal currentObserverInternal = null;
                 int currentObserverImpact = Integer.MAX_VALUE;
                 for (int i = 0; i < mAllObservers.size(); i++) {
                     final ObserverInternal observer = mAllObservers.valueAt(i);
                     PackageHealthObserver registeredObserver = observer.registeredObserver;
                     if (registeredObserver != null) {
-                        int impact = registeredObserver.onBootLoop(mitigationCount);
+                        int impact = Flags.recoverabilityDetection()
+                                ? registeredObserver.onBootLoop(
+                                        observer.getBootMitigationCount() + 1)
+                                : registeredObserver.onBootLoop(mitigationCount);
                         if (impact != PackageHealthObserverImpact.USER_IMPACT_LEVEL_0
                                 && impact < currentObserverImpact) {
                             currentObserverToNotify = registeredObserver;
+                            currentObserverInternal = observer;
                             currentObserverImpact = impact;
                         }
                     }
                 }
                 if (currentObserverToNotify != null) {
-                    mBootThreshold.setMitigationCount(mitigationCount);
-                    mBootThreshold.saveMitigationCountToMetadata();
-                    currentObserverToNotify.executeBootLoopMitigation(mitigationCount);
+                    if (Flags.recoverabilityDetection()) {
+                        if (currentObserverImpact < getUserImpactLevelLimit()
+                                || (currentObserverImpact >= getUserImpactLevelLimit()
+                                        && mBootThreshold.getCount() >= getBootLoopThreshold())) {
+                            int currentObserverMitigationCount =
+                                    currentObserverInternal.getBootMitigationCount() + 1;
+                            currentObserverInternal.setBootMitigationCount(
+                                    currentObserverMitigationCount);
+                            saveAllObserversBootMitigationCountToMetadata(METADATA_FILE);
+                            currentObserverToNotify.executeBootLoopMitigation(
+                                    currentObserverMitigationCount);
+                        }
+                    } else {
+                        mBootThreshold.setMitigationCount(mitigationCount);
+                        mBootThreshold.saveMitigationCountToMetadata();
+                        currentObserverToNotify.executeBootLoopMitigation(mitigationCount);
+                    }
                 }
             }
         }
@@ -567,13 +642,27 @@
         mShortTaskHandler.post(()->checkAndMitigateNativeCrashes());
     }
 
+    private int getUserImpactLevelLimit() {
+        return SystemProperties.getInt(MAJOR_USER_IMPACT_LEVEL_THRESHOLD,
+                DEFAULT_MAJOR_USER_IMPACT_LEVEL_THRESHOLD);
+    }
+
+    private int getBootLoopThreshold() {
+        return SystemProperties.getInt(BOOT_LOOP_THRESHOLD,
+                DEFAULT_BOOT_LOOP_THRESHOLD);
+    }
+
     /** Possible severity values of the user impact of a {@link PackageHealthObserver#execute}. */
     @Retention(SOURCE)
     @IntDef(value = {PackageHealthObserverImpact.USER_IMPACT_LEVEL_0,
                      PackageHealthObserverImpact.USER_IMPACT_LEVEL_10,
+                     PackageHealthObserverImpact.USER_IMPACT_LEVEL_20,
                      PackageHealthObserverImpact.USER_IMPACT_LEVEL_30,
                      PackageHealthObserverImpact.USER_IMPACT_LEVEL_50,
                      PackageHealthObserverImpact.USER_IMPACT_LEVEL_70,
+                     PackageHealthObserverImpact.USER_IMPACT_LEVEL_71,
+                     PackageHealthObserverImpact.USER_IMPACT_LEVEL_75,
+                     PackageHealthObserverImpact.USER_IMPACT_LEVEL_80,
                      PackageHealthObserverImpact.USER_IMPACT_LEVEL_90,
                      PackageHealthObserverImpact.USER_IMPACT_LEVEL_100})
     public @interface PackageHealthObserverImpact {
@@ -582,11 +671,15 @@
         /* Action has low user impact, user of a device will barely notice. */
         int USER_IMPACT_LEVEL_10 = 10;
         /* Actions having medium user impact, user of a device will likely notice. */
+        int USER_IMPACT_LEVEL_20 = 20;
         int USER_IMPACT_LEVEL_30 = 30;
         int USER_IMPACT_LEVEL_50 = 50;
         int USER_IMPACT_LEVEL_70 = 70;
-        int USER_IMPACT_LEVEL_90 = 90;
         /* Action has high user impact, a last resort, user of a device will be very frustrated. */
+        int USER_IMPACT_LEVEL_71 = 71;
+        int USER_IMPACT_LEVEL_75 = 75;
+        int USER_IMPACT_LEVEL_80 = 80;
+        int USER_IMPACT_LEVEL_90 = 90;
         int USER_IMPACT_LEVEL_100 = 100;
     }
 
@@ -1144,6 +1237,12 @@
         }
     }
 
+    @VisibleForTesting
+    @GuardedBy("mLock")
+    void registerObserverInternal(ObserverInternal observerInternal) {
+        mAllObservers.put(observerInternal.name, observerInternal);
+    }
+
     /**
      * Represents an observer monitoring a set of packages along with the failure thresholds for
      * each package.
@@ -1151,17 +1250,23 @@
      * <p> Note, the PackageWatchdog#mLock must always be held when reading or writing
      * instances of this class.
      */
-    private static class ObserverInternal {
+    static class ObserverInternal {
         public final String name;
         @GuardedBy("mLock")
         private final ArrayMap<String, MonitoredPackage> mPackages = new ArrayMap<>();
         @Nullable
         @GuardedBy("mLock")
         public PackageHealthObserver registeredObserver;
+        private int mMitigationCount;
 
         ObserverInternal(String name, List<MonitoredPackage> packages) {
+            this(name, packages, /*mitigationCount=*/ 0);
+        }
+
+        ObserverInternal(String name, List<MonitoredPackage> packages, int mitigationCount) {
             this.name = name;
             updatePackagesLocked(packages);
+            this.mMitigationCount = mitigationCount;
         }
 
         /**
@@ -1173,6 +1278,9 @@
             try {
                 out.startTag(null, TAG_OBSERVER);
                 out.attribute(null, ATTR_NAME, name);
+                if (Flags.recoverabilityDetection()) {
+                    out.attributeInt(null, ATTR_MITIGATION_COUNT, mMitigationCount);
+                }
                 for (int i = 0; i < mPackages.size(); i++) {
                     MonitoredPackage p = mPackages.valueAt(i);
                     p.writeLocked(out);
@@ -1185,6 +1293,14 @@
             }
         }
 
+        public int getBootMitigationCount() {
+            return mMitigationCount;
+        }
+
+        public void setBootMitigationCount(int mitigationCount) {
+            mMitigationCount = mitigationCount;
+        }
+
         @GuardedBy("mLock")
         public void updatePackagesLocked(List<MonitoredPackage> packages) {
             for (int pIndex = 0; pIndex < packages.size(); pIndex++) {
@@ -1289,6 +1405,7 @@
          **/
         public static ObserverInternal read(TypedXmlPullParser parser, PackageWatchdog watchdog) {
             String observerName = null;
+            int observerMitigationCount = 0;
             if (TAG_OBSERVER.equals(parser.getName())) {
                 observerName = parser.getAttributeValue(null, ATTR_NAME);
                 if (TextUtils.isEmpty(observerName)) {
@@ -1299,6 +1416,9 @@
             List<MonitoredPackage> packages = new ArrayList<>();
             int innerDepth = parser.getDepth();
             try {
+                if (Flags.recoverabilityDetection()) {
+                    observerMitigationCount = parser.getAttributeInt(null, ATTR_MITIGATION_COUNT);
+                }
                 while (XmlUtils.nextElementWithin(parser, innerDepth)) {
                     if (TAG_PACKAGE.equals(parser.getName())) {
                         try {
@@ -1319,7 +1439,7 @@
             if (packages.isEmpty()) {
                 return null;
             }
-            return new ObserverInternal(observerName, packages);
+            return new ObserverInternal(observerName, packages, observerMitigationCount);
         }
 
         /** Dumps information about this observer and the packages it watches. */
@@ -1679,6 +1799,27 @@
         }
     }
 
+    @GuardedBy("mLock")
+    @SuppressWarnings("GuardedBy")
+    void saveAllObserversBootMitigationCountToMetadata(String filePath) {
+        HashMap<String, Integer> bootMitigationCounts = new HashMap<>();
+        for (int i = 0; i < mAllObservers.size(); i++) {
+            final ObserverInternal observer = mAllObservers.valueAt(i);
+            bootMitigationCounts.put(observer.name, observer.getBootMitigationCount());
+        }
+
+        try {
+            FileOutputStream fileStream = new FileOutputStream(new File(filePath));
+            ObjectOutputStream objectStream = new ObjectOutputStream(fileStream);
+            objectStream.writeObject(bootMitigationCounts);
+            objectStream.flush();
+            objectStream.close();
+            fileStream.close();
+        } catch (Exception e) {
+            Slog.i(TAG, "Could not save observers metadata to file: " + e);
+        }
+    }
+
     /**
      * Handles the thresholding logic for system server boots.
      */
@@ -1686,10 +1827,16 @@
 
         private final int mBootTriggerCount;
         private final long mTriggerWindow;
+        private final int mBootMitigationIncrement;
 
         BootThreshold(int bootTriggerCount, long triggerWindow) {
+            this(bootTriggerCount, triggerWindow, /*bootMitigationIncrement=*/ 1);
+        }
+
+        BootThreshold(int bootTriggerCount, long triggerWindow, int bootMitigationIncrement) {
             this.mBootTriggerCount = bootTriggerCount;
             this.mTriggerWindow = triggerWindow;
+            this.mBootMitigationIncrement = bootMitigationIncrement;
         }
 
         public void reset() {
@@ -1761,8 +1908,13 @@
 
 
         /** Increments the boot counter, and returns whether the device is bootlooping. */
+        @GuardedBy("mLock")
         public boolean incrementAndTest() {
-            readMitigationCountFromMetadataIfNecessary();
+            if (Flags.recoverabilityDetection()) {
+                readAllObserversBootMitigationCountIfNecessary(METADATA_FILE);
+            } else {
+                readMitigationCountFromMetadataIfNecessary();
+            }
             final long now = mSystemClock.uptimeMillis();
             if (now - getStart() < 0) {
                 Slog.e(TAG, "Window was less than zero. Resetting start to current time.");
@@ -1770,8 +1922,12 @@
                 setMitigationStart(now);
             }
             if (now - getMitigationStart() > DEFAULT_DEESCALATION_WINDOW_MS) {
-                setMitigationCount(0);
                 setMitigationStart(now);
+                if (Flags.recoverabilityDetection()) {
+                    resetAllObserversBootMitigationCount();
+                } else {
+                    setMitigationCount(0);
+                }
             }
             final long window = now - getStart();
             if (window >= mTriggerWindow) {
@@ -1782,9 +1938,48 @@
                 int count = getCount() + 1;
                 setCount(count);
                 EventLogTags.writeRescueNote(Process.ROOT_UID, count, window);
+                if (Flags.recoverabilityDetection()) {
+                    boolean mitigate = (count >= mBootTriggerCount)
+                            && (count - mBootTriggerCount) % mBootMitigationIncrement == 0;
+                    return mitigate;
+                }
                 return count >= mBootTriggerCount;
             }
         }
 
+        @GuardedBy("mLock")
+        private void resetAllObserversBootMitigationCount() {
+            for (int i = 0; i < mAllObservers.size(); i++) {
+                final ObserverInternal observer = mAllObservers.valueAt(i);
+                observer.setBootMitigationCount(0);
+            }
+        }
+
+        @GuardedBy("mLock")
+        @SuppressWarnings("GuardedBy")
+        void readAllObserversBootMitigationCountIfNecessary(String filePath) {
+            File metadataFile = new File(filePath);
+            if (metadataFile.exists()) {
+                try {
+                    FileInputStream fileStream = new FileInputStream(metadataFile);
+                    ObjectInputStream objectStream = new ObjectInputStream(fileStream);
+                    HashMap<String, Integer> bootMitigationCounts =
+                            (HashMap<String, Integer>) objectStream.readObject();
+                    objectStream.close();
+                    fileStream.close();
+
+                    for (int i = 0; i < mAllObservers.size(); i++) {
+                        final ObserverInternal observer = mAllObservers.valueAt(i);
+                        if (bootMitigationCounts.containsKey(observer.name)) {
+                            observer.setBootMitigationCount(
+                                    bootMitigationCounts.get(observer.name));
+                        }
+                    }
+                } catch (Exception e) {
+                    Slog.i(TAG, "Could not read observer metadata file: " + e);
+                }
+            }
+        }
+
     }
 }
diff --git a/packages/CrashRecovery/services/java/com/android/server/RescueParty.java b/packages/CrashRecovery/services/java/com/android/server/RescueParty.java
index 7bdc1a0..7093ba4 100644
--- a/packages/CrashRecovery/services/java/com/android/server/RescueParty.java
+++ b/packages/CrashRecovery/services/java/com/android/server/RescueParty.java
@@ -20,6 +20,7 @@
 
 import static com.android.server.pm.PackageManagerServiceUtils.logCriticalInfo;
 
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.ContentResolver;
@@ -27,6 +28,7 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.VersionedPackage;
+import android.crashrecovery.flags.Flags;
 import android.os.Build;
 import android.os.Environment;
 import android.os.PowerManager;
@@ -53,6 +55,8 @@
 import com.android.server.crashrecovery.proto.CrashRecoveryStatsLog;
 
 import java.io.File;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -89,6 +93,40 @@
     @VisibleForTesting
     static final int LEVEL_FACTORY_RESET = 5;
     @VisibleForTesting
+    static final int RESCUE_LEVEL_NONE = 0;
+    @VisibleForTesting
+    static final int RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET = 1;
+    @VisibleForTesting
+    static final int RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET = 2;
+    @VisibleForTesting
+    static final int RESCUE_LEVEL_WARM_REBOOT = 3;
+    @VisibleForTesting
+    static final int RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS = 4;
+    @VisibleForTesting
+    static final int RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES = 5;
+    @VisibleForTesting
+    static final int RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS = 6;
+    @VisibleForTesting
+    static final int RESCUE_LEVEL_FACTORY_RESET = 7;
+
+    @IntDef(prefix = { "RESCUE_LEVEL_" }, value = {
+        RESCUE_LEVEL_NONE,
+        RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET,
+        RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET,
+        RESCUE_LEVEL_WARM_REBOOT,
+        RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS,
+        RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES,
+        RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS,
+        RESCUE_LEVEL_FACTORY_RESET
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface RescueLevels {}
+
+    @VisibleForTesting
+    static final String RESCUE_NON_REBOOT_LEVEL_LIMIT = "persist.sys.rescue_non_reboot_level_limit";
+    @VisibleForTesting
+    static final int DEFAULT_RESCUE_NON_REBOOT_LEVEL_LIMIT = RESCUE_LEVEL_WARM_REBOOT - 1;
+    @VisibleForTesting
     static final String TAG = "RescueParty";
     @VisibleForTesting
     static final long DEFAULT_OBSERVING_DURATION_MS = TimeUnit.DAYS.toMillis(2);
@@ -347,11 +385,20 @@
     }
 
     private static int getMaxRescueLevel(boolean mayPerformReboot) {
-        if (!mayPerformReboot
-                || SystemProperties.getBoolean(PROP_DISABLE_FACTORY_RESET_FLAG, false)) {
-            return LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS;
+        if (Flags.recoverabilityDetection()) {
+            if (!mayPerformReboot
+                    || SystemProperties.getBoolean(PROP_DISABLE_FACTORY_RESET_FLAG, false)) {
+                return SystemProperties.getInt(RESCUE_NON_REBOOT_LEVEL_LIMIT,
+                        DEFAULT_RESCUE_NON_REBOOT_LEVEL_LIMIT);
+            }
+            return RESCUE_LEVEL_FACTORY_RESET;
+        } else {
+            if (!mayPerformReboot
+                    || SystemProperties.getBoolean(PROP_DISABLE_FACTORY_RESET_FLAG, false)) {
+                return LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS;
+            }
+            return LEVEL_FACTORY_RESET;
         }
-        return LEVEL_FACTORY_RESET;
     }
 
     /**
@@ -379,6 +426,46 @@
         }
     }
 
+    /**
+     * Get the rescue level to perform if this is the n-th attempt at mitigating failure.
+     * When failedPackage is null then 1st and 2nd mitigation counts are redundant (scoped and
+     * all device config reset). Behaves as if one mitigation attempt was already done.
+     *
+     * @param mitigationCount the mitigation attempt number (1 = first attempt etc.).
+     * @param mayPerformReboot whether or not a reboot and factory reset may be performed
+     * for the given failure.
+     * @param failedPackage in case of bootloop this is null.
+     * @return the rescue level for the n-th mitigation attempt.
+     */
+    private static @RescueLevels int getRescueLevel(int mitigationCount, boolean mayPerformReboot,
+            @Nullable VersionedPackage failedPackage) {
+        // Skipping RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET since it's not defined without a failed
+        // package.
+        if (failedPackage == null && mitigationCount > 0) {
+            mitigationCount += 1;
+        }
+        if (mitigationCount == 1) {
+            return RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET;
+        } else if (mitigationCount == 2) {
+            return RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET;
+        } else if (mitigationCount == 3) {
+            return Math.min(getMaxRescueLevel(mayPerformReboot), RESCUE_LEVEL_WARM_REBOOT);
+        } else if (mitigationCount == 4) {
+            return Math.min(getMaxRescueLevel(mayPerformReboot),
+                                RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS);
+        } else if (mitigationCount == 5) {
+            return Math.min(getMaxRescueLevel(mayPerformReboot),
+                                RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES);
+        } else if (mitigationCount == 6) {
+            return Math.min(getMaxRescueLevel(mayPerformReboot),
+                                RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS);
+        } else if (mitigationCount >= 7) {
+            return Math.min(getMaxRescueLevel(mayPerformReboot), RESCUE_LEVEL_FACTORY_RESET);
+        } else {
+            return RESCUE_LEVEL_NONE;
+        }
+    }
+
     private static void executeRescueLevel(Context context, @Nullable String failedPackage,
             int level) {
         Slog.w(TAG, "Attempting rescue level " + levelToString(level));
@@ -397,6 +484,15 @@
 
     private static void executeRescueLevelInternal(Context context, int level, @Nullable
             String failedPackage) throws Exception {
+        if (Flags.recoverabilityDetection()) {
+            executeRescueLevelInternalNew(context, level, failedPackage);
+        } else {
+            executeRescueLevelInternalOld(context, level, failedPackage);
+        }
+    }
+
+    private static void executeRescueLevelInternalOld(Context context, int level, @Nullable
+            String failedPackage) throws Exception {
 
         if (level <= LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS) {
             // Disabling flag resets on master branch for trunk stable launch.
@@ -410,8 +506,6 @@
         // Try our best to reset all settings possible, and once finished
         // rethrow any exception that we encountered
         Exception res = null;
-        Runnable runnable;
-        Thread thread;
         switch (level) {
             case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS:
                 try {
@@ -453,21 +547,7 @@
                 }
                 break;
             case LEVEL_WARM_REBOOT:
-                // Request the reboot from a separate thread to avoid deadlock on PackageWatchdog
-                // when device shutting down.
-                setRebootProperty(true);
-                runnable = () -> {
-                    try {
-                        PowerManager pm = context.getSystemService(PowerManager.class);
-                        if (pm != null) {
-                            pm.reboot(TAG);
-                        }
-                    } catch (Throwable t) {
-                        logRescueException(level, failedPackage, t);
-                    }
-                };
-                thread = new Thread(runnable);
-                thread.start();
+                executeWarmReboot(context, level, failedPackage);
                 break;
             case LEVEL_FACTORY_RESET:
                 // Before the completion of Reboot, if any crash happens then PackageWatchdog
@@ -475,23 +555,9 @@
                 // Adding a check to prevent factory reset to execute before above reboot completes.
                 // Note: this reboot property is not persistent resets after reboot is completed.
                 if (isRebootPropertySet()) {
-                    break;
+                    return;
                 }
-                setFactoryResetProperty(true);
-                long now = System.currentTimeMillis();
-                setLastFactoryResetTimeMs(now);
-                runnable = new Runnable() {
-                    @Override
-                    public void run() {
-                        try {
-                            RecoverySystem.rebootPromptAndWipeUserData(context, TAG);
-                        } catch (Throwable t) {
-                            logRescueException(level, failedPackage, t);
-                        }
-                    }
-                };
-                thread = new Thread(runnable);
-                thread.start();
+                executeFactoryReset(context, level, failedPackage);
                 break;
         }
 
@@ -500,6 +566,83 @@
         }
     }
 
+    private static void executeRescueLevelInternalNew(Context context, @RescueLevels int level,
+            @Nullable String failedPackage) throws Exception {
+        CrashRecoveryStatsLog.write(CrashRecoveryStatsLog.RESCUE_PARTY_RESET_REPORTED,
+                level, levelToString(level));
+        switch (level) {
+            case RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET:
+                // Temporary disable deviceConfig reset
+                // resetDeviceConfig(context, /*isScoped=*/true, failedPackage);
+                break;
+            case RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET:
+                // Temporary disable deviceConfig reset
+                // resetDeviceConfig(context, /*isScoped=*/false, failedPackage);
+                break;
+            case RESCUE_LEVEL_WARM_REBOOT:
+                executeWarmReboot(context, level, failedPackage);
+                break;
+            case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS:
+                resetAllSettingsIfNecessary(context, Settings.RESET_MODE_UNTRUSTED_DEFAULTS, level);
+                break;
+            case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES:
+                resetAllSettingsIfNecessary(context, Settings.RESET_MODE_UNTRUSTED_CHANGES, level);
+                break;
+            case RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS:
+                resetAllSettingsIfNecessary(context, Settings.RESET_MODE_TRUSTED_DEFAULTS, level);
+                break;
+            case RESCUE_LEVEL_FACTORY_RESET:
+                // Before the completion of Reboot, if any crash happens then PackageWatchdog
+                // escalates to next level i.e. factory reset, as they happen in separate threads.
+                // Adding a check to prevent factory reset to execute before above reboot completes.
+                // Note: this reboot property is not persistent resets after reboot is completed.
+                if (isRebootPropertySet()) {
+                    return;
+                }
+                executeFactoryReset(context, level, failedPackage);
+                break;
+        }
+    }
+
+    private static void executeWarmReboot(Context context, int level,
+            @Nullable String failedPackage) {
+        // Request the reboot from a separate thread to avoid deadlock on PackageWatchdog
+        // when device shutting down.
+        setRebootProperty(true);
+        Runnable runnable = () -> {
+            try {
+                PowerManager pm = context.getSystemService(PowerManager.class);
+                if (pm != null) {
+                    pm.reboot(TAG);
+                }
+            } catch (Throwable t) {
+                logRescueException(level, failedPackage, t);
+            }
+        };
+        Thread thread = new Thread(runnable);
+        thread.start();
+    }
+
+    private static void executeFactoryReset(Context context, int level,
+            @Nullable String failedPackage) {
+        setFactoryResetProperty(true);
+        long now = System.currentTimeMillis();
+        setLastFactoryResetTimeMs(now);
+        Runnable runnable = new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    RecoverySystem.rebootPromptAndWipeUserData(context, TAG);
+                } catch (Throwable t) {
+                    logRescueException(level, failedPackage, t);
+                }
+            }
+        };
+        Thread thread = new Thread(runnable);
+        thread.start();
+    }
+
+
     private static String getCompleteMessage(Throwable t) {
         final StringBuilder builder = new StringBuilder();
         builder.append(t.getMessage());
@@ -521,17 +664,38 @@
     }
 
     private static int mapRescueLevelToUserImpact(int rescueLevel) {
-        switch(rescueLevel) {
-            case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS:
-            case LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES:
-                return PackageHealthObserverImpact.USER_IMPACT_LEVEL_10;
-            case LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS:
-            case LEVEL_WARM_REBOOT:
-                return PackageHealthObserverImpact.USER_IMPACT_LEVEL_50;
-            case LEVEL_FACTORY_RESET:
-                return PackageHealthObserverImpact.USER_IMPACT_LEVEL_100;
-            default:
-                return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
+        if (Flags.recoverabilityDetection()) {
+            switch (rescueLevel) {
+                case RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET:
+                    return PackageHealthObserverImpact.USER_IMPACT_LEVEL_10;
+                case RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET:
+                    return PackageHealthObserverImpact.USER_IMPACT_LEVEL_20;
+                case RESCUE_LEVEL_WARM_REBOOT:
+                    return PackageHealthObserverImpact.USER_IMPACT_LEVEL_50;
+                case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS:
+                    return PackageHealthObserverImpact.USER_IMPACT_LEVEL_71;
+                case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES:
+                    return PackageHealthObserverImpact.USER_IMPACT_LEVEL_75;
+                case RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS:
+                    return PackageHealthObserverImpact.USER_IMPACT_LEVEL_80;
+                case RESCUE_LEVEL_FACTORY_RESET:
+                    return PackageHealthObserverImpact.USER_IMPACT_LEVEL_100;
+                default:
+                    return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
+            }
+        } else {
+            switch (rescueLevel) {
+                case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS:
+                case LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES:
+                    return PackageHealthObserverImpact.USER_IMPACT_LEVEL_10;
+                case LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS:
+                case LEVEL_WARM_REBOOT:
+                    return PackageHealthObserverImpact.USER_IMPACT_LEVEL_50;
+                case LEVEL_FACTORY_RESET:
+                    return PackageHealthObserverImpact.USER_IMPACT_LEVEL_100;
+                default:
+                    return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
+            }
         }
     }
 
@@ -548,7 +712,7 @@
         final ContentResolver resolver = context.getContentResolver();
         try {
             Settings.Global.resetToDefaultsAsUser(resolver, null, mode,
-                UserHandle.SYSTEM.getIdentifier());
+                    UserHandle.SYSTEM.getIdentifier());
         } catch (Exception e) {
             res = new RuntimeException("Failed to reset global settings", e);
         }
@@ -667,8 +831,13 @@
                 @FailureReasons int failureReason, int mitigationCount) {
             if (!isDisabled() && (failureReason == PackageWatchdog.FAILURE_REASON_APP_CRASH
                     || failureReason == PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING)) {
-                return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount,
+                if (Flags.recoverabilityDetection()) {
+                    return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount,
+                            mayPerformReboot(failedPackage), failedPackage));
+                } else {
+                    return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount,
                         mayPerformReboot(failedPackage)));
+                }
             } else {
                 return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
             }
@@ -682,8 +851,10 @@
             }
             if (failureReason == PackageWatchdog.FAILURE_REASON_APP_CRASH
                     || failureReason == PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING) {
-                final int level = getRescueLevel(mitigationCount,
-                        mayPerformReboot(failedPackage));
+                final int level = Flags.recoverabilityDetection() ? getRescueLevel(mitigationCount,
+                        mayPerformReboot(failedPackage), failedPackage)
+                        : getRescueLevel(mitigationCount,
+                                mayPerformReboot(failedPackage));
                 executeRescueLevel(mContext,
                         failedPackage == null ? null : failedPackage.getPackageName(), level);
                 return true;
@@ -716,7 +887,12 @@
             if (isDisabled()) {
                 return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
             }
-            return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount, true));
+            if (Flags.recoverabilityDetection()) {
+                return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount,
+                        true, /*failedPackage=*/ null));
+            } else {
+                return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount, true));
+            }
         }
 
         @Override
@@ -725,8 +901,10 @@
                 return false;
             }
             boolean mayPerformReboot = !shouldThrottleReboot();
-            executeRescueLevel(mContext, /*failedPackage=*/ null,
-                    getRescueLevel(mitigationCount, mayPerformReboot));
+            final int level = Flags.recoverabilityDetection() ? getRescueLevel(mitigationCount,
+                        mayPerformReboot, /*failedPackage=*/ null)
+                        : getRescueLevel(mitigationCount, mayPerformReboot);
+            executeRescueLevel(mContext, /*failedPackage=*/ null, level);
             return true;
         }
 
@@ -843,14 +1021,44 @@
     }
 
     private static String levelToString(int level) {
-        switch (level) {
-            case LEVEL_NONE: return "NONE";
-            case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: return "RESET_SETTINGS_UNTRUSTED_DEFAULTS";
-            case LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES: return "RESET_SETTINGS_UNTRUSTED_CHANGES";
-            case LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS: return "RESET_SETTINGS_TRUSTED_DEFAULTS";
-            case LEVEL_WARM_REBOOT: return "WARM_REBOOT";
-            case LEVEL_FACTORY_RESET: return "FACTORY_RESET";
-            default: return Integer.toString(level);
+        if (Flags.recoverabilityDetection()) {
+            switch (level) {
+                case RESCUE_LEVEL_NONE:
+                    return "NONE";
+                case RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET:
+                    return "SCOPED_DEVICE_CONFIG_RESET";
+                case RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET:
+                    return "ALL_DEVICE_CONFIG_RESET";
+                case RESCUE_LEVEL_WARM_REBOOT:
+                    return "WARM_REBOOT";
+                case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS:
+                    return "RESET_SETTINGS_UNTRUSTED_DEFAULTS";
+                case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES:
+                    return "RESET_SETTINGS_UNTRUSTED_CHANGES";
+                case RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS:
+                    return "RESET_SETTINGS_TRUSTED_DEFAULTS";
+                case RESCUE_LEVEL_FACTORY_RESET:
+                    return "FACTORY_RESET";
+                default:
+                    return Integer.toString(level);
+            }
+        } else {
+            switch (level) {
+                case LEVEL_NONE:
+                    return "NONE";
+                case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS:
+                    return "RESET_SETTINGS_UNTRUSTED_DEFAULTS";
+                case LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES:
+                    return "RESET_SETTINGS_UNTRUSTED_CHANGES";
+                case LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS:
+                    return "RESET_SETTINGS_TRUSTED_DEFAULTS";
+                case LEVEL_WARM_REBOOT:
+                    return "WARM_REBOOT";
+                case LEVEL_FACTORY_RESET:
+                    return "FACTORY_RESET";
+                default:
+                    return Integer.toString(level);
+            }
         }
     }
 }
diff --git a/packages/CrashRecovery/services/java/com/android/server/rollback/RollbackPackageHealthObserver.java b/packages/CrashRecovery/services/java/com/android/server/rollback/RollbackPackageHealthObserver.java
index 0fb9327..93f26ae 100644
--- a/packages/CrashRecovery/services/java/com/android/server/rollback/RollbackPackageHealthObserver.java
+++ b/packages/CrashRecovery/services/java/com/android/server/rollback/RollbackPackageHealthObserver.java
@@ -69,7 +69,7 @@
  *
  * @hide
  */
-final class RollbackPackageHealthObserver implements PackageHealthObserver {
+public final class RollbackPackageHealthObserver implements PackageHealthObserver {
     private static final String TAG = "RollbackPackageHealthObserver";
     private static final String NAME = "rollback-observer";
     private static final int PERSISTENT_MASK = ApplicationInfo.FLAG_PERSISTENT
@@ -89,7 +89,7 @@
     private boolean mTwoPhaseRollbackEnabled;
 
     @VisibleForTesting
-    RollbackPackageHealthObserver(Context context, ApexManager apexManager) {
+    public RollbackPackageHealthObserver(Context context, ApexManager apexManager) {
         mContext = context;
         HandlerThread handlerThread = new HandlerThread("RollbackPackageHealthObserver");
         handlerThread.start();
diff --git a/packages/CredentialManager/shared/AndroidManifest.xml b/packages/CredentialManager/shared/AndroidManifest.xml
index a460887..51c7fb6 100644
--- a/packages/CredentialManager/shared/AndroidManifest.xml
+++ b/packages/CredentialManager/shared/AndroidManifest.xml
@@ -17,6 +17,6 @@
  */
 -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.credentialmanager">
+    package="com.android.credentialmanager.shared">
 
 </manifest>
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt
index 892eabf..12cb7ff 100644
--- a/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt
@@ -40,13 +40,13 @@
 import androidx.credentials.provider.PublicKeyCredentialEntry
 import androidx.credentials.provider.RemoteEntry
 import com.android.credentialmanager.IS_AUTO_SELECTED_KEY
-import com.android.credentialmanager.R
 import com.android.credentialmanager.model.get.ActionEntryInfo
 import com.android.credentialmanager.model.get.AuthenticationEntryInfo
 import com.android.credentialmanager.model.get.CredentialEntryInfo
 import com.android.credentialmanager.model.CredentialType
 import com.android.credentialmanager.model.get.ProviderInfo
 import com.android.credentialmanager.model.get.RemoteEntryInfo
+import com.android.credentialmanager.shared.R
 import com.android.credentialmanager.TAG
 import com.android.credentialmanager.model.EntryInfo
 
@@ -386,4 +386,4 @@
         PackageManager.PackageInfoFlags.of(
             (packageManagerFlags).toLong())
     )
-}
\ No newline at end of file
+}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt
index 99a9409..d13d86f 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt
@@ -305,10 +305,14 @@
         modifier = Modifier.fillMaxWidth()
     ) {
         if (leftButton != null) {
-            leftButton()
+            Box(modifier = Modifier.wrapContentSize().weight(1f, fill = false)) {
+                leftButton()
+            }
         }
         if (rightButton != null) {
-            rightButton()
+            Box(modifier = Modifier.wrapContentSize().weight(1f, fill = false)) {
+                rightButton()
+            }
         }
     }
 }
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt
index a46e358..3fb91522 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt
@@ -17,7 +17,6 @@
 package com.android.credentialmanager.common.ui
 
 import android.content.Context
-import android.content.res.Configuration
 import android.widget.RemoteViews
 import androidx.core.content.ContextCompat
 import com.android.credentialmanager.model.get.CredentialEntryInfo
@@ -27,10 +26,12 @@
 class RemoteViewsFactory {
 
     companion object {
-        private const val setAdjustViewBoundsMethodName = "setAdjustViewBounds"
-        private const val setMaxHeightMethodName = "setMaxHeight"
-        private const val setBackgroundResourceMethodName = "setBackgroundResource"
-        private const val bulletPoint = "\u2022"
+        private const val SET_ADJUST_VIEW_BOUNDS_METHOD_NAME = "setAdjustViewBounds"
+        private const val SET_MAX_HEIGHT_METHOD_NAME = "setMaxHeight"
+        private const val SET_BACKGROUND_RESOURCE_METHOD_NAME = "setBackgroundResource"
+        private const val BULLET_POINT = "\u2022"
+        // TODO(jbabs): RemoteViews#setViewPadding renders this as 8dp on the display. Debug why.
+        private const val END_ITEMS_PADDING = 28
 
         fun createDropdownPresentation(
             context: Context,
@@ -50,18 +51,18 @@
             val secondaryText =
                 if (credentialEntryInfo.displayName != null
                     && (credentialEntryInfo.displayName != credentialEntryInfo.userName))
-                    (credentialEntryInfo.userName + " " + bulletPoint + " "
+                    (credentialEntryInfo.userName + " " + BULLET_POINT + " "
                             + credentialEntryInfo.credentialTypeDisplayName
-                            + " " + bulletPoint + " " + credentialEntryInfo.providerDisplayName)
-                else (credentialEntryInfo.credentialTypeDisplayName + " " + bulletPoint + " "
+                            + " " + BULLET_POINT + " " + credentialEntryInfo.providerDisplayName)
+                else (credentialEntryInfo.credentialTypeDisplayName + " " + BULLET_POINT + " "
                         + credentialEntryInfo.providerDisplayName)
             remoteViews.setTextViewText(android.R.id.text2, secondaryText)
             remoteViews.setImageViewIcon(android.R.id.icon1, icon);
             remoteViews.setBoolean(
-                android.R.id.icon1, setAdjustViewBoundsMethodName, true);
+                android.R.id.icon1, SET_ADJUST_VIEW_BOUNDS_METHOD_NAME, true);
             remoteViews.setInt(
                 android.R.id.icon1,
-                setMaxHeightMethodName,
+                SET_MAX_HEIGHT_METHOD_NAME,
                 context.resources.getDimensionPixelSize(
                     com.android.credentialmanager.R.dimen.autofill_icon_size));
             remoteViews.setContentDescription(android.R.id.icon1, credentialEntryInfo
@@ -71,11 +72,11 @@
                     com.android.credentialmanager.R.drawable.fill_dialog_dynamic_list_item_one else
                     com.android.credentialmanager.R.drawable.fill_dialog_dynamic_list_item_middle
             remoteViews.setInt(
-                android.R.id.content, setBackgroundResourceMethodName, drawableId);
+                android.R.id.content, SET_BACKGROUND_RESOURCE_METHOD_NAME, drawableId);
             if (isFirstEntry) remoteViews.setViewPadding(
                 com.android.credentialmanager.R.id.credential_card,
                 /* left=*/0,
-                /* top=*/8,
+                /* top=*/END_ITEMS_PADDING,
                 /* right=*/0,
                 /* bottom=*/0)
             if (isLastEntry) remoteViews.setViewPadding(
@@ -83,7 +84,7 @@
                 /*left=*/0,
                 /* top=*/0,
                 /* right=*/0,
-                /* bottom=*/8)
+                /* bottom=*/END_ITEMS_PADDING)
             return remoteViews
         }
 
@@ -95,16 +96,16 @@
                 com.android.credentialmanager
                         .R.string.dropdown_presentation_more_sign_in_options_text))
             remoteViews.setBoolean(
-                android.R.id.icon1, setAdjustViewBoundsMethodName, true);
+                android.R.id.icon1, SET_ADJUST_VIEW_BOUNDS_METHOD_NAME, true);
             remoteViews.setInt(
                 android.R.id.icon1,
-                setMaxHeightMethodName,
+                SET_MAX_HEIGHT_METHOD_NAME,
                 context.resources.getDimensionPixelSize(
                     com.android.credentialmanager.R.dimen.autofill_icon_size));
             val drawableId =
                 com.android.credentialmanager.R.drawable.more_options_list_item
             remoteViews.setInt(
-                android.R.id.content, setBackgroundResourceMethodName, drawableId);
+                android.R.id.content, SET_BACKGROUND_RESOURCE_METHOD_NAME, drawableId);
             return remoteViews
         }
     }
diff --git a/packages/InputDevices/res/raw/keyboard_layout_azerbaijani.kcm b/packages/InputDevices/res/raw/keyboard_layout_azerbaijani.kcm
index 3f5e894..f2843ed 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_azerbaijani.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_azerbaijani.kcm
@@ -18,6 +18,8 @@
 
 type OVERLAY
 
+map key 86 PLUS
+
 ### ROW 1
 
 key GRAVE {
@@ -42,13 +44,14 @@
 key 3 {
     label:                              '3'
     base:                               '3'
-    shift:                              '\u2166'
+    shift:                              '\u2116'
 }
 
 key 4 {
     label:                              '4'
     base:                               '4'
     shift:                              ';'
+    ralt:                               '\u20bc'
 }
 
 key 5 {
@@ -61,14 +64,12 @@
     label:                              '6'
     base:                               '6'
     shift:                              ':'
-    shift+ralt:                         '^'
 }
 
 key 7 {
     label:                              '7'
     base:                               '7'
     shift:                              '?'
-    ralt:                               '&'
 }
 
 key 8 {
@@ -176,21 +177,21 @@
 key LEFT_BRACKET {
     label:                              '\u00d6'
     base:                               '\u00f6'
-    shift:                              '\u00d6'
+    shift, capslock:                    '\u00d6'
     shift+capslock:                     '\u00f6'
 }
 
 key RIGHT_BRACKET {
     label:                              '\u011e'
     base:                               '\u011f'
-    shift:                              '\u011e'
+    shift, capslock:                    '\u011e'
     shift+capslock:                     '\u011f'
 }
 
 key BACKSLASH {
     label:                              '\\'
     base:                               '\\'
-    shift:                              '|'
+    shift:                              '/'
 }
 
 ### ROW 3
@@ -261,19 +262,25 @@
 key SEMICOLON {
     label:                              'I'
     base:                               '\u0131'
-    shift:                              'I'
+    shift, capslock:                    'I'
     shift+capslock:                     '\u0131'
 }
 
 key APOSTROPHE {
     label:                              '\u018f'
     base:                               '\u0259'
-    shift:                              '\u018f'
+    shift, capslock:                    '\u018f'
     shift+capslock:                     '\u0259'
 }
 
 ### ROW 4
 
+key PLUS {
+    label:                              '\\'
+    base:                               '\\'
+    shift:                              '/'
+}
+
 key Z {
     label:                              'Z'
     base:                               'z'
@@ -326,14 +333,14 @@
 key COMMA {
     label:                              '\u00c7'
     base:                               '\u00e7'
-    shift:                              '\u00c7'
+    shift, capslock:                    '\u00c7'
     shift+capslock:                     '\u00e7'
 }
 
 key PERIOD {
     label:                              '\u015e'
     base:                               '\u015f'
-    shift:                              '\u015e'
+    shift, capslock:                    '\u015e'
     shift+capslock:                     '\u015f'
 }
 
diff --git a/packages/InputDevices/res/raw/keyboard_layout_german.kcm b/packages/InputDevices/res/raw/keyboard_layout_german.kcm
index 23ccc9a..fbb9bb6 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_german.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_german.kcm
@@ -101,6 +101,7 @@
 key SLASH {
     label:                              '\u00df'
     base:                               '\u00df'
+    capslock:                           '\u1e9e'
     shift:                              '?'
     ralt:                               '\\'
 }
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java
index ef418a5..a4c6ac7 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java
@@ -28,6 +28,7 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageInstaller;
+import android.content.pm.PackageInstaller.SessionInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ProviderInfo;
 import android.net.Uri;
@@ -38,7 +39,6 @@
 import android.text.TextUtils;
 import android.util.EventLog;
 import android.util.Log;
-import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import com.android.packageinstaller.v2.ui.InstallLaunch;
 import java.util.Arrays;
@@ -51,6 +51,7 @@
     private static final String TAG = InstallStart.class.getSimpleName();
 
     private PackageManager mPackageManager;
+    private PackageInstaller mPackageInstaller;
     private UserManager mUserManager;
     private boolean mAbortInstall = false;
     private boolean mShouldFinish = true;
@@ -66,7 +67,7 @@
             Log.i(TAG, "Using Pia V2");
 
             Intent piaV2 = new Intent(getIntent());
-            piaV2.putExtra(InstallLaunch.EXTRA_CALLING_PKG_NAME, getCallingPackage());
+            piaV2.putExtra(InstallLaunch.EXTRA_CALLING_PKG_NAME, getLaunchedFromPackage());
             piaV2.putExtra(InstallLaunch.EXTRA_CALLING_PKG_UID, getLaunchedFromUid());
             piaV2.setClass(this, InstallLaunch.class);
             piaV2.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
@@ -75,10 +76,11 @@
             return;
         }
         mPackageManager = getPackageManager();
+        mPackageInstaller = mPackageManager.getPackageInstaller();
         mUserManager = getSystemService(UserManager.class);
 
         Intent intent = getIntent();
-        String callingPackage = getCallingPackage();
+        String callingPackage = getLaunchedFromPackage();
         String callingAttributionTag = null;
 
         // Uid of the source package, coming from ActivityManager
@@ -87,31 +89,33 @@
             Log.w(TAG, "Could not determine the launching uid.");
         }
 
+        // The UID of the origin of the installation. Note that it can be different than the
+        // "installer" of the session. For instance, if a 3P caller launched PIA with an ACTION_VIEW
+        // intent, the originatingUid is the 3P caller, but the "installer" in this case would
+        // be PIA.
+        int originatingUid = callingUid;
+
         final boolean isSessionInstall =
                 PackageInstaller.ACTION_CONFIRM_PRE_APPROVAL.equals(intent.getAction())
                         || PackageInstaller.ACTION_CONFIRM_INSTALL.equals(intent.getAction());
 
-        // If the activity was started via a PackageInstaller session, we retrieve the calling
-        // package from that session
+        // If the activity was started via a PackageInstaller session, we retrieve the originating
+        // UID from that session
         final int sessionId = (isSessionInstall
-                ? intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1)
-                : -1);
-        int originatingUidFromSession = callingUid;
-        if (callingPackage == null && sessionId != -1) {
-            PackageInstaller packageInstaller = getPackageManager().getPackageInstaller();
-            PackageInstaller.SessionInfo sessionInfo = packageInstaller.getSessionInfo(sessionId);
+                ? intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, SessionInfo.INVALID_ID)
+                : SessionInfo.INVALID_ID);
+        if (sessionId != SessionInfo.INVALID_ID) {
+            PackageInstaller.SessionInfo sessionInfo = mPackageInstaller.getSessionInfo(sessionId);
             if (sessionInfo != null) {
-                callingPackage = sessionInfo.getInstallerPackageName();
                 callingAttributionTag = sessionInfo.getInstallerAttributionTag();
-                originatingUidFromSession = sessionInfo.getOriginatingUid();
+                if (sessionInfo.getOriginatingUid() != Process.INVALID_UID) {
+                    originatingUid = sessionInfo.getOriginatingUid();
+                }
             }
         }
 
         final ApplicationInfo sourceInfo = getSourceInfo(callingPackage);
 
-        // Uid of the source package, with a preference to uid from ApplicationInfo
-        final int originatingUid = sourceInfo != null ? sourceInfo.uid : callingUid;
-
         if (callingUid == Process.INVALID_UID && sourceInfo == null) {
             Log.e(TAG, "Cannot determine caller since UID is invalid and sourceInfo is null");
             mAbortInstall = true;
@@ -124,28 +128,28 @@
         boolean isTrustedSource = false;
         if (sourceInfo != null && sourceInfo.isPrivilegedApp()) {
             isTrustedSource = intent.getBooleanExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, false) || (
-                    originatingUid != Process.INVALID_UID && checkPermission(
-                            Manifest.permission.INSTALL_PACKAGES, -1 /* pid */, originatingUid)
-                            == PackageManager.PERMISSION_GRANTED);
+                callingUid != Process.INVALID_UID && checkPermission(
+                    Manifest.permission.INSTALL_PACKAGES, -1 /* pid */, callingUid)
+                    == PackageManager.PERMISSION_GRANTED);
         }
 
         if (!isTrustedSource && !isSystemDownloadsProvider && !isDocumentsManager
-                && originatingUid != Process.INVALID_UID) {
-            final int targetSdkVersion = getMaxTargetSdkVersionForUid(this, originatingUid);
+                && callingUid != Process.INVALID_UID) {
+            final int targetSdkVersion = getMaxTargetSdkVersionForUid(this, callingUid);
             if (targetSdkVersion < 0) {
-                Log.e(TAG, "Cannot get target sdk version for uid " + originatingUid);
+                Log.e(TAG, "Cannot get target sdk version for uid " + callingUid);
                 // Invalid originating uid supplied. Abort install.
                 mAbortInstall = true;
             } else if (targetSdkVersion >= Build.VERSION_CODES.O && !isUidRequestingPermission(
-                    originatingUid, Manifest.permission.REQUEST_INSTALL_PACKAGES)) {
-                Log.e(TAG, "Requesting uid " + originatingUid + " needs to declare permission "
+                callingUid, Manifest.permission.REQUEST_INSTALL_PACKAGES)) {
+                Log.e(TAG, "Requesting uid " + callingUid + " needs to declare permission "
                         + Manifest.permission.REQUEST_INSTALL_PACKAGES);
                 mAbortInstall = true;
             }
         }
 
-        if (sessionId != -1 && !isCallerSessionOwner(originatingUid, sessionId)) {
-            Log.e(TAG, "UID " + originatingUid + " is not the owner of session " +
+        if (sessionId != -1 && !isCallerSessionOwner(callingUid, sessionId)) {
+            Log.e(TAG, "CallingUid " + callingUid + " is not the owner of session " +
                 sessionId);
             mAbortInstall = true;
         }
@@ -155,10 +159,9 @@
         final String installerPackageNameFromIntent = getIntent().getStringExtra(
                 Intent.EXTRA_INSTALLER_PACKAGE_NAME);
         if (installerPackageNameFromIntent != null) {
-            final String callingPkgName = getLaunchedFromPackage();
-            if (!TextUtils.equals(installerPackageNameFromIntent, callingPkgName)
+            if (!TextUtils.equals(installerPackageNameFromIntent, callingPackage)
                     && mPackageManager.checkPermission(Manifest.permission.INSTALL_PACKAGES,
-                    callingPkgName) != PackageManager.PERMISSION_GRANTED) {
+                    callingPackage) != PackageManager.PERMISSION_GRANTED) {
                 Log.e(TAG, "The given installer package name " + installerPackageNameFromIntent
                         + " is invalid. Remove it.");
                 EventLog.writeEvent(0x534e4554, "236687884", getLaunchedFromUid(),
@@ -186,8 +189,7 @@
                 callingAttributionTag);
         nextActivity.putExtra(PackageInstallerActivity.EXTRA_ORIGINAL_SOURCE_INFO, sourceInfo);
         nextActivity.putExtra(Intent.EXTRA_ORIGINATING_UID, originatingUid);
-        nextActivity.putExtra(PackageInstallerActivity.EXTRA_ORIGINATING_UID_FROM_SESSION_INFO,
-            originatingUidFromSession);
+        nextActivity.putExtra(PackageInstallerActivity.EXTRA_IS_TRUSTED_SOURCE, isTrustedSource);
 
         if (isSessionInstall) {
             nextActivity.setClass(this, PackageInstallerActivity.class);
@@ -257,7 +259,7 @@
     private ApplicationInfo getSourceInfo(@Nullable String callingPackage) {
         if (callingPackage != null) {
             try {
-                return getPackageManager().getApplicationInfo(callingPackage, 0);
+                return mPackageManager.getApplicationInfo(callingPackage, 0);
             } catch (PackageManager.NameNotFoundException ex) {
                 // ignore
             }
@@ -265,8 +267,6 @@
         return null;
     }
 
-
-    @NonNull
     private boolean canPackageQuery(int callingUid, Uri packageUri) {
         ProviderInfo info = mPackageManager.resolveContentProvider(packageUri.getAuthority(),
                 PackageManager.ComponentInfoFlags.of(0));
@@ -291,17 +291,16 @@
         return false;
     }
 
-    private boolean isCallerSessionOwner(int originatingUid, int sessionId) {
-        if (originatingUid == Process.ROOT_UID) {
+    private boolean isCallerSessionOwner(int callingUid, int sessionId) {
+        if (callingUid == Process.ROOT_UID) {
             return true;
         }
-        PackageInstaller packageInstaller = getPackageManager().getPackageInstaller();
-        PackageInstaller.SessionInfo sessionInfo = packageInstaller.getSessionInfo(sessionId);
+        PackageInstaller.SessionInfo sessionInfo = mPackageInstaller.getSessionInfo(sessionId);
         if (sessionInfo == null) {
             return false;
         }
         int installerUid = sessionInfo.getInstallerUid();
-        return originatingUid == installerUid;
+        return callingUid == installerUid;
     }
 
     private void checkDevicePolicyRestrictions() {
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
index 45bfe54..8bed945 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
@@ -84,8 +84,7 @@
     static final String EXTRA_ORIGINAL_SOURCE_INFO = "EXTRA_ORIGINAL_SOURCE_INFO";
     static final String EXTRA_STAGED_SESSION_ID = "EXTRA_STAGED_SESSION_ID";
     static final String EXTRA_APP_SNIPPET = "EXTRA_APP_SNIPPET";
-    static final String EXTRA_ORIGINATING_UID_FROM_SESSION_INFO =
-        "EXTRA_ORIGINATING_UID_FROM_SESSION_INFO";
+    static final String EXTRA_IS_TRUSTED_SOURCE = "EXTRA_IS_TRUSTED_SOURCE";
     private static final String ALLOW_UNKNOWN_SOURCES_KEY =
             PackageInstallerActivity.class.getName() + "ALLOW_UNKNOWN_SOURCES_KEY";
 
@@ -98,10 +97,6 @@
      * The package name corresponding to #mOriginatingUid
      */
     private String mOriginatingPackage;
-    /**
-     * The package name corresponding to the app updater in the update-ownership confirmation dialog
-     */
-    private String mOriginatingPackageFromSessionInfo;
     private int mActivityResultCode = Activity.RESULT_CANCELED;
     private int mPendingUserActionReason = -1;
 
@@ -154,8 +149,7 @@
             viewToEnable = mDialog.requireViewById(R.id.install_confirm_question_update);
 
             final CharSequence existingUpdateOwnerLabel = getExistingUpdateOwnerLabel();
-            final CharSequence requestedUpdateOwnerLabel =
-                getApplicationLabel(mOriginatingPackageFromSessionInfo);
+            final CharSequence requestedUpdateOwnerLabel = getApplicationLabel(mOriginatingPackage);
             if (!TextUtils.isEmpty(existingUpdateOwnerLabel)
                     && mPendingUserActionReason == PackageInstaller.REASON_REMIND_OWNERSHIP) {
                 String updateOwnerString =
@@ -304,21 +298,6 @@
         return packagesForUid[0];
     }
 
-    private boolean isInstallRequestFromUnknownSource(Intent intent) {
-        if (mCallingPackage != null && intent.getBooleanExtra(
-                Intent.EXTRA_NOT_UNKNOWN_SOURCE, false)) {
-            if (mSourceInfo != null && mSourceInfo.isPrivilegedApp()) {
-                // Privileged apps can bypass unknown sources check if they want.
-                return false;
-            }
-        }
-        if (mSourceInfo != null && checkPermission(Manifest.permission.INSTALL_PACKAGES,
-                -1 /* pid */, mSourceInfo.uid) == PackageManager.PERMISSION_GRANTED) {
-            return false;
-        }
-        return true;
-    }
-
     private void initiateInstall() {
         String pkgName = mPkgInfo.packageName;
         // Check if there is already a package on the device with this name
@@ -384,15 +363,9 @@
         mCallingPackage = intent.getStringExtra(EXTRA_CALLING_PACKAGE);
         mCallingAttributionTag = intent.getStringExtra(EXTRA_CALLING_ATTRIBUTION_TAG);
         mSourceInfo = intent.getParcelableExtra(EXTRA_ORIGINAL_SOURCE_INFO);
-        mOriginatingUid = intent.getIntExtra(Intent.EXTRA_ORIGINATING_UID,
-                Process.INVALID_UID);
+        mOriginatingUid = intent.getIntExtra(Intent.EXTRA_ORIGINATING_UID, Process.INVALID_UID);
         mOriginatingPackage = (mOriginatingUid != Process.INVALID_UID)
                 ? getPackageNameForUid(mOriginatingUid) : null;
-        int originatingUidFromSessionInfo =
-            intent.getIntExtra(EXTRA_ORIGINATING_UID_FROM_SESSION_INFO, Process.INVALID_UID);
-        mOriginatingPackageFromSessionInfo = (originatingUidFromSessionInfo != Process.INVALID_UID)
-            ? getPackageNameForUid(originatingUidFromSessionInfo) : mCallingPackage;
-
 
         final Object packageSource;
         if (PackageInstaller.ACTION_CONFIRM_INSTALL.equals(action)) {
@@ -557,7 +530,7 @@
      * Check if it is allowed to install the package and initiate install if allowed.
      */
     private void checkIfAllowedAndInitiateInstall() {
-        if (mAllowUnknownSources || !isInstallRequestFromUnknownSource(getIntent())) {
+        if (mAllowUnknownSources || getIntent().getBooleanExtra(EXTRA_IS_TRUSTED_SOURCE, false)) {
             if (mLocalLOGV) Log.i(TAG, "install allowed");
             initiateInstall();
         } else {
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
index 32795e4..e48c0f4 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
@@ -96,6 +96,7 @@
     var stagedSessionId = SessionInfo.INVALID_ID
         private set
     private var callingUid = Process.INVALID_UID
+    private var originatingUid = Process.INVALID_UID
     private var callingPackage: String? = null
     private var sessionStager: SessionStager? = null
     private lateinit var intent: Intent
@@ -148,7 +149,7 @@
         }
         val sourceInfo: ApplicationInfo? = getSourceInfo(callingPackage)
         // Uid of the source package, with a preference to uid from ApplicationInfo
-        val originatingUid = sourceInfo?.uid ?: callingUid
+        originatingUid = sourceInfo?.uid ?: callingUid
         appOpRequestInfo = AppOpRequestInfo(
             getPackageNameForUid(context, originatingUid, callingPackage),
             originatingUid, callingAttributionTag
@@ -282,7 +283,7 @@
                     context.contentResolver.openAssetFileDescriptor(uri, "r").use { afd ->
                         val pfd: ParcelFileDescriptor? = afd?.parcelFileDescriptor
                         val params: SessionParams =
-                            createSessionParams(intent, pfd, uri.toString())
+                            createSessionParams(originatingUid, intent, pfd, uri.toString())
                         stagedSessionId = packageInstaller.createSession(params)
                     }
                 } catch (e: Exception) {
@@ -338,6 +339,7 @@
     }
 
     private fun createSessionParams(
+        originatingUid: Int,
         intent: Intent,
         pfd: ParcelFileDescriptor?,
         debugPathName: String,
@@ -354,9 +356,7 @@
         params.setOriginatingUri(
             intent.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI, Uri::class.java)
         )
-        params.setOriginatingUid(
-            intent.getIntExtra(Intent.EXTRA_ORIGINATING_UID, Process.INVALID_UID)
-        )
+        params.setOriginatingUid(originatingUid)
         params.setInstallerPackageName(intent.getStringExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME))
         params.setInstallReason(PackageManager.INSTALL_REASON_USER)
         // Disable full screen intent usage by for sideloads.
diff --git a/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/KeyedObserverTest.kt b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/KeyedObserverTest.kt
new file mode 100644
index 0000000..b52586c
--- /dev/null
+++ b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/KeyedObserverTest.kt
@@ -0,0 +1,215 @@
+/*
+ * 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.settingslib.datastore
+
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors.directExecutor
+import java.util.concurrent.Executor
+import java.util.concurrent.atomic.AtomicInteger
+import org.junit.Assert
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
+import org.mockito.kotlin.never
+import org.mockito.kotlin.reset
+import org.mockito.kotlin.verify
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class KeyedObserverTest {
+    @get:Rule
+    val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+    @Mock
+    private lateinit var observer1: KeyedObserver<Any?>
+
+    @Mock
+    private lateinit var observer2: KeyedObserver<Any?>
+
+    @Mock
+    private lateinit var keyedObserver1: KeyedObserver<Any>
+
+    @Mock
+    private lateinit var keyedObserver2: KeyedObserver<Any>
+
+    @Mock
+    private lateinit var key1: Any
+
+    @Mock
+    private lateinit var key2: Any
+
+    @Mock
+    private lateinit var executor: Executor
+
+    private val keyedObservable = KeyedDataObservable<Any>()
+
+    @Test
+    fun addObserver_sameExecutor() {
+        keyedObservable.addObserver(observer1, executor)
+        keyedObservable.addObserver(observer1, executor)
+    }
+
+    @Test
+    fun addObserver_keyedObserver_sameExecutor() {
+        keyedObservable.addObserver(key1, keyedObserver1, executor)
+        keyedObservable.addObserver(key1, keyedObserver1, executor)
+    }
+
+    @Test
+    fun addObserver_differentExecutor() {
+        keyedObservable.addObserver(observer1, executor)
+        Assert.assertThrows(IllegalStateException::class.java) {
+            keyedObservable.addObserver(observer1, directExecutor())
+        }
+    }
+
+    @Test
+    fun addObserver_keyedObserver_differentExecutor() {
+        keyedObservable.addObserver(key1, keyedObserver1, executor)
+        Assert.assertThrows(IllegalStateException::class.java) {
+            keyedObservable.addObserver(key1, keyedObserver1, directExecutor())
+        }
+    }
+
+    @Test
+    fun addObserver_weaklyReferenced() {
+        val counter = AtomicInteger()
+        var observer: KeyedObserver<Any?>? = KeyedObserver { _, _ -> counter.incrementAndGet() }
+        keyedObservable.addObserver(observer!!, directExecutor())
+
+        keyedObservable.notifyChange(ChangeReason.UPDATE)
+        assertThat(counter.get()).isEqualTo(1)
+
+        // trigger GC, the observer callback should not be invoked
+        null.also { observer = it }
+        System.gc()
+        System.runFinalization()
+
+        keyedObservable.notifyChange(ChangeReason.UPDATE)
+        assertThat(counter.get()).isEqualTo(1)
+    }
+
+    @Test
+    fun addObserver_keyedObserver_weaklyReferenced() {
+        val counter = AtomicInteger()
+        var keyObserver: KeyedObserver<Any>? = KeyedObserver { _, _ -> counter.incrementAndGet() }
+        keyedObservable.addObserver(key1, keyObserver!!, directExecutor())
+
+        keyedObservable.notifyChange(key1, ChangeReason.UPDATE)
+        assertThat(counter.get()).isEqualTo(1)
+
+        // trigger GC, the observer callback should not be invoked
+        null.also { keyObserver = it }
+        System.gc()
+        System.runFinalization()
+
+        keyedObservable.notifyChange(key1, ChangeReason.UPDATE)
+        assertThat(counter.get()).isEqualTo(1)
+    }
+
+    @Test
+    fun addObserver_notifyObservers_removeObserver() {
+        keyedObservable.addObserver(observer1, directExecutor())
+        keyedObservable.addObserver(observer2, executor)
+
+        keyedObservable.notifyChange(ChangeReason.UPDATE)
+        verify(observer1).onKeyChanged(null, ChangeReason.UPDATE)
+        verify(observer2, never()).onKeyChanged(any(), any())
+        verify(executor).execute(any())
+
+        reset(observer1, executor)
+        keyedObservable.removeObserver(observer2)
+
+        keyedObservable.notifyChange(ChangeReason.DELETE)
+        verify(observer1).onKeyChanged(null, ChangeReason.DELETE)
+        verify(executor, never()).execute(any())
+    }
+
+    @Test
+    fun addObserver_keyedObserver_notifyObservers_removeObserver() {
+        keyedObservable.addObserver(key1, keyedObserver1, directExecutor())
+        keyedObservable.addObserver(key2, keyedObserver2, executor)
+
+        keyedObservable.notifyChange(key1, ChangeReason.UPDATE)
+        verify(keyedObserver1).onKeyChanged(key1, ChangeReason.UPDATE)
+        verify(keyedObserver2, never()).onKeyChanged(any(), any())
+        verify(executor, never()).execute(any())
+
+        reset(keyedObserver1, executor)
+        keyedObservable.removeObserver(key2, keyedObserver2)
+
+        keyedObservable.notifyChange(key1, ChangeReason.DELETE)
+        verify(keyedObserver1).onKeyChanged(key1, ChangeReason.DELETE)
+        verify(executor, never()).execute(any())
+    }
+
+    @Test
+    fun notifyChange_addMoreTypeObservers_checkOnKeyChanged() {
+        keyedObservable.addObserver(observer1, directExecutor())
+        keyedObservable.addObserver(key1, keyedObserver1, directExecutor())
+        keyedObservable.addObserver(key2, keyedObserver2, directExecutor())
+
+        keyedObservable.notifyChange(ChangeReason.UPDATE)
+        verify(observer1).onKeyChanged(null, ChangeReason.UPDATE)
+        verify(keyedObserver1).onKeyChanged(key1, ChangeReason.UPDATE)
+        verify(keyedObserver2).onKeyChanged(key2, ChangeReason.UPDATE)
+
+        reset(observer1, keyedObserver1, keyedObserver2)
+        keyedObservable.notifyChange(key1, ChangeReason.UPDATE)
+
+        verify(observer1).onKeyChanged(key1, ChangeReason.UPDATE)
+        verify(keyedObserver1).onKeyChanged(key1, ChangeReason.UPDATE)
+        verify(keyedObserver2, never()).onKeyChanged(key1, ChangeReason.UPDATE)
+
+        reset(observer1, keyedObserver1, keyedObserver2)
+        keyedObservable.notifyChange(key2, ChangeReason.UPDATE)
+
+        verify(observer1).onKeyChanged(key2, ChangeReason.UPDATE)
+        verify(keyedObserver1, never()).onKeyChanged(key2, ChangeReason.UPDATE)
+        verify(keyedObserver2).onKeyChanged(key2, ChangeReason.UPDATE)
+    }
+
+    @Test
+    fun notifyChange_addObserverWithinCallback() {
+        // ConcurrentModificationException is raised if it is not implemented correctly
+        val observer: KeyedObserver<Any?> = KeyedObserver { _, _ ->
+            keyedObservable.addObserver(observer1, executor)
+        }
+
+        keyedObservable.addObserver(observer, directExecutor())
+
+        keyedObservable.notifyChange(ChangeReason.UPDATE)
+        keyedObservable.removeObserver(observer)
+    }
+
+    @Test
+    fun notifyChange_KeyedObserver_addObserverWithinCallback() {
+        // ConcurrentModificationException is raised if it is not implemented correctly
+        val keyObserver: KeyedObserver<Any?> = KeyedObserver { _, _ ->
+            keyedObservable.addObserver(key1, keyedObserver1, executor)
+        }
+
+        keyedObservable.addObserver(key1, keyObserver, directExecutor())
+
+        keyedObservable.notifyChange(key1, ChangeReason.UPDATE)
+        keyedObservable.removeObserver(key1, keyObserver)
+    }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/ObserverTest.kt b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/ObserverTest.kt
index bb791dc..f065829 100644
--- a/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/ObserverTest.kt
+++ b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/ObserverTest.kt
@@ -69,8 +69,7 @@
         assertThat(counter.get()).isEqualTo(1)
 
         // trigger GC, the observer callback should not be invoked
-        @Suppress("unused")
-        observer = null
+        null.also { observer = it }
         System.gc()
         System.runFinalization()
 
@@ -100,10 +99,12 @@
     @Test
     fun notifyChange_addObserverWithinCallback() {
         // ConcurrentModificationException is raised if it is not implemented correctly
+        val observer = Observer { observable.addObserver(observer1, executor) }
         observable.addObserver(
-            { observable.addObserver(observer1, executor) },
+            observer,
             MoreExecutors.directExecutor()
         )
         observable.notifyChange(ChangeReason.UPDATE)
+        observable.removeObserver(observer)
     }
 }
diff --git a/packages/SettingsLib/ProfileSelector/Android.bp b/packages/SettingsLib/ProfileSelector/Android.bp
index 6dc07b2..4aa67c1 100644
--- a/packages/SettingsLib/ProfileSelector/Android.bp
+++ b/packages/SettingsLib/ProfileSelector/Android.bp
@@ -20,6 +20,7 @@
     static_libs: [
         "com.google.android.material_material",
         "SettingsLibSettingsTheme",
+        "android.os.flags-aconfig-java-export",
     ],
 
     sdk_version: "system_current",
diff --git a/packages/SettingsLib/ProfileSelector/AndroidManifest.xml b/packages/SettingsLib/ProfileSelector/AndroidManifest.xml
index 80f6b76..303e20c 100644
--- a/packages/SettingsLib/ProfileSelector/AndroidManifest.xml
+++ b/packages/SettingsLib/ProfileSelector/AndroidManifest.xml
@@ -18,5 +18,5 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="com.android.settingslib.widget.profileselector">
 
-    <uses-sdk android:minSdkVersion="23" />
+    <uses-sdk android:minSdkVersion="29" />
 </manifest>
diff --git a/packages/SettingsLib/ProfileSelector/res/values/strings.xml b/packages/SettingsLib/ProfileSelector/res/values/strings.xml
index 68d4047..76ccb65 100644
--- a/packages/SettingsLib/ProfileSelector/res/values/strings.xml
+++ b/packages/SettingsLib/ProfileSelector/res/values/strings.xml
@@ -21,4 +21,6 @@
     <string name="settingslib_category_personal">Personal</string>
     <!-- Header for items under the work user [CHAR LIMIT=30] -->
     <string name="settingslib_category_work">Work</string>
+    <!-- Header for items under the private profile user [CHAR LIMIT=30] -->
+    <string name="settingslib_category_private">Private</string>
 </resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileSelectFragment.java b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileSelectFragment.java
index be5753be..c52386b 100644
--- a/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileSelectFragment.java
+++ b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileSelectFragment.java
@@ -16,31 +16,77 @@
 
 package com.android.settingslib.widget;
 
+import android.annotation.TargetApi;
 import android.app.Activity;
+import android.content.Context;
+import android.content.pm.UserProperties;
+import android.os.Build;
 import android.os.Bundle;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.ArrayMap;
+import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 
+import androidx.core.os.BuildCompat;
 import androidx.fragment.app.Fragment;
 import androidx.viewpager2.widget.ViewPager2;
 
+import com.android.settingslib.widget.profileselector.R;
+
 import com.google.android.material.tabs.TabLayout;
 import com.google.android.material.tabs.TabLayoutMediator;
-import com.android.settingslib.widget.profileselector.R;
+
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * Base fragment class for profile settings.
  */
 public abstract class ProfileSelectFragment extends Fragment {
+    private static final String TAG = "ProfileSelectFragment";
+    // UserHandle#USER_NULL is a @TestApi so is not accessible.
+    private static final int USER_NULL = -10000;
+    private static final int DEFAULT_POSITION = 0;
 
     /**
-     * Personal or Work profile tab of {@link ProfileSelectFragment}
-     * <p>0: Personal tab.
-     * <p>1: Work profile tab.
+     * The type of profile tab of {@link ProfileSelectFragment} to show
+     * <ul>
+     *   <li>0: Personal tab.
+     *   <li>1: Work profile tab.
+     * </ul>
+     *
+     * <p> Please note that this is supported for legacy reasons. Please use
+     * {@link #EXTRA_SHOW_FRAGMENT_USER_ID} instead.
      */
-    public static final String EXTRA_SHOW_FRAGMENT_TAB =
-            ":settings:show_fragment_tab";
+    public static final String EXTRA_SHOW_FRAGMENT_TAB = ":settings:show_fragment_tab";
+
+    /**
+     * An {@link ArrayList} of users to show. The supported users are: System user, the managed
+     * profile user, and the private profile user. A client should pass all the user ids that need
+     * to be shown in this list. Note that if this list is not provided then, for legacy reasons
+     * see {@link #EXTRA_SHOW_FRAGMENT_TAB}, an attempt will be made to show two tabs: one for the
+     * System user and one for the managed profile user.
+     *
+     * <p>Please note that this MUST be used in conjunction with
+     * {@link #EXTRA_SHOW_FRAGMENT_USER_ID}
+     */
+    public static final String EXTRA_LIST_OF_USER_IDS = ":settings:list_user_ids";
+
+    /**
+     * The user id of the user to be show in {@link ProfileSelectFragment}. Only the below user
+     * types are supported:
+     * <ul>
+     *   <li> System user.
+     *   <li> Managed profile user.
+     *   <li> Private profile user.
+     * </ul>
+     *
+     * <p>Please note that this MUST be used in conjunction with {@link #EXTRA_LIST_OF_USER_IDS}.
+     */
+    public static final String EXTRA_SHOW_FRAGMENT_USER_ID = ":settings:show_fragment_user_id";
 
     /**
      * Used in fragment argument with Extra key EXTRA_SHOW_FRAGMENT_TAB
@@ -48,13 +94,23 @@
     public static final int PERSONAL_TAB = 0;
 
     /**
-     * Used in fragment argument with Extra key EXTRA_SHOW_FRAGMENT_TAB
+     * Used in fragment argument with Extra key EXTRA_SHOW_FRAGMENT_TAB for the managed profile
      */
     public static final int WORK_TAB = 1;
 
+    /**
+     * Please note that private profile is available from API LEVEL
+     * {@link Build.VERSION_CODES.VANILLA_ICE_CREAM} only, therefore PRIVATE_TAB MUST be
+     * passed in {@link #EXTRA_SHOW_FRAGMENT_TAB} and {@link #EXTRA_LIST_OF_PROFILE_TABS} for
+     * {@link Build.VERSION_CODES.VANILLA_ICE_CREAM} or higher API Levels only.
+     */
+    private static final int PRIVATE_TAB = 2;
+
     private ViewGroup mContentView;
 
     private ViewPager2 mViewPager;
+    private final ArrayMap<UserHandle, Integer> mProfileTabsByUsers = new ArrayMap<>();
+    private boolean mUsingUserIds = false;
 
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
@@ -67,7 +123,7 @@
         if (titleResId > 0) {
             activity.setTitle(titleResId);
         }
-        final int selectedTab = getTabId(activity, getArguments());
+        initProfileTabsToShow();
 
         final View tabContainer = mContentView.findViewById(R.id.tab_container);
         mViewPager = tabContainer.findViewById(R.id.view_pager);
@@ -78,16 +134,14 @@
         ).attach();
 
         tabContainer.setVisibility(View.VISIBLE);
-        final TabLayout.Tab tab = tabs.getTabAt(selectedTab);
+        final TabLayout.Tab tab = tabs.getTabAt(getSelectedTabPosition(activity, getArguments()));
         tab.select();
 
         return mContentView;
     }
 
     /**
-     * create Personal or Work profile fragment
-     * <p>0: Personal profile.
-     * <p>1: Work profile.
+     * Create Personal or Work or Private profile fragment. See {@link #EXTRA_SHOW_FRAGMENT_USER_ID}
      */
     public abstract Fragment createFragment(int position);
 
@@ -99,21 +153,90 @@
         return 0;
     }
 
-    int getTabId(Activity activity, Bundle bundle) {
+    int getSelectedTabPosition(Activity activity, Bundle bundle) {
         if (bundle != null) {
+            final int extraUserId = bundle.getInt(EXTRA_SHOW_FRAGMENT_USER_ID, USER_NULL);
+            if (extraUserId != USER_NULL) {
+                return mProfileTabsByUsers.indexOfKey(UserHandle.of(extraUserId));
+            }
             final int extraTab = bundle.getInt(EXTRA_SHOW_FRAGMENT_TAB, -1);
             if (extraTab != -1) {
                 return extraTab;
             }
         }
-        return PERSONAL_TAB;
+        return DEFAULT_POSITION;
+    }
+
+    int getTabCount() {
+        return mUsingUserIds ? mProfileTabsByUsers.size() : 2;
+    }
+
+    void initProfileTabsToShow() {
+        Bundle bundle = getArguments();
+        if (bundle != null) {
+            ArrayList<Integer> userIdsToShow =
+                    bundle.getIntegerArrayList(EXTRA_LIST_OF_USER_IDS);
+            if (userIdsToShow != null && !userIdsToShow.isEmpty()) {
+                mUsingUserIds = true;
+                UserManager userManager = getContext().getSystemService(UserManager.class);
+                List<UserHandle> userHandles = userManager.getUserProfiles();
+                for (UserHandle userHandle : userHandles) {
+                    if (!userIdsToShow.contains(userHandle.getIdentifier())) {
+                        continue;
+                    }
+                    if (userHandle.isSystem()) {
+                        mProfileTabsByUsers.put(userHandle, PERSONAL_TAB);
+                    } else if (userManager.isManagedProfile(userHandle.getIdentifier())) {
+                        mProfileTabsByUsers.put(userHandle, WORK_TAB);
+                    } else if (shouldShowPrivateProfileIfItsOne(userHandle)) {
+                        mProfileTabsByUsers.put(userHandle, PRIVATE_TAB);
+                    }
+                }
+            }
+        }
+    }
+
+    private int getProfileTabForPosition(int position) {
+        return mUsingUserIds ? mProfileTabsByUsers.valueAt(position) : position;
+    }
+
+    int getUserIdForPosition(int position) {
+        return mUsingUserIds ? mProfileTabsByUsers.keyAt(position).getIdentifier() : position;
     }
 
     private CharSequence getPageTitle(int position) {
-        if (position == WORK_TAB) {
+        int tab = getProfileTabForPosition(position);
+        if (tab == WORK_TAB) {
             return getContext().getString(R.string.settingslib_category_work);
+        } else if (tab == PRIVATE_TAB) {
+            return getContext().getString(R.string.settingslib_category_private);
         }
 
         return getString(R.string.settingslib_category_personal);
     }
+
+    @TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    private boolean shouldShowUserInQuietMode(UserHandle userHandle, UserManager userManager) {
+        UserProperties userProperties = userManager.getUserProperties(userHandle);
+        return !userManager.isQuietModeEnabled(userHandle)
+                || userProperties.getShowInQuietMode() != UserProperties.SHOW_IN_QUIET_MODE_HIDDEN;
+    }
+
+    // It's sufficient to have this method marked with the appropriate API level because we expect
+    // to be here only for this API level - when then private profile was introduced.
+    @TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    private boolean shouldShowPrivateProfileIfItsOne(UserHandle userHandle) {
+        if (!BuildCompat.isAtLeastV() || !android.os.Flags.allowPrivateProfile()) {
+            return false;
+        }
+        try {
+            Context userContext = getContext().createContextAsUser(userHandle, /* flags= */ 0);
+            UserManager userManager = userContext.getSystemService(UserManager.class);
+            return userManager.isPrivateProfile()
+                    && shouldShowUserInQuietMode(userHandle, userManager);
+        } catch (IllegalStateException exception) {
+            Log.i(TAG, "Ignoring this user as the calling package not available in this user.");
+        }
+        return false;
+    }
 }
diff --git a/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileViewPagerAdapter.java b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileViewPagerAdapter.java
index f5ab647..37f4f27 100644
--- a/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileViewPagerAdapter.java
+++ b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileViewPagerAdapter.java
@@ -18,7 +18,6 @@
 
 import androidx.fragment.app.Fragment;
 import androidx.viewpager2.adapter.FragmentStateAdapter;
-import com.android.settingslib.widget.profileselector.R;
 
 /**
  * ViewPager Adapter to handle between TabLayout and ViewPager2
@@ -34,11 +33,11 @@
 
     @Override
     public Fragment createFragment(int position) {
-        return mParentFragments.createFragment(position);
+        return mParentFragments.createFragment(mParentFragments.getUserIdForPosition(position));
     }
 
     @Override
     public int getItemCount() {
-        return 2;
+        return mParentFragments.getTabCount();
     }
 }
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceModel.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceModel.kt
index 5dfecb0..87cd2b8 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceModel.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceModel.kt
@@ -70,26 +70,26 @@
             is BlockedByAdmin -> {
                 Box(
                     Modifier
-                            .clickable(
-                                role = Role.Switch,
-                                onClick = { restrictedMode.sendShowAdminSupportDetailsIntent() },
-                            )
-                            .semantics {
-                                this.toggleableState = ToggleableState(checked())
-                            },
+                        .clickable(
+                            role = Role.Switch,
+                            onClick = { restrictedMode.sendShowAdminSupportDetailsIntent() },
+                        )
+                        .semantics {
+                            this.toggleableState = ToggleableState(checked())
+                        },
                 ) { content() }
             }
 
             is BlockedByEcm -> {
                 Box(
                     Modifier
-                            .clickable(
-                                role = Role.Switch,
-                                onClick = { restrictedMode.showRestrictedSettingsDetails() },
-                            )
-                            .semantics {
-                                this.toggleableState = ToggleableState(checked())
-                            },
+                        .clickable(
+                            role = Role.Switch,
+                            onClick = { restrictedMode.showRestrictedSettingsDetails() },
+                        )
+                        .semantics {
+                            this.toggleableState = ToggleableState(checked())
+                        },
                 ) { content() }
             }
 
@@ -113,7 +113,7 @@
             content: @Composable (SwitchPreferenceModel) -> Unit,
         ) {
             val context = LocalContext.current
-            val restrictedSwitchPreferenceModel = remember(restrictedMode) {
+            val restrictedSwitchPreferenceModel = remember(restrictedMode, model.title) {
                 RestrictedSwitchPreferenceModel(context, model, restrictedMode)
             }
             restrictedSwitchPreferenceModel.RestrictionWrapper {
diff --git a/packages/SettingsLib/src/com/android/settingslib/Utils.java b/packages/SettingsLib/src/com/android/settingslib/Utils.java
index 87b4c0f..60a0529 100644
--- a/packages/SettingsLib/src/com/android/settingslib/Utils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/Utils.java
@@ -27,6 +27,7 @@
 import android.hardware.usb.UsbPort;
 import android.hardware.usb.UsbPortStatus;
 import android.hardware.usb.flags.Flags;
+import android.icu.text.NumberFormat;
 import android.location.LocationManager;
 import android.media.AudioManager;
 import android.net.NetworkCapabilities;
@@ -67,7 +68,6 @@
 import com.android.settingslib.fuelgauge.BatteryStatus;
 import com.android.settingslib.utils.BuildCompatUtils;
 
-import java.text.NumberFormat;
 import java.time.Duration;
 import java.util.List;
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/RecentAppOpsAccess.java b/packages/SettingsLib/src/com/android/settingslib/applications/RecentAppOpsAccess.java
index 9a29f22..f73081a 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/RecentAppOpsAccess.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/RecentAppOpsAccess.java
@@ -23,6 +23,7 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.UserProperties;
 import android.graphics.drawable.Drawable;
 import android.os.UserHandle;
 import android.os.UserManager;
@@ -132,8 +133,9 @@
             int uid = ops.getUid();
             UserHandle user = UserHandle.getUserHandleForUid(uid);
 
-            // Don't show apps belonging to background users except managed users.
-            if (!profiles.contains(user)) {
+            // Don't show apps belonging to background users except for profiles that shouldn't
+            // be shown in quiet mode.
+            if (!profiles.contains(user) || isHideInQuietEnabledForProfile(um, user)) {
                 continue;
             }
 
@@ -192,6 +194,16 @@
         return accesses;
     }
 
+    private boolean isHideInQuietEnabledForProfile(UserManager userManager, UserHandle userHandle) {
+        if (android.multiuser.Flags.enablePrivateSpaceFeatures()
+                && android.multiuser.Flags.handleInterleavedSettingsForPrivateSpace()) {
+            return userManager.isQuietModeEnabled(userHandle)
+                    && userManager.getUserProperties(userHandle).getShowInQuietMode()
+                            == UserProperties.SHOW_IN_QUIET_MODE_HIDDEN;
+        }
+        return false;
+    }
+
     /**
      * Creates a Access entry for the given PackageOps.
      *
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt b/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt
index cda6b8b..68f471d 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt
@@ -17,6 +17,7 @@
 package com.android.settingslib.media.session
 
 import android.media.session.MediaController
+import android.media.session.MediaSession
 import android.media.session.MediaSessionManager
 import android.os.UserHandle
 import androidx.concurrent.futures.DirectExecutor
@@ -28,7 +29,7 @@
 import kotlinx.coroutines.launch
 
 /** [Flow] for [MediaSessionManager.OnActiveSessionsChangedListener]. */
-val MediaSessionManager.activeMediaChanges: Flow<Collection<MediaController>?>
+val MediaSessionManager.activeMediaChanges: Flow<List<MediaController>?>
     get() =
         callbackFlow {
                 val listener =
@@ -42,3 +43,24 @@
                 awaitClose { removeOnActiveSessionsChangedListener(listener) }
             }
             .buffer(capacity = Channel.CONFLATED)
+
+/** [Flow] for [MediaSessionManager.RemoteSessionCallback]. */
+val MediaSessionManager.remoteSessionChanges: Flow<MediaSession.Token?>
+    get() =
+        callbackFlow {
+                val callback =
+                    object : MediaSessionManager.RemoteSessionCallback {
+                        override fun onVolumeChanged(sessionToken: MediaSession.Token, flags: Int) {
+                            launch { send(sessionToken) }
+                        }
+
+                        override fun onDefaultRemoteSessionChanged(
+                            sessionToken: MediaSession.Token?
+                        ) {
+                            launch { send(sessionToken) }
+                        }
+                    }
+                registerRemoteSessionCallback(DirectExecutor.INSTANCE, callback)
+                awaitClose { unregisterRemoteSessionCallback(callback) }
+            }
+            .buffer(capacity = Channel.CONFLATED)
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt
index 6730aad..e7fec69 100644
--- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt
@@ -19,7 +19,6 @@
 import android.media.AudioDeviceInfo
 import android.media.AudioManager
 import android.media.AudioManager.OnCommunicationDeviceChangedListener
-import androidx.concurrent.futures.DirectExecutor
 import com.android.internal.util.ConcurrentUtils
 import com.android.settingslib.volume.shared.AudioManagerEventsReceiver
 import com.android.settingslib.volume.shared.model.AudioManagerEvent
@@ -109,8 +108,8 @@
             callbackFlow {
                     val listener = OnCommunicationDeviceChangedListener { trySend(Unit) }
                     audioManager.addOnCommunicationDeviceChangedListener(
-                        DirectExecutor.INSTANCE,
-                        listener
+                        ConcurrentUtils.DIRECT_EXECUTOR,
+                        listener,
                     )
 
                     awaitClose { audioManager.removeOnCommunicationDeviceChangedListener(listener) }
@@ -146,7 +145,7 @@
             maxVolume = audioManager.getStreamMaxVolume(audioStream.value),
             volume = audioManager.getStreamVolume(audioStream.value),
             isAffectedByRingerMode = audioManager.isStreamAffectedByRingerMode(audioStream.value),
-            isMuted = audioManager.isStreamMute(audioStream.value),
+            isMuted = audioManager.isStreamMute(audioStream.value)
         )
     }
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt
index 298dd71e..724dd51 100644
--- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt
@@ -15,14 +15,10 @@
  */
 package com.android.settingslib.volume.data.repository
 
-import android.media.MediaRouter2Manager
-import android.media.RoutingSessionInfo
 import com.android.settingslib.media.LocalMediaManager
 import com.android.settingslib.media.MediaDevice
-import com.android.settingslib.volume.data.model.RoutingSession
 import com.android.settingslib.volume.shared.AudioManagerEventsReceiver
 import com.android.settingslib.volume.shared.model.AudioManagerEvent
-import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
@@ -30,35 +26,23 @@
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.callbackFlow
 import kotlinx.coroutines.flow.filterIsInstance
-import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.mapNotNull
 import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.shareIn
 import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.withContext
 
 /** Repository providing data about connected media devices. */
 interface LocalMediaRepository {
 
-    /** Available devices list */
-    val mediaDevices: StateFlow<Collection<MediaDevice>>
-
     /** Currently connected media device */
     val currentConnectedDevice: StateFlow<MediaDevice?>
-
-    val remoteRoutingSessions: StateFlow<Collection<RoutingSession>>
-
-    suspend fun adjustSessionVolume(sessionId: String?, volume: Int)
 }
 
 class LocalMediaRepositoryImpl(
     audioManagerEventsReceiver: AudioManagerEventsReceiver,
     private val localMediaManager: LocalMediaManager,
-    private val mediaRouter2Manager: MediaRouter2Manager,
     coroutineScope: CoroutineScope,
-    private val backgroundContext: CoroutineContext,
 ) : LocalMediaRepository {
 
     private val devicesChanges =
@@ -94,18 +78,6 @@
             }
             .shareIn(coroutineScope, SharingStarted.WhileSubscribed(), replay = 0)
 
-    override val mediaDevices: StateFlow<Collection<MediaDevice>> =
-        mediaDevicesUpdates
-            .mapNotNull {
-                if (it is DevicesUpdate.DeviceListUpdate) {
-                    it.newDevices ?: emptyList()
-                } else {
-                    null
-                }
-            }
-            .flowOn(backgroundContext)
-            .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList())
-
     override val currentConnectedDevice: StateFlow<MediaDevice?> =
         merge(devicesChanges, mediaDevicesUpdates)
             .map { localMediaManager.currentConnectedDevice }
@@ -116,30 +88,6 @@
                 localMediaManager.currentConnectedDevice
             )
 
-    override val remoteRoutingSessions: StateFlow<Collection<RoutingSession>> =
-        merge(devicesChanges, mediaDevicesUpdates)
-            .onStart { emit(Unit) }
-            .map { localMediaManager.remoteRoutingSessions.map(::toRoutingSession) }
-            .flowOn(backgroundContext)
-            .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList())
-
-    override suspend fun adjustSessionVolume(sessionId: String?, volume: Int) {
-        withContext(backgroundContext) {
-            if (sessionId == null) {
-                localMediaManager.adjustSessionVolume(volume)
-            } else {
-                localMediaManager.adjustSessionVolume(sessionId, volume)
-            }
-        }
-    }
-
-    private fun toRoutingSession(info: RoutingSessionInfo): RoutingSession =
-        RoutingSession(
-            info,
-            isMediaOutputDisabled = mediaRouter2Manager.getTransferableRoutes(info).isEmpty(),
-            isVolumeSeekBarEnabled = localMediaManager.shouldEnableVolumeSeekBar(info)
-        )
-
     private sealed interface DevicesUpdate {
 
         data class DeviceListUpdate(val newDevices: List<MediaDevice>?) : DevicesUpdate
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt
index 7c231d1..e4ac9fe 100644
--- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt
@@ -27,18 +27,26 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.flow.filterIsInstance
-import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.stateIn
 
 /** Provides controllers for currently active device media sessions. */
 interface MediaControllerRepository {
 
-    /** Current [MediaController]. Null is emitted when there is no active [MediaController]. */
-    val activeLocalMediaController: StateFlow<MediaController?>
+    /**
+     * Get a list of controllers for all ongoing sessions. The controllers will be provided in
+     * priority order with the most important controller at index 0.
+     *
+     * This requires the [android.Manifest.permission.MEDIA_CONTENT_CONTROL] permission be held by
+     * the calling app.
+     */
+    val activeSessions: StateFlow<List<MediaController>>
 }
 
 class MediaControllerRepositoryImpl(
@@ -49,51 +57,17 @@
     backgroundContext: CoroutineContext,
 ) : MediaControllerRepository {
 
-    private val devicesChanges =
-        audioManagerEventsReceiver.events.filterIsInstance(
-            AudioManagerEvent.StreamDevicesChanged::class
-        )
-
-    override val activeLocalMediaController: StateFlow<MediaController?> =
-        combine(
-                mediaSessionManager.activeMediaChanges.onStart {
-                    emit(mediaSessionManager.getActiveSessions(null))
-                },
-                localBluetoothManager?.headsetAudioModeChanges?.onStart { emit(Unit) }
-                    ?: flowOf(null),
-                devicesChanges.onStart { emit(AudioManagerEvent.StreamDevicesChanged) },
-            ) { controllers, _, _ ->
-                controllers?.let(::findLocalMediaController)
-            }
+    override val activeSessions: StateFlow<List<MediaController>> =
+        merge(
+                mediaSessionManager.activeMediaChanges.filterNotNull(),
+                localBluetoothManager?.headsetAudioModeChanges?.map {
+                    mediaSessionManager.getActiveSessions(null)
+                } ?: emptyFlow(),
+                audioManagerEventsReceiver.events
+                    .filterIsInstance(AudioManagerEvent.StreamDevicesChanged::class)
+                    .map { mediaSessionManager.getActiveSessions(null) },
+            )
+            .onStart { emit(mediaSessionManager.getActiveSessions(null)) }
             .flowOn(backgroundContext)
-            .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), null)
-
-    private fun findLocalMediaController(
-        controllers: Collection<MediaController>,
-    ): MediaController? {
-        var localController: MediaController? = null
-        val remoteMediaSessionLists: MutableList<String> = ArrayList()
-        for (controller in controllers) {
-            val playbackInfo: MediaController.PlaybackInfo = controller.playbackInfo ?: continue
-            when (playbackInfo.playbackType) {
-                MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE -> {
-                    if (localController?.packageName.equals(controller.packageName)) {
-                        localController = null
-                    }
-                    if (!remoteMediaSessionLists.contains(controller.packageName)) {
-                        remoteMediaSessionLists.add(controller.packageName)
-                    }
-                }
-                MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL -> {
-                    if (
-                        localController == null &&
-                            !remoteMediaSessionLists.contains(controller.packageName)
-                    ) {
-                        localController = controller
-                    }
-                }
-            }
-        }
-        return localController
-    }
+            .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList())
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/AudioVolumeInteractor.kt b/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/AudioVolumeInteractor.kt
index c9ac97d..778653b 100644
--- a/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/AudioVolumeInteractor.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/AudioVolumeInteractor.kt
@@ -66,6 +66,10 @@
         }
     }
 
+    fun isMutable(audioStream: AudioStream): Boolean =
+        // Alarm stream doesn't support muting
+        audioStream.value != AudioManager.STREAM_ALARM
+
     private suspend fun processVolume(
         audioStreamModel: AudioStreamModel,
         ringerMode: RingerMode,
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/LocalMediaInteractor.kt b/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/LocalMediaInteractor.kt
deleted file mode 100644
index f621335..0000000
--- a/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/LocalMediaInteractor.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * 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.settingslib.volume.domain.interactor
-
-import com.android.settingslib.media.MediaDevice
-import com.android.settingslib.volume.data.repository.LocalMediaRepository
-import com.android.settingslib.volume.domain.model.RoutingSession
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
-
-class LocalMediaInteractor(
-    private val repository: LocalMediaRepository,
-    coroutineScope: CoroutineScope,
-) {
-
-    /** Available devices list */
-    val mediaDevices: StateFlow<Collection<MediaDevice>>
-        get() = repository.mediaDevices
-
-    /** Currently connected media device */
-    val currentConnectedDevice: StateFlow<MediaDevice?>
-        get() = repository.currentConnectedDevice
-
-    val remoteRoutingSessions: StateFlow<List<RoutingSession>> =
-        repository.remoteRoutingSessions
-            .map { sessions ->
-                sessions.map {
-                    RoutingSession(
-                        routingSessionInfo = it.routingSessionInfo,
-                        isMediaOutputDisabled = it.isMediaOutputDisabled,
-                        isVolumeSeekBarEnabled =
-                            it.isVolumeSeekBarEnabled && it.routingSessionInfo.volumeMax > 0
-                    )
-                }
-            }
-            .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList())
-
-    suspend fun adjustSessionVolume(sessionId: String?, volume: Int) =
-        repository.adjustSessionVolume(sessionId, volume)
-}
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/LocalMediaRepositoryImplTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/LocalMediaRepositoryImplTest.kt
index 2d12dae..caf41f2 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/LocalMediaRepositoryImplTest.kt
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/LocalMediaRepositoryImplTest.kt
@@ -15,17 +15,12 @@
  */
 package com.android.settingslib.volume.data.repository
 
-import android.media.MediaRoute2Info
-import android.media.MediaRouter2Manager
-import android.media.RoutingSessionInfo
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.settingslib.media.LocalMediaManager
 import com.android.settingslib.media.MediaDevice
-import com.android.settingslib.volume.data.model.RoutingSession
 import com.android.settingslib.volume.shared.FakeAudioManagerEventsReceiver
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.test.TestScope
@@ -37,15 +32,10 @@
 import org.mockito.ArgumentCaptor
 import org.mockito.Captor
 import org.mockito.Mock
-import org.mockito.Mockito.any
-import org.mockito.Mockito.anyInt
-import org.mockito.Mockito.anyString
-import org.mockito.Mockito.mock
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
 
-@OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(AndroidJUnit4::class)
 @SmallTest
 class LocalMediaRepositoryImplTest {
@@ -53,7 +43,6 @@
     @Mock private lateinit var localMediaManager: LocalMediaManager
     @Mock private lateinit var mediaDevice1: MediaDevice
     @Mock private lateinit var mediaDevice2: MediaDevice
-    @Mock private lateinit var mediaRouter2Manager: MediaRouter2Manager
 
     @Captor
     private lateinit var deviceCallbackCaptor: ArgumentCaptor<LocalMediaManager.DeviceCallback>
@@ -71,29 +60,11 @@
             LocalMediaRepositoryImpl(
                 eventsReceiver,
                 localMediaManager,
-                mediaRouter2Manager,
                 testScope.backgroundScope,
-                testScope.testScheduler,
             )
     }
 
     @Test
-    fun mediaDevices_areUpdated() {
-        testScope.runTest {
-            var mediaDevices: Collection<MediaDevice>? = null
-            underTest.mediaDevices.onEach { mediaDevices = it }.launchIn(backgroundScope)
-            runCurrent()
-            verify(localMediaManager).registerCallback(deviceCallbackCaptor.capture())
-            deviceCallbackCaptor.value.onDeviceListUpdate(listOf(mediaDevice1, mediaDevice2))
-            runCurrent()
-
-            assertThat(mediaDevices).hasSize(2)
-            assertThat(mediaDevices).contains(mediaDevice1)
-            assertThat(mediaDevices).contains(mediaDevice2)
-        }
-    }
-
-    @Test
     fun deviceListUpdated_currentConnectedDeviceUpdated() {
         testScope.runTest {
             var currentConnectedDevice: MediaDevice? = null
@@ -110,78 +81,4 @@
             assertThat(currentConnectedDevice).isEqualTo(mediaDevice1)
         }
     }
-
-    @Test
-    fun kek() {
-        testScope.runTest {
-            `when`(localMediaManager.remoteRoutingSessions)
-                .thenReturn(
-                    listOf(
-                        testRoutingSessionInfo1,
-                        testRoutingSessionInfo2,
-                        testRoutingSessionInfo3,
-                    )
-                )
-            `when`(localMediaManager.shouldEnableVolumeSeekBar(any())).then {
-                (it.arguments[0] as RoutingSessionInfo) == testRoutingSessionInfo1
-            }
-            `when`(mediaRouter2Manager.getTransferableRoutes(any<RoutingSessionInfo>())).then {
-                if ((it.arguments[0] as RoutingSessionInfo) == testRoutingSessionInfo2) {
-                    return@then listOf(mock(MediaRoute2Info::class.java))
-                }
-                emptyList<MediaRoute2Info>()
-            }
-            var remoteRoutingSessions: Collection<RoutingSession>? = null
-            underTest.remoteRoutingSessions
-                .onEach { remoteRoutingSessions = it }
-                .launchIn(backgroundScope)
-
-            runCurrent()
-
-            assertThat(remoteRoutingSessions)
-                .containsExactlyElementsIn(
-                    listOf(
-                        RoutingSession(
-                            routingSessionInfo = testRoutingSessionInfo1,
-                            isVolumeSeekBarEnabled = true,
-                            isMediaOutputDisabled = true,
-                        ),
-                        RoutingSession(
-                            routingSessionInfo = testRoutingSessionInfo2,
-                            isVolumeSeekBarEnabled = false,
-                            isMediaOutputDisabled = false,
-                        ),
-                        RoutingSession(
-                            routingSessionInfo = testRoutingSessionInfo3,
-                            isVolumeSeekBarEnabled = false,
-                            isMediaOutputDisabled = true,
-                        )
-                    )
-                )
-        }
-    }
-
-    @Test
-    fun adjustSessionVolume_adjusts() {
-        testScope.runTest {
-            var volume = 0
-            `when`(localMediaManager.adjustSessionVolume(anyString(), anyInt())).then {
-                volume = it.arguments[1] as Int
-                Unit
-            }
-
-            underTest.adjustSessionVolume("test_session", 10)
-
-            assertThat(volume).isEqualTo(10)
-        }
-    }
-
-    private companion object {
-        val testRoutingSessionInfo1 =
-            RoutingSessionInfo.Builder("id_1", "test.pkg.1").addSelectedRoute("route_1").build()
-        val testRoutingSessionInfo2 =
-            RoutingSessionInfo.Builder("id_2", "test.pkg.2").addSelectedRoute("route_2").build()
-        val testRoutingSessionInfo3 =
-            RoutingSessionInfo.Builder("id_3", "test.pkg.3").addSelectedRoute("route_3").build()
-    }
 }
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt
index f3d1714..964c3f7 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt
@@ -22,13 +22,10 @@
 import android.media.session.PlaybackState
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.settingslib.bluetooth.BluetoothCallback
 import com.android.settingslib.bluetooth.BluetoothEventManager
 import com.android.settingslib.bluetooth.LocalBluetoothManager
 import com.android.settingslib.volume.shared.FakeAudioManagerEventsReceiver
-import com.android.settingslib.volume.shared.model.AudioManagerEvent
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.test.TestScope
@@ -37,21 +34,15 @@
 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.any
-import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
 
-@OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(AndroidJUnit4::class)
 @SmallTest
 class MediaControllerRepositoryImplTest {
 
-    @Captor private lateinit var callbackCaptor: ArgumentCaptor<BluetoothCallback>
-
     @Mock private lateinit var mediaSessionManager: MediaSessionManager
     @Mock private lateinit var localBluetoothManager: LocalBluetoothManager
     @Mock private lateinit var eventManager: BluetoothEventManager
@@ -103,7 +94,7 @@
     }
 
     @Test
-    fun playingMediaDevicesAvailable_sessionIsActive() {
+    fun mediaDevicesAvailable_returnsAllActiveOnes() {
         testScope.runTest {
             `when`(mediaSessionManager.getActiveSessions(any()))
                 .thenReturn(
@@ -112,53 +103,25 @@
                         statelessMediaController,
                         errorMediaController,
                         remoteMediaController,
-                        localMediaController
+                        localMediaController,
                     )
                 )
-            var mediaController: MediaController? = null
-            underTest.activeLocalMediaController
-                .onEach { mediaController = it }
-                .launchIn(backgroundScope)
+
+            var mediaControllers: Collection<MediaController>? = null
+            underTest.activeSessions.onEach { mediaControllers = it }.launchIn(backgroundScope)
             runCurrent()
 
-            eventsReceiver.triggerEvent(AudioManagerEvent.StreamDevicesChanged)
-            triggerOnAudioModeChanged()
-            runCurrent()
-
-            assertThat(mediaController).isSameInstanceAs(localMediaController)
-        }
-    }
-
-    @Test
-    fun noPlayingMediaDevicesAvailable_sessionIsInactive() {
-        testScope.runTest {
-            `when`(mediaSessionManager.getActiveSessions(any()))
-                .thenReturn(
-                    listOf(
-                        stoppedMediaController,
-                        statelessMediaController,
-                        errorMediaController,
-                    )
+            assertThat(mediaControllers)
+                .containsExactly(
+                    stoppedMediaController,
+                    statelessMediaController,
+                    errorMediaController,
+                    remoteMediaController,
+                    localMediaController,
                 )
-            var mediaController: MediaController? = null
-            underTest.activeLocalMediaController
-                .onEach { mediaController = it }
-                .launchIn(backgroundScope)
-            runCurrent()
-
-            eventsReceiver.triggerEvent(AudioManagerEvent.StreamDevicesChanged)
-            triggerOnAudioModeChanged()
-            runCurrent()
-
-            assertThat(mediaController).isNull()
         }
     }
 
-    private fun triggerOnAudioModeChanged() {
-        verify(eventManager).registerCallback(callbackCaptor.capture())
-        callbackCaptor.value.onAudioModeChanged()
-    }
-
     private companion object {
         val statePlaying: PlaybackState =
             PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0, 0f).build()
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/RecentAppOpsAccessesTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/RecentAppOpsAccessesTest.java
index f9505dd..52622a7 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/RecentAppOpsAccessesTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/RecentAppOpsAccessesTest.java
@@ -32,14 +32,17 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.UserProperties;
 import android.os.Process;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.util.LongSparseArray;
 
 import com.android.settingslib.testutils.shadow.ShadowPermissionChecker;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -58,6 +61,8 @@
 @Config(shadows = {ShadowPermissionChecker.class})
 public class RecentAppOpsAccessesTest {
 
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
     private static final int TEST_UID = 1234;
     private static final long NOW = 1_000_000_000;  // Approximately 9/8/2001
     private static final long ONE_MIN_AGO = NOW - TimeUnit.MINUTES.toMillis(1);
@@ -73,6 +78,8 @@
     @Mock
     private UserManager mUserManager;
     @Mock
+    private UserProperties mUserProperties;
+    @Mock
     private Clock mClock;
     private Context mContext;
     private int mTestUserId;
@@ -132,6 +139,58 @@
     }
 
     @Test
+    public void testGetAppList_quietModeDisabled_shouldFilterRecentAccesses() {
+        mSetFlagsRule.enableFlags(
+                android.multiuser.Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE,
+                android.multiuser.Flags.FLAG_HANDLE_INTERLEAVED_SETTINGS_FOR_PRIVATE_SPACE);
+        when(mUserManager.isQuietModeEnabled(any())).thenReturn(false);
+
+        List<RecentAppOpsAccess.Access> requests = mRecentAppOpsAccess.getAppList(false);
+        // Only two of the apps have requested location within 15 min.
+        assertThat(requests).hasSize(2);
+        // Make sure apps are ordered by recency
+        assertThat(requests.get(0).packageName).isEqualTo(TEST_PACKAGE_NAMES[0]);
+        assertThat(requests.get(0).accessFinishTime).isEqualTo(ONE_MIN_AGO);
+        assertThat(requests.get(1).packageName).isEqualTo(TEST_PACKAGE_NAMES[1]);
+        assertThat(requests.get(1).accessFinishTime).isEqualTo(TWENTY_THREE_HOURS_AGO);
+    }
+
+    @Test
+    public void testGetAppList_quietModeEnabledShowInQuietDefault_shouldFilterRecentAccesses() {
+        mSetFlagsRule.enableFlags(
+                android.multiuser.Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE,
+                android.multiuser.Flags.FLAG_HANDLE_INTERLEAVED_SETTINGS_FOR_PRIVATE_SPACE);
+        when(mUserManager.isQuietModeEnabled(any())).thenReturn(true);
+        when(mUserManager.getUserProperties(any())).thenReturn(mUserProperties);
+        when(mUserProperties.getShowInQuietMode())
+                .thenReturn(UserProperties.SHOW_IN_QUIET_MODE_DEFAULT);
+
+        List<RecentAppOpsAccess.Access> requests = mRecentAppOpsAccess.getAppList(false);
+        // Only two of the apps have requested location within 15 min.
+        assertThat(requests).hasSize(2);
+        // Make sure apps are ordered by recency
+        assertThat(requests.get(0).packageName).isEqualTo(TEST_PACKAGE_NAMES[0]);
+        assertThat(requests.get(0).accessFinishTime).isEqualTo(ONE_MIN_AGO);
+        assertThat(requests.get(1).packageName).isEqualTo(TEST_PACKAGE_NAMES[1]);
+        assertThat(requests.get(1).accessFinishTime).isEqualTo(TWENTY_THREE_HOURS_AGO);
+    }
+
+    @Test
+    public void testGetAppList_quietModeEnabledShowInQuietHidden_shouldNotFilterRecentAccesses() {
+        mSetFlagsRule.enableFlags(
+                android.multiuser.Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE,
+                android.multiuser.Flags.FLAG_HANDLE_INTERLEAVED_SETTINGS_FOR_PRIVATE_SPACE);
+        when(mUserManager.isQuietModeEnabled(any())).thenReturn(true);
+        when(mUserManager.getUserProperties(any())).thenReturn(mUserProperties);
+        when(mUserProperties.getShowInQuietMode())
+                .thenReturn(UserProperties.SHOW_IN_QUIET_MODE_HIDDEN);
+
+        List<RecentAppOpsAccess.Access> requests = mRecentAppOpsAccess.getAppList(false);
+        // Apps doesn't show up in the list of apps.
+        assertThat(requests).hasSize(0);
+    }
+
+    @Test
     public void testGetAppList_shouldNotShowAndroidOS() throws NameNotFoundException {
         // Add android OS to the list of apps.
         PackageOps androidSystemPackageOps =
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index eaec617..5629a7b 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -256,8 +256,7 @@
         Settings.Secure.HEARING_AID_MEDIA_ROUTING,
         Settings.Secure.HEARING_AID_NOTIFICATION_ROUTING,
         Settings.Secure.ACCESSIBILITY_FONT_SCALING_HAS_BEEN_CHANGED,
-        Settings.Secure.SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED,
-        Settings.Secure.SEARCH_LONG_PRESS_HOME_ENABLED,
+        Settings.Secure.SEARCH_ALL_ENTRYPOINTS_ENABLED,
         Settings.Secure.HUB_MODE_TUTORIAL_STATE,
         Settings.Secure.STYLUS_BUTTONS_ENABLED,
         Settings.Secure.STYLUS_HANDWRITING_ENABLED,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index 046d6e2..b8d95eb 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -208,8 +208,7 @@
         VALIDATORS.put(Secure.ASSIST_TOUCH_GESTURE_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.ASSIST_LONG_PRESS_HOME_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.VISUAL_QUERY_ACCESSIBILITY_DETECTION_ENABLED, BOOLEAN_VALIDATOR);
-        VALIDATORS.put(Secure.SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED, BOOLEAN_VALIDATOR);
-        VALIDATORS.put(Secure.SEARCH_LONG_PRESS_HOME_ENABLED, BOOLEAN_VALIDATOR);
+        VALIDATORS.put(Secure.SEARCH_ALL_ENTRYPOINTS_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.VR_DISPLAY_MODE, new DiscreteValueValidator(new String[] {"0", "1"}));
         VALIDATORS.put(Secure.NOTIFICATION_BADGING, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.NOTIFICATION_DISMISS_RTL, BOOLEAN_VALIDATOR);
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
index 02d212c..dba3bac 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
@@ -1950,11 +1950,8 @@
                 Settings.Secure.ASSIST_LONG_PRESS_HOME_ENABLED,
                 SecureSettingsProto.Assist.LONG_PRESS_HOME_ENABLED);
         dumpSetting(s, p,
-                Settings.Secure.SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED,
-                SecureSettingsProto.Assist.SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED);
-        dumpSetting(s, p,
-                Settings.Secure.SEARCH_LONG_PRESS_HOME_ENABLED,
-                SecureSettingsProto.Assist.SEARCH_LONG_PRESS_HOME_ENABLED);
+                Settings.Secure.SEARCH_ALL_ENTRYPOINTS_ENABLED,
+                SecureSettingsProto.Assist.SEARCH_ALL_ENTRYPOINTS_ENABLED);
         dumpSetting(s, p,
                 Settings.Secure.VISUAL_QUERY_ACCESSIBILITY_DETECTION_ENABLED,
                 SecureSettingsProto.Assist.VISUAL_QUERY_ACCESSIBILITY_DETECTION_ENABLED);
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index 6eb2dd0..8cafe5f 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -688,6 +688,7 @@
                  Settings.Secure.DEVICE_PAIRED,
                  Settings.Secure.DIALER_DEFAULT_APPLICATION,
                  Settings.Secure.DISABLED_PRINT_SERVICES,
+                 Settings.Secure.DISABLE_SECURE_WINDOWS,
                  Settings.Secure.DISABLED_SYSTEM_INPUT_METHODS,
                  Settings.Secure.DOCKED_CLOCK_FACE,
                  Settings.Secure.DOZE_PULSE_ON_LONG_PRESS,
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 02d19dc..5804071 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -932,6 +932,9 @@
     <uses-permission
         android:name="android.permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW" />
 
+    <!-- Permission required for Cts test - CtsSettingsTestCases -->
+    <uses-permission android:name="android.permission.PREPARE_FACTORY_RESET" />
+
     <application
         android:label="@string/app_label"
         android:theme="@android:style/Theme.DeviceDefault.DayNight"
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java
index 6546b87..f70ad9e 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java
+++ b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java
@@ -23,10 +23,10 @@
 import static android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_RECENTS;
 import static android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_TAKE_SCREENSHOT;
 
-import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_OPEN_BLOCKED;
 import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_GLOBAL_ACTION;
 import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_GLOBAL_ACTION_EXTRA;
 import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_HIDE_MENU;
+import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_OPEN_BLOCKED;
 import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_TOGGLE_MENU;
 import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.PACKAGE_NAME;
 
@@ -77,6 +77,8 @@
     private static final int TIMEOUT_SERVICE_STATUS_CHANGE_S = 5;
     private static final int TIMEOUT_UI_CHANGE_S = 5;
     private static final int NO_GLOBAL_ACTION = -1;
+    private static final Intent INTENT_OPEN_MENU = new Intent(INTENT_TOGGLE_MENU)
+            .setPackage(PACKAGE_NAME);
 
     private static Instrumentation sInstrumentation;
     private static UiAutomation sUiAutomation;
@@ -152,9 +154,6 @@
     @Before
     public void setup() throws Throwable {
         sOpenBlocked.set(false);
-        wakeUpScreen();
-        sUiAutomation.executeShellCommand("input keyevent KEYCODE_MENU");
-        openMenu();
     }
 
     @After
@@ -188,24 +187,17 @@
     }
 
     private static void openMenu() throws Throwable {
-        openMenu(false);
-    }
-
-    private static void openMenu(boolean abandonOnBlock) throws Throwable {
-        Intent intent = new Intent(INTENT_TOGGLE_MENU);
-        intent.setPackage(PACKAGE_NAME);
-        sInstrumentation.getContext().sendBroadcast(intent);
+        unlockSignal();
+        sInstrumentation.getContext().sendBroadcast(INTENT_OPEN_MENU);
 
         TestUtils.waitUntil("Timed out before menu could appear.",
                 TIMEOUT_UI_CHANGE_S,
                 () -> {
-                    if (sOpenBlocked.get() && abandonOnBlock) {
-                        throw new IllegalStateException();
-                    }
                     if (isMenuVisible()) {
                         return true;
                     } else {
-                        sInstrumentation.getContext().sendBroadcast(intent);
+                        unlockSignal();
+                        sInstrumentation.getContext().sendBroadcast(INTENT_OPEN_MENU);
                         return false;
                     }
                 });
@@ -249,6 +241,7 @@
 
     @Test
     public void testAdjustBrightness() throws Throwable {
+        openMenu();
         Context context = sInstrumentation.getTargetContext();
         DisplayManager displayManager = context.getSystemService(
                 DisplayManager.class);
@@ -264,22 +257,28 @@
                 context.getDisplayId()).getBrightnessInfo();
 
         try {
-            displayManager.setBrightness(context.getDisplayId(), brightnessInfo.brightnessMinimum);
             TestUtils.waitUntil("Could not change to minimum brightness",
                     TIMEOUT_UI_CHANGE_S,
-                    () -> displayManager.getBrightness(context.getDisplayId())
-                            == brightnessInfo.brightnessMinimum);
+                    () -> {
+                        displayManager.setBrightness(
+                                context.getDisplayId(), brightnessInfo.brightnessMinimum);
+                        return displayManager.getBrightness(context.getDisplayId())
+                                == brightnessInfo.brightnessMinimum;
+                    });
             brightnessUpButton.performAction(CLICK_ID);
             TestUtils.waitUntil("Did not detect an increase in brightness.",
                     TIMEOUT_UI_CHANGE_S,
                     () -> displayManager.getBrightness(context.getDisplayId())
                             > brightnessInfo.brightnessMinimum);
 
-            displayManager.setBrightness(context.getDisplayId(), brightnessInfo.brightnessMaximum);
             TestUtils.waitUntil("Could not change to maximum brightness",
                     TIMEOUT_UI_CHANGE_S,
-                    () -> displayManager.getBrightness(context.getDisplayId())
-                            == brightnessInfo.brightnessMaximum);
+                    () -> {
+                        displayManager.setBrightness(
+                                context.getDisplayId(), brightnessInfo.brightnessMaximum);
+                        return displayManager.getBrightness(context.getDisplayId())
+                                == brightnessInfo.brightnessMaximum;
+                    });
             brightnessDownButton.performAction(CLICK_ID);
             TestUtils.waitUntil("Did not detect a decrease in brightness.",
                     TIMEOUT_UI_CHANGE_S,
@@ -292,6 +291,7 @@
 
     @Test
     public void testAdjustVolume() throws Throwable {
+        openMenu();
         Context context = sInstrumentation.getTargetContext();
         AudioManager audioManager = context.getSystemService(AudioManager.class);
         int resetVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
@@ -332,6 +332,7 @@
 
     @Test
     public void testAssistantButton_opensVoiceAssistant() throws Throwable {
+        openMenu();
         AccessibilityNodeInfo assistantButton = findGridButtonInfo(getGridButtonList(),
                 String.valueOf(ShortcutId.ID_ASSISTANT_VALUE.ordinal()));
         Intent expectedIntent = new Intent(Intent.ACTION_VOICE_COMMAND);
@@ -349,6 +350,7 @@
 
     @Test
     public void testAccessibilitySettingsButton_opensAccessibilitySettings() throws Throwable {
+        openMenu();
         AccessibilityNodeInfo settingsButton = findGridButtonInfo(getGridButtonList(),
                 String.valueOf(ShortcutId.ID_A11YSETTING_VALUE.ordinal()));
         Intent expectedIntent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
@@ -364,6 +366,7 @@
 
     @Test
     public void testPowerButton_performsGlobalAction() throws Throwable {
+        openMenu();
         AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(),
                 String.valueOf(ShortcutId.ID_POWER_VALUE.ordinal()));
 
@@ -376,6 +379,7 @@
 
     @Test
     public void testRecentButton_performsGlobalAction() throws Throwable {
+        openMenu();
         AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(),
                 String.valueOf(ShortcutId.ID_RECENT_VALUE.ordinal()));
 
@@ -388,6 +392,7 @@
 
     @Test
     public void testLockButton_performsGlobalAction() throws Throwable {
+        openMenu();
         AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(),
                 String.valueOf(ShortcutId.ID_LOCKSCREEN_VALUE.ordinal()));
 
@@ -400,6 +405,7 @@
 
     @Test
     public void testQuickSettingsButton_performsGlobalAction() throws Throwable {
+        openMenu();
         AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(),
                 String.valueOf(ShortcutId.ID_QUICKSETTING_VALUE.ordinal()));
 
@@ -412,6 +418,7 @@
 
     @Test
     public void testNotificationsButton_performsGlobalAction() throws Throwable {
+        openMenu();
         AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(),
                 String.valueOf(ShortcutId.ID_NOTIFICATION_VALUE.ordinal()));
 
@@ -424,6 +431,7 @@
 
     @Test
     public void testScreenshotButton_performsGlobalAction() throws Throwable {
+        openMenu();
         AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(),
                 String.valueOf(ShortcutId.ID_SCREENSHOT_VALUE.ordinal()));
 
@@ -436,6 +444,7 @@
 
     @Test
     public void testOnScreenLock_closesMenu() throws Throwable {
+        openMenu();
         closeScreen();
         wakeUpScreen();
 
@@ -447,13 +456,18 @@
         closeScreen();
         wakeUpScreen();
 
-        boolean blocked = false;
-        try {
-            openMenu(true);
-        } catch (IllegalStateException e) {
-            // Expected
-            blocked = true;
-        }
-        assertThat(blocked).isTrue();
+        TestUtils.waitUntil("Did not receive signal that menu cannot open",
+                TIMEOUT_UI_CHANGE_S,
+                () -> {
+                    sInstrumentation.getContext().sendBroadcast(INTENT_OPEN_MENU);
+                    return sOpenBlocked.get();
+                });
+    }
+
+    private static void unlockSignal() {
+        // MENU unlocks screen,
+        // BACK closes any menu that may appear if the screen wasn't locked.
+        sUiAutomation.executeShellCommand("input keyevent KEYCODE_MENU");
+        sUiAutomation.executeShellCommand("input keyevent KEYCODE_BACK");
     }
 }
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 8da5021..a155dc4 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -104,6 +104,13 @@
 }
 
 flag {
+    name: "notifications_heads_up_refactor"
+    namespace: "systemui"
+    description: "Use HeadsUpInteractor to feed HUN updates to the NSSL."
+    bug: "325936094"
+}
+
+flag {
    name: "pss_app_selector_abrupt_exit_fix"
    namespace: "systemui"
    description: "Fixes the app selector abruptly disappearing without an animation, when the"
@@ -424,6 +431,13 @@
 }
 
 flag {
+    name: "screenshot_shelf_ui"
+    namespace: "systemui"
+    description: "Use new shelf UI flow for screenshots"
+    bug: "329659738"
+}
+
+flag {
    name: "run_fingerprint_detect_on_dismissible_keyguard"
    namespace: "systemui"
    description: "Run fingerprint detect instead of authenticate if the keyguard is dismissible."
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
index 621ddf7..1da6c1e 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
@@ -53,6 +53,7 @@
 import androidx.compose.material3.Text
 import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
@@ -71,6 +72,7 @@
 import androidx.compose.ui.unit.DpOffset
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
 import androidx.compose.ui.unit.times
 import com.android.compose.PlatformButton
 import com.android.compose.animation.scene.ElementKey
@@ -84,7 +86,9 @@
 import com.android.systemui.bouncer.ui.BouncerDialogFactory
 import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout
 import com.android.systemui.bouncer.ui.viewmodel.AuthMethodBouncerViewModel
+import com.android.systemui.bouncer.ui.viewmodel.BouncerMessageViewModel
 import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
+import com.android.systemui.bouncer.ui.viewmodel.MessageViewModel
 import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel
 import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel
 import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel
@@ -166,7 +170,7 @@
                 modifier = Modifier.fillMaxWidth(),
             ) {
                 StatusMessage(
-                    viewModel = viewModel,
+                    viewModel = viewModel.message,
                     modifier = Modifier,
                 )
 
@@ -228,7 +232,7 @@
             when (authMethod) {
                 is PinBouncerViewModel -> {
                     StatusMessage(
-                        viewModel = viewModel,
+                        viewModel = viewModel.message,
                         modifier = Modifier.align(Alignment.TopCenter),
                     )
 
@@ -241,7 +245,7 @@
                 }
                 is PatternBouncerViewModel -> {
                     StatusMessage(
-                        viewModel = viewModel,
+                        viewModel = viewModel.message,
                         modifier = Modifier.align(Alignment.TopCenter),
                     )
 
@@ -280,7 +284,7 @@
                         modifier = Modifier.fillMaxWidth().align(Alignment.Center),
                     ) {
                         StatusMessage(
-                            viewModel = viewModel,
+                            viewModel = viewModel.message,
                         )
 
                         OutputArea(viewModel = viewModel, modifier = Modifier.padding(top = 24.dp))
@@ -376,7 +380,7 @@
                     modifier = Modifier.fillMaxWidth()
                 ) {
                     StatusMessage(
-                        viewModel = viewModel,
+                        viewModel = viewModel.message,
                     )
 
                     OutputArea(viewModel = viewModel, modifier = Modifier.padding(top = 24.dp))
@@ -441,7 +445,7 @@
                 modifier = Modifier.fillMaxWidth(),
             ) {
                 StatusMessage(
-                    viewModel = viewModel,
+                    viewModel = viewModel.message,
                 )
 
                 OutputArea(viewModel = viewModel, modifier = Modifier.padding(top = 24.dp))
@@ -548,26 +552,44 @@
 
 @Composable
 private fun StatusMessage(
-    viewModel: BouncerViewModel,
+    viewModel: BouncerMessageViewModel,
     modifier: Modifier = Modifier,
 ) {
-    val message: BouncerViewModel.MessageViewModel by viewModel.message.collectAsState()
+    val message: MessageViewModel? by viewModel.message.collectAsState()
+
+    DisposableEffect(Unit) {
+        viewModel.onShown()
+        onDispose {}
+    }
 
     Crossfade(
         targetState = message,
         label = "Bouncer message",
-        animationSpec = if (message.isUpdateAnimated) tween() else snap(),
+        animationSpec = if (message?.isUpdateAnimated == true) tween() else snap(),
         modifier = modifier.fillMaxWidth(),
-    ) {
-        Box(
-            contentAlignment = Alignment.Center,
+    ) { msg ->
+        Column(
+            horizontalAlignment = Alignment.CenterHorizontally,
             modifier = Modifier.fillMaxWidth(),
         ) {
-            Text(
-                text = it.text,
-                color = MaterialTheme.colorScheme.onSurface,
-                style = MaterialTheme.typography.bodyLarge,
-            )
+            msg?.let {
+                Text(
+                    text = it.text,
+                    color = MaterialTheme.colorScheme.onSurface,
+                    fontSize = 18.sp,
+                    lineHeight = 24.sp,
+                    overflow = TextOverflow.Ellipsis,
+                )
+                Spacer(modifier = Modifier.size(10.dp))
+                Text(
+                    text = it.secondaryText ?: "",
+                    color = MaterialTheme.colorScheme.onSurface,
+                    fontSize = 14.sp,
+                    lineHeight = 20.sp,
+                    overflow = TextOverflow.Ellipsis,
+                    maxLines = 2
+                )
+            }
         }
     }
 }
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 2a13d49..c34f2fd 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
@@ -74,10 +74,7 @@
     val isImeSwitcherButtonVisible by viewModel.isImeSwitcherButtonVisible.collectAsState()
     val selectedUserId by viewModel.selectedUserId.collectAsState()
 
-    DisposableEffect(Unit) {
-        viewModel.onShown()
-        onDispose { viewModel.onHidden() }
-    }
+    DisposableEffect(Unit) { onDispose { viewModel.onHidden() } }
 
     LaunchedEffect(animateFailure) {
         if (animateFailure) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
index 0a5f5d2..a78c2c0 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
@@ -72,10 +72,7 @@
     centerDotsVertically: Boolean,
     modifier: Modifier = Modifier,
 ) {
-    DisposableEffect(Unit) {
-        viewModel.onShown()
-        onDispose { viewModel.onHidden() }
-    }
+    DisposableEffect(Unit) { onDispose { viewModel.onHidden() } }
 
     val colCount = viewModel.columnCount
     val rowCount = viewModel.rowCount
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
index f505b90..5651a46 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
@@ -72,10 +72,7 @@
     verticalSpacing: Dp,
     modifier: Modifier = Modifier,
 ) {
-    DisposableEffect(Unit) {
-        viewModel.onShown()
-        onDispose { viewModel.onHidden() }
-    }
+    DisposableEffect(Unit) { onDispose { viewModel.onHidden() } }
 
     val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState()
     val backspaceButtonAppearance by viewModel.backspaceButtonAppearance.collectAsState()
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt
index 82e19e7c..1d86b15 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt
@@ -58,7 +58,6 @@
         if (currentClock?.smallClock?.view == null) {
             return
         }
-        viewModel.clock = currentClock
 
         val context = LocalContext.current
         MovableElement(key = smallClockElementKey, modifier = modifier) {
@@ -89,7 +88,6 @@
     @Composable
     fun SceneScope.LargeClock(modifier: Modifier = Modifier) {
         val currentClock by viewModel.currentClock.collectAsState()
-        viewModel.clock = currentClock
         if (currentClock?.largeClock?.view == null) {
             return
         }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt
index 31d3fa0..9f02201 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt
@@ -32,12 +32,12 @@
 import com.android.compose.animation.scene.SceneScope
 import com.android.keyguard.LockIconView
 import com.android.keyguard.LockIconViewController
-import com.android.systemui.Flags.keyguardBottomAreaRefactor
 import com.android.systemui.biometrics.AuthController
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
 import com.android.systemui.flags.FeatureFlagsClassic
 import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
 import com.android.systemui.keyguard.ui.binder.DeviceEntryIconViewBinder
 import com.android.systemui.keyguard.ui.composable.blueprint.BlueprintAlignmentLines
 import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
@@ -69,7 +69,7 @@
 ) {
     @Composable
     fun SceneScope.LockIcon(modifier: Modifier = Modifier) {
-        if (!keyguardBottomAreaRefactor() && !DeviceEntryUdfpsRefactor.isEnabled) {
+        if (!KeyguardBottomAreaRefactor.isEnabled && !DeviceEntryUdfpsRefactor.isEnabled) {
             return
         }
 
@@ -96,7 +96,7 @@
                             )
                         }
                     } else {
-                        // keyguardBottomAreaRefactor()
+                        // KeyguardBottomAreaRefactor.isEnabled
                         LockIconView(context, null).apply {
                             id = R.id.lock_icon_view
                             lockIconViewController.get().setLockIconView(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 5c9b271..6b86a48 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
@@ -16,50 +16,34 @@
 
 package com.android.systemui.keyguard.ui.composable.section
 
-import android.content.Context
 import android.view.ViewGroup
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import com.android.compose.animation.scene.SceneScope
-import com.android.systemui.Flags.migrateClocksToBlueprint
 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.MigrateClocksToBlueprint
 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.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(
-    @Application private val context: Context,
     private val viewModel: NotificationsPlaceholderViewModel,
-    controller: NotificationStackScrollLayoutController,
-    sceneContainerFlags: SceneContainerFlags,
     sharedNotificationContainer: SharedNotificationContainer,
     sharedNotificationContainerViewModel: SharedNotificationContainerViewModel,
     stackScrollLayout: NotificationStackScrollLayout,
-    notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel,
-    ambientState: AmbientState,
-    notificationStackSizeCalculator: NotificationStackSizeCalculator,
-    @Main private val mainImmediateDispatcher: CoroutineDispatcher,
+    sharedNotificationContainerBinder: SharedNotificationContainerBinder,
 ) {
 
     init {
-        if (!migrateClocksToBlueprint()) {
-            throw IllegalStateException("this requires migrateClocksToBlueprint()")
+        if (!MigrateClocksToBlueprint.isEnabled) {
+            throw IllegalStateException("this requires MigrateClocksToBlueprint.isEnabled")
         }
         // This scene container section moves the NSSL to the SharedNotificationContainer.
         // This also requires that SharedNotificationContainer gets moved to the
@@ -73,25 +57,10 @@
             sharedNotificationContainer.addNotificationStackScrollLayout(stackScrollLayout)
         }
 
-        SharedNotificationContainerBinder.bind(
+        sharedNotificationContainerBinder.bind(
             sharedNotificationContainer,
             sharedNotificationContainerViewModel,
-            sceneContainerFlags,
-            controller,
-            notificationStackSizeCalculator,
-            mainImmediateDispatcher = mainImmediateDispatcher,
         )
-
-        if (sceneContainerFlags.isEnabled()) {
-            NotificationStackAppearanceViewBinder.bind(
-                context,
-                sharedNotificationContainer,
-                notificationStackAppearanceViewModel,
-                ambientState,
-                controller,
-                mainImmediateDispatcher = mainImmediateDispatcher,
-            )
-        }
     }
 
     @Composable
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 d780978..9ba5e3b 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
@@ -57,6 +57,7 @@
 import androidx.compose.ui.layout.positionInWindow
 import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.dimensionResource
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
@@ -70,9 +71,10 @@
 import com.android.systemui.notifications.ui.composable.Notifications.Form
 import com.android.systemui.notifications.ui.composable.Notifications.TransitionThresholds.EXPANSION_FOR_MAX_CORNER_RADIUS
 import com.android.systemui.notifications.ui.composable.Notifications.TransitionThresholds.EXPANSION_FOR_MAX_SCRIM_ALPHA
+import com.android.systemui.res.R
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shade.ui.composable.ShadeHeader
-import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackAppearanceViewBinder.SCRIM_CORNER_RADIUS
+import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
 import kotlin.math.roundToInt
 
@@ -139,6 +141,7 @@
 ) {
     val density = LocalDensity.current
     val screenCornerRadius = LocalScreenCornerRadius.current
+    val scrimCornerRadius = dimensionResource(R.dimen.notification_scrim_corner_radius)
     val scrollState = rememberScrollState()
     val syntheticScroll = viewModel.syntheticScroll.collectAsState(0f)
     val expansionFraction by viewModel.expandFraction.collectAsState(0f)
@@ -156,6 +159,8 @@
 
     val contentHeight = viewModel.intrinsicContentHeight.collectAsState()
 
+    val stackRounding = viewModel.stackRounding.collectAsState(StackRounding())
+
     // 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.
@@ -222,16 +227,12 @@
                 .graphicsLayer {
                     shape =
                         calculateCornerRadius(
+                                scrimCornerRadius,
                                 screenCornerRadius,
                                 { expansionFraction },
                                 layoutState.isTransitioningBetween(Scenes.Gone, Scenes.Shade)
                             )
-                            .let {
-                                RoundedCornerShape(
-                                    topStart = it,
-                                    topEnd = it,
-                                )
-                            }
+                            .let { stackRounding.value.toRoundedCornerShape(it) }
                     clip = true
                 }
     ) {
@@ -359,6 +360,7 @@
 }
 
 private fun calculateCornerRadius(
+    scrimCornerRadius: Dp,
     screenCornerRadius: Dp,
     expansionFraction: () -> Float,
     transitioning: Boolean,
@@ -366,12 +368,12 @@
     return if (transitioning) {
         lerp(
                 start = screenCornerRadius.value,
-                stop = SCRIM_CORNER_RADIUS,
-                fraction = (expansionFraction() / EXPANSION_FOR_MAX_CORNER_RADIUS).coerceAtMost(1f),
+                stop = scrimCornerRadius.value,
+                fraction = (expansionFraction() / EXPANSION_FOR_MAX_CORNER_RADIUS).coerceIn(0f, 1f),
             )
             .dp
     } else {
-        SCRIM_CORNER_RADIUS.dp
+        scrimCornerRadius
     }
 }
 
@@ -394,5 +396,16 @@
         this
     }
 
+fun StackRounding.toRoundedCornerShape(radius: Dp): RoundedCornerShape {
+    val topRadius = if (roundTop) radius else 0.dp
+    val bottomRadius = if (roundBottom) radius else 0.dp
+    return RoundedCornerShape(
+        topStart = topRadius,
+        topEnd = topRadius,
+        bottomStart = bottomRadius,
+        bottomEnd = bottomRadius,
+    )
+}
+
 private const val TAG = "FlexiNotifs"
 private val DEBUG_COLOR = Color(1f, 0f, 0f, 0.2f)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
index bc48dd1..244861c 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
@@ -20,6 +20,7 @@
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
@@ -36,7 +37,8 @@
 import com.android.systemui.qs.ui.adapter.QSSceneAdapter
 import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.Companion.Collapsing
 import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.Expanding
-import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.Unsquishing
+import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.UnsquishingQQS
+import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.UnsquishingQS
 import com.android.systemui.scene.shared.model.Scenes
 
 object QuickSettings {
@@ -49,6 +51,8 @@
     object Elements {
         val Content =
             ElementKey("QuickSettingsContent", scenePicker = MovableElementScenePicker(SCENES))
+        val QuickQuickSettings = ElementKey("QuickQuickSettings")
+        val SplitShadeQuickSettings = ElementKey("SplitShadeQuickSettings")
         val FooterActions = ElementKey("QuickSettingsFooterActions")
     }
 
@@ -78,12 +82,16 @@
         is TransitionState.Transition ->
             with(transitionState) {
                 when {
-                    isSplitShade -> QSSceneAdapter.State.QS
-                    fromScene == Scenes.Shade && toScene == Scenes.QuickSettings ->
+                    isSplitShade -> UnsquishingQS(squishiness)
+                    fromScene == Scenes.Shade && toScene == Scenes.QuickSettings -> {
                         Expanding(progress)
-                    fromScene == Scenes.QuickSettings && toScene == Scenes.Shade ->
+                    }
+                    fromScene == Scenes.QuickSettings && toScene == Scenes.Shade -> {
                         Collapsing(progress)
-                    fromScene == Scenes.Shade || toScene == Scenes.Shade -> Unsquishing(squishiness)
+                    }
+                    fromScene == Scenes.Shade || toScene == Scenes.Shade -> {
+                        UnsquishingQQS(squishiness)
+                    }
                     fromScene == Scenes.QuickSettings || toScene == Scenes.QuickSettings -> {
                         QSSceneAdapter.State.QS
                     }
@@ -119,6 +127,18 @@
     squishiness: Float = QuickSettings.SharedValues.SquishinessValues.Default,
 ) {
     val contentState = stateForQuickSettingsContent(isSplitShade, squishiness)
+    val transitionState = layoutState.transitionState
+    val isClosing =
+        transitionState is TransitionState.Transition &&
+            transitionState.progress >= 0.9f && // almost done closing
+            !(layoutState.isTransitioning(to = Scenes.Shade) ||
+                layoutState.isTransitioning(to = Scenes.QuickSettings))
+
+    if (isClosing) {
+        DisposableEffect(Unit) {
+            onDispose { qsSceneAdapter.setState(QSSceneAdapter.State.CLOSED) }
+        }
+    }
 
     MovableElement(
         key = QuickSettings.Elements.Content,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt
index 5c6e1c8..9b59708 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt
@@ -13,11 +13,18 @@
 ) {
     spec = tween(durationMillis = DefaultDuration.times(durationScale).inWholeMilliseconds.toInt())
 
-    fractionRange(start = .58f) { fade(ShadeHeader.Elements.Clock) }
-    fractionRange(start = .58f) { fade(ShadeHeader.Elements.CollapsedContentStart) }
-    fractionRange(start = .58f) { fade(ShadeHeader.Elements.CollapsedContentEnd) }
-    fractionRange(start = .58f) { fade(ShadeHeader.Elements.PrivacyChip) }
-    translate(QuickSettings.Elements.Content, y = -ShadeHeader.Dimensions.CollapsedHeight * .66f)
+    fractionRange(start = .58f) {
+        fade(ShadeHeader.Elements.Clock)
+        fade(ShadeHeader.Elements.CollapsedContentStart)
+        fade(ShadeHeader.Elements.CollapsedContentEnd)
+        fade(ShadeHeader.Elements.PrivacyChip)
+        fade(QuickSettings.Elements.SplitShadeQuickSettings)
+        fade(QuickSettings.Elements.FooterActions)
+    }
+    translate(
+        QuickSettings.Elements.QuickQuickSettings,
+        y = -ShadeHeader.Dimensions.CollapsedHeight * .66f
+    )
     translate(Notifications.Elements.NotificationScrim, Edge.Top, false)
 }
 
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 15e7b51..85798ac 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
@@ -55,6 +55,7 @@
 import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.LowestZIndexScenePicker
 import com.android.compose.animation.scene.SceneScope
+import com.android.compose.animation.scene.TransitionState
 import com.android.compose.animation.scene.UserAction
 import com.android.compose.animation.scene.UserActionResult
 import com.android.compose.animation.scene.animateSceneFloatAsState
@@ -222,15 +223,17 @@
                                         horizontal = Shade.Dimensions.HorizontalPadding
                                     )
                             )
-                            QuickSettings(
-                                viewModel.qsSceneAdapter,
-                                {
-                                    (viewModel.qsSceneAdapter.qqsHeight * tileSquishiness)
-                                        .roundToInt()
-                                },
-                                isSplitShade = false,
-                                squishiness = tileSquishiness,
-                            )
+                            Box(Modifier.element(QuickSettings.Elements.QuickQuickSettings)) {
+                                QuickSettings(
+                                    viewModel.qsSceneAdapter,
+                                    {
+                                        (viewModel.qsSceneAdapter.qqsHeight * tileSquishiness)
+                                            .roundToInt()
+                                    },
+                                    isSplitShade = false,
+                                    squishiness = tileSquishiness,
+                                )
+                            }
 
                             MediaIfVisible(
                                 viewModel = viewModel,
@@ -280,6 +283,8 @@
     val lifecycleOwner = LocalLifecycleOwner.current
     val footerActionsViewModel =
         remember(lifecycleOwner, viewModel) { viewModel.getFooterActionsViewModel(lifecycleOwner) }
+    val tileSquishiness by
+        animateSceneFloatAsState(value = 1f, key = QuickSettings.SharedValues.TilesSquishiness)
 
     val navBarBottomHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
     val density = LocalDensity.current
@@ -290,6 +295,7 @@
     }
 
     val quickSettingsScrollState = rememberScrollState()
+    val isScrollable = layoutState.transitionState is TransitionState.Idle
     LaunchedEffect(isCustomizing, quickSettingsScrollState) {
         if (isCustomizing) {
             quickSettingsScrollState.scrollTo(0)
@@ -318,31 +324,41 @@
                 Column(
                     verticalArrangement = Arrangement.Top,
                     modifier =
-                        Modifier.weight(1f).fillMaxHeight().thenIf(!isCustomizing) {
-                            Modifier.verticalNestedScrollToScene()
-                                .verticalScroll(quickSettingsScrollState)
-                                .clipScrollableContainer(Orientation.Horizontal)
-                                .padding(bottom = navBarBottomHeight)
-                        }
+                        Modifier.weight(1f).fillMaxSize().thenIf(!isCustomizing) {
+                            Modifier.padding(bottom = navBarBottomHeight)
+                        },
                 ) {
-                    QuickSettings(
-                        qsSceneAdapter = viewModel.qsSceneAdapter,
-                        heightProvider = { viewModel.qsSceneAdapter.qsHeight },
-                        isSplitShade = true,
-                        modifier = Modifier.fillMaxWidth(),
-                    )
+                    Column(
+                        modifier =
+                            Modifier.fillMaxSize().weight(1f).thenIf(!isCustomizing) {
+                                Modifier.verticalNestedScrollToScene()
+                                    .verticalScroll(
+                                        quickSettingsScrollState,
+                                        enabled = isScrollable
+                                    )
+                                    .clipScrollableContainer(Orientation.Horizontal)
+                            }
+                    ) {
+                        Box(
+                            modifier =
+                                Modifier.element(QuickSettings.Elements.SplitShadeQuickSettings)
+                        ) {
+                            QuickSettings(
+                                qsSceneAdapter = viewModel.qsSceneAdapter,
+                                heightProvider = { viewModel.qsSceneAdapter.qsHeight },
+                                isSplitShade = true,
+                                modifier = Modifier.fillMaxWidth(),
+                                squishiness = tileSquishiness,
+                            )
+                        }
 
-                    MediaIfVisible(
-                        viewModel = viewModel,
-                        mediaCarouselController = mediaCarouselController,
-                        mediaHost = mediaHost,
-                        modifier = Modifier.fillMaxWidth(),
-                    )
-
-                    Spacer(
-                        modifier = Modifier.weight(1f),
-                    )
-
+                        MediaIfVisible(
+                            viewModel = viewModel,
+                            mediaCarouselController = mediaCarouselController,
+                            mediaHost = mediaHost,
+                            modifier = Modifier.fillMaxWidth(),
+                        )
+                    }
                     FooterActionsWithAnimatedVisibility(
                         viewModel = footerActionsViewModel,
                         isCustomizing = isCustomizing,
@@ -354,7 +370,8 @@
                 NotificationScrollingStack(
                     viewModel = viewModel.notifications,
                     maxScrimTop = { 0f },
-                    modifier = Modifier.weight(1f).fillMaxHeight(),
+                    modifier =
+                        Modifier.weight(1f).fillMaxHeight().padding(bottom = navBarBottomHeight),
                 )
             }
         }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
index 2435170..248dfee 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
@@ -18,6 +18,7 @@
 
 import androidx.compose.animation.core.animateFloatAsState
 import androidx.compose.foundation.basicMarquee
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.size
 import androidx.compose.material3.IconButton
@@ -27,6 +28,7 @@
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.semantics.ProgressBarRangeInfo
@@ -38,6 +40,7 @@
 import androidx.compose.ui.unit.dp
 import com.android.compose.PlatformSlider
 import com.android.compose.PlatformSliderColors
+import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.ui.compose.Icon
 import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderState
 
@@ -86,18 +89,11 @@
                 Text(text = state.valueText, color = LocalContentColor.current)
             } else {
                 state.icon?.let {
-                    IconButton(
-                        onClick = onIconTapped,
-                        colors =
-                            IconButtonColors(
-                                contentColor = LocalContentColor.current,
-                                containerColor = Color.Transparent,
-                                disabledContentColor = LocalContentColor.current,
-                                disabledContainerColor = Color.Transparent,
-                            )
-                    ) {
-                        Icon(modifier = Modifier.size(24.dp), icon = it)
-                    }
+                    SliderIcon(
+                        icon = it,
+                        onIconTapped = onIconTapped,
+                        isTappable = state.isMutable,
+                    )
                 }
             }
         },
@@ -127,3 +123,32 @@
         }
     )
 }
+
+@Composable
+private fun SliderIcon(
+    icon: Icon,
+    onIconTapped: () -> Unit,
+    isTappable: Boolean,
+    modifier: Modifier = Modifier
+) {
+    if (isTappable) {
+        IconButton(
+            modifier = modifier,
+            onClick = onIconTapped,
+            colors =
+                IconButtonColors(
+                    contentColor = LocalContentColor.current,
+                    containerColor = Color.Transparent,
+                    disabledContentColor = LocalContentColor.current,
+                    disabledContainerColor = Color.Transparent,
+                ),
+            content = { Icon(modifier = Modifier.size(24.dp), icon = icon) },
+        )
+    } else {
+        Box(
+            modifier = modifier,
+            contentAlignment = Alignment.Center,
+            content = { Icon(modifier = Modifier.size(24.dp), icon = icon) },
+        )
+    }
+}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
index af51cee..dc3b612 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
@@ -73,7 +73,7 @@
 internal class SceneScopeImpl(
     private val layoutImpl: SceneTransitionLayoutImpl,
     private val scene: Scene,
-) : SceneScope {
+) : SceneScope, ElementStateScope by layoutImpl.elementStateScope {
     override val layoutState: SceneTransitionLayoutState = layoutImpl.state
 
     override fun Modifier.element(key: ElementKey): Modifier {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index b7e2dd1..ebc9099 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -131,9 +131,30 @@
  */
 @DslMarker annotation class ElementDsl
 
+/** A scope that can be used to query the target state of an element or scene. */
+interface ElementStateScope {
+    /**
+     * Return the *target* size of [this] element in the given [scene], i.e. the size of the element
+     * when idle, or `null` if the element is not composed and measured in that scene (yet).
+     */
+    fun ElementKey.targetSize(scene: SceneKey): IntSize?
+
+    /**
+     * Return the *target* offset of [this] element in the given [scene], i.e. the size of the
+     * element when idle, or `null` if the element is not composed and placed in that scene (yet).
+     */
+    fun ElementKey.targetOffset(scene: SceneKey): Offset?
+
+    /**
+     * Return the *target* size of [this] scene, i.e. the size of the scene when idle, or `null` if
+     * the scene was never composed.
+     */
+    fun SceneKey.targetSize(): IntSize?
+}
+
 @Stable
 @ElementDsl
-interface BaseSceneScope {
+interface BaseSceneScope : ElementStateScope {
     /** The state of the [SceneTransitionLayout] in which this scene is contained. */
     val layoutState: SceneTransitionLayoutState
 
@@ -415,25 +436,7 @@
     ): Float
 }
 
-interface UserActionDistanceScope : Density {
-    /**
-     * Return the *target* size of [this] element in the given [scene], i.e. the size of the element
-     * when idle, or `null` if the element is not composed and measured in that scene (yet).
-     */
-    fun ElementKey.targetSize(scene: SceneKey): IntSize?
-
-    /**
-     * Return the *target* offset of [this] element in the given [scene], i.e. the size of the
-     * element when idle, or `null` if the element is not composed and placed in that scene (yet).
-     */
-    fun ElementKey.targetOffset(scene: SceneKey): Offset?
-
-    /**
-     * Return the *target* size of [this] scene, i.e. the size of the scene when idle, or `null` if
-     * the scene was never composed.
-     */
-    fun SceneKey.targetSize(): IntSize?
-}
+interface UserActionDistanceScope : Density, ElementStateScope
 
 /** The user action has a fixed [absoluteDistance]. */
 class FixedDistance(private val distance: Dp) : UserActionDistance {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index 25b0895..b1cfdcf 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -98,6 +98,7 @@
     private val horizontalDraggableHandler: DraggableHandlerImpl
     private val verticalDraggableHandler: DraggableHandlerImpl
 
+    internal val elementStateScope = ElementStateScopeImpl(this)
     private var _userActionDistanceScope: UserActionDistanceScope? = null
     internal val userActionDistanceScope: UserActionDistanceScope
         get() =
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt
index 228d19f..b7abb33 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt
@@ -19,15 +19,9 @@
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.unit.IntSize
 
-internal class UserActionDistanceScopeImpl(
+internal class ElementStateScopeImpl(
     private val layoutImpl: SceneTransitionLayoutImpl,
-) : UserActionDistanceScope {
-    override val density: Float
-        get() = layoutImpl.density.density
-
-    override val fontScale: Float
-        get() = layoutImpl.density.fontScale
-
+) : ElementStateScope {
     override fun ElementKey.targetSize(scene: SceneKey): IntSize? {
         return layoutImpl.elements[this]?.sceneStates?.get(scene)?.targetSize.takeIf {
             it != Element.SizeUnspecified
@@ -44,3 +38,13 @@
         return layoutImpl.scenes[this]?.targetSize.takeIf { it != IntSize.Zero }
     }
 }
+
+internal class UserActionDistanceScopeImpl(
+    private val layoutImpl: SceneTransitionLayoutImpl,
+) : UserActionDistanceScope, ElementStateScope by layoutImpl.elementStateScope {
+    override val density: Float
+        get() = layoutImpl.density.density
+
+    override val fontScale: Float
+        get() = layoutImpl.density.fontScale
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
index 707777b..b0d03b1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
@@ -71,34 +71,6 @@
     }
 
     @Test
-    fun pinAuthMethod() =
-        testScope.runTest {
-            val message by collectLastValue(underTest.message)
-
-            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
-                AuthenticationMethodModel.Pin
-            )
-            runCurrent()
-            underTest.clearMessage()
-            assertThat(message).isNull()
-
-            underTest.resetMessage()
-            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
-
-            // Wrong input.
-            assertThat(underTest.authenticate(listOf(9, 8, 7)))
-                .isEqualTo(AuthenticationResult.FAILED)
-            assertThat(message).isEqualTo(MESSAGE_WRONG_PIN)
-
-            underTest.resetMessage()
-            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
-
-            // Correct input.
-            assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
-                .isEqualTo(AuthenticationResult.SUCCEEDED)
-        }
-
-    @Test
     fun pinAuthMethod_sim_skipsAuthentication() =
         testScope.runTest {
             kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
@@ -146,8 +118,6 @@
     @Test
     fun pinAuthMethod_tryAutoConfirm_withoutAutoConfirmPin() =
         testScope.runTest {
-            val message by collectLastValue(underTest.message)
-
             kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
                 AuthenticationMethodModel.Pin
             )
@@ -156,7 +126,6 @@
             // Incomplete input.
             assertThat(underTest.authenticate(listOf(1, 2), tryAutoConfirm = true))
                 .isEqualTo(AuthenticationResult.SKIPPED)
-            assertThat(message).isNull()
 
             // Correct input.
             assertThat(
@@ -166,28 +135,19 @@
                     )
                 )
                 .isEqualTo(AuthenticationResult.SKIPPED)
-            assertThat(message).isNull()
         }
 
     @Test
     fun passwordAuthMethod() =
         testScope.runTest {
-            val message by collectLastValue(underTest.message)
             kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
                 AuthenticationMethodModel.Password
             )
             runCurrent()
 
-            underTest.resetMessage()
-            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
-
             // Wrong input.
             assertThat(underTest.authenticate("alohamora".toList()))
                 .isEqualTo(AuthenticationResult.FAILED)
-            assertThat(message).isEqualTo(MESSAGE_WRONG_PASSWORD)
-
-            underTest.resetMessage()
-            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
 
             // Too short input.
             assertThat(
@@ -201,7 +161,6 @@
                     )
                 )
                 .isEqualTo(AuthenticationResult.SKIPPED)
-            assertThat(message).isEqualTo(MESSAGE_WRONG_PASSWORD)
 
             // Correct input.
             assertThat(underTest.authenticate("password".toList()))
@@ -211,13 +170,10 @@
     @Test
     fun patternAuthMethod() =
         testScope.runTest {
-            val message by collectLastValue(underTest.message)
             kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
                 AuthenticationMethodModel.Pattern
             )
             runCurrent()
-            underTest.resetMessage()
-            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
 
             // Wrong input.
             val wrongPattern =
@@ -231,10 +187,6 @@
             assertThat(wrongPattern.size)
                 .isAtLeast(kosmos.fakeAuthenticationRepository.minPatternLength)
             assertThat(underTest.authenticate(wrongPattern)).isEqualTo(AuthenticationResult.FAILED)
-            assertThat(message).isEqualTo(MESSAGE_WRONG_PATTERN)
-
-            underTest.resetMessage()
-            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
 
             // Too short input.
             val tooShortPattern =
@@ -244,10 +196,6 @@
                 )
             assertThat(underTest.authenticate(tooShortPattern))
                 .isEqualTo(AuthenticationResult.SKIPPED)
-            assertThat(message).isEqualTo(MESSAGE_WRONG_PATTERN)
-
-            underTest.resetMessage()
-            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
 
             // Correct input.
             assertThat(underTest.authenticate(FakeAuthenticationRepository.PATTERN))
@@ -258,7 +206,6 @@
     fun lockoutStarted() =
         testScope.runTest {
             val lockoutStartedEvents by collectValues(underTest.onLockoutStarted)
-            val message by collectLastValue(underTest.message)
 
             kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
                 AuthenticationMethodModel.Pin
@@ -272,17 +219,14 @@
                     .isEqualTo(AuthenticationResult.FAILED)
                 if (times < FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) {
                     assertThat(lockoutStartedEvents).isEmpty()
-                    assertThat(message).isNotEmpty()
                 }
             }
             assertThat(authenticationInteractor.lockoutEndTimestamp).isNotNull()
             assertThat(lockoutStartedEvents.size).isEqualTo(1)
-            assertThat(message).isNull()
 
             // Advance the time to finish the lockout:
             advanceTimeBy(FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS.seconds)
             assertThat(authenticationInteractor.lockoutEndTimestamp).isNull()
-            assertThat(message).isNull()
             assertThat(lockoutStartedEvents.size).isEqualTo(1)
 
             // Trigger lockout again:
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt
similarity index 83%
rename from packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt
index 701b703..c878e0b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.bouncer.domain.interactor
 
 import android.content.pm.UserInfo
-import android.os.Handler
 import android.testing.TestableLooper
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
@@ -28,27 +27,25 @@
 import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.biometrics.data.repository.FaceSensorInfo
-import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository
+import com.android.systemui.biometrics.data.repository.fakeFacePropertyRepository
+import com.android.systemui.biometrics.data.repository.fakeFingerprintPropertyRepository
 import com.android.systemui.biometrics.shared.model.SensorStrength
 import com.android.systemui.bouncer.data.repository.BouncerMessageRepositoryImpl
-import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
+import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
 import com.android.systemui.bouncer.shared.model.BouncerMessageModel
-import com.android.systemui.bouncer.ui.BouncerView
-import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryFingerprintAuthInteractor
 import com.android.systemui.flags.SystemPropertiesHelper
-import com.android.systemui.keyguard.DismissCallbackRegistry
-import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
-import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository
-import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
-import com.android.systemui.keyguard.data.repository.FakeTrustRepository
+import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeTrustRepository
 import com.android.systemui.keyguard.shared.model.AuthenticationFlags
+import com.android.systemui.kosmos.testScope
 import com.android.systemui.res.R.string.kg_too_many_failed_attempts_countdown
 import com.android.systemui.res.R.string.kg_trust_agent_disabled
-import com.android.systemui.statusbar.policy.KeyguardStateController
-import com.android.systemui.user.data.repository.FakeUserRepository
-import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.fakeUserRepository
 import com.android.systemui.util.mockito.KotlinArgumentCaptor
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
@@ -61,7 +58,6 @@
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.eq
 import org.mockito.Mock
-import org.mockito.Mockito.mock
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
@@ -70,34 +66,22 @@
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 @RunWith(AndroidJUnit4::class)
 class BouncerMessageInteractorTest : SysuiTestCase() {
-
+    private val kosmos = testKosmos()
     private val countDownTimerCallback = KotlinArgumentCaptor(CountDownTimerCallback::class.java)
     private val repository = BouncerMessageRepositoryImpl()
-    private val userRepository = FakeUserRepository()
-    private val fakeTrustRepository = FakeTrustRepository()
-    private val fakeFacePropertyRepository = FakeFacePropertyRepository()
-    private val bouncerRepository = FakeKeyguardBouncerRepository()
-    private val fakeDeviceEntryFingerprintAuthRepository =
-        FakeDeviceEntryFingerprintAuthRepository()
-    private val fakeDeviceEntryFaceAuthRepository = FakeDeviceEntryFaceAuthRepository()
-    private val biometricSettingsRepository: FakeBiometricSettingsRepository =
-        FakeBiometricSettingsRepository()
+    private val biometricSettingsRepository = kosmos.fakeBiometricSettingsRepository
+    private val testScope = kosmos.testScope
     @Mock private lateinit var updateMonitor: KeyguardUpdateMonitor
     @Mock private lateinit var securityModel: KeyguardSecurityModel
     @Mock private lateinit var countDownTimerUtil: CountDownTimerUtil
     @Mock private lateinit var systemPropertiesHelper: SystemPropertiesHelper
-    @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
-    @Mock private lateinit var mSelectedUserInteractor: SelectedUserInteractor
 
-    private lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor
-    private lateinit var testScope: TestScope
     private lateinit var underTest: BouncerMessageInteractor
 
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
-        userRepository.setUserInfos(listOf(PRIMARY_USER))
-        testScope = TestScope()
+        kosmos.fakeUserRepository.setUserInfos(listOf(PRIMARY_USER))
         allowTestableLooperAsMainThread()
         whenever(securityModel.getSecurityMode(PRIMARY_USER_ID)).thenReturn(PIN)
         biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
@@ -105,44 +89,28 @@
     }
 
     suspend fun TestScope.init() {
-        userRepository.setSelectedUserInfo(PRIMARY_USER)
+        kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
         mSetFlagsRule.enableFlags(Flags.FLAG_REVAMPED_BOUNCER_MESSAGES)
-        primaryBouncerInteractor =
-            PrimaryBouncerInteractor(
-                bouncerRepository,
-                mock(BouncerView::class.java),
-                mock(Handler::class.java),
-                mock(KeyguardStateController::class.java),
-                mock(KeyguardSecurityModel::class.java),
-                mock(PrimaryBouncerCallbackInteractor::class.java),
-                mock(FalsingCollector::class.java),
-                mock(DismissCallbackRegistry::class.java),
-                context,
-                keyguardUpdateMonitor,
-                fakeTrustRepository,
-                testScope.backgroundScope,
-                mSelectedUserInteractor,
-                mock(DeviceEntryFaceAuthInteractor::class.java),
-            )
         underTest =
             BouncerMessageInteractor(
                 repository = repository,
-                userRepository = userRepository,
+                userRepository = kosmos.fakeUserRepository,
                 countDownTimerUtil = countDownTimerUtil,
                 updateMonitor = updateMonitor,
                 biometricSettingsRepository = biometricSettingsRepository,
-                applicationScope = this.backgroundScope,
-                trustRepository = fakeTrustRepository,
+                applicationScope = testScope.backgroundScope,
+                trustRepository = kosmos.fakeTrustRepository,
                 systemPropertiesHelper = systemPropertiesHelper,
-                primaryBouncerInteractor = primaryBouncerInteractor,
-                facePropertyRepository = fakeFacePropertyRepository,
-                deviceEntryFingerprintAuthRepository = fakeDeviceEntryFingerprintAuthRepository,
-                faceAuthRepository = fakeDeviceEntryFaceAuthRepository,
+                primaryBouncerInteractor = kosmos.primaryBouncerInteractor,
+                facePropertyRepository = kosmos.fakeFacePropertyRepository,
+                deviceEntryFingerprintAuthInteractor = kosmos.deviceEntryFingerprintAuthInteractor,
+                faceAuthRepository = kosmos.fakeDeviceEntryFaceAuthRepository,
                 securityModel = securityModel
             )
         biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
-        fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false)
-        bouncerRepository.setPrimaryShow(true)
+        kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false)
+        kosmos.fakeFingerprintPropertyRepository.supportsSideFps()
+        kosmos.fakeKeyguardBouncerRepository.setPrimaryShow(true)
         runCurrent()
     }
 
@@ -268,7 +236,7 @@
             init()
             val lockoutMessage by collectLastValue(underTest.bouncerMessage)
 
-            fakeDeviceEntryFaceAuthRepository.setLockedOut(true)
+            kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(true)
             runCurrent()
 
             assertThat(primaryResMessage(lockoutMessage))
@@ -276,7 +244,7 @@
             assertThat(secondaryResMessage(lockoutMessage))
                 .isEqualTo("Can’t unlock with face. Too many attempts.")
 
-            fakeDeviceEntryFaceAuthRepository.setLockedOut(false)
+            kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(false)
             runCurrent()
 
             assertThat(primaryResMessage(lockoutMessage))
@@ -289,15 +257,17 @@
         testScope.runTest {
             init()
             val lockoutMessage by collectLastValue(underTest.bouncerMessage)
-            fakeFacePropertyRepository.setSensorInfo(FaceSensorInfo(1, SensorStrength.STRONG))
-            fakeDeviceEntryFaceAuthRepository.setLockedOut(true)
+            kosmos.fakeFacePropertyRepository.setSensorInfo(
+                FaceSensorInfo(1, SensorStrength.STRONG)
+            )
+            kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(true)
             runCurrent()
 
             assertThat(primaryResMessage(lockoutMessage)).isEqualTo("Enter PIN")
             assertThat(secondaryResMessage(lockoutMessage))
                 .isEqualTo("PIN is required after too many attempts")
 
-            fakeDeviceEntryFaceAuthRepository.setLockedOut(false)
+            kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(false)
             runCurrent()
 
             assertThat(primaryResMessage(lockoutMessage))
@@ -311,14 +281,14 @@
             init()
             val lockedOutMessage by collectLastValue(underTest.bouncerMessage)
 
-            fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true)
+            kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true)
             runCurrent()
 
             assertThat(primaryResMessage(lockedOutMessage)).isEqualTo("Enter PIN")
             assertThat(secondaryResMessage(lockedOutMessage))
                 .isEqualTo("PIN is required after too many attempts")
 
-            fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false)
+            kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false)
             runCurrent()
 
             assertThat(primaryResMessage(lockedOutMessage))
@@ -327,6 +297,19 @@
         }
 
     @Test
+    fun onUdfpsFingerprint_DoesNotShowFingerprintMessage() =
+        testScope.runTest {
+            init()
+            kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false)
+            kosmos.fakeFingerprintPropertyRepository.supportsUdfps()
+            val lockedOutMessage by collectLastValue(underTest.bouncerMessage)
+
+            runCurrent()
+
+            assertThat(primaryResMessage(lockedOutMessage)).isEqualTo("Enter PIN")
+        }
+
+    @Test
     fun onRestartForMainlineUpdate_shouldProvideRelevantMessage() =
         testScope.runTest {
             init()
@@ -344,9 +327,10 @@
     fun onAuthFlagsChanged_withTrustNotManagedAndNoBiometrics_isANoop() =
         testScope.runTest {
             init()
-            fakeTrustRepository.setTrustUsuallyManaged(false)
+            kosmos.fakeTrustRepository.setTrustUsuallyManaged(false)
             biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false)
             biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
+            runCurrent()
 
             val defaultMessage = Pair("Enter PIN", null)
 
@@ -377,12 +361,13 @@
         testScope.runTest {
             init()
 
-            userRepository.setSelectedUserInfo(PRIMARY_USER)
+            kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
             biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false)
             biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
+            runCurrent()
 
-            fakeTrustRepository.setCurrentUserTrustManaged(true)
-            fakeTrustRepository.setTrustUsuallyManaged(true)
+            kosmos.fakeTrustRepository.setCurrentUserTrustManaged(true)
+            kosmos.fakeTrustRepository.setTrustUsuallyManaged(true)
 
             val defaultMessage = Pair("Enter PIN", null)
 
@@ -415,8 +400,8 @@
     fun authFlagsChanges_withFaceEnrolled_providesDifferentMessages() =
         testScope.runTest {
             init()
-            userRepository.setSelectedUserInfo(PRIMARY_USER)
-            fakeTrustRepository.setTrustUsuallyManaged(false)
+            kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+            kosmos.fakeTrustRepository.setTrustUsuallyManaged(false)
             biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
 
             biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
@@ -453,12 +438,13 @@
     fun authFlagsChanges_withFingerprintEnrolled_providesDifferentMessages() =
         testScope.runTest {
             init()
-            userRepository.setSelectedUserInfo(PRIMARY_USER)
-            fakeTrustRepository.setCurrentUserTrustManaged(false)
+            kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+            kosmos.fakeTrustRepository.setCurrentUserTrustManaged(false)
             biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false)
 
             biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
             biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
+            runCurrent()
 
             verifyMessagesForAuthFlag(
                 LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED to
@@ -466,6 +452,7 @@
             )
 
             biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(false)
+            runCurrent()
 
             verifyMessagesForAuthFlag(
                 LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
index d30e333..c9fa671 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
@@ -48,6 +48,7 @@
             isInputEnabled = MutableStateFlow(true),
             simBouncerInteractor = kosmos.simBouncerInteractor,
             authenticationMethod = AuthenticationMethodModel.Pin,
+            onIntentionalUserInput = {},
         )
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt
new file mode 100644
index 0000000..16ec9aa
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt
@@ -0,0 +1,455 @@
+/*
+ * 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.bouncer.ui.viewmodel
+
+import android.content.pm.UserInfo
+import android.hardware.biometrics.BiometricFaceConstants
+import android.hardware.fingerprint.FingerprintManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.widget.LockPatternUtils
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
+import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
+import com.android.systemui.authentication.domain.interactor.authenticationInteractor
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pattern
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pin
+import com.android.systemui.biometrics.data.repository.FaceSensorInfo
+import com.android.systemui.biometrics.data.repository.fakeFacePropertyRepository
+import com.android.systemui.biometrics.data.repository.fakeFingerprintPropertyRepository
+import com.android.systemui.biometrics.shared.model.SensorStrength
+import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
+import com.android.systemui.bouncer.shared.flag.fakeComposeBouncerFlags
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
+import com.android.systemui.deviceentry.shared.model.ErrorFaceAuthenticationStatus
+import com.android.systemui.deviceentry.shared.model.FailedFaceAuthenticationStatus
+import com.android.systemui.deviceentry.shared.model.HelpFaceAuthenticationStatus
+import com.android.systemui.flags.fakeSystemPropertiesHelper
+import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeTrustRepository
+import com.android.systemui.keyguard.shared.model.AuthenticationFlags
+import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.HelpFingerprintAuthenticationStatus
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.res.R
+import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.google.common.truth.Truth.assertThat
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.currentTime
+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 BouncerMessageViewModelTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val authenticationInteractor by lazy { kosmos.authenticationInteractor }
+    private val bouncerInteractor by lazy { kosmos.bouncerInteractor }
+    private lateinit var underTest: BouncerMessageViewModel
+
+    @Before
+    fun setUp() {
+        kosmos.fakeUserRepository.setUserInfos(listOf(PRIMARY_USER))
+        kosmos.fakeComposeBouncerFlags.composeBouncerEnabled = true
+        underTest = kosmos.bouncerMessageViewModel
+        overrideResource(R.string.kg_trust_agent_disabled, "Trust agent is unavailable")
+        kosmos.fakeSystemPropertiesHelper.set(
+            DeviceEntryInteractor.SYS_BOOT_REASON_PROP,
+            "not mainline reboot"
+        )
+    }
+
+    @Test
+    fun message_defaultMessage_basedOnAuthMethod() =
+        testScope.runTest {
+            val message by collectLastValue(underTest.message)
+
+            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin)
+            kosmos.fakeFingerprintPropertyRepository.supportsSideFps()
+            kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+            kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
+            runCurrent()
+
+            assertThat(message!!.text).isEqualTo("Unlock with PIN or fingerprint")
+
+            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pattern)
+            runCurrent()
+            assertThat(message!!.text).isEqualTo("Unlock with pattern or fingerprint")
+
+            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Password
+            )
+            runCurrent()
+            assertThat(message!!.text).isEqualTo("Unlock with password or fingerprint")
+        }
+
+    @Test
+    fun message() =
+        testScope.runTest {
+            val message by collectLastValue(underTest.message)
+            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin)
+            assertThat(message?.isUpdateAnimated).isTrue()
+
+            repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) {
+                bouncerInteractor.authenticate(WRONG_PIN)
+            }
+            assertThat(message?.isUpdateAnimated).isFalse()
+
+            val lockoutEndMs = authenticationInteractor.lockoutEndTimestamp ?: 0
+            advanceTimeBy(lockoutEndMs - testScope.currentTime)
+            assertThat(message?.isUpdateAnimated).isTrue()
+        }
+
+    @Test
+    fun lockoutMessage() =
+        testScope.runTest {
+            val message by collectLastValue(underTest.message)
+            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin)
+            assertThat(kosmos.fakeAuthenticationRepository.lockoutEndTimestamp).isNull()
+            runCurrent()
+
+            repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) { times ->
+                bouncerInteractor.authenticate(WRONG_PIN)
+                runCurrent()
+                if (times < FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) {
+                    assertThat(message?.text).isEqualTo("Wrong PIN. Try again.")
+                    assertThat(message?.isUpdateAnimated).isTrue()
+                }
+            }
+            val lockoutSeconds = FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS
+            assertTryAgainMessage(message?.text, lockoutSeconds)
+            assertThat(message?.isUpdateAnimated).isFalse()
+
+            repeat(FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS) { time ->
+                advanceTimeBy(1.seconds)
+                val remainingSeconds = lockoutSeconds - time - 1
+                if (remainingSeconds > 0) {
+                    assertTryAgainMessage(message?.text, remainingSeconds)
+                }
+            }
+            assertThat(message?.text).isEqualTo("Enter PIN")
+            assertThat(message?.isUpdateAnimated).isTrue()
+        }
+
+    @Test
+    fun defaultMessage_mapsToDeviceEntryRestrictionReason_whenTrustAgentIsEnabled() =
+        testScope.runTest {
+            kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+            kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false)
+            kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
+            kosmos.fakeTrustRepository.setTrustUsuallyManaged(true)
+            kosmos.fakeTrustRepository.setCurrentUserTrustManaged(false)
+            runCurrent()
+
+            val defaultMessage = Pair("Enter PIN", null)
+
+            verifyMessagesForAuthFlags(
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED to defaultMessage,
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to
+                    Pair("Enter PIN", "PIN is required after device restarts"),
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to
+                    Pair("Enter PIN", "Added security required. PIN not used for a while."),
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to
+                    Pair("Enter PIN", "For added security, device was locked by work policy"),
+                LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to
+                    Pair("Enter PIN", "Trust agent is unavailable"),
+                LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to
+                    Pair("Enter PIN", "Trust agent is unavailable"),
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to
+                    Pair("Enter PIN", "PIN is required after lockdown"),
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to
+                    Pair("Enter PIN", "PIN required for additional security"),
+                LockPatternUtils.StrongAuthTracker
+                    .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to
+                    Pair(
+                        "Enter PIN",
+                        "Added security required. Device wasn’t unlocked for a while."
+                    ),
+            )
+        }
+
+    @Test
+    fun defaultMessage_mapsToDeviceEntryRestrictionReason_whenFingerprintIsAvailable() =
+        testScope.runTest {
+            kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+            kosmos.fakeFingerprintPropertyRepository.supportsSideFps()
+            kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+            kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
+            kosmos.fakeTrustRepository.setCurrentUserTrustManaged(false)
+            kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false)
+            runCurrent()
+
+            verifyMessagesForAuthFlags(
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED to
+                    Pair("Unlock with PIN or fingerprint", null),
+                LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to
+                    Pair("Unlock with PIN or fingerprint", null),
+                LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to
+                    Pair("Unlock with PIN or fingerprint", null),
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to
+                    Pair("Enter PIN", "PIN is required after device restarts"),
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to
+                    Pair("Enter PIN", "Added security required. PIN not used for a while."),
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to
+                    Pair("Enter PIN", "For added security, device was locked by work policy"),
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to
+                    Pair("Enter PIN", "PIN is required after lockdown"),
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to
+                    Pair("Enter PIN", "PIN required for additional security"),
+                LockPatternUtils.StrongAuthTracker
+                    .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to
+                    Pair(
+                        "Unlock with PIN or fingerprint",
+                        "Added security required. Device wasn’t unlocked for a while."
+                    ),
+            )
+        }
+
+    @Test
+    fun onFingerprintLockout_messageUpdated() =
+        testScope.runTest {
+            kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+            kosmos.fakeFingerprintPropertyRepository.supportsSideFps()
+            kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+            kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
+
+            val lockedOutMessage by collectLastValue(underTest.message)
+
+            kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true)
+            runCurrent()
+
+            assertThat(lockedOutMessage?.text).isEqualTo("Enter PIN")
+            assertThat(lockedOutMessage?.secondaryText)
+                .isEqualTo("PIN is required after too many attempts")
+
+            kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false)
+            runCurrent()
+
+            assertThat(lockedOutMessage?.text).isEqualTo("Unlock with PIN or fingerprint")
+            assertThat(lockedOutMessage?.secondaryText.isNullOrBlank()).isTrue()
+        }
+
+    @Test
+    fun onUdfpsFingerprint_DoesNotShowFingerprintMessage() =
+        testScope.runTest {
+            kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+            kosmos.fakeFingerprintPropertyRepository.supportsUdfps()
+            kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+            kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
+            kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false)
+            val message by collectLastValue(underTest.message)
+
+            runCurrent()
+
+            assertThat(message?.text).isEqualTo("Enter PIN")
+        }
+
+    @Test
+    fun onRestartForMainlineUpdate_shouldProvideRelevantMessage() =
+        testScope.runTest {
+            kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+            kosmos.fakeSystemPropertiesHelper.set("sys.boot.reason.last", "reboot,mainline_update")
+            kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+            runCurrent()
+
+            verifyMessagesForAuthFlags(
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to
+                    Pair("Enter PIN", "Device updated. Enter PIN to continue.")
+            )
+        }
+
+    @Test
+    fun onFaceLockout_whenItIsClass3_shouldProvideRelevantMessage() =
+        testScope.runTest {
+            kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+            kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
+            val lockoutMessage by collectLastValue(underTest.message)
+            kosmos.fakeFacePropertyRepository.setSensorInfo(
+                FaceSensorInfo(1, SensorStrength.STRONG)
+            )
+            kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(true)
+            runCurrent()
+
+            assertThat(lockoutMessage?.text).isEqualTo("Enter PIN")
+            assertThat(lockoutMessage?.secondaryText)
+                .isEqualTo("PIN is required after too many attempts")
+
+            kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(false)
+            runCurrent()
+
+            assertThat(lockoutMessage?.text).isEqualTo("Enter PIN")
+            assertThat(lockoutMessage?.secondaryText.isNullOrBlank()).isTrue()
+        }
+
+    @Test
+    fun onFaceLockout_whenItIsNotStrong_shouldProvideRelevantMessage() =
+        testScope.runTest {
+            kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+            kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
+            val lockoutMessage by collectLastValue(underTest.message)
+            kosmos.fakeFacePropertyRepository.setSensorInfo(FaceSensorInfo(1, SensorStrength.WEAK))
+            kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(true)
+            runCurrent()
+
+            assertThat(lockoutMessage?.text).isEqualTo("Enter PIN")
+            assertThat(lockoutMessage?.secondaryText)
+                .isEqualTo("Can’t unlock with face. Too many attempts.")
+
+            kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(false)
+            runCurrent()
+
+            assertThat(lockoutMessage?.text).isEqualTo("Enter PIN")
+            assertThat(lockoutMessage?.secondaryText.isNullOrBlank()).isTrue()
+        }
+
+    @Test
+    fun setFingerprintMessage_propagateValue() =
+        testScope.runTest {
+            val bouncerMessage by collectLastValue(underTest.message)
+
+            kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+            kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+            kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
+            kosmos.fakeFingerprintPropertyRepository.supportsSideFps()
+            runCurrent()
+
+            kosmos.deviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+                HelpFingerprintAuthenticationStatus(1, "some helpful message")
+            )
+            runCurrent()
+            assertThat(bouncerMessage?.text).isEqualTo("Unlock with PIN or fingerprint")
+            assertThat(bouncerMessage?.secondaryText).isEqualTo("some helpful message")
+
+            kosmos.deviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+                FailFingerprintAuthenticationStatus
+            )
+            runCurrent()
+            assertThat(bouncerMessage?.text).isEqualTo("Fingerprint not recognized")
+            assertThat(bouncerMessage?.secondaryText).isEqualTo("Try again or enter PIN")
+
+            kosmos.deviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+                ErrorFingerprintAuthenticationStatus(
+                    FingerprintManager.FINGERPRINT_ERROR_LOCKOUT,
+                    "locked out"
+                )
+            )
+            runCurrent()
+            assertThat(bouncerMessage?.text).isEqualTo("Enter PIN")
+            assertThat(bouncerMessage?.secondaryText)
+                .isEqualTo("PIN is required after too many attempts")
+        }
+
+    @Test
+    fun setFaceMessage_propagateValue() =
+        testScope.runTest {
+            val bouncerMessage by collectLastValue(underTest.message)
+
+            kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+            kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
+            kosmos.fakeBiometricSettingsRepository.setIsFaceAuthCurrentlyAllowed(true)
+            runCurrent()
+
+            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(
+                HelpFaceAuthenticationStatus(1, "some helpful message")
+            )
+            runCurrent()
+            assertThat(bouncerMessage?.text).isEqualTo("Enter PIN")
+            assertThat(bouncerMessage?.secondaryText).isEqualTo("some helpful message")
+
+            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(
+                ErrorFaceAuthenticationStatus(
+                    BiometricFaceConstants.FACE_ERROR_TIMEOUT,
+                    "Try again"
+                )
+            )
+            runCurrent()
+            assertThat(bouncerMessage?.text).isEqualTo("Enter PIN")
+            assertThat(bouncerMessage?.secondaryText).isEqualTo("Try again")
+
+            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(
+                FailedFaceAuthenticationStatus()
+            )
+            runCurrent()
+            assertThat(bouncerMessage?.text).isEqualTo("Face not recognized")
+            assertThat(bouncerMessage?.secondaryText).isEqualTo("Try again or enter PIN")
+
+            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(
+                ErrorFaceAuthenticationStatus(
+                    BiometricFaceConstants.FACE_ERROR_LOCKOUT,
+                    "locked out"
+                )
+            )
+            runCurrent()
+            assertThat(bouncerMessage?.text).isEqualTo("Enter PIN")
+            assertThat(bouncerMessage?.secondaryText)
+                .isEqualTo("Can’t unlock with face. Too many attempts.")
+        }
+
+    private fun TestScope.verifyMessagesForAuthFlags(
+        vararg authFlagToMessagePair: Pair<Int, Pair<String, String?>>
+    ) {
+        val actualMessage by collectLastValue(underTest.message)
+
+        authFlagToMessagePair.forEach { (flag, expectedMessagePair) ->
+            kosmos.fakeBiometricSettingsRepository.setAuthenticationFlags(
+                AuthenticationFlags(userId = PRIMARY_USER_ID, flag = flag)
+            )
+            runCurrent()
+
+            assertThat(actualMessage?.text).isEqualTo(expectedMessagePair.first)
+
+            if (expectedMessagePair.second == null) {
+                assertThat(actualMessage?.secondaryText.isNullOrBlank()).isTrue()
+            } else {
+                assertThat(actualMessage?.secondaryText).isEqualTo(expectedMessagePair.second)
+            }
+        }
+    }
+
+    private fun assertTryAgainMessage(
+        message: String?,
+        time: Int,
+    ) {
+        assertThat(message).contains("Try again in $time second")
+    }
+
+    companion object {
+        private val WRONG_PIN = FakeAuthenticationRepository.DEFAULT_PIN.map { it + 1 }
+        private const val PRIMARY_USER_ID = 0
+        private val PRIMARY_USER =
+            UserInfo(
+                /* id= */ PRIMARY_USER_ID,
+                /* name= */ "primary user",
+                /* flags= */ UserInfo.FLAG_PRIMARY
+            )
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
index 73db175..3afca96 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
@@ -37,7 +37,6 @@
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
-import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.flow.flatMapLatest
@@ -142,54 +141,6 @@
     }
 
     @Test
-    fun message() =
-        testScope.runTest {
-            val message by collectLastValue(underTest.message)
-            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin)
-            assertThat(message?.isUpdateAnimated).isTrue()
-
-            repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) {
-                bouncerInteractor.authenticate(WRONG_PIN)
-            }
-            assertThat(message?.isUpdateAnimated).isFalse()
-
-            val lockoutEndMs = authenticationInteractor.lockoutEndTimestamp ?: 0
-            advanceTimeBy(lockoutEndMs - testScope.currentTime)
-            assertThat(message?.isUpdateAnimated).isTrue()
-        }
-
-    @Test
-    fun lockoutMessage() =
-        testScope.runTest {
-            val authMethodViewModel by collectLastValue(underTest.authMethodViewModel)
-            val message by collectLastValue(underTest.message)
-            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin)
-            assertThat(kosmos.fakeAuthenticationRepository.lockoutEndTimestamp).isNull()
-            assertThat(authMethodViewModel?.lockoutMessageId).isNotNull()
-
-            repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) { times ->
-                bouncerInteractor.authenticate(WRONG_PIN)
-                if (times < FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) {
-                    assertThat(message?.text).isEqualTo(bouncerInteractor.message.value)
-                    assertThat(message?.isUpdateAnimated).isTrue()
-                }
-            }
-            val lockoutSeconds = FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS
-            assertTryAgainMessage(message?.text, lockoutSeconds)
-            assertThat(message?.isUpdateAnimated).isFalse()
-
-            repeat(FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS) { time ->
-                advanceTimeBy(1.seconds)
-                val remainingSeconds = lockoutSeconds - time - 1
-                if (remainingSeconds > 0) {
-                    assertTryAgainMessage(message?.text, remainingSeconds)
-                }
-            }
-            assertThat(message?.text).isEmpty()
-            assertThat(message?.isUpdateAnimated).isTrue()
-        }
-
-    @Test
     fun isInputEnabled() =
         testScope.runTest {
             val isInputEnabled by
@@ -212,25 +163,6 @@
         }
 
     @Test
-    fun dialogViewModel() =
-        testScope.runTest {
-            val authMethodViewModel by collectLastValue(underTest.authMethodViewModel)
-            val dialogViewModel by collectLastValue(underTest.dialogViewModel)
-            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin)
-            assertThat(authMethodViewModel?.lockoutMessageId).isNotNull()
-
-            repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) {
-                assertThat(dialogViewModel).isNull()
-                bouncerInteractor.authenticate(WRONG_PIN)
-            }
-            assertThat(dialogViewModel).isNotNull()
-            assertThat(dialogViewModel?.text).isNotEmpty()
-
-            dialogViewModel?.onDismiss?.invoke()
-            assertThat(dialogViewModel).isNull()
-        }
-
-    @Test
     fun isSideBySideSupported() =
         testScope.runTest {
             val isSideBySideSupported by collectLastValue(underTest.isSideBySideSupported)
@@ -265,13 +197,6 @@
         return listOf(None, Pin, Password, Pattern, Sim)
     }
 
-    private fun assertTryAgainMessage(
-        message: String?,
-        time: Int,
-    ) {
-        assertThat(message).isEqualTo("Try again in $time seconds.")
-    }
-
     companion object {
         private val WRONG_PIN = FakeAuthenticationRepository.DEFAULT_PIN.map { it + 1 }
     }
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 df50eb6..71c5785 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
@@ -66,7 +66,6 @@
     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 =
@@ -76,6 +75,7 @@
             interactor = bouncerInteractor,
             inputMethodInteractor = inputMethodInteractor,
             selectedUserInteractor = selectedUserInteractor,
+            onIntentionalUserInput = {},
         )
 
     @Before
@@ -88,11 +88,9 @@
     fun onShown() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.currentScene)
-            val message by collectLastValue(bouncerViewModel.message)
             val password by collectLastValue(underTest.password)
             lockDeviceAndOpenPasswordBouncer()
 
-            assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD)
             assertThat(password).isEmpty()
             assertThat(currentScene).isEqualTo(Scenes.Bouncer)
             assertThat(underTest.authenticationMethod).isEqualTo(AuthenticationMethodModel.Password)
@@ -101,16 +99,13 @@
     @Test
     fun onHidden_resetsPasswordInputAndMessage() =
         testScope.runTest {
-            val message by collectLastValue(bouncerViewModel.message)
             val password by collectLastValue(underTest.password)
             lockDeviceAndOpenPasswordBouncer()
 
             underTest.onPasswordInputChanged("password")
-            assertThat(message?.text).isNotEqualTo(ENTER_YOUR_PASSWORD)
             assertThat(password).isNotEmpty()
 
             underTest.onHidden()
-            assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD)
             assertThat(password).isEmpty()
         }
 
@@ -118,13 +113,11 @@
     fun onPasswordInputChanged() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.currentScene)
-            val message by collectLastValue(bouncerViewModel.message)
             val password by collectLastValue(underTest.password)
             lockDeviceAndOpenPasswordBouncer()
 
             underTest.onPasswordInputChanged("password")
 
-            assertThat(message?.text).isEmpty()
             assertThat(password).isEqualTo("password")
             assertThat(currentScene).isEqualTo(Scenes.Bouncer)
         }
@@ -144,7 +137,6 @@
     @Test
     fun onAuthenticateKeyPressed_whenWrong() =
         testScope.runTest {
-            val message by collectLastValue(bouncerViewModel.message)
             val password by collectLastValue(underTest.password)
             lockDeviceAndOpenPasswordBouncer()
 
@@ -152,13 +144,11 @@
             underTest.onAuthenticateKeyPressed()
 
             assertThat(password).isEmpty()
-            assertThat(message?.text).isEqualTo(WRONG_PASSWORD)
         }
 
     @Test
     fun onAuthenticateKeyPressed_whenEmpty() =
         testScope.runTest {
-            val message by collectLastValue(bouncerViewModel.message)
             val password by collectLastValue(underTest.password)
             kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
                 AuthenticationMethodModel.Password
@@ -171,14 +161,12 @@
             underTest.onAuthenticateKeyPressed()
 
             assertThat(password).isEmpty()
-            assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD)
         }
 
     @Test
     fun onAuthenticateKeyPressed_correctAfterWrong() =
         testScope.runTest {
             val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult)
-            val message by collectLastValue(bouncerViewModel.message)
             val password by collectLastValue(underTest.password)
             lockDeviceAndOpenPasswordBouncer()
 
@@ -186,12 +174,10 @@
             underTest.onPasswordInputChanged("wrong")
             underTest.onAuthenticateKeyPressed()
             assertThat(password).isEqualTo("")
-            assertThat(message?.text).isEqualTo(WRONG_PASSWORD)
             assertThat(authResult).isFalse()
 
             // Enter the correct password:
             underTest.onPasswordInputChanged("password")
-            assertThat(message?.text).isEmpty()
 
             underTest.onAuthenticateKeyPressed()
 
@@ -331,10 +317,8 @@
 
     private fun TestScope.switchToScene(toScene: SceneKey) {
         val currentScene by collectLastValue(sceneInteractor.currentScene)
-        val bouncerShown = currentScene != Scenes.Bouncer && toScene == Scenes.Bouncer
         val bouncerHidden = currentScene == Scenes.Bouncer && toScene != Scenes.Bouncer
         sceneInteractor.changeScene(toScene, "reason")
-        if (bouncerShown) underTest.onShown()
         if (bouncerHidden) underTest.onHidden()
         runCurrent()
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
index 91a056d..51b73ee9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
@@ -63,6 +63,7 @@
             viewModelScope = testScope.backgroundScope,
             interactor = bouncerInteractor,
             isInputEnabled = MutableStateFlow(true).asStateFlow(),
+            onIntentionalUserInput = {},
         )
     }
 
@@ -79,12 +80,10 @@
     fun onShown() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.currentScene)
-            val message by collectLastValue(bouncerViewModel.message)
             val selectedDots by collectLastValue(underTest.selectedDots)
             val currentDot by collectLastValue(underTest.currentDot)
             lockDeviceAndOpenPatternBouncer()
 
-            assertThat(message?.text).isEqualTo(ENTER_YOUR_PATTERN)
             assertThat(selectedDots).isEmpty()
             assertThat(currentDot).isNull()
             assertThat(currentScene).isEqualTo(Scenes.Bouncer)
@@ -95,14 +94,12 @@
     fun onDragStart() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.currentScene)
-            val message by collectLastValue(bouncerViewModel.message)
             val selectedDots by collectLastValue(underTest.selectedDots)
             val currentDot by collectLastValue(underTest.currentDot)
             lockDeviceAndOpenPatternBouncer()
 
             underTest.onDragStart()
 
-            assertThat(message?.text).isEmpty()
             assertThat(selectedDots).isEmpty()
             assertThat(currentDot).isNull()
             assertThat(currentScene).isEqualTo(Scenes.Bouncer)
@@ -148,7 +145,6 @@
     fun onDragEnd_whenWrong() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.currentScene)
-            val message by collectLastValue(bouncerViewModel.message)
             val selectedDots by collectLastValue(underTest.selectedDots)
             val currentDot by collectLastValue(underTest.currentDot)
             lockDeviceAndOpenPatternBouncer()
@@ -159,7 +155,6 @@
 
             assertThat(selectedDots).isEmpty()
             assertThat(currentDot).isNull()
-            assertThat(message?.text).isEqualTo(WRONG_PATTERN)
             assertThat(currentScene).isEqualTo(Scenes.Bouncer)
         }
 
@@ -302,7 +297,6 @@
     @Test
     fun onDragEnd_whenPatternTooShort() =
         testScope.runTest {
-            val message by collectLastValue(bouncerViewModel.message)
             val dialogViewModel by collectLastValue(bouncerViewModel.dialogViewModel)
             lockDeviceAndOpenPatternBouncer()
 
@@ -325,7 +319,6 @@
 
                 underTest.onDragEnd()
 
-                assertWithMessage("Attempt #$attempt").that(message?.text).isEqualTo(WRONG_PATTERN)
                 assertWithMessage("Attempt #$attempt").that(dialogViewModel).isNull()
             }
         }
@@ -334,7 +327,6 @@
     fun onDragEnd_correctAfterWrong() =
         testScope.runTest {
             val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult)
-            val message by collectLastValue(bouncerViewModel.message)
             val selectedDots by collectLastValue(underTest.selectedDots)
             val currentDot by collectLastValue(underTest.currentDot)
             lockDeviceAndOpenPatternBouncer()
@@ -344,7 +336,6 @@
             underTest.onDragEnd()
             assertThat(selectedDots).isEmpty()
             assertThat(currentDot).isNull()
-            assertThat(message?.text).isEqualTo(WRONG_PATTERN)
             assertThat(authResult).isFalse()
 
             // Enter the correct pattern:
@@ -370,10 +361,8 @@
 
     private fun TestScope.switchToScene(toScene: SceneKey) {
         val currentScene by collectLastValue(sceneInteractor.currentScene)
-        val bouncerShown = currentScene != Scenes.Bouncer && toScene == Scenes.Bouncer
         val bouncerHidden = currentScene == Scenes.Bouncer && toScene != Scenes.Bouncer
         sceneInteractor.changeScene(toScene, "reason")
-        if (bouncerShown) underTest.onShown()
         if (bouncerHidden) underTest.onHidden()
         runCurrent()
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
index 7b75a37..5647954 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
@@ -56,7 +56,6 @@
     private val sceneInteractor by lazy { kosmos.sceneInteractor }
     private val authenticationInteractor by lazy { kosmos.authenticationInteractor }
     private val bouncerInteractor by lazy { kosmos.bouncerInteractor }
-    private val bouncerViewModel by lazy { kosmos.bouncerViewModel }
     private lateinit var underTest: PinBouncerViewModel
 
     @Before
@@ -69,6 +68,7 @@
                 isInputEnabled = MutableStateFlow(true).asStateFlow(),
                 simBouncerInteractor = kosmos.simBouncerInteractor,
                 authenticationMethod = AuthenticationMethodModel.Pin,
+                onIntentionalUserInput = {},
             )
 
         overrideResource(R.string.keyguard_enter_your_pin, ENTER_YOUR_PIN)
@@ -78,11 +78,9 @@
     @Test
     fun onShown() =
         testScope.runTest {
-            val message by collectLastValue(bouncerViewModel.message)
             val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
             lockDeviceAndOpenPinBouncer()
 
-            assertThat(message?.text).ignoringCase().isEqualTo(ENTER_YOUR_PIN)
             assertThat(pin).isEmpty()
             assertThat(underTest.authenticationMethod).isEqualTo(AuthenticationMethodModel.Pin)
         }
@@ -98,6 +96,7 @@
                     isInputEnabled = MutableStateFlow(true).asStateFlow(),
                     simBouncerInteractor = kosmos.simBouncerInteractor,
                     authenticationMethod = AuthenticationMethodModel.Sim,
+                    onIntentionalUserInput = {},
                 )
 
             assertThat(underTest.isSimAreaVisible).isTrue()
@@ -126,6 +125,7 @@
                     isInputEnabled = MutableStateFlow(true).asStateFlow(),
                     simBouncerInteractor = kosmos.simBouncerInteractor,
                     authenticationMethod = AuthenticationMethodModel.Sim,
+                    onIntentionalUserInput = {},
                 )
             kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true)
             val hintedPinLength by collectLastValue(underTest.hintedPinLength)
@@ -136,20 +136,17 @@
     @Test
     fun onPinButtonClicked() =
         testScope.runTest {
-            val message by collectLastValue(bouncerViewModel.message)
             val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
             lockDeviceAndOpenPinBouncer()
 
             underTest.onPinButtonClicked(1)
 
-            assertThat(message?.text).isEmpty()
             assertThat(pin).containsExactly(1)
         }
 
     @Test
     fun onBackspaceButtonClicked() =
         testScope.runTest {
-            val message by collectLastValue(bouncerViewModel.message)
             val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
             lockDeviceAndOpenPinBouncer()
 
@@ -158,7 +155,6 @@
 
             underTest.onBackspaceButtonClicked()
 
-            assertThat(message?.text).isEmpty()
             assertThat(pin).isEmpty()
         }
 
@@ -183,7 +179,6 @@
     fun onBackspaceButtonLongPressed() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.currentScene)
-            val message by collectLastValue(bouncerViewModel.message)
             val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
             lockDeviceAndOpenPinBouncer()
 
@@ -195,7 +190,6 @@
 
             underTest.onBackspaceButtonLongPressed()
 
-            assertThat(message?.text).isEmpty()
             assertThat(pin).isEmpty()
             assertThat(currentScene).isEqualTo(Scenes.Bouncer)
         }
@@ -217,7 +211,6 @@
     fun onAuthenticateButtonClicked_whenWrong() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.currentScene)
-            val message by collectLastValue(bouncerViewModel.message)
             val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
             lockDeviceAndOpenPinBouncer()
 
@@ -230,7 +223,6 @@
             underTest.onAuthenticateButtonClicked()
 
             assertThat(pin).isEmpty()
-            assertThat(message?.text).ignoringCase().isEqualTo(WRONG_PIN)
             assertThat(currentScene).isEqualTo(Scenes.Bouncer)
         }
 
@@ -238,7 +230,6 @@
     fun onAuthenticateButtonClicked_correctAfterWrong() =
         testScope.runTest {
             val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult)
-            val message by collectLastValue(bouncerViewModel.message)
             val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
             lockDeviceAndOpenPinBouncer()
 
@@ -248,13 +239,11 @@
             underTest.onPinButtonClicked(4)
             underTest.onPinButtonClicked(5) // PIN is now wrong!
             underTest.onAuthenticateButtonClicked()
-            assertThat(message?.text).ignoringCase().isEqualTo(WRONG_PIN)
             assertThat(pin).isEmpty()
             assertThat(authResult).isFalse()
 
             // Enter the correct PIN:
             FakeAuthenticationRepository.DEFAULT_PIN.forEach(underTest::onPinButtonClicked)
-            assertThat(message?.text).isEmpty()
 
             underTest.onAuthenticateButtonClicked()
 
@@ -277,7 +266,6 @@
     fun onAutoConfirm_whenWrong() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.currentScene)
-            val message by collectLastValue(bouncerViewModel.message)
             val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
             kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true)
             lockDeviceAndOpenPinBouncer()
@@ -290,7 +278,6 @@
             ) // PIN is now wrong!
 
             assertThat(pin).isEmpty()
-            assertThat(message?.text).ignoringCase().isEqualTo(WRONG_PIN)
             assertThat(currentScene).isEqualTo(Scenes.Bouncer)
         }
 
@@ -390,10 +377,8 @@
 
     private fun TestScope.switchToScene(toScene: SceneKey) {
         val currentScene by collectLastValue(sceneInteractor.currentScene)
-        val bouncerShown = currentScene != Scenes.Bouncer && toScene == Scenes.Bouncer
         val bouncerHidden = currentScene == Scenes.Bouncer && toScene != Scenes.Bouncer
         sceneInteractor.changeScene(toScene, "reason")
-        if (bouncerShown) underTest.onShown()
         if (bouncerHidden) underTest.onHidden()
         runCurrent()
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
index 8e2e947..a7e98ea 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
@@ -18,10 +18,16 @@
 
 import android.app.smartspace.SmartspaceTarget
 import android.appwidget.AppWidgetProviderInfo
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
 import android.content.pm.UserInfo
 import android.os.UserHandle
 import android.provider.Settings
 import android.widget.RemoteViews
+import androidx.activity.result.ActivityResultLauncher
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.UiEventLogger
@@ -39,6 +45,7 @@
 import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
 import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.media.controls.ui.view.MediaHost
@@ -46,15 +53,19 @@
 import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository
 import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository
 import com.android.systemui.testKosmos
-import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.Mock
 import org.mockito.Mockito
+import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
@@ -64,6 +75,8 @@
     @Mock private lateinit var mediaHost: MediaHost
     @Mock private lateinit var uiEventLogger: UiEventLogger
     @Mock private lateinit var providerInfo: AppWidgetProviderInfo
+    @Mock private lateinit var packageManager: PackageManager
+    @Mock private lateinit var activityResultLauncher: ActivityResultLauncher<Intent>
 
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
@@ -73,6 +86,8 @@
     private lateinit var smartspaceRepository: FakeSmartspaceRepository
     private lateinit var mediaRepository: FakeCommunalMediaRepository
 
+    private val testableResources = context.orCreateTestableResources
+
     private lateinit var underTest: CommunalEditModeViewModel
 
     @Before
@@ -96,6 +111,7 @@
                 mediaHost,
                 uiEventLogger,
                 logcatLogBuffer("CommunalEditModeViewModelTest"),
+                kosmos.testDispatcher,
             )
     }
 
@@ -217,7 +233,69 @@
         verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL)
     }
 
+    @Test
+    fun onOpenWidgetPicker_launchesWidgetPickerActivity() {
+        testScope.runTest {
+            whenever(packageManager.resolveActivity(any(), anyInt())).then {
+                ResolveInfo().apply {
+                    activityInfo = ActivityInfo().apply { packageName = WIDGET_PICKER_PACKAGE_NAME }
+                }
+            }
+
+            val success =
+                underTest.onOpenWidgetPicker(
+                    testableResources.resources,
+                    packageManager,
+                    activityResultLauncher
+                )
+
+            verify(activityResultLauncher).launch(any())
+            assertTrue(success)
+        }
+    }
+
+    @Test
+    fun onOpenWidgetPicker_launcherActivityNotResolved_doesNotLaunchWidgetPickerActivity() {
+        testScope.runTest {
+            whenever(packageManager.resolveActivity(any(), anyInt())).thenReturn(null)
+
+            val success =
+                underTest.onOpenWidgetPicker(
+                    testableResources.resources,
+                    packageManager,
+                    activityResultLauncher
+                )
+
+            verify(activityResultLauncher, never()).launch(any())
+            assertFalse(success)
+        }
+    }
+
+    @Test
+    fun onOpenWidgetPicker_activityLaunchThrowsException_failure() {
+        testScope.runTest {
+            whenever(packageManager.resolveActivity(any(), anyInt())).then {
+                ResolveInfo().apply {
+                    activityInfo = ActivityInfo().apply { packageName = WIDGET_PICKER_PACKAGE_NAME }
+                }
+            }
+
+            whenever(activityResultLauncher.launch(any()))
+                .thenThrow(ActivityNotFoundException::class.java)
+
+            val success =
+                underTest.onOpenWidgetPicker(
+                    testableResources.resources,
+                    packageManager,
+                    activityResultLauncher,
+                )
+
+            assertFalse(success)
+        }
+    }
+
     private companion object {
         val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN)
+        const val WIDGET_PICKER_PACKAGE_NAME = "widget_picker_package_name"
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorTest.kt
similarity index 83%
rename from packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorTest.kt
index decbdaf..51f9957 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorTest.kt
@@ -26,12 +26,10 @@
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
-import kotlin.test.Test
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.runTest
+import org.junit.Test
 import org.junit.runner.RunWith
 
-@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class DeviceEntryFingerprintAuthInteractorTest : SysuiTestCase() {
@@ -59,17 +57,20 @@
         }
 
     @Test
-    fun isSensorUnderDisplay_trueForUdfpsSensorTypes() =
+    fun isFingerprintCurrentlyAllowedInBouncer_trueForNonUdfpsSensorTypes() =
         testScope.runTest {
-            val isSensorUnderDisplay by collectLastValue(underTest.isSensorUnderDisplay)
+            biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
+
+            val isFingerprintCurrentlyAllowedInBouncer by
+                collectLastValue(underTest.isFingerprintCurrentlyAllowedOnBouncer)
 
             fingerprintPropertyRepository.supportsUdfps()
-            assertThat(isSensorUnderDisplay).isTrue()
+            assertThat(isFingerprintCurrentlyAllowedInBouncer).isFalse()
 
             fingerprintPropertyRepository.supportsRearFps()
-            assertThat(isSensorUnderDisplay).isFalse()
+            assertThat(isFingerprintCurrentlyAllowedInBouncer).isTrue()
 
             fingerprintPropertyRepository.supportsSideFps()
-            assertThat(isSensorUnderDisplay).isFalse()
+            assertThat(isFingerprintCurrentlyAllowedInBouncer).isTrue()
         }
 }
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 769caaa..36458ed 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
@@ -270,12 +270,61 @@
         }
 
     @Test
+    fun transitionValue_canceled_toAnotherState() =
+        testScope.runTest {
+            val transitionValuesGone by collectValues(underTest.transitionValue(state = GONE))
+            val transitionValuesAod by collectValues(underTest.transitionValue(state = AOD))
+            val transitionValuesLs by collectValues(underTest.transitionValue(state = LOCKSCREEN))
+
+            listOf(
+                    TransitionStep(GONE, AOD, 0f, STARTED),
+                    TransitionStep(GONE, AOD, 0.5f, RUNNING),
+                    TransitionStep(GONE, AOD, 0.5f, CANCELED),
+                    TransitionStep(AOD, LOCKSCREEN, 0.5f, STARTED),
+                    TransitionStep(AOD, LOCKSCREEN, 0.7f, RUNNING),
+                    TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED),
+                )
+                .forEach {
+                    repository.sendTransitionStep(it)
+                    runCurrent()
+                }
+
+            assertThat(transitionValuesGone).isEqualTo(listOf(1f, 0.5f, 0f))
+            assertThat(transitionValuesAod).isEqualTo(listOf(0f, 0.5f, 0.5f, 0.3f, 0f))
+            assertThat(transitionValuesLs).isEqualTo(listOf(0.5f, 0.7f, 1f))
+        }
+
+    @Test
+    fun transitionValue_canceled_backToOriginalState() =
+        testScope.runTest {
+            val transitionValuesGone by collectValues(underTest.transitionValue(state = GONE))
+            val transitionValuesAod by collectValues(underTest.transitionValue(state = AOD))
+
+            listOf(
+                    TransitionStep(GONE, AOD, 0f, STARTED),
+                    TransitionStep(GONE, AOD, 0.5f, RUNNING),
+                    TransitionStep(GONE, AOD, 1f, CANCELED),
+                    TransitionStep(AOD, GONE, 0.5f, STARTED),
+                    TransitionStep(AOD, GONE, 0.7f, RUNNING),
+                    TransitionStep(AOD, GONE, 1f, FINISHED),
+                )
+                .forEach {
+                    repository.sendTransitionStep(it)
+                    runCurrent()
+                }
+
+            assertThat(transitionValuesGone).isEqualTo(listOf(1f, 0.5f, 0.5f, 0.7f, 1f))
+            assertThat(transitionValuesAod).isEqualTo(listOf(0f, 0.5f, 0.5f, 0.3f, 0f))
+        }
+
+    @Test
     fun isInTransitionToAnyState() =
         testScope.runTest {
             val inTransition by collectValues(underTest.isInTransitionToAnyState)
 
             assertEquals(
                 listOf(
+                    false,
                     true, // The repo is seeded with a transition from OFF to LOCKSCREEN.
                     false,
                 ),
@@ -288,6 +337,7 @@
 
             assertEquals(
                 listOf(
+                    false,
                     true,
                     false,
                     true,
@@ -301,6 +351,7 @@
 
             assertEquals(
                 listOf(
+                    false,
                     true,
                     false,
                     true,
@@ -314,6 +365,7 @@
 
             assertEquals(
                 listOf(
+                    false,
                     true,
                     false,
                     true,
@@ -330,6 +382,7 @@
 
             assertEquals(
                 listOf(
+                    false,
                     true,
                     false,
                 ),
@@ -345,6 +398,7 @@
 
             assertEquals(
                 listOf(
+                    false,
                     true,
                     false,
                     true,
@@ -359,6 +413,7 @@
 
             assertEquals(
                 listOf(
+                    false,
                     true,
                     false,
                     true,
@@ -379,6 +434,7 @@
 
             assertEquals(
                 listOf(
+                    false,
                     true,
                     false,
                     true,
@@ -398,6 +454,7 @@
 
             assertEquals(
                 listOf(
+                    false,
                     true,
                     false,
                     true,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelTest.kt
index d443851..0cc0c2f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelTest.kt
@@ -19,6 +19,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.flags.Flags
 import com.android.systemui.flags.fakeFeatureFlagsClassic
@@ -32,6 +33,7 @@
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -49,9 +51,7 @@
         }
     private val testScope = kosmos.testScope
     private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
-    private val underTest by lazy {
-        kosmos.alternateBouncerToGoneTransitionViewModel
-    }
+    private val underTest by lazy { kosmos.alternateBouncerToGoneTransitionViewModel }
 
     @Test
     fun deviceEntryParentViewDisappear() =
@@ -73,6 +73,61 @@
             values.forEach { assertThat(it).isEqualTo(0f) }
         }
 
+    @Test
+    fun lockscreenAlpha() =
+        testScope.runTest {
+            val startAlpha = 0.6f
+            val viewState = ViewStateAccessor(alpha = { startAlpha })
+            val alpha by collectLastValue(underTest.lockscreenAlpha(viewState))
+            runCurrent()
+
+            keyguardTransitionRepository.sendTransitionSteps(
+                listOf(
+                    step(0f, TransitionState.STARTED),
+                    step(0.25f),
+                    step(0.5f),
+                    step(0.75f),
+                    step(1f),
+                ),
+                testScope,
+            )
+
+            // Alpha starts at the starting value from ViewStateAccessor.
+            keyguardTransitionRepository.sendTransitionStep(
+                step(0f, state = TransitionState.STARTED)
+            )
+            runCurrent()
+            assertThat(alpha).isEqualTo(startAlpha)
+
+            // Alpha finishes in 200ms out of 500ms, check the alpha at the halfway point.
+            val progress = 0.2f
+            keyguardTransitionRepository.sendTransitionStep(step(progress))
+            runCurrent()
+            assertThat(alpha).isEqualTo(0.3f)
+
+            // Alpha ends at 0.
+            keyguardTransitionRepository.sendTransitionStep(step(1f))
+            runCurrent()
+            assertThat(alpha).isEqualTo(0f)
+        }
+
+    @Test
+    fun lockscreenAlpha_zeroInitialAlpha() =
+        testScope.runTest {
+            // ViewState starts at 0 alpha.
+            val viewState = ViewStateAccessor(alpha = { 0f })
+            val alpha by collectValues(underTest.lockscreenAlpha(viewState))
+
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.ALTERNATE_BOUNCER,
+                to = GONE,
+                testScope
+            )
+
+            // Alpha starts and ends at 0.
+            alpha.forEach { assertThat(it).isEqualTo(0f) }
+        }
+
     private fun step(value: Float, state: TransitionState = RUNNING): TransitionStep {
         return TransitionStep(
             from = KeyguardState.ALTERNATE_BOUNCER,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelTest.kt
index 0796af0..409c551 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelTest.kt
@@ -91,27 +91,6 @@
             assertThat(bgViewAlpha).isEqualTo(1f)
         }
 
-    @Test
-    fun deviceEntryBackgroundViewAlpha_rearFpEnrolled_noUpdates() =
-        testScope.runTest {
-            fingerprintPropertyRepository.supportsRearFps()
-            val bgViewAlpha by collectLastValue(underTest.deviceEntryBackgroundViewAlpha)
-            keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
-            assertThat(bgViewAlpha).isNull()
-
-            keyguardTransitionRepository.sendTransitionStep(step(0.5f))
-            assertThat(bgViewAlpha).isNull()
-
-            keyguardTransitionRepository.sendTransitionStep(step(.75f))
-            assertThat(bgViewAlpha).isNull()
-
-            keyguardTransitionRepository.sendTransitionStep(step(1f))
-            assertThat(bgViewAlpha).isNull()
-
-            keyguardTransitionRepository.sendTransitionStep(step(1f, TransitionState.FINISHED))
-            assertThat(bgViewAlpha).isNull()
-        }
-
     private fun step(
         value: Float,
         state: TransitionState = TransitionState.RUNNING
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/MediaTestHelper.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/MediaTestHelper.kt
new file mode 100644
index 0000000..8e44932
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/MediaTestHelper.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.media.controls
+
+import android.R
+import android.app.smartspace.SmartspaceAction
+import android.content.Context
+import android.graphics.drawable.Icon
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+
+class MediaTestHelper {
+    companion object {
+        /** Returns a list of three mocked recommendations */
+        fun getValidRecommendationList(context: Context): List<SmartspaceAction> {
+            val mediaRecommendationItem =
+                mock<SmartspaceAction> {
+                    whenever(icon)
+                        .thenReturn(
+                            Icon.createWithResource(
+                                context,
+                                R.drawable.ic_media_play,
+                            )
+                        )
+                }
+            return listOf(mediaRecommendationItem, mediaRecommendationItem, mediaRecommendationItem)
+        }
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaDataRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaDataRepositoryTest.kt
new file mode 100644
index 0000000..6c41bc3
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaDataRepositoryTest.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.media.controls.data.repository
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.Flags
+import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.media.controls.MediaTestHelper
+import com.android.systemui.media.controls.shared.model.MediaData
+import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class MediaDataRepositoryTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+
+    private val underTest: MediaDataRepository = kosmos.mediaDataRepository
+
+    @Test
+    fun setRecommendation() =
+        testScope.runTest {
+            val smartspaceData by collectLastValue(underTest.smartspaceMediaData)
+            val recommendation = SmartspaceMediaData(isActive = true)
+
+            underTest.setRecommendation(recommendation)
+
+            assertThat(smartspaceData).isEqualTo(recommendation)
+        }
+
+    @Test
+    fun addAndRemoveMediaData() =
+        testScope.runTest {
+            val entries by collectLastValue(underTest.mediaEntries)
+
+            val firstKey = "key1"
+            val firstData = MediaData().copy(isPlaying = true)
+
+            val secondKey = "key2"
+            val secondData = MediaData().copy(resumption = true)
+
+            underTest.addMediaEntry(firstKey, firstData)
+            underTest.addMediaEntry(secondKey, secondData)
+            underTest.addMediaEntry(firstKey, firstData.copy(isPlaying = false))
+
+            assertThat(entries!!.size).isEqualTo(2)
+            assertThat(entries!![firstKey]).isNotEqualTo(firstData)
+
+            underTest.removeMediaEntry(firstKey)
+
+            assertThat(entries!!.size).isEqualTo(1)
+            assertThat(entries!![secondKey]).isEqualTo(secondData)
+        }
+
+    @Test
+    fun setRecommendationInactive() =
+        testScope.runTest {
+            kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, true)
+            val smartspaceData by collectLastValue(underTest.smartspaceMediaData)
+            val recommendation =
+                SmartspaceMediaData(
+                    targetId = KEY_MEDIA_SMARTSPACE,
+                    isActive = true,
+                    recommendations = MediaTestHelper.getValidRecommendationList(context),
+                )
+
+            underTest.setRecommendation(recommendation)
+
+            assertThat(smartspaceData).isEqualTo(recommendation)
+
+            underTest.setRecommendationInactive(KEY_MEDIA_SMARTSPACE)
+
+            assertThat(smartspaceData).isNotEqualTo(recommendation)
+            assertThat(smartspaceData!!.isActive).isFalse()
+        }
+
+    @Test
+    fun dismissRecommendation() =
+        testScope.runTest {
+            val smartspaceData by collectLastValue(underTest.smartspaceMediaData)
+            val recommendation =
+                SmartspaceMediaData(
+                    targetId = KEY_MEDIA_SMARTSPACE,
+                    isActive = true,
+                    recommendations = MediaTestHelper.getValidRecommendationList(context),
+                )
+
+            underTest.setRecommendation(recommendation)
+
+            assertThat(smartspaceData).isEqualTo(recommendation)
+
+            underTest.dismissSmartspaceRecommendation(KEY_MEDIA_SMARTSPACE)
+
+            assertThat(smartspaceData!!.isActive).isFalse()
+        }
+
+    companion object {
+        private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID"
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt
new file mode 100644
index 0000000..d39e77d
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt
@@ -0,0 +1,144 @@
+/*
+ * 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.media.controls.data.repository
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.media.controls.MediaTestHelper
+import com.android.systemui.media.controls.shared.model.MediaData
+import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class MediaFilterRepositoryTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+
+    private val underTest: MediaFilterRepository = kosmos.mediaFilterRepository
+
+    @Test
+    fun addSelectedUserMediaEntry_activeThenInactivate() =
+        testScope.runTest {
+            val selectedUserEntries by collectLastValue(underTest.selectedUserEntries)
+
+            val userMedia = MediaData().copy(active = true)
+
+            underTest.addSelectedUserMediaEntry(KEY, userMedia)
+
+            assertThat(selectedUserEntries?.get(KEY)).isEqualTo(userMedia)
+
+            underTest.addSelectedUserMediaEntry(KEY, userMedia.copy(active = false))
+
+            assertThat(selectedUserEntries?.get(KEY)).isNotEqualTo(userMedia)
+            assertThat(selectedUserEntries?.get(KEY)?.active).isFalse()
+        }
+
+    @Test
+    fun addSelectedUserMediaEntry_thenRemove_returnsBoolean() =
+        testScope.runTest {
+            val selectedUserEntries by collectLastValue(underTest.selectedUserEntries)
+
+            val userMedia = MediaData()
+
+            underTest.addSelectedUserMediaEntry(KEY, userMedia)
+
+            assertThat(selectedUserEntries?.get(KEY)).isEqualTo(userMedia)
+
+            assertThat(underTest.removeSelectedUserMediaEntry(KEY, userMedia)).isTrue()
+        }
+
+    @Test
+    fun addSelectedUserMediaEntry_thenRemove_returnsValue() =
+        testScope.runTest {
+            val selectedUserEntries by collectLastValue(underTest.selectedUserEntries)
+
+            val userMedia = MediaData()
+
+            underTest.addSelectedUserMediaEntry(KEY, userMedia)
+
+            assertThat(selectedUserEntries?.get(KEY)).isEqualTo(userMedia)
+
+            assertThat(underTest.removeSelectedUserMediaEntry(KEY)).isEqualTo(userMedia)
+        }
+
+    @Test
+    fun addAllUserMediaEntry_activeThenInactivate() =
+        testScope.runTest {
+            val allUserEntries by collectLastValue(underTest.allUserEntries)
+
+            val userMedia = MediaData().copy(active = true)
+
+            underTest.addMediaEntry(KEY, userMedia)
+
+            assertThat(allUserEntries?.get(KEY)).isEqualTo(userMedia)
+
+            underTest.addMediaEntry(KEY, userMedia.copy(active = false))
+
+            assertThat(allUserEntries?.get(KEY)).isNotEqualTo(userMedia)
+            assertThat(allUserEntries?.get(KEY)?.active).isFalse()
+        }
+
+    @Test
+    fun addAllUserMediaEntry_thenRemove_returnsValue() =
+        testScope.runTest {
+            val allUserEntries by collectLastValue(underTest.allUserEntries)
+
+            val userMedia = MediaData()
+
+            underTest.addMediaEntry(KEY, userMedia)
+
+            assertThat(allUserEntries?.get(KEY)).isEqualTo(userMedia)
+
+            assertThat(underTest.removeMediaEntry(KEY)).isEqualTo(userMedia)
+        }
+
+    @Test
+    fun addActiveRecommendation_thenInactive() =
+        testScope.runTest {
+            val smartspaceMediaData by collectLastValue(underTest.smartspaceMediaData)
+
+            val mediaRecommendation =
+                SmartspaceMediaData(
+                    targetId = KEY_MEDIA_SMARTSPACE,
+                    isActive = true,
+                    recommendations = MediaTestHelper.getValidRecommendationList(context),
+                )
+
+            underTest.setRecommendation(mediaRecommendation)
+
+            assertThat(smartspaceMediaData).isEqualTo(mediaRecommendation)
+
+            underTest.setRecommendation(mediaRecommendation.copy(isActive = false))
+
+            assertThat(smartspaceMediaData).isNotEqualTo(mediaRecommendation)
+            assertThat(smartspaceMediaData?.isActive).isFalse()
+        }
+
+    companion object {
+        private const val KEY = "KEY"
+        private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID"
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt
new file mode 100644
index 0000000..6e67000
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt
@@ -0,0 +1,199 @@
+/*
+ * 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.media.controls.domain.interactor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.Flags
+import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.media.controls.MediaTestHelper
+import com.android.systemui.media.controls.data.repository.MediaFilterRepository
+import com.android.systemui.media.controls.data.repository.mediaFilterRepository
+import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor
+import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor
+import com.android.systemui.media.controls.shared.model.MediaData
+import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class MediaCarouselInteractorTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+
+    private val mediaFilterRepository: MediaFilterRepository = kosmos.mediaFilterRepository
+    private val underTest: MediaCarouselInteractor = kosmos.mediaCarouselInteractor
+
+    @Test
+    fun addUserMediaEntry_activeThenInactivate() =
+        testScope.runTest {
+            val hasActiveMediaOrRecommendation by
+                collectLastValue(underTest.hasActiveMediaOrRecommendation)
+            val hasActiveMedia by collectLastValue(underTest.hasActiveMedia)
+            val hasAnyMedia by collectLastValue(underTest.hasAnyMedia)
+
+            val userMedia = MediaData().copy(active = true)
+
+            mediaFilterRepository.addSelectedUserMediaEntry(KEY, userMedia)
+
+            assertThat(hasActiveMediaOrRecommendation).isTrue()
+            assertThat(hasActiveMedia).isTrue()
+            assertThat(hasAnyMedia).isTrue()
+
+            mediaFilterRepository.addSelectedUserMediaEntry(KEY, userMedia.copy(active = false))
+
+            assertThat(hasActiveMediaOrRecommendation).isFalse()
+            assertThat(hasActiveMedia).isFalse()
+            assertThat(hasAnyMedia).isTrue()
+        }
+
+    @Test
+    fun addInactiveUserMediaEntry_thenRemove() =
+        testScope.runTest {
+            val hasActiveMediaOrRecommendation by
+                collectLastValue(underTest.hasActiveMediaOrRecommendation)
+            val hasActiveMedia by collectLastValue(underTest.hasActiveMedia)
+            val hasAnyMedia by collectLastValue(underTest.hasAnyMedia)
+
+            val userMedia = MediaData().copy(active = false)
+
+            mediaFilterRepository.addSelectedUserMediaEntry(KEY, userMedia)
+
+            assertThat(hasActiveMediaOrRecommendation).isFalse()
+            assertThat(hasActiveMedia).isFalse()
+            assertThat(hasAnyMedia).isTrue()
+
+            assertThat(mediaFilterRepository.removeSelectedUserMediaEntry(KEY, userMedia)).isTrue()
+
+            assertThat(hasActiveMediaOrRecommendation).isFalse()
+            assertThat(hasActiveMedia).isFalse()
+            assertThat(hasAnyMedia).isFalse()
+        }
+
+    @Test
+    fun addActiveRecommendation_inactiveMedia() =
+        testScope.runTest {
+            val hasActiveMediaOrRecommendation by
+                collectLastValue(underTest.hasActiveMediaOrRecommendation)
+            val hasAnyMediaOrRecommendation by
+                collectLastValue(underTest.hasAnyMediaOrRecommendation)
+            kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, false)
+
+            val userMediaRecommendation =
+                SmartspaceMediaData(
+                    targetId = KEY_MEDIA_SMARTSPACE,
+                    isActive = true,
+                    recommendations = MediaTestHelper.getValidRecommendationList(context),
+                )
+            val userMedia = MediaData().copy(active = false)
+
+            mediaFilterRepository.setRecommendation(userMediaRecommendation)
+
+            assertThat(hasActiveMediaOrRecommendation).isTrue()
+            assertThat(hasAnyMediaOrRecommendation).isTrue()
+
+            mediaFilterRepository.addSelectedUserMediaEntry(KEY, userMedia)
+
+            assertThat(hasActiveMediaOrRecommendation).isTrue()
+            assertThat(hasAnyMediaOrRecommendation).isTrue()
+        }
+
+    @Test
+    fun addActiveRecommendation_thenInactive() =
+        testScope.runTest {
+            val hasActiveMediaOrRecommendation by
+                collectLastValue(underTest.hasActiveMediaOrRecommendation)
+            val hasAnyMediaOrRecommendation by
+                collectLastValue(underTest.hasAnyMediaOrRecommendation)
+            kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, false)
+
+            val mediaRecommendation =
+                SmartspaceMediaData(
+                    targetId = KEY_MEDIA_SMARTSPACE,
+                    isActive = true,
+                    recommendations = MediaTestHelper.getValidRecommendationList(context),
+                )
+
+            mediaFilterRepository.setRecommendation(mediaRecommendation)
+
+            assertThat(hasActiveMediaOrRecommendation).isTrue()
+            assertThat(hasAnyMediaOrRecommendation).isTrue()
+
+            mediaFilterRepository.setRecommendation(mediaRecommendation.copy(isActive = false))
+
+            assertThat(hasActiveMediaOrRecommendation).isFalse()
+            assertThat(hasAnyMediaOrRecommendation).isFalse()
+        }
+
+    @Test
+    fun addActiveRecommendation_thenInvalid() =
+        testScope.runTest {
+            val hasActiveMediaOrRecommendation by
+                collectLastValue(underTest.hasActiveMediaOrRecommendation)
+            val hasAnyMediaOrRecommendation by
+                collectLastValue(underTest.hasAnyMediaOrRecommendation)
+            kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, false)
+
+            val mediaRecommendation =
+                SmartspaceMediaData(
+                    targetId = KEY_MEDIA_SMARTSPACE,
+                    isActive = true,
+                    recommendations = MediaTestHelper.getValidRecommendationList(context),
+                )
+
+            mediaFilterRepository.setRecommendation(mediaRecommendation)
+
+            assertThat(hasActiveMediaOrRecommendation).isTrue()
+            assertThat(hasAnyMediaOrRecommendation).isTrue()
+
+            mediaFilterRepository.setRecommendation(
+                mediaRecommendation.copy(recommendations = listOf())
+            )
+
+            assertThat(hasActiveMediaOrRecommendation).isFalse()
+            assertThat(hasAnyMediaOrRecommendation).isFalse()
+        }
+
+    @Test
+    fun hasAnyMedia_noMediaSet_returnsFalse() =
+        testScope.runTest { assertThat(underTest.hasAnyMedia.value).isFalse() }
+
+    @Test
+    fun hasAnyMediaOrRecommendation_noMediaSet_returnsFalse() =
+        testScope.runTest { assertThat(underTest.hasAnyMediaOrRecommendation.value).isFalse() }
+
+    @Test
+    fun hasActiveMedia_noMediaSet_returnsFalse() =
+        testScope.runTest { assertThat(underTest.hasActiveMedia.value).isFalse() }
+
+    @Test
+    fun hasActiveMediaOrRecommendation_nothingSet_returnsFalse() =
+        testScope.runTest { assertThat(underTest.hasActiveMediaOrRecommendation.value).isFalse() }
+
+    companion object {
+        private const val KEY = "KEY"
+        private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID"
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapperTest.kt
index c2ce392..f1cd0c8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapperTest.kt
@@ -185,7 +185,7 @@
             setOf(QSTileState.UserAction.CLICK),
             label,
             null,
-            QSTileState.SideViewIcon.None,
+            QSTileState.SideViewIcon.Chevron,
             QSTileState.EnabledState.ENABLED,
             Switch::class.qualifiedName
         )
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegateTest.kt
index f24723a..97a10e6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegateTest.kt
@@ -33,7 +33,6 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.Mockito.mock
 import org.mockito.Mockito.verify
 
 /** Test [DataSaverDialogDelegate]. */
@@ -69,7 +68,7 @@
     fun delegateSetsDialogTitleCorrectly() {
         val expectedResId = R.string.data_saver_enable_title
 
-        dataSaverDialogDelegate.onCreate(sysuiDialog, null)
+        dataSaverDialogDelegate.beforeCreate(sysuiDialog, null)
 
         verify(sysuiDialog).setTitle(eq(expectedResId))
     }
@@ -78,7 +77,7 @@
     fun delegateSetsDialogMessageCorrectly() {
         val expectedResId = R.string.data_saver_description
 
-        dataSaverDialogDelegate.onCreate(sysuiDialog, null)
+        dataSaverDialogDelegate.beforeCreate(sysuiDialog, null)
 
         verify(sysuiDialog).setMessage(expectedResId)
     }
@@ -87,7 +86,7 @@
     fun delegateSetsDialogPositiveButtonCorrectly() {
         val expectedResId = R.string.data_saver_enable_button
 
-        dataSaverDialogDelegate.onCreate(sysuiDialog, null)
+        dataSaverDialogDelegate.beforeCreate(sysuiDialog, null)
 
         verify(sysuiDialog).setPositiveButton(eq(expectedResId), any())
     }
@@ -96,7 +95,7 @@
     fun delegateSetsDialogCancelButtonCorrectly() {
         val expectedResId = R.string.cancel
 
-        dataSaverDialogDelegate.onCreate(sysuiDialog, null)
+        dataSaverDialogDelegate.beforeCreate(sysuiDialog, null)
 
         verify(sysuiDialog).setNeutralButton(eq(expectedResId), eq(null))
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileDataInteractorTest.kt
new file mode 100644
index 0000000..8651300
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileDataInteractorTest.kt
@@ -0,0 +1,93 @@
+/*
+ * 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.qs.tiles.impl.work.domain.interactor
+
+import android.os.UserHandle
+import android.platform.test.annotations.EnabledOnRavenwood
+import android.testing.LeakCheck
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.qs.tiles.impl.work.domain.model.WorkModeTileModel
+import com.android.systemui.utils.leaks.FakeManagedProfileController
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@EnabledOnRavenwood
+@RunWith(AndroidJUnit4::class)
+class WorkModeTileDataInteractorTest : SysuiTestCase() {
+    private val controller = FakeManagedProfileController(LeakCheck())
+    private val underTest: WorkModeTileDataInteractor = WorkModeTileDataInteractor(controller)
+
+    @Test
+    fun availability_matchesControllerHasActiveProfiles() = runTest {
+        val availability by collectLastValue(underTest.availability(TEST_USER))
+
+        assertThat(availability).isFalse()
+
+        controller.setHasActiveProfile(true)
+        assertThat(availability).isTrue()
+
+        controller.setHasActiveProfile(false)
+        assertThat(availability).isFalse()
+    }
+
+    @Test
+    fun tileData_whenHasActiveProfile_matchesControllerIsEnabled() = runTest {
+        controller.setHasActiveProfile(true)
+        val data by
+            collectLastValue(
+                underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest))
+            )
+
+        assertThat(data).isInstanceOf(WorkModeTileModel.HasActiveProfile::class.java)
+        assertThat((data as WorkModeTileModel.HasActiveProfile).isEnabled).isFalse()
+
+        controller.isWorkModeEnabled = true
+        assertThat(data).isInstanceOf(WorkModeTileModel.HasActiveProfile::class.java)
+        assertThat((data as WorkModeTileModel.HasActiveProfile).isEnabled).isTrue()
+
+        controller.isWorkModeEnabled = false
+        assertThat(data).isInstanceOf(WorkModeTileModel.HasActiveProfile::class.java)
+        assertThat((data as WorkModeTileModel.HasActiveProfile).isEnabled).isFalse()
+    }
+
+    @Test
+    fun tileData_matchesControllerHasActiveProfile() = runTest {
+        val data by
+            collectLastValue(
+                underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest))
+            )
+        assertThat(data).isInstanceOf(WorkModeTileModel.NoActiveProfile::class.java)
+
+        controller.setHasActiveProfile(true)
+        assertThat(data).isInstanceOf(WorkModeTileModel.HasActiveProfile::class.java)
+
+        controller.setHasActiveProfile(false)
+        assertThat(data).isInstanceOf(WorkModeTileModel.NoActiveProfile::class.java)
+    }
+
+    private companion object {
+        val TEST_USER = UserHandle.of(1)!!
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileUserActionInteractorTest.kt
new file mode 100644
index 0000000..8a63e2c
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileUserActionInteractorTest.kt
@@ -0,0 +1,115 @@
+/*
+ * 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.qs.tiles.impl.work.domain.interactor
+
+import android.platform.test.annotations.EnabledOnRavenwood
+import android.provider.Settings
+import android.testing.LeakCheck
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject
+import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx
+import com.android.systemui.qs.tiles.impl.work.domain.model.WorkModeTileModel
+import com.android.systemui.utils.leaks.FakeManagedProfileController
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@EnabledOnRavenwood
+@RunWith(AndroidJUnit4::class)
+class WorkModeTileUserActionInteractorTest : SysuiTestCase() {
+
+    private val inputHandler = FakeQSTileIntentUserInputHandler()
+    private val profileController = FakeManagedProfileController(LeakCheck())
+
+    private val underTest =
+        WorkModeTileUserActionInteractor(
+            profileController,
+            inputHandler,
+        )
+
+    @Test
+    fun handleClickWhenEnabled() = runTest {
+        val wasEnabled = true
+        profileController.isWorkModeEnabled = wasEnabled
+
+        underTest.handleInput(
+            QSTileInputTestKtx.click(WorkModeTileModel.HasActiveProfile(wasEnabled))
+        )
+
+        assertThat(profileController.isWorkModeEnabled).isEqualTo(!wasEnabled)
+    }
+
+    @Test
+    fun handleClickWhenDisabled() = runTest {
+        val wasEnabled = false
+        profileController.isWorkModeEnabled = wasEnabled
+
+        underTest.handleInput(
+            QSTileInputTestKtx.click(WorkModeTileModel.HasActiveProfile(wasEnabled))
+        )
+
+        assertThat(profileController.isWorkModeEnabled).isEqualTo(!wasEnabled)
+    }
+
+    @Test
+    fun handleClickWhenUnavailable() = runTest {
+        val wasEnabled = false
+        profileController.isWorkModeEnabled = wasEnabled
+
+        underTest.handleInput(QSTileInputTestKtx.click(WorkModeTileModel.NoActiveProfile))
+
+        assertThat(profileController.isWorkModeEnabled).isEqualTo(wasEnabled)
+    }
+
+    @Test
+    fun handleLongClickWhenDisabled() = runTest {
+        val enabled = false
+
+        underTest.handleInput(
+            QSTileInputTestKtx.longClick(WorkModeTileModel.HasActiveProfile(enabled))
+        )
+
+        QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput {
+            assertThat(it.intent.action).isEqualTo(Settings.ACTION_MANAGED_PROFILE_SETTINGS)
+        }
+    }
+
+    @Test
+    fun handleLongClickWhenEnabled() = runTest {
+        val enabled = true
+
+        underTest.handleInput(
+            QSTileInputTestKtx.longClick(WorkModeTileModel.HasActiveProfile(enabled))
+        )
+
+        QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput {
+            assertThat(it.intent.action).isEqualTo(Settings.ACTION_MANAGED_PROFILE_SETTINGS)
+        }
+    }
+
+    @Test
+    fun handleLongClickWhenUnavailable() = runTest {
+        underTest.handleInput(QSTileInputTestKtx.longClick(WorkModeTileModel.NoActiveProfile))
+
+        QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledNoInputs()
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
index 3c0ab24..27c4ec1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
@@ -27,9 +27,17 @@
 import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
 import com.android.systemui.qs.QSImpl
 import com.android.systemui.qs.dagger.QSComponent
 import com.android.systemui.qs.dagger.QSSceneComponent
+import com.android.systemui.shade.data.repository.fakeShadeRepository
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.capture
@@ -41,8 +49,6 @@
 import java.util.Locale
 import javax.inject.Provider
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.StandardTestDispatcher
-import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
@@ -57,8 +63,9 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 class QSSceneAdapterImplTest : SysuiTestCase() {
 
-    private val testDispatcher = StandardTestDispatcher()
-    private val testScope = TestScope(testDispatcher)
+    private val kosmos = Kosmos().apply { testCase = this@QSSceneAdapterImplTest }
+    private val testDispatcher = kosmos.testDispatcher
+    private val testScope = kosmos.testScope
 
     private val qsImplProvider =
         object : Provider<QSImpl> {
@@ -107,10 +114,15 @@
             }
         }
 
+    private val shadeInteractor = kosmos.shadeInteractor
+    private val dumpManager = mock<DumpManager>()
+
     private val underTest =
         QSSceneAdapterImpl(
             qsSceneComponentFactory,
             qsImplProvider,
+            shadeInteractor,
+            dumpManager,
             testDispatcher,
             testScope.backgroundScope,
             configurationInteractor,
@@ -158,12 +170,6 @@
                     )
                 verify(this).setListening(false)
                 verify(this).setExpanded(false)
-                verify(this)
-                    .setTransitionToFullShadeProgress(
-                        /* isTransitioningToFullShade= */ false,
-                        /* qsTransitionFraction= */ 1f,
-                        /* qsSquishinessFraction = */ 1f,
-                    )
             }
         }
 
@@ -187,13 +193,7 @@
                         /* squishinessFraction= */ 1f,
                     )
                 verify(this).setListening(true)
-                verify(this).setExpanded(true)
-                verify(this)
-                    .setTransitionToFullShadeProgress(
-                        /* isTransitioningToFullShade= */ false,
-                        /* qsTransitionFraction= */ 1f,
-                        /* qsSquishinessFraction = */ 1f,
-                    )
+                verify(this).setExpanded(false)
             }
         }
 
@@ -218,12 +218,6 @@
                     )
                 verify(this).setListening(true)
                 verify(this).setExpanded(true)
-                verify(this)
-                    .setTransitionToFullShadeProgress(
-                        /* isTransitioningToFullShade= */ false,
-                        /* qsTransitionFraction= */ 1f,
-                        /* qsSquishinessFraction = */ 1f,
-                    )
             }
         }
 
@@ -249,12 +243,6 @@
                     )
                 verify(this).setListening(true)
                 verify(this).setExpanded(true)
-                verify(this)
-                    .setTransitionToFullShadeProgress(
-                        /* isTransitioningToFullShade= */ false,
-                        /* qsTransitionFraction= */ 1f,
-                        /* qsSquishinessFraction = */ 1f,
-                    )
             }
         }
 
@@ -268,7 +256,7 @@
             runCurrent()
             clearInvocations(qsImpl!!)
 
-            underTest.setState(QSSceneAdapter.State.Unsquishing(squishiness))
+            underTest.setState(QSSceneAdapter.State.UnsquishingQQS(squishiness))
             with(qsImpl!!) {
                 verify(this).setQsVisible(true)
                 verify(this)
@@ -279,13 +267,7 @@
                         /* squishinessFraction= */ squishiness,
                     )
                 verify(this).setListening(true)
-                verify(this).setExpanded(true)
-                verify(this)
-                    .setTransitionToFullShadeProgress(
-                        /* isTransitioningToFullShade= */ false,
-                        /* qsTransitionFraction= */ 1f,
-                        /* qsSquishinessFraction = */ squishiness,
-                    )
+                verify(this).setExpanded(false)
             }
         }
 
@@ -497,4 +479,21 @@
 
             verify(qsImpl!!).applyBottomNavBarToCustomizerPadding(navBarHeight)
         }
+
+    @Test
+    fun dispatchSplitShade() =
+        testScope.runTest {
+            val shadeRepository = kosmos.fakeShadeRepository
+            shadeRepository.setShadeMode(ShadeMode.Single)
+            val qsImpl by collectLastValue(underTest.qsImpl)
+
+            underTest.inflate(context)
+            runCurrent()
+
+            verify(qsImpl!!).setInSplitShade(false)
+
+            shadeRepository.setShadeMode(ShadeMode.Split)
+            runCurrent()
+            verify(qsImpl!!).setInSplitShade(true)
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterTest.kt
index e281383..ebd65fd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterTest.kt
@@ -49,9 +49,16 @@
     }
 
     @Test
-    fun unsquishing_expansionSameAsQQS() {
+    fun unsquishingQQS_expansionSameAsQQS() {
         val squishiness = 0.6f
-        assertThat(QSSceneAdapter.State.Unsquishing(squishiness).expansion)
+        assertThat(QSSceneAdapter.State.UnsquishingQQS(squishiness).expansion)
             .isEqualTo(QSSceneAdapter.State.QQS.expansion)
     }
+
+    @Test
+    fun unsquishingQS_expansionSameAsQS() {
+        val squishiness = 0.6f
+        assertThat(QSSceneAdapter.State.UnsquishingQS(squishiness).expansion)
+            .isEqualTo(QSSceneAdapter.State.QS.expansion)
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
index 1c54961..d1c4ec3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
@@ -95,7 +95,7 @@
             scope = testScope.backgroundScope,
         )
 
-    private val qsFlexiglassAdapter = FakeQSSceneAdapter({ mock() })
+    private val qsSceneAdapter = FakeQSSceneAdapter({ mock() })
 
     private lateinit var shadeHeaderViewModel: ShadeHeaderViewModel
 
@@ -122,7 +122,7 @@
                 applicationScope = testScope.backgroundScope,
                 deviceEntryInteractor = deviceEntryInteractor,
                 shadeHeaderViewModel = shadeHeaderViewModel,
-                qsSceneAdapter = qsFlexiglassAdapter,
+                qsSceneAdapter = qsSceneAdapter,
                 notifications = kosmos.notificationsPlaceholderViewModel,
                 mediaDataManager = mediaDataManager,
                 shadeInteractor = kosmos.shadeInteractor,
@@ -279,6 +279,20 @@
         }
 
     @Test
+    fun upTransitionSceneKey_customizing_noTransition() =
+            testScope.runTest {
+                val destinationScenes by collectLastValue(underTest.destinationScenes)
+
+                qsSceneAdapter.setCustomizing(true)
+                assertThat(
+                        destinationScenes!!
+                                .keys
+                                .filterIsInstance<Swipe>()
+                                .filter { it.direction == SwipeDirection.Up }
+                ).isEmpty()
+            }
+
+    @Test
     fun shadeMode() =
         testScope.runTest {
             val shadeMode by collectLastValue(underTest.shadeMode)
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 2689fc1..94539a3 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
@@ -22,7 +22,6 @@
 import androidx.test.filters.SmallTest
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.common.shared.model.NotificationContainerBounds
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.flags.Flags
 import com.android.systemui.flags.fakeFeatureFlagsClassic
@@ -31,6 +30,7 @@
 import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.shared.model.fakeSceneDataSource
+import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationStackAppearanceViewModel
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel
 import com.android.systemui.testKosmos
@@ -64,7 +64,7 @@
     @Test
     fun updateBounds() =
         testScope.runTest {
-            val bounds by collectLastValue(appearanceViewModel.stackBounds)
+            val clipping by collectLastValue(appearanceViewModel.stackClipping)
 
             val top = 200f
             val left = 0f
@@ -76,15 +76,8 @@
                 right = right,
                 bottom = bottom
             )
-            assertThat(bounds)
-                .isEqualTo(
-                    NotificationContainerBounds(
-                        left = left,
-                        top = top,
-                        right = right,
-                        bottom = bottom
-                    )
-                )
+            assertThat(clipping?.bounds)
+                .isEqualTo(StackBounds(left = left, top = top, right = right, bottom = bottom))
         }
 
     @Test
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 ffe6e6d..e3fa89c 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
@@ -19,10 +19,13 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.common.shared.model.NotificationContainerBounds
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.shade.data.repository.shadeRepository
+import com.android.systemui.shade.shared.model.ShadeMode
+import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds
+import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding
+import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
@@ -30,10 +33,9 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
-@android.platform.test.annotations.EnabledOnRavenwood
 class NotificationStackAppearanceInteractorTest : SysuiTestCase() {
 
-    private val kosmos = Kosmos()
+    private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
     private val underTest = kosmos.notificationStackAppearanceInteractor
 
@@ -43,29 +45,39 @@
             val stackBounds by collectLastValue(underTest.stackBounds)
 
             val bounds1 =
-                NotificationContainerBounds(
+                StackBounds(
                     top = 100f,
                     bottom = 200f,
-                    isAnimated = true,
                 )
             underTest.setStackBounds(bounds1)
             assertThat(stackBounds).isEqualTo(bounds1)
 
             val bounds2 =
-                NotificationContainerBounds(
+                StackBounds(
                     top = 200f,
                     bottom = 300f,
-                    isAnimated = false,
                 )
             underTest.setStackBounds(bounds2)
             assertThat(stackBounds).isEqualTo(bounds2)
         }
 
+    @Test
+    fun stackRounding() =
+        testScope.runTest {
+            val stackRounding by collectLastValue(underTest.stackRounding)
+
+            kosmos.shadeRepository.setShadeMode(ShadeMode.Single)
+            assertThat(stackRounding).isEqualTo(StackRounding(roundTop = true, roundBottom = false))
+
+            kosmos.shadeRepository.setShadeMode(ShadeMode.Split)
+            assertThat(stackRounding).isEqualTo(StackRounding(roundTop = true, roundBottom = true))
+        }
+
     @Test(expected = IllegalStateException::class)
     fun setStackBounds_withImproperBounds_throwsException() =
         testScope.runTest {
             underTest.setStackBounds(
-                NotificationContainerBounds(
+                StackBounds(
                     top = 100f,
                     bottom = 99f,
                 )
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt
index 693de55..2ccc8b4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.common.shared.model.NotificationContainerBounds
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor
+import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
@@ -36,9 +37,9 @@
     fun onBoundsChanged_setsNotificationContainerBounds() {
         underTest.onBoundsChanged(left = 5f, top = 5f, right = 5f, bottom = 5f)
         assertThat(kosmos.keyguardInteractor.notificationContainerBounds.value)
-            .isEqualTo(NotificationContainerBounds(left = 5f, top = 5f, right = 5f, bottom = 5f))
+            .isEqualTo(NotificationContainerBounds(top = 5f, bottom = 5f))
         assertThat(kosmos.notificationStackAppearanceInteractor.stackBounds.value)
-            .isEqualTo(NotificationContainerBounds(left = 5f, top = 5f, right = 5f, bottom = 5f))
+            .isEqualTo(StackBounds(left = 5f, top = 5f, right = 5f, bottom = 5f))
     }
     @Test
     fun onContentTopChanged_setsContentTop() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt
index be63301..30564bb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt
@@ -60,7 +60,7 @@
     private val mGlobalSettings = FakeGlobalSettings()
     private val mSystemClock = FakeSystemClock()
     private val mExecutor = FakeExecutor(mSystemClock)
-    private var testableHeadsUpManager: BaseHeadsUpManager? = null
+    private lateinit var testableHeadsUpManager: BaseHeadsUpManager
 
     @Before
     fun setUp() {
@@ -88,20 +88,15 @@
     }
 
     private fun createHeadsUpEntry(id: Int): BaseHeadsUpManager.HeadsUpEntry {
-        val entry = testableHeadsUpManager!!.createHeadsUpEntry()
-
-        entry.setEntry(
+        return testableHeadsUpManager.createHeadsUpEntry(
             NotificationEntryBuilder()
                 .setSbn(HeadsUpManagerTestUtil.createSbn(id, Notification.Builder(mContext, "")))
                 .build()
         )
-        return entry
     }
 
     private fun createFsiHeadsUpEntry(id: Int): BaseHeadsUpManager.HeadsUpEntry {
-        val entry = testableHeadsUpManager!!.createHeadsUpEntry()
-        entry.setEntry(createFullScreenIntentEntry(id, mContext))
-        return entry
+        return testableHeadsUpManager.createHeadsUpEntry(createFullScreenIntentEntry(id, mContext))
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java
index ed0d272..3dc4495 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java
@@ -38,7 +38,6 @@
 import android.app.Notification;
 import android.app.PendingIntent;
 import android.app.Person;
-import android.content.Intent;
 import android.testing.TestableLooper;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -498,16 +497,16 @@
     public void testAlertEntryCompareTo_ongoingCallLessThanActiveRemoteInput() {
         final BaseHeadsUpManager hum = createHeadsUpManager();
 
-        final BaseHeadsUpManager.HeadsUpEntry ongoingCall = hum.new HeadsUpEntry();
-        ongoingCall.setEntry(new NotificationEntryBuilder()
-                .setSbn(HeadsUpManagerTestUtil.createSbn(/* id = */ 0,
-                        new Notification.Builder(mContext, "")
-                                .setCategory(Notification.CATEGORY_CALL)
-                                .setOngoing(true)))
-                .build());
+        final BaseHeadsUpManager.HeadsUpEntry ongoingCall = hum.new HeadsUpEntry(
+                new NotificationEntryBuilder()
+                        .setSbn(HeadsUpManagerTestUtil.createSbn(/* id = */ 0,
+                                new Notification.Builder(mContext, "")
+                                        .setCategory(Notification.CATEGORY_CALL)
+                                        .setOngoing(true)))
+                        .build());
 
-        final BaseHeadsUpManager.HeadsUpEntry activeRemoteInput = hum.new HeadsUpEntry();
-        activeRemoteInput.setEntry(HeadsUpManagerTestUtil.createEntry(/* id = */ 1, mContext));
+        final BaseHeadsUpManager.HeadsUpEntry activeRemoteInput = hum.new HeadsUpEntry(
+                HeadsUpManagerTestUtil.createEntry(/* id = */ 1, mContext));
         activeRemoteInput.mRemoteInputActive = true;
 
         assertThat(ongoingCall.compareTo(activeRemoteInput)).isLessThan(0);
@@ -518,18 +517,18 @@
     public void testAlertEntryCompareTo_incomingCallLessThanActiveRemoteInput() {
         final BaseHeadsUpManager hum = createHeadsUpManager();
 
-        final BaseHeadsUpManager.HeadsUpEntry incomingCall = hum.new HeadsUpEntry();
         final Person person = new Person.Builder().setName("person").build();
         final PendingIntent intent = mock(PendingIntent.class);
-        incomingCall.setEntry(new NotificationEntryBuilder()
-                .setSbn(HeadsUpManagerTestUtil.createSbn(/* id = */ 0,
-                        new Notification.Builder(mContext, "")
-                                .setStyle(Notification.CallStyle
-                                        .forIncomingCall(person, intent, intent))))
-                .build());
+        final BaseHeadsUpManager.HeadsUpEntry incomingCall = hum.new HeadsUpEntry(
+                new NotificationEntryBuilder()
+                        .setSbn(HeadsUpManagerTestUtil.createSbn(/* id = */ 0,
+                                new Notification.Builder(mContext, "")
+                                        .setStyle(Notification.CallStyle
+                                                .forIncomingCall(person, intent, intent))))
+                        .build());
 
-        final BaseHeadsUpManager.HeadsUpEntry activeRemoteInput = hum.new HeadsUpEntry();
-        activeRemoteInput.setEntry(HeadsUpManagerTestUtil.createEntry(/* id = */ 1, mContext));
+        final BaseHeadsUpManager.HeadsUpEntry activeRemoteInput = hum.new HeadsUpEntry(
+                HeadsUpManagerTestUtil.createEntry(/* id = */ 1, mContext));
         activeRemoteInput.mRemoteInputActive = true;
 
         assertThat(incomingCall.compareTo(activeRemoteInput)).isLessThan(0);
@@ -541,8 +540,7 @@
         final BaseHeadsUpManager hum = createHeadsUpManager();
 
         // Needs full screen intent in order to be pinned
-        final BaseHeadsUpManager.HeadsUpEntry entryToPin = hum.new HeadsUpEntry();
-        entryToPin.setEntry(
+        final BaseHeadsUpManager.HeadsUpEntry entryToPin = hum.new HeadsUpEntry(
                 HeadsUpManagerTestUtil.createFullScreenIntentEntry(/* id = */ 0, mContext));
 
         // Note: the standard way to show a notification would be calling showNotification rather
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/TestableHeadsUpManager.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/TestableHeadsUpManager.java
index d8f77f0..3c9dc63 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/TestableHeadsUpManager.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/TestableHeadsUpManager.java
@@ -54,9 +54,10 @@
         mStickyForSomeTimeAutoDismissTime = BaseHeadsUpManagerTest.TEST_STICKY_AUTO_DISMISS_TIME;
     }
 
+    @NonNull
     @Override
-    protected HeadsUpEntry createHeadsUpEntry() {
-        mLastCreatedEntry = spy(super.createHeadsUpEntry());
+    protected HeadsUpEntry createHeadsUpEntry(NotificationEntry entry) {
+        mLastCreatedEntry = spy(super.createHeadsUpEntry(entry));
         return mLastCreatedEntry;
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/DisposableHandlesTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/DisposableHandlesTest.kt
new file mode 100644
index 0000000..a5ad3c3
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/DisposableHandlesTest.kt
@@ -0,0 +1,68 @@
+/*
+ * 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.kotlin
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.DisposableHandle
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DisposableHandlesTest : SysuiTestCase() {
+    @Test
+    fun disposeWorksOnce() {
+        var handleDisposeCount = 0
+        val underTest = DisposableHandles()
+
+        // Add a handle
+        underTest += DisposableHandle { handleDisposeCount++ }
+
+        // dispose() calls dispose() on children
+        underTest.dispose()
+        assertThat(handleDisposeCount).isEqualTo(1)
+
+        // Once disposed, children are not disposed again
+        underTest.dispose()
+        assertThat(handleDisposeCount).isEqualTo(1)
+    }
+
+    @Test
+    fun replaceCallsDispose() {
+        var handleDisposeCount1 = 0
+        var handleDisposeCount2 = 0
+        val underTest = DisposableHandles()
+        val handle1 = DisposableHandle { handleDisposeCount1++ }
+        val handle2 = DisposableHandle { handleDisposeCount2++ }
+
+        // First add handle1
+        underTest += handle1
+
+        // replace() calls dispose() on existing children
+        underTest.replaceAll(handle2)
+        assertThat(handleDisposeCount1).isEqualTo(1)
+        assertThat(handleDisposeCount2).isEqualTo(0)
+
+        // Once disposed, replaced children are not disposed again
+        underTest.dispose()
+        assertThat(handleDisposeCount1).isEqualTo(1)
+        assertThat(handleDisposeCount2).isEqualTo(1)
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt
index 3d93654..5358a6d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt
@@ -200,6 +200,15 @@
         }
     }
 
+    @Test
+    fun alarmStream_isNotMutable() {
+        with(kosmos) {
+            val isMutable = underTest.isMutable(AudioStream(AudioManager.STREAM_ALARM))
+
+            assertThat(isMutable).isFalse()
+        }
+    }
+
     private companion object {
         val audioStream = AudioStream(AudioManager.STREAM_SYSTEM)
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractorTest.kt
new file mode 100644
index 0000000..b5c5809
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractorTest.kt
@@ -0,0 +1,93 @@
+/*
+ * 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.volume.panel.component.mediaoutput.domain.interactor
+
+import android.os.Handler
+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.coroutines.collectLastValue
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.volume.localMediaController
+import com.android.systemui.volume.mediaControllerRepository
+import com.android.systemui.volume.mediaOutputInteractor
+import com.android.systemui.volume.remoteMediaController
+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
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class MediaDeviceSessionInteractorTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+
+    private lateinit var underTest: MediaDeviceSessionInteractor
+
+    @Before
+    fun setup() {
+        with(kosmos) {
+            mediaControllerRepository.setActiveSessions(
+                listOf(localMediaController, remoteMediaController)
+            )
+
+            underTest =
+                MediaDeviceSessionInteractor(
+                    testScope.testScheduler,
+                    Handler(TestableLooper.get(kosmos.testCase).looper),
+                    mediaControllerRepository,
+                )
+        }
+    }
+
+    @Test
+    fun playbackInfo_returnsPlaybackInfo() {
+        with(kosmos) {
+            testScope.runTest {
+                val session by collectLastValue(mediaOutputInteractor.defaultActiveMediaSession)
+                runCurrent()
+                val info by collectLastValue(underTest.playbackInfo(session!!))
+                runCurrent()
+
+                assertThat(info).isEqualTo(localMediaController.playbackInfo)
+            }
+        }
+    }
+
+    @Test
+    fun playbackState_returnsPlaybackState() {
+        with(kosmos) {
+            testScope.runTest {
+                val session by collectLastValue(mediaOutputInteractor.defaultActiveMediaSession)
+                runCurrent()
+                val state by collectLastValue(underTest.playbackState(session!!))
+                runCurrent()
+
+                assertThat(state).isEqualTo(localMediaController.playbackState)
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelTest.kt
index dcf635e..6f7f20b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelTest.kt
@@ -29,9 +29,10 @@
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
+import com.android.systemui.volume.localMediaController
 import com.android.systemui.volume.localMediaRepository
-import com.android.systemui.volume.mediaController
 import com.android.systemui.volume.mediaControllerRepository
+import com.android.systemui.volume.mediaDeviceSessionInteractor
 import com.android.systemui.volume.mediaOutputActionsInteractor
 import com.android.systemui.volume.mediaOutputInteractor
 import com.android.systemui.volume.panel.volumePanelViewModel
@@ -63,6 +64,7 @@
                     testScope.backgroundScope,
                     volumePanelViewModel,
                     mediaOutputActionsInteractor,
+                    mediaDeviceSessionInteractor,
                     mediaOutputInteractor,
                 )
 
@@ -74,11 +76,11 @@
                 )
             }
 
-            whenever(mediaController.packageName).thenReturn("test.pkg")
-            whenever(mediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {}))
-            whenever(mediaController.playbackState).then { playbackStateBuilder.build() }
+            whenever(localMediaController.packageName).thenReturn("test.pkg")
+            whenever(localMediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {}))
+            whenever(localMediaController.playbackState).then { playbackStateBuilder.build() }
 
-            mediaControllerRepository.setActiveLocalMediaController(mediaController)
+            mediaControllerRepository.setActiveSessions(listOf(localMediaController))
         }
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt
index 1ed7f5d..2f69942 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt
@@ -32,8 +32,8 @@
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
+import com.android.systemui.volume.localMediaController
 import com.android.systemui.volume.localMediaRepository
-import com.android.systemui.volume.mediaController
 import com.android.systemui.volume.mediaControllerRepository
 import com.android.systemui.volume.panel.component.spatial.spatialAudioComponentInteractor
 import com.google.common.truth.Truth.assertThat
@@ -66,11 +66,11 @@
                 }
             )
 
-            whenever(mediaController.packageName).thenReturn("test.pkg")
-            whenever(mediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {}))
-            whenever(mediaController.playbackState).thenReturn(PlaybackState.Builder().build())
+            whenever(localMediaController.packageName).thenReturn("test.pkg")
+            whenever(localMediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {}))
+            whenever(localMediaController.playbackState).thenReturn(PlaybackState.Builder().build())
 
-            mediaControllerRepository.setActiveLocalMediaController(mediaController)
+            mediaControllerRepository.setActiveSessions(listOf(localMediaController))
 
             underTest = SpatialAudioAvailabilityCriteria(spatialAudioComponentInteractor)
         }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt
index 281b03d..e36ae60 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt
@@ -34,8 +34,8 @@
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
+import com.android.systemui.volume.localMediaController
 import com.android.systemui.volume.localMediaRepository
-import com.android.systemui.volume.mediaController
 import com.android.systemui.volume.mediaControllerRepository
 import com.android.systemui.volume.mediaOutputInteractor
 import com.android.systemui.volume.panel.component.spatial.domain.model.SpatialAudioAvailabilityModel
@@ -70,11 +70,11 @@
                 }
             )
 
-            whenever(mediaController.packageName).thenReturn("test.pkg")
-            whenever(mediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {}))
-            whenever(mediaController.playbackState).thenReturn(PlaybackState.Builder().build())
+            whenever(localMediaController.packageName).thenReturn("test.pkg")
+            whenever(localMediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {}))
+            whenever(localMediaController.playbackState).thenReturn(PlaybackState.Builder().build())
 
-            mediaControllerRepository.setActiveLocalMediaController(mediaController)
+            mediaControllerRepository.setActiveSessions(listOf(localMediaController))
 
             underTest =
                 SpatialAudioComponentInteractor(
diff --git a/packages/SystemUI/res/layout/screenshot_shelf.xml b/packages/SystemUI/res/layout/screenshot_shelf.xml
new file mode 100644
index 0000000..ef1a21f
--- /dev/null
+++ b/packages/SystemUI/res/layout/screenshot_shelf.xml
@@ -0,0 +1,160 @@
+<?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.
+  -->
+<com.android.systemui.screenshot.ui.ScreenshotShelfView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    <ImageView
+        android:id="@+id/actions_container_background"
+        android:visibility="gone"
+        android:layout_height="0dp"
+        android:layout_width="0dp"
+        android:elevation="4dp"
+        android:background="@drawable/action_chip_container_background"
+        android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal"
+        android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="@+id/actions_container"
+        app:layout_constraintEnd_toEndOf="@+id/actions_container"
+        app:layout_constraintBottom_toTopOf="@id/guideline"/>
+    <HorizontalScrollView
+        android:id="@+id/actions_container"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="@dimen/overlay_action_container_margin_horizontal"
+        android:paddingEnd="@dimen/overlay_action_container_padding_end"
+        android:paddingVertical="@dimen/overlay_action_container_padding_vertical"
+        android:elevation="4dp"
+        android:scrollbars="none"
+        app:layout_constraintHorizontal_bias="0"
+        app:layout_constraintWidth_percent="1.0"
+        app:layout_constraintWidth_max="wrap"
+        app:layout_constraintStart_toEndOf="@+id/screenshot_preview_border"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintBottom_toBottomOf="@id/actions_container_background">
+        <LinearLayout
+            android:id="@+id/screenshot_actions"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content">
+            <include layout="@layout/overlay_action_chip"
+                     android:id="@+id/screenshot_share_chip"/>
+            <include layout="@layout/overlay_action_chip"
+                     android:id="@+id/screenshot_edit_chip"/>
+            <include layout="@layout/overlay_action_chip"
+                     android:id="@+id/screenshot_scroll_chip"
+                     android:visibility="gone" />
+        </LinearLayout>
+    </HorizontalScrollView>
+    <View
+        android:id="@+id/screenshot_preview_border"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:layout_marginStart="16dp"
+        android:layout_marginTop="@dimen/overlay_border_width_neg"
+        android:layout_marginEnd="@dimen/overlay_border_width_neg"
+        android:layout_marginBottom="14dp"
+        android:elevation="8dp"
+        android:background="@drawable/overlay_border"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="@id/screenshot_preview"
+        app:layout_constraintEnd_toEndOf="@id/screenshot_preview"
+        app:layout_constraintBottom_toBottomOf="parent"/>
+    <ImageView
+        android:id="@+id/screenshot_preview"
+        android:layout_width="@dimen/overlay_x_scale"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/overlay_border_width"
+        android:layout_marginBottom="@dimen/overlay_border_width"
+        android:layout_gravity="center"
+        android:elevation="8dp"
+        android:contentDescription="@string/screenshot_edit_description"
+        android:scaleType="fitEnd"
+        android:background="@drawable/overlay_preview_background"
+        android:adjustViewBounds="true"
+        android:clickable="true"
+        app:layout_constraintStart_toStartOf="@id/screenshot_preview_border"
+        app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border"/>
+    <ImageView
+        android:id="@+id/screenshot_badge"
+        android:layout_width="56dp"
+        android:layout_height="56dp"
+        android:visibility="gone"
+        android:elevation="9dp"
+        app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border"
+        app:layout_constraintEnd_toEndOf="@id/screenshot_preview_border"/>
+    <FrameLayout
+        android:id="@+id/screenshot_dismiss_button"
+        android:layout_width="@dimen/overlay_dismiss_button_tappable_size"
+        android:layout_height="@dimen/overlay_dismiss_button_tappable_size"
+        android:elevation="11dp"
+        android:visibility="gone"
+        app:layout_constraintStart_toEndOf="@id/screenshot_preview"
+        app:layout_constraintEnd_toEndOf="@id/screenshot_preview"
+        app:layout_constraintTop_toTopOf="@id/screenshot_preview"
+        app:layout_constraintBottom_toTopOf="@id/screenshot_preview"
+        android:contentDescription="@string/screenshot_dismiss_description">
+        <ImageView
+            android:id="@+id/screenshot_dismiss_image"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:layout_margin="@dimen/overlay_dismiss_button_margin"
+            android:background="@drawable/circular_background"
+            android:backgroundTint="?androidprv:attr/materialColorPrimary"
+            android:tint="?androidprv:attr/materialColorOnPrimary"
+            android:padding="4dp"
+            android:src="@drawable/ic_close"/>
+    </FrameLayout>
+    <ImageView
+        android:id="@+id/screenshot_scrollable_preview"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:scaleType="matrix"
+        android:visibility="gone"
+        app:layout_constraintStart_toStartOf="@id/screenshot_preview"
+        app:layout_constraintTop_toTopOf="@id/screenshot_preview"
+        android:elevation="7dp"/>
+
+    <androidx.constraintlayout.widget.Guideline
+        android:id="@+id/guideline"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        app:layout_constraintGuide_end="0dp" />
+
+    <FrameLayout
+        android:id="@+id/screenshot_message_container"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginHorizontal="@dimen/overlay_action_container_margin_horizontal"
+        android:layout_marginTop="4dp"
+        android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom"
+        android:paddingHorizontal="@dimen/overlay_action_container_padding_end"
+        android:paddingVertical="@dimen/overlay_action_container_padding_vertical"
+        android:elevation="4dp"
+        android:background="@drawable/action_chip_container_background"
+        android:visibility="gone"
+        app:layout_constraintTop_toBottomOf="@id/guideline"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintWidth_max="450dp"
+        app:layout_constraintHorizontal_bias="0">
+        <include layout="@layout/screenshot_work_profile_first_run" />
+        <include layout="@layout/screenshot_detection_notice" />
+    </FrameLayout>
+</com.android.systemui.screenshot.ui.ScreenshotShelfView>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 77d1484..3029888 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -235,6 +235,8 @@
     <string name="screenshot_edit_label">Edit</string>
     <!-- Content description indicating that tapping the element will allow editing the screenshot [CHAR LIMIT=NONE] -->
     <string name="screenshot_edit_description">Edit screenshot</string>
+    <!-- Label for UI element which allows sharing the screenshot [CHAR LIMIT=30] -->
+    <string name="screenshot_share_label">Share</string>
     <!-- Content description indicating that tapping the element will allow sharing the screenshot [CHAR LIMIT=NONE] -->
     <string name="screenshot_share_description">Share screenshot</string>
     <!-- Label for UI element which allows the user to capture additional off-screen content in a screenshot. [CHAR LIMIT=30] -->
@@ -1994,8 +1996,6 @@
     <string name="group_system_cycle_back">Cycle backward through recent apps</string>
     <!-- User visible title for the keyboard shortcut that accesses list of all apps and search. [CHAR LIMIT=70] -->
     <string name="group_system_access_all_apps_search">Open apps list</string>
-    <!-- User visible title for the keyboard shortcut that hides and (re)showes taskbar. [CHAR LIMIT=70] -->
-    <string name="group_system_hide_reshow_taskbar">Show taskbar</string>
     <!-- User visible title for the keyboard shortcut that accesses [system] settings. [CHAR LIMIT=70] -->
     <string name="group_system_access_system_settings">Open settings</string>
     <!-- User visible title for the keyboard shortcut that accesses Assistant app. [CHAR LIMIT=70] -->
@@ -2013,6 +2013,10 @@
     <string name="system_multitasking_lhs">Enter split screen with current app to LHS</string>
     <!-- User visible title for the keyboard shortcut that switches from split screen to full screen [CHAR LIMIT=70] -->
     <string name="system_multitasking_full_screen">Switch from split screen to full screen</string>
+    <!-- User visible title for the keyboard shortcut that switches to app on right or below while using split screen [CHAR LIMIT=70] -->
+    <string name="system_multitasking_splitscreen_focus_rhs">Switch to app on right or below while using split screen</string>
+    <!-- User visible title for the keyboard shortcut that switches to app on left or above while using split screen [CHAR LIMIT=70] -->
+    <string name="system_multitasking_splitscreen_focus_lhs">Switch to app on left or above while using split screen</string>
     <!-- User visible title for the keyboard shortcut that replaces an app from one to another during split screen [CHAR LIMIT=70] -->
     <string name="system_multitasking_replace">During split screen: replace an app from one to another</string>
 
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index 8a2245d..48271de 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -31,7 +31,6 @@
 import androidx.annotation.VisibleForTesting
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
-import com.android.systemui.Flags.migrateClocksToBlueprint
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.customization.R
 import com.android.systemui.dagger.qualifiers.Background
@@ -39,6 +38,7 @@
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.flags.FeatureFlagsClassic
 import com.android.systemui.flags.Flags.REGION_SAMPLING
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
@@ -328,7 +328,7 @@
         object : KeyguardUpdateMonitorCallback() {
             override fun onKeyguardVisibilityChanged(visible: Boolean) {
                 isKeyguardVisible = visible
-                if (!migrateClocksToBlueprint()) {
+                if (!MigrateClocksToBlueprint.isEnabled) {
                     if (!isKeyguardVisible) {
                         clock?.run {
                             smallClock.animations.doze(if (isDozing) 1f else 0f)
@@ -368,7 +368,7 @@
             }
 
             private fun refreshTime() {
-                if (!migrateClocksToBlueprint()) {
+                if (!MigrateClocksToBlueprint.isEnabled) {
                     return
                 }
 
@@ -427,7 +427,7 @@
             parent.repeatWhenAttached {
                 repeatOnLifecycle(Lifecycle.State.CREATED) {
                     listenForDozing(this)
-                    if (migrateClocksToBlueprint()) {
+                    if (MigrateClocksToBlueprint.isEnabled) {
                         listenForDozeAmountTransition(this)
                         listenForAnyStateToAodTransition(this)
                     } else {
diff --git a/packages/SystemUI/src/com/android/keyguard/ConnectedDisplayKeyguardPresentation.kt b/packages/SystemUI/src/com/android/keyguard/ConnectedDisplayKeyguardPresentation.kt
index 630610d..df77a58 100644
--- a/packages/SystemUI/src/com/android/keyguard/ConnectedDisplayKeyguardPresentation.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ConnectedDisplayKeyguardPresentation.kt
@@ -30,7 +30,7 @@
 import android.widget.FrameLayout
 import android.widget.FrameLayout.LayoutParams
 import com.android.keyguard.dagger.KeyguardStatusViewComponent
-import com.android.systemui.Flags.migrateClocksToBlueprint
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.plugins.clocks.ClockController
 import com.android.systemui.plugins.clocks.ClockFaceController
 import com.android.systemui.res.R
@@ -95,7 +95,7 @@
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
 
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled) {
             onCreateV2()
         } else {
             onCreate()
@@ -132,7 +132,7 @@
     }
 
     override fun onAttachedToWindow() {
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled) {
             clockRegistry.registerClockChangeListener(clockChangedListener)
             clockEventController.registerListeners(clock!!)
 
@@ -141,7 +141,7 @@
     }
 
     override fun onDetachedFromWindow() {
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled) {
             clockEventController.unregisterListeners()
             clockRegistry.unregisterClockChangeListener(clockChangedListener)
         }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
index 28013c6..4a96e9e 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
@@ -3,7 +3,6 @@
 import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_X_CLOCK_DESIGN;
 import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_Y_CLOCK_DESIGN;
 import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_Y_CLOCK_SIZE;
-import static com.android.systemui.Flags.migrateClocksToBlueprint;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -23,6 +22,7 @@
 
 import com.android.app.animation.Interpolators;
 import com.android.keyguard.dagger.KeyguardStatusViewScope;
+import com.android.systemui.keyguard.MigrateClocksToBlueprint;
 import com.android.systemui.log.LogBuffer;
 import com.android.systemui.log.core.LogLevel;
 import com.android.systemui.plugins.clocks.ClockController;
@@ -192,7 +192,7 @@
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled()) {
             mSmallClockFrame = findViewById(R.id.lockscreen_clock_view);
             mLargeClockFrame = findViewById(R.id.lockscreen_clock_view_large);
             mStatusArea = findViewById(R.id.keyguard_status_area);
@@ -266,7 +266,7 @@
     }
 
     void updateClockTargetRegions() {
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled()) {
             return;
         }
         if (mClock != null) {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index e621ffe..5b8eb9d 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -21,7 +21,6 @@
 
 import static com.android.keyguard.KeyguardClockSwitch.LARGE;
 import static com.android.keyguard.KeyguardClockSwitch.SMALL;
-import static com.android.systemui.Flags.migrateClocksToBlueprint;
 import static com.android.systemui.Flags.smartspaceRelocateToBottom;
 import static com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED;
 import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
@@ -45,6 +44,7 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlagsClassic;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
+import com.android.systemui.keyguard.MigrateClocksToBlueprint;
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager;
@@ -202,7 +202,7 @@
         mClockChangedListener = new ClockRegistry.ClockChangeListener() {
             @Override
             public void onCurrentClockChanged() {
-                if (!migrateClocksToBlueprint()) {
+                if (!MigrateClocksToBlueprint.isEnabled()) {
                     setClock(mClockRegistry.createCurrentClock());
                 }
             }
@@ -245,7 +245,7 @@
     protected void onInit() {
         mKeyguardSliceViewController.init();
 
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled()) {
             mSmallClockFrame = mView.findViewById(R.id.lockscreen_clock_view);
             mLargeClockFrame = mView.findViewById(R.id.lockscreen_clock_view_large);
         }
@@ -340,7 +340,7 @@
                 addDateWeatherView();
             }
         }
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled()) {
             setDateWeatherVisibility();
             setWeatherVisibility();
         }
@@ -348,7 +348,7 @@
     }
 
     int getNotificationIconAreaHeight() {
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled()) {
             return 0;
         } else if (NotificationIconContainerRefactor.isEnabled()) {
             return mAodIconContainer != null ? mAodIconContainer.getHeight() : 0;
@@ -391,7 +391,7 @@
     }
 
     private void addDateWeatherView() {
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled()) {
             return;
         }
         mDateWeatherView = (ViewGroup) mSmartspaceController.buildAndConnectDateView(mView);
@@ -407,7 +407,7 @@
     }
 
     private void addWeatherView() {
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled()) {
             return;
         }
         LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
@@ -420,7 +420,7 @@
     }
 
     private void addSmartspaceView() {
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled()) {
             return;
         }
 
@@ -528,7 +528,7 @@
      */
     void updatePosition(int x, float scale, AnimationProperties props, boolean animate) {
         x = getCurrentLayoutDirection() == View.LAYOUT_DIRECTION_RTL ? -x : x;
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled()) {
             PropertyAnimator.setProperty(mSmallClockFrame, AnimatableProperty.TRANSLATION_X,
                     x, props, animate);
             PropertyAnimator.setProperty(mLargeClockFrame, AnimatableProperty.SCALE_X,
@@ -554,7 +554,7 @@
             return 0;
         }
 
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled()) {
             return 0;
         }
 
@@ -589,14 +589,14 @@
     }
 
     boolean isClockTopAligned() {
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled()) {
             return mKeyguardClockInteractor.getClockSize().getValue() == LARGE;
         }
         return mLargeClockFrame.getVisibility() != View.VISIBLE;
     }
 
     private void updateAodIcons() {
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled()) {
             NotificationIconContainer nic = (NotificationIconContainer)
                     mView.findViewById(
                             com.android.systemui.res.R.id.left_aligned_notification_icon_container);
@@ -616,7 +616,7 @@
     }
 
     private void setClock(ClockController clock) {
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled()) {
             return;
         }
         if (clock != null && mLogBuffer != null) {
@@ -630,8 +630,8 @@
 
     @Nullable
     public ClockController getClock() {
-        if (migrateClocksToBlueprint()) {
-            return mKeyguardClockInteractor.getClock();
+        if (MigrateClocksToBlueprint.isEnabled()) {
+            return mKeyguardClockInteractor.getCurrentClock().getValue();
         } else {
             return mClockEventController.getClock();
         }
@@ -642,7 +642,7 @@
     }
 
     private void updateDoubleLineClock() {
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled()) {
             return;
         }
         mCanShowDoubleLineClock = mSecureSettings.getIntForUser(
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
index 7f9ae5e..603a47e 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
@@ -20,7 +20,6 @@
 import static androidx.constraintlayout.widget.ConstraintSet.PARENT_ID;
 
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION;
-import static com.android.systemui.Flags.migrateClocksToBlueprint;
 import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
 
 import android.animation.Animator;
@@ -52,6 +51,7 @@
 import com.android.systemui.Dumpable;
 import com.android.systemui.animation.ViewHierarchyAnimator;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.keyguard.MigrateClocksToBlueprint;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.plugins.clocks.ClockController;
 import com.android.systemui.power.domain.interactor.PowerInteractor;
@@ -223,7 +223,7 @@
         }
 
         mDumpManager.registerDumpable(getInstanceName(), this);
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled()) {
             startCoroutines(EmptyCoroutineContext.INSTANCE);
             mView.setVisibility(View.GONE);
         }
@@ -250,7 +250,7 @@
     @Override
     protected void onViewAttached() {
         mStatusArea = mView.findViewById(R.id.keyguard_status_area);
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled()) {
             return;
         }
 
@@ -261,7 +261,7 @@
 
     @Override
     protected void onViewDetached() {
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled()) {
             return;
         }
 
@@ -485,7 +485,7 @@
             boolean splitShadeEnabled,
             boolean shouldBeCentered,
             boolean animate) {
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled()) {
             mKeyguardInteractor.setClockShouldBeCentered(shouldBeCentered);
         } else {
             mKeyguardClockSwitchController.setSplitShadeCentered(
@@ -503,7 +503,7 @@
         ConstraintSet constraintSet = new ConstraintSet();
         constraintSet.clone(layout);
         int guideline;
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled()) {
             guideline = R.id.split_shade_guideline;
         } else {
             guideline = R.id.qs_edge_guideline;
@@ -548,7 +548,7 @@
                 && clock.getLargeClock().getConfig().getHasCustomPositionUpdatedAnimation();
         // When migrateClocksToBlueprint is on, customized clock animation is conducted in
         // KeyguardClockViewBinder
-        if (customClockAnimation && !migrateClocksToBlueprint()) {
+        if (customClockAnimation && !MigrateClocksToBlueprint.isEnabled()) {
             // Find the clock, so we can exclude it from this transition.
             FrameLayout clockContainerView = mView.findViewById(R.id.lockscreen_clock_view_large);
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java b/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
index f5a6cb3..fd8b6d5 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
@@ -16,7 +16,6 @@
 
 package com.android.keyguard;
 
-import static com.android.systemui.Flags.migrateClocksToBlueprint;
 import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
 import static com.android.systemui.statusbar.StatusBarState.SHADE;
 
@@ -24,6 +23,7 @@
 import android.view.View;
 
 import com.android.app.animation.Interpolators;
+import com.android.systemui.keyguard.MigrateClocksToBlueprint;
 import com.android.systemui.log.LogBuffer;
 import com.android.systemui.log.core.LogLevel;
 import com.android.systemui.statusbar.StatusBarState;
@@ -88,7 +88,7 @@
             boolean keyguardFadingAway,
             boolean goingToFullShade,
             int oldStatusBarState) {
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled()) {
             log("Ignoring KeyguardVisibilityelper, migrateClocksToBlueprint flag on");
             return;
         }
@@ -113,7 +113,7 @@
                 animProps.setDelay(0).setDuration(160);
                 log("goingToFullShade && !keyguardFadingAway");
             }
-            if (migrateClocksToBlueprint()) {
+            if (MigrateClocksToBlueprint.isEnabled()) {
                 log("Using LockscreenToGoneTransition 1");
             } else {
                 PropertyAnimator.setProperty(
@@ -171,7 +171,7 @@
                         animProps,
                         true /* animate */);
             } else if (mScreenOffAnimationController.shouldAnimateInKeyguard()) {
-                if (migrateClocksToBlueprint()) {
+                if (MigrateClocksToBlueprint.isEnabled()) {
                     log("Using GoneToAodTransition");
                     mKeyguardViewVisibilityAnimating = false;
                 } else {
@@ -187,7 +187,7 @@
                 mView.setVisibility(View.VISIBLE);
             }
         } else {
-            if (migrateClocksToBlueprint()) {
+            if (MigrateClocksToBlueprint.isEnabled()) {
                 log("Using LockscreenToGoneTransition 2");
             } else {
                 log("Direct set Visibility to GONE");
diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
index 039a2e5..8f1a5f7 100644
--- a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
@@ -22,8 +22,6 @@
 import static com.android.keyguard.LockIconView.ICON_FINGERPRINT;
 import static com.android.keyguard.LockIconView.ICON_LOCK;
 import static com.android.keyguard.LockIconView.ICON_UNLOCK;
-import static com.android.systemui.Flags.keyguardBottomAreaRefactor;
-import static com.android.systemui.Flags.migrateClocksToBlueprint;
 import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset;
 import static com.android.systemui.flags.Flags.DOZING_MIGRATION_1;
 import static com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED;
@@ -68,6 +66,8 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
+import com.android.systemui.keyguard.KeyguardBottomAreaRefactor;
+import com.android.systemui.keyguard.MigrateClocksToBlueprint;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
 import com.android.systemui.keyguard.shared.model.TransitionStep;
@@ -453,7 +453,7 @@
     private void updateLockIconLocation() {
         final float scaleFactor = mAuthController.getScaleFactor();
         final int scaledPadding = (int) (mDefaultPaddingPx * scaleFactor);
-        if (keyguardBottomAreaRefactor() || migrateClocksToBlueprint()) {
+        if (KeyguardBottomAreaRefactor.isEnabled() || MigrateClocksToBlueprint.isEnabled()) {
             mView.getLockIcon().setPadding(scaledPadding, scaledPadding, scaledPadding,
                     scaledPadding);
         } else {
diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java
index a0f15ef..781f6dd 100644
--- a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java
+++ b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java
@@ -16,8 +16,6 @@
 
 package com.android.keyguard.dagger;
 
-import static com.android.systemui.Flags.migrateClocksToBlueprint;
-
 import android.content.Context;
 import android.content.res.Resources;
 import android.view.LayoutInflater;
@@ -28,6 +26,7 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
+import com.android.systemui.keyguard.MigrateClocksToBlueprint;
 import com.android.systemui.plugins.PluginManager;
 import com.android.systemui.plugins.clocks.ClockMessageBuffers;
 import com.android.systemui.res.R;
@@ -70,7 +69,7 @@
                         layoutInflater,
                         resources,
                         featureFlags.isEnabled(Flags.STEP_CLOCK_ANIMATION),
-                        migrateClocksToBlueprint()),
+                        MigrateClocksToBlueprint.isEnabled()),
                 context.getString(R.string.lockscreen_clock_id_fallback),
                 clockBuffers,
                 /* keepAllLoaded = */ false,
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 d849b3a..94e0854 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
@@ -20,7 +20,6 @@
 import com.android.systemui.flags.FeatureFlagsClassic
 import com.android.systemui.flags.Flags
 import javax.inject.Inject
-import kotlinx.coroutines.flow.MutableStateFlow
 
 /** Provides access to bouncer-related application state. */
 @SysUISingleton
@@ -29,9 +28,6 @@
 constructor(
     private val flags: FeatureFlagsClassic,
 ) {
-    /** The user-facing message to show in the bouncer. */
-    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)
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 d8be1af..aeb564d5 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
@@ -16,13 +16,8 @@
 
 package com.android.systemui.bouncer.domain.interactor
 
-import android.content.Context
 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
 import com.android.systemui.authentication.domain.interactor.AuthenticationResult
-import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
-import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Password
-import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pattern
-import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pin
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Sim
 import com.android.systemui.bouncer.data.repository.BouncerRepository
 import com.android.systemui.classifier.FalsingClassifier
@@ -31,7 +26,6 @@
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
 import com.android.systemui.power.domain.interactor.PowerInteractor
-import com.android.systemui.res.R
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.async
@@ -41,7 +35,6 @@
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.launch
 
 /** Encapsulates business logic and application state accessing use-cases. */
 @SysUISingleton
@@ -49,16 +42,14 @@
 @Inject
 constructor(
     @Application private val applicationScope: CoroutineScope,
-    @Application private val applicationContext: Context,
     private val repository: BouncerRepository,
     private val authenticationInteractor: AuthenticationInteractor,
     private val deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor,
     private val falsingInteractor: FalsingInteractor,
     private val powerInteractor: PowerInteractor,
-    private val simBouncerInteractor: SimBouncerInteractor,
 ) {
-    /** The user-facing message to show in the bouncer when lockout is not active. */
-    val message: StateFlow<String?> = repository.message
+    private val _onIncorrectBouncerInput = MutableSharedFlow<Unit>()
+    val onIncorrectBouncerInput: SharedFlow<Unit> = _onIncorrectBouncerInput
 
     /** Whether the auto confirm feature is enabled for the currently-selected user. */
     val isAutoConfirmEnabled: StateFlow<Boolean> = authenticationInteractor.isAutoConfirmEnabled
@@ -119,25 +110,6 @@
         )
     }
 
-    fun setMessage(message: String?) {
-        repository.message.value = message
-    }
-
-    /**
-     * Resets the user-facing message back to the default according to the current authentication
-     * method.
-     */
-    fun resetMessage() {
-        applicationScope.launch {
-            setMessage(promptMessage(authenticationInteractor.getAuthenticationMethod()))
-        }
-    }
-
-    /** Removes the user-facing message. */
-    fun clearMessage() {
-        setMessage(null)
-    }
-
     /**
      * Attempts to authenticate based on the given user input.
      *
@@ -176,50 +148,17 @@
                 .async { authenticationInteractor.authenticate(input, tryAutoConfirm) }
                 .await()
 
-        if (authenticationInteractor.lockoutEndTimestamp != null) {
-            clearMessage()
-        } else if (
+        if (
             authResult == AuthenticationResult.FAILED ||
                 (authResult == AuthenticationResult.SKIPPED && !tryAutoConfirm)
         ) {
-            showWrongInputMessage()
+            _onIncorrectBouncerInput.emit(Unit)
         }
         return authResult
     }
 
-    /**
-     * Shows the a message notifying the user that their credentials input is wrong.
-     *
-     * Callers should use this instead of [authenticate] when they know ahead of time that an auth
-     * attempt will fail but aren't interested in the other side effects like triggering lockout.
-     * For example, if the user entered a pattern that's too short, the system can show the error
-     * message without having the attempt trigger lockout.
-     */
-    private suspend fun showWrongInputMessage() {
-        setMessage(wrongInputMessage(authenticationInteractor.getAuthenticationMethod()))
-    }
-
     /** Notifies that the input method editor (software keyboard) has been hidden by the user. */
     suspend fun onImeHiddenByUser() {
         _onImeHiddenByUser.emit(Unit)
     }
-
-    private fun promptMessage(authMethod: AuthenticationMethodModel): String {
-        return when (authMethod) {
-            is Sim -> simBouncerInteractor.getDefaultMessage()
-            is Pin -> applicationContext.getString(R.string.keyguard_enter_your_pin)
-            is Password -> applicationContext.getString(R.string.keyguard_enter_your_password)
-            is Pattern -> applicationContext.getString(R.string.keyguard_enter_your_pattern)
-            else -> ""
-        }
-    }
-
-    private fun wrongInputMessage(authMethod: AuthenticationMethodModel): String {
-        return when (authMethod) {
-            is Pin -> applicationContext.getString(R.string.kg_wrong_pin)
-            is Password -> applicationContext.getString(R.string.kg_wrong_password)
-            is Pattern -> applicationContext.getString(R.string.kg_wrong_pattern)
-            else -> ""
-        }
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt
index 7f6fc91..d20c607 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt
@@ -33,15 +33,17 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepository
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFingerprintAuthInteractor
 import com.android.systemui.flags.SystemPropertiesHelper
 import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
-import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.data.repository.TrustRepository
 import com.android.systemui.user.data.repository.UserRepository
-import com.android.systemui.util.kotlin.Quint
+import com.android.systemui.util.kotlin.Sextuple
+import com.android.systemui.util.kotlin.combine
 import javax.inject.Inject
 import kotlin.math.roundToInt
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.combine
@@ -56,6 +58,7 @@
 private const val TAG = "BouncerMessageInteractor"
 
 /** Handles business logic for the primary bouncer message area. */
+@OptIn(ExperimentalCoroutinesApi::class)
 @SysUISingleton
 class BouncerMessageInteractor
 @Inject
@@ -63,23 +66,24 @@
     private val repository: BouncerMessageRepository,
     private val userRepository: UserRepository,
     private val countDownTimerUtil: CountDownTimerUtil,
-    private val updateMonitor: KeyguardUpdateMonitor,
+    updateMonitor: KeyguardUpdateMonitor,
     trustRepository: TrustRepository,
     biometricSettingsRepository: BiometricSettingsRepository,
     private val systemPropertiesHelper: SystemPropertiesHelper,
     primaryBouncerInteractor: PrimaryBouncerInteractor,
     @Application private val applicationScope: CoroutineScope,
     private val facePropertyRepository: FacePropertyRepository,
-    deviceEntryFingerprintAuthRepository: DeviceEntryFingerprintAuthRepository,
+    private val deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor,
     faceAuthRepository: DeviceEntryFaceAuthRepository,
     private val securityModel: KeyguardSecurityModel,
 ) {
 
-    private val isFingerprintAuthCurrentlyAllowed =
-        deviceEntryFingerprintAuthRepository.isLockedOut
-            .isFalse()
-            .and(biometricSettingsRepository.isFingerprintAuthCurrentlyAllowed)
-            .stateIn(applicationScope, SharingStarted.Eagerly, false)
+    private val isFingerprintAuthCurrentlyAllowedOnBouncer =
+        deviceEntryFingerprintAuthInteractor.isFingerprintCurrentlyAllowedOnBouncer.stateIn(
+            applicationScope,
+            SharingStarted.Eagerly,
+            false
+        )
 
     private val currentSecurityMode
         get() = securityModel.getSecurityMode(currentUserId)
@@ -99,13 +103,13 @@
                         BiometricSourceType.FACE ->
                             BouncerMessageStrings.incorrectFaceInput(
                                     currentSecurityMode.toAuthModel(),
-                                    isFingerprintAuthCurrentlyAllowed.value
+                                    isFingerprintAuthCurrentlyAllowedOnBouncer.value
                                 )
                                 .toMessage()
                         else ->
                             BouncerMessageStrings.defaultMessage(
                                     currentSecurityMode.toAuthModel(),
-                                    isFingerprintAuthCurrentlyAllowed.value
+                                    isFingerprintAuthCurrentlyAllowedOnBouncer.value
                                 )
                                 .toMessage()
                     }
@@ -144,11 +148,12 @@
                 biometricSettingsRepository.authenticationFlags,
                 trustRepository.isCurrentUserTrustManaged,
                 isAnyBiometricsEnabledAndEnrolled,
-                deviceEntryFingerprintAuthRepository.isLockedOut,
+                deviceEntryFingerprintAuthInteractor.isLockedOut,
                 faceAuthRepository.isLockedOut,
-                ::Quint
+                isFingerprintAuthCurrentlyAllowedOnBouncer,
+                ::Sextuple
             )
-            .map { (flags, _, biometricsEnrolledAndEnabled, fpLockedOut, faceLockedOut) ->
+            .map { (flags, _, biometricsEnrolledAndEnabled, fpLockedOut, faceLockedOut, _) ->
                 val isTrustUsuallyManaged = trustRepository.isCurrentUserTrustUsuallyManaged.value
                 val trustOrBiometricsAvailable =
                     (isTrustUsuallyManaged || biometricsEnrolledAndEnabled)
@@ -193,14 +198,14 @@
                     } else {
                         BouncerMessageStrings.faceLockedOut(
                                 currentSecurityMode.toAuthModel(),
-                                isFingerprintAuthCurrentlyAllowed.value
+                                isFingerprintAuthCurrentlyAllowedOnBouncer.value
                             )
                             .toMessage()
                     }
                 } else if (flags.isSomeAuthRequiredAfterAdaptiveAuthRequest) {
                     BouncerMessageStrings.authRequiredAfterAdaptiveAuthRequest(
                             currentSecurityMode.toAuthModel(),
-                            isFingerprintAuthCurrentlyAllowed.value
+                            isFingerprintAuthCurrentlyAllowedOnBouncer.value
                         )
                         .toMessage()
                 } else if (
@@ -209,19 +214,19 @@
                 ) {
                     BouncerMessageStrings.nonStrongAuthTimeout(
                             currentSecurityMode.toAuthModel(),
-                            isFingerprintAuthCurrentlyAllowed.value
+                            isFingerprintAuthCurrentlyAllowedOnBouncer.value
                         )
                         .toMessage()
                 } else if (isTrustUsuallyManaged && flags.someAuthRequiredAfterUserRequest) {
                     BouncerMessageStrings.trustAgentDisabled(
                             currentSecurityMode.toAuthModel(),
-                            isFingerprintAuthCurrentlyAllowed.value
+                            isFingerprintAuthCurrentlyAllowedOnBouncer.value
                         )
                         .toMessage()
                 } else if (isTrustUsuallyManaged && flags.someAuthRequiredAfterTrustAgentExpired) {
                     BouncerMessageStrings.trustAgentDisabled(
                             currentSecurityMode.toAuthModel(),
-                            isFingerprintAuthCurrentlyAllowed.value
+                            isFingerprintAuthCurrentlyAllowedOnBouncer.value
                         )
                         .toMessage()
                 } else if (trustOrBiometricsAvailable && flags.isInUserLockdown) {
@@ -265,7 +270,7 @@
         repository.setMessage(
             BouncerMessageStrings.incorrectSecurityInput(
                     currentSecurityMode.toAuthModel(),
-                    isFingerprintAuthCurrentlyAllowed.value
+                    isFingerprintAuthCurrentlyAllowedOnBouncer.value
                 )
                 .toMessage()
         )
@@ -274,14 +279,22 @@
     fun setFingerprintAcquisitionMessage(value: String?) {
         if (!Flags.revampedBouncerMessages()) return
         repository.setMessage(
-            defaultMessage(currentSecurityMode, value, isFingerprintAuthCurrentlyAllowed.value)
+            defaultMessage(
+                currentSecurityMode,
+                value,
+                isFingerprintAuthCurrentlyAllowedOnBouncer.value
+            )
         )
     }
 
     fun setFaceAcquisitionMessage(value: String?) {
         if (!Flags.revampedBouncerMessages()) return
         repository.setMessage(
-            defaultMessage(currentSecurityMode, value, isFingerprintAuthCurrentlyAllowed.value)
+            defaultMessage(
+                currentSecurityMode,
+                value,
+                isFingerprintAuthCurrentlyAllowedOnBouncer.value
+            )
         )
     }
 
@@ -289,7 +302,11 @@
         if (!Flags.revampedBouncerMessages()) return
 
         repository.setMessage(
-            defaultMessage(currentSecurityMode, value, isFingerprintAuthCurrentlyAllowed.value)
+            defaultMessage(
+                currentSecurityMode,
+                value,
+                isFingerprintAuthCurrentlyAllowedOnBouncer.value
+            )
         )
     }
 
@@ -297,7 +314,7 @@
         get() =
             BouncerMessageStrings.defaultMessage(
                     currentSecurityMode.toAuthModel(),
-                    isFingerprintAuthCurrentlyAllowed.value
+                    isFingerprintAuthCurrentlyAllowedOnBouncer.value
                 )
                 .toMessage()
 
@@ -355,11 +372,6 @@
 private fun Flow<Boolean>.or(anotherFlow: Flow<Boolean>) =
     this.combine(anotherFlow) { a, b -> a || b }
 
-private fun Flow<Boolean>.and(anotherFlow: Flow<Boolean>) =
-    this.combine(anotherFlow) { a, b -> a && b }
-
-private fun Flow<Boolean>.isFalse() = this.map { !it }
-
 private fun defaultMessage(
     securityMode: SecurityMode,
     secondaryMessage: String?,
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt
index f3903de..aebc50f 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt
@@ -18,6 +18,7 @@
 
 import android.app.AlertDialog
 import android.content.Context
+import com.android.systemui.bouncer.ui.viewmodel.BouncerMessageViewModelModule
 import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModelModule
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
@@ -30,6 +31,7 @@
     includes =
         [
             BouncerViewModelModule::class,
+            BouncerMessageViewModelModule::class,
         ],
 )
 interface BouncerViewModule {
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
index 0d7f6dc..4fbf735 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
@@ -57,17 +57,11 @@
      */
     @get:StringRes abstract val lockoutMessageId: Int
 
-    /** Notifies that the UI has been shown to the user. */
-    fun onShown() {
-        interactor.resetMessage()
-    }
-
     /**
      * Notifies that the UI has been hidden from the user (after any transitions have completed).
      */
     open fun onHidden() {
         clearInput()
-        interactor.resetMessage()
     }
 
     /** Notifies that the user has placed down a pointer. */
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt
new file mode 100644
index 0000000..6cb9b16
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt
@@ -0,0 +1,436 @@
+/*
+ * 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.bouncer.ui.viewmodel
+
+import android.content.Context
+import android.util.PluralsMessageFormatter
+import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor
+import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags
+import com.android.systemui.bouncer.shared.model.BouncerMessagePair
+import com.android.systemui.bouncer.shared.model.BouncerMessageStrings
+import com.android.systemui.bouncer.shared.model.primaryMessage
+import com.android.systemui.bouncer.shared.model.secondaryMessage
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.deviceentry.domain.interactor.BiometricMessageInteractor
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFingerprintAuthInteractor
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
+import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason
+import com.android.systemui.deviceentry.shared.model.FaceFailureMessage
+import com.android.systemui.deviceentry.shared.model.FaceLockoutMessage
+import com.android.systemui.deviceentry.shared.model.FaceTimeoutMessage
+import com.android.systemui.deviceentry.shared.model.FingerprintFailureMessage
+import com.android.systemui.deviceentry.shared.model.FingerprintLockoutMessage
+import com.android.systemui.res.R.string.kg_too_many_failed_attempts_countdown
+import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
+import com.android.systemui.user.ui.viewmodel.UserViewModel
+import com.android.systemui.util.kotlin.Utils.Companion.sample
+import com.android.systemui.util.time.SystemClock
+import dagger.Module
+import dagger.Provides
+import kotlin.math.ceil
+import kotlin.math.max
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+
+/** Holds UI state for the 2-line status message shown on the bouncer. */
+@OptIn(ExperimentalCoroutinesApi::class)
+class BouncerMessageViewModel(
+    @Application private val applicationContext: Context,
+    @Application private val applicationScope: CoroutineScope,
+    private val bouncerInteractor: BouncerInteractor,
+    private val simBouncerInteractor: SimBouncerInteractor,
+    private val authenticationInteractor: AuthenticationInteractor,
+    selectedUser: Flow<UserViewModel>,
+    private val clock: SystemClock,
+    private val biometricMessageInteractor: BiometricMessageInteractor,
+    private val faceAuthInteractor: DeviceEntryFaceAuthInteractor,
+    private val deviceEntryInteractor: DeviceEntryInteractor,
+    private val fingerprintInteractor: DeviceEntryFingerprintAuthInteractor,
+    flags: ComposeBouncerFlags,
+) {
+    /**
+     * A message shown when the user has attempted the wrong credential too many times and now must
+     * wait a while before attempting to authenticate again.
+     *
+     * This is updated every second (countdown) during the lockout. When lockout is not active, this
+     * is `null` and no lockout message should be shown.
+     */
+    private val lockoutMessage: MutableStateFlow<MessageViewModel?> = MutableStateFlow(null)
+
+    /** Whether there is a lockout message that is available to be shown in the status message. */
+    val isLockoutMessagePresent: Flow<Boolean> = lockoutMessage.map { it != null }
+
+    /** The user-facing message to show in the bouncer. */
+    val message: MutableStateFlow<MessageViewModel?> = MutableStateFlow(null)
+
+    /** Initializes the bouncer message to default whenever it is shown. */
+    fun onShown() {
+        showDefaultMessage()
+    }
+
+    /** Reset the message shown on the bouncer to the default message. */
+    fun showDefaultMessage() {
+        resetToDefault.tryEmit(Unit)
+    }
+
+    private val resetToDefault = MutableSharedFlow<Unit>(replay = 1)
+
+    private var lockoutCountdownJob: Job? = null
+
+    private fun defaultBouncerMessageInitializer() {
+        applicationScope.launch {
+            resetToDefault.emit(Unit)
+            authenticationInteractor.authenticationMethod
+                .flatMapLatest { authMethod ->
+                    if (authMethod == AuthenticationMethodModel.Sim) {
+                        resetToDefault.map {
+                            MessageViewModel(simBouncerInteractor.getDefaultMessage())
+                        }
+                    } else if (authMethod.isSecure) {
+                        combine(
+                            deviceEntryInteractor.deviceEntryRestrictionReason,
+                            lockoutMessage,
+                            fingerprintInteractor.isFingerprintCurrentlyAllowedOnBouncer,
+                            resetToDefault,
+                        ) { deviceEntryRestrictedReason, lockoutMsg, isFpAllowedInBouncer, _ ->
+                            lockoutMsg
+                                ?: deviceEntryRestrictedReason.toMessage(
+                                    authMethod,
+                                    isFpAllowedInBouncer
+                                )
+                        }
+                    } else {
+                        emptyFlow()
+                    }
+                }
+                .collectLatest { messageViewModel -> message.value = messageViewModel }
+        }
+    }
+
+    private fun listenForSimBouncerEvents() {
+        // Listen for any events from the SIM bouncer and update the message shown on the bouncer.
+        applicationScope.launch {
+            authenticationInteractor.authenticationMethod
+                .flatMapLatest { authMethod ->
+                    if (authMethod == AuthenticationMethodModel.Sim) {
+                        simBouncerInteractor.bouncerMessageChanged.map { simMsg ->
+                            simMsg?.let { MessageViewModel(it) }
+                        }
+                    } else {
+                        emptyFlow()
+                    }
+                }
+                .collectLatest {
+                    if (it != null) {
+                        message.value = it
+                    } else {
+                        resetToDefault.emit(Unit)
+                    }
+                }
+        }
+    }
+
+    private fun listenForFaceMessages() {
+        // Listen for any events from face authentication and update the message shown on the
+        // bouncer.
+        applicationScope.launch {
+            biometricMessageInteractor.faceMessage
+                .sample(
+                    authenticationInteractor.authenticationMethod,
+                    fingerprintInteractor.isFingerprintCurrentlyAllowedOnBouncer,
+                )
+                .collectLatest { (faceMessage, authMethod, fingerprintAllowedOnBouncer) ->
+                    val isFaceAuthStrong = faceAuthInteractor.isFaceAuthStrong()
+                    val defaultPrimaryMessage =
+                        BouncerMessageStrings.defaultMessage(
+                                authMethod,
+                                fingerprintAllowedOnBouncer
+                            )
+                            .primaryMessage
+                            .toResString()
+                    message.value =
+                        when (faceMessage) {
+                            is FaceTimeoutMessage ->
+                                MessageViewModel(
+                                    text = defaultPrimaryMessage,
+                                    secondaryText = faceMessage.message,
+                                    isUpdateAnimated = true
+                                )
+                            is FaceLockoutMessage ->
+                                if (isFaceAuthStrong)
+                                    BouncerMessageStrings.class3AuthLockedOut(authMethod)
+                                        .toMessage()
+                                else
+                                    BouncerMessageStrings.faceLockedOut(
+                                            authMethod,
+                                            fingerprintAllowedOnBouncer
+                                        )
+                                        .toMessage()
+                            is FaceFailureMessage ->
+                                BouncerMessageStrings.incorrectFaceInput(
+                                        authMethod,
+                                        fingerprintAllowedOnBouncer
+                                    )
+                                    .toMessage()
+                            else ->
+                                MessageViewModel(
+                                    text = defaultPrimaryMessage,
+                                    secondaryText = faceMessage.message,
+                                    isUpdateAnimated = false
+                                )
+                        }
+                    delay(MESSAGE_DURATION)
+                    resetToDefault.emit(Unit)
+                }
+        }
+    }
+
+    private fun listenForFingerprintMessages() {
+        applicationScope.launch {
+            // Listen for any events from fingerprint authentication and update the message shown
+            // on the bouncer.
+            biometricMessageInteractor.fingerprintMessage
+                .sample(
+                    authenticationInteractor.authenticationMethod,
+                    fingerprintInteractor.isFingerprintCurrentlyAllowedOnBouncer
+                )
+                .collectLatest { (fingerprintMessage, authMethod, isFingerprintAllowed) ->
+                    val defaultPrimaryMessage =
+                        BouncerMessageStrings.defaultMessage(authMethod, isFingerprintAllowed)
+                            .primaryMessage
+                            .toResString()
+                    message.value =
+                        when (fingerprintMessage) {
+                            is FingerprintLockoutMessage ->
+                                BouncerMessageStrings.class3AuthLockedOut(authMethod).toMessage()
+                            is FingerprintFailureMessage ->
+                                BouncerMessageStrings.incorrectFingerprintInput(authMethod)
+                                    .toMessage()
+                            else ->
+                                MessageViewModel(
+                                    text = defaultPrimaryMessage,
+                                    secondaryText = fingerprintMessage.message,
+                                    isUpdateAnimated = false
+                                )
+                        }
+                    delay(MESSAGE_DURATION)
+                    resetToDefault.emit(Unit)
+                }
+        }
+    }
+
+    private fun listenForBouncerEvents() {
+        // Keeps the lockout message up-to-date.
+        applicationScope.launch {
+            bouncerInteractor.onLockoutStarted.collect { startLockoutCountdown() }
+        }
+
+        // Listens to relevant bouncer events
+        applicationScope.launch {
+            bouncerInteractor.onIncorrectBouncerInput
+                .sample(
+                    authenticationInteractor.authenticationMethod,
+                    fingerprintInteractor.isFingerprintCurrentlyAllowedOnBouncer
+                )
+                .collectLatest { (_, authMethod, isFingerprintAllowed) ->
+                    message.emit(
+                        BouncerMessageStrings.incorrectSecurityInput(
+                                authMethod,
+                                isFingerprintAllowed
+                            )
+                            .toMessage()
+                    )
+                    delay(MESSAGE_DURATION)
+                    resetToDefault.emit(Unit)
+                }
+        }
+    }
+
+    private fun DeviceEntryRestrictionReason?.toMessage(
+        authMethod: AuthenticationMethodModel,
+        isFingerprintAllowedOnBouncer: Boolean,
+    ): MessageViewModel {
+        return when (this) {
+            DeviceEntryRestrictionReason.UserLockdown ->
+                BouncerMessageStrings.authRequiredAfterUserLockdown(authMethod)
+            DeviceEntryRestrictionReason.DeviceNotUnlockedSinceReboot ->
+                BouncerMessageStrings.authRequiredAfterReboot(authMethod)
+            DeviceEntryRestrictionReason.PolicyLockdown ->
+                BouncerMessageStrings.authRequiredAfterAdminLockdown(authMethod)
+            DeviceEntryRestrictionReason.UnattendedUpdate ->
+                BouncerMessageStrings.authRequiredForUnattendedUpdate(authMethod)
+            DeviceEntryRestrictionReason.DeviceNotUnlockedSinceMainlineUpdate ->
+                BouncerMessageStrings.authRequiredForMainlineUpdate(authMethod)
+            DeviceEntryRestrictionReason.SecurityTimeout ->
+                BouncerMessageStrings.authRequiredAfterPrimaryAuthTimeout(authMethod)
+            DeviceEntryRestrictionReason.StrongBiometricsLockedOut ->
+                BouncerMessageStrings.class3AuthLockedOut(authMethod)
+            DeviceEntryRestrictionReason.NonStrongFaceLockedOut ->
+                BouncerMessageStrings.faceLockedOut(authMethod, isFingerprintAllowedOnBouncer)
+            DeviceEntryRestrictionReason.NonStrongBiometricsSecurityTimeout ->
+                BouncerMessageStrings.nonStrongAuthTimeout(
+                    authMethod,
+                    isFingerprintAllowedOnBouncer
+                )
+            DeviceEntryRestrictionReason.TrustAgentDisabled ->
+                BouncerMessageStrings.trustAgentDisabled(authMethod, isFingerprintAllowedOnBouncer)
+            DeviceEntryRestrictionReason.AdaptiveAuthRequest ->
+                BouncerMessageStrings.authRequiredAfterAdaptiveAuthRequest(
+                    authMethod,
+                    isFingerprintAllowedOnBouncer
+                )
+            else -> BouncerMessageStrings.defaultMessage(authMethod, isFingerprintAllowedOnBouncer)
+        }.toMessage()
+    }
+
+    private fun BouncerMessagePair.toMessage(): MessageViewModel {
+        val primaryMsg = this.primaryMessage.toResString()
+        val secondaryMsg =
+            if (this.secondaryMessage == 0) "" else this.secondaryMessage.toResString()
+        return MessageViewModel(primaryMsg, secondaryText = secondaryMsg, isUpdateAnimated = true)
+    }
+
+    /** Shows the countdown message and refreshes it every second. */
+    private fun startLockoutCountdown() {
+        lockoutCountdownJob?.cancel()
+        lockoutCountdownJob =
+            applicationScope.launch {
+                authenticationInteractor.authenticationMethod.collectLatest { authMethod ->
+                    do {
+                        val remainingSeconds = remainingLockoutSeconds()
+                        val authLockedOutMsg =
+                            BouncerMessageStrings.primaryAuthLockedOut(authMethod)
+                        lockoutMessage.value =
+                            if (remainingSeconds > 0) {
+                                MessageViewModel(
+                                    text =
+                                        kg_too_many_failed_attempts_countdown.toPluralString(
+                                            mutableMapOf<String, Any>(
+                                                Pair("count", remainingSeconds)
+                                            )
+                                        ),
+                                    secondaryText = authLockedOutMsg.secondaryMessage.toResString(),
+                                    isUpdateAnimated = false
+                                )
+                            } else {
+                                null
+                            }
+                        delay(1.seconds)
+                    } while (remainingSeconds > 0)
+                    lockoutCountdownJob = null
+                }
+            }
+    }
+
+    private fun remainingLockoutSeconds(): Int {
+        val endTimestampMs = authenticationInteractor.lockoutEndTimestamp ?: 0
+        val remainingMs = max(0, endTimestampMs - clock.elapsedRealtime())
+        return ceil(remainingMs / 1000f).toInt()
+    }
+
+    private fun Int.toPluralString(formatterArgs: Map<String, Any>): String =
+        PluralsMessageFormatter.format(applicationContext.resources, formatterArgs, this)
+
+    private fun Int.toResString(): String = applicationContext.getString(this)
+
+    init {
+        if (flags.isComposeBouncerOrSceneContainerEnabled()) {
+            applicationScope.launch {
+                // Update the lockout countdown whenever the selected user is switched.
+                selectedUser.collect { startLockoutCountdown() }
+            }
+
+            defaultBouncerMessageInitializer()
+
+            listenForSimBouncerEvents()
+            listenForBouncerEvents()
+            listenForFaceMessages()
+            listenForFingerprintMessages()
+        }
+    }
+
+    companion object {
+        private const val MESSAGE_DURATION = 2000L
+    }
+}
+
+/** Data class that represents the status message show on the bouncer. */
+data class MessageViewModel(
+    val text: String,
+    val secondaryText: String? = null,
+    /**
+     * Whether updates to the message should be cross-animated from one message to another.
+     *
+     * If `false`, no animation should be applied, the message text should just be replaced
+     * instantly.
+     */
+    val isUpdateAnimated: Boolean = true,
+)
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@Module
+object BouncerMessageViewModelModule {
+
+    @Provides
+    @SysUISingleton
+    fun viewModel(
+        @Application applicationContext: Context,
+        @Application applicationScope: CoroutineScope,
+        bouncerInteractor: BouncerInteractor,
+        simBouncerInteractor: SimBouncerInteractor,
+        authenticationInteractor: AuthenticationInteractor,
+        clock: SystemClock,
+        biometricMessageInteractor: BiometricMessageInteractor,
+        faceAuthInteractor: DeviceEntryFaceAuthInteractor,
+        deviceEntryInteractor: DeviceEntryInteractor,
+        fingerprintInteractor: DeviceEntryFingerprintAuthInteractor,
+        flags: ComposeBouncerFlags,
+        userSwitcherViewModel: UserSwitcherViewModel,
+    ): BouncerMessageViewModel {
+        return BouncerMessageViewModel(
+            applicationContext = applicationContext,
+            applicationScope = applicationScope,
+            bouncerInteractor = bouncerInteractor,
+            simBouncerInteractor = simBouncerInteractor,
+            authenticationInteractor = authenticationInteractor,
+            clock = clock,
+            biometricMessageInteractor = biometricMessageInteractor,
+            faceAuthInteractor = faceAuthInteractor,
+            deviceEntryInteractor = deviceEntryInteractor,
+            fingerprintInteractor = fingerprintInteractor,
+            flags = flags,
+            selectedUser = userSwitcherViewModel.selectedUser,
+        )
+    }
+}
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 6287578..5c07cc5 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
@@ -21,7 +21,6 @@
 import android.content.Context
 import android.graphics.Bitmap
 import androidx.core.graphics.drawable.toBitmap
-import com.android.internal.R
 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.authentication.shared.model.AuthenticationWipeModel
@@ -40,18 +39,12 @@
 import com.android.systemui.user.ui.viewmodel.UserActionViewModel
 import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
 import com.android.systemui.user.ui.viewmodel.UserViewModel
-import com.android.systemui.util.time.SystemClock
 import dagger.Module
 import dagger.Provides
-import kotlin.math.ceil
-import kotlin.math.max
-import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Job
 import kotlinx.coroutines.SupervisorJob
 import kotlinx.coroutines.cancel
-import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
@@ -72,13 +65,13 @@
     private val simBouncerInteractor: SimBouncerInteractor,
     private val authenticationInteractor: AuthenticationInteractor,
     private val selectedUserInteractor: SelectedUserInteractor,
+    private val devicePolicyManager: DevicePolicyManager,
+    bouncerMessageViewModel: BouncerMessageViewModel,
     flags: ComposeBouncerFlags,
     selectedUser: Flow<UserViewModel>,
     users: Flow<List<UserViewModel>>,
     userSwitcherMenu: Flow<List<UserActionViewModel>>,
     actionButton: Flow<BouncerActionButtonModel?>,
-    private val clock: SystemClock,
-    private val devicePolicyManager: DevicePolicyManager,
 ) {
     val selectedUserImage: StateFlow<Bitmap?> =
         selectedUser
@@ -89,6 +82,8 @@
                 initialValue = null,
             )
 
+    val message: BouncerMessageViewModel = bouncerMessageViewModel
+
     val userSwitcherDropdown: StateFlow<List<UserSwitcherDropdownItemViewModel>> =
         combine(
                 users,
@@ -163,24 +158,6 @@
             )
 
     /**
-     * A message shown when the user has attempted the wrong credential too many times and now must
-     * wait a while before attempting to authenticate again.
-     *
-     * This is updated every second (countdown) during the lockout duration. When lockout is not
-     * active, this is `null` and no lockout message should be shown.
-     */
-    private val lockoutMessage = MutableStateFlow<String?>(null)
-
-    /** The user-facing message to show in the bouncer. */
-    val message: StateFlow<MessageViewModel> =
-        combine(bouncerInteractor.message, lockoutMessage) { _, _ -> createMessageViewModel() }
-            .stateIn(
-                scope = applicationScope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = createMessageViewModel(),
-            )
-
-    /**
      * The bouncer action button (Return to Call / Emergency Call). If `null`, the button should not
      * be shown.
      */
@@ -222,31 +199,16 @@
             )
 
     private val isInputEnabled: StateFlow<Boolean> =
-        lockoutMessage
-            .map { it == null }
+        bouncerMessageViewModel.isLockoutMessagePresent
+            .map { lockoutMessagePresent -> !lockoutMessagePresent }
             .stateIn(
                 scope = applicationScope,
                 started = SharingStarted.WhileSubscribed(),
                 initialValue = authenticationInteractor.lockoutEndTimestamp == null,
             )
 
-    private var lockoutCountdownJob: Job? = null
-
     init {
         if (flags.isComposeBouncerOrSceneContainerEnabled()) {
-            // Keeps the lockout dialog up-to-date.
-            applicationScope.launch {
-                bouncerInteractor.onLockoutStarted.collect {
-                    showLockoutDialog()
-                    startLockoutCountdown()
-                }
-            }
-
-            applicationScope.launch {
-                // Update the lockout countdown whenever the selected user is switched.
-                selectedUser.collect { startLockoutCountdown() }
-            }
-
             // Keeps the upcoming wipe dialog up-to-date.
             applicationScope.launch {
                 authenticationInteractor.upcomingWipe.collect { wipeModel ->
@@ -256,48 +218,6 @@
         }
     }
 
-    private fun showLockoutDialog() {
-        applicationScope.launch {
-            val failedAttempts = authenticationInteractor.failedAuthenticationAttempts.value
-            lockoutDialogMessage.value =
-                authMethodViewModel.value?.lockoutMessageId?.let { messageId ->
-                    applicationContext.getString(
-                        messageId,
-                        failedAttempts,
-                        remainingLockoutSeconds()
-                    )
-                }
-        }
-    }
-
-    /** Shows the countdown message and refreshes it every second. */
-    private fun startLockoutCountdown() {
-        lockoutCountdownJob?.cancel()
-        lockoutCountdownJob =
-            applicationScope.launch {
-                do {
-                    val remainingSeconds = remainingLockoutSeconds()
-                    lockoutMessage.value =
-                        if (remainingSeconds > 0) {
-                            applicationContext.getString(
-                                R.string.lockscreen_too_many_failed_attempts_countdown,
-                                remainingSeconds,
-                            )
-                        } else {
-                            null
-                        }
-                    delay(1.seconds)
-                } while (remainingSeconds > 0)
-                lockoutCountdownJob = null
-            }
-    }
-
-    private fun remainingLockoutSeconds(): Int {
-        val endTimestampMs = authenticationInteractor.lockoutEndTimestamp ?: 0
-        val remainingMs = max(0, endTimestampMs - clock.elapsedRealtime())
-        return ceil(remainingMs / 1000f).toInt()
-    }
-
     private fun isSideBySideSupported(authMethod: AuthMethodBouncerViewModel?): Boolean {
         return isUserSwitcherVisible || authMethod !is PasswordBouncerViewModel
     }
@@ -306,15 +226,6 @@
         return authMethod !is PasswordBouncerViewModel
     }
 
-    private fun createMessageViewModel(): MessageViewModel {
-        val isLockedOut = lockoutMessage.value != null
-        return MessageViewModel(
-            // A lockout message takes precedence over the non-lockout message.
-            text = lockoutMessage.value ?: bouncerInteractor.message.value ?: "",
-            isUpdateAnimated = !isLockedOut,
-        )
-    }
-
     private fun getChildViewModel(
         authenticationMethod: AuthenticationMethodModel,
     ): AuthMethodBouncerViewModel? {
@@ -336,7 +247,8 @@
                     interactor = bouncerInteractor,
                     isInputEnabled = isInputEnabled,
                     simBouncerInteractor = simBouncerInteractor,
-                    authenticationMethod = authenticationMethod
+                    authenticationMethod = authenticationMethod,
+                    onIntentionalUserInput = ::onIntentionalUserInput
                 )
             is AuthenticationMethodModel.Sim ->
                 PinBouncerViewModel(
@@ -346,6 +258,7 @@
                     isInputEnabled = isInputEnabled,
                     simBouncerInteractor = simBouncerInteractor,
                     authenticationMethod = authenticationMethod,
+                    onIntentionalUserInput = ::onIntentionalUserInput
                 )
             is AuthenticationMethodModel.Password ->
                 PasswordBouncerViewModel(
@@ -354,6 +267,7 @@
                     interactor = bouncerInteractor,
                     inputMethodInteractor = inputMethodInteractor,
                     selectedUserInteractor = selectedUserInteractor,
+                    onIntentionalUserInput = ::onIntentionalUserInput
                 )
             is AuthenticationMethodModel.Pattern ->
                 PatternBouncerViewModel(
@@ -361,11 +275,17 @@
                     viewModelScope = newViewModelScope,
                     interactor = bouncerInteractor,
                     isInputEnabled = isInputEnabled,
+                    onIntentionalUserInput = ::onIntentionalUserInput
                 )
             else -> null
         }
     }
 
+    private fun onIntentionalUserInput() {
+        message.showDefaultMessage()
+        bouncerInteractor.onIntentionalUserInput()
+    }
+
     private fun createChildCoroutineScope(parentScope: CoroutineScope): CoroutineScope {
         return CoroutineScope(
             SupervisorJob(parent = parentScope.coroutineContext.job) + mainDispatcher
@@ -437,18 +357,6 @@
         }
     }
 
-    data class MessageViewModel(
-        val text: String,
-
-        /**
-         * Whether updates to the message should be cross-animated from one message to another.
-         *
-         * If `false`, no animation should be applied, the message text should just be replaced
-         * instantly.
-         */
-        val isUpdateAnimated: Boolean,
-    )
-
     data class DialogViewModel(
         val text: String,
 
@@ -480,8 +388,8 @@
         selectedUserInteractor: SelectedUserInteractor,
         flags: ComposeBouncerFlags,
         userSwitcherViewModel: UserSwitcherViewModel,
-        clock: SystemClock,
         devicePolicyManager: DevicePolicyManager,
+        bouncerMessageViewModel: BouncerMessageViewModel,
     ): BouncerViewModel {
         return BouncerViewModel(
             applicationContext = applicationContext,
@@ -497,8 +405,8 @@
             users = userSwitcherViewModel.users,
             userSwitcherMenu = userSwitcherViewModel.menu,
             actionButton = actionButtonInteractor.actionButton,
-            clock = clock,
             devicePolicyManager = devicePolicyManager,
+            bouncerMessageViewModel = bouncerMessageViewModel,
         )
     }
 }
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 b42eda1..052fb6b 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
@@ -40,6 +40,7 @@
     viewModelScope: CoroutineScope,
     isInputEnabled: StateFlow<Boolean>,
     interactor: BouncerInteractor,
+    private val onIntentionalUserInput: () -> Unit,
     private val inputMethodInteractor: InputMethodInteractor,
     private val selectedUserInteractor: SelectedUserInteractor,
 ) :
@@ -96,12 +97,8 @@
 
     /** Notifies that the user has changed the password input. */
     fun onPasswordInputChanged(newPassword: String) {
-        if (this.password.value.isEmpty() && newPassword.isNotEmpty()) {
-            interactor.clearMessage()
-        }
-
         if (newPassword.isNotEmpty()) {
-            interactor.onIntentionalUserInput()
+            onIntentionalUserInput()
         }
 
         _password.value = newPassword
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
index 69f8032..a401600 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
@@ -40,6 +40,7 @@
     viewModelScope: CoroutineScope,
     interactor: BouncerInteractor,
     isInputEnabled: StateFlow<Boolean>,
+    private val onIntentionalUserInput: () -> Unit,
 ) :
     AuthMethodBouncerViewModel(
         viewModelScope = viewModelScope,
@@ -84,7 +85,7 @@
 
     /** Notifies that the user has started a drag gesture across the dot grid. */
     fun onDragStart() {
-        interactor.clearMessage()
+        onIntentionalUserInput()
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
index e910a92..62da5c0 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
@@ -41,6 +41,7 @@
     viewModelScope: CoroutineScope,
     interactor: BouncerInteractor,
     isInputEnabled: StateFlow<Boolean>,
+    private val onIntentionalUserInput: () -> Unit,
     private val simBouncerInteractor: SimBouncerInteractor,
     authenticationMethod: AuthenticationMethodModel,
 ) :
@@ -131,11 +132,8 @@
     /** Notifies that the user clicked on a PIN button with the given digit value. */
     fun onPinButtonClicked(input: Int) {
         val pinInput = mutablePinInput.value
-        if (pinInput.isEmpty()) {
-            interactor.clearMessage()
-        }
 
-        interactor.onIntentionalUserInput()
+        onIntentionalUserInput()
 
         mutablePinInput.value = pinInput.append(input)
         tryAuthenticate(useAutoConfirm = true)
@@ -149,7 +147,6 @@
     /** Notifies that the user long-pressed the backspace button. */
     fun onBackspaceButtonLongPressed() {
         clearInput()
-        interactor.clearMessage()
     }
 
     /** Notifies that the user clicked the "enter" button. */
@@ -173,7 +170,6 @@
     /** Resets the sim screen and shows a default message. */
     private fun onResetSimFlow() {
         simBouncerInteractor.resetSimPukUserInput()
-        interactor.resetMessage()
         clearInput()
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/NotificationContainerBounds.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/NotificationContainerBounds.kt
index 3063ebd..fdd98bec 100644
--- a/packages/SystemUI/src/com/android/systemui/common/shared/model/NotificationContainerBounds.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/NotificationContainerBounds.kt
@@ -18,12 +18,8 @@
 
 /** Models the bounds of the notification container. */
 data class NotificationContainerBounds(
-    /** The position of the left of the container in its window coordinate system, in pixels. */
-    val left: Float = 0f,
     /** The position of the top of the container in its window coordinate system, in pixels. */
     val top: Float = 0f,
-    /** The position of the right of the container in its window coordinate system, in pixels. */
-    val right: Float = 0f,
     /** The position of the bottom of the container in its window coordinate system, in pixels. */
     val bottom: Float = 0f,
     /** Whether any modifications to top/bottom should be smoothly animated. */
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt b/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt
index 964eb6f..578389b 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt
@@ -54,6 +54,18 @@
     }
 
     /**
+     * Returns a [Flow] that emits a dimension pixel size that is kept in sync with the device
+     * configuration.
+     *
+     * @see android.content.res.Resources.getDimensionPixelSize
+     */
+    fun getDimensionPixelOffset(@DimenRes id: Int): Flow<Int> {
+        return configurationController.onDensityOrFontScaleChanged.emitOnStart().map {
+            context.resources.getDimensionPixelOffset(id)
+        }
+    }
+
+    /**
      * Returns a [Flow] that emits a color that is kept in sync with the device theme.
      *
      * @see Utils.getColorAttrDefaultColor
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
index bfe751a..afa7c37 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
@@ -16,24 +16,36 @@
 
 package com.android.systemui.communal.ui.viewmodel
 
+import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetProviderInfo
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.res.Resources
+import android.util.Log
+import androidx.activity.result.ActivityResultLauncher
 import com.android.internal.logging.UiEventLogger
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
 import com.android.systemui.communal.domain.model.CommunalContentModel
 import com.android.systemui.communal.shared.log.CommunalUiEvent
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.Logger
 import com.android.systemui.log.dagger.CommunalLog
 import com.android.systemui.media.controls.ui.view.MediaHost
 import com.android.systemui.media.dagger.MediaModule
+import com.android.systemui.res.R
 import javax.inject.Inject
 import javax.inject.Named
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.withContext
 
 /** The view model for communal hub in edit mode. */
 @SysUISingleton
@@ -45,6 +57,7 @@
     @Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost,
     private val uiEventLogger: UiEventLogger,
     @CommunalLog logBuffer: LogBuffer,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
 ) : BaseCommunalViewModel(communalInteractor, mediaHost) {
 
     private val logger = Logger(logBuffer, "CommunalEditModeViewModel")
@@ -86,10 +99,77 @@
         uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL)
     }
 
-    /** Returns the widget categories to show on communal hub. */
-    val getCommunalWidgetCategories: Int
-        get() = communalSettingsInteractor.communalWidgetCategories.value
+    /** Launch the widget picker activity using the given {@link ActivityResultLauncher}. */
+    suspend fun onOpenWidgetPicker(
+        resources: Resources,
+        packageManager: PackageManager,
+        activityLauncher: ActivityResultLauncher<Intent>
+    ): Boolean =
+        withContext(backgroundDispatcher) {
+            val widgets = communalInteractor.widgetContent.first()
+            val excludeList = widgets.mapTo(ArrayList()) { it.providerInfo }
+            getWidgetPickerActivityIntent(resources, packageManager, excludeList)?.let {
+                try {
+                    activityLauncher.launch(it)
+                    return@withContext true
+                } catch (e: Exception) {
+                    Log.e(TAG, "Failed to launch widget picker activity", e)
+                }
+            }
+            false
+        }
+
+    private fun getWidgetPickerActivityIntent(
+        resources: Resources,
+        packageManager: PackageManager,
+        excludeList: ArrayList<AppWidgetProviderInfo>
+    ): Intent? {
+        val packageName =
+            getLauncherPackageName(packageManager)
+                ?: run {
+                    Log.e(TAG, "Couldn't resolve launcher package name")
+                    return@getWidgetPickerActivityIntent null
+                }
+
+        return Intent(Intent.ACTION_PICK).apply {
+            setPackage(packageName)
+            putExtra(
+                EXTRA_DESIRED_WIDGET_WIDTH,
+                resources.getDimensionPixelSize(R.dimen.communal_widget_picker_desired_width)
+            )
+            putExtra(
+                EXTRA_DESIRED_WIDGET_HEIGHT,
+                resources.getDimensionPixelSize(R.dimen.communal_widget_picker_desired_height)
+            )
+            putExtra(
+                AppWidgetManager.EXTRA_CATEGORY_FILTER,
+                communalSettingsInteractor.communalWidgetCategories.value
+            )
+            putExtra(EXTRA_UI_SURFACE_KEY, EXTRA_UI_SURFACE_VALUE)
+            putParcelableArrayListExtra(EXTRA_ADDED_APP_WIDGETS_KEY, excludeList)
+        }
+    }
+
+    private fun getLauncherPackageName(packageManager: PackageManager): String? {
+        return packageManager
+            .resolveActivity(
+                Intent(Intent.ACTION_MAIN).also { it.addCategory(Intent.CATEGORY_HOME) },
+                PackageManager.MATCH_DEFAULT_ONLY
+            )
+            ?.activityInfo
+            ?.packageName
+    }
 
     /** Sets whether edit mode is currently open */
     fun setEditModeOpen(isOpen: Boolean) = communalInteractor.setEditModeOpen(isOpen)
+
+    companion object {
+        private const val TAG = "CommunalEditModeViewModel"
+
+        private const val EXTRA_DESIRED_WIDGET_WIDTH = "desired_widget_width"
+        private const val EXTRA_DESIRED_WIDGET_HEIGHT = "desired_widget_height"
+        private const val EXTRA_UI_SURFACE_KEY = "ui_surface"
+        private const val EXTRA_UI_SURFACE_VALUE = "widgets_hub"
+        const val EXTRA_ADDED_APP_WIDGETS_KEY = "added_app_widgets"
+    }
 }
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 b6ad26b..ba18f01 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
@@ -16,9 +16,7 @@
 
 package com.android.systemui.communal.widgets
 
-import android.appwidget.AppWidgetManager
 import android.content.Intent
-import android.content.pm.PackageManager
 import android.os.Bundle
 import android.os.RemoteException
 import android.util.Log
@@ -32,6 +30,8 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.ui.Modifier
+import androidx.lifecycle.lifecycleScope
+import com.android.app.tracing.coroutines.launch
 import com.android.compose.theme.LocalAndroidColorScheme
 import com.android.compose.theme.PlatformTheme
 import com.android.internal.logging.UiEventLogger
@@ -43,8 +43,8 @@
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.Logger
 import com.android.systemui.log.dagger.CommunalLog
-import com.android.systemui.res.R
 import javax.inject.Inject
+import kotlinx.coroutines.launch
 
 /** An Activity for editing the widgets that appear in hub mode. */
 class EditWidgetsActivity
@@ -57,11 +57,8 @@
     @CommunalLog logBuffer: LogBuffer,
 ) : ComponentActivity() {
     companion object {
-        private const val EXTRA_IS_PENDING_WIDGET_DRAG = "is_pending_widget_drag"
-        private const val EXTRA_DESIRED_WIDGET_WIDTH = "desired_widget_width"
-        private const val EXTRA_DESIRED_WIDGET_HEIGHT = "desired_widget_height"
-
         private const val TAG = "EditWidgetsActivity"
+        private const val EXTRA_IS_PENDING_WIDGET_DRAG = "is_pending_widget_drag"
         const val EXTRA_PRESELECTED_KEY = "preselected_key"
     }
 
@@ -136,39 +133,13 @@
     }
 
     private fun onOpenWidgetPicker() {
-        val intent = Intent(Intent.ACTION_MAIN).also { it.addCategory(Intent.CATEGORY_HOME) }
-        packageManager
-            .resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY)
-            ?.activityInfo
-            ?.packageName
-            ?.let { packageName ->
-                try {
-                    addWidgetActivityLauncher.launch(
-                        Intent(Intent.ACTION_PICK).apply {
-                            setPackage(packageName)
-                            putExtra(
-                                EXTRA_DESIRED_WIDGET_WIDTH,
-                                resources.getDimensionPixelSize(
-                                    R.dimen.communal_widget_picker_desired_width
-                                )
-                            )
-                            putExtra(
-                                EXTRA_DESIRED_WIDGET_HEIGHT,
-                                resources.getDimensionPixelSize(
-                                    R.dimen.communal_widget_picker_desired_height
-                                )
-                            )
-                            putExtra(
-                                AppWidgetManager.EXTRA_CATEGORY_FILTER,
-                                communalViewModel.getCommunalWidgetCategories
-                            )
-                        }
-                    )
-                } catch (e: Exception) {
-                    Log.e(TAG, "Failed to launch widget picker activity", e)
-                }
-            }
-            ?: run { Log.e(TAG, "Couldn't resolve launcher package name") }
+        lifecycleScope.launch {
+            communalViewModel.onOpenWidgetPicker(
+                resources,
+                packageManager,
+                addWidgetActivityLauncher
+            )
+        }
     }
 
     private fun onEditDone() {
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt
index 8059993..c4e0ef7 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt
@@ -29,6 +29,8 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.filterIsInstance
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 
 @OptIn(ExperimentalCoroutinesApi::class)
@@ -72,4 +74,14 @@
      */
     val isSensorUnderDisplay =
         fingerprintPropertyRepository.sensorType.map(FingerprintSensorType::isUdfps)
+
+    /** Whether fingerprint authentication is currently allowed while on the bouncer. */
+    val isFingerprintCurrentlyAllowedOnBouncer =
+        isSensorUnderDisplay.flatMapLatest { sensorBelowDisplay ->
+            if (sensorBelowDisplay) {
+                flowOf(false)
+            } else {
+                isFingerprintAuthCurrentlyAllowed
+            }
+        }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
index 298da13..1bcee74 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -23,13 +23,11 @@
 import com.android.server.notification.Flags.politeNotifications
 import com.android.server.notification.Flags.vibrateWhileUnlocked
 import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
-import com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR
-import com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT
 import com.android.systemui.Flags.communalHub
-import com.android.systemui.Flags.keyguardBottomAreaRefactor
-import com.android.systemui.Flags.migrateClocksToBlueprint
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.flags.Flags.MIGRATE_KEYGUARD_STATUS_BAR_VIEW
+import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.keyguard.shared.ComposeLockscreen
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
@@ -58,11 +56,11 @@
         SceneContainerFlag.getMainStaticFlag() dependsOn MIGRATE_KEYGUARD_STATUS_BAR_VIEW
 
         // ComposeLockscreen dependencies
-        ComposeLockscreen.token dependsOn keyguardBottomAreaRefactor
-        ComposeLockscreen.token dependsOn migrateClocksToBlueprint
+        ComposeLockscreen.token dependsOn KeyguardBottomAreaRefactor.token
+        ComposeLockscreen.token dependsOn MigrateClocksToBlueprint.token
 
         // CommunalHub dependencies
-        communalHub dependsOn migrateClocksToBlueprint
+        communalHub dependsOn MigrateClocksToBlueprint.token
     }
 
     private inline val politeNotifications
@@ -71,10 +69,6 @@
         get() = FlagToken(FLAG_CROSS_APP_POLITE_NOTIFICATIONS, crossAppPoliteNotifications())
     private inline val vibrateWhileUnlockedToken: FlagToken
         get() = FlagToken(FLAG_VIBRATE_WHILE_UNLOCKED, vibrateWhileUnlocked())
-    private inline val keyguardBottomAreaRefactor
-        get() = FlagToken(FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, keyguardBottomAreaRefactor())
-    private inline val migrateClocksToBlueprint
-        get() = FlagToken(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, migrateClocksToBlueprint())
     private inline val communalHub
         get() = FlagToken(FLAG_COMMUNAL_HUB, communalHub())
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardBottomAreaRefactor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardBottomAreaRefactor.kt
new file mode 100644
index 0000000..779b27b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardBottomAreaRefactor.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.keyguard
+
+import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading or using the keyguard bottom area refactor flag. */
+@Suppress("NOTHING_TO_INLINE")
+object KeyguardBottomAreaRefactor {
+    /** The aconfig flag name */
+    const val FLAG_NAME = Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR
+
+    /** A token used for dependency declaration */
+    val token: FlagToken
+        get() = FlagToken(FLAG_NAME, isEnabled)
+
+    /** Is the refactor enabled */
+    @JvmStatic
+    inline val isEnabled
+        get() = Flags.keyguardBottomAreaRefactor()
+
+    /**
+     * Called to ensure code is only run when the flag is enabled. This protects users from the
+     * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+     * build to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun isUnexpectedlyInLegacyMode() =
+        RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+    /**
+     * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+     * the flag is enabled to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
index 5565ee2..d9d7470 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
@@ -36,7 +36,6 @@
 import com.android.keyguard.LockIconViewController
 import com.android.keyguard.dagger.KeyguardStatusViewComponent
 import com.android.systemui.CoreStartable
-import com.android.systemui.Flags.keyguardBottomAreaRefactor
 import com.android.systemui.common.ui.ConfigurationState
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor
@@ -166,7 +165,7 @@
     fun bindIndicationArea() {
         indicationAreaHandle?.dispose()
 
-        if (!keyguardBottomAreaRefactor()) {
+        if (!KeyguardBottomAreaRefactor.isEnabled) {
             keyguardRootView.findViewById<View?>(R.id.keyguard_indication_area)?.let {
                 keyguardRootView.removeView(it)
             }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 3b34750..f700e03 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -40,7 +40,6 @@
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN;
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE;
 import static com.android.systemui.DejankUtils.whitelistIpcs;
-import static com.android.systemui.Flags.migrateClocksToBlueprint;
 import static com.android.systemui.Flags.notifyPowerManagerUserActivityBackground;
 import static com.android.systemui.Flags.refactorGetCurrentUser;
 import static com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel.DREAMING_ANIMATION_DURATION_MS;
@@ -3404,7 +3403,8 @@
         }
 
         // Ensure that keyguard becomes visible if the going away animation is canceled
-        if (showKeyguard && !KeyguardWmStateRefactor.isEnabled() && migrateClocksToBlueprint()) {
+        if (showKeyguard && !KeyguardWmStateRefactor.isEnabled()
+                && MigrateClocksToBlueprint.isEnabled()) {
             mKeyguardInteractor.showKeyguard();
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/MigrateClocksToBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/MigrateClocksToBlueprint.kt
new file mode 100644
index 0000000..5a2943b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/MigrateClocksToBlueprint.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.keyguard
+
+import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading or using the migrate clocks to blueprint flag. */
+@Suppress("NOTHING_TO_INLINE")
+object MigrateClocksToBlueprint {
+    /** The aconfig flag name */
+    const val FLAG_NAME = Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT
+
+    /** A token used for dependency declaration */
+    val token: FlagToken
+        get() = FlagToken(FLAG_NAME, isEnabled)
+
+    /** Is the refactor enabled */
+    @JvmStatic
+    inline val isEnabled
+        get() = Flags.migrateClocksToBlueprint()
+
+    /**
+     * Called to ensure code is only run when the flag is enabled. This protects users from the
+     * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+     * build to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun isUnexpectedlyInLegacyMode() =
+        RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+    /**
+     * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+     * the flag is enabled to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt
index 7ad5aac..3f4d3a8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt
@@ -18,7 +18,6 @@
 
 import android.os.UserHandle
 import android.provider.Settings
-import androidx.annotation.VisibleForTesting
 import com.android.keyguard.ClockEventController
 import com.android.keyguard.KeyguardClockSwitch.ClockSize
 import com.android.keyguard.KeyguardClockSwitch.LARGE
@@ -52,14 +51,14 @@
     val clockSize: StateFlow<Int>
 
     /** clock size selected in picker, DYNAMIC or SMALL */
-    val selectedClockSize: Flow<SettingsClockSize>
+    val selectedClockSize: StateFlow<SettingsClockSize>
 
     /** clock id, selected from clock carousel in wallpaper picker */
     val currentClockId: Flow<ClockId>
 
     val currentClock: StateFlow<ClockController?>
 
-    val previewClockPair: StateFlow<Pair<ClockController, ClockController>>
+    val previewClock: Flow<ClockController>
 
     val clockEventController: ClockEventController
     fun setClockSize(@ClockSize size: Int)
@@ -84,14 +83,19 @@
         _clockSize.value = size
     }
 
-    override val selectedClockSize: Flow<SettingsClockSize> =
+    override val selectedClockSize: StateFlow<SettingsClockSize> =
         secureSettings
             .observerFlow(
                 names = arrayOf(Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK),
                 userId = UserHandle.USER_SYSTEM,
             )
             .onStart { emit(Unit) } // Forces an initial update.
-            .map { getClockSize() }
+            .map { withContext(backgroundDispatcher) { getClockSize() } }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = getClockSize()
+            )
 
     override val currentClockId: Flow<ClockId> =
         callbackFlow {
@@ -113,37 +117,35 @@
 
     override val currentClock: StateFlow<ClockController?> =
         currentClockId
-            .map { clockRegistry.createCurrentClock() }
+            .map {
+                clockEventController.clock = clockRegistry.createCurrentClock()
+                clockEventController.clock
+            }
             .stateIn(
                 scope = applicationScope,
                 started = SharingStarted.WhileSubscribed(),
                 initialValue = clockRegistry.createCurrentClock()
             )
 
-    override val previewClockPair: StateFlow<Pair<ClockController, ClockController>> =
-        currentClockId
-            .map { Pair(clockRegistry.createCurrentClock(), clockRegistry.createCurrentClock()) }
-            .stateIn(
-                scope = applicationScope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue =
-                    Pair(clockRegistry.createCurrentClock(), clockRegistry.createCurrentClock())
-            )
+    override val previewClock: Flow<ClockController> =
+        currentClockId.map {
+            // We should create a new instance for each collect call
+            // cause in preview, the same clock will be attached to different view
+            // at the same time
+            clockRegistry.createCurrentClock()
+        }
 
-    @VisibleForTesting
-    suspend fun getClockSize(): SettingsClockSize {
-        return withContext(backgroundDispatcher) {
-            if (
-                secureSettings.getIntForUser(
-                    Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK,
-                    1,
-                    UserHandle.USER_CURRENT
-                ) == 1
-            ) {
-                SettingsClockSize.DYNAMIC
-            } else {
-                SettingsClockSize.SMALL
-            }
+    private fun getClockSize(): SettingsClockSize {
+        return if (
+            secureSettings.getIntForUser(
+                Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK,
+                1,
+                UserHandle.USER_CURRENT
+            ) == 1
+        ) {
+            SettingsClockSize.DYNAMIC
+        } else {
+            SettingsClockSize.SMALL
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
index 9c68c45..a36bf8b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
@@ -119,24 +119,7 @@
     init {
         // Seed with transitions signaling a boot into lockscreen state. If updating this, please
         // also update FakeKeyguardTransitionRepository.
-        emitTransition(
-            TransitionStep(
-                KeyguardState.OFF,
-                KeyguardState.LOCKSCREEN,
-                0f,
-                TransitionState.STARTED,
-                KeyguardTransitionRepositoryImpl::class.simpleName!!,
-            )
-        )
-        emitTransition(
-            TransitionStep(
-                KeyguardState.OFF,
-                KeyguardState.LOCKSCREEN,
-                1f,
-                TransitionState.FINISHED,
-                KeyguardTransitionRepositoryImpl::class.simpleName!!,
-            )
-        )
+        initialTransitionSteps.forEach(::emitTransition)
     }
 
     override fun startTransition(info: TransitionInfo): UUID? {
@@ -256,5 +239,31 @@
 
     companion object {
         private const val TAG = "KeyguardTransitionRepository"
+
+        /**
+         * Transition steps to seed the repository with, so that all of the transition interactor
+         * flows emit reasonable initial values.
+         */
+        val initialTransitionSteps: List<TransitionStep> =
+            listOf(
+                TransitionStep(
+                    KeyguardState.OFF,
+                    KeyguardState.OFF,
+                    1f,
+                    TransitionState.FINISHED,
+                ),
+                TransitionStep(
+                    KeyguardState.OFF,
+                    KeyguardState.LOCKSCREEN,
+                    0f,
+                    TransitionState.STARTED,
+                ),
+                TransitionStep(
+                    KeyguardState.OFF,
+                    KeyguardState.LOCKSCREEN,
+                    1f,
+                    TransitionState.FINISHED,
+                ),
+            )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
index 9040e03..d09ee54 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
@@ -252,5 +252,6 @@
         val TO_LOCKSCREEN_DURATION = 500.milliseconds
         val TO_GONE_DURATION = DEFAULT_DURATION
         val TO_OCCLUDED_DURATION = DEFAULT_DURATION
+        val TO_PRIMARY_BOUNCER_DURATION = DEFAULT_DURATION
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
index 9a6088d..1f24fc2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
@@ -231,5 +231,7 @@
         private val DEFAULT_DURATION = 500.milliseconds
         val TO_GLANCEABLE_HUB_DURATION = 1.seconds
         val TO_LOCKSCREEN_DURATION = 1167.milliseconds
+        val TO_AOD_DURATION = 300.milliseconds
+        val TO_GONE_DURATION = DEFAULT_DURATION
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBottomAreaInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBottomAreaInteractor.kt
index b9ec58c..53f2416 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBottomAreaInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBottomAreaInteractor.kt
@@ -39,7 +39,7 @@
     /** The position of the keyguard clock. */
     private val _clockPosition = MutableStateFlow(Position(0, 0))
     /** See [ClockSection] */
-    @Deprecated("with migrateClocksToBlueprint()")
+    @Deprecated("with MigrateClocksToBlueprint.isEnabled")
     val clockPosition: Flow<Position> = _clockPosition.asStateFlow()
 
     fun setClockPosition(x: Int, y: Int) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt
index 2cf9156..d492135 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt
@@ -38,14 +38,13 @@
     private val keyguardClockRepository: KeyguardClockRepository,
 ) {
 
-    val selectedClockSize: Flow<SettingsClockSize> = keyguardClockRepository.selectedClockSize
+    val selectedClockSize: StateFlow<SettingsClockSize> = keyguardClockRepository.selectedClockSize
 
     val currentClockId: Flow<ClockId> = keyguardClockRepository.currentClockId
 
     val currentClock: StateFlow<ClockController?> = keyguardClockRepository.currentClock
 
-    val previewClockPair: StateFlow<Pair<ClockController, ClockController>> =
-        keyguardClockRepository.previewClockPair
+    val previewClock: Flow<ClockController> = keyguardClockRepository.previewClock
 
     var clock: ClockController? by keyguardClockRepository.clockEventController::clock
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
index e6655ee..0cd7d18 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -91,11 +91,46 @@
         }
     }
 
+    val transitions = repository.transitions
+
+    /**
+     * A pair of the most recent STARTED step, and the transition step immediately preceding it. The
+     * transition framework enforces that the previous step is either a CANCELED or FINISHED step,
+     * and that the previous step was *to* the state the STARTED step is *from*.
+     *
+     * This flow can be used to access the previous step to determine whether it was CANCELED or
+     * FINISHED. In the case of a CANCELED step, we can also figure out which state we were coming
+     * from when we were canceled.
+     */
+    val startedStepWithPrecedingStep =
+        transitions
+            .pairwise()
+            .filter { it.newValue.transitionState == TransitionState.STARTED }
+            .shareIn(scope, SharingStarted.Eagerly)
+
     init {
+        // Collect non-canceled steps and emit transition values.
         scope.launch(mainDispatcher) {
-            repository.transitions.collect { step ->
-                getTransitionValueFlow(step.from).emit(1f - step.value)
-                getTransitionValueFlow(step.to).emit(step.value)
+            repository.transitions
+                .filter { it.transitionState != TransitionState.CANCELED }
+                .collect { step ->
+                    getTransitionValueFlow(step.from).emit(1f - step.value)
+                    getTransitionValueFlow(step.to).emit(step.value)
+                }
+        }
+
+        // If a transition from state A -> B is canceled in favor of a transition from B -> C, we
+        // need to ensure we emit transitionValue(A) = 0f, since no further steps will be emitted
+        // where the from or to states are A. This would leave transitionValue(A) stuck at an
+        // arbitrary non-zero value.
+        scope.launch(mainDispatcher) {
+            startedStepWithPrecedingStep.collect { (prevStep, startedStep) ->
+                if (
+                    prevStep.transitionState == TransitionState.CANCELED &&
+                        startedStep.to != prevStep.from
+                ) {
+                    getTransitionValueFlow(prevStep.from).emit(0f)
+                }
             }
         }
     }
@@ -202,8 +237,6 @@
     val dozingToLockscreenTransition: Flow<TransitionStep> =
         repository.transition(DOZING, LOCKSCREEN)
 
-    val transitions = repository.transitions
-
     /** Receive all [TransitionStep] matching a filter of [from]->[to] */
     fun transition(from: KeyguardState, to: KeyguardState): Flow<TransitionStep> {
         return repository.transition(from, to)
@@ -250,21 +283,6 @@
             .stateIn(scope, SharingStarted.Eagerly, DOZING)
 
     /**
-     * A pair of the most recent STARTED step, and the transition step immediately preceding it. The
-     * transition framework enforces that the previous step is either a CANCELED or FINISHED step,
-     * and that the previous step was *to* the state the STARTED step is *from*.
-     *
-     * This flow can be used to access the previous step to determine whether it was CANCELED or
-     * FINISHED. In the case of a CANCELED step, we can also figure out which state we were coming
-     * from when we were canceled.
-     */
-    val startedStepWithPrecedingStep =
-        transitions
-            .pairwise()
-            .filter { it.newValue.transitionState == TransitionState.STARTED }
-            .stateIn(scope, SharingStarted.Eagerly, null)
-
-    /**
      * The last [KeyguardState] to which we [TransitionState.FINISHED] a transition.
      *
      * WARNING: This will NOT emit a value if a transition is CANCELED, and will also not emit a
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
index 4812e03..7e3ddf9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
@@ -26,9 +26,9 @@
 import androidx.constraintlayout.widget.ConstraintSet
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
-import com.android.systemui.Flags.keyguardBottomAreaRefactor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
 import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.BaseBlueprintTransition
 import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition
 import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Config
@@ -105,7 +105,7 @@
 
                         var transition =
                             if (
-                                !keyguardBottomAreaRefactor() &&
+                                !KeyguardBottomAreaRefactor.isEnabled &&
                                     prevBluePrint != null &&
                                     prevBluePrint != blueprint
                             ) {
@@ -213,9 +213,10 @@
         cs: ConstraintSet,
         viewModel: KeyguardClockViewModel
     ) {
-        if (!DEBUG || viewModel.clock == null) return
+        val currentClock = viewModel.currentClock.value
+        if (!DEBUG || currentClock == null) return
         val smallClockViewId = R.id.lockscreen_clock_view
-        val largeClockViewId = viewModel.clock!!.largeClock.layout.views[0].id
+        val largeClockViewId = currentClock.largeClock.layout.views[0].id
         Log.i(
             TAG,
             "applyCsToSmallClock: vis=${cs.getVisibility(smallClockViewId)} " +
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
index 01596ed..6255f0d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
@@ -19,6 +19,7 @@
 import android.transition.TransitionManager
 import android.transition.TransitionSet
 import android.view.View.INVISIBLE
+import android.view.ViewGroup
 import androidx.annotation.VisibleForTesting
 import androidx.constraintlayout.helper.widget.Layer
 import androidx.constraintlayout.widget.ConstraintLayout
@@ -27,7 +28,7 @@
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.keyguard.KeyguardClockSwitch.LARGE
 import com.android.keyguard.KeyguardClockSwitch.SMALL
-import com.android.systemui.Flags.migrateClocksToBlueprint
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Type
@@ -40,7 +41,8 @@
 
 object KeyguardClockViewBinder {
     private val TAG = KeyguardClockViewBinder::class.simpleName!!
-
+    // When changing to new clock, we need to remove old clock views from burnInLayer
+    private var lastClock: ClockController? = null
     @JvmStatic
     fun bind(
         clockSection: ClockSection,
@@ -55,28 +57,27 @@
             }
         }
         keyguardRootView.repeatWhenAttached {
-            repeatOnLifecycle(Lifecycle.State.STARTED) {
+            repeatOnLifecycle(Lifecycle.State.CREATED) {
                 launch {
-                    if (!migrateClocksToBlueprint()) return@launch
+                    if (!MigrateClocksToBlueprint.isEnabled) return@launch
                     viewModel.currentClock.collect { currentClock ->
-                        cleanupClockViews(viewModel.clock, keyguardRootView, viewModel.burnInLayer)
-                        viewModel.clock = currentClock
+                        cleanupClockViews(currentClock, keyguardRootView, viewModel.burnInLayer)
                         addClockViews(currentClock, keyguardRootView)
                         updateBurnInLayer(keyguardRootView, viewModel)
                         applyConstraints(clockSection, keyguardRootView, true)
                     }
                 }
                 launch {
-                    if (!migrateClocksToBlueprint()) return@launch
+                    if (!MigrateClocksToBlueprint.isEnabled) return@launch
                     viewModel.clockSize.collect {
                         updateBurnInLayer(keyguardRootView, viewModel)
                         blueprintInteractor.refreshBlueprint(Type.ClockSize)
                     }
                 }
                 launch {
-                    if (!migrateClocksToBlueprint()) return@launch
+                    if (!MigrateClocksToBlueprint.isEnabled) return@launch
                     viewModel.clockShouldBeCentered.collect { clockShouldBeCentered ->
-                        viewModel.clock?.let {
+                        viewModel.currentClock.value?.let {
                             // Weather clock also has hasCustomPositionUpdatedAnimation as true
                             // TODO(b/323020908): remove ID check
                             if (
@@ -91,9 +92,9 @@
                     }
                 }
                 launch {
-                    if (!migrateClocksToBlueprint()) return@launch
+                    if (!MigrateClocksToBlueprint.isEnabled) return@launch
                     viewModel.isAodIconsVisible.collect { isAodIconsVisible ->
-                        viewModel.clock?.let {
+                        viewModel.currentClock.value?.let {
                             // Weather clock also has hasCustomPositionUpdatedAnimation as true
                             if (
                                 viewModel.useLargeClock && it.config.id == "DIGITAL_CLOCK_WEATHER"
@@ -132,11 +133,14 @@
     }
 
     private fun cleanupClockViews(
-        clockController: ClockController?,
+        currentClock: ClockController?,
         rootView: ConstraintLayout,
         burnInLayer: Layer?
     ) {
-        clockController?.let { clock ->
+        if (lastClock == currentClock) {
+            return
+        }
+        lastClock?.let { clock ->
             clock.smallClock.layout.views.forEach {
                 burnInLayer?.removeView(it)
                 rootView.removeView(it)
@@ -150,6 +154,7 @@
             }
             clock.largeClock.layout.views.forEach { rootView.removeView(it) }
         }
+        lastClock = currentClock
     }
 
     @VisibleForTesting
@@ -157,11 +162,19 @@
         clockController: ClockController?,
         rootView: ConstraintLayout,
     ) {
+        // We'll collect the same clock when exiting wallpaper picker without changing clock
+        // so we need to remove clock views from parent before addView again
         clockController?.let { clock ->
             clock.smallClock.layout.views.forEach {
+                if (it.parent != null) {
+                    (it.parent as ViewGroup).removeView(it)
+                }
                 rootView.addView(it).apply { it.visibility = INVISIBLE }
             }
             clock.largeClock.layout.views.forEach {
+                if (it.parent != null) {
+                    (it.parent as ViewGroup).removeView(it)
+                }
                 rootView.addView(it).apply { it.visibility = INVISIBLE }
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt
index 841f52d..267d68e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt
@@ -22,8 +22,8 @@
 import android.widget.TextView
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
-import com.android.systemui.Flags.keyguardBottomAreaRefactor
-import com.android.systemui.Flags.migrateClocksToBlueprint
+import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.res.R
@@ -69,7 +69,10 @@
                     launch {
                         // Do not independently apply alpha, as [KeyguardRootViewModel] should work
                         // for this and all its children
-                        if (!(migrateClocksToBlueprint() || keyguardBottomAreaRefactor())) {
+                        if (
+                            !(MigrateClocksToBlueprint.isEnabled ||
+                                KeyguardBottomAreaRefactor.isEnabled)
+                        ) {
                             viewModel.alpha.collect { alpha -> view.alpha = alpha }
                         }
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt
index 46c354a..d9f12c3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt
@@ -32,7 +32,6 @@
 import androidx.core.view.isVisible
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
-import com.android.keyguard.ClockEventController
 import com.android.systemui.customization.R as customizationR
 import com.android.systemui.keyguard.shared.model.SettingsClockSize
 import com.android.systemui.keyguard.ui.preview.KeyguardPreviewRenderer
@@ -44,12 +43,10 @@
 import com.android.systemui.res.R
 import com.android.systemui.util.Utils
 import kotlin.reflect.KSuspendFunction1
-import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.launch
 
 /** Binder for the small clock view, large clock view. */
 object KeyguardPreviewClockViewBinder {
-
     @JvmStatic
     fun bind(
         largeClockHostView: View,
@@ -72,52 +69,38 @@
     @JvmStatic
     fun bind(
         context: Context,
-        displayId: Int,
         rootView: ConstraintLayout,
         viewModel: KeyguardPreviewClockViewModel,
-        clockEventController: ClockEventController,
         updateClockAppearance: KSuspendFunction1<ClockController, Unit>,
     ) {
-        // TODO(b/327668072): When this function is called multiple times, the clock view can be
-        //                    gone due to a race condition on removeView and addView.
         rootView.repeatWhenAttached {
             repeatOnLifecycle(Lifecycle.State.STARTED) {
                 launch {
-                    combine(viewModel.selectedClockSize, viewModel.previewClockPair) { _, clock ->
-                            clock
+                    var lastClock: ClockController? = null
+                    viewModel.previewClock.collect { currentClock ->
+                        lastClock?.let { clock ->
+                            (clock.largeClock.layout.views + clock.smallClock.layout.views)
+                                .forEach { rootView.removeView(it) }
                         }
-                        .collect { previewClockPair ->
-                            viewModel.lastClockPair?.let { clockPair ->
-                                (clockPair.first.largeClock.layout.views +
-                                        clockPair.first.smallClock.layout.views)
-                                    .forEach { rootView.removeView(it) }
-                                (clockPair.second.largeClock.layout.views +
-                                        clockPair.second.smallClock.layout.views)
-                                    .forEach { rootView.removeView(it) }
-                            }
-                            viewModel.lastClockPair = previewClockPair
-                            val clockPreview =
-                                if (displayId == 0) previewClockPair.first
-                                else previewClockPair.second
-                            clockEventController.clock = clockPreview
-                            updateClockAppearance(clockPreview)
+                        lastClock = currentClock
+                        updateClockAppearance(currentClock)
 
-                            if (viewModel.shouldHighlightSelectedAffordance) {
-                                (clockPreview.largeClock.layout.views +
-                                        clockPreview.smallClock.layout.views)
-                                    .forEach { it.alpha = KeyguardPreviewRenderer.DIM_ALPHA }
-                            }
-                            clockPreview.largeClock.layout.views.forEach {
-                                (it.parent as? ViewGroup)?.removeView(it)
-                                rootView.addView(it)
-                            }
-
-                            clockPreview.smallClock.layout.views.forEach {
-                                (it.parent as? ViewGroup)?.removeView(it)
-                                rootView.addView(it)
-                            }
-                            applyPreviewConstraints(context, rootView, viewModel)
+                        if (viewModel.shouldHighlightSelectedAffordance) {
+                            (currentClock.largeClock.layout.views +
+                                    currentClock.smallClock.layout.views)
+                                .forEach { it.alpha = KeyguardPreviewRenderer.DIM_ALPHA }
                         }
+                        currentClock.largeClock.layout.views.forEach {
+                            (it.parent as? ViewGroup)?.removeView(it)
+                            rootView.addView(it)
+                        }
+
+                        currentClock.smallClock.layout.views.forEach {
+                            (it.parent as? ViewGroup)?.removeView(it)
+                            rootView.addView(it)
+                        }
+                        applyPreviewConstraints(context, rootView, currentClock, viewModel)
+                    }
                 }
             }
         }
@@ -170,15 +153,13 @@
     private fun applyPreviewConstraints(
         context: Context,
         rootView: ConstraintLayout,
+        previewClock: ClockController,
         viewModel: KeyguardPreviewClockViewModel
     ) {
         val cs = ConstraintSet().apply { clone(rootView) }
-        val clockPair = viewModel.previewClockPair.value
         applyClockDefaultConstraints(context, cs)
-        clockPair.first.largeClock.layout.applyPreviewConstraints(cs)
-        clockPair.first.smallClock.layout.applyPreviewConstraints(cs)
-        clockPair.second.largeClock.layout.applyPreviewConstraints(cs)
-        clockPair.second.smallClock.layout.applyPreviewConstraints(cs)
+        previewClock.largeClock.layout.applyPreviewConstraints(cs)
+        previewClock.smallClock.layout.applyPreviewConstraints(cs)
 
         // When selectedClockSize is the initial value, make both clocks invisible to avoid
         // flickering
@@ -194,12 +175,9 @@
                 SettingsClockSize.SMALL -> VISIBLE
                 null -> INVISIBLE
             }
-
         cs.apply {
-            setVisibility(clockPair.first.largeClock.layout.views, largeClockVisibility)
-            setVisibility(clockPair.first.smallClock.layout.views, smallClockVisibility)
-            setVisibility(clockPair.second.largeClock.layout.views, largeClockVisibility)
-            setVisibility(clockPair.second.smallClock.layout.views, smallClockVisibility)
+            setVisibility(previewClock.largeClock.layout.views, largeClockVisibility)
+            setVisibility(previewClock.smallClock.layout.views, smallClockVisibility)
         }
         cs.applyTo(rootView)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
index d0246a8..0ed42ef 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
@@ -36,8 +36,6 @@
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD
 import com.android.keyguard.KeyguardClockSwitch.MISSING_CLOCK_ID
-import com.android.systemui.Flags.keyguardBottomAreaRefactor
-import com.android.systemui.Flags.migrateClocksToBlueprint
 import com.android.systemui.Flags.newAodTransition
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.shared.model.Text
@@ -45,6 +43,8 @@
 import com.android.systemui.common.ui.ConfigurationState
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor
 import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
+import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
@@ -109,7 +109,7 @@
         val endButton = R.id.end_button
         val lockIcon = R.id.lock_icon_view
 
-        if (keyguardBottomAreaRefactor()) {
+        if (KeyguardBottomAreaRefactor.isEnabled) {
             view.setOnTouchListener { _, event ->
                 if (falsingManager?.isFalseTap(FalsingManager.LOW_PENALTY) == false) {
                     viewModel.setRootViewLastTapPosition(Point(event.x.toInt(), event.y.toInt()))
@@ -143,11 +143,13 @@
                         }
                     }
 
-                    if (keyguardBottomAreaRefactor() || DeviceEntryUdfpsRefactor.isEnabled) {
+                    if (
+                        KeyguardBottomAreaRefactor.isEnabled || DeviceEntryUdfpsRefactor.isEnabled
+                    ) {
                         launch {
                             viewModel.alpha(viewState).collect { alpha ->
                                 view.alpha = alpha
-                                if (keyguardBottomAreaRefactor()) {
+                                if (KeyguardBottomAreaRefactor.isEnabled) {
                                     childViews[statusViewId]?.alpha = alpha
                                     childViews[burnInLayerId]?.alpha = alpha
                                 }
@@ -155,7 +157,7 @@
                         }
                     }
 
-                    if (migrateClocksToBlueprint()) {
+                    if (MigrateClocksToBlueprint.isEnabled) {
                         launch {
                             viewModel.burnInLayerVisibility.collect { visibility ->
                                 childViews[burnInLayerId]?.visibility = visibility
@@ -342,13 +344,13 @@
                 }
             }
 
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled) {
             burnInParams.update { current ->
                 current.copy(clockControllerProvider = clockControllerProvider)
             }
         }
 
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled) {
             burnInParams.update { current ->
                 current.copy(translationY = { childViews[burnInLayerId]?.translationY })
             }
@@ -439,7 +441,7 @@
             burnInParams.update { current ->
                 current.copy(
                     minViewY =
-                        if (migrateClocksToBlueprint()) {
+                        if (MigrateClocksToBlueprint.isEnabled) {
                             // To ensure burn-in doesn't enroach the top inset, get the min top Y
                             childViews.entries.fold(Int.MAX_VALUE) { currentMin, (viewId, view) ->
                                 min(
@@ -472,7 +474,7 @@
         configuration: ConfigurationState,
         screenOffAnimationController: ScreenOffAnimationController,
     ) {
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled) {
             throw IllegalStateException("should only be called in legacy code paths")
         }
         if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return
@@ -503,7 +505,7 @@
             }
         when {
             !isVisible.isAnimating -> {
-                if (!migrateClocksToBlueprint()) {
+                if (!MigrateClocksToBlueprint.isEnabled) {
                     translationY = 0f
                 }
                 visibility =
@@ -553,7 +555,7 @@
         animatorListener: Animator.AnimatorListener,
     ) {
         if (animate) {
-            if (!migrateClocksToBlueprint()) {
+            if (!MigrateClocksToBlueprint.isEnabled) {
                 translationY = -iconAppearTranslation.toFloat()
             }
             alpha = 0f
@@ -561,19 +563,19 @@
                 .alpha(1f)
                 .setInterpolator(Interpolators.LINEAR)
                 .setDuration(AOD_ICONS_APPEAR_DURATION)
-                .apply { if (migrateClocksToBlueprint()) animateInIconTranslation() }
+                .apply { if (MigrateClocksToBlueprint.isEnabled) animateInIconTranslation() }
                 .setListener(animatorListener)
                 .start()
         } else {
             alpha = 1.0f
-            if (!migrateClocksToBlueprint()) {
+            if (!MigrateClocksToBlueprint.isEnabled) {
                 translationY = 0f
             }
         }
     }
 
     private fun View.animateInIconTranslation() {
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled) {
             animate().animateInIconTranslation().setDuration(AOD_ICONS_APPEAR_DURATION).start()
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
index b77f0c5..9aebf66 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
@@ -21,7 +21,7 @@
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
-import com.android.systemui.Flags.migrateClocksToBlueprint
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
 import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Config
 import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Type
@@ -41,9 +41,9 @@
         blueprintInteractor: KeyguardBlueprintInteractor,
     ) {
         keyguardRootView.repeatWhenAttached {
-            repeatOnLifecycle(Lifecycle.State.STARTED) {
+            repeatOnLifecycle(Lifecycle.State.CREATED) {
                 launch {
-                    if (!migrateClocksToBlueprint()) return@launch
+                    if (!MigrateClocksToBlueprint.isEnabled) return@launch
                     clockViewModel.hasCustomWeatherDataDisplay.collect { hasCustomWeatherDataDisplay
                         ->
                         updateDateWeatherToBurnInLayer(
@@ -62,7 +62,7 @@
                 }
 
                 launch {
-                    if (!migrateClocksToBlueprint()) return@launch
+                    if (!MigrateClocksToBlueprint.isEnabled) return@launch
                     smartspaceViewModel.bcSmartspaceVisibility.collect {
                         updateBCSmartspaceInBurnInLayer(keyguardRootView, clockViewModel)
                         blueprintInteractor.refreshBlueprint(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
index 7c76e6a..14ab17f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
@@ -50,8 +50,6 @@
 import androidx.core.view.isInvisible
 import com.android.keyguard.ClockEventController
 import com.android.keyguard.KeyguardClockSwitch
-import com.android.systemui.Flags.keyguardBottomAreaRefactor
-import com.android.systemui.Flags.migrateClocksToBlueprint
 import com.android.systemui.animation.view.LaunchableImageView
 import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
 import com.android.systemui.broadcast.BroadcastDispatcher
@@ -61,6 +59,8 @@
 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.keyguard.KeyguardBottomAreaRefactor
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.keyguard.ui.binder.KeyguardPreviewClockViewBinder
 import com.android.systemui.keyguard.ui.binder.KeyguardPreviewSmartspaceViewBinder
 import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder
@@ -90,6 +90,7 @@
 import com.android.systemui.statusbar.phone.KeyguardBottomAreaView
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController
 import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
+import com.android.systemui.util.kotlin.DisposableHandles
 import com.android.systemui.util.settings.SecureSettings
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedInject
@@ -173,7 +174,7 @@
     private lateinit var smallClockHostView: FrameLayout
     private var smartSpaceView: View? = null
 
-    private val disposables = mutableSetOf<DisposableHandle>()
+    private val disposables = DisposableHandles()
     private var isDestroyed = false
 
     private val shortcutsBindings = mutableSetOf<KeyguardQuickAffordanceViewBinder.Binding>()
@@ -183,9 +184,9 @@
 
     init {
         coroutineScope = CoroutineScope(applicationScope.coroutineContext + Job())
-        disposables.add(DisposableHandle { coroutineScope.cancel() })
+        disposables += DisposableHandle { coroutineScope.cancel() }
 
-        if (keyguardBottomAreaRefactor()) {
+        if (KeyguardBottomAreaRefactor.isEnabled) {
             quickAffordancesCombinedViewModel.enablePreviewMode(
                 initiallySelectedSlotId =
                     bundle.getString(
@@ -203,7 +204,7 @@
                 shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance,
             )
         }
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled) {
             clockViewModel.shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance
         }
         runBlocking(mainDispatcher) {
@@ -214,7 +215,7 @@
                     if (hostToken == null) null else InputTransferToken(hostToken),
                     "KeyguardPreviewRenderer"
                 )
-            disposables.add(DisposableHandle { host.release() })
+            disposables += DisposableHandle { host.release() }
         }
     }
 
@@ -230,7 +231,7 @@
 
             setupKeyguardRootView(previewContext, rootView)
 
-            if (!keyguardBottomAreaRefactor()) {
+            if (!KeyguardBottomAreaRefactor.isEnabled) {
                 setUpBottomArea(rootView)
             }
 
@@ -274,7 +275,7 @@
     }
 
     fun onSlotSelected(slotId: String) {
-        if (keyguardBottomAreaRefactor()) {
+        if (KeyguardBottomAreaRefactor.isEnabled) {
             quickAffordancesCombinedViewModel.onPreviewSlotSelected(slotId = slotId)
         } else {
             bottomAreaViewModel.onPreviewSlotSelected(slotId = slotId)
@@ -284,8 +285,8 @@
     fun destroy() {
         isDestroyed = true
         lockscreenSmartspaceController.disconnect()
-        disposables.forEach { it.dispose() }
-        if (keyguardBottomAreaRefactor()) {
+        disposables.dispose()
+        if (KeyguardBottomAreaRefactor.isEnabled) {
             shortcutsBindings.forEach { it.destroy() }
         }
     }
@@ -371,8 +372,8 @@
     @OptIn(ExperimentalCoroutinesApi::class)
     private fun setupKeyguardRootView(previewContext: Context, rootView: FrameLayout) {
         val keyguardRootView = KeyguardRootView(previewContext, null)
-        if (!keyguardBottomAreaRefactor()) {
-            disposables.add(
+        if (!KeyguardBottomAreaRefactor.isEnabled) {
+            disposables +=
                 KeyguardRootViewBinder.bind(
                     keyguardRootView,
                     keyguardRootViewModel,
@@ -387,7 +388,6 @@
                     null, // device entry haptics not required for preview mode
                     null, // falsing manager not required for preview mode
                 )
-            )
         }
         rootView.addView(
             keyguardRootView,
@@ -397,21 +397,22 @@
             ),
         )
 
-        setUpUdfps(previewContext, if (migrateClocksToBlueprint()) keyguardRootView else rootView)
+        setUpUdfps(
+            previewContext,
+            if (MigrateClocksToBlueprint.isEnabled) keyguardRootView else rootView
+        )
 
-        if (keyguardBottomAreaRefactor()) {
+        if (KeyguardBottomAreaRefactor.isEnabled) {
             setupShortcuts(keyguardRootView)
         }
 
         if (!shouldHideClock) {
             setUpClock(previewContext, rootView)
-            if (migrateClocksToBlueprint()) {
+            if (MigrateClocksToBlueprint.isEnabled) {
                 KeyguardPreviewClockViewBinder.bind(
                     context,
-                    displayId,
                     keyguardRootView,
                     clockViewModel,
-                    clockController,
                     ::updateClockAppearance
                 )
             } else {
@@ -482,7 +483,7 @@
                 ) as View
 
         // Place the UDFPS view in the proper sensor location
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled) {
             finger.id = R.id.lock_icon_view
             parentView.addView(finger)
             val cs = ConstraintSet()
@@ -509,7 +510,7 @@
 
     private fun setUpClock(previewContext: Context, parentView: ViewGroup) {
         val resources = parentView.resources
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled) {
             largeClockHostView = FrameLayout(previewContext)
             largeClockHostView.layoutParams =
                 FrameLayout.LayoutParams(
@@ -547,7 +548,7 @@
         }
 
         // TODO (b/283465254): Move the listeners to KeyguardClockRepository
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled) {
             val clockChangeListener =
                 object : ClockRegistry.ClockChangeListener {
                     override fun onCurrentClockChanged() {
@@ -555,14 +556,12 @@
                     }
                 }
             clockRegistry.registerClockChangeListener(clockChangeListener)
-            disposables.add(
-                DisposableHandle {
-                    clockRegistry.unregisterClockChangeListener(clockChangeListener)
-                }
-            )
+            disposables += DisposableHandle {
+                clockRegistry.unregisterClockChangeListener(clockChangeListener)
+            }
 
             clockController.registerListeners(parentView)
-            disposables.add(DisposableHandle { clockController.unregisterListeners() })
+            disposables += DisposableHandle { clockController.unregisterListeners() }
         }
 
         val receiver =
@@ -581,9 +580,9 @@
                 addAction(Intent.ACTION_TIME_CHANGED)
             },
         )
-        disposables.add(DisposableHandle { broadcastDispatcher.unregisterReceiver(receiver) })
+        disposables += DisposableHandle { broadcastDispatcher.unregisterReceiver(receiver) }
 
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled) {
             val layoutChangeListener =
                 View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
                     if (clockController.clock !is DefaultClockController) {
@@ -602,9 +601,9 @@
                     }
                 }
             parentView.addOnLayoutChangeListener(layoutChangeListener)
-            disposables.add(
-                DisposableHandle { parentView.removeOnLayoutChangeListener(layoutChangeListener) }
-            )
+            disposables += DisposableHandle {
+                parentView.removeOnLayoutChangeListener(layoutChangeListener)
+            }
         }
 
         onClockChanged()
@@ -631,7 +630,7 @@
         }
     }
     private fun onClockChanged() {
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled) {
             return
         }
         coroutineScope.launch {
@@ -678,7 +677,7 @@
     }
 
     private fun updateLargeClock(clock: ClockController) {
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled) {
             return
         }
         clock.largeClock.events.onTargetRegionChanged(
@@ -692,7 +691,7 @@
     }
 
     private fun updateSmallClock(clock: ClockController) {
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled) {
             return
         }
         clock.smallClock.events.onTargetRegionChanged(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt
index f20c4ac..3b21141 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt
@@ -22,10 +22,12 @@
 import com.android.systemui.keyguard.ui.viewmodel.AodToGoneTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.AodToLockscreenTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.AodToOccludedTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.AodToPrimaryBouncerTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.DozingToGoneTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.DozingToLockscreenTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.DozingToOccludedTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.DozingToPrimaryBouncerTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToAodTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.GoneToAodTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.GoneToDozingTransitionViewModel
@@ -89,6 +91,12 @@
 
     @Binds
     @IntoSet
+    abstract fun aodToPrimaryBouncer(
+        impl: AodToPrimaryBouncerTransitionViewModel
+    ): DeviceEntryIconTransition
+
+    @Binds
+    @IntoSet
     abstract fun dozingToGone(impl: DozingToGoneTransitionViewModel): DeviceEntryIconTransition
 
     @Binds
@@ -111,6 +119,10 @@
 
     @Binds
     @IntoSet
+    abstract fun dreamingToAod(impl: DreamingToAodTransitionViewModel): DeviceEntryIconTransition
+
+    @Binds
+    @IntoSet
     abstract fun dreamingToLockscreen(
         impl: DreamingToLockscreenTransitionViewModel
     ): DeviceEntryIconTransition
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt
index 9c9df80..a215efa 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt
@@ -41,7 +41,7 @@
 
     private fun excludeClockAndSmartspaceViews(transition: Transition) {
         transition.excludeTarget(SmartspaceView::class.java, true)
-        clockViewModel.clock?.let { clock ->
+        clockViewModel.currentClock.value?.let { clock ->
             clock.largeClock.layout.views.forEach { view -> transition.excludeTarget(view, true) }
             clock.smallClock.layout.views.forEach { view -> transition.excludeTarget(view, true) }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt
index 3adeb2a..c69d868 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt
@@ -57,7 +57,9 @@
         when (config.type) {
             Type.NoTransition -> {}
             Type.DefaultClockStepping ->
-                addTransition(clockViewModel.clock?.let { DefaultClockSteppingTransition(it) })
+                addTransition(
+                    clockViewModel.currentClock.value?.let { DefaultClockSteppingTransition(it) }
+                )
             else -> addTransition(ClockSizeTransition(config, clockViewModel, smartspaceViewModel))
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt
index cd46d6c..2e96638 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt
@@ -25,9 +25,9 @@
 import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
 import androidx.constraintlayout.widget.ConstraintSet.RIGHT
 import androidx.constraintlayout.widget.ConstraintSet.TOP
-import com.android.systemui.Flags.keyguardBottomAreaRefactor
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
+import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
 import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
@@ -49,14 +49,14 @@
     private val vibratorHelper: VibratorHelper,
 ) : BaseShortcutSection() {
     override fun addViews(constraintLayout: ConstraintLayout) {
-        if (keyguardBottomAreaRefactor()) {
+        if (KeyguardBottomAreaRefactor.isEnabled) {
             addLeftShortcut(constraintLayout)
             addRightShortcut(constraintLayout)
         }
     }
 
     override fun bindData(constraintLayout: ConstraintLayout) {
-        if (keyguardBottomAreaRefactor()) {
+        if (KeyguardBottomAreaRefactor.isEnabled) {
             leftShortcutHandle =
                 KeyguardQuickAffordanceViewBinder.bind(
                     constraintLayout.requireViewById(R.id.start_button),
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt
index 88ce9dc..d639978 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt
@@ -23,7 +23,7 @@
 import androidx.constraintlayout.widget.ConstraintSet
 import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
 import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
-import com.android.systemui.Flags.migrateClocksToBlueprint
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.keyguard.shared.model.KeyguardSection
 import com.android.systemui.keyguard.ui.view.KeyguardRootView
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
@@ -47,7 +47,7 @@
         }
     }
     override fun addViews(constraintLayout: ConstraintLayout) {
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled) {
             return
         }
 
@@ -62,14 +62,14 @@
     }
 
     override fun bindData(constraintLayout: ConstraintLayout) {
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled) {
             return
         }
         clockViewModel.burnInLayer = burnInLayer
     }
 
     override fun applyConstraints(constraintSet: ConstraintSet) {
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled) {
             return
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt
index 3d9c04e..2832e9d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt
@@ -26,8 +26,8 @@
 import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
 import androidx.constraintlayout.widget.ConstraintSet.START
 import androidx.constraintlayout.widget.ConstraintSet.TOP
-import com.android.systemui.Flags.migrateClocksToBlueprint
 import com.android.systemui.common.ui.ConfigurationState
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.keyguard.shared.model.KeyguardSection
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.AlwaysOnDisplayNotificationIconViewStore
@@ -58,7 +58,7 @@
     private lateinit var nic: NotificationIconContainer
 
     override fun addViews(constraintLayout: ConstraintLayout) {
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled) {
             return
         }
         nic =
@@ -77,7 +77,7 @@
     }
 
     override fun bindData(constraintLayout: ConstraintLayout) {
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled) {
             return
         }
 
@@ -98,7 +98,7 @@
     }
 
     override fun applyConstraints(constraintSet: ConstraintSet) {
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled) {
             return
         }
         val bottomMargin =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
index a183b72..881467f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
@@ -30,8 +30,8 @@
 import androidx.constraintlayout.widget.ConstraintSet.TOP
 import androidx.constraintlayout.widget.ConstraintSet.VISIBLE
 import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT
-import com.android.systemui.Flags
 import com.android.systemui.customization.R as customizationR
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardSection
@@ -70,7 +70,7 @@
     override fun addViews(constraintLayout: ConstraintLayout) {}
 
     override fun bindData(constraintLayout: ConstraintLayout) {
-        if (!Flags.migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled) {
             return
         }
         KeyguardClockViewBinder.bind(
@@ -83,10 +83,10 @@
     }
 
     override fun applyConstraints(constraintSet: ConstraintSet) {
-        if (!Flags.migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled) {
             return
         }
-        clockInteractor.clock?.let { clock ->
+        keyguardClockViewModel.currentClock.value?.let { clock ->
             constraintSet.applyDeltaFrom(buildConstraints(clock, constraintSet))
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt
index 8fd8bec..4c846e4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt
@@ -28,13 +28,12 @@
 import androidx.constraintlayout.widget.ConstraintSet
 import com.android.keyguard.LockIconView
 import com.android.keyguard.LockIconViewController
-import com.android.systemui.Flags.keyguardBottomAreaRefactor
-import com.android.systemui.Flags.migrateClocksToBlueprint
 import com.android.systemui.biometrics.AuthController
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
 import com.android.systemui.keyguard.shared.model.KeyguardSection
 import com.android.systemui.keyguard.ui.binder.DeviceEntryIconViewBinder
 import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
@@ -72,8 +71,8 @@
 
     override fun addViews(constraintLayout: ConstraintLayout) {
         if (
-            !keyguardBottomAreaRefactor() &&
-                !migrateClocksToBlueprint() &&
+            !KeyguardBottomAreaRefactor.isEnabled &&
+                !DeviceEntryUdfpsRefactor.isEnabled &&
                 !DeviceEntryUdfpsRefactor.isEnabled
         ) {
             return
@@ -87,7 +86,7 @@
             if (DeviceEntryUdfpsRefactor.isEnabled) {
                 DeviceEntryIconView(context, null).apply { id = deviceEntryIconViewId }
             } else {
-                // keyguardBottomAreaRefactor() or migrateClocksToBlueprint()
+                // KeyguardBottomAreaRefactor.isEnabled or MigrateClocksToBlueprint.isEnabled
                 LockIconView(context, null).apply { id = R.id.lock_icon_view }
             }
         constraintLayout.addView(view)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt
index 3361343..af0528a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt
@@ -21,7 +21,7 @@
 import android.view.ViewGroup
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.constraintlayout.widget.ConstraintSet
-import com.android.systemui.Flags.keyguardBottomAreaRefactor
+import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
 import com.android.systemui.keyguard.shared.model.KeyguardSection
 import com.android.systemui.keyguard.ui.binder.KeyguardIndicationAreaBinder
 import com.android.systemui.keyguard.ui.view.KeyguardIndicationArea
@@ -42,14 +42,14 @@
     private var indicationAreaHandle: DisposableHandle? = null
 
     override fun addViews(constraintLayout: ConstraintLayout) {
-        if (keyguardBottomAreaRefactor()) {
+        if (KeyguardBottomAreaRefactor.isEnabled) {
             val view = KeyguardIndicationArea(context, null)
             constraintLayout.addView(view)
         }
     }
 
     override fun bindData(constraintLayout: ConstraintLayout) {
-        if (keyguardBottomAreaRefactor()) {
+        if (KeyguardBottomAreaRefactor.isEnabled) {
             indicationAreaHandle =
                 KeyguardIndicationAreaBinder.bind(
                     constraintLayout.requireViewById(R.id.keyguard_indication_area),
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt
index 6a3b920..380e361 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt
@@ -25,58 +25,42 @@
 import androidx.constraintlayout.widget.ConstraintSet.START
 import androidx.constraintlayout.widget.ConstraintSet.TOP
 import com.android.systemui.Flags.centralizedStatusBarHeightFix
-import com.android.systemui.Flags.migrateClocksToBlueprint
-import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.res.R
-import com.android.systemui.scene.shared.flag.SceneContainerFlags
 import com.android.systemui.shade.LargeScreenHeaderHelper
 import com.android.systemui.shade.NotificationPanelView
-import com.android.systemui.statusbar.notification.stack.AmbientState
-import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
-import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
-import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel
+import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
 import dagger.Lazy
 import javax.inject.Inject
-import kotlinx.coroutines.CoroutineDispatcher
 
 /** Single column format for notifications (default for phones) */
 class DefaultNotificationStackScrollLayoutSection
 @Inject
 constructor(
     context: Context,
-    sceneContainerFlags: SceneContainerFlags,
     notificationPanelView: NotificationPanelView,
     sharedNotificationContainer: SharedNotificationContainer,
     sharedNotificationContainerViewModel: SharedNotificationContainerViewModel,
-    notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel,
-    ambientState: AmbientState,
-    controller: NotificationStackScrollLayoutController,
-    notificationStackSizeCalculator: NotificationStackSizeCalculator,
+    sharedNotificationContainerBinder: SharedNotificationContainerBinder,
     private val largeScreenHeaderHelperLazy: Lazy<LargeScreenHeaderHelper>,
-    @Main mainDispatcher: CoroutineDispatcher,
 ) :
     NotificationStackScrollLayoutSection(
         context,
-        sceneContainerFlags,
         notificationPanelView,
         sharedNotificationContainer,
         sharedNotificationContainerViewModel,
-        notificationStackAppearanceViewModel,
-        ambientState,
-        controller,
-        notificationStackSizeCalculator,
-        mainDispatcher,
+        sharedNotificationContainerBinder,
     ) {
     override fun applyConstraints(constraintSet: ConstraintSet) {
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled) {
             return
         }
         constraintSet.apply {
             val bottomMargin =
                 context.resources.getDimensionPixelSize(R.dimen.keyguard_status_view_bottom_margin)
-            if (migrateClocksToBlueprint()) {
+            if (MigrateClocksToBlueprint.isEnabled) {
                 val useLargeScreenHeader =
                     context.resources.getBoolean(R.bool.config_use_large_screen_shade_header)
                 val marginTopLargeScreen =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultSettingsPopupMenuSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultSettingsPopupMenuSection.kt
index a203c53..32e76d0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultSettingsPopupMenuSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultSettingsPopupMenuSection.kt
@@ -29,9 +29,9 @@
 import androidx.constraintlayout.widget.ConstraintSet.VISIBILITY_MODE_IGNORE
 import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT
 import androidx.core.view.isVisible
-import com.android.systemui.Flags.keyguardBottomAreaRefactor
 import com.android.systemui.animation.view.LaunchableLinearLayout
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
 import com.android.systemui.keyguard.shared.model.KeyguardSection
 import com.android.systemui.keyguard.ui.binder.KeyguardSettingsViewBinder
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardLongPressViewModel
@@ -56,7 +56,7 @@
     private var settingsPopupMenuHandle: DisposableHandle? = null
 
     override fun addViews(constraintLayout: ConstraintLayout) {
-        if (!keyguardBottomAreaRefactor()) {
+        if (!KeyguardBottomAreaRefactor.isEnabled) {
             return
         }
         val view =
@@ -71,7 +71,7 @@
     }
 
     override fun bindData(constraintLayout: ConstraintLayout) {
-        if (keyguardBottomAreaRefactor()) {
+        if (KeyguardBottomAreaRefactor.isEnabled) {
             settingsPopupMenuHandle =
                 KeyguardSettingsViewBinder.bind(
                     constraintLayout.requireViewById<View>(R.id.keyguard_settings_button),
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt
index 0c0eb8a..45b8257 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt
@@ -25,8 +25,8 @@
 import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
 import androidx.constraintlayout.widget.ConstraintSet.RIGHT
 import androidx.constraintlayout.widget.ConstraintSet.VISIBILITY_MODE_IGNORE
-import com.android.systemui.Flags.keyguardBottomAreaRefactor
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
 import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
@@ -48,14 +48,14 @@
     private val vibratorHelper: VibratorHelper,
 ) : BaseShortcutSection() {
     override fun addViews(constraintLayout: ConstraintLayout) {
-        if (keyguardBottomAreaRefactor()) {
+        if (KeyguardBottomAreaRefactor.isEnabled) {
             addLeftShortcut(constraintLayout)
             addRightShortcut(constraintLayout)
         }
     }
 
     override fun bindData(constraintLayout: ConstraintLayout) {
-        if (keyguardBottomAreaRefactor()) {
+        if (KeyguardBottomAreaRefactor.isEnabled) {
             leftShortcutHandle =
                 KeyguardQuickAffordanceViewBinder.bind(
                     constraintLayout.requireViewById(R.id.start_button),
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultStatusViewSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultStatusViewSection.kt
index 6e8605b..45641db 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultStatusViewSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultStatusViewSection.kt
@@ -31,8 +31,8 @@
 import androidx.constraintlayout.widget.ConstraintSet.TOP
 import com.android.keyguard.KeyguardStatusView
 import com.android.keyguard.dagger.KeyguardStatusViewComponent
-import com.android.systemui.Flags.migrateClocksToBlueprint
 import com.android.systemui.keyguard.KeyguardViewConfigurator
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.keyguard.shared.model.KeyguardSection
 import com.android.systemui.media.controls.ui.controller.KeyguardMediaController
 import com.android.systemui.res.R
@@ -58,7 +58,7 @@
     private val statusViewId = R.id.keyguard_status_view
 
     override fun addViews(constraintLayout: ConstraintLayout) {
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled) {
             return
         }
         // At startup, 2 views with the ID `R.id.keyguard_status_view` will be available.
@@ -83,7 +83,7 @@
     }
 
     override fun bindData(constraintLayout: ConstraintLayout) {
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled) {
             constraintLayout.findViewById<KeyguardStatusView?>(R.id.keyguard_status_view)?.let {
                 val statusViewComponent =
                     keyguardStatusViewComponentFactory.build(it, context.display)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultUdfpsAccessibilityOverlaySection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultUdfpsAccessibilityOverlaySection.kt
index 3265d79..2abb7ba 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultUdfpsAccessibilityOverlaySection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultUdfpsAccessibilityOverlaySection.kt
@@ -20,10 +20,10 @@
 import android.content.Context
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.constraintlayout.widget.ConstraintSet
-import com.android.systemui.Flags
 import com.android.systemui.deviceentry.ui.binder.UdfpsAccessibilityOverlayBinder
 import com.android.systemui.deviceentry.ui.view.UdfpsAccessibilityOverlay
 import com.android.systemui.deviceentry.ui.viewmodel.DeviceEntryUdfpsAccessibilityOverlayViewModel
+import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
 import com.android.systemui.keyguard.shared.model.KeyguardSection
 import com.android.systemui.res.R
 import javax.inject.Inject
@@ -66,7 +66,7 @@
                 ConstraintSet.BOTTOM,
             )
 
-            if (Flags.keyguardBottomAreaRefactor()) {
+            if (KeyguardBottomAreaRefactor.isEnabled) {
                 connect(
                     viewId,
                     ConstraintSet.BOTTOM,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardSliceViewSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardSliceViewSection.kt
index d572c51..a17c5e5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardSliceViewSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardSliceViewSection.kt
@@ -22,7 +22,7 @@
 import androidx.constraintlayout.widget.Barrier
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.constraintlayout.widget.ConstraintSet
-import com.android.systemui.Flags.migrateClocksToBlueprint
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.keyguard.shared.model.KeyguardSection
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController
@@ -34,7 +34,7 @@
     val smartspaceController: LockscreenSmartspaceController,
 ) : KeyguardSection() {
     override fun addViews(constraintLayout: ConstraintLayout) {
-        if (!migrateClocksToBlueprint()) return
+        if (!MigrateClocksToBlueprint.isEnabled) return
         if (smartspaceController.isEnabled()) return
 
         constraintLayout.findViewById<View?>(R.id.keyguard_slice_view)?.let {
@@ -46,7 +46,7 @@
     override fun bindData(constraintLayout: ConstraintLayout) {}
 
     override fun applyConstraints(constraintSet: ConstraintSet) {
-        if (!migrateClocksToBlueprint()) return
+        if (!MigrateClocksToBlueprint.isEnabled) return
         if (smartspaceController.isEnabled()) return
 
         constraintSet.apply {
@@ -81,7 +81,7 @@
     }
 
     override fun removeViews(constraintLayout: ConstraintLayout) {
-        if (!migrateClocksToBlueprint()) return
+        if (!MigrateClocksToBlueprint.isEnabled) return
         if (smartspaceController.isEnabled()) return
 
         constraintLayout.removeView(R.id.keyguard_slice_view)
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 5dea7cb..2b601cd 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
@@ -25,38 +25,26 @@
 import androidx.constraintlayout.widget.ConstraintSet
 import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
 import androidx.constraintlayout.widget.ConstraintSet.TOP
-import com.android.systemui.Flags.migrateClocksToBlueprint
 import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.keyguard.shared.model.KeyguardSection
 import com.android.systemui.res.R
-import com.android.systemui.scene.shared.flag.SceneContainerFlags
 import com.android.systemui.shade.NotificationPanelView
-import com.android.systemui.statusbar.notification.stack.AmbientState
-import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
-import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
 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.SharedNotificationContainerViewModel
-import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.DisposableHandle
 
 abstract class NotificationStackScrollLayoutSection
 constructor(
     protected val context: Context,
-    private val sceneContainerFlags: SceneContainerFlags,
     private val notificationPanelView: NotificationPanelView,
     private val sharedNotificationContainer: SharedNotificationContainer,
     private val sharedNotificationContainerViewModel: SharedNotificationContainerViewModel,
-    private val notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel,
-    private val ambientState: AmbientState,
-    private val controller: NotificationStackScrollLayoutController,
-    private val notificationStackSizeCalculator: NotificationStackSizeCalculator,
-    private val mainDispatcher: CoroutineDispatcher,
+    private val sharedNotificationContainerBinder: SharedNotificationContainerBinder,
 ) : KeyguardSection() {
     private val placeHolderId = R.id.nssl_placeholder
-    private val disposableHandles: MutableList<DisposableHandle> = mutableListOf()
+    private var disposableHandle: DisposableHandle? = null
 
     /**
      * Align the notification placeholder bottom to the top of either the lock icon or the ambient
@@ -82,7 +70,7 @@
     }
 
     override fun addViews(constraintLayout: ConstraintLayout) {
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled) {
             return
         }
         // This moves the existing NSSL view to a different parent, as the controller is a
@@ -98,43 +86,21 @@
     }
 
     override fun bindData(constraintLayout: ConstraintLayout) {
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled) {
             return
         }
 
-        disposeHandles()
-        disposableHandles.add(
-            SharedNotificationContainerBinder.bind(
+        disposableHandle?.dispose()
+        disposableHandle =
+            sharedNotificationContainerBinder.bind(
                 sharedNotificationContainer,
                 sharedNotificationContainerViewModel,
-                sceneContainerFlags,
-                controller,
-                notificationStackSizeCalculator,
-                mainImmediateDispatcher = mainDispatcher,
             )
-        )
-
-        if (sceneContainerFlags.isEnabled()) {
-            disposableHandles.add(
-                NotificationStackAppearanceViewBinder.bind(
-                    context,
-                    sharedNotificationContainer,
-                    notificationStackAppearanceViewModel,
-                    ambientState,
-                    controller,
-                    mainImmediateDispatcher = mainDispatcher,
-                )
-            )
-        }
     }
 
     override fun removeViews(constraintLayout: ConstraintLayout) {
-        disposeHandles()
+        disposableHandle?.dispose()
+        disposableHandle = null
         constraintLayout.removeView(placeHolderId)
     }
-
-    private fun disposeHandles() {
-        disposableHandles.forEach { it.dispose() }
-        disposableHandles.clear()
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt
index b0f7a25..1847d27 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt
@@ -23,8 +23,8 @@
 import androidx.constraintlayout.widget.Barrier
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.constraintlayout.widget.ConstraintSet
-import com.android.systemui.Flags.migrateClocksToBlueprint
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardSmartspaceInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardSection
@@ -56,7 +56,7 @@
     private var pastVisibility: Int = -1
 
     override fun addViews(constraintLayout: ConstraintLayout) {
-        if (!migrateClocksToBlueprint()) return
+        if (!MigrateClocksToBlueprint.isEnabled) return
         if (!keyguardSmartspaceViewModel.isSmartspaceEnabled) return
         smartspaceView = smartspaceController.buildAndConnectView(constraintLayout)
         weatherView = smartspaceController.buildAndConnectWeatherView(constraintLayout)
@@ -83,7 +83,7 @@
     }
 
     override fun bindData(constraintLayout: ConstraintLayout) {
-        if (!migrateClocksToBlueprint()) return
+        if (!MigrateClocksToBlueprint.isEnabled) return
         if (!keyguardSmartspaceViewModel.isSmartspaceEnabled) return
         KeyguardSmartspaceViewBinder.bind(
             constraintLayout,
@@ -94,7 +94,7 @@
     }
 
     override fun applyConstraints(constraintSet: ConstraintSet) {
-        if (!migrateClocksToBlueprint()) return
+        if (!MigrateClocksToBlueprint.isEnabled) return
         if (!keyguardSmartspaceViewModel.isSmartspaceEnabled) return
         val horizontalPaddingStart =
             context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start) +
@@ -191,7 +191,7 @@
     }
 
     override fun removeViews(constraintLayout: ConstraintLayout) {
-        if (!migrateClocksToBlueprint()) return
+        if (!MigrateClocksToBlueprint.isEnabled) return
         if (!keyguardSmartspaceViewModel.isSmartspaceEnabled) return
         listOf(smartspaceView, dateView, weatherView).forEach {
             it?.let {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt
index 21e9455..5dbba75 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt
@@ -28,7 +28,7 @@
 import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
 import androidx.constraintlayout.widget.ConstraintSet.START
 import androidx.constraintlayout.widget.ConstraintSet.TOP
-import com.android.systemui.Flags.migrateClocksToBlueprint
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.keyguard.shared.model.KeyguardSection
 import com.android.systemui.media.controls.ui.controller.KeyguardMediaController
 import com.android.systemui.res.R
@@ -46,7 +46,7 @@
     private val mediaContainerId = R.id.status_view_media_container
 
     override fun addViews(constraintLayout: ConstraintLayout) {
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled) {
             return
         }
 
@@ -73,7 +73,7 @@
     override fun bindData(constraintLayout: ConstraintLayout) {}
 
     override fun applyConstraints(constraintSet: ConstraintSet) {
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled) {
             return
         }
 
@@ -87,7 +87,7 @@
     }
 
     override fun removeViews(constraintLayout: ConstraintLayout) {
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled) {
             return
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt
index 2545302..1a73866 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt
@@ -23,51 +23,33 @@
 import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
 import androidx.constraintlayout.widget.ConstraintSet.START
 import androidx.constraintlayout.widget.ConstraintSet.TOP
-import com.android.systemui.Flags.migrateClocksToBlueprint
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.res.R
-import com.android.systemui.scene.shared.flag.SceneContainerFlags
 import com.android.systemui.shade.NotificationPanelView
-import com.android.systemui.statusbar.notification.stack.AmbientState
-import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
-import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
-import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel
+import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
 import javax.inject.Inject
-import kotlinx.coroutines.CoroutineDispatcher
 
 /** Large-screen format for notifications, shown as two columns on the device */
 class SplitShadeNotificationStackScrollLayoutSection
 @Inject
 constructor(
     context: Context,
-    sceneContainerFlags: SceneContainerFlags,
     notificationPanelView: NotificationPanelView,
     sharedNotificationContainer: SharedNotificationContainer,
     sharedNotificationContainerViewModel: SharedNotificationContainerViewModel,
-    notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel,
-    ambientState: AmbientState,
-    controller: NotificationStackScrollLayoutController,
-    notificationStackSizeCalculator: NotificationStackSizeCalculator,
-    private val smartspaceViewModel: KeyguardSmartspaceViewModel,
-    @Main mainDispatcher: CoroutineDispatcher,
+    sharedNotificationContainerBinder: SharedNotificationContainerBinder,
 ) :
     NotificationStackScrollLayoutSection(
         context,
-        sceneContainerFlags,
         notificationPanelView,
         sharedNotificationContainer,
         sharedNotificationContainerViewModel,
-        notificationStackAppearanceViewModel,
-        ambientState,
-        controller,
-        notificationStackSizeCalculator,
-        mainDispatcher,
+        sharedNotificationContainerBinder,
     ) {
     override fun applyConstraints(constraintSet: ConstraintSet) {
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled) {
             return
         }
         constraintSet.apply {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt
index 6184c82..4d3a78d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt
@@ -216,7 +216,9 @@
             captureSmartspace = !viewModel.useLargeClock && smartspaceViewModel.isSmartspaceEnabled
 
             if (viewModel.useLargeClock) {
-                viewModel.clock?.let { it.largeClock.layout.views.forEach { addTarget(it) } }
+                viewModel.currentClock.value?.let {
+                    it.largeClock.layout.views.forEach { addTarget(it) }
+                }
             } else {
                 addTarget(R.id.lockscreen_clock_view)
             }
@@ -276,7 +278,9 @@
             if (viewModel.useLargeClock) {
                 addTarget(R.id.lockscreen_clock_view)
             } else {
-                viewModel.clock?.let { it.largeClock.layout.views.forEach { addTarget(it) } }
+                viewModel.currentClock.value?.let {
+                    it.largeClock.layout.views.forEach { addTarget(it) }
+                }
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt
index d26356e..ac2713d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import android.util.MathUtils
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromAlternateBouncerTransitionInteractor.Companion.TO_GONE_DURATION
 import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -47,13 +48,16 @@
             to = KeyguardState.GONE,
         )
 
-    val lockscreenAlpha: Flow<Float> =
-        transitionAnimation.sharedFlow(
+    fun lockscreenAlpha(viewState: ViewStateAccessor): Flow<Float> {
+        var startAlpha = 1f
+        return transitionAnimation.sharedFlow(
             duration = 200.milliseconds,
-            onStep = { 1 - it },
+            onStart = { startAlpha = viewState.alpha() },
+            onStep = { MathUtils.lerp(startAlpha, 0f, it) },
             onFinish = { 0f },
-            onCancel = { 1f },
+            onCancel = { startAlpha },
         )
+    }
 
     /** Scrim alpha values */
     val scrimAlpha: Flow<ScrimAlpha> =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModel.kt
index 5741b94..1e5f5a7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModel.kt
@@ -18,8 +18,8 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
-import com.android.systemui.Flags.migrateClocksToBlueprint
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
@@ -60,7 +60,7 @@
                 emit(goneToAodAlpha)
             } else if (step.from == GONE && step.to == DOZING) {
                 emit(goneToDozingAlpha)
-            } else if (!migrateClocksToBlueprint()) {
+            } else if (!MigrateClocksToBlueprint.isEnabled) {
                 emit(keyguardAlpha)
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt
index f961e08..2054932 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt
@@ -22,9 +22,9 @@
 import android.util.MathUtils
 import com.android.app.animation.Interpolators
 import com.android.keyguard.KeyguardClockSwitch
-import com.android.systemui.Flags
 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.keyguard.domain.interactor.BurnInInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
@@ -145,7 +145,7 @@
                 // Ensure the desired translation doesn't encroach on the top inset
                 val burnInY = MathUtils.lerp(0, burnIn.translationY, interpolated).toInt()
                 val translationY =
-                    if (Flags.migrateClocksToBlueprint()) {
+                    if (MigrateClocksToBlueprint.isEnabled) {
                         max(params.topInset - params.minViewY, burnInY)
                     } else {
                         max(params.topInset, params.minViewY + burnInY) - params.minViewY
@@ -168,8 +168,8 @@
     private fun clockController(
         provider: Provider<ClockController>?,
     ): Provider<ClockController>? {
-        return if (Flags.migrateClocksToBlueprint()) {
-            Provider { keyguardClockViewModel.clock }
+        return if (MigrateClocksToBlueprint.isEnabled) {
+            Provider { keyguardClockViewModel.currentClock.value }
         } else {
             provider
         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt
new file mode 100644
index 0000000..9a23007
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.FromAodTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Breaks down AOD->PRIMARY BOUNCER transition into discrete steps for corresponding views to
+ * consume.
+ */
+@ExperimentalCoroutinesApi
+@SysUISingleton
+class AodToPrimaryBouncerTransitionViewModel
+@Inject
+constructor(
+    animationFlow: KeyguardTransitionAnimationFlow,
+) : DeviceEntryIconTransition {
+    private val transitionAnimation =
+        animationFlow.setup(
+            duration = FromAodTransitionInteractor.TO_PRIMARY_BOUNCER_DURATION,
+            from = KeyguardState.AOD,
+            to = KeyguardState.PRIMARY_BOUNCER,
+        )
+
+    override val deviceEntryParentViewAlpha: Flow<Float> =
+        transitionAnimation.immediatelyTransitionTo(0f)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt
index 4c0a949..1b91c49 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt
@@ -55,6 +55,8 @@
     lockscreenToDozingTransitionViewModel: LockscreenToDozingTransitionViewModel,
     dozingToLockscreenTransitionViewModel: DozingToLockscreenTransitionViewModel,
     alternateBouncerToDozingTransitionViewModel: AlternateBouncerToDozingTransitionViewModel,
+    dreamingToAodTransitionViewModel: DreamingToAodTransitionViewModel,
+    primaryBouncerToLockscreenTransitionViewModel: PrimaryBouncerToLockscreenTransitionViewModel,
 ) {
     val color: Flow<Int> =
         deviceEntryIconViewModel.useBackgroundProtection.flatMapLatest { useBackground ->
@@ -96,6 +98,9 @@
                         lockscreenToDozingTransitionViewModel.deviceEntryBackgroundViewAlpha,
                         dozingToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
                         alternateBouncerToDozingTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                        dreamingToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                        primaryBouncerToLockscreenTransitionViewModel
+                            .deviceEntryBackgroundViewAlpha,
                     )
                     .merge()
                     .onStart {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt
index 1a01897..bc4fd1c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt
@@ -19,6 +19,7 @@
 import android.animation.FloatEvaluator
 import android.animation.IntEvaluator
 import com.android.keyguard.KeyguardViewController
+import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntrySourceInteractor
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
@@ -33,9 +34,11 @@
 import com.android.systemui.util.kotlin.sample
 import dagger.Lazy
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
@@ -45,6 +48,7 @@
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.shareIn
 
 /** Models the UI state for the containing device entry icon & long-press handling view. */
 @ExperimentalCoroutinesApi
@@ -62,6 +66,7 @@
     private val keyguardViewController: Lazy<KeyguardViewController>,
     private val deviceEntryInteractor: DeviceEntryInteractor,
     private val deviceEntrySourceInteractor: DeviceEntrySourceInteractor,
+    @Application private val scope: CoroutineScope,
 ) {
     val isUdfpsSupported: StateFlow<Boolean> = deviceEntryUdfpsInteractor.isUdfpsSupported
     private val intEvaluator = IntEvaluator()
@@ -73,7 +78,10 @@
     private val qsProgress: Flow<Float> = shadeInteractor.qsExpansion.onStart { emit(0f) }
     private val shadeExpansion: Flow<Float> = shadeInteractor.shadeExpansion.onStart { emit(0f) }
     private val transitionAlpha: Flow<Float> =
-        transitions.map { it.deviceEntryParentViewAlpha }.merge()
+        transitions
+            .map { it.deviceEntryParentViewAlpha }
+            .merge()
+            .shareIn(scope, SharingStarted.WhileSubscribed())
     private val alphaMultiplierFromShadeExpansion: Flow<Float> =
         combine(
             showingAlternateBouncer,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToAodTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToAodTransitionViewModel.kt
new file mode 100644
index 0000000..0fa7475
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToAodTransitionViewModel.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
+import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapLatest
+
+/** Breaks down DREAMING->AOD transition into discrete steps for corresponding views to consume. */
+@ExperimentalCoroutinesApi
+@SysUISingleton
+class DreamingToAodTransitionViewModel
+@Inject
+constructor(
+    deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
+    animationFlow: KeyguardTransitionAnimationFlow,
+) : DeviceEntryIconTransition {
+    private val transitionAnimation =
+        animationFlow.setup(
+            duration = FromDreamingTransitionInteractor.TO_AOD_DURATION,
+            from = KeyguardState.DREAMING,
+            to = KeyguardState.AOD,
+        )
+
+    val deviceEntryBackgroundViewAlpha: Flow<Float> =
+        transitionAnimation.immediatelyTransitionTo(0f)
+    override val deviceEntryParentViewAlpha: Flow<Float> =
+        deviceEntryUdfpsInteractor.isUdfpsEnrolledAndEnabled.flatMapLatest { udfpsEnrolledAndEnabled
+            ->
+            if (udfpsEnrolledAndEnabled) {
+                transitionAnimation.sharedFlow(
+                    duration = FromDreamingTransitionInteractor.TO_AOD_DURATION,
+                    onStep = { it },
+                    onFinish = { 1f },
+                )
+            } else {
+                emptyFlow()
+            }
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGoneTransitionViewModel.kt
new file mode 100644
index 0000000..ec7b931
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGoneTransitionViewModel.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import kotlinx.coroutines.flow.Flow
+import javax.inject.Inject
+
+@SysUISingleton
+class DreamingToGoneTransitionViewModel
+@Inject
+constructor(
+    animationFlow: KeyguardTransitionAnimationFlow,
+) {
+
+    private val transitionAnimation =
+            animationFlow.setup(
+                duration = FromDreamingTransitionInteractor.TO_GONE_DURATION,
+                from = KeyguardState.DREAMING,
+                to = KeyguardState.GONE,
+            )
+
+    /** Lockscreen views alpha */
+    val lockscreenAlpha: Flow<Float> = transitionAnimation.immediatelyTransitionTo(0f)
+
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToLockscreenTransitionViewModel.kt
index e0b1c50..a2ce408 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToLockscreenTransitionViewModel.kt
@@ -43,9 +43,11 @@
         transitionAnimation.sharedFlow(
             duration = 250.milliseconds,
             onStep = { it },
-            onCancel = { 0f },
+            onCancel = { 1f },
         )
 
+    val lockscreenAlpha: Flow<Float> = shortcutsAlpha
+
     val deviceEntryBackgroundViewAlpha: Flow<Float> =
         transitionAnimation.immediatelyTransitionTo(1f)
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
index b6622e5..1c1c33a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
@@ -26,7 +26,6 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.shared.ComposeLockscreen
 import com.android.systemui.keyguard.shared.model.SettingsClockSize
-import com.android.systemui.plugins.clocks.ClockController
 import com.android.systemui.res.R
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.shade.shared.model.ShadeMode
@@ -54,8 +53,6 @@
     val useLargeClock: Boolean
         get() = clockSize.value == LARGE
 
-    var clock: ClockController? by keyguardClockInteractor::clock
-
     val clockSize =
         combine(keyguardClockInteractor.selectedClockSize, keyguardClockInteractor.clockSize) {
                 selectedSize,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt
index e35e065..8409f15 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt
@@ -16,10 +16,10 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
-import com.android.systemui.Flags.keyguardBottomAreaRefactor
-import com.android.systemui.Flags.migrateClocksToBlueprint
 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
 import com.android.systemui.doze.util.BurnInHelperWrapper
+import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.keyguard.domain.interactor.BurnInInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
@@ -52,7 +52,7 @@
 
     /** An observable for whether the indication area should be padded. */
     val isIndicationAreaPadded: Flow<Boolean> =
-        if (keyguardBottomAreaRefactor()) {
+        if (KeyguardBottomAreaRefactor.isEnabled) {
             combine(shortcutsCombinedViewModel.startButton, shortcutsCombinedViewModel.endButton) {
                     startButtonModel,
                     endButtonModel ->
@@ -79,7 +79,7 @@
 
     /** An observable for the x-offset by which the indication area should be translated. */
     val indicationAreaTranslationX: Flow<Float> =
-        if (migrateClocksToBlueprint() || keyguardBottomAreaRefactor()) {
+        if (MigrateClocksToBlueprint.isEnabled || KeyguardBottomAreaRefactor.isEnabled) {
             burnIn.map { it.translationX.toFloat() }
         } else {
             bottomAreaInteractor.clockPosition.map { it.x.toFloat() }.distinctUntilChanged()
@@ -87,7 +87,7 @@
 
     /** Returns an observable for the y-offset by which the indication area should be translated. */
     fun indicationAreaTranslationY(defaultBurnInOffset: Int): Flow<Float> {
-        return if (migrateClocksToBlueprint()) {
+        return if (MigrateClocksToBlueprint.isEnabled) {
             burnIn.map { it.translationY.toFloat() }
         } else {
             keyguardInteractor.dozeAmount
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewClockViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewClockViewModel.kt
index b9ff259..4f2c6f5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewClockViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewClockViewModel.kt
@@ -24,10 +24,8 @@
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
 
 /** View model for the small clock view, large clock view. */
 class KeyguardPreviewClockViewModel
@@ -45,15 +43,7 @@
     val isSmallClockVisible: Flow<Boolean> =
         interactor.selectedClockSize.map { it == SettingsClockSize.SMALL }
 
-    var lastClockPair: Pair<ClockController, ClockController>? = null
+    val previewClock: Flow<ClockController> = interactor.previewClock
 
-    val previewClockPair: StateFlow<Pair<ClockController, ClockController>> =
-        interactor.previewClockPair
-
-    val selectedClockSize: StateFlow<SettingsClockSize?> =
-        interactor.selectedClockSize.stateIn(
-            scope = applicationScope,
-            started = SharingStarted.WhileSubscribed(),
-            initialValue = null
-        )
+    val selectedClockSize: StateFlow<SettingsClockSize?> = interactor.selectedClockSize
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index 55a4025..5337ca3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -85,10 +85,13 @@
     private val dozingToLockscreenTransitionViewModel: DozingToLockscreenTransitionViewModel,
     private val dozingToOccludedTransitionViewModel: DozingToOccludedTransitionViewModel,
     private val dreamingToLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel,
+    private val dreamingToGoneTransitionViewModel: DreamingToGoneTransitionViewModel,
     private val glanceableHubToLockscreenTransitionViewModel:
         GlanceableHubToLockscreenTransitionViewModel,
     private val goneToAodTransitionViewModel: GoneToAodTransitionViewModel,
     private val goneToDozingTransitionViewModel: GoneToDozingTransitionViewModel,
+    private val goneToDreamingTransitionViewModel: GoneToDreamingTransitionViewModel,
+    private val goneToLockscreenTransitionViewModel: GoneToLockscreenTransitionViewModel,
     private val lockscreenToAodTransitionViewModel: LockscreenToAodTransitionViewModel,
     private val lockscreenToDozingTransitionViewModel: LockscreenToDozingTransitionViewModel,
     private val lockscreenToDreamingTransitionViewModel: LockscreenToDreamingTransitionViewModel,
@@ -136,14 +139,20 @@
             }
             .distinctUntilChanged()
 
+    private val lockscreenToGoneTransitionRunning: Flow<Boolean> =
+        keyguardTransitionInteractor
+            .isInTransitionWhere { from, to -> from == LOCKSCREEN && to == GONE }
+            .onStart { emit(false) }
+
     private val alphaOnShadeExpansion: Flow<Float> =
         combineTransform(
+                lockscreenToGoneTransitionRunning,
                 isOnLockscreen,
                 shadeInteractor.qsExpansion,
                 shadeInteractor.shadeExpansion,
-            ) { isOnLockscreen, qsExpansion, shadeExpansion ->
+            ) { lockscreenToGoneTransitionRunning, isOnLockscreen, qsExpansion, shadeExpansion ->
                 // Fade out quickly as the shade expands
-                if (isOnLockscreen) {
+                if (isOnLockscreen && !lockscreenToGoneTransitionRunning) {
                     val alpha =
                         1f -
                             MathUtils.constrainedMap(
@@ -197,17 +206,20 @@
                 merge(
                         alphaOnShadeExpansion,
                         keyguardInteractor.dismissAlpha.filterNotNull(),
-                        alternateBouncerToGoneTransitionViewModel.lockscreenAlpha,
+                        alternateBouncerToGoneTransitionViewModel.lockscreenAlpha(viewState),
                         aodToGoneTransitionViewModel.lockscreenAlpha(viewState),
                         aodToLockscreenTransitionViewModel.lockscreenAlpha(viewState),
                         aodToOccludedTransitionViewModel.lockscreenAlpha(viewState),
                         dozingToGoneTransitionViewModel.lockscreenAlpha(viewState),
                         dozingToLockscreenTransitionViewModel.lockscreenAlpha,
                         dozingToOccludedTransitionViewModel.lockscreenAlpha(viewState),
+                        dreamingToGoneTransitionViewModel.lockscreenAlpha,
                         dreamingToLockscreenTransitionViewModel.lockscreenAlpha,
                         glanceableHubToLockscreenTransitionViewModel.keyguardAlpha,
                         goneToAodTransitionViewModel.enterFromTopAnimationAlpha,
                         goneToDozingTransitionViewModel.lockscreenAlpha,
+                        goneToDreamingTransitionViewModel.lockscreenAlpha,
+                        goneToLockscreenTransitionViewModel.lockscreenAlpha,
                         lockscreenToAodTransitionViewModel.lockscreenAlpha(viewState),
                         lockscreenToDozingTransitionViewModel.lockscreenAlpha,
                         lockscreenToDreamingTransitionViewModel.lockscreenAlpha,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt
index 34c9ac9..2575041 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt
@@ -18,7 +18,6 @@
 
 import com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
 import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
@@ -27,8 +26,6 @@
 import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.emptyFlow
-import kotlinx.coroutines.flow.flatMapLatest
 
 /**
  * Breaks down PRIMARY BOUNCER->LOCKSCREEN transition into discrete steps for corresponding views to
@@ -39,7 +36,6 @@
 class PrimaryBouncerToLockscreenTransitionViewModel
 @Inject
 constructor(
-    deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
     animationFlow: KeyguardTransitionAnimationFlow,
 ) : DeviceEntryIconTransition {
     private val transitionAnimation =
@@ -49,15 +45,6 @@
             to = KeyguardState.LOCKSCREEN,
         )
 
-    val deviceEntryBackgroundViewAlpha: Flow<Float> =
-        deviceEntryUdfpsInteractor.isUdfpsSupported.flatMapLatest { isUdfps ->
-            if (isUdfps) {
-                transitionAnimation.immediatelyTransitionTo(1f)
-            } else {
-                emptyFlow()
-            }
-        }
-
     val shortcutsAlpha: Flow<Float> =
         transitionAnimation.sharedFlow(
             duration = 250.milliseconds,
@@ -67,6 +54,8 @@
 
     val lockscreenAlpha: Flow<Float> = shortcutsAlpha
 
+    val deviceEntryBackgroundViewAlpha: Flow<Float> =
+        transitionAnimation.immediatelyTransitionTo(1f)
     override val deviceEntryParentViewAlpha: Flow<Float> =
         transitionAnimation.immediatelyTransitionTo(1f)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaDataRepository.kt b/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaDataRepository.kt
new file mode 100644
index 0000000..b6fd287
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaDataRepository.kt
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.data.repository
+
+import android.util.Log
+import com.android.systemui.Dumpable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.media.controls.shared.model.MediaData
+import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
+import com.android.systemui.media.controls.util.MediaFlags
+import java.io.PrintWriter
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+private const val TAG = "MediaDataRepository"
+private const val DEBUG = true
+
+/** A repository that holds the state of all media controls in carousel. */
+@SysUISingleton
+class MediaDataRepository
+@Inject
+constructor(
+    private val mediaFlags: MediaFlags,
+    dumpManager: DumpManager,
+) : Dumpable {
+
+    private val _mediaEntries: MutableStateFlow<Map<String, MediaData>> =
+        MutableStateFlow(LinkedHashMap())
+    val mediaEntries: StateFlow<Map<String, MediaData>> = _mediaEntries.asStateFlow()
+
+    private val _smartspaceMediaData: MutableStateFlow<SmartspaceMediaData> =
+        MutableStateFlow(SmartspaceMediaData())
+    val smartspaceMediaData: StateFlow<SmartspaceMediaData> = _smartspaceMediaData.asStateFlow()
+
+    init {
+        dumpManager.registerNormalDumpable(TAG, this)
+    }
+
+    /** Updates the recommendation data with a new smartspace media data. */
+    fun setRecommendation(recommendation: SmartspaceMediaData) {
+        _smartspaceMediaData.value = recommendation
+    }
+
+    /**
+     * Marks the recommendation data as inactive.
+     *
+     * @return true if the recommendation was actually marked as inactive, false otherwise.
+     */
+    fun setRecommendationInactive(key: String): Boolean {
+        if (!mediaFlags.isPersistentSsCardEnabled()) {
+            Log.e(TAG, "Only persistent recommendation can be inactive!")
+            return false
+        }
+        if (DEBUG) Log.d(TAG, "Setting smartspace recommendation inactive")
+
+        if (smartspaceMediaData.value.targetId != key || !smartspaceMediaData.value.isValid()) {
+            // If this doesn't match, or we've already invalidated the data, no action needed
+            return false
+        }
+
+        setRecommendation(smartspaceMediaData.value.copy(isActive = false))
+        return true
+    }
+
+    /**
+     * Marks the recommendation data as dismissed.
+     *
+     * @return true if the recommendation was dismissed or already inactive, false otherwise.
+     */
+    fun dismissSmartspaceRecommendation(key: String): Boolean {
+        val data = smartspaceMediaData.value
+        if (data.targetId != key || !data.isValid()) {
+            // If this doesn't match, or we've already invalidated the data, no action needed
+            return false
+        }
+
+        if (DEBUG) Log.d(TAG, "Dismissing Smartspace media target")
+        if (data.isActive) {
+            setRecommendation(
+                SmartspaceMediaData(
+                    targetId = smartspaceMediaData.value.targetId,
+                    instanceId = smartspaceMediaData.value.instanceId
+                )
+            )
+        }
+        return true
+    }
+
+    fun removeMediaEntry(key: String): MediaData? {
+        val entries = LinkedHashMap<String, MediaData>(_mediaEntries.value)
+        val mediaData = entries.remove(key)
+        _mediaEntries.value = entries
+        return mediaData
+    }
+
+    fun addMediaEntry(key: String, data: MediaData): MediaData? {
+        val entries = LinkedHashMap<String, MediaData>(_mediaEntries.value)
+        val mediaData = entries.put(key, data)
+        _mediaEntries.value = entries
+        return mediaData
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        pw.apply { println("mediaEntries: ${mediaEntries.value}") }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt b/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt
new file mode 100644
index 0000000..b94a4af
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt
@@ -0,0 +1,111 @@
+/*
+ * 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.media.controls.data.repository
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.media.controls.shared.model.MediaData
+import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+/** A repository that holds the state of filtered media data on the device. */
+@SysUISingleton
+class MediaFilterRepository @Inject constructor() {
+
+    /** Key of media control that recommendations card reactivated. */
+    private val _reactivatedKey: MutableStateFlow<String?> = MutableStateFlow(null)
+    val reactivatedKey: StateFlow<String?> = _reactivatedKey.asStateFlow()
+
+    private val _smartspaceMediaData: MutableStateFlow<SmartspaceMediaData> =
+        MutableStateFlow(SmartspaceMediaData())
+    val smartspaceMediaData: StateFlow<SmartspaceMediaData> = _smartspaceMediaData.asStateFlow()
+
+    private val _selectedUserEntries: MutableStateFlow<Map<String, MediaData>> =
+        MutableStateFlow(LinkedHashMap())
+    val selectedUserEntries: StateFlow<Map<String, MediaData>> = _selectedUserEntries.asStateFlow()
+
+    private val _allUserEntries: MutableStateFlow<Map<String, MediaData>> =
+        MutableStateFlow(LinkedHashMap())
+    val allUserEntries: StateFlow<Map<String, MediaData>> = _allUserEntries.asStateFlow()
+
+    fun addMediaEntry(key: String, data: MediaData) {
+        val entries = LinkedHashMap<String, MediaData>(_allUserEntries.value)
+        entries[key] = data
+        _allUserEntries.value = entries
+    }
+
+    /**
+     * Removes the media entry corresponding to the given [key].
+     *
+     * @return media data if an entry is actually removed, `null` otherwise.
+     */
+    fun removeMediaEntry(key: String): MediaData? {
+        val entries = LinkedHashMap<String, MediaData>(_allUserEntries.value)
+        val mediaData = entries.remove(key)
+        _allUserEntries.value = entries
+        return mediaData
+    }
+
+    fun addSelectedUserMediaEntry(key: String, data: MediaData) {
+        val entries = LinkedHashMap<String, MediaData>(_selectedUserEntries.value)
+        entries[key] = data
+        _selectedUserEntries.value = entries
+    }
+
+    /**
+     * Removes selected user media entry given the corresponding key.
+     *
+     * @return media data if an entry is actually removed, `null` otherwise.
+     */
+    fun removeSelectedUserMediaEntry(key: String): MediaData? {
+        val entries = LinkedHashMap<String, MediaData>(_selectedUserEntries.value)
+        val mediaData = entries.remove(key)
+        _selectedUserEntries.value = entries
+        return mediaData
+    }
+
+    /**
+     * Removes selected user media entry given a key and media data.
+     *
+     * @return true if media data is removed, false otherwise.
+     */
+    fun removeSelectedUserMediaEntry(key: String, data: MediaData): Boolean {
+        val entries = LinkedHashMap<String, MediaData>(_selectedUserEntries.value)
+        val succeed = entries.remove(key, data)
+        if (!succeed) {
+            return false
+        }
+        _selectedUserEntries.value = entries
+        return true
+    }
+
+    fun clearSelectedUserMedia() {
+        _selectedUserEntries.value = LinkedHashMap()
+    }
+
+    /** Updates recommendation data with a new smartspace media data. */
+    fun setRecommendation(smartspaceMediaData: SmartspaceMediaData) {
+        _smartspaceMediaData.value = smartspaceMediaData
+    }
+
+    /** Updates media control key that recommendations card reactivated. */
+    fun setReactivatedKey(key: String?) {
+        _reactivatedKey.value = key
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/MediaDomainModule.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/MediaDomainModule.kt
new file mode 100644
index 0000000..e0c5419
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/MediaDomainModule.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.media.controls.domain
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.media.controls.domain.pipeline.LegacyMediaDataManagerImpl
+import com.android.systemui.media.controls.domain.pipeline.MediaDataManager
+import com.android.systemui.media.controls.domain.pipeline.MediaDataProcessor
+import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor
+import com.android.systemui.media.controls.util.MediaFlags
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+import javax.inject.Provider
+
+/** Dagger module for injecting media controls domain interfaces. */
+@Module
+interface MediaDomainModule {
+
+    @Binds
+    @IntoMap
+    @ClassKey(MediaCarouselInteractor::class)
+    fun bindMediaCarouselInteractor(interactor: MediaCarouselInteractor): CoreStartable
+
+    @Binds
+    @IntoMap
+    @ClassKey(MediaDataProcessor::class)
+    fun bindMediaDataProcessor(interactor: MediaDataProcessor): CoreStartable
+    companion object {
+
+        @Provides
+        @SysUISingleton
+        fun providesMediaDataManager(
+            legacyProvider: Provider<LegacyMediaDataManagerImpl>,
+            newProvider: Provider<MediaCarouselInteractor>,
+            mediaFlags: MediaFlags,
+        ): MediaDataManager {
+            return if (mediaFlags.isMediaControlsRefactorEnabled()) {
+                newProvider.get()
+            } else {
+                legacyProvider.get()
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilter.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImpl.kt
similarity index 98%
rename from packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilter.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImpl.kt
index bc539ef..c02478b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilter.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImpl.kt
@@ -61,7 +61,7 @@
  * This is added at the end of the pipeline since we may still need to handle callbacks from
  * background users (e.g. timeouts).
  */
-class MediaDataFilter
+class LegacyMediaDataFilterImpl
 @Inject
 constructor(
     private val context: Context,
@@ -74,9 +74,9 @@
     private val mediaFlags: MediaFlags,
 ) : MediaDataManager.Listener {
     private val _listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf()
-    internal val listeners: Set<MediaDataManager.Listener>
+    val listeners: Set<MediaDataManager.Listener>
         get() = _listeners.toSet()
-    internal lateinit var mediaDataManager: MediaDataManager
+    lateinit var mediaDataManager: MediaDataManager
 
     private val allEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
     // The filtered userEntries, which will be a subset of all userEntries in MediaDataManager
@@ -279,7 +279,7 @@
         val mediaKeys = userEntries.keys.toSet()
         mediaKeys.forEach {
             // Force updates to listeners, needed for re-activated card
-            mediaDataManager.setTimedOut(it, timedOut = true, forceUpdate = true)
+            mediaDataManager.setInactive(it, timedOut = true, forceUpdate = true)
         }
         if (smartspaceMediaData.isActive) {
             val dismissIntent = smartspaceMediaData.dismissIntent
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
new file mode 100644
index 0000000..3a831156
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
@@ -0,0 +1,1693 @@
+/*
+ * Copyright (C) 2020 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.media.controls.domain.pipeline
+
+import android.annotation.SuppressLint
+import android.app.ActivityOptions
+import android.app.BroadcastOptions
+import android.app.Notification
+import android.app.Notification.EXTRA_SUBSTITUTE_APP_NAME
+import android.app.PendingIntent
+import android.app.StatusBarManager
+import android.app.UriGrantsManager
+import android.app.smartspace.SmartspaceAction
+import android.app.smartspace.SmartspaceConfig
+import android.app.smartspace.SmartspaceManager
+import android.app.smartspace.SmartspaceSession
+import android.app.smartspace.SmartspaceTarget
+import android.content.BroadcastReceiver
+import android.content.ContentProvider
+import android.content.ContentResolver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.graphics.Bitmap
+import android.graphics.ImageDecoder
+import android.graphics.drawable.Animatable
+import android.graphics.drawable.Icon
+import android.media.MediaDescription
+import android.media.MediaMetadata
+import android.media.session.MediaController
+import android.media.session.MediaSession
+import android.media.session.PlaybackState
+import android.net.Uri
+import android.os.Parcelable
+import android.os.Process
+import android.os.UserHandle
+import android.provider.Settings
+import android.service.notification.StatusBarNotification
+import android.support.v4.media.MediaMetadataCompat
+import android.text.TextUtils
+import android.util.Log
+import android.util.Pair as APair
+import androidx.media.utils.MediaConstants
+import com.android.app.tracing.traceSection
+import com.android.internal.annotations.Keep
+import com.android.internal.logging.InstanceId
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.Dumpable
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.media.controls.domain.pipeline.MediaDataManager.Companion.isMediaNotification
+import com.android.systemui.media.controls.domain.resume.MediaResumeListener
+import com.android.systemui.media.controls.domain.resume.ResumeMediaBrowser
+import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_SOURCE
+import com.android.systemui.media.controls.shared.model.EXTRA_VALUE_TRIGGER_PERIODIC
+import com.android.systemui.media.controls.shared.model.MediaAction
+import com.android.systemui.media.controls.shared.model.MediaButton
+import com.android.systemui.media.controls.shared.model.MediaData
+import com.android.systemui.media.controls.shared.model.MediaDeviceData
+import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
+import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvider
+import com.android.systemui.media.controls.ui.view.MediaViewHolder
+import com.android.systemui.media.controls.util.MediaControllerFactory
+import com.android.systemui.media.controls.util.MediaDataUtils
+import com.android.systemui.media.controls.util.MediaFlags
+import com.android.systemui.media.controls.util.MediaUiEventLogger
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.plugins.BcSmartspaceDataPlugin
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.NotificationMediaManager.isConnectingState
+import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
+import com.android.systemui.statusbar.notification.row.HybridGroupManager
+import com.android.systemui.tuner.TunerService
+import com.android.systemui.util.Assert
+import com.android.systemui.util.Utils
+import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.util.concurrency.ThreadFactory
+import com.android.systemui.util.time.SystemClock
+import java.io.IOException
+import java.io.PrintWriter
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+// URI fields to try loading album art from
+private val ART_URIS =
+    arrayOf(
+        MediaMetadata.METADATA_KEY_ALBUM_ART_URI,
+        MediaMetadata.METADATA_KEY_ART_URI,
+        MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI
+    )
+
+private const val TAG = "MediaDataManager"
+private const val DEBUG = true
+private const val EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY = "dismiss_intent"
+
+private val LOADING =
+    MediaData(
+        userId = -1,
+        initialized = false,
+        app = null,
+        appIcon = null,
+        artist = null,
+        song = null,
+        artwork = null,
+        actions = emptyList(),
+        actionsToShowInCompact = emptyList(),
+        packageName = "INVALID",
+        token = null,
+        clickIntent = null,
+        device = null,
+        active = true,
+        resumeAction = null,
+        instanceId = InstanceId.fakeInstanceId(-1),
+        appUid = Process.INVALID_UID
+    )
+
+internal val EMPTY_SMARTSPACE_MEDIA_DATA =
+    SmartspaceMediaData(
+        targetId = "INVALID",
+        isActive = false,
+        packageName = "INVALID",
+        cardAction = null,
+        recommendations = emptyList(),
+        dismissIntent = null,
+        headphoneConnectionTimeMillis = 0,
+        instanceId = InstanceId.fakeInstanceId(-1),
+        expiryTimeMs = 0,
+    )
+
+const val MEDIA_TITLE_ERROR_MESSAGE = "Invalid media data: title is null or blank."
+
+/**
+ * Allow recommendations from smartspace to show in media controls. Requires
+ * [Utils.useQsMediaPlayer] to be enabled. On by default, but can be disabled by setting to 0
+ */
+private fun allowMediaRecommendations(context: Context): Boolean {
+    val flag =
+        Settings.Secure.getInt(
+            context.contentResolver,
+            Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
+            1
+        )
+    return Utils.useQsMediaPlayer(context) && flag > 0
+}
+
+/** A class that facilitates management and loading of Media Data, ready for binding. */
+@SysUISingleton
+class LegacyMediaDataManagerImpl(
+    private val context: Context,
+    @Background private val backgroundExecutor: Executor,
+    @Main private val uiExecutor: Executor,
+    @Main private val foregroundExecutor: DelayableExecutor,
+    private val mediaControllerFactory: MediaControllerFactory,
+    private val broadcastDispatcher: BroadcastDispatcher,
+    dumpManager: DumpManager,
+    mediaTimeoutListener: MediaTimeoutListener,
+    mediaResumeListener: MediaResumeListener,
+    mediaSessionBasedFilter: MediaSessionBasedFilter,
+    private val mediaDeviceManager: MediaDeviceManager,
+    mediaDataCombineLatest: MediaDataCombineLatest,
+    private val mediaDataFilter: LegacyMediaDataFilterImpl,
+    private val activityStarter: ActivityStarter,
+    private val smartspaceMediaDataProvider: SmartspaceMediaDataProvider,
+    private var useMediaResumption: Boolean,
+    private val useQsMediaPlayer: Boolean,
+    private val systemClock: SystemClock,
+    private val tunerService: TunerService,
+    private val mediaFlags: MediaFlags,
+    private val logger: MediaUiEventLogger,
+    private val smartspaceManager: SmartspaceManager?,
+    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+) : Dumpable, BcSmartspaceDataPlugin.SmartspaceTargetListener, MediaDataManager {
+
+    companion object {
+        // UI surface label for subscribing Smartspace updates.
+        @JvmField val SMARTSPACE_UI_SURFACE_LABEL = "media_data_manager"
+
+        // Smartspace package name's extra key.
+        @JvmField val EXTRAS_MEDIA_SOURCE_PACKAGE_NAME = "package_name"
+
+        // Maximum number of actions allowed in compact view
+        @JvmField val MAX_COMPACT_ACTIONS = 3
+
+        // Maximum number of actions allowed in expanded view
+        @JvmField val MAX_NOTIFICATION_ACTIONS = MediaViewHolder.genericButtonIds.size
+    }
+
+    private val themeText =
+        com.android.settingslib.Utils.getColorAttr(
+                context,
+                com.android.internal.R.attr.textColorPrimary
+            )
+            .defaultColor
+
+    // Internal listeners are part of the internal pipeline. External listeners (those registered
+    // with [MediaDeviceManager.addListener]) receive events after they have propagated through
+    // the internal pipeline.
+    // Another way to think of the distinction between internal and external listeners is the
+    // following. Internal listeners are listeners that MediaDataManager depends on, and external
+    // listeners are listeners that depend on MediaDataManager.
+    // TODO(b/159539991#comment5): Move internal listeners to separate package.
+    private val internalListeners: MutableSet<MediaDataManager.Listener> = mutableSetOf()
+    private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
+    // There should ONLY be at most one Smartspace media recommendation.
+    var smartspaceMediaData: SmartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA
+    @Keep private var smartspaceSession: SmartspaceSession? = null
+    private var allowMediaRecommendations = allowMediaRecommendations(context)
+
+    private val artworkWidth =
+        context.resources.getDimensionPixelSize(
+            com.android.internal.R.dimen.config_mediaMetadataBitmapMaxSize
+        )
+    private val artworkHeight =
+        context.resources.getDimensionPixelSize(R.dimen.qs_media_session_height_expanded)
+
+    @SuppressLint("WrongConstant") // sysui allowed to call STATUS_BAR_SERVICE
+    private val statusBarManager =
+        context.getSystemService(Context.STATUS_BAR_SERVICE) as StatusBarManager
+
+    /** Check whether this notification is an RCN */
+    private fun isRemoteCastNotification(sbn: StatusBarNotification): Boolean {
+        return sbn.notification.extras.containsKey(Notification.EXTRA_MEDIA_REMOTE_DEVICE)
+    }
+
+    @Inject
+    constructor(
+        context: Context,
+        threadFactory: ThreadFactory,
+        @Main uiExecutor: Executor,
+        @Main foregroundExecutor: DelayableExecutor,
+        mediaControllerFactory: MediaControllerFactory,
+        dumpManager: DumpManager,
+        broadcastDispatcher: BroadcastDispatcher,
+        mediaTimeoutListener: MediaTimeoutListener,
+        mediaResumeListener: MediaResumeListener,
+        mediaSessionBasedFilter: MediaSessionBasedFilter,
+        mediaDeviceManager: MediaDeviceManager,
+        mediaDataCombineLatest: MediaDataCombineLatest,
+        mediaDataFilter: LegacyMediaDataFilterImpl,
+        activityStarter: ActivityStarter,
+        smartspaceMediaDataProvider: SmartspaceMediaDataProvider,
+        clock: SystemClock,
+        tunerService: TunerService,
+        mediaFlags: MediaFlags,
+        logger: MediaUiEventLogger,
+        smartspaceManager: SmartspaceManager?,
+        keyguardUpdateMonitor: KeyguardUpdateMonitor,
+    ) : this(
+        context,
+        // Loading bitmap for UMO background can take longer time, so it cannot run on the default
+        // background thread. Use a custom thread for media.
+        threadFactory.buildExecutorOnNewThread(TAG),
+        uiExecutor,
+        foregroundExecutor,
+        mediaControllerFactory,
+        broadcastDispatcher,
+        dumpManager,
+        mediaTimeoutListener,
+        mediaResumeListener,
+        mediaSessionBasedFilter,
+        mediaDeviceManager,
+        mediaDataCombineLatest,
+        mediaDataFilter,
+        activityStarter,
+        smartspaceMediaDataProvider,
+        Utils.useMediaResumption(context),
+        Utils.useQsMediaPlayer(context),
+        clock,
+        tunerService,
+        mediaFlags,
+        logger,
+        smartspaceManager,
+        keyguardUpdateMonitor,
+    )
+
+    private val appChangeReceiver =
+        object : BroadcastReceiver() {
+            override fun onReceive(context: Context, intent: Intent) {
+                when (intent.action) {
+                    Intent.ACTION_PACKAGES_SUSPENDED -> {
+                        val packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST)
+                        packages?.forEach { removeAllForPackage(it) }
+                    }
+                    Intent.ACTION_PACKAGE_REMOVED,
+                    Intent.ACTION_PACKAGE_RESTARTED -> {
+                        intent.data?.encodedSchemeSpecificPart?.let { removeAllForPackage(it) }
+                    }
+                }
+            }
+        }
+
+    init {
+        dumpManager.registerDumpable(TAG, this)
+
+        // Initialize the internal processing pipeline. The listeners at the front of the pipeline
+        // are set as internal listeners so that they receive events. From there, events are
+        // propagated through the pipeline. The end of the pipeline is currently mediaDataFilter,
+        // so it is responsible for dispatching events to external listeners. To achieve this,
+        // external listeners that are registered with [MediaDataManager.addListener] are actually
+        // registered as listeners to mediaDataFilter.
+        addInternalListener(mediaTimeoutListener)
+        addInternalListener(mediaResumeListener)
+        addInternalListener(mediaSessionBasedFilter)
+        mediaSessionBasedFilter.addListener(mediaDeviceManager)
+        mediaSessionBasedFilter.addListener(mediaDataCombineLatest)
+        mediaDeviceManager.addListener(mediaDataCombineLatest)
+        mediaDataCombineLatest.addListener(mediaDataFilter)
+
+        // Set up links back into the pipeline for listeners that need to send events upstream.
+        mediaTimeoutListener.timeoutCallback = { key: String, timedOut: Boolean ->
+            setInactive(key, timedOut)
+        }
+        mediaTimeoutListener.stateCallback = { key: String, state: PlaybackState ->
+            updateState(key, state)
+        }
+        mediaTimeoutListener.sessionCallback = { key: String -> onSessionDestroyed(key) }
+        mediaResumeListener.setManager(this)
+        mediaDataFilter.mediaDataManager = this
+
+        val suspendFilter = IntentFilter(Intent.ACTION_PACKAGES_SUSPENDED)
+        broadcastDispatcher.registerReceiver(appChangeReceiver, suspendFilter, null, UserHandle.ALL)
+
+        val uninstallFilter =
+            IntentFilter().apply {
+                addAction(Intent.ACTION_PACKAGE_REMOVED)
+                addAction(Intent.ACTION_PACKAGE_RESTARTED)
+                addDataScheme("package")
+            }
+        // BroadcastDispatcher does not allow filters with data schemes
+        context.registerReceiver(appChangeReceiver, uninstallFilter)
+
+        // Register for Smartspace data updates.
+        smartspaceMediaDataProvider.registerListener(this)
+        smartspaceSession =
+            smartspaceManager?.createSmartspaceSession(
+                SmartspaceConfig.Builder(context, SMARTSPACE_UI_SURFACE_LABEL).build()
+            )
+        smartspaceSession?.let {
+            it.addOnTargetsAvailableListener(
+                // Use a main uiExecutor thread listening to Smartspace updates instead of using
+                // the existing background executor.
+                // SmartspaceSession has scheduled routine updates which can be unpredictable on
+                // test simulators, using the backgroundExecutor makes it's hard to test the threads
+                // numbers.
+                uiExecutor,
+                SmartspaceSession.OnTargetsAvailableListener { targets ->
+                    smartspaceMediaDataProvider.onTargetsAvailable(targets)
+                }
+            )
+        }
+        smartspaceSession?.let { it.requestSmartspaceUpdate() }
+        tunerService.addTunable(
+            object : TunerService.Tunable {
+                override fun onTuningChanged(key: String?, newValue: String?) {
+                    allowMediaRecommendations = allowMediaRecommendations(context)
+                    if (!allowMediaRecommendations) {
+                        dismissSmartspaceRecommendation(
+                            key = smartspaceMediaData.targetId,
+                            delay = 0L
+                        )
+                    }
+                }
+            },
+            Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION
+        )
+    }
+
+    override fun destroy() {
+        smartspaceMediaDataProvider.unregisterListener(this)
+        smartspaceSession?.close()
+        smartspaceSession = null
+        context.unregisterReceiver(appChangeReceiver)
+    }
+
+    override fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
+        if (useQsMediaPlayer && isMediaNotification(sbn)) {
+            var isNewlyActiveEntry = false
+            Assert.isMainThread()
+            val oldKey = findExistingEntry(key, sbn.packageName)
+            if (oldKey == null) {
+                val instanceId = logger.getNewInstanceId()
+                val temp =
+                    LOADING.copy(
+                        packageName = sbn.packageName,
+                        instanceId = instanceId,
+                        createdTimestampMillis = systemClock.currentTimeMillis(),
+                    )
+                mediaEntries.put(key, temp)
+                isNewlyActiveEntry = true
+            } else if (oldKey != key) {
+                // Resume -> active conversion; move to new key
+                val oldData = mediaEntries.remove(oldKey)!!
+                isNewlyActiveEntry = true
+                mediaEntries.put(key, oldData)
+            }
+            loadMediaData(key, sbn, oldKey, isNewlyActiveEntry)
+        } else {
+            onNotificationRemoved(key)
+        }
+    }
+
+    private fun removeAllForPackage(packageName: String) {
+        Assert.isMainThread()
+        val toRemove = mediaEntries.filter { it.value.packageName == packageName }
+        toRemove.forEach { removeEntry(it.key) }
+    }
+
+    override fun setResumeAction(key: String, action: Runnable?) {
+        mediaEntries.get(key)?.let {
+            it.resumeAction = action
+            it.hasCheckedForResume = true
+        }
+    }
+
+    override fun addResumptionControls(
+        userId: Int,
+        desc: MediaDescription,
+        action: Runnable,
+        token: MediaSession.Token,
+        appName: String,
+        appIntent: PendingIntent,
+        packageName: String
+    ) {
+        // Resume controls don't have a notification key, so store by package name instead
+        if (!mediaEntries.containsKey(packageName)) {
+            val instanceId = logger.getNewInstanceId()
+            val appUid =
+                try {
+                    context.packageManager.getApplicationInfo(packageName, 0)?.uid!!
+                } catch (e: PackageManager.NameNotFoundException) {
+                    Log.w(TAG, "Could not get app UID for $packageName", e)
+                    Process.INVALID_UID
+                }
+
+            val resumeData =
+                LOADING.copy(
+                    packageName = packageName,
+                    resumeAction = action,
+                    hasCheckedForResume = true,
+                    instanceId = instanceId,
+                    appUid = appUid,
+                    createdTimestampMillis = systemClock.currentTimeMillis(),
+                )
+            mediaEntries.put(packageName, resumeData)
+            logSingleVsMultipleMediaAdded(appUid, packageName, instanceId)
+            logger.logResumeMediaAdded(appUid, packageName, instanceId)
+        }
+        backgroundExecutor.execute {
+            loadMediaDataInBgForResumption(
+                userId,
+                desc,
+                action,
+                token,
+                appName,
+                appIntent,
+                packageName
+            )
+        }
+    }
+
+    /**
+     * Check if there is an existing entry that matches the key or package name. Returns the key
+     * that matches, or null if not found.
+     */
+    private fun findExistingEntry(key: String, packageName: String): String? {
+        if (mediaEntries.containsKey(key)) {
+            return key
+        }
+        // Check if we already had a resume player
+        if (mediaEntries.containsKey(packageName)) {
+            return packageName
+        }
+        return null
+    }
+
+    private fun loadMediaData(
+        key: String,
+        sbn: StatusBarNotification,
+        oldKey: String?,
+        isNewlyActiveEntry: Boolean = false,
+    ) {
+        backgroundExecutor.execute { loadMediaDataInBg(key, sbn, oldKey, isNewlyActiveEntry) }
+    }
+
+    /** Add a listener for changes in this class */
+    override fun addListener(listener: MediaDataManager.Listener) {
+        // mediaDataFilter is the current end of the internal pipeline. Register external
+        // listeners as listeners to it.
+        mediaDataFilter.addListener(listener)
+    }
+
+    /** Remove a listener for changes in this class */
+    override fun removeListener(listener: MediaDataManager.Listener) {
+        // Since mediaDataFilter is the current end of the internal pipelie, external listeners
+        // have been registered to it. So, they need to be removed from it too.
+        mediaDataFilter.removeListener(listener)
+    }
+
+    /** Add a listener for internal events. */
+    private fun addInternalListener(listener: MediaDataManager.Listener) =
+        internalListeners.add(listener)
+
+    /**
+     * Notify internal listeners of media loaded event.
+     *
+     * External listeners registered with [addListener] will be notified after the event propagates
+     * through the internal listener pipeline.
+     */
+    private fun notifyMediaDataLoaded(key: String, oldKey: String?, info: MediaData) {
+        internalListeners.forEach { it.onMediaDataLoaded(key, oldKey, info) }
+    }
+
+    /**
+     * Notify internal listeners of Smartspace media loaded event.
+     *
+     * External listeners registered with [addListener] will be notified after the event propagates
+     * through the internal listener pipeline.
+     */
+    private fun notifySmartspaceMediaDataLoaded(key: String, info: SmartspaceMediaData) {
+        internalListeners.forEach { it.onSmartspaceMediaDataLoaded(key, info) }
+    }
+
+    /**
+     * Notify internal listeners of media removed event.
+     *
+     * External listeners registered with [addListener] will be notified after the event propagates
+     * through the internal listener pipeline.
+     */
+    private fun notifyMediaDataRemoved(key: String) {
+        internalListeners.forEach { it.onMediaDataRemoved(key) }
+    }
+
+    /**
+     * Notify internal listeners of Smartspace media removed event.
+     *
+     * External listeners registered with [addListener] will be notified after the event propagates
+     * through the internal listener pipeline.
+     *
+     * @param immediately indicates should apply the UI changes immediately, otherwise wait until
+     *   the next refresh-round before UI becomes visible. Should only be true if the update is
+     *   initiated by user's interaction.
+     */
+    private fun notifySmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
+        internalListeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) }
+    }
+
+    /**
+     * Called whenever the player has been paused or stopped for a while, or swiped from QQS. This
+     * will make the player not active anymore, hiding it from QQS and Keyguard.
+     *
+     * @see MediaData.active
+     */
+    override fun setInactive(key: String, timedOut: Boolean, forceUpdate: Boolean) {
+        mediaEntries[key]?.let {
+            if (timedOut && !forceUpdate) {
+                // Only log this event when media expires on its own
+                logger.logMediaTimeout(it.appUid, it.packageName, it.instanceId)
+            }
+            if (it.active == !timedOut && !forceUpdate) {
+                if (it.resumption) {
+                    if (DEBUG) Log.d(TAG, "timing out resume player $key")
+                    dismissMediaData(key, 0L /* delay */)
+                }
+                return
+            }
+            // Update last active if media was still active.
+            if (it.active) {
+                it.lastActive = systemClock.elapsedRealtime()
+            }
+            it.active = !timedOut
+            if (DEBUG) Log.d(TAG, "Updating $key timedOut: $timedOut")
+            onMediaDataLoaded(key, key, it)
+        }
+
+        if (key == smartspaceMediaData.targetId) {
+            if (DEBUG) Log.d(TAG, "smartspace card expired")
+            dismissSmartspaceRecommendation(key, delay = 0L)
+        }
+    }
+
+    /** Called when the player's [PlaybackState] has been updated with new actions and/or state */
+    private fun updateState(key: String, state: PlaybackState) {
+        mediaEntries.get(key)?.let {
+            val token = it.token
+            if (token == null) {
+                if (DEBUG) Log.d(TAG, "State updated, but token was null")
+                return
+            }
+            val actions =
+                createActionsFromState(
+                    it.packageName,
+                    mediaControllerFactory.create(it.token),
+                    UserHandle(it.userId)
+                )
+
+            // Control buttons
+            // If flag is enabled and controller has a PlaybackState,
+            // create actions from session info
+            // otherwise, no need to update semantic actions.
+            val data =
+                if (actions != null) {
+                    it.copy(semanticActions = actions, isPlaying = isPlayingState(state.state))
+                } else {
+                    it.copy(isPlaying = isPlayingState(state.state))
+                }
+            if (DEBUG) Log.d(TAG, "State updated outside of notification")
+            onMediaDataLoaded(key, key, data)
+        }
+    }
+
+    private fun removeEntry(key: String, logEvent: Boolean = true) {
+        mediaEntries.remove(key)?.let {
+            if (logEvent) {
+                logger.logMediaRemoved(it.appUid, it.packageName, it.instanceId)
+            }
+        }
+        notifyMediaDataRemoved(key)
+    }
+
+    /** Dismiss a media entry. Returns false if the key was not found. */
+    override fun dismissMediaData(key: String, delay: Long): Boolean {
+        val existed = mediaEntries[key] != null
+        backgroundExecutor.execute {
+            mediaEntries[key]?.let { mediaData ->
+                if (mediaData.isLocalSession()) {
+                    mediaData.token?.let {
+                        val mediaController = mediaControllerFactory.create(it)
+                        mediaController.transportControls.stop()
+                    }
+                }
+            }
+        }
+        foregroundExecutor.executeDelayed({ removeEntry(key) }, delay)
+        return existed
+    }
+
+    /**
+     * Called whenever the recommendation has been expired or removed by the user. This will remove
+     * the recommendation card entirely from the carousel.
+     */
+    override fun dismissSmartspaceRecommendation(key: String, delay: Long) {
+        if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid()) {
+            // If this doesn't match, or we've already invalidated the data, no action needed
+            return
+        }
+
+        if (DEBUG) Log.d(TAG, "Dismissing Smartspace media target")
+        if (smartspaceMediaData.isActive) {
+            smartspaceMediaData =
+                EMPTY_SMARTSPACE_MEDIA_DATA.copy(
+                    targetId = smartspaceMediaData.targetId,
+                    instanceId = smartspaceMediaData.instanceId
+                )
+        }
+        foregroundExecutor.executeDelayed(
+            { notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = true) },
+            delay
+        )
+    }
+
+    /** Called when the recommendation card should no longer be visible in QQS or lockscreen */
+    override fun setRecommendationInactive(key: String) {
+        if (!mediaFlags.isPersistentSsCardEnabled()) {
+            Log.e(TAG, "Only persistent recommendation can be inactive!")
+            return
+        }
+        if (DEBUG) Log.d(TAG, "Setting smartspace recommendation inactive")
+
+        if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid()) {
+            // If this doesn't match, or we've already invalidated the data, no action needed
+            return
+        }
+
+        smartspaceMediaData = smartspaceMediaData.copy(isActive = false)
+        notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData)
+    }
+
+    private fun loadMediaDataInBgForResumption(
+        userId: Int,
+        desc: MediaDescription,
+        resumeAction: Runnable,
+        token: MediaSession.Token,
+        appName: String,
+        appIntent: PendingIntent,
+        packageName: String
+    ) {
+        if (desc.title.isNullOrBlank()) {
+            Log.e(TAG, "Description incomplete")
+            // Delete the placeholder entry
+            mediaEntries.remove(packageName)
+            return
+        }
+
+        if (DEBUG) {
+            Log.d(TAG, "adding track for $userId from browser: $desc")
+        }
+
+        val currentEntry = mediaEntries.get(packageName)
+        val appUid = currentEntry?.appUid ?: Process.INVALID_UID
+
+        // Album art
+        var artworkBitmap = desc.iconBitmap
+        if (artworkBitmap == null && desc.iconUri != null) {
+            artworkBitmap = loadBitmapFromUriForUser(desc.iconUri!!, userId, appUid, packageName)
+        }
+        val artworkIcon =
+            if (artworkBitmap != null) {
+                Icon.createWithBitmap(artworkBitmap)
+            } else {
+                null
+            }
+
+        val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId()
+        val isExplicit =
+            desc.extras?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) ==
+                MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT
+
+        val progress =
+            if (mediaFlags.isResumeProgressEnabled()) {
+                MediaDataUtils.getDescriptionProgress(desc.extras)
+            } else null
+
+        val mediaAction = getResumeMediaAction(resumeAction)
+        val lastActive = systemClock.elapsedRealtime()
+        val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L
+        foregroundExecutor.execute {
+            onMediaDataLoaded(
+                packageName,
+                null,
+                MediaData(
+                    userId,
+                    true,
+                    appName,
+                    null,
+                    desc.subtitle,
+                    desc.title,
+                    artworkIcon,
+                    listOf(mediaAction),
+                    listOf(0),
+                    MediaButton(playOrPause = mediaAction),
+                    packageName,
+                    token,
+                    appIntent,
+                    device = null,
+                    active = false,
+                    resumeAction = resumeAction,
+                    resumption = true,
+                    notificationKey = packageName,
+                    hasCheckedForResume = true,
+                    lastActive = lastActive,
+                    createdTimestampMillis = createdTimestampMillis,
+                    instanceId = instanceId,
+                    appUid = appUid,
+                    isExplicit = isExplicit,
+                    resumeProgress = progress,
+                )
+            )
+        }
+    }
+
+    fun loadMediaDataInBg(
+        key: String,
+        sbn: StatusBarNotification,
+        oldKey: String?,
+        isNewlyActiveEntry: Boolean = false,
+    ) {
+        val token =
+            sbn.notification.extras.getParcelable(
+                Notification.EXTRA_MEDIA_SESSION,
+                MediaSession.Token::class.java
+            )
+        if (token == null) {
+            return
+        }
+        val mediaController = mediaControllerFactory.create(token)
+        val metadata = mediaController.metadata
+        val notif: Notification = sbn.notification
+
+        val appInfo =
+            notif.extras.getParcelable(
+                Notification.EXTRA_BUILDER_APPLICATION_INFO,
+                ApplicationInfo::class.java
+            )
+                ?: getAppInfoFromPackage(sbn.packageName)
+
+        // App name
+        val appName = getAppName(sbn, appInfo)
+
+        // Song name
+        var song: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE)
+        if (song.isNullOrBlank()) {
+            song = metadata?.getString(MediaMetadata.METADATA_KEY_TITLE)
+        }
+        if (song.isNullOrBlank()) {
+            song = HybridGroupManager.resolveTitle(notif)
+        }
+        if (song.isNullOrBlank()) {
+            // For apps that don't include a title, log and add a placeholder
+            song = context.getString(R.string.controls_media_empty_title, appName)
+            try {
+                statusBarManager.logBlankMediaTitle(sbn.packageName, sbn.user.identifier)
+            } catch (e: RuntimeException) {
+                Log.e(TAG, "Error reporting blank media title for package ${sbn.packageName}")
+            }
+        }
+
+        // Album art
+        var artworkBitmap = metadata?.let { loadBitmapFromUri(it) }
+        if (artworkBitmap == null) {
+            artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ART)
+        }
+        if (artworkBitmap == null) {
+            artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART)
+        }
+        val artWorkIcon =
+            if (artworkBitmap == null) {
+                notif.getLargeIcon()
+            } else {
+                Icon.createWithBitmap(artworkBitmap)
+            }
+
+        // App Icon
+        val smallIcon = sbn.notification.smallIcon
+
+        // Explicit Indicator
+        var isExplicit = false
+        val mediaMetadataCompat = MediaMetadataCompat.fromMediaMetadata(metadata)
+        isExplicit =
+            mediaMetadataCompat?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) ==
+                MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT
+
+        // Artist name
+        var artist: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST)
+        if (artist.isNullOrBlank()) {
+            artist = HybridGroupManager.resolveText(notif)
+        }
+
+        // Device name (used for remote cast notifications)
+        var device: MediaDeviceData? = null
+        if (isRemoteCastNotification(sbn)) {
+            val extras = sbn.notification.extras
+            val deviceName = extras.getCharSequence(Notification.EXTRA_MEDIA_REMOTE_DEVICE, null)
+            val deviceIcon = extras.getInt(Notification.EXTRA_MEDIA_REMOTE_ICON, -1)
+            val deviceIntent =
+                extras.getParcelable(
+                    Notification.EXTRA_MEDIA_REMOTE_INTENT,
+                    PendingIntent::class.java
+                )
+            Log.d(TAG, "$key is RCN for $deviceName")
+
+            if (deviceName != null && deviceIcon > -1) {
+                // Name and icon must be present, but intent may be null
+                val enabled = deviceIntent != null && deviceIntent.isActivity
+                val deviceDrawable =
+                    Icon.createWithResource(sbn.packageName, deviceIcon)
+                        .loadDrawable(sbn.getPackageContext(context))
+                device =
+                    MediaDeviceData(
+                        enabled,
+                        deviceDrawable,
+                        deviceName,
+                        deviceIntent,
+                        showBroadcastButton = false
+                    )
+            }
+        }
+
+        // Control buttons
+        // If flag is enabled and controller has a PlaybackState, create actions from session info
+        // Otherwise, use the notification actions
+        var actionIcons: List<MediaAction> = emptyList()
+        var actionsToShowCollapsed: List<Int> = emptyList()
+        val semanticActions = createActionsFromState(sbn.packageName, mediaController, sbn.user)
+        if (semanticActions == null) {
+            val actions = createActionsFromNotification(sbn)
+            actionIcons = actions.first
+            actionsToShowCollapsed = actions.second
+        }
+
+        val playbackLocation =
+            if (isRemoteCastNotification(sbn)) MediaData.PLAYBACK_CAST_REMOTE
+            else if (
+                mediaController.playbackInfo?.playbackType ==
+                    MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL
+            )
+                MediaData.PLAYBACK_LOCAL
+            else MediaData.PLAYBACK_CAST_LOCAL
+        val isPlaying = mediaController.playbackState?.let { isPlayingState(it.state) } ?: null
+
+        val currentEntry = mediaEntries.get(key)
+        val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId()
+        val appUid = appInfo?.uid ?: Process.INVALID_UID
+
+        if (isNewlyActiveEntry) {
+            logSingleVsMultipleMediaAdded(appUid, sbn.packageName, instanceId)
+            logger.logActiveMediaAdded(appUid, sbn.packageName, instanceId, playbackLocation)
+        } else if (playbackLocation != currentEntry?.playbackLocation) {
+            logger.logPlaybackLocationChange(appUid, sbn.packageName, instanceId, playbackLocation)
+        }
+
+        val lastActive = systemClock.elapsedRealtime()
+        val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L
+        foregroundExecutor.execute {
+            val resumeAction: Runnable? = mediaEntries[key]?.resumeAction
+            val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true
+            val active = mediaEntries[key]?.active ?: true
+            onMediaDataLoaded(
+                key,
+                oldKey,
+                MediaData(
+                    sbn.normalizedUserId,
+                    true,
+                    appName,
+                    smallIcon,
+                    artist,
+                    song,
+                    artWorkIcon,
+                    actionIcons,
+                    actionsToShowCollapsed,
+                    semanticActions,
+                    sbn.packageName,
+                    token,
+                    notif.contentIntent,
+                    device,
+                    active,
+                    resumeAction = resumeAction,
+                    playbackLocation = playbackLocation,
+                    notificationKey = key,
+                    hasCheckedForResume = hasCheckedForResume,
+                    isPlaying = isPlaying,
+                    isClearable = !sbn.isOngoing,
+                    lastActive = lastActive,
+                    createdTimestampMillis = createdTimestampMillis,
+                    instanceId = instanceId,
+                    appUid = appUid,
+                    isExplicit = isExplicit,
+                )
+            )
+        }
+    }
+
+    private fun logSingleVsMultipleMediaAdded(
+        appUid: Int,
+        packageName: String,
+        instanceId: InstanceId
+    ) {
+        if (mediaEntries.size == 1) {
+            logger.logSingleMediaPlayerInCarousel(appUid, packageName, instanceId)
+        } else if (mediaEntries.size == 2) {
+            // Since this method is only called when there is a new media session added.
+            // logging needed once there is more than one media session in carousel.
+            logger.logMultipleMediaPlayersInCarousel(appUid, packageName, instanceId)
+        }
+    }
+
+    private fun getAppInfoFromPackage(packageName: String): ApplicationInfo? {
+        try {
+            return context.packageManager.getApplicationInfo(packageName, 0)
+        } catch (e: PackageManager.NameNotFoundException) {
+            Log.w(TAG, "Could not get app info for $packageName", e)
+        }
+        return null
+    }
+
+    private fun getAppName(sbn: StatusBarNotification, appInfo: ApplicationInfo?): String {
+        val name = sbn.notification.extras.getString(EXTRA_SUBSTITUTE_APP_NAME)
+        if (name != null) {
+            return name
+        }
+
+        return if (appInfo != null) {
+            context.packageManager.getApplicationLabel(appInfo).toString()
+        } else {
+            sbn.packageName
+        }
+    }
+
+    /** Generate action buttons based on notification actions */
+    private fun createActionsFromNotification(
+        sbn: StatusBarNotification
+    ): Pair<List<MediaAction>, List<Int>> {
+        val notif = sbn.notification
+        val actionIcons: MutableList<MediaAction> = ArrayList()
+        val actions = notif.actions
+        var actionsToShowCollapsed =
+            notif.extras.getIntArray(Notification.EXTRA_COMPACT_ACTIONS)?.toMutableList()
+                ?: mutableListOf()
+        if (actionsToShowCollapsed.size > MAX_COMPACT_ACTIONS) {
+            Log.e(
+                TAG,
+                "Too many compact actions for ${sbn.key}," +
+                    "limiting to first $MAX_COMPACT_ACTIONS"
+            )
+            actionsToShowCollapsed = actionsToShowCollapsed.subList(0, MAX_COMPACT_ACTIONS)
+        }
+
+        if (actions != null) {
+            for ((index, action) in actions.withIndex()) {
+                if (index == MAX_NOTIFICATION_ACTIONS) {
+                    Log.w(
+                        TAG,
+                        "Too many notification actions for ${sbn.key}," +
+                            " limiting to first $MAX_NOTIFICATION_ACTIONS"
+                    )
+                    break
+                }
+                if (action.getIcon() == null) {
+                    if (DEBUG) Log.i(TAG, "No icon for action $index ${action.title}")
+                    actionsToShowCollapsed.remove(index)
+                    continue
+                }
+                val runnable =
+                    if (action.actionIntent != null) {
+                        Runnable {
+                            if (action.actionIntent.isActivity) {
+                                activityStarter.startPendingIntentDismissingKeyguard(
+                                    action.actionIntent
+                                )
+                            } else if (action.isAuthenticationRequired()) {
+                                activityStarter.dismissKeyguardThenExecute(
+                                    {
+                                        var result = sendPendingIntent(action.actionIntent)
+                                        result
+                                    },
+                                    {},
+                                    true
+                                )
+                            } else {
+                                sendPendingIntent(action.actionIntent)
+                            }
+                        }
+                    } else {
+                        null
+                    }
+                val mediaActionIcon =
+                    if (action.getIcon()?.getType() == Icon.TYPE_RESOURCE) {
+                            Icon.createWithResource(sbn.packageName, action.getIcon()!!.getResId())
+                        } else {
+                            action.getIcon()
+                        }
+                        .setTint(themeText)
+                        .loadDrawable(context)
+                val mediaAction = MediaAction(mediaActionIcon, runnable, action.title, null)
+                actionIcons.add(mediaAction)
+            }
+        }
+        return Pair(actionIcons, actionsToShowCollapsed)
+    }
+
+    /**
+     * Generates action button info for this media session based on the PlaybackState
+     *
+     * @param packageName Package name for the media app
+     * @param controller MediaController for the current session
+     * @return a Pair consisting of a list of media actions, and a list of ints representing which
+     *
+     * ```
+     *      of those actions should be shown in the compact player
+     * ```
+     */
+    private fun createActionsFromState(
+        packageName: String,
+        controller: MediaController,
+        user: UserHandle
+    ): MediaButton? {
+        val state = controller.playbackState
+        if (state == null || !mediaFlags.areMediaSessionActionsEnabled(packageName, user)) {
+            return null
+        }
+
+        // First, check for standard actions
+        val playOrPause =
+            if (isConnectingState(state.state)) {
+                // Spinner needs to be animating to render anything. Start it here.
+                val drawable =
+                    context.getDrawable(com.android.internal.R.drawable.progress_small_material)
+                (drawable as Animatable).start()
+                MediaAction(
+                    drawable,
+                    null, // no action to perform when clicked
+                    context.getString(R.string.controls_media_button_connecting),
+                    context.getDrawable(R.drawable.ic_media_connecting_container),
+                    // Specify a rebind id to prevent the spinner from restarting on later binds.
+                    com.android.internal.R.drawable.progress_small_material
+                )
+            } else if (isPlayingState(state.state)) {
+                getStandardAction(controller, state.actions, PlaybackState.ACTION_PAUSE)
+            } else {
+                getStandardAction(controller, state.actions, PlaybackState.ACTION_PLAY)
+            }
+        val prevButton =
+            getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_PREVIOUS)
+        val nextButton =
+            getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_NEXT)
+
+        // Then, create a way to build any custom actions that will be needed
+        val customActions =
+            state.customActions
+                .asSequence()
+                .filterNotNull()
+                .map { getCustomAction(state, packageName, controller, it) }
+                .iterator()
+        fun nextCustomAction() = if (customActions.hasNext()) customActions.next() else null
+
+        // Finally, assign the remaining button slots: play/pause A B C D
+        // A = previous, else custom action (if not reserved)
+        // B = next, else custom action (if not reserved)
+        // C and D are always custom actions
+        val reservePrev =
+            controller.extras?.getBoolean(
+                MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV
+            ) == true
+        val reserveNext =
+            controller.extras?.getBoolean(
+                MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT
+            ) == true
+
+        val prevOrCustom =
+            if (prevButton != null) {
+                prevButton
+            } else if (!reservePrev) {
+                nextCustomAction()
+            } else {
+                null
+            }
+
+        val nextOrCustom =
+            if (nextButton != null) {
+                nextButton
+            } else if (!reserveNext) {
+                nextCustomAction()
+            } else {
+                null
+            }
+
+        return MediaButton(
+            playOrPause,
+            nextOrCustom,
+            prevOrCustom,
+            nextCustomAction(),
+            nextCustomAction(),
+            reserveNext,
+            reservePrev
+        )
+    }
+
+    /**
+     * Create a [MediaAction] for a given action and media session
+     *
+     * @param controller MediaController for the session
+     * @param stateActions The actions included with the session's [PlaybackState]
+     * @param action A [PlaybackState.Actions] value representing what action to generate. One of:
+     * ```
+     *      [PlaybackState.ACTION_PLAY]
+     *      [PlaybackState.ACTION_PAUSE]
+     *      [PlaybackState.ACTION_SKIP_TO_PREVIOUS]
+     *      [PlaybackState.ACTION_SKIP_TO_NEXT]
+     * @return
+     * ```
+     *
+     * A [MediaAction] with correct values set, or null if the state doesn't support it
+     */
+    private fun getStandardAction(
+        controller: MediaController,
+        stateActions: Long,
+        @PlaybackState.Actions action: Long
+    ): MediaAction? {
+        if (!includesAction(stateActions, action)) {
+            return null
+        }
+
+        return when (action) {
+            PlaybackState.ACTION_PLAY -> {
+                MediaAction(
+                    context.getDrawable(R.drawable.ic_media_play),
+                    { controller.transportControls.play() },
+                    context.getString(R.string.controls_media_button_play),
+                    context.getDrawable(R.drawable.ic_media_play_container)
+                )
+            }
+            PlaybackState.ACTION_PAUSE -> {
+                MediaAction(
+                    context.getDrawable(R.drawable.ic_media_pause),
+                    { controller.transportControls.pause() },
+                    context.getString(R.string.controls_media_button_pause),
+                    context.getDrawable(R.drawable.ic_media_pause_container)
+                )
+            }
+            PlaybackState.ACTION_SKIP_TO_PREVIOUS -> {
+                MediaAction(
+                    context.getDrawable(R.drawable.ic_media_prev),
+                    { controller.transportControls.skipToPrevious() },
+                    context.getString(R.string.controls_media_button_prev),
+                    null
+                )
+            }
+            PlaybackState.ACTION_SKIP_TO_NEXT -> {
+                MediaAction(
+                    context.getDrawable(R.drawable.ic_media_next),
+                    { controller.transportControls.skipToNext() },
+                    context.getString(R.string.controls_media_button_next),
+                    null
+                )
+            }
+            else -> null
+        }
+    }
+
+    /** Check whether the actions from a [PlaybackState] include a specific action */
+    private fun includesAction(stateActions: Long, @PlaybackState.Actions action: Long): Boolean {
+        if (
+            (action == PlaybackState.ACTION_PLAY || action == PlaybackState.ACTION_PAUSE) &&
+                (stateActions and PlaybackState.ACTION_PLAY_PAUSE > 0L)
+        ) {
+            return true
+        }
+        return (stateActions and action != 0L)
+    }
+
+    /** Get a [MediaAction] representing a [PlaybackState.CustomAction] */
+    private fun getCustomAction(
+        state: PlaybackState,
+        packageName: String,
+        controller: MediaController,
+        customAction: PlaybackState.CustomAction
+    ): MediaAction {
+        return MediaAction(
+            Icon.createWithResource(packageName, customAction.icon).loadDrawable(context),
+            { controller.transportControls.sendCustomAction(customAction, customAction.extras) },
+            customAction.name,
+            null
+        )
+    }
+
+    /** Load a bitmap from the various Art metadata URIs */
+    private fun loadBitmapFromUri(metadata: MediaMetadata): Bitmap? {
+        for (uri in ART_URIS) {
+            val uriString = metadata.getString(uri)
+            if (!TextUtils.isEmpty(uriString)) {
+                val albumArt = loadBitmapFromUri(Uri.parse(uriString))
+                if (albumArt != null) {
+                    if (DEBUG) Log.d(TAG, "loaded art from $uri")
+                    return albumArt
+                }
+            }
+        }
+        return null
+    }
+
+    private fun sendPendingIntent(intent: PendingIntent): Boolean {
+        return try {
+            val options = BroadcastOptions.makeBasic()
+            options.setInteractive(true)
+            options.setPendingIntentBackgroundActivityStartMode(
+                ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
+            )
+            intent.send(options.toBundle())
+            true
+        } catch (e: PendingIntent.CanceledException) {
+            Log.d(TAG, "Intent canceled", e)
+            false
+        }
+    }
+
+    /** Returns a bitmap if the user can access the given URI, else null */
+    private fun loadBitmapFromUriForUser(
+        uri: Uri,
+        userId: Int,
+        appUid: Int,
+        packageName: String,
+    ): Bitmap? {
+        try {
+            val ugm = UriGrantsManager.getService()
+            ugm.checkGrantUriPermission_ignoreNonSystem(
+                appUid,
+                packageName,
+                ContentProvider.getUriWithoutUserId(uri),
+                Intent.FLAG_GRANT_READ_URI_PERMISSION,
+                ContentProvider.getUserIdFromUri(uri, userId)
+            )
+            return loadBitmapFromUri(uri)
+        } catch (e: SecurityException) {
+            Log.e(TAG, "Failed to get URI permission: $e")
+        }
+        return null
+    }
+
+    /**
+     * Load a bitmap from a URI
+     *
+     * @param uri the uri to load
+     * @return bitmap, or null if couldn't be loaded
+     */
+    private fun loadBitmapFromUri(uri: Uri): Bitmap? {
+        // ImageDecoder requires a scheme of the following types
+        if (uri.scheme == null) {
+            return null
+        }
+
+        if (
+            !uri.scheme.equals(ContentResolver.SCHEME_CONTENT) &&
+                !uri.scheme.equals(ContentResolver.SCHEME_ANDROID_RESOURCE) &&
+                !uri.scheme.equals(ContentResolver.SCHEME_FILE)
+        ) {
+            return null
+        }
+
+        val source = ImageDecoder.createSource(context.contentResolver, uri)
+        return try {
+            ImageDecoder.decodeBitmap(source) { decoder, info, _ ->
+                val width = info.size.width
+                val height = info.size.height
+                val scale =
+                    MediaDataUtils.getScaleFactor(
+                        APair(width, height),
+                        APair(artworkWidth, artworkHeight)
+                    )
+
+                // Downscale if needed
+                if (scale != 0f && scale < 1) {
+                    decoder.setTargetSize((scale * width).toInt(), (scale * height).toInt())
+                }
+                decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
+            }
+        } catch (e: IOException) {
+            Log.e(TAG, "Unable to load bitmap", e)
+            null
+        } catch (e: RuntimeException) {
+            Log.e(TAG, "Unable to load bitmap", e)
+            null
+        }
+    }
+
+    private fun getResumeMediaAction(action: Runnable): MediaAction {
+        return MediaAction(
+            Icon.createWithResource(context, R.drawable.ic_media_play)
+                .setTint(themeText)
+                .loadDrawable(context),
+            action,
+            context.getString(R.string.controls_media_resume),
+            context.getDrawable(R.drawable.ic_media_play_container)
+        )
+    }
+
+    fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) =
+        traceSection("MediaDataManager#onMediaDataLoaded") {
+            Assert.isMainThread()
+            if (mediaEntries.containsKey(key)) {
+                // Otherwise this was removed already
+                mediaEntries.put(key, data)
+                notifyMediaDataLoaded(key, oldKey, data)
+            }
+        }
+
+    override fun onSmartspaceTargetsUpdated(targets: List<Parcelable>) {
+        if (!allowMediaRecommendations) {
+            if (DEBUG) Log.d(TAG, "Smartspace recommendation is disabled in Settings.")
+            return
+        }
+
+        val mediaTargets = targets.filterIsInstance<SmartspaceTarget>()
+        when (mediaTargets.size) {
+            0 -> {
+                if (!smartspaceMediaData.isActive) {
+                    return
+                }
+                if (DEBUG) {
+                    Log.d(TAG, "Set Smartspace media to be inactive for the data update")
+                }
+                if (mediaFlags.isPersistentSsCardEnabled()) {
+                    // Smartspace uses this signal to hide the card (e.g. when it expires or user
+                    // disconnects headphones), so treat as setting inactive when flag is on
+                    smartspaceMediaData = smartspaceMediaData.copy(isActive = false)
+                    notifySmartspaceMediaDataLoaded(
+                        smartspaceMediaData.targetId,
+                        smartspaceMediaData,
+                    )
+                } else {
+                    smartspaceMediaData =
+                        EMPTY_SMARTSPACE_MEDIA_DATA.copy(
+                            targetId = smartspaceMediaData.targetId,
+                            instanceId = smartspaceMediaData.instanceId,
+                        )
+                    notifySmartspaceMediaDataRemoved(
+                        smartspaceMediaData.targetId,
+                        immediately = false,
+                    )
+                }
+            }
+            1 -> {
+                val newMediaTarget = mediaTargets.get(0)
+                if (smartspaceMediaData.targetId == newMediaTarget.smartspaceTargetId) {
+                    // The same Smartspace updates can be received. Skip the duplicate updates.
+                    return
+                }
+                if (DEBUG) Log.d(TAG, "Forwarding Smartspace media update.")
+                smartspaceMediaData = toSmartspaceMediaData(newMediaTarget)
+                notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData)
+            }
+            else -> {
+                // There should NOT be more than 1 Smartspace media update. When it happens, it
+                // indicates a bad state or an error. Reset the status accordingly.
+                Log.wtf(TAG, "More than 1 Smartspace Media Update. Resetting the status...")
+                notifySmartspaceMediaDataRemoved(
+                    smartspaceMediaData.targetId,
+                    immediately = false,
+                )
+                smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA
+            }
+        }
+    }
+
+    override fun onNotificationRemoved(key: String) {
+        Assert.isMainThread()
+        val removed = mediaEntries.remove(key) ?: return
+        if (keyguardUpdateMonitor.isUserInLockdown(removed.userId)) {
+            logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
+        } else if (isAbleToResume(removed)) {
+            convertToResumePlayer(key, removed)
+        } else if (mediaFlags.isRetainingPlayersEnabled()) {
+            handlePossibleRemoval(key, removed, notificationRemoved = true)
+        } else {
+            notifyMediaDataRemoved(key)
+            logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
+        }
+    }
+
+    private fun onSessionDestroyed(key: String) {
+        if (DEBUG) Log.d(TAG, "session destroyed for $key")
+        val entry = mediaEntries.remove(key) ?: return
+        // Clear token since the session is no longer valid
+        val updated = entry.copy(token = null)
+        handlePossibleRemoval(key, updated)
+    }
+
+    private fun isAbleToResume(data: MediaData): Boolean {
+        val isEligibleForResume =
+            data.isLocalSession() ||
+                (mediaFlags.isRemoteResumeAllowed() &&
+                    data.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE)
+        return useMediaResumption && data.resumeAction != null && isEligibleForResume
+    }
+
+    /**
+     * Convert to resume state if the player is no longer valid and active, then notify listeners
+     * that the data was updated. Does not convert to resume state if the player is still valid, or
+     * if it was removed before becoming inactive. (Assumes that [removed] was removed from
+     * [mediaEntries] before this function was called)
+     */
+    private fun handlePossibleRemoval(
+        key: String,
+        removed: MediaData,
+        notificationRemoved: Boolean = false
+    ) {
+        val hasSession = removed.token != null
+        if (hasSession && removed.semanticActions != null) {
+            // The app was using session actions, and the session is still valid: keep player
+            if (DEBUG) Log.d(TAG, "Notification removed but using session actions $key")
+            mediaEntries.put(key, removed)
+            notifyMediaDataLoaded(key, key, removed)
+        } else if (!notificationRemoved && removed.semanticActions == null) {
+            // The app was using notification actions, and notif wasn't removed yet: keep player
+            if (DEBUG) Log.d(TAG, "Session destroyed but using notification actions $key")
+            mediaEntries.put(key, removed)
+            notifyMediaDataLoaded(key, key, removed)
+        } else if (removed.active && !isAbleToResume(removed)) {
+            // This player was still active - it didn't last long enough to time out,
+            // and its app doesn't normally support resume: remove
+            if (DEBUG) Log.d(TAG, "Removing still-active player $key")
+            notifyMediaDataRemoved(key)
+            logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
+        } else if (mediaFlags.isRetainingPlayersEnabled() || isAbleToResume(removed)) {
+            // Convert to resume
+            if (DEBUG) {
+                Log.d(
+                    TAG,
+                    "Notification ($notificationRemoved) and/or session " +
+                        "($hasSession) gone for inactive player $key"
+                )
+            }
+            convertToResumePlayer(key, removed)
+        } else {
+            // Retaining players flag is off and app doesn't support resume: remove player.
+            if (DEBUG) Log.d(TAG, "Removing player $key")
+            notifyMediaDataRemoved(key)
+            logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
+        }
+    }
+
+    /** Set the given [MediaData] as a resume state player and notify listeners */
+    private fun convertToResumePlayer(key: String, data: MediaData) {
+        if (DEBUG) Log.d(TAG, "Converting $key to resume")
+        // Resumption controls must have a title.
+        if (data.song.isNullOrBlank()) {
+            Log.e(TAG, "Description incomplete")
+            notifyMediaDataRemoved(key)
+            logger.logMediaRemoved(data.appUid, data.packageName, data.instanceId)
+            return
+        }
+        // Move to resume key (aka package name) if that key doesn't already exist.
+        val resumeAction = data.resumeAction?.let { getResumeMediaAction(it) }
+        val actions = resumeAction?.let { listOf(resumeAction) } ?: emptyList()
+        val launcherIntent =
+            context.packageManager.getLaunchIntentForPackage(data.packageName)?.let {
+                PendingIntent.getActivity(context, 0, it, PendingIntent.FLAG_IMMUTABLE)
+            }
+        val lastActive =
+            if (data.active) {
+                systemClock.elapsedRealtime()
+            } else {
+                data.lastActive
+            }
+        val updated =
+            data.copy(
+                token = null,
+                actions = actions,
+                semanticActions = MediaButton(playOrPause = resumeAction),
+                actionsToShowInCompact = listOf(0),
+                active = false,
+                resumption = true,
+                isPlaying = false,
+                isClearable = true,
+                clickIntent = launcherIntent,
+                lastActive = lastActive,
+            )
+        val pkg = data.packageName
+        val migrate = mediaEntries.put(pkg, updated) == null
+        // Notify listeners of "new" controls when migrating or removed and update when not
+        Log.d(TAG, "migrating? $migrate from $key -> $pkg")
+        if (migrate) {
+            notifyMediaDataLoaded(key = pkg, oldKey = key, info = updated)
+        } else {
+            // Since packageName is used for the key of the resumption controls, it is
+            // possible that another notification has already been reused for the resumption
+            // controls of this package. In this case, rather than renaming this player as
+            // packageName, just remove it and then send a update to the existing resumption
+            // controls.
+            notifyMediaDataRemoved(key)
+            notifyMediaDataLoaded(key = pkg, oldKey = pkg, info = updated)
+        }
+        logger.logActiveConvertedToResume(updated.appUid, pkg, updated.instanceId)
+
+        // Limit total number of resume controls
+        val resumeEntries = mediaEntries.filter { (key, data) -> data.resumption }
+        val numResume = resumeEntries.size
+        if (numResume > ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) {
+            resumeEntries
+                .toList()
+                .sortedBy { (key, data) -> data.lastActive }
+                .subList(0, numResume - ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS)
+                .forEach { (key, data) ->
+                    Log.d(TAG, "Removing excess control $key")
+                    mediaEntries.remove(key)
+                    notifyMediaDataRemoved(key)
+                    logger.logMediaRemoved(data.appUid, data.packageName, data.instanceId)
+                }
+        }
+    }
+
+    override fun setMediaResumptionEnabled(isEnabled: Boolean) {
+        if (useMediaResumption == isEnabled) {
+            return
+        }
+
+        useMediaResumption = isEnabled
+
+        if (!useMediaResumption) {
+            // Remove any existing resume controls
+            val filtered = mediaEntries.filter { !it.value.active }
+            filtered.forEach {
+                mediaEntries.remove(it.key)
+                notifyMediaDataRemoved(it.key)
+                logger.logMediaRemoved(it.value.appUid, it.value.packageName, it.value.instanceId)
+            }
+        }
+    }
+
+    /** Invoked when the user has dismissed the media carousel */
+    override fun onSwipeToDismiss() = mediaDataFilter.onSwipeToDismiss()
+
+    /** Are there any media notifications active, including the recommendations? */
+    override fun hasActiveMediaOrRecommendation() = mediaDataFilter.hasActiveMediaOrRecommendation()
+
+    /**
+     * Are there any media entries we should display, including the recommendations?
+     * - If resumption is enabled, this will include inactive players
+     * - If resumption is disabled, we only want to show active players
+     */
+    override fun hasAnyMediaOrRecommendation() = mediaDataFilter.hasAnyMediaOrRecommendation()
+
+    /** Are there any resume media notifications active, excluding the recommendations? */
+    override fun hasActiveMedia() = mediaDataFilter.hasActiveMedia()
+
+    /**
+     * Are there any resume media notifications active, excluding the recommendations?
+     * - If resumption is enabled, this will include inactive players
+     * - If resumption is disabled, we only want to show active players
+     */
+    override fun hasAnyMedia() = mediaDataFilter.hasAnyMedia()
+    override fun isRecommendationActive() = smartspaceMediaData.isActive
+
+    /**
+     * Converts the pass-in SmartspaceTarget to SmartspaceMediaData
+     *
+     * @return An empty SmartspaceMediaData with the valid target Id is returned if the
+     *   SmartspaceTarget's data is invalid.
+     */
+    private fun toSmartspaceMediaData(target: SmartspaceTarget): SmartspaceMediaData {
+        val baseAction: SmartspaceAction? = target.baseAction
+        val dismissIntent =
+            baseAction?.extras?.getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY) as Intent?
+
+        val isActive =
+            when {
+                !mediaFlags.isPersistentSsCardEnabled() -> true
+                baseAction == null -> true
+                else -> {
+                    val triggerSource = baseAction.extras?.getString(EXTRA_KEY_TRIGGER_SOURCE)
+                    triggerSource != EXTRA_VALUE_TRIGGER_PERIODIC
+                }
+            }
+
+        packageName(target)?.let {
+            return SmartspaceMediaData(
+                targetId = target.smartspaceTargetId,
+                isActive = isActive,
+                packageName = it,
+                cardAction = target.baseAction,
+                recommendations = target.iconGrid,
+                dismissIntent = dismissIntent,
+                headphoneConnectionTimeMillis = target.creationTimeMillis,
+                instanceId = logger.getNewInstanceId(),
+                expiryTimeMs = target.expiryTimeMillis,
+            )
+        }
+        return EMPTY_SMARTSPACE_MEDIA_DATA.copy(
+            targetId = target.smartspaceTargetId,
+            isActive = isActive,
+            dismissIntent = dismissIntent,
+            headphoneConnectionTimeMillis = target.creationTimeMillis,
+            instanceId = logger.getNewInstanceId(),
+            expiryTimeMs = target.expiryTimeMillis,
+        )
+    }
+
+    private fun packageName(target: SmartspaceTarget): String? {
+        val recommendationList = target.iconGrid
+        if (recommendationList == null || recommendationList.isEmpty()) {
+            Log.w(TAG, "Empty or null media recommendation list.")
+            return null
+        }
+        for (recommendation in recommendationList) {
+            val extras = recommendation.extras
+            extras?.let {
+                it.getString(EXTRAS_MEDIA_SOURCE_PACKAGE_NAME)?.let { packageName ->
+                    return packageName
+                }
+            }
+        }
+        Log.w(TAG, "No valid package name is provided.")
+        return null
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        pw.apply {
+            println("internalListeners: $internalListeners")
+            println("externalListeners: ${mediaDataFilter.listeners}")
+            println("mediaEntries: $mediaEntries")
+            println("useMediaResumption: $useMediaResumption")
+            println("allowMediaRecommendations: $allowMediaRecommendations")
+        }
+        mediaDeviceManager.dump(pw)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilter.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt
similarity index 76%
copy from packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilter.kt
copy to packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt
index bc539ef..a65db35 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilter.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 The Android Open Source Project
+ * 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.
@@ -24,6 +24,7 @@
 import com.android.internal.annotations.VisibleForTesting
 import com.android.systemui.broadcast.BroadcastSender
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.media.controls.data.repository.MediaFilterRepository
 import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_RESUME
 import com.android.systemui.media.controls.shared.model.MediaData
 import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
@@ -36,7 +37,6 @@
 import java.util.concurrent.Executor
 import java.util.concurrent.TimeUnit
 import javax.inject.Inject
-import kotlin.collections.LinkedHashMap
 
 private const val TAG = "MediaDataFilter"
 private const val DEBUG = true
@@ -46,14 +46,6 @@
 private const val RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY = "resumable_media_max_age_seconds"
 
 /**
- * Maximum age of a media control to re-activate on smartspace signal. If there is no media control
- * available within this time window, smartspace recommendations will be shown instead.
- */
-@VisibleForTesting
-internal val SMARTSPACE_MAX_AGE =
-    SystemProperties.getLong("debug.sysui.smartspace_max_age", TimeUnit.MINUTES.toMillis(30))
-
-/**
  * Filters data updates from [MediaDataCombineLatest] based on the current user ID, and handles user
  * switches (removing entries for the previous user, adding back entries for the current user). Also
  * filters out smartspace updates in favor of local recent media, when avaialble.
@@ -61,28 +53,23 @@
  * This is added at the end of the pipeline since we may still need to handle callbacks from
  * background users (e.g. timeouts).
  */
-class MediaDataFilter
+class MediaDataFilterImpl
 @Inject
 constructor(
     private val context: Context,
-    private val userTracker: UserTracker,
+    userTracker: UserTracker,
     private val broadcastSender: BroadcastSender,
     private val lockscreenUserManager: NotificationLockscreenUserManager,
     @Main private val executor: Executor,
     private val systemClock: SystemClock,
     private val logger: MediaUiEventLogger,
     private val mediaFlags: MediaFlags,
+    private val mediaFilterRepository: MediaFilterRepository,
 ) : MediaDataManager.Listener {
     private val _listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf()
-    internal val listeners: Set<MediaDataManager.Listener>
+    val listeners: Set<MediaDataManager.Listener>
         get() = _listeners.toSet()
-    internal lateinit var mediaDataManager: MediaDataManager
-
-    private val allEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
-    // The filtered userEntries, which will be a subset of all userEntries in MediaDataManager
-    private val userEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
-    private var smartspaceMediaData: SmartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA
-    private var reactivatedKey: String? = null
+    lateinit var mediaDataManager: MediaDataManager
 
     // Ensure the field (and associated reference) isn't removed during optimization.
     @KeepForWeakReference
@@ -110,9 +97,9 @@
         isSsReactivated: Boolean
     ) {
         if (oldKey != null && oldKey != key) {
-            allEntries.remove(oldKey)
+            mediaFilterRepository.removeMediaEntry(oldKey)
         }
-        allEntries.put(key, data)
+        mediaFilterRepository.addMediaEntry(key, data)
 
         if (
             !lockscreenUserManager.isCurrentProfile(data.userId) ||
@@ -122,9 +109,9 @@
         }
 
         if (oldKey != null && oldKey != key) {
-            userEntries.remove(oldKey)
+            mediaFilterRepository.removeSelectedUserMediaEntry(oldKey)
         }
-        userEntries.put(key, data)
+        mediaFilterRepository.addSelectedUserMediaEntry(key, data)
 
         // Notify listeners
         listeners.forEach { it.onMediaDataLoaded(key, oldKey, data) }
@@ -144,10 +131,12 @@
 
         // Override the pass-in value here, as the order of Smartspace card is only determined here.
         var shouldPrioritizeMutable = false
-        smartspaceMediaData = data
+        mediaFilterRepository.setRecommendation(data)
 
         // Before forwarding the smartspace target, first check if we have recently inactive media
-        val sorted = userEntries.toSortedMap(compareBy { userEntries.get(it)?.lastActive ?: -1 })
+        val selectedUserEntries = mediaFilterRepository.selectedUserEntries.value
+        val sorted =
+            selectedUserEntries.toSortedMap(compareBy { selectedUserEntries[it]?.lastActive ?: -1 })
         val timeSinceActive = timeSinceActiveForMostRecentMedia(sorted)
         var smartspaceMaxAgeMillis = SMARTSPACE_MAX_AGE
         data.cardAction?.extras?.let {
@@ -162,7 +151,10 @@
         val shouldTriggerResume =
             data.cardAction?.extras?.getBoolean(EXTRA_KEY_TRIGGER_RESUME, true) ?: true
         val shouldReactivate =
-            shouldTriggerResume && !hasActiveMedia() && hasAnyMedia() && data.isActive
+            shouldTriggerResume &&
+                !selectedUserEntries.any { it.value.active } &&
+                selectedUserEntries.isNotEmpty() &&
+                data.isActive
 
         if (timeSinceActive < smartspaceMaxAgeMillis) {
             // It could happen there are existing active media resume cards, then we don't need to
@@ -171,8 +163,8 @@
                 val lastActiveKey = sorted.lastKey() // most recently active
                 // Notify listeners to consider this media active
                 Log.d(TAG, "reactivating $lastActiveKey instead of smartspace")
-                reactivatedKey = lastActiveKey
-                val mediaData = sorted.get(lastActiveKey)!!.copy(active = true)
+                mediaFilterRepository.setReactivatedKey(lastActiveKey)
+                val mediaData = sorted[lastActiveKey]!!.copy(active = true)
                 logger.logRecommendationActivated(
                     mediaData.appUid,
                     mediaData.packageName,
@@ -199,6 +191,7 @@
             Log.d(TAG, "Invalid recommendation data. Skip showing the rec card")
             return
         }
+        val smartspaceMediaData = mediaFilterRepository.smartspaceMediaData.value
         logger.logRecommendationAdded(
             smartspaceMediaData.packageName,
             smartspaceMediaData.instanceId
@@ -207,8 +200,8 @@
     }
 
     override fun onMediaDataRemoved(key: String) {
-        allEntries.remove(key)
-        userEntries.remove(key)?.let {
+        mediaFilterRepository.removeMediaEntry(key)
+        mediaFilterRepository.removeSelectedUserMediaEntry(key)?.let {
             // Only notify listeners if something actually changed
             listeners.forEach { it.onMediaDataRemoved(key) }
         }
@@ -216,24 +209,26 @@
 
     override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
         // First check if we had reactivated media instead of forwarding smartspace
-        reactivatedKey?.let {
+        mediaFilterRepository.reactivatedKey.value?.let {
             val lastActiveKey = it
-            reactivatedKey = null
+            mediaFilterRepository.setReactivatedKey(null)
             Log.d(TAG, "expiring reactivated key $lastActiveKey")
             // Notify listeners to update with actual active value
-            userEntries.get(lastActiveKey)?.let { mediaData ->
-                listeners.forEach {
-                    it.onMediaDataLoaded(lastActiveKey, lastActiveKey, mediaData, immediately)
+            mediaFilterRepository.selectedUserEntries.value[lastActiveKey]?.let { mediaData ->
+                listeners.forEach { listener ->
+                    listener.onMediaDataLoaded(lastActiveKey, lastActiveKey, mediaData, immediately)
                 }
             }
         }
 
+        val smartspaceMediaData = mediaFilterRepository.smartspaceMediaData.value
         if (smartspaceMediaData.isActive) {
-            smartspaceMediaData =
+            mediaFilterRepository.setRecommendation(
                 EMPTY_SMARTSPACE_MEDIA_DATA.copy(
                     targetId = smartspaceMediaData.targetId,
                     instanceId = smartspaceMediaData.instanceId
                 )
+            )
         }
         listeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) }
     }
@@ -241,11 +236,11 @@
     @VisibleForTesting
     internal fun handleProfileChanged() {
         // TODO(b/317221348) re-add media removed when profile is available.
-        allEntries.forEach { (key, data) ->
+        mediaFilterRepository.allUserEntries.value.forEach { (key, data) ->
             if (!lockscreenUserManager.isProfileAvailable(data.userId)) {
                 // Only remove media when the profile is unavailable.
                 if (DEBUG) Log.d(TAG, "Removing $key after profile change")
-                userEntries.remove(key, data)
+                mediaFilterRepository.removeSelectedUserMediaEntry(key, data)
                 listeners.forEach { listener -> listener.onMediaDataRemoved(key) }
             }
         }
@@ -255,19 +250,19 @@
     internal fun handleUserSwitched() {
         // If the user changes, remove all current MediaData objects and inform listeners
         val listenersCopy = listeners
-        val keyCopy = userEntries.keys.toMutableList()
+        val keyCopy = mediaFilterRepository.selectedUserEntries.value.keys.toMutableList()
         // Clear the list first, to make sure callbacks from listeners if we have any entries
         // are up to date
-        userEntries.clear()
+        mediaFilterRepository.clearSelectedUserMedia()
         keyCopy.forEach {
             if (DEBUG) Log.d(TAG, "Removing $it after user change")
             listenersCopy.forEach { listener -> listener.onMediaDataRemoved(it) }
         }
 
-        allEntries.forEach { (key, data) ->
+        mediaFilterRepository.allUserEntries.value.forEach { (key, data) ->
             if (lockscreenUserManager.isCurrentProfile(data.userId)) {
                 if (DEBUG) Log.d(TAG, "Re-adding $key after user change")
-                userEntries.put(key, data)
+                mediaFilterRepository.addSelectedUserMediaEntry(key, data)
                 listenersCopy.forEach { listener -> listener.onMediaDataLoaded(key, null, data) }
             }
         }
@@ -276,11 +271,12 @@
     /** Invoked when the user has dismissed the media carousel */
     fun onSwipeToDismiss() {
         if (DEBUG) Log.d(TAG, "Media carousel swiped away")
-        val mediaKeys = userEntries.keys.toSet()
+        val mediaKeys = mediaFilterRepository.selectedUserEntries.value.keys.toSet()
         mediaKeys.forEach {
             // Force updates to listeners, needed for re-activated card
-            mediaDataManager.setTimedOut(it, timedOut = true, forceUpdate = true)
+            mediaDataManager.setInactive(it, timedOut = true, forceUpdate = true)
         }
+        val smartspaceMediaData = mediaFilterRepository.smartspaceMediaData.value
         if (smartspaceMediaData.isActive) {
             val dismissIntent = smartspaceMediaData.dismissIntent
             if (dismissIntent == null) {
@@ -298,14 +294,15 @@
             }
 
             if (mediaFlags.isPersistentSsCardEnabled()) {
-                smartspaceMediaData = smartspaceMediaData.copy(isActive = false)
+                mediaFilterRepository.setRecommendation(smartspaceMediaData.copy(isActive = false))
                 mediaDataManager.setRecommendationInactive(smartspaceMediaData.targetId)
             } else {
-                smartspaceMediaData =
+                mediaFilterRepository.setRecommendation(
                     EMPTY_SMARTSPACE_MEDIA_DATA.copy(
                         targetId = smartspaceMediaData.targetId,
                         instanceId = smartspaceMediaData.instanceId,
                     )
+                )
                 mediaDataManager.dismissSmartspaceRecommendation(
                     smartspaceMediaData.targetId,
                     delay = 0L,
@@ -314,29 +311,6 @@
         }
     }
 
-    /** Are there any active media entries, including the recommendation? */
-    fun hasActiveMediaOrRecommendation() =
-        userEntries.any { it.value.active } ||
-            (smartspaceMediaData.isActive &&
-                (smartspaceMediaData.isValid() || reactivatedKey != null))
-
-    /** Are there any media entries we should display? */
-    fun hasAnyMediaOrRecommendation(): Boolean {
-        val hasSmartspace =
-            if (mediaFlags.isPersistentSsCardEnabled()) {
-                smartspaceMediaData.isValid()
-            } else {
-                smartspaceMediaData.isActive && smartspaceMediaData.isValid()
-            }
-        return userEntries.isNotEmpty() || hasSmartspace
-    }
-
-    /** Are there any media notifications active (excluding the recommendation)? */
-    fun hasActiveMedia() = userEntries.any { it.value.active }
-
-    /** Are there any media entries we should display (excluding the recommendation)? */
-    fun hasAnyMedia() = userEntries.isNotEmpty()
-
     /** Add a listener for filtered [MediaData] changes */
     fun addListener(listener: MediaDataManager.Listener) = _listeners.add(listener)
 
@@ -346,7 +320,7 @@
     /**
      * Return the time since last active for the most-recent media.
      *
-     * @param sortedEntries userEntries sorted from the earliest to the most-recent.
+     * @param sortedEntries selectedUserEntries sorted from the earliest to the most-recent.
      * @return The duration in milliseconds from the most-recent media's last active timestamp to
      *   the present. MAX_VALUE will be returned if there is no media.
      */
@@ -359,6 +333,21 @@
 
         val now = systemClock.elapsedRealtime()
         val lastActiveKey = sortedEntries.lastKey() // most recently active
-        return sortedEntries.get(lastActiveKey)?.let { now - it.lastActive } ?: Long.MAX_VALUE
+        return sortedEntries[lastActiveKey]?.let { now - it.lastActive } ?: Long.MAX_VALUE
+    }
+
+    companion object {
+        /**
+         * Maximum age of a media control to re-activate on smartspace signal. If there is no media
+         * control available within this time window, smartspace recommendations will be shown
+         * instead.
+         */
+        @VisibleForTesting
+        internal val SMARTSPACE_MAX_AGE: Long
+            get() =
+                SystemProperties.getLong(
+                    "debug.sysui.smartspace_max_age",
+                    TimeUnit.MINUTES.toMillis(30)
+                )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManager.kt
index 865c49e..2b1070c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManager.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 The Android Open Source Project
+ * 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.
@@ -16,424 +16,39 @@
 
 package com.android.systemui.media.controls.domain.pipeline
 
-import android.annotation.SuppressLint
-import android.app.ActivityOptions
-import android.app.BroadcastOptions
-import android.app.Notification
-import android.app.Notification.EXTRA_SUBSTITUTE_APP_NAME
 import android.app.PendingIntent
-import android.app.StatusBarManager
-import android.app.UriGrantsManager
-import android.app.smartspace.SmartspaceAction
-import android.app.smartspace.SmartspaceConfig
-import android.app.smartspace.SmartspaceManager
-import android.app.smartspace.SmartspaceSession
-import android.app.smartspace.SmartspaceTarget
-import android.content.BroadcastReceiver
-import android.content.ContentProvider
-import android.content.ContentResolver
-import android.content.Context
-import android.content.Intent
-import android.content.IntentFilter
-import android.content.pm.ApplicationInfo
-import android.content.pm.PackageManager
-import android.graphics.Bitmap
-import android.graphics.ImageDecoder
-import android.graphics.drawable.Animatable
-import android.graphics.drawable.Icon
 import android.media.MediaDescription
-import android.media.MediaMetadata
-import android.media.session.MediaController
 import android.media.session.MediaSession
-import android.media.session.PlaybackState
-import android.net.Uri
-import android.os.Parcelable
-import android.os.Process
-import android.os.UserHandle
-import android.provider.Settings
 import android.service.notification.StatusBarNotification
-import android.support.v4.media.MediaMetadataCompat
-import android.text.TextUtils
-import android.util.Log
-import android.util.Pair as APair
-import androidx.media.utils.MediaConstants
-import com.android.app.tracing.traceSection
-import com.android.internal.annotations.Keep
-import com.android.internal.logging.InstanceId
-import com.android.keyguard.KeyguardUpdateMonitor
-import com.android.systemui.Dumpable
-import com.android.systemui.broadcast.BroadcastDispatcher
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.media.controls.domain.resume.MediaResumeListener
-import com.android.systemui.media.controls.domain.resume.ResumeMediaBrowser
-import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_SOURCE
-import com.android.systemui.media.controls.shared.model.EXTRA_VALUE_TRIGGER_PERIODIC
-import com.android.systemui.media.controls.shared.model.MediaAction
-import com.android.systemui.media.controls.shared.model.MediaButton
 import com.android.systemui.media.controls.shared.model.MediaData
-import com.android.systemui.media.controls.shared.model.MediaDeviceData
 import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
-import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvider
-import com.android.systemui.media.controls.ui.view.MediaViewHolder
-import com.android.systemui.media.controls.util.MediaControllerFactory
-import com.android.systemui.media.controls.util.MediaDataUtils
-import com.android.systemui.media.controls.util.MediaFlags
-import com.android.systemui.media.controls.util.MediaUiEventLogger
-import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.plugins.BcSmartspaceDataPlugin
-import com.android.systemui.res.R
-import com.android.systemui.statusbar.NotificationMediaManager.isConnectingState
-import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
-import com.android.systemui.statusbar.notification.row.HybridGroupManager
-import com.android.systemui.tuner.TunerService
-import com.android.systemui.util.Assert
-import com.android.systemui.util.Utils
-import com.android.systemui.util.concurrency.DelayableExecutor
-import com.android.systemui.util.concurrency.ThreadFactory
-import com.android.systemui.util.time.SystemClock
-import java.io.IOException
-import java.io.PrintWriter
-import java.util.concurrent.Executor
-import javax.inject.Inject
 
-// URI fields to try loading album art from
-private val ART_URIS =
-    arrayOf(
-        MediaMetadata.METADATA_KEY_ALBUM_ART_URI,
-        MediaMetadata.METADATA_KEY_ART_URI,
-        MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI
-    )
+/** Facilitates management and loading of Media Data, ready for binding. */
+interface MediaDataManager {
 
-private const val TAG = "MediaDataManager"
-private const val DEBUG = true
-private const val EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY = "dismiss_intent"
+    /** Add a listener for changes in this class */
+    fun addListener(listener: Listener)
 
-private val LOADING =
-    MediaData(
-        userId = -1,
-        initialized = false,
-        app = null,
-        appIcon = null,
-        artist = null,
-        song = null,
-        artwork = null,
-        actions = emptyList(),
-        actionsToShowInCompact = emptyList(),
-        packageName = "INVALID",
-        token = null,
-        clickIntent = null,
-        device = null,
-        active = true,
-        resumeAction = null,
-        instanceId = InstanceId.fakeInstanceId(-1),
-        appUid = Process.INVALID_UID
-    )
+    /** Remove a listener for changes in this class */
+    fun removeListener(listener: Listener)
 
-internal val EMPTY_SMARTSPACE_MEDIA_DATA =
-    SmartspaceMediaData(
-        targetId = "INVALID",
-        isActive = false,
-        packageName = "INVALID",
-        cardAction = null,
-        recommendations = emptyList(),
-        dismissIntent = null,
-        headphoneConnectionTimeMillis = 0,
-        instanceId = InstanceId.fakeInstanceId(-1),
-        expiryTimeMs = 0,
-    )
+    /**
+     * Called whenever the player has been paused or stopped for a while, or swiped from QQS. This
+     * will make the player not active anymore, hiding it from QQS and Keyguard.
+     *
+     * @see MediaData.active
+     */
+    fun setInactive(key: String, timedOut: Boolean, forceUpdate: Boolean = false)
 
-const val MEDIA_TITLE_ERROR_MESSAGE = "Invalid media data: title is null or blank."
+    /** Invoked when media notification is added. */
+    fun onNotificationAdded(key: String, sbn: StatusBarNotification)
 
-fun isMediaNotification(sbn: StatusBarNotification): Boolean {
-    return sbn.notification.isMediaNotification()
-}
+    fun destroy()
 
-/**
- * Allow recommendations from smartspace to show in media controls. Requires
- * [Utils.useQsMediaPlayer] to be enabled. On by default, but can be disabled by setting to 0
- */
-private fun allowMediaRecommendations(context: Context): Boolean {
-    val flag =
-        Settings.Secure.getInt(
-            context.contentResolver,
-            Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
-            1
-        )
-    return Utils.useQsMediaPlayer(context) && flag > 0
-}
+    /** Sets resume action. */
+    fun setResumeAction(key: String, action: Runnable?)
 
-/** A class that facilitates management and loading of Media Data, ready for binding. */
-@SysUISingleton
-class MediaDataManager(
-    private val context: Context,
-    @Background private val backgroundExecutor: Executor,
-    @Main private val uiExecutor: Executor,
-    @Main private val foregroundExecutor: DelayableExecutor,
-    private val mediaControllerFactory: MediaControllerFactory,
-    private val broadcastDispatcher: BroadcastDispatcher,
-    dumpManager: DumpManager,
-    mediaTimeoutListener: MediaTimeoutListener,
-    mediaResumeListener: MediaResumeListener,
-    mediaSessionBasedFilter: MediaSessionBasedFilter,
-    mediaDeviceManager: MediaDeviceManager,
-    mediaDataCombineLatest: MediaDataCombineLatest,
-    private val mediaDataFilter: MediaDataFilter,
-    private val activityStarter: ActivityStarter,
-    private val smartspaceMediaDataProvider: SmartspaceMediaDataProvider,
-    private var useMediaResumption: Boolean,
-    private val useQsMediaPlayer: Boolean,
-    private val systemClock: SystemClock,
-    private val tunerService: TunerService,
-    private val mediaFlags: MediaFlags,
-    private val logger: MediaUiEventLogger,
-    private val smartspaceManager: SmartspaceManager?,
-    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
-) : Dumpable, BcSmartspaceDataPlugin.SmartspaceTargetListener {
-
-    companion object {
-        // UI surface label for subscribing Smartspace updates.
-        @JvmField val SMARTSPACE_UI_SURFACE_LABEL = "media_data_manager"
-
-        // Smartspace package name's extra key.
-        @JvmField val EXTRAS_MEDIA_SOURCE_PACKAGE_NAME = "package_name"
-
-        // Maximum number of actions allowed in compact view
-        @JvmField val MAX_COMPACT_ACTIONS = 3
-
-        // Maximum number of actions allowed in expanded view
-        @JvmField val MAX_NOTIFICATION_ACTIONS = MediaViewHolder.genericButtonIds.size
-    }
-
-    private val themeText =
-        com.android.settingslib.Utils.getColorAttr(
-                context,
-                com.android.internal.R.attr.textColorPrimary
-            )
-            .defaultColor
-
-    // Internal listeners are part of the internal pipeline. External listeners (those registered
-    // with [MediaDeviceManager.addListener]) receive events after they have propagated through
-    // the internal pipeline.
-    // Another way to think of the distinction between internal and external listeners is the
-    // following. Internal listeners are listeners that MediaDataManager depends on, and external
-    // listeners are listeners that depend on MediaDataManager.
-    // TODO(b/159539991#comment5): Move internal listeners to separate package.
-    private val internalListeners: MutableSet<Listener> = mutableSetOf()
-    private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
-    // There should ONLY be at most one Smartspace media recommendation.
-    var smartspaceMediaData: SmartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA
-    @Keep private var smartspaceSession: SmartspaceSession? = null
-    private var allowMediaRecommendations = allowMediaRecommendations(context)
-
-    private val artworkWidth =
-        context.resources.getDimensionPixelSize(
-            com.android.internal.R.dimen.config_mediaMetadataBitmapMaxSize
-        )
-    private val artworkHeight =
-        context.resources.getDimensionPixelSize(R.dimen.qs_media_session_height_expanded)
-
-    @SuppressLint("WrongConstant") // sysui allowed to call STATUS_BAR_SERVICE
-    private val statusBarManager =
-        context.getSystemService(Context.STATUS_BAR_SERVICE) as StatusBarManager
-
-    /** Check whether this notification is an RCN */
-    private fun isRemoteCastNotification(sbn: StatusBarNotification): Boolean {
-        return sbn.notification.extras.containsKey(Notification.EXTRA_MEDIA_REMOTE_DEVICE)
-    }
-
-    @Inject
-    constructor(
-        context: Context,
-        threadFactory: ThreadFactory,
-        @Main uiExecutor: Executor,
-        @Main foregroundExecutor: DelayableExecutor,
-        mediaControllerFactory: MediaControllerFactory,
-        dumpManager: DumpManager,
-        broadcastDispatcher: BroadcastDispatcher,
-        mediaTimeoutListener: MediaTimeoutListener,
-        mediaResumeListener: MediaResumeListener,
-        mediaSessionBasedFilter: MediaSessionBasedFilter,
-        mediaDeviceManager: MediaDeviceManager,
-        mediaDataCombineLatest: MediaDataCombineLatest,
-        mediaDataFilter: MediaDataFilter,
-        activityStarter: ActivityStarter,
-        smartspaceMediaDataProvider: SmartspaceMediaDataProvider,
-        clock: SystemClock,
-        tunerService: TunerService,
-        mediaFlags: MediaFlags,
-        logger: MediaUiEventLogger,
-        smartspaceManager: SmartspaceManager?,
-        keyguardUpdateMonitor: KeyguardUpdateMonitor,
-    ) : this(
-        context,
-        // Loading bitmap for UMO background can take longer time, so it cannot run on the default
-        // background thread. Use a custom thread for media.
-        threadFactory.buildExecutorOnNewThread(TAG),
-        uiExecutor,
-        foregroundExecutor,
-        mediaControllerFactory,
-        broadcastDispatcher,
-        dumpManager,
-        mediaTimeoutListener,
-        mediaResumeListener,
-        mediaSessionBasedFilter,
-        mediaDeviceManager,
-        mediaDataCombineLatest,
-        mediaDataFilter,
-        activityStarter,
-        smartspaceMediaDataProvider,
-        Utils.useMediaResumption(context),
-        Utils.useQsMediaPlayer(context),
-        clock,
-        tunerService,
-        mediaFlags,
-        logger,
-        smartspaceManager,
-        keyguardUpdateMonitor,
-    )
-
-    private val appChangeReceiver =
-        object : BroadcastReceiver() {
-            override fun onReceive(context: Context, intent: Intent) {
-                when (intent.action) {
-                    Intent.ACTION_PACKAGES_SUSPENDED -> {
-                        val packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST)
-                        packages?.forEach { removeAllForPackage(it) }
-                    }
-                    Intent.ACTION_PACKAGE_REMOVED,
-                    Intent.ACTION_PACKAGE_RESTARTED -> {
-                        intent.data?.encodedSchemeSpecificPart?.let { removeAllForPackage(it) }
-                    }
-                }
-            }
-        }
-
-    init {
-        dumpManager.registerDumpable(TAG, this)
-
-        // Initialize the internal processing pipeline. The listeners at the front of the pipeline
-        // are set as internal listeners so that they receive events. From there, events are
-        // propagated through the pipeline. The end of the pipeline is currently mediaDataFilter,
-        // so it is responsible for dispatching events to external listeners. To achieve this,
-        // external listeners that are registered with [MediaDataManager.addListener] are actually
-        // registered as listeners to mediaDataFilter.
-        addInternalListener(mediaTimeoutListener)
-        addInternalListener(mediaResumeListener)
-        addInternalListener(mediaSessionBasedFilter)
-        mediaSessionBasedFilter.addListener(mediaDeviceManager)
-        mediaSessionBasedFilter.addListener(mediaDataCombineLatest)
-        mediaDeviceManager.addListener(mediaDataCombineLatest)
-        mediaDataCombineLatest.addListener(mediaDataFilter)
-
-        // Set up links back into the pipeline for listeners that need to send events upstream.
-        mediaTimeoutListener.timeoutCallback = { key: String, timedOut: Boolean ->
-            setTimedOut(key, timedOut)
-        }
-        mediaTimeoutListener.stateCallback = { key: String, state: PlaybackState ->
-            updateState(key, state)
-        }
-        mediaTimeoutListener.sessionCallback = { key: String -> onSessionDestroyed(key) }
-        mediaResumeListener.setManager(this)
-        mediaDataFilter.mediaDataManager = this
-
-        val suspendFilter = IntentFilter(Intent.ACTION_PACKAGES_SUSPENDED)
-        broadcastDispatcher.registerReceiver(appChangeReceiver, suspendFilter, null, UserHandle.ALL)
-
-        val uninstallFilter =
-            IntentFilter().apply {
-                addAction(Intent.ACTION_PACKAGE_REMOVED)
-                addAction(Intent.ACTION_PACKAGE_RESTARTED)
-                addDataScheme("package")
-            }
-        // BroadcastDispatcher does not allow filters with data schemes
-        context.registerReceiver(appChangeReceiver, uninstallFilter)
-
-        // Register for Smartspace data updates.
-        smartspaceMediaDataProvider.registerListener(this)
-        smartspaceSession =
-            smartspaceManager?.createSmartspaceSession(
-                SmartspaceConfig.Builder(context, SMARTSPACE_UI_SURFACE_LABEL).build()
-            )
-        smartspaceSession?.let {
-            it.addOnTargetsAvailableListener(
-                // Use a main uiExecutor thread listening to Smartspace updates instead of using
-                // the existing background executor.
-                // SmartspaceSession has scheduled routine updates which can be unpredictable on
-                // test simulators, using the backgroundExecutor makes it's hard to test the threads
-                // numbers.
-                uiExecutor,
-                SmartspaceSession.OnTargetsAvailableListener { targets ->
-                    smartspaceMediaDataProvider.onTargetsAvailable(targets)
-                }
-            )
-        }
-        smartspaceSession?.let { it.requestSmartspaceUpdate() }
-        tunerService.addTunable(
-            object : TunerService.Tunable {
-                override fun onTuningChanged(key: String?, newValue: String?) {
-                    allowMediaRecommendations = allowMediaRecommendations(context)
-                    if (!allowMediaRecommendations) {
-                        dismissSmartspaceRecommendation(
-                            key = smartspaceMediaData.targetId,
-                            delay = 0L
-                        )
-                    }
-                }
-            },
-            Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION
-        )
-    }
-
-    fun destroy() {
-        smartspaceMediaDataProvider.unregisterListener(this)
-        smartspaceSession?.close()
-        smartspaceSession = null
-        context.unregisterReceiver(appChangeReceiver)
-    }
-
-    fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
-        if (useQsMediaPlayer && isMediaNotification(sbn)) {
-            var isNewlyActiveEntry = false
-            Assert.isMainThread()
-            val oldKey = findExistingEntry(key, sbn.packageName)
-            if (oldKey == null) {
-                val instanceId = logger.getNewInstanceId()
-                val temp =
-                    LOADING.copy(
-                        packageName = sbn.packageName,
-                        instanceId = instanceId,
-                        createdTimestampMillis = systemClock.currentTimeMillis(),
-                    )
-                mediaEntries.put(key, temp)
-                isNewlyActiveEntry = true
-            } else if (oldKey != key) {
-                // Resume -> active conversion; move to new key
-                val oldData = mediaEntries.remove(oldKey)!!
-                isNewlyActiveEntry = true
-                mediaEntries.put(key, oldData)
-            }
-            loadMediaData(key, sbn, oldKey, isNewlyActiveEntry)
-        } else {
-            onNotificationRemoved(key)
-        }
-    }
-
-    private fun removeAllForPackage(packageName: String) {
-        Assert.isMainThread()
-        val toRemove = mediaEntries.filter { it.value.packageName == packageName }
-        toRemove.forEach { removeEntry(it.key) }
-    }
-
-    fun setResumeAction(key: String, action: Runnable?) {
-        mediaEntries.get(key)?.let {
-            it.resumeAction = action
-            it.hasCheckedForResume = true
-        }
-    }
-
+    /** Adds resume media data. */
     fun addResumptionControls(
         userId: Int,
         desc: MediaDescription,
@@ -442,1184 +57,45 @@
         appName: String,
         appIntent: PendingIntent,
         packageName: String
-    ) {
-        // Resume controls don't have a notification key, so store by package name instead
-        if (!mediaEntries.containsKey(packageName)) {
-            val instanceId = logger.getNewInstanceId()
-            val appUid =
-                try {
-                    context.packageManager.getApplicationInfo(packageName, 0)?.uid!!
-                } catch (e: PackageManager.NameNotFoundException) {
-                    Log.w(TAG, "Could not get app UID for $packageName", e)
-                    Process.INVALID_UID
-                }
-
-            val resumeData =
-                LOADING.copy(
-                    packageName = packageName,
-                    resumeAction = action,
-                    hasCheckedForResume = true,
-                    instanceId = instanceId,
-                    appUid = appUid,
-                    createdTimestampMillis = systemClock.currentTimeMillis(),
-                )
-            mediaEntries.put(packageName, resumeData)
-            logSingleVsMultipleMediaAdded(appUid, packageName, instanceId)
-            logger.logResumeMediaAdded(appUid, packageName, instanceId)
-        }
-        backgroundExecutor.execute {
-            loadMediaDataInBgForResumption(
-                userId,
-                desc,
-                action,
-                token,
-                appName,
-                appIntent,
-                packageName
-            )
-        }
-    }
-
-    /**
-     * Check if there is an existing entry that matches the key or package name. Returns the key
-     * that matches, or null if not found.
-     */
-    private fun findExistingEntry(key: String, packageName: String): String? {
-        if (mediaEntries.containsKey(key)) {
-            return key
-        }
-        // Check if we already had a resume player
-        if (mediaEntries.containsKey(packageName)) {
-            return packageName
-        }
-        return null
-    }
-
-    private fun loadMediaData(
-        key: String,
-        sbn: StatusBarNotification,
-        oldKey: String?,
-        isNewlyActiveEntry: Boolean = false,
-    ) {
-        backgroundExecutor.execute { loadMediaDataInBg(key, sbn, oldKey, isNewlyActiveEntry) }
-    }
-
-    /** Add a listener for changes in this class */
-    fun addListener(listener: Listener) {
-        // mediaDataFilter is the current end of the internal pipeline. Register external
-        // listeners as listeners to it.
-        mediaDataFilter.addListener(listener)
-    }
-
-    /** Remove a listener for changes in this class */
-    fun removeListener(listener: Listener) {
-        // Since mediaDataFilter is the current end of the internal pipelie, external listeners
-        // have been registered to it. So, they need to be removed from it too.
-        mediaDataFilter.removeListener(listener)
-    }
-
-    /** Add a listener for internal events. */
-    private fun addInternalListener(listener: Listener) = internalListeners.add(listener)
-
-    /**
-     * Notify internal listeners of media loaded event.
-     *
-     * External listeners registered with [addListener] will be notified after the event propagates
-     * through the internal listener pipeline.
-     */
-    private fun notifyMediaDataLoaded(key: String, oldKey: String?, info: MediaData) {
-        internalListeners.forEach { it.onMediaDataLoaded(key, oldKey, info) }
-    }
-
-    /**
-     * Notify internal listeners of Smartspace media loaded event.
-     *
-     * External listeners registered with [addListener] will be notified after the event propagates
-     * through the internal listener pipeline.
-     */
-    private fun notifySmartspaceMediaDataLoaded(key: String, info: SmartspaceMediaData) {
-        internalListeners.forEach { it.onSmartspaceMediaDataLoaded(key, info) }
-    }
-
-    /**
-     * Notify internal listeners of media removed event.
-     *
-     * External listeners registered with [addListener] will be notified after the event propagates
-     * through the internal listener pipeline.
-     */
-    private fun notifyMediaDataRemoved(key: String) {
-        internalListeners.forEach { it.onMediaDataRemoved(key) }
-    }
-
-    /**
-     * Notify internal listeners of Smartspace media removed event.
-     *
-     * External listeners registered with [addListener] will be notified after the event propagates
-     * through the internal listener pipeline.
-     *
-     * @param immediately indicates should apply the UI changes immediately, otherwise wait until
-     *   the next refresh-round before UI becomes visible. Should only be true if the update is
-     *   initiated by user's interaction.
-     */
-    private fun notifySmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
-        internalListeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) }
-    }
-
-    /**
-     * Called whenever the player has been paused or stopped for a while, or swiped from QQS. This
-     * will make the player not active anymore, hiding it from QQS and Keyguard.
-     *
-     * @see MediaData.active
-     */
-    internal fun setTimedOut(key: String, timedOut: Boolean, forceUpdate: Boolean = false) {
-        mediaEntries[key]?.let {
-            if (timedOut && !forceUpdate) {
-                // Only log this event when media expires on its own
-                logger.logMediaTimeout(it.appUid, it.packageName, it.instanceId)
-            }
-            if (it.active == !timedOut && !forceUpdate) {
-                if (it.resumption) {
-                    if (DEBUG) Log.d(TAG, "timing out resume player $key")
-                    dismissMediaData(key, 0L /* delay */)
-                }
-                return
-            }
-            // Update last active if media was still active.
-            if (it.active) {
-                it.lastActive = systemClock.elapsedRealtime()
-            }
-            it.active = !timedOut
-            if (DEBUG) Log.d(TAG, "Updating $key timedOut: $timedOut")
-            onMediaDataLoaded(key, key, it)
-        }
-
-        if (key == smartspaceMediaData.targetId) {
-            if (DEBUG) Log.d(TAG, "smartspace card expired")
-            dismissSmartspaceRecommendation(key, delay = 0L)
-        }
-    }
-
-    /** Called when the player's [PlaybackState] has been updated with new actions and/or state */
-    private fun updateState(key: String, state: PlaybackState) {
-        mediaEntries.get(key)?.let {
-            val token = it.token
-            if (token == null) {
-                if (DEBUG) Log.d(TAG, "State updated, but token was null")
-                return
-            }
-            val actions =
-                createActionsFromState(
-                    it.packageName,
-                    mediaControllerFactory.create(it.token),
-                    UserHandle(it.userId)
-                )
-
-            // Control buttons
-            // If flag is enabled and controller has a PlaybackState,
-            // create actions from session info
-            // otherwise, no need to update semantic actions.
-            val data =
-                if (actions != null) {
-                    it.copy(semanticActions = actions, isPlaying = isPlayingState(state.state))
-                } else {
-                    it.copy(isPlaying = isPlayingState(state.state))
-                }
-            if (DEBUG) Log.d(TAG, "State updated outside of notification")
-            onMediaDataLoaded(key, key, data)
-        }
-    }
-
-    private fun removeEntry(key: String, logEvent: Boolean = true) {
-        mediaEntries.remove(key)?.let {
-            if (logEvent) {
-                logger.logMediaRemoved(it.appUid, it.packageName, it.instanceId)
-            }
-        }
-        notifyMediaDataRemoved(key)
-    }
+    )
 
     /** Dismiss a media entry. Returns false if the key was not found. */
-    fun dismissMediaData(key: String, delay: Long): Boolean {
-        val existed = mediaEntries[key] != null
-        backgroundExecutor.execute {
-            mediaEntries[key]?.let { mediaData ->
-                if (mediaData.isLocalSession()) {
-                    mediaData.token?.let {
-                        val mediaController = mediaControllerFactory.create(it)
-                        mediaController.transportControls.stop()
-                    }
-                }
-            }
-        }
-        foregroundExecutor.executeDelayed({ removeEntry(key) }, delay)
-        return existed
-    }
+    fun dismissMediaData(key: String, delay: Long): Boolean
 
     /**
      * Called whenever the recommendation has been expired or removed by the user. This will remove
      * the recommendation card entirely from the carousel.
      */
-    fun dismissSmartspaceRecommendation(key: String, delay: Long) {
-        if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid()) {
-            // If this doesn't match, or we've already invalidated the data, no action needed
-            return
-        }
-
-        if (DEBUG) Log.d(TAG, "Dismissing Smartspace media target")
-        if (smartspaceMediaData.isActive) {
-            smartspaceMediaData =
-                EMPTY_SMARTSPACE_MEDIA_DATA.copy(
-                    targetId = smartspaceMediaData.targetId,
-                    instanceId = smartspaceMediaData.instanceId
-                )
-        }
-        foregroundExecutor.executeDelayed(
-            { notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = true) },
-            delay
-        )
-    }
+    fun dismissSmartspaceRecommendation(key: String, delay: Long)
 
     /** Called when the recommendation card should no longer be visible in QQS or lockscreen */
-    fun setRecommendationInactive(key: String) {
-        if (!mediaFlags.isPersistentSsCardEnabled()) {
-            Log.e(TAG, "Only persistent recommendation can be inactive!")
-            return
-        }
-        if (DEBUG) Log.d(TAG, "Setting smartspace recommendation inactive")
+    fun setRecommendationInactive(key: String)
 
-        if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid()) {
-            // If this doesn't match, or we've already invalidated the data, no action needed
-            return
-        }
+    /** Invoked when notification is removed. */
+    fun onNotificationRemoved(key: String)
 
-        smartspaceMediaData = smartspaceMediaData.copy(isActive = false)
-        notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData)
-    }
-
-    private fun loadMediaDataInBgForResumption(
-        userId: Int,
-        desc: MediaDescription,
-        resumeAction: Runnable,
-        token: MediaSession.Token,
-        appName: String,
-        appIntent: PendingIntent,
-        packageName: String
-    ) {
-        if (desc.title.isNullOrBlank()) {
-            Log.e(TAG, "Description incomplete")
-            // Delete the placeholder entry
-            mediaEntries.remove(packageName)
-            return
-        }
-
-        if (DEBUG) {
-            Log.d(TAG, "adding track for $userId from browser: $desc")
-        }
-
-        val currentEntry = mediaEntries.get(packageName)
-        val appUid = currentEntry?.appUid ?: Process.INVALID_UID
-
-        // Album art
-        var artworkBitmap = desc.iconBitmap
-        if (artworkBitmap == null && desc.iconUri != null) {
-            artworkBitmap = loadBitmapFromUriForUser(desc.iconUri!!, userId, appUid, packageName)
-        }
-        val artworkIcon =
-            if (artworkBitmap != null) {
-                Icon.createWithBitmap(artworkBitmap)
-            } else {
-                null
-            }
-
-        val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId()
-        val isExplicit =
-            desc.extras?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) ==
-                MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT
-
-        val progress =
-            if (mediaFlags.isResumeProgressEnabled()) {
-                MediaDataUtils.getDescriptionProgress(desc.extras)
-            } else null
-
-        val mediaAction = getResumeMediaAction(resumeAction)
-        val lastActive = systemClock.elapsedRealtime()
-        val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L
-        foregroundExecutor.execute {
-            onMediaDataLoaded(
-                packageName,
-                null,
-                MediaData(
-                    userId,
-                    true,
-                    appName,
-                    null,
-                    desc.subtitle,
-                    desc.title,
-                    artworkIcon,
-                    listOf(mediaAction),
-                    listOf(0),
-                    MediaButton(playOrPause = mediaAction),
-                    packageName,
-                    token,
-                    appIntent,
-                    device = null,
-                    active = false,
-                    resumeAction = resumeAction,
-                    resumption = true,
-                    notificationKey = packageName,
-                    hasCheckedForResume = true,
-                    lastActive = lastActive,
-                    createdTimestampMillis = createdTimestampMillis,
-                    instanceId = instanceId,
-                    appUid = appUid,
-                    isExplicit = isExplicit,
-                    resumeProgress = progress,
-                )
-            )
-        }
-    }
-
-    fun loadMediaDataInBg(
-        key: String,
-        sbn: StatusBarNotification,
-        oldKey: String?,
-        isNewlyActiveEntry: Boolean = false,
-    ) {
-        val token =
-            sbn.notification.extras.getParcelable(
-                Notification.EXTRA_MEDIA_SESSION,
-                MediaSession.Token::class.java
-            )
-        if (token == null) {
-            return
-        }
-        val mediaController = mediaControllerFactory.create(token)
-        val metadata = mediaController.metadata
-        val notif: Notification = sbn.notification
-
-        val appInfo =
-            notif.extras.getParcelable(
-                Notification.EXTRA_BUILDER_APPLICATION_INFO,
-                ApplicationInfo::class.java
-            )
-                ?: getAppInfoFromPackage(sbn.packageName)
-
-        // App name
-        val appName = getAppName(sbn, appInfo)
-
-        // Song name
-        var song: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE)
-        if (song.isNullOrBlank()) {
-            song = metadata?.getString(MediaMetadata.METADATA_KEY_TITLE)
-        }
-        if (song.isNullOrBlank()) {
-            song = HybridGroupManager.resolveTitle(notif)
-        }
-        if (song.isNullOrBlank()) {
-            // For apps that don't include a title, log and add a placeholder
-            song = context.getString(R.string.controls_media_empty_title, appName)
-            try {
-                statusBarManager.logBlankMediaTitle(sbn.packageName, sbn.user.identifier)
-            } catch (e: RuntimeException) {
-                Log.e(TAG, "Error reporting blank media title for package ${sbn.packageName}")
-            }
-        }
-
-        // Album art
-        var artworkBitmap = metadata?.let { loadBitmapFromUri(it) }
-        if (artworkBitmap == null) {
-            artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ART)
-        }
-        if (artworkBitmap == null) {
-            artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART)
-        }
-        val artWorkIcon =
-            if (artworkBitmap == null) {
-                notif.getLargeIcon()
-            } else {
-                Icon.createWithBitmap(artworkBitmap)
-            }
-
-        // App Icon
-        val smallIcon = sbn.notification.smallIcon
-
-        // Explicit Indicator
-        var isExplicit = false
-        val mediaMetadataCompat = MediaMetadataCompat.fromMediaMetadata(metadata)
-        isExplicit =
-            mediaMetadataCompat?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) ==
-                MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT
-
-        // Artist name
-        var artist: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST)
-        if (artist.isNullOrBlank()) {
-            artist = HybridGroupManager.resolveText(notif)
-        }
-
-        // Device name (used for remote cast notifications)
-        var device: MediaDeviceData? = null
-        if (isRemoteCastNotification(sbn)) {
-            val extras = sbn.notification.extras
-            val deviceName = extras.getCharSequence(Notification.EXTRA_MEDIA_REMOTE_DEVICE, null)
-            val deviceIcon = extras.getInt(Notification.EXTRA_MEDIA_REMOTE_ICON, -1)
-            val deviceIntent =
-                extras.getParcelable(
-                    Notification.EXTRA_MEDIA_REMOTE_INTENT,
-                    PendingIntent::class.java
-                )
-            Log.d(TAG, "$key is RCN for $deviceName")
-
-            if (deviceName != null && deviceIcon > -1) {
-                // Name and icon must be present, but intent may be null
-                val enabled = deviceIntent != null && deviceIntent.isActivity
-                val deviceDrawable =
-                    Icon.createWithResource(sbn.packageName, deviceIcon)
-                        .loadDrawable(sbn.getPackageContext(context))
-                device =
-                    MediaDeviceData(
-                        enabled,
-                        deviceDrawable,
-                        deviceName,
-                        deviceIntent,
-                        showBroadcastButton = false
-                    )
-            }
-        }
-
-        // Control buttons
-        // If flag is enabled and controller has a PlaybackState, create actions from session info
-        // Otherwise, use the notification actions
-        var actionIcons: List<MediaAction> = emptyList()
-        var actionsToShowCollapsed: List<Int> = emptyList()
-        val semanticActions = createActionsFromState(sbn.packageName, mediaController, sbn.user)
-        if (semanticActions == null) {
-            val actions = createActionsFromNotification(sbn)
-            actionIcons = actions.first
-            actionsToShowCollapsed = actions.second
-        }
-
-        val playbackLocation =
-            if (isRemoteCastNotification(sbn)) MediaData.PLAYBACK_CAST_REMOTE
-            else if (
-                mediaController.playbackInfo?.playbackType ==
-                    MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL
-            )
-                MediaData.PLAYBACK_LOCAL
-            else MediaData.PLAYBACK_CAST_LOCAL
-        val isPlaying = mediaController.playbackState?.let { isPlayingState(it.state) } ?: null
-
-        val currentEntry = mediaEntries.get(key)
-        val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId()
-        val appUid = appInfo?.uid ?: Process.INVALID_UID
-
-        if (isNewlyActiveEntry) {
-            logSingleVsMultipleMediaAdded(appUid, sbn.packageName, instanceId)
-            logger.logActiveMediaAdded(appUid, sbn.packageName, instanceId, playbackLocation)
-        } else if (playbackLocation != currentEntry?.playbackLocation) {
-            logger.logPlaybackLocationChange(appUid, sbn.packageName, instanceId, playbackLocation)
-        }
-
-        val lastActive = systemClock.elapsedRealtime()
-        val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L
-        foregroundExecutor.execute {
-            val resumeAction: Runnable? = mediaEntries[key]?.resumeAction
-            val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true
-            val active = mediaEntries[key]?.active ?: true
-            onMediaDataLoaded(
-                key,
-                oldKey,
-                MediaData(
-                    sbn.normalizedUserId,
-                    true,
-                    appName,
-                    smallIcon,
-                    artist,
-                    song,
-                    artWorkIcon,
-                    actionIcons,
-                    actionsToShowCollapsed,
-                    semanticActions,
-                    sbn.packageName,
-                    token,
-                    notif.contentIntent,
-                    device,
-                    active,
-                    resumeAction = resumeAction,
-                    playbackLocation = playbackLocation,
-                    notificationKey = key,
-                    hasCheckedForResume = hasCheckedForResume,
-                    isPlaying = isPlaying,
-                    isClearable = !sbn.isOngoing,
-                    lastActive = lastActive,
-                    createdTimestampMillis = createdTimestampMillis,
-                    instanceId = instanceId,
-                    appUid = appUid,
-                    isExplicit = isExplicit,
-                )
-            )
-        }
-    }
-
-    private fun logSingleVsMultipleMediaAdded(
-        appUid: Int,
-        packageName: String,
-        instanceId: InstanceId
-    ) {
-        if (mediaEntries.size == 1) {
-            logger.logSingleMediaPlayerInCarousel(appUid, packageName, instanceId)
-        } else if (mediaEntries.size == 2) {
-            // Since this method is only called when there is a new media session added.
-            // logging needed once there is more than one media session in carousel.
-            logger.logMultipleMediaPlayersInCarousel(appUid, packageName, instanceId)
-        }
-    }
-
-    private fun getAppInfoFromPackage(packageName: String): ApplicationInfo? {
-        try {
-            return context.packageManager.getApplicationInfo(packageName, 0)
-        } catch (e: PackageManager.NameNotFoundException) {
-            Log.w(TAG, "Could not get app info for $packageName", e)
-        }
-        return null
-    }
-
-    private fun getAppName(sbn: StatusBarNotification, appInfo: ApplicationInfo?): String {
-        val name = sbn.notification.extras.getString(EXTRA_SUBSTITUTE_APP_NAME)
-        if (name != null) {
-            return name
-        }
-
-        return if (appInfo != null) {
-            context.packageManager.getApplicationLabel(appInfo).toString()
-        } else {
-            sbn.packageName
-        }
-    }
-
-    /** Generate action buttons based on notification actions */
-    private fun createActionsFromNotification(
-        sbn: StatusBarNotification
-    ): Pair<List<MediaAction>, List<Int>> {
-        val notif = sbn.notification
-        val actionIcons: MutableList<MediaAction> = ArrayList()
-        val actions = notif.actions
-        var actionsToShowCollapsed =
-            notif.extras.getIntArray(Notification.EXTRA_COMPACT_ACTIONS)?.toMutableList()
-                ?: mutableListOf()
-        if (actionsToShowCollapsed.size > MAX_COMPACT_ACTIONS) {
-            Log.e(
-                TAG,
-                "Too many compact actions for ${sbn.key}," +
-                    "limiting to first $MAX_COMPACT_ACTIONS"
-            )
-            actionsToShowCollapsed = actionsToShowCollapsed.subList(0, MAX_COMPACT_ACTIONS)
-        }
-
-        if (actions != null) {
-            for ((index, action) in actions.withIndex()) {
-                if (index == MAX_NOTIFICATION_ACTIONS) {
-                    Log.w(
-                        TAG,
-                        "Too many notification actions for ${sbn.key}," +
-                            " limiting to first $MAX_NOTIFICATION_ACTIONS"
-                    )
-                    break
-                }
-                if (action.getIcon() == null) {
-                    if (DEBUG) Log.i(TAG, "No icon for action $index ${action.title}")
-                    actionsToShowCollapsed.remove(index)
-                    continue
-                }
-                val runnable =
-                    if (action.actionIntent != null) {
-                        Runnable {
-                            if (action.actionIntent.isActivity) {
-                                activityStarter.startPendingIntentDismissingKeyguard(
-                                    action.actionIntent
-                                )
-                            } else if (action.isAuthenticationRequired()) {
-                                activityStarter.dismissKeyguardThenExecute(
-                                    {
-                                        var result = sendPendingIntent(action.actionIntent)
-                                        result
-                                    },
-                                    {},
-                                    true
-                                )
-                            } else {
-                                sendPendingIntent(action.actionIntent)
-                            }
-                        }
-                    } else {
-                        null
-                    }
-                val mediaActionIcon =
-                    if (action.getIcon()?.getType() == Icon.TYPE_RESOURCE) {
-                            Icon.createWithResource(sbn.packageName, action.getIcon()!!.getResId())
-                        } else {
-                            action.getIcon()
-                        }
-                        .setTint(themeText)
-                        .loadDrawable(context)
-                val mediaAction = MediaAction(mediaActionIcon, runnable, action.title, null)
-                actionIcons.add(mediaAction)
-            }
-        }
-        return Pair(actionIcons, actionsToShowCollapsed)
-    }
-
-    /**
-     * Generates action button info for this media session based on the PlaybackState
-     *
-     * @param packageName Package name for the media app
-     * @param controller MediaController for the current session
-     * @return a Pair consisting of a list of media actions, and a list of ints representing which
-     *
-     * ```
-     *      of those actions should be shown in the compact player
-     * ```
-     */
-    private fun createActionsFromState(
-        packageName: String,
-        controller: MediaController,
-        user: UserHandle
-    ): MediaButton? {
-        val state = controller.playbackState
-        if (state == null || !mediaFlags.areMediaSessionActionsEnabled(packageName, user)) {
-            return null
-        }
-
-        // First, check for standard actions
-        val playOrPause =
-            if (isConnectingState(state.state)) {
-                // Spinner needs to be animating to render anything. Start it here.
-                val drawable =
-                    context.getDrawable(com.android.internal.R.drawable.progress_small_material)
-                (drawable as Animatable).start()
-                MediaAction(
-                    drawable,
-                    null, // no action to perform when clicked
-                    context.getString(R.string.controls_media_button_connecting),
-                    context.getDrawable(R.drawable.ic_media_connecting_container),
-                    // Specify a rebind id to prevent the spinner from restarting on later binds.
-                    com.android.internal.R.drawable.progress_small_material
-                )
-            } else if (isPlayingState(state.state)) {
-                getStandardAction(controller, state.actions, PlaybackState.ACTION_PAUSE)
-            } else {
-                getStandardAction(controller, state.actions, PlaybackState.ACTION_PLAY)
-            }
-        val prevButton =
-            getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_PREVIOUS)
-        val nextButton =
-            getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_NEXT)
-
-        // Then, create a way to build any custom actions that will be needed
-        val customActions =
-            state.customActions
-                .asSequence()
-                .filterNotNull()
-                .map { getCustomAction(state, packageName, controller, it) }
-                .iterator()
-        fun nextCustomAction() = if (customActions.hasNext()) customActions.next() else null
-
-        // Finally, assign the remaining button slots: play/pause A B C D
-        // A = previous, else custom action (if not reserved)
-        // B = next, else custom action (if not reserved)
-        // C and D are always custom actions
-        val reservePrev =
-            controller.extras?.getBoolean(
-                MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV
-            ) == true
-        val reserveNext =
-            controller.extras?.getBoolean(
-                MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT
-            ) == true
-
-        val prevOrCustom =
-            if (prevButton != null) {
-                prevButton
-            } else if (!reservePrev) {
-                nextCustomAction()
-            } else {
-                null
-            }
-
-        val nextOrCustom =
-            if (nextButton != null) {
-                nextButton
-            } else if (!reserveNext) {
-                nextCustomAction()
-            } else {
-                null
-            }
-
-        return MediaButton(
-            playOrPause,
-            nextOrCustom,
-            prevOrCustom,
-            nextCustomAction(),
-            nextCustomAction(),
-            reserveNext,
-            reservePrev
-        )
-    }
-
-    /**
-     * Create a [MediaAction] for a given action and media session
-     *
-     * @param controller MediaController for the session
-     * @param stateActions The actions included with the session's [PlaybackState]
-     * @param action A [PlaybackState.Actions] value representing what action to generate. One of:
-     * ```
-     *      [PlaybackState.ACTION_PLAY]
-     *      [PlaybackState.ACTION_PAUSE]
-     *      [PlaybackState.ACTION_SKIP_TO_PREVIOUS]
-     *      [PlaybackState.ACTION_SKIP_TO_NEXT]
-     * @return
-     * ```
-     *
-     * A [MediaAction] with correct values set, or null if the state doesn't support it
-     */
-    private fun getStandardAction(
-        controller: MediaController,
-        stateActions: Long,
-        @PlaybackState.Actions action: Long
-    ): MediaAction? {
-        if (!includesAction(stateActions, action)) {
-            return null
-        }
-
-        return when (action) {
-            PlaybackState.ACTION_PLAY -> {
-                MediaAction(
-                    context.getDrawable(R.drawable.ic_media_play),
-                    { controller.transportControls.play() },
-                    context.getString(R.string.controls_media_button_play),
-                    context.getDrawable(R.drawable.ic_media_play_container)
-                )
-            }
-            PlaybackState.ACTION_PAUSE -> {
-                MediaAction(
-                    context.getDrawable(R.drawable.ic_media_pause),
-                    { controller.transportControls.pause() },
-                    context.getString(R.string.controls_media_button_pause),
-                    context.getDrawable(R.drawable.ic_media_pause_container)
-                )
-            }
-            PlaybackState.ACTION_SKIP_TO_PREVIOUS -> {
-                MediaAction(
-                    context.getDrawable(R.drawable.ic_media_prev),
-                    { controller.transportControls.skipToPrevious() },
-                    context.getString(R.string.controls_media_button_prev),
-                    null
-                )
-            }
-            PlaybackState.ACTION_SKIP_TO_NEXT -> {
-                MediaAction(
-                    context.getDrawable(R.drawable.ic_media_next),
-                    { controller.transportControls.skipToNext() },
-                    context.getString(R.string.controls_media_button_next),
-                    null
-                )
-            }
-            else -> null
-        }
-    }
-
-    /** Check whether the actions from a [PlaybackState] include a specific action */
-    private fun includesAction(stateActions: Long, @PlaybackState.Actions action: Long): Boolean {
-        if (
-            (action == PlaybackState.ACTION_PLAY || action == PlaybackState.ACTION_PAUSE) &&
-                (stateActions and PlaybackState.ACTION_PLAY_PAUSE > 0L)
-        ) {
-            return true
-        }
-        return (stateActions and action != 0L)
-    }
-
-    /** Get a [MediaAction] representing a [PlaybackState.CustomAction] */
-    private fun getCustomAction(
-        state: PlaybackState,
-        packageName: String,
-        controller: MediaController,
-        customAction: PlaybackState.CustomAction
-    ): MediaAction {
-        return MediaAction(
-            Icon.createWithResource(packageName, customAction.icon).loadDrawable(context),
-            { controller.transportControls.sendCustomAction(customAction, customAction.extras) },
-            customAction.name,
-            null
-        )
-    }
-
-    /** Load a bitmap from the various Art metadata URIs */
-    private fun loadBitmapFromUri(metadata: MediaMetadata): Bitmap? {
-        for (uri in ART_URIS) {
-            val uriString = metadata.getString(uri)
-            if (!TextUtils.isEmpty(uriString)) {
-                val albumArt = loadBitmapFromUri(Uri.parse(uriString))
-                if (albumArt != null) {
-                    if (DEBUG) Log.d(TAG, "loaded art from $uri")
-                    return albumArt
-                }
-            }
-        }
-        return null
-    }
-
-    private fun sendPendingIntent(intent: PendingIntent): Boolean {
-        return try {
-            val options = BroadcastOptions.makeBasic()
-            options.setInteractive(true)
-            options.setPendingIntentBackgroundActivityStartMode(
-                ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
-            )
-            intent.send(options.toBundle())
-            true
-        } catch (e: PendingIntent.CanceledException) {
-            Log.d(TAG, "Intent canceled", e)
-            false
-        }
-    }
-
-    /** Returns a bitmap if the user can access the given URI, else null */
-    private fun loadBitmapFromUriForUser(
-        uri: Uri,
-        userId: Int,
-        appUid: Int,
-        packageName: String,
-    ): Bitmap? {
-        try {
-            val ugm = UriGrantsManager.getService()
-            ugm.checkGrantUriPermission_ignoreNonSystem(
-                appUid,
-                packageName,
-                ContentProvider.getUriWithoutUserId(uri),
-                Intent.FLAG_GRANT_READ_URI_PERMISSION,
-                ContentProvider.getUserIdFromUri(uri, userId)
-            )
-            return loadBitmapFromUri(uri)
-        } catch (e: SecurityException) {
-            Log.e(TAG, "Failed to get URI permission: $e")
-        }
-        return null
-    }
-
-    /**
-     * Load a bitmap from a URI
-     *
-     * @param uri the uri to load
-     * @return bitmap, or null if couldn't be loaded
-     */
-    private fun loadBitmapFromUri(uri: Uri): Bitmap? {
-        // ImageDecoder requires a scheme of the following types
-        if (uri.scheme == null) {
-            return null
-        }
-
-        if (
-            !uri.scheme.equals(ContentResolver.SCHEME_CONTENT) &&
-                !uri.scheme.equals(ContentResolver.SCHEME_ANDROID_RESOURCE) &&
-                !uri.scheme.equals(ContentResolver.SCHEME_FILE)
-        ) {
-            return null
-        }
-
-        val source = ImageDecoder.createSource(context.contentResolver, uri)
-        return try {
-            ImageDecoder.decodeBitmap(source) { decoder, info, _ ->
-                val width = info.size.width
-                val height = info.size.height
-                val scale =
-                    MediaDataUtils.getScaleFactor(
-                        APair(width, height),
-                        APair(artworkWidth, artworkHeight)
-                    )
-
-                // Downscale if needed
-                if (scale != 0f && scale < 1) {
-                    decoder.setTargetSize((scale * width).toInt(), (scale * height).toInt())
-                }
-                decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
-            }
-        } catch (e: IOException) {
-            Log.e(TAG, "Unable to load bitmap", e)
-            null
-        } catch (e: RuntimeException) {
-            Log.e(TAG, "Unable to load bitmap", e)
-            null
-        }
-    }
-
-    private fun getResumeMediaAction(action: Runnable): MediaAction {
-        return MediaAction(
-            Icon.createWithResource(context, R.drawable.ic_media_play)
-                .setTint(themeText)
-                .loadDrawable(context),
-            action,
-            context.getString(R.string.controls_media_resume),
-            context.getDrawable(R.drawable.ic_media_play_container)
-        )
-    }
-
-    fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) =
-        traceSection("MediaDataManager#onMediaDataLoaded") {
-            Assert.isMainThread()
-            if (mediaEntries.containsKey(key)) {
-                // Otherwise this was removed already
-                mediaEntries.put(key, data)
-                notifyMediaDataLoaded(key, oldKey, data)
-            }
-        }
-
-    override fun onSmartspaceTargetsUpdated(targets: List<Parcelable>) {
-        if (!allowMediaRecommendations) {
-            if (DEBUG) Log.d(TAG, "Smartspace recommendation is disabled in Settings.")
-            return
-        }
-
-        val mediaTargets = targets.filterIsInstance<SmartspaceTarget>()
-        when (mediaTargets.size) {
-            0 -> {
-                if (!smartspaceMediaData.isActive) {
-                    return
-                }
-                if (DEBUG) {
-                    Log.d(TAG, "Set Smartspace media to be inactive for the data update")
-                }
-                if (mediaFlags.isPersistentSsCardEnabled()) {
-                    // Smartspace uses this signal to hide the card (e.g. when it expires or user
-                    // disconnects headphones), so treat as setting inactive when flag is on
-                    smartspaceMediaData = smartspaceMediaData.copy(isActive = false)
-                    notifySmartspaceMediaDataLoaded(
-                        smartspaceMediaData.targetId,
-                        smartspaceMediaData,
-                    )
-                } else {
-                    smartspaceMediaData =
-                        EMPTY_SMARTSPACE_MEDIA_DATA.copy(
-                            targetId = smartspaceMediaData.targetId,
-                            instanceId = smartspaceMediaData.instanceId,
-                        )
-                    notifySmartspaceMediaDataRemoved(
-                        smartspaceMediaData.targetId,
-                        immediately = false,
-                    )
-                }
-            }
-            1 -> {
-                val newMediaTarget = mediaTargets.get(0)
-                if (smartspaceMediaData.targetId == newMediaTarget.smartspaceTargetId) {
-                    // The same Smartspace updates can be received. Skip the duplicate updates.
-                    return
-                }
-                if (DEBUG) Log.d(TAG, "Forwarding Smartspace media update.")
-                smartspaceMediaData = toSmartspaceMediaData(newMediaTarget)
-                notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData)
-            }
-            else -> {
-                // There should NOT be more than 1 Smartspace media update. When it happens, it
-                // indicates a bad state or an error. Reset the status accordingly.
-                Log.wtf(TAG, "More than 1 Smartspace Media Update. Resetting the status...")
-                notifySmartspaceMediaDataRemoved(
-                    smartspaceMediaData.targetId,
-                    immediately = false,
-                )
-                smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA
-            }
-        }
-    }
-
-    fun onNotificationRemoved(key: String) {
-        Assert.isMainThread()
-        val removed = mediaEntries.remove(key) ?: return
-        if (keyguardUpdateMonitor.isUserInLockdown(removed.userId)) {
-            logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
-        } else if (isAbleToResume(removed)) {
-            convertToResumePlayer(key, removed)
-        } else if (mediaFlags.isRetainingPlayersEnabled()) {
-            handlePossibleRemoval(key, removed, notificationRemoved = true)
-        } else {
-            notifyMediaDataRemoved(key)
-            logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
-        }
-    }
-
-    private fun onSessionDestroyed(key: String) {
-        if (DEBUG) Log.d(TAG, "session destroyed for $key")
-        val entry = mediaEntries.remove(key) ?: return
-        // Clear token since the session is no longer valid
-        val updated = entry.copy(token = null)
-        handlePossibleRemoval(key, updated)
-    }
-
-    private fun isAbleToResume(data: MediaData): Boolean {
-        val isEligibleForResume =
-            data.isLocalSession() ||
-                (mediaFlags.isRemoteResumeAllowed() &&
-                    data.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE)
-        return useMediaResumption && data.resumeAction != null && isEligibleForResume
-    }
-
-    /**
-     * Convert to resume state if the player is no longer valid and active, then notify listeners
-     * that the data was updated. Does not convert to resume state if the player is still valid, or
-     * if it was removed before becoming inactive. (Assumes that [removed] was removed from
-     * [mediaEntries] before this function was called)
-     */
-    private fun handlePossibleRemoval(
-        key: String,
-        removed: MediaData,
-        notificationRemoved: Boolean = false
-    ) {
-        val hasSession = removed.token != null
-        if (hasSession && removed.semanticActions != null) {
-            // The app was using session actions, and the session is still valid: keep player
-            if (DEBUG) Log.d(TAG, "Notification removed but using session actions $key")
-            mediaEntries.put(key, removed)
-            notifyMediaDataLoaded(key, key, removed)
-        } else if (!notificationRemoved && removed.semanticActions == null) {
-            // The app was using notification actions, and notif wasn't removed yet: keep player
-            if (DEBUG) Log.d(TAG, "Session destroyed but using notification actions $key")
-            mediaEntries.put(key, removed)
-            notifyMediaDataLoaded(key, key, removed)
-        } else if (removed.active && !isAbleToResume(removed)) {
-            // This player was still active - it didn't last long enough to time out,
-            // and its app doesn't normally support resume: remove
-            if (DEBUG) Log.d(TAG, "Removing still-active player $key")
-            notifyMediaDataRemoved(key)
-            logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
-        } else if (mediaFlags.isRetainingPlayersEnabled() || isAbleToResume(removed)) {
-            // Convert to resume
-            if (DEBUG) {
-                Log.d(
-                    TAG,
-                    "Notification ($notificationRemoved) and/or session " +
-                        "($hasSession) gone for inactive player $key"
-                )
-            }
-            convertToResumePlayer(key, removed)
-        } else {
-            // Retaining players flag is off and app doesn't support resume: remove player.
-            if (DEBUG) Log.d(TAG, "Removing player $key")
-            notifyMediaDataRemoved(key)
-            logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
-        }
-    }
-
-    /** Set the given [MediaData] as a resume state player and notify listeners */
-    private fun convertToResumePlayer(key: String, data: MediaData) {
-        if (DEBUG) Log.d(TAG, "Converting $key to resume")
-        // Resumption controls must have a title.
-        if (data.song.isNullOrBlank()) {
-            Log.e(TAG, "Description incomplete")
-            notifyMediaDataRemoved(key)
-            logger.logMediaRemoved(data.appUid, data.packageName, data.instanceId)
-            return
-        }
-        // Move to resume key (aka package name) if that key doesn't already exist.
-        val resumeAction = data.resumeAction?.let { getResumeMediaAction(it) }
-        val actions = resumeAction?.let { listOf(resumeAction) } ?: emptyList()
-        val launcherIntent =
-            context.packageManager.getLaunchIntentForPackage(data.packageName)?.let {
-                PendingIntent.getActivity(context, 0, it, PendingIntent.FLAG_IMMUTABLE)
-            }
-        val lastActive =
-            if (data.active) {
-                systemClock.elapsedRealtime()
-            } else {
-                data.lastActive
-            }
-        val updated =
-            data.copy(
-                token = null,
-                actions = actions,
-                semanticActions = MediaButton(playOrPause = resumeAction),
-                actionsToShowInCompact = listOf(0),
-                active = false,
-                resumption = true,
-                isPlaying = false,
-                isClearable = true,
-                clickIntent = launcherIntent,
-                lastActive = lastActive,
-            )
-        val pkg = data.packageName
-        val migrate = mediaEntries.put(pkg, updated) == null
-        // Notify listeners of "new" controls when migrating or removed and update when not
-        Log.d(TAG, "migrating? $migrate from $key -> $pkg")
-        if (migrate) {
-            notifyMediaDataLoaded(key = pkg, oldKey = key, info = updated)
-        } else {
-            // Since packageName is used for the key of the resumption controls, it is
-            // possible that another notification has already been reused for the resumption
-            // controls of this package. In this case, rather than renaming this player as
-            // packageName, just remove it and then send a update to the existing resumption
-            // controls.
-            notifyMediaDataRemoved(key)
-            notifyMediaDataLoaded(key = pkg, oldKey = pkg, info = updated)
-        }
-        logger.logActiveConvertedToResume(updated.appUid, pkg, updated.instanceId)
-
-        // Limit total number of resume controls
-        val resumeEntries = mediaEntries.filter { (key, data) -> data.resumption }
-        val numResume = resumeEntries.size
-        if (numResume > ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) {
-            resumeEntries
-                .toList()
-                .sortedBy { (key, data) -> data.lastActive }
-                .subList(0, numResume - ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS)
-                .forEach { (key, data) ->
-                    Log.d(TAG, "Removing excess control $key")
-                    mediaEntries.remove(key)
-                    notifyMediaDataRemoved(key)
-                    logger.logMediaRemoved(data.appUid, data.packageName, data.instanceId)
-                }
-        }
-    }
-
-    fun setMediaResumptionEnabled(isEnabled: Boolean) {
-        if (useMediaResumption == isEnabled) {
-            return
-        }
-
-        useMediaResumption = isEnabled
-
-        if (!useMediaResumption) {
-            // Remove any existing resume controls
-            val filtered = mediaEntries.filter { !it.value.active }
-            filtered.forEach {
-                mediaEntries.remove(it.key)
-                notifyMediaDataRemoved(it.key)
-                logger.logMediaRemoved(it.value.appUid, it.value.packageName, it.value.instanceId)
-            }
-        }
-    }
+    fun setMediaResumptionEnabled(isEnabled: Boolean)
 
     /** Invoked when the user has dismissed the media carousel */
-    fun onSwipeToDismiss() = mediaDataFilter.onSwipeToDismiss()
+    fun onSwipeToDismiss()
 
     /** Are there any media notifications active, including the recommendations? */
-    fun hasActiveMediaOrRecommendation() = mediaDataFilter.hasActiveMediaOrRecommendation()
+    fun hasActiveMediaOrRecommendation(): Boolean
 
-    /**
-     * Are there any media entries we should display, including the recommendations?
-     * - If resumption is enabled, this will include inactive players
-     * - If resumption is disabled, we only want to show active players
-     */
-    fun hasAnyMediaOrRecommendation() = mediaDataFilter.hasAnyMediaOrRecommendation()
+    /** Are there any media entries we should display, including the recommendations? */
+    fun hasAnyMediaOrRecommendation(): Boolean
 
     /** Are there any resume media notifications active, excluding the recommendations? */
-    fun hasActiveMedia() = mediaDataFilter.hasActiveMedia()
+    fun hasActiveMedia(): Boolean
 
-    /**
-     * Are there any resume media notifications active, excluding the recommendations?
-     * - If resumption is enabled, this will include inactive players
-     * - If resumption is disabled, we only want to show active players
-     */
-    fun hasAnyMedia() = mediaDataFilter.hasAnyMedia()
+    /** Are there any resume media notifications active, excluding the recommendations? */
+    fun hasAnyMedia(): Boolean
 
-    interface Listener {
+    /** Is recommendation card active? */
+    fun isRecommendationActive(): Boolean
+
+    // Uses [MediaDataProcessor.Listener] in order to link the new logic code with UI layer.
+    interface Listener : MediaDataProcessor.Listener {
 
         /**
          * Called whenever there's new MediaData Loaded for the consumption in views.
@@ -1637,13 +113,13 @@
          * @param isSsReactivated indicates resume media card is reactivated by Smartspace
          *   recommendation signal
          */
-        fun onMediaDataLoaded(
+        override fun onMediaDataLoaded(
             key: String,
             oldKey: String?,
             data: MediaData,
-            immediately: Boolean = true,
-            receivedSmartspaceCardLatency: Int = 0,
-            isSsReactivated: Boolean = false
+            immediately: Boolean,
+            receivedSmartspaceCardLatency: Int,
+            isSsReactivated: Boolean,
         ) {}
 
         /**
@@ -1653,14 +129,14 @@
          *   it will be prioritized as the first card. Otherwise, it will show up as the last card
          *   as default.
          */
-        fun onSmartspaceMediaDataLoaded(
+        override fun onSmartspaceMediaDataLoaded(
             key: String,
             data: SmartspaceMediaData,
-            shouldPrioritize: Boolean = false
+            shouldPrioritize: Boolean,
         ) {}
 
         /** Called whenever a previously existing Media notification was removed. */
-        fun onMediaDataRemoved(key: String) {}
+        override fun onMediaDataRemoved(key: String) {}
 
         /**
          * Called whenever a previously existing Smartspace media data was removed.
@@ -1669,78 +145,14 @@
          *   until the next refresh-round before UI becomes visible. True by default to take in
          *   place immediately.
          */
-        fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean = true) {}
+        override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {}
     }
 
-    /**
-     * Converts the pass-in SmartspaceTarget to SmartspaceMediaData
-     *
-     * @return An empty SmartspaceMediaData with the valid target Id is returned if the
-     *   SmartspaceTarget's data is invalid.
-     */
-    private fun toSmartspaceMediaData(target: SmartspaceTarget): SmartspaceMediaData {
-        val baseAction: SmartspaceAction? = target.baseAction
-        val dismissIntent =
-            baseAction?.extras?.getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY) as Intent?
+    companion object {
 
-        val isActive =
-            when {
-                !mediaFlags.isPersistentSsCardEnabled() -> true
-                baseAction == null -> true
-                else -> {
-                    val triggerSource = baseAction.extras?.getString(EXTRA_KEY_TRIGGER_SOURCE)
-                    triggerSource != EXTRA_VALUE_TRIGGER_PERIODIC
-                }
-            }
-
-        packageName(target)?.let {
-            return SmartspaceMediaData(
-                targetId = target.smartspaceTargetId,
-                isActive = isActive,
-                packageName = it,
-                cardAction = target.baseAction,
-                recommendations = target.iconGrid,
-                dismissIntent = dismissIntent,
-                headphoneConnectionTimeMillis = target.creationTimeMillis,
-                instanceId = logger.getNewInstanceId(),
-                expiryTimeMs = target.expiryTimeMillis,
-            )
-        }
-        return EMPTY_SMARTSPACE_MEDIA_DATA.copy(
-            targetId = target.smartspaceTargetId,
-            isActive = isActive,
-            dismissIntent = dismissIntent,
-            headphoneConnectionTimeMillis = target.creationTimeMillis,
-            instanceId = logger.getNewInstanceId(),
-            expiryTimeMs = target.expiryTimeMillis,
-        )
-    }
-
-    private fun packageName(target: SmartspaceTarget): String? {
-        val recommendationList = target.iconGrid
-        if (recommendationList == null || recommendationList.isEmpty()) {
-            Log.w(TAG, "Empty or null media recommendation list.")
-            return null
-        }
-        for (recommendation in recommendationList) {
-            val extras = recommendation.extras
-            extras?.let {
-                it.getString(EXTRAS_MEDIA_SOURCE_PACKAGE_NAME)?.let { packageName ->
-                    return packageName
-                }
-            }
-        }
-        Log.w(TAG, "No valid package name is provided.")
-        return null
-    }
-
-    override fun dump(pw: PrintWriter, args: Array<out String>) {
-        pw.apply {
-            println("internalListeners: $internalListeners")
-            println("externalListeners: ${mediaDataFilter.listeners}")
-            println("mediaEntries: $mediaEntries")
-            println("useMediaResumption: $useMediaResumption")
-            println("allowMediaRecommendations: $allowMediaRecommendations")
+        @JvmStatic
+        fun isMediaNotification(sbn: StatusBarNotification): Boolean {
+            return sbn.notification.isMediaNotification()
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
new file mode 100644
index 0000000..7412290
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
@@ -0,0 +1,1654 @@
+/*
+ * 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.media.controls.domain.pipeline
+
+import android.annotation.SuppressLint
+import android.app.ActivityOptions
+import android.app.BroadcastOptions
+import android.app.Notification
+import android.app.Notification.EXTRA_SUBSTITUTE_APP_NAME
+import android.app.PendingIntent
+import android.app.StatusBarManager
+import android.app.UriGrantsManager
+import android.app.smartspace.SmartspaceAction
+import android.app.smartspace.SmartspaceConfig
+import android.app.smartspace.SmartspaceManager
+import android.app.smartspace.SmartspaceSession
+import android.app.smartspace.SmartspaceTarget
+import android.content.BroadcastReceiver
+import android.content.ContentProvider
+import android.content.ContentResolver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.graphics.Bitmap
+import android.graphics.ImageDecoder
+import android.graphics.drawable.Animatable
+import android.graphics.drawable.Icon
+import android.media.MediaDescription
+import android.media.MediaMetadata
+import android.media.session.MediaController
+import android.media.session.MediaSession
+import android.media.session.PlaybackState
+import android.net.Uri
+import android.os.Handler
+import android.os.Parcelable
+import android.os.Process
+import android.os.UserHandle
+import android.provider.Settings
+import android.service.notification.StatusBarNotification
+import android.support.v4.media.MediaMetadataCompat
+import android.text.TextUtils
+import android.util.Log
+import android.util.Pair as APair
+import androidx.media.utils.MediaConstants
+import com.android.app.tracing.traceSection
+import com.android.internal.annotations.Keep
+import com.android.internal.logging.InstanceId
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.CoreStartable
+import com.android.systemui.broadcast.BroadcastDispatcher
+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.dump.DumpManager
+import com.android.systemui.media.controls.data.repository.MediaDataRepository
+import com.android.systemui.media.controls.domain.pipeline.MediaDataManager.Companion.isMediaNotification
+import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor
+import com.android.systemui.media.controls.domain.resume.ResumeMediaBrowser
+import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_SOURCE
+import com.android.systemui.media.controls.shared.model.EXTRA_VALUE_TRIGGER_PERIODIC
+import com.android.systemui.media.controls.shared.model.MediaAction
+import com.android.systemui.media.controls.shared.model.MediaButton
+import com.android.systemui.media.controls.shared.model.MediaData
+import com.android.systemui.media.controls.shared.model.MediaDeviceData
+import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
+import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvider
+import com.android.systemui.media.controls.ui.view.MediaViewHolder
+import com.android.systemui.media.controls.util.MediaControllerFactory
+import com.android.systemui.media.controls.util.MediaDataUtils
+import com.android.systemui.media.controls.util.MediaFlags
+import com.android.systemui.media.controls.util.MediaUiEventLogger
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.plugins.BcSmartspaceDataPlugin
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.NotificationMediaManager.isConnectingState
+import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
+import com.android.systemui.statusbar.notification.row.HybridGroupManager
+import com.android.systemui.util.Assert
+import com.android.systemui.util.Utils
+import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.util.concurrency.ThreadFactory
+import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
+import com.android.systemui.util.time.SystemClock
+import java.io.IOException
+import java.io.PrintWriter
+import java.util.concurrent.Executor
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+// URI fields to try loading album art from
+private val ART_URIS =
+    arrayOf(
+        MediaMetadata.METADATA_KEY_ALBUM_ART_URI,
+        MediaMetadata.METADATA_KEY_ART_URI,
+        MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI
+    )
+
+private const val TAG = "MediaDataProcessor"
+private const val DEBUG = true
+private const val EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY = "dismiss_intent"
+
+/** Processes all media data fields and encapsulates logic for managing media data entries. */
+@SysUISingleton
+class MediaDataProcessor(
+    private val context: Context,
+    @Application private val applicationScope: CoroutineScope,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+    @Background private val backgroundExecutor: Executor,
+    @Main private val uiExecutor: Executor,
+    @Main private val foregroundExecutor: DelayableExecutor,
+    @Main private val handler: Handler,
+    private val mediaControllerFactory: MediaControllerFactory,
+    private val broadcastDispatcher: BroadcastDispatcher,
+    private val dumpManager: DumpManager,
+    private val activityStarter: ActivityStarter,
+    private val smartspaceMediaDataProvider: SmartspaceMediaDataProvider,
+    private var useMediaResumption: Boolean,
+    private val useQsMediaPlayer: Boolean,
+    private val systemClock: SystemClock,
+    private val secureSettings: SecureSettings,
+    private val mediaFlags: MediaFlags,
+    private val logger: MediaUiEventLogger,
+    private val smartspaceManager: SmartspaceManager?,
+    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+    private val mediaDataRepository: MediaDataRepository,
+) : CoreStartable, BcSmartspaceDataPlugin.SmartspaceTargetListener {
+
+    companion object {
+        /**
+         * UI surface label for subscribing Smartspace updates. String must match with
+         * [BcSmartspaceDataPlugin.UI_SURFACE_MEDIA]
+         */
+        @JvmField val SMARTSPACE_UI_SURFACE_LABEL = "media_data_manager"
+
+        // Smartspace package name's extra key.
+        @JvmField val EXTRAS_MEDIA_SOURCE_PACKAGE_NAME = "package_name"
+
+        // Maximum number of actions allowed in compact view
+        @JvmField val MAX_COMPACT_ACTIONS = 3
+
+        /**
+         * Maximum number of actions allowed in expanded view. Number must match with the size of
+         * [MediaViewHolder.genericButtonIds]
+         */
+        @JvmField val MAX_NOTIFICATION_ACTIONS = 5
+    }
+
+    private val themeText =
+        com.android.settingslib.Utils.getColorAttr(
+                context,
+                com.android.internal.R.attr.textColorPrimary
+            )
+            .defaultColor
+
+    // Internal listeners are part of the internal pipeline. External listeners (those registered
+    // with [MediaDeviceManager.addListener]) receive events after they have propagated through
+    // the internal pipeline.
+    // Another way to think of the distinction between internal and external listeners is the
+    // following. Internal listeners are listeners that MediaDataProcessor depends on, and external
+    // listeners are listeners that depend on MediaDataProcessor.
+    private val internalListeners: MutableSet<Listener> = mutableSetOf()
+
+    // There should ONLY be at most one Smartspace media recommendation.
+    @Keep private var smartspaceSession: SmartspaceSession? = null
+    private var allowMediaRecommendations = false
+
+    private val artworkWidth =
+        context.resources.getDimensionPixelSize(
+            com.android.internal.R.dimen.config_mediaMetadataBitmapMaxSize
+        )
+    private val artworkHeight =
+        context.resources.getDimensionPixelSize(R.dimen.qs_media_session_height_expanded)
+
+    @SuppressLint("WrongConstant") // sysui allowed to call STATUS_BAR_SERVICE
+    private val statusBarManager =
+        context.getSystemService(Context.STATUS_BAR_SERVICE) as StatusBarManager
+
+    /** Check whether this notification is an RCN */
+    private fun isRemoteCastNotification(sbn: StatusBarNotification): Boolean {
+        return sbn.notification.extras.containsKey(Notification.EXTRA_MEDIA_REMOTE_DEVICE)
+    }
+
+    @Inject
+    constructor(
+        context: Context,
+        @Application applicationScope: CoroutineScope,
+        @Background backgroundDispatcher: CoroutineDispatcher,
+        threadFactory: ThreadFactory,
+        @Main uiExecutor: Executor,
+        @Main foregroundExecutor: DelayableExecutor,
+        @Main handler: Handler,
+        mediaControllerFactory: MediaControllerFactory,
+        dumpManager: DumpManager,
+        broadcastDispatcher: BroadcastDispatcher,
+        activityStarter: ActivityStarter,
+        smartspaceMediaDataProvider: SmartspaceMediaDataProvider,
+        clock: SystemClock,
+        secureSettings: SecureSettings,
+        mediaFlags: MediaFlags,
+        logger: MediaUiEventLogger,
+        smartspaceManager: SmartspaceManager?,
+        keyguardUpdateMonitor: KeyguardUpdateMonitor,
+        mediaDataRepository: MediaDataRepository,
+    ) : this(
+        context,
+        applicationScope,
+        backgroundDispatcher,
+        // Loading bitmap for UMO background can take longer time, so it cannot run on the default
+        // background thread. Use a custom thread for media.
+        threadFactory.buildExecutorOnNewThread(TAG),
+        uiExecutor,
+        foregroundExecutor,
+        handler,
+        mediaControllerFactory,
+        broadcastDispatcher,
+        dumpManager,
+        activityStarter,
+        smartspaceMediaDataProvider,
+        Utils.useMediaResumption(context),
+        Utils.useQsMediaPlayer(context),
+        clock,
+        secureSettings,
+        mediaFlags,
+        logger,
+        smartspaceManager,
+        keyguardUpdateMonitor,
+        mediaDataRepository,
+    )
+
+    private val appChangeReceiver =
+        object : BroadcastReceiver() {
+            override fun onReceive(context: Context, intent: Intent) {
+                when (intent.action) {
+                    Intent.ACTION_PACKAGES_SUSPENDED -> {
+                        val packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST)
+                        packages?.forEach { removeAllForPackage(it) }
+                    }
+                    Intent.ACTION_PACKAGE_REMOVED,
+                    Intent.ACTION_PACKAGE_RESTARTED -> {
+                        intent.data?.encodedSchemeSpecificPart?.let { removeAllForPackage(it) }
+                    }
+                }
+            }
+        }
+
+    override fun start() {
+        if (!mediaFlags.isMediaControlsRefactorEnabled()) {
+            return
+        }
+
+        dumpManager.registerNormalDumpable(TAG, this)
+
+        val suspendFilter = IntentFilter(Intent.ACTION_PACKAGES_SUSPENDED)
+        broadcastDispatcher.registerReceiver(appChangeReceiver, suspendFilter, null, UserHandle.ALL)
+
+        val uninstallFilter =
+            IntentFilter().apply {
+                addAction(Intent.ACTION_PACKAGE_REMOVED)
+                addAction(Intent.ACTION_PACKAGE_RESTARTED)
+                addDataScheme("package")
+            }
+        // BroadcastDispatcher does not allow filters with data schemes
+        context.registerReceiver(appChangeReceiver, uninstallFilter)
+
+        // Register for Smartspace data updates.
+        smartspaceMediaDataProvider.registerListener(this)
+        smartspaceSession =
+            smartspaceManager?.createSmartspaceSession(
+                SmartspaceConfig.Builder(context, SMARTSPACE_UI_SURFACE_LABEL).build()
+            )
+        smartspaceSession?.let {
+            it.addOnTargetsAvailableListener(
+                // Use a main uiExecutor thread listening to Smartspace updates instead of using
+                // the existing background executor.
+                // SmartspaceSession has scheduled routine updates which can be unpredictable on
+                // test simulators, using the backgroundExecutor makes it's hard to test the threads
+                // numbers.
+                uiExecutor
+            ) { targets ->
+                smartspaceMediaDataProvider.onTargetsAvailable(targets)
+            }
+        }
+        smartspaceSession?.requestSmartspaceUpdate()
+
+        // Track media controls recommendation setting.
+        applicationScope.launch { trackMediaControlsRecommendationSetting() }
+    }
+
+    fun destroy() {
+        smartspaceMediaDataProvider.unregisterListener(this)
+        smartspaceSession?.close()
+        smartspaceSession = null
+        context.unregisterReceiver(appChangeReceiver)
+        internalListeners.clear()
+    }
+
+    fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
+        if (useQsMediaPlayer && isMediaNotification(sbn)) {
+            var isNewlyActiveEntry = false
+            Assert.isMainThread()
+            val oldKey = findExistingEntry(key, sbn.packageName)
+            if (oldKey == null) {
+                val instanceId = logger.getNewInstanceId()
+                val temp =
+                    MediaData()
+                        .copy(
+                            packageName = sbn.packageName,
+                            instanceId = instanceId,
+                            createdTimestampMillis = systemClock.currentTimeMillis(),
+                        )
+                mediaDataRepository.addMediaEntry(key, temp)
+                isNewlyActiveEntry = true
+            } else if (oldKey != key) {
+                // Resume -> active conversion; move to new key
+                val oldData = mediaDataRepository.removeMediaEntry(oldKey)!!
+                isNewlyActiveEntry = true
+                mediaDataRepository.addMediaEntry(key, oldData)
+            }
+            loadMediaData(key, sbn, oldKey, isNewlyActiveEntry)
+        } else {
+            onNotificationRemoved(key)
+        }
+    }
+
+    /**
+     * Allow recommendations from smartspace to show in media controls. Requires
+     * [Utils.useQsMediaPlayer] to be enabled. On by default, but can be disabled by setting to 0
+     */
+    private suspend fun allowMediaRecommendations(): Boolean {
+        return withContext(backgroundDispatcher) {
+            val flag =
+                secureSettings.getBoolForUser(
+                    Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
+                    true,
+                    UserHandle.USER_CURRENT
+                )
+
+            useQsMediaPlayer && flag
+        }
+    }
+
+    private suspend fun trackMediaControlsRecommendationSetting() {
+        secureSettings
+            .observerFlow(UserHandle.USER_ALL, Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION)
+            // perform a query at the beginning.
+            .onStart { emit(Unit) }
+            .map { allowMediaRecommendations() }
+            .distinctUntilChanged()
+            // only track the most recent emission
+            .collectLatest {
+                allowMediaRecommendations = it
+                if (!allowMediaRecommendations) {
+                    dismissSmartspaceRecommendation(
+                        key = mediaDataRepository.smartspaceMediaData.value.targetId,
+                        delay = 0L
+                    )
+                }
+            }
+    }
+
+    private fun removeAllForPackage(packageName: String) {
+        Assert.isMainThread()
+        val toRemove =
+            mediaDataRepository.mediaEntries.value.filter { it.value.packageName == packageName }
+        toRemove.forEach { removeEntry(it.key) }
+    }
+
+    fun setResumeAction(key: String, action: Runnable?) {
+        mediaDataRepository.mediaEntries.value.get(key)?.let {
+            it.resumeAction = action
+            it.hasCheckedForResume = true
+        }
+    }
+
+    fun addResumptionControls(
+        userId: Int,
+        desc: MediaDescription,
+        action: Runnable,
+        token: MediaSession.Token,
+        appName: String,
+        appIntent: PendingIntent,
+        packageName: String
+    ) {
+        // Resume controls don't have a notification key, so store by package name instead
+        if (!mediaDataRepository.mediaEntries.value.containsKey(packageName)) {
+            val instanceId = logger.getNewInstanceId()
+            val appUid =
+                try {
+                    context.packageManager.getApplicationInfo(packageName, 0).uid
+                } catch (e: PackageManager.NameNotFoundException) {
+                    Log.w(TAG, "Could not get app UID for $packageName", e)
+                    Process.INVALID_UID
+                }
+
+            val resumeData =
+                MediaData()
+                    .copy(
+                        packageName = packageName,
+                        resumeAction = action,
+                        hasCheckedForResume = true,
+                        instanceId = instanceId,
+                        appUid = appUid,
+                        createdTimestampMillis = systemClock.currentTimeMillis(),
+                    )
+            mediaDataRepository.addMediaEntry(packageName, resumeData)
+            logSingleVsMultipleMediaAdded(appUid, packageName, instanceId)
+            logger.logResumeMediaAdded(appUid, packageName, instanceId)
+        }
+        backgroundExecutor.execute {
+            loadMediaDataInBgForResumption(
+                userId,
+                desc,
+                action,
+                token,
+                appName,
+                appIntent,
+                packageName
+            )
+        }
+    }
+
+    /**
+     * Check if there is an existing entry that matches the key or package name. Returns the key
+     * that matches, or null if not found.
+     */
+    private fun findExistingEntry(key: String, packageName: String): String? {
+        val mediaEntries = mediaDataRepository.mediaEntries.value
+        if (mediaEntries.containsKey(key)) {
+            return key
+        }
+        // Check if we already had a resume player
+        if (mediaEntries.containsKey(packageName)) {
+            return packageName
+        }
+        return null
+    }
+
+    private fun loadMediaData(
+        key: String,
+        sbn: StatusBarNotification,
+        oldKey: String?,
+        isNewlyActiveEntry: Boolean = false,
+    ) {
+        backgroundExecutor.execute { loadMediaDataInBg(key, sbn, oldKey, isNewlyActiveEntry) }
+    }
+
+    /** Add a listener for internal events. */
+    fun addInternalListener(listener: Listener) = internalListeners.add(listener)
+
+    /**
+     * Notify internal listeners of media loaded event.
+     *
+     * External listeners registered with [MediaCarouselInteractor.addListener] will be notified
+     * after the event propagates through the internal listener pipeline.
+     */
+    private fun notifyMediaDataLoaded(key: String, oldKey: String?, info: MediaData) {
+        internalListeners.forEach { it.onMediaDataLoaded(key, oldKey, info) }
+    }
+
+    /**
+     * Notify internal listeners of Smartspace media loaded event.
+     *
+     * External listeners registered with [MediaCarouselInteractor.addListener] will be notified
+     * after the event propagates through the internal listener pipeline.
+     */
+    private fun notifySmartspaceMediaDataLoaded(key: String, info: SmartspaceMediaData) {
+        internalListeners.forEach { it.onSmartspaceMediaDataLoaded(key, info) }
+    }
+
+    /**
+     * Notify internal listeners of media removed event.
+     *
+     * External listeners registered with [MediaCarouselInteractor.addListener] will be notified
+     * after the event propagates through the internal listener pipeline.
+     */
+    private fun notifyMediaDataRemoved(key: String) {
+        internalListeners.forEach { it.onMediaDataRemoved(key) }
+    }
+
+    /**
+     * Notify internal listeners of Smartspace media removed event.
+     *
+     * External listeners registered with [MediaCarouselInteractor.addListener] will be notified
+     * after the event propagates through the internal listener pipeline.
+     *
+     * @param immediately indicates should apply the UI changes immediately, otherwise wait until
+     *   the next refresh-round before UI becomes visible. Should only be true if the update is
+     *   initiated by user's interaction.
+     */
+    private fun notifySmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
+        internalListeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) }
+    }
+
+    /**
+     * Called whenever the player has been paused or stopped for a while, or swiped from QQS. This
+     * will make the player not active anymore, hiding it from QQS and Keyguard.
+     *
+     * @see MediaData.active
+     */
+    fun setInactive(key: String, timedOut: Boolean, forceUpdate: Boolean = false) {
+        mediaDataRepository.mediaEntries.value[key]?.let {
+            if (timedOut && !forceUpdate) {
+                // Only log this event when media expires on its own
+                logger.logMediaTimeout(it.appUid, it.packageName, it.instanceId)
+            }
+            if (it.active == !timedOut && !forceUpdate) {
+                if (it.resumption) {
+                    if (DEBUG) Log.d(TAG, "timing out resume player $key")
+                    dismissMediaData(key, 0L /* delay */)
+                }
+                return
+            }
+            // Update last active if media was still active.
+            if (it.active) {
+                it.lastActive = systemClock.elapsedRealtime()
+            }
+            it.active = !timedOut
+            if (DEBUG) Log.d(TAG, "Updating $key timedOut: $timedOut")
+            onMediaDataLoaded(key, key, it)
+        }
+
+        if (key == mediaDataRepository.smartspaceMediaData.value.targetId) {
+            if (DEBUG) Log.d(TAG, "smartspace card expired")
+            dismissSmartspaceRecommendation(key, delay = 0L)
+        }
+    }
+
+    /** Called when the player's [PlaybackState] has been updated with new actions and/or state */
+    internal fun updateState(key: String, state: PlaybackState) {
+        mediaDataRepository.mediaEntries.value.get(key)?.let {
+            val token = it.token
+            if (token == null) {
+                if (DEBUG) Log.d(TAG, "State updated, but token was null")
+                return
+            }
+            val actions =
+                createActionsFromState(
+                    it.packageName,
+                    mediaControllerFactory.create(it.token),
+                    UserHandle(it.userId)
+                )
+
+            // Control buttons
+            // If flag is enabled and controller has a PlaybackState,
+            // create actions from session info
+            // otherwise, no need to update semantic actions.
+            val data =
+                if (actions != null) {
+                    it.copy(semanticActions = actions, isPlaying = isPlayingState(state.state))
+                } else {
+                    it.copy(isPlaying = isPlayingState(state.state))
+                }
+            if (DEBUG) Log.d(TAG, "State updated outside of notification")
+            onMediaDataLoaded(key, key, data)
+        }
+    }
+
+    private fun removeEntry(key: String, logEvent: Boolean = true) {
+        mediaDataRepository.removeMediaEntry(key)?.let {
+            if (logEvent) {
+                logger.logMediaRemoved(it.appUid, it.packageName, it.instanceId)
+            }
+        }
+        notifyMediaDataRemoved(key)
+    }
+
+    /** Dismiss a media entry. Returns false if the key was not found. */
+    fun dismissMediaData(key: String, delay: Long): Boolean {
+        val existed = mediaDataRepository.mediaEntries.value[key] != null
+        backgroundExecutor.execute {
+            mediaDataRepository.mediaEntries.value[key]?.let { mediaData ->
+                if (mediaData.isLocalSession()) {
+                    mediaData.token?.let {
+                        val mediaController = mediaControllerFactory.create(it)
+                        mediaController.transportControls.stop()
+                    }
+                }
+            }
+        }
+        foregroundExecutor.executeDelayed({ removeEntry(key) }, delay)
+        return existed
+    }
+
+    /**
+     * Called whenever the recommendation has been expired or removed by the user. This will remove
+     * the recommendation card entirely from the carousel.
+     */
+    fun dismissSmartspaceRecommendation(key: String, delay: Long) {
+        if (mediaDataRepository.dismissSmartspaceRecommendation(key)) {
+            foregroundExecutor.executeDelayed(
+                { notifySmartspaceMediaDataRemoved(key, immediately = true) },
+                delay
+            )
+        }
+    }
+
+    /** Called when the recommendation card should no longer be visible in QQS or lockscreen */
+    fun setRecommendationInactive(key: String) {
+        if (mediaDataRepository.setRecommendationInactive(key)) {
+            val recommendation = mediaDataRepository.smartspaceMediaData.value
+            notifySmartspaceMediaDataLoaded(recommendation.targetId, recommendation)
+        }
+    }
+
+    private fun loadMediaDataInBgForResumption(
+        userId: Int,
+        desc: MediaDescription,
+        resumeAction: Runnable,
+        token: MediaSession.Token,
+        appName: String,
+        appIntent: PendingIntent,
+        packageName: String
+    ) {
+        if (desc.title.isNullOrBlank()) {
+            Log.e(TAG, "Description incomplete")
+            // Delete the placeholder entry
+            mediaDataRepository.removeMediaEntry(packageName)
+            return
+        }
+
+        if (DEBUG) {
+            Log.d(TAG, "adding track for $userId from browser: $desc")
+        }
+
+        val currentEntry = mediaDataRepository.mediaEntries.value.get(packageName)
+        val appUid = currentEntry?.appUid ?: Process.INVALID_UID
+
+        // Album art
+        var artworkBitmap = desc.iconBitmap
+        if (artworkBitmap == null && desc.iconUri != null) {
+            artworkBitmap = loadBitmapFromUriForUser(desc.iconUri!!, userId, appUid, packageName)
+        }
+        val artworkIcon =
+            if (artworkBitmap != null) {
+                Icon.createWithBitmap(artworkBitmap)
+            } else {
+                null
+            }
+
+        val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId()
+        val isExplicit =
+            desc.extras?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) ==
+                MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT
+
+        val progress =
+            if (mediaFlags.isResumeProgressEnabled()) {
+                MediaDataUtils.getDescriptionProgress(desc.extras)
+            } else null
+
+        val mediaAction = getResumeMediaAction(resumeAction)
+        val lastActive = systemClock.elapsedRealtime()
+        val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L
+        foregroundExecutor.execute {
+            onMediaDataLoaded(
+                packageName,
+                null,
+                MediaData(
+                    userId,
+                    true,
+                    appName,
+                    null,
+                    desc.subtitle,
+                    desc.title,
+                    artworkIcon,
+                    listOf(mediaAction),
+                    listOf(0),
+                    MediaButton(playOrPause = mediaAction),
+                    packageName,
+                    token,
+                    appIntent,
+                    device = null,
+                    active = false,
+                    resumeAction = resumeAction,
+                    resumption = true,
+                    notificationKey = packageName,
+                    hasCheckedForResume = true,
+                    lastActive = lastActive,
+                    createdTimestampMillis = createdTimestampMillis,
+                    instanceId = instanceId,
+                    appUid = appUid,
+                    isExplicit = isExplicit,
+                    resumeProgress = progress,
+                )
+            )
+        }
+    }
+
+    fun loadMediaDataInBg(
+        key: String,
+        sbn: StatusBarNotification,
+        oldKey: String?,
+        isNewlyActiveEntry: Boolean = false,
+    ) {
+        val token =
+            sbn.notification.extras.getParcelable(
+                Notification.EXTRA_MEDIA_SESSION,
+                MediaSession.Token::class.java
+            )
+        if (token == null) {
+            return
+        }
+        val mediaController = mediaControllerFactory.create(token)
+        val metadata = mediaController.metadata
+        val notif: Notification = sbn.notification
+
+        val appInfo =
+            notif.extras.getParcelable(
+                Notification.EXTRA_BUILDER_APPLICATION_INFO,
+                ApplicationInfo::class.java
+            )
+                ?: getAppInfoFromPackage(sbn.packageName)
+
+        // App name
+        val appName = getAppName(sbn, appInfo)
+
+        // Song name
+        var song: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE)
+        if (song.isNullOrBlank()) {
+            song = metadata?.getString(MediaMetadata.METADATA_KEY_TITLE)
+        }
+        if (song.isNullOrBlank()) {
+            song = HybridGroupManager.resolveTitle(notif)
+        }
+        if (song.isNullOrBlank()) {
+            // For apps that don't include a title, log and add a placeholder
+            song = context.getString(R.string.controls_media_empty_title, appName)
+            try {
+                statusBarManager.logBlankMediaTitle(sbn.packageName, sbn.user.identifier)
+            } catch (e: RuntimeException) {
+                Log.e(TAG, "Error reporting blank media title for package ${sbn.packageName}")
+            }
+        }
+
+        // Album art
+        var artworkBitmap = metadata?.let { loadBitmapFromUri(it) }
+        if (artworkBitmap == null) {
+            artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ART)
+        }
+        if (artworkBitmap == null) {
+            artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART)
+        }
+        val artWorkIcon =
+            if (artworkBitmap == null) {
+                notif.getLargeIcon()
+            } else {
+                Icon.createWithBitmap(artworkBitmap)
+            }
+
+        // App Icon
+        val smallIcon = sbn.notification.smallIcon
+
+        // Explicit Indicator
+        val isExplicit: Boolean
+        val mediaMetadataCompat = MediaMetadataCompat.fromMediaMetadata(metadata)
+        isExplicit =
+            mediaMetadataCompat?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) ==
+                MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT
+
+        // Artist name
+        var artist: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST)
+        if (artist.isNullOrBlank()) {
+            artist = HybridGroupManager.resolveText(notif)
+        }
+
+        // Device name (used for remote cast notifications)
+        var device: MediaDeviceData? = null
+        if (isRemoteCastNotification(sbn)) {
+            val extras = sbn.notification.extras
+            val deviceName = extras.getCharSequence(Notification.EXTRA_MEDIA_REMOTE_DEVICE, null)
+            val deviceIcon = extras.getInt(Notification.EXTRA_MEDIA_REMOTE_ICON, -1)
+            val deviceIntent =
+                extras.getParcelable(
+                    Notification.EXTRA_MEDIA_REMOTE_INTENT,
+                    PendingIntent::class.java
+                )
+            Log.d(TAG, "$key is RCN for $deviceName")
+
+            if (deviceName != null && deviceIcon > -1) {
+                // Name and icon must be present, but intent may be null
+                val enabled = deviceIntent != null && deviceIntent.isActivity
+                val deviceDrawable =
+                    Icon.createWithResource(sbn.packageName, deviceIcon)
+                        .loadDrawable(sbn.getPackageContext(context))
+                device =
+                    MediaDeviceData(
+                        enabled,
+                        deviceDrawable,
+                        deviceName,
+                        deviceIntent,
+                        showBroadcastButton = false
+                    )
+            }
+        }
+
+        // Control buttons
+        // If flag is enabled and controller has a PlaybackState, create actions from session info
+        // Otherwise, use the notification actions
+        var actionIcons: List<MediaAction> = emptyList()
+        var actionsToShowCollapsed: List<Int> = emptyList()
+        val semanticActions = createActionsFromState(sbn.packageName, mediaController, sbn.user)
+        if (semanticActions == null) {
+            val actions = createActionsFromNotification(sbn)
+            actionIcons = actions.first
+            actionsToShowCollapsed = actions.second
+        }
+
+        val playbackLocation =
+            if (isRemoteCastNotification(sbn)) MediaData.PLAYBACK_CAST_REMOTE
+            else if (
+                mediaController.playbackInfo?.playbackType ==
+                    MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL
+            )
+                MediaData.PLAYBACK_LOCAL
+            else MediaData.PLAYBACK_CAST_LOCAL
+        val isPlaying = mediaController.playbackState?.let { isPlayingState(it.state) }
+
+        val currentEntry = mediaDataRepository.mediaEntries.value.get(key)
+        val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId()
+        val appUid = appInfo?.uid ?: Process.INVALID_UID
+
+        if (isNewlyActiveEntry) {
+            logSingleVsMultipleMediaAdded(appUid, sbn.packageName, instanceId)
+            logger.logActiveMediaAdded(appUid, sbn.packageName, instanceId, playbackLocation)
+        } else if (playbackLocation != currentEntry?.playbackLocation) {
+            logger.logPlaybackLocationChange(appUid, sbn.packageName, instanceId, playbackLocation)
+        }
+
+        val lastActive = systemClock.elapsedRealtime()
+        val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L
+        foregroundExecutor.execute {
+            val resumeAction: Runnable? = mediaDataRepository.mediaEntries.value[key]?.resumeAction
+            val hasCheckedForResume =
+                mediaDataRepository.mediaEntries.value[key]?.hasCheckedForResume == true
+            val active = mediaDataRepository.mediaEntries.value[key]?.active ?: true
+            onMediaDataLoaded(
+                key,
+                oldKey,
+                MediaData(
+                    sbn.normalizedUserId,
+                    true,
+                    appName,
+                    smallIcon,
+                    artist,
+                    song,
+                    artWorkIcon,
+                    actionIcons,
+                    actionsToShowCollapsed,
+                    semanticActions,
+                    sbn.packageName,
+                    token,
+                    notif.contentIntent,
+                    device,
+                    active,
+                    resumeAction = resumeAction,
+                    playbackLocation = playbackLocation,
+                    notificationKey = key,
+                    hasCheckedForResume = hasCheckedForResume,
+                    isPlaying = isPlaying,
+                    isClearable = !sbn.isOngoing,
+                    lastActive = lastActive,
+                    createdTimestampMillis = createdTimestampMillis,
+                    instanceId = instanceId,
+                    appUid = appUid,
+                    isExplicit = isExplicit,
+                )
+            )
+        }
+    }
+
+    private fun logSingleVsMultipleMediaAdded(
+        appUid: Int,
+        packageName: String,
+        instanceId: InstanceId
+    ) {
+        if (mediaDataRepository.mediaEntries.value.size == 1) {
+            logger.logSingleMediaPlayerInCarousel(appUid, packageName, instanceId)
+        } else if (mediaDataRepository.mediaEntries.value.size == 2) {
+            // Since this method is only called when there is a new media session added.
+            // logging needed once there is more than one media session in carousel.
+            logger.logMultipleMediaPlayersInCarousel(appUid, packageName, instanceId)
+        }
+    }
+
+    private fun getAppInfoFromPackage(packageName: String): ApplicationInfo? {
+        try {
+            return context.packageManager.getApplicationInfo(packageName, 0)
+        } catch (e: PackageManager.NameNotFoundException) {
+            Log.w(TAG, "Could not get app info for $packageName", e)
+        }
+        return null
+    }
+
+    private fun getAppName(sbn: StatusBarNotification, appInfo: ApplicationInfo?): String {
+        val name = sbn.notification.extras.getString(EXTRA_SUBSTITUTE_APP_NAME)
+        if (name != null) {
+            return name
+        }
+
+        return if (appInfo != null) {
+            context.packageManager.getApplicationLabel(appInfo).toString()
+        } else {
+            sbn.packageName
+        }
+    }
+
+    /** Generate action buttons based on notification actions */
+    private fun createActionsFromNotification(
+        sbn: StatusBarNotification
+    ): Pair<List<MediaAction>, List<Int>> {
+        val notif = sbn.notification
+        val actionIcons: MutableList<MediaAction> = ArrayList()
+        val actions = notif.actions
+        var actionsToShowCollapsed =
+            notif.extras.getIntArray(Notification.EXTRA_COMPACT_ACTIONS)?.toMutableList()
+                ?: mutableListOf()
+        if (actionsToShowCollapsed.size > MAX_COMPACT_ACTIONS) {
+            Log.e(
+                TAG,
+                "Too many compact actions for ${sbn.key}," +
+                    "limiting to first $MAX_COMPACT_ACTIONS"
+            )
+            actionsToShowCollapsed = actionsToShowCollapsed.subList(0, MAX_COMPACT_ACTIONS)
+        }
+
+        if (actions != null) {
+            for ((index, action) in actions.withIndex()) {
+                if (index == MAX_NOTIFICATION_ACTIONS) {
+                    Log.w(
+                        TAG,
+                        "Too many notification actions for ${sbn.key}," +
+                            " limiting to first $MAX_NOTIFICATION_ACTIONS"
+                    )
+                    break
+                }
+                if (action.getIcon() == null) {
+                    if (DEBUG) Log.i(TAG, "No icon for action $index ${action.title}")
+                    actionsToShowCollapsed.remove(index)
+                    continue
+                }
+                val runnable =
+                    if (action.actionIntent != null) {
+                        Runnable {
+                            if (action.actionIntent.isActivity) {
+                                activityStarter.startPendingIntentDismissingKeyguard(
+                                    action.actionIntent
+                                )
+                            } else if (action.isAuthenticationRequired()) {
+                                activityStarter.dismissKeyguardThenExecute(
+                                    {
+                                        var result = sendPendingIntent(action.actionIntent)
+                                        result
+                                    },
+                                    {},
+                                    true
+                                )
+                            } else {
+                                sendPendingIntent(action.actionIntent)
+                            }
+                        }
+                    } else {
+                        null
+                    }
+                val mediaActionIcon =
+                    if (action.getIcon()?.getType() == Icon.TYPE_RESOURCE) {
+                            Icon.createWithResource(sbn.packageName, action.getIcon()!!.getResId())
+                        } else {
+                            action.getIcon()
+                        }
+                        .setTint(themeText)
+                        .loadDrawable(context)
+                val mediaAction = MediaAction(mediaActionIcon, runnable, action.title, null)
+                actionIcons.add(mediaAction)
+            }
+        }
+        return Pair(actionIcons, actionsToShowCollapsed)
+    }
+
+    /**
+     * Generates action button info for this media session based on the PlaybackState
+     *
+     * @param packageName Package name for the media app
+     * @param controller MediaController for the current session
+     * @return a Pair consisting of a list of media actions, and a list of ints representing which
+     *
+     * ```
+     *      of those actions should be shown in the compact player
+     * ```
+     */
+    private fun createActionsFromState(
+        packageName: String,
+        controller: MediaController,
+        user: UserHandle
+    ): MediaButton? {
+        val state = controller.playbackState
+        if (state == null || !mediaFlags.areMediaSessionActionsEnabled(packageName, user)) {
+            return null
+        }
+
+        // First, check for standard actions
+        val playOrPause =
+            if (isConnectingState(state.state)) {
+                // Spinner needs to be animating to render anything. Start it here.
+                val drawable =
+                    context.getDrawable(com.android.internal.R.drawable.progress_small_material)
+                (drawable as Animatable).start()
+                MediaAction(
+                    drawable,
+                    null, // no action to perform when clicked
+                    context.getString(R.string.controls_media_button_connecting),
+                    context.getDrawable(R.drawable.ic_media_connecting_container),
+                    // Specify a rebind id to prevent the spinner from restarting on later binds.
+                    com.android.internal.R.drawable.progress_small_material
+                )
+            } else if (isPlayingState(state.state)) {
+                getStandardAction(controller, state.actions, PlaybackState.ACTION_PAUSE)
+            } else {
+                getStandardAction(controller, state.actions, PlaybackState.ACTION_PLAY)
+            }
+        val prevButton =
+            getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_PREVIOUS)
+        val nextButton =
+            getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_NEXT)
+
+        // Then, create a way to build any custom actions that will be needed
+        val customActions =
+            state.customActions
+                .asSequence()
+                .filterNotNull()
+                .map { getCustomAction(packageName, controller, it) }
+                .iterator()
+        fun nextCustomAction() = if (customActions.hasNext()) customActions.next() else null
+
+        // Finally, assign the remaining button slots: play/pause A B C D
+        // A = previous, else custom action (if not reserved)
+        // B = next, else custom action (if not reserved)
+        // C and D are always custom actions
+        val reservePrev =
+            controller.extras?.getBoolean(
+                MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV
+            ) == true
+        val reserveNext =
+            controller.extras?.getBoolean(
+                MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT
+            ) == true
+
+        val prevOrCustom =
+            if (prevButton != null) {
+                prevButton
+            } else if (!reservePrev) {
+                nextCustomAction()
+            } else {
+                null
+            }
+
+        val nextOrCustom =
+            if (nextButton != null) {
+                nextButton
+            } else if (!reserveNext) {
+                nextCustomAction()
+            } else {
+                null
+            }
+
+        return MediaButton(
+            playOrPause,
+            nextOrCustom,
+            prevOrCustom,
+            nextCustomAction(),
+            nextCustomAction(),
+            reserveNext,
+            reservePrev
+        )
+    }
+
+    /**
+     * Create a [MediaAction] for a given action and media session
+     *
+     * @param controller MediaController for the session
+     * @param stateActions The actions included with the session's [PlaybackState]
+     * @param action A [PlaybackState.Actions] value representing what action to generate. One of:
+     * ```
+     *      [PlaybackState.ACTION_PLAY]
+     *      [PlaybackState.ACTION_PAUSE]
+     *      [PlaybackState.ACTION_SKIP_TO_PREVIOUS]
+     *      [PlaybackState.ACTION_SKIP_TO_NEXT]
+     * @return
+     * ```
+     *
+     * A [MediaAction] with correct values set, or null if the state doesn't support it
+     */
+    private fun getStandardAction(
+        controller: MediaController,
+        stateActions: Long,
+        @PlaybackState.Actions action: Long
+    ): MediaAction? {
+        if (!includesAction(stateActions, action)) {
+            return null
+        }
+
+        return when (action) {
+            PlaybackState.ACTION_PLAY -> {
+                MediaAction(
+                    context.getDrawable(R.drawable.ic_media_play),
+                    { controller.transportControls.play() },
+                    context.getString(R.string.controls_media_button_play),
+                    context.getDrawable(R.drawable.ic_media_play_container)
+                )
+            }
+            PlaybackState.ACTION_PAUSE -> {
+                MediaAction(
+                    context.getDrawable(R.drawable.ic_media_pause),
+                    { controller.transportControls.pause() },
+                    context.getString(R.string.controls_media_button_pause),
+                    context.getDrawable(R.drawable.ic_media_pause_container)
+                )
+            }
+            PlaybackState.ACTION_SKIP_TO_PREVIOUS -> {
+                MediaAction(
+                    context.getDrawable(R.drawable.ic_media_prev),
+                    { controller.transportControls.skipToPrevious() },
+                    context.getString(R.string.controls_media_button_prev),
+                    null
+                )
+            }
+            PlaybackState.ACTION_SKIP_TO_NEXT -> {
+                MediaAction(
+                    context.getDrawable(R.drawable.ic_media_next),
+                    { controller.transportControls.skipToNext() },
+                    context.getString(R.string.controls_media_button_next),
+                    null
+                )
+            }
+            else -> null
+        }
+    }
+
+    /** Check whether the actions from a [PlaybackState] include a specific action */
+    private fun includesAction(stateActions: Long, @PlaybackState.Actions action: Long): Boolean {
+        if (
+            (action == PlaybackState.ACTION_PLAY || action == PlaybackState.ACTION_PAUSE) &&
+                (stateActions and PlaybackState.ACTION_PLAY_PAUSE > 0L)
+        ) {
+            return true
+        }
+        return (stateActions and action != 0L)
+    }
+
+    /** Get a [MediaAction] representing a [PlaybackState.CustomAction] */
+    private fun getCustomAction(
+        packageName: String,
+        controller: MediaController,
+        customAction: PlaybackState.CustomAction
+    ): MediaAction {
+        return MediaAction(
+            Icon.createWithResource(packageName, customAction.icon).loadDrawable(context),
+            { controller.transportControls.sendCustomAction(customAction, customAction.extras) },
+            customAction.name,
+            null
+        )
+    }
+
+    /** Load a bitmap from the various Art metadata URIs */
+    private fun loadBitmapFromUri(metadata: MediaMetadata): Bitmap? {
+        for (uri in ART_URIS) {
+            val uriString = metadata.getString(uri)
+            if (!TextUtils.isEmpty(uriString)) {
+                val albumArt = loadBitmapFromUri(Uri.parse(uriString))
+                if (albumArt != null) {
+                    if (DEBUG) Log.d(TAG, "loaded art from $uri")
+                    return albumArt
+                }
+            }
+        }
+        return null
+    }
+
+    private fun sendPendingIntent(intent: PendingIntent): Boolean {
+        return try {
+            val options = BroadcastOptions.makeBasic()
+            options.setInteractive(true)
+            options.setPendingIntentBackgroundActivityStartMode(
+                ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
+            )
+            intent.send(options.toBundle())
+            true
+        } catch (e: PendingIntent.CanceledException) {
+            Log.d(TAG, "Intent canceled", e)
+            false
+        }
+    }
+
+    /** Returns a bitmap if the user can access the given URI, else null */
+    private fun loadBitmapFromUriForUser(
+        uri: Uri,
+        userId: Int,
+        appUid: Int,
+        packageName: String,
+    ): Bitmap? {
+        try {
+            val ugm = UriGrantsManager.getService()
+            ugm.checkGrantUriPermission_ignoreNonSystem(
+                appUid,
+                packageName,
+                ContentProvider.getUriWithoutUserId(uri),
+                Intent.FLAG_GRANT_READ_URI_PERMISSION,
+                ContentProvider.getUserIdFromUri(uri, userId)
+            )
+            return loadBitmapFromUri(uri)
+        } catch (e: SecurityException) {
+            Log.e(TAG, "Failed to get URI permission: $e")
+        }
+        return null
+    }
+
+    /**
+     * Load a bitmap from a URI
+     *
+     * @param uri the uri to load
+     * @return bitmap, or null if couldn't be loaded
+     */
+    private fun loadBitmapFromUri(uri: Uri): Bitmap? {
+        // ImageDecoder requires a scheme of the following types
+        if (uri.scheme == null) {
+            return null
+        }
+
+        if (
+            !uri.scheme.equals(ContentResolver.SCHEME_CONTENT) &&
+                !uri.scheme.equals(ContentResolver.SCHEME_ANDROID_RESOURCE) &&
+                !uri.scheme.equals(ContentResolver.SCHEME_FILE)
+        ) {
+            return null
+        }
+
+        val source = ImageDecoder.createSource(context.contentResolver, uri)
+        return try {
+            ImageDecoder.decodeBitmap(source) { decoder, info, _ ->
+                val width = info.size.width
+                val height = info.size.height
+                val scale =
+                    MediaDataUtils.getScaleFactor(
+                        APair(width, height),
+                        APair(artworkWidth, artworkHeight)
+                    )
+
+                // Downscale if needed
+                if (scale != 0f && scale < 1) {
+                    decoder.setTargetSize((scale * width).toInt(), (scale * height).toInt())
+                }
+                decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
+            }
+        } catch (e: IOException) {
+            Log.e(TAG, "Unable to load bitmap", e)
+            null
+        } catch (e: RuntimeException) {
+            Log.e(TAG, "Unable to load bitmap", e)
+            null
+        }
+    }
+
+    private fun getResumeMediaAction(action: Runnable): MediaAction {
+        return MediaAction(
+            Icon.createWithResource(context, R.drawable.ic_media_play)
+                .setTint(themeText)
+                .loadDrawable(context),
+            action,
+            context.getString(R.string.controls_media_resume),
+            context.getDrawable(R.drawable.ic_media_play_container)
+        )
+    }
+
+    fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) =
+        traceSection("MediaDataProcessor#onMediaDataLoaded") {
+            Assert.isMainThread()
+            if (mediaDataRepository.mediaEntries.value.containsKey(key)) {
+                // Otherwise this was removed already
+                mediaDataRepository.addMediaEntry(key, data)
+                notifyMediaDataLoaded(key, oldKey, data)
+            }
+        }
+
+    override fun onSmartspaceTargetsUpdated(targets: List<Parcelable>) {
+        if (!allowMediaRecommendations) {
+            if (DEBUG) Log.d(TAG, "Smartspace recommendation is disabled in Settings.")
+            return
+        }
+
+        val mediaTargets = targets.filterIsInstance<SmartspaceTarget>()
+        val smartspaceMediaData = mediaDataRepository.smartspaceMediaData.value
+        when (mediaTargets.size) {
+            0 -> {
+                if (!smartspaceMediaData.isActive) {
+                    return
+                }
+                if (DEBUG) {
+                    Log.d(TAG, "Set Smartspace media to be inactive for the data update")
+                }
+                if (mediaFlags.isPersistentSsCardEnabled()) {
+                    // Smartspace uses this signal to hide the card (e.g. when it expires or user
+                    // disconnects headphones), so treat as setting inactive when flag is on
+                    val recommendation = smartspaceMediaData.copy(isActive = false)
+                    mediaDataRepository.setRecommendation(recommendation)
+                    notifySmartspaceMediaDataLoaded(recommendation.targetId, recommendation)
+                } else {
+                    notifySmartspaceMediaDataRemoved(
+                        smartspaceMediaData.targetId,
+                        immediately = false
+                    )
+                    mediaDataRepository.setRecommendation(
+                        SmartspaceMediaData(
+                            targetId = smartspaceMediaData.targetId,
+                            instanceId = smartspaceMediaData.instanceId,
+                        )
+                    )
+                }
+            }
+            1 -> {
+                val newMediaTarget = mediaTargets.get(0)
+                if (smartspaceMediaData.targetId == newMediaTarget.smartspaceTargetId) {
+                    // The same Smartspace updates can be received. Skip the duplicate updates.
+                    return
+                }
+                if (DEBUG) Log.d(TAG, "Forwarding Smartspace media update.")
+                val recommendation = toSmartspaceMediaData(newMediaTarget)
+                mediaDataRepository.setRecommendation(recommendation)
+                notifySmartspaceMediaDataLoaded(recommendation.targetId, recommendation)
+            }
+            else -> {
+                // There should NOT be more than 1 Smartspace media update. When it happens, it
+                // indicates a bad state or an error. Reset the status accordingly.
+                Log.wtf(TAG, "More than 1 Smartspace Media Update. Resetting the status...")
+                notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = false)
+                mediaDataRepository.setRecommendation(SmartspaceMediaData())
+            }
+        }
+    }
+
+    fun onNotificationRemoved(key: String) {
+        Assert.isMainThread()
+        val removed = mediaDataRepository.removeMediaEntry(key) ?: return
+        if (keyguardUpdateMonitor.isUserInLockdown(removed.userId)) {
+            logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
+        } else if (isAbleToResume(removed)) {
+            convertToResumePlayer(key, removed)
+        } else if (mediaFlags.isRetainingPlayersEnabled()) {
+            handlePossibleRemoval(key, removed, notificationRemoved = true)
+        } else {
+            notifyMediaDataRemoved(key)
+            logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
+        }
+    }
+
+    internal fun onSessionDestroyed(key: String) {
+        if (DEBUG) Log.d(TAG, "session destroyed for $key")
+        val entry = mediaDataRepository.removeMediaEntry(key) ?: return
+        // Clear token since the session is no longer valid
+        val updated = entry.copy(token = null)
+        handlePossibleRemoval(key, updated)
+    }
+
+    private fun isAbleToResume(data: MediaData): Boolean {
+        val isEligibleForResume =
+            data.isLocalSession() ||
+                (mediaFlags.isRemoteResumeAllowed() &&
+                    data.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE)
+        return useMediaResumption && data.resumeAction != null && isEligibleForResume
+    }
+
+    /**
+     * Convert to resume state if the player is no longer valid and active, then notify listeners
+     * that the data was updated. Does not convert to resume state if the player is still valid, or
+     * if it was removed before becoming inactive. (Assumes that [removed] was removed from
+     * [mediaDataRepository.mediaEntries] state before this function was called)
+     */
+    private fun handlePossibleRemoval(
+        key: String,
+        removed: MediaData,
+        notificationRemoved: Boolean = false
+    ) {
+        val hasSession = removed.token != null
+        if (hasSession && removed.semanticActions != null) {
+            // The app was using session actions, and the session is still valid: keep player
+            if (DEBUG) Log.d(TAG, "Notification removed but using session actions $key")
+            mediaDataRepository.addMediaEntry(key, removed)
+            notifyMediaDataLoaded(key, key, removed)
+        } else if (!notificationRemoved && removed.semanticActions == null) {
+            // The app was using notification actions, and notif wasn't removed yet: keep player
+            if (DEBUG) Log.d(TAG, "Session destroyed but using notification actions $key")
+            mediaDataRepository.addMediaEntry(key, removed)
+            notifyMediaDataLoaded(key, key, removed)
+        } else if (removed.active && !isAbleToResume(removed)) {
+            // This player was still active - it didn't last long enough to time out,
+            // and its app doesn't normally support resume: remove
+            if (DEBUG) Log.d(TAG, "Removing still-active player $key")
+            notifyMediaDataRemoved(key)
+            logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
+        } else if (mediaFlags.isRetainingPlayersEnabled() || isAbleToResume(removed)) {
+            // Convert to resume
+            if (DEBUG) {
+                Log.d(
+                    TAG,
+                    "Notification ($notificationRemoved) and/or session " +
+                        "($hasSession) gone for inactive player $key"
+                )
+            }
+            convertToResumePlayer(key, removed)
+        } else {
+            // Retaining players flag is off and app doesn't support resume: remove player.
+            if (DEBUG) Log.d(TAG, "Removing player $key")
+            notifyMediaDataRemoved(key)
+            logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
+        }
+    }
+
+    /** Set the given [MediaData] as a resume state player and notify listeners */
+    private fun convertToResumePlayer(key: String, data: MediaData) {
+        if (DEBUG) Log.d(TAG, "Converting $key to resume")
+        // Resumption controls must have a title.
+        if (data.song.isNullOrBlank()) {
+            Log.e(TAG, "Description incomplete")
+            notifyMediaDataRemoved(key)
+            logger.logMediaRemoved(data.appUid, data.packageName, data.instanceId)
+            return
+        }
+        // Move to resume key (aka package name) if that key doesn't already exist.
+        val resumeAction = data.resumeAction?.let { getResumeMediaAction(it) }
+        val actions = resumeAction?.let { listOf(resumeAction) } ?: emptyList()
+        val launcherIntent =
+            context.packageManager.getLaunchIntentForPackage(data.packageName)?.let {
+                PendingIntent.getActivity(context, 0, it, PendingIntent.FLAG_IMMUTABLE)
+            }
+        val lastActive =
+            if (data.active) {
+                systemClock.elapsedRealtime()
+            } else {
+                data.lastActive
+            }
+        val updated =
+            data.copy(
+                token = null,
+                actions = actions,
+                semanticActions = MediaButton(playOrPause = resumeAction),
+                actionsToShowInCompact = listOf(0),
+                active = false,
+                resumption = true,
+                isPlaying = false,
+                isClearable = true,
+                clickIntent = launcherIntent,
+                lastActive = lastActive,
+            )
+        val pkg = data.packageName
+        val migrate = mediaDataRepository.addMediaEntry(pkg, updated) == null
+        // Notify listeners of "new" controls when migrating or removed and update when not
+        Log.d(TAG, "migrating? $migrate from $key -> $pkg")
+        if (migrate) {
+            notifyMediaDataLoaded(key = pkg, oldKey = key, info = updated)
+        } else {
+            // Since packageName is used for the key of the resumption controls, it is
+            // possible that another notification has already been reused for the resumption
+            // controls of this package. In this case, rather than renaming this player as
+            // packageName, just remove it and then send a update to the existing resumption
+            // controls.
+            notifyMediaDataRemoved(key)
+            notifyMediaDataLoaded(key = pkg, oldKey = pkg, info = updated)
+        }
+        logger.logActiveConvertedToResume(updated.appUid, pkg, updated.instanceId)
+
+        // Limit total number of resume controls
+        val resumeEntries =
+            mediaDataRepository.mediaEntries.value.filter { (_, data) -> data.resumption }
+        val numResume = resumeEntries.size
+        if (numResume > ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) {
+            resumeEntries
+                .toList()
+                .sortedBy { (_, data) -> data.lastActive }
+                .subList(0, numResume - ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS)
+                .forEach { (key, data) ->
+                    Log.d(TAG, "Removing excess control $key")
+                    mediaDataRepository.removeMediaEntry(key)
+                    notifyMediaDataRemoved(key)
+                    logger.logMediaRemoved(data.appUid, data.packageName, data.instanceId)
+                }
+        }
+    }
+
+    fun setMediaResumptionEnabled(isEnabled: Boolean) {
+        if (useMediaResumption == isEnabled) {
+            return
+        }
+
+        useMediaResumption = isEnabled
+
+        if (!useMediaResumption) {
+            // Remove any existing resume controls
+            val filtered = mediaDataRepository.mediaEntries.value.filter { !it.value.active }
+            filtered.forEach {
+                mediaDataRepository.removeMediaEntry(it.key)
+                notifyMediaDataRemoved(it.key)
+                logger.logMediaRemoved(it.value.appUid, it.value.packageName, it.value.instanceId)
+            }
+        }
+    }
+
+    /** Listener to data changes. */
+    interface Listener {
+
+        /**
+         * Called whenever there's new MediaData Loaded for the consumption in views.
+         *
+         * oldKey is provided to check whether the view has changed keys, which can happen when a
+         * player has gone from resume state (key is package name) to active state (key is
+         * notification key) or vice versa.
+         *
+         * @param immediately indicates should apply the UI changes immediately, otherwise wait
+         *   until the next refresh-round before UI becomes visible. True by default to take in
+         *   place immediately.
+         * @param receivedSmartspaceCardLatency is the latency between headphone connects and sysUI
+         *   displays Smartspace media targets. Will be 0 if the data is not activated by Smartspace
+         *   signal.
+         * @param isSsReactivated indicates resume media card is reactivated by Smartspace
+         *   recommendation signal
+         */
+        fun onMediaDataLoaded(
+            key: String,
+            oldKey: String?,
+            data: MediaData,
+            immediately: Boolean = true,
+            receivedSmartspaceCardLatency: Int = 0,
+            isSsReactivated: Boolean = false
+        ) {}
+
+        /**
+         * Called whenever there's new Smartspace media data loaded.
+         *
+         * @param shouldPrioritize indicates the sorting priority of the Smartspace card. If true,
+         *   it will be prioritized as the first card. Otherwise, it will show up as the last card
+         *   as default.
+         */
+        fun onSmartspaceMediaDataLoaded(
+            key: String,
+            data: SmartspaceMediaData,
+            shouldPrioritize: Boolean = false
+        ) {}
+
+        /** Called whenever a previously existing Media notification was removed. */
+        fun onMediaDataRemoved(key: String) {}
+
+        /**
+         * Called whenever a previously existing Smartspace media data was removed.
+         *
+         * @param immediately indicates should apply the UI changes immediately, otherwise wait
+         *   until the next refresh-round before UI becomes visible. True by default to take in
+         *   place immediately.
+         */
+        fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean = true) {}
+    }
+
+    /**
+     * Converts the pass-in SmartspaceTarget to SmartspaceMediaData
+     *
+     * @return An empty SmartspaceMediaData with the valid target Id is returned if the
+     *   SmartspaceTarget's data is invalid.
+     */
+    private fun toSmartspaceMediaData(target: SmartspaceTarget): SmartspaceMediaData {
+        val baseAction: SmartspaceAction? = target.baseAction
+        val dismissIntent =
+            baseAction
+                ?.extras
+                ?.getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY, Intent::class.java)
+
+        val isActive =
+            when {
+                !mediaFlags.isPersistentSsCardEnabled() -> true
+                baseAction == null -> true
+                else -> {
+                    val triggerSource = baseAction.extras?.getString(EXTRA_KEY_TRIGGER_SOURCE)
+                    triggerSource != EXTRA_VALUE_TRIGGER_PERIODIC
+                }
+            }
+
+        packageName(target)?.let {
+            return SmartspaceMediaData(
+                targetId = target.smartspaceTargetId,
+                isActive = isActive,
+                packageName = it,
+                cardAction = target.baseAction,
+                recommendations = target.iconGrid,
+                dismissIntent = dismissIntent,
+                headphoneConnectionTimeMillis = target.creationTimeMillis,
+                instanceId = logger.getNewInstanceId(),
+                expiryTimeMs = target.expiryTimeMillis,
+            )
+        }
+        return SmartspaceMediaData(
+            targetId = target.smartspaceTargetId,
+            isActive = isActive,
+            dismissIntent = dismissIntent,
+            headphoneConnectionTimeMillis = target.creationTimeMillis,
+            instanceId = logger.getNewInstanceId(),
+            expiryTimeMs = target.expiryTimeMillis,
+        )
+    }
+
+    private fun packageName(target: SmartspaceTarget): String? {
+        val recommendationList: MutableList<SmartspaceAction> = target.iconGrid
+        if (recommendationList.isEmpty()) {
+            Log.w(TAG, "Empty or null media recommendation list.")
+            return null
+        }
+        for (recommendation in recommendationList) {
+            val extras = recommendation.extras
+            extras?.let {
+                it.getString(EXTRAS_MEDIA_SOURCE_PACKAGE_NAME)?.let { packageName ->
+                    return packageName
+                }
+            }
+        }
+        Log.w(TAG, "No valid package name is provided.")
+        return null
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        pw.apply {
+            println("internalListeners: $internalListeners")
+            println("useMediaResumption: $useMediaResumption")
+            println("allowMediaRecommendations: $allowMediaRecommendations")
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt
index f4d70a5..c7cfb0b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt
@@ -35,10 +35,8 @@
 import com.android.settingslib.media.LocalMediaManager
 import com.android.settingslib.media.MediaDevice
 import com.android.settingslib.media.PhoneMediaDevice
-import com.android.systemui.Dumpable
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.dump.DumpManager
 import com.android.systemui.media.controls.shared.model.MediaData
 import com.android.systemui.media.controls.shared.model.MediaDeviceData
 import com.android.systemui.media.controls.util.LocalMediaManagerFactory
@@ -70,16 +68,11 @@
     private val localBluetoothManager: Lazy<LocalBluetoothManager?>,
     @Main private val fgExecutor: Executor,
     @Background private val bgExecutor: Executor,
-    dumpManager: DumpManager,
-) : MediaDataManager.Listener, Dumpable {
+) : MediaDataManager.Listener {
 
     private val listeners: MutableSet<Listener> = mutableSetOf()
     private val entries: MutableMap<String, Entry> = mutableMapOf()
 
-    init {
-        dumpManager.registerDumpable(this)
-    }
-
     /** Add a listener for changes to the media route (ie. device). */
     fun addListener(listener: Listener) = listeners.add(listener)
 
@@ -123,7 +116,7 @@
         token?.let { listeners.forEach { it.onKeyRemoved(key) } }
     }
 
-    override fun dump(pw: PrintWriter, args: Array<String>) {
+    fun dump(pw: PrintWriter) {
         with(pw) {
             println("MediaDeviceManager state:")
             entries.forEach { (key, entry) ->
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt
new file mode 100644
index 0000000..4a92b71
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt
@@ -0,0 +1,234 @@
+/*
+ * 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.media.controls.domain.pipeline.interactor
+
+import android.app.PendingIntent
+import android.media.MediaDescription
+import android.media.session.MediaSession
+import android.media.session.PlaybackState
+import android.service.notification.StatusBarNotification
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.media.controls.data.repository.MediaDataRepository
+import com.android.systemui.media.controls.data.repository.MediaFilterRepository
+import com.android.systemui.media.controls.domain.pipeline.MediaDataCombineLatest
+import com.android.systemui.media.controls.domain.pipeline.MediaDataFilterImpl
+import com.android.systemui.media.controls.domain.pipeline.MediaDataManager
+import com.android.systemui.media.controls.domain.pipeline.MediaDataProcessor
+import com.android.systemui.media.controls.domain.pipeline.MediaDeviceManager
+import com.android.systemui.media.controls.domain.pipeline.MediaSessionBasedFilter
+import com.android.systemui.media.controls.domain.pipeline.MediaTimeoutListener
+import com.android.systemui.media.controls.domain.resume.MediaResumeListener
+import com.android.systemui.media.controls.util.MediaFlags
+import java.io.PrintWriter
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.stateIn
+
+/** Encapsulates business logic for media pipeline. */
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class MediaCarouselInteractor
+@Inject
+constructor(
+    @Application applicationScope: CoroutineScope,
+    private val mediaDataRepository: MediaDataRepository,
+    private val mediaDataProcessor: MediaDataProcessor,
+    private val mediaTimeoutListener: MediaTimeoutListener,
+    private val mediaResumeListener: MediaResumeListener,
+    private val mediaSessionBasedFilter: MediaSessionBasedFilter,
+    private val mediaDeviceManager: MediaDeviceManager,
+    private val mediaDataCombineLatest: MediaDataCombineLatest,
+    private val mediaDataFilter: MediaDataFilterImpl,
+    mediaFilterRepository: MediaFilterRepository,
+    private val mediaFlags: MediaFlags,
+) : MediaDataManager, CoreStartable {
+
+    /** Are there any media notifications active, including the recommendations? */
+    val hasActiveMediaOrRecommendation: StateFlow<Boolean> =
+        combine(
+                mediaFilterRepository.selectedUserEntries,
+                mediaFilterRepository.smartspaceMediaData,
+                mediaFilterRepository.reactivatedKey
+            ) { entries, smartspaceMediaData, reactivatedKey ->
+                entries.any { it.value.active } ||
+                    (smartspaceMediaData.isActive &&
+                        (smartspaceMediaData.isValid() || reactivatedKey != null))
+            }
+            .distinctUntilChanged()
+            .stateIn(applicationScope, SharingStarted.WhileSubscribed(), false)
+
+    /** Are there any media entries we should display, including the recommendations? */
+    val hasAnyMediaOrRecommendation: StateFlow<Boolean> =
+        combine(
+                mediaFilterRepository.selectedUserEntries,
+                mediaFilterRepository.smartspaceMediaData
+            ) { entries, smartspaceMediaData ->
+                entries.isNotEmpty() ||
+                    (if (mediaFlags.isPersistentSsCardEnabled()) {
+                        smartspaceMediaData.isValid()
+                    } else {
+                        smartspaceMediaData.isActive && smartspaceMediaData.isValid()
+                    })
+            }
+            .distinctUntilChanged()
+            .stateIn(applicationScope, SharingStarted.WhileSubscribed(), false)
+
+    /** Are there any media notifications active, excluding the recommendations? */
+    val hasActiveMedia: StateFlow<Boolean> =
+        mediaFilterRepository.selectedUserEntries
+            .mapLatest { entries -> entries.any { it.value.active } }
+            .distinctUntilChanged()
+            .stateIn(applicationScope, SharingStarted.WhileSubscribed(), false)
+
+    /** Are there any media notifications, excluding the recommendations? */
+    val hasAnyMedia: StateFlow<Boolean> =
+        mediaFilterRepository.selectedUserEntries
+            .mapLatest { entries -> entries.isNotEmpty() }
+            .distinctUntilChanged()
+            .stateIn(applicationScope, SharingStarted.WhileSubscribed(), false)
+
+    override fun start() {
+        if (!mediaFlags.isMediaControlsRefactorEnabled()) {
+            return
+        }
+
+        // Initialize the internal processing pipeline. The listeners at the front of the pipeline
+        // are set as internal listeners so that they receive events. From there, events are
+        // propagated through the pipeline. The end of the pipeline is currently mediaDataFilter,
+        // so it is responsible for dispatching events to external listeners. To achieve this,
+        // external listeners that are registered with [MediaDataManager.addListener] are actually
+        // registered as listeners to mediaDataFilter.
+        addInternalListener(mediaTimeoutListener)
+        addInternalListener(mediaResumeListener)
+        addInternalListener(mediaSessionBasedFilter)
+        mediaSessionBasedFilter.addListener(mediaDeviceManager)
+        mediaSessionBasedFilter.addListener(mediaDataCombineLatest)
+        mediaDeviceManager.addListener(mediaDataCombineLatest)
+        mediaDataCombineLatest.addListener(mediaDataFilter)
+
+        // Set up links back into the pipeline for listeners that need to send events upstream.
+        mediaTimeoutListener.timeoutCallback = { key: String, timedOut: Boolean ->
+            setInactive(key, timedOut)
+        }
+        mediaTimeoutListener.stateCallback = { key: String, state: PlaybackState ->
+            mediaDataProcessor.updateState(key, state)
+        }
+        mediaTimeoutListener.sessionCallback = { key: String ->
+            mediaDataProcessor.onSessionDestroyed(key)
+        }
+        mediaResumeListener.setManager(this)
+        mediaDataFilter.mediaDataManager = this
+    }
+
+    override fun addListener(listener: MediaDataManager.Listener) {
+        mediaDataFilter.addListener(listener)
+    }
+
+    override fun removeListener(listener: MediaDataManager.Listener) {
+        mediaDataFilter.removeListener(listener)
+    }
+
+    override fun setInactive(key: String, timedOut: Boolean, forceUpdate: Boolean) {
+        mediaDataProcessor.setInactive(key, timedOut, forceUpdate)
+    }
+
+    override fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
+        mediaDataProcessor.onNotificationAdded(key, sbn)
+    }
+
+    override fun destroy() {
+        mediaSessionBasedFilter.removeListener(mediaDeviceManager)
+        mediaSessionBasedFilter.removeListener(mediaDataCombineLatest)
+        mediaDeviceManager.removeListener(mediaDataCombineLatest)
+        mediaDataCombineLatest.removeListener(mediaDataFilter)
+        mediaDataProcessor.destroy()
+    }
+
+    override fun setResumeAction(key: String, action: Runnable?) {
+        mediaDataProcessor.setResumeAction(key, action)
+    }
+
+    override fun addResumptionControls(
+        userId: Int,
+        desc: MediaDescription,
+        action: Runnable,
+        token: MediaSession.Token,
+        appName: String,
+        appIntent: PendingIntent,
+        packageName: String
+    ) {
+        mediaDataProcessor.addResumptionControls(
+            userId,
+            desc,
+            action,
+            token,
+            appName,
+            appIntent,
+            packageName
+        )
+    }
+
+    override fun dismissMediaData(key: String, delay: Long): Boolean {
+        return mediaDataProcessor.dismissMediaData(key, delay)
+    }
+
+    override fun dismissSmartspaceRecommendation(key: String, delay: Long) {
+        return mediaDataProcessor.dismissSmartspaceRecommendation(key, delay)
+    }
+
+    override fun setRecommendationInactive(key: String) {
+        mediaDataProcessor.setRecommendationInactive(key)
+    }
+
+    override fun onNotificationRemoved(key: String) {
+        mediaDataProcessor.onNotificationRemoved(key)
+    }
+
+    override fun setMediaResumptionEnabled(isEnabled: Boolean) {
+        mediaDataProcessor.setMediaResumptionEnabled(isEnabled)
+    }
+
+    override fun onSwipeToDismiss() {
+        mediaDataFilter.onSwipeToDismiss()
+    }
+
+    override fun hasActiveMediaOrRecommendation() = hasActiveMediaOrRecommendation.value
+
+    override fun hasAnyMediaOrRecommendation() = hasAnyMediaOrRecommendation.value
+
+    override fun hasActiveMedia() = hasActiveMedia.value
+
+    override fun hasAnyMedia() = hasAnyMedia.value
+
+    override fun isRecommendationActive() = mediaDataRepository.smartspaceMediaData.value.isActive
+
+    /** Add a listener for internal events. */
+    private fun addInternalListener(listener: MediaDataManager.Listener) =
+        mediaDataProcessor.addInternalListener(listener)
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        mediaDeviceManager.dump(pw)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaData.kt
index 4fa7cb5..11a5629 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaData.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaData.kt
@@ -20,48 +20,49 @@
 import android.graphics.drawable.Drawable
 import android.graphics.drawable.Icon
 import android.media.session.MediaSession
+import android.os.Process
 import com.android.internal.logging.InstanceId
 import com.android.systemui.res.R
 
 /** State of a media view. */
 data class MediaData(
-    val userId: Int,
+    val userId: Int = -1,
     val initialized: Boolean = false,
     /** App name that will be displayed on the player. */
-    val app: String?,
+    val app: String? = null,
     /** App icon shown on player. */
-    val appIcon: Icon?,
+    val appIcon: Icon? = null,
     /** Artist name. */
-    val artist: CharSequence?,
+    val artist: CharSequence? = null,
     /** Song name. */
-    val song: CharSequence?,
+    val song: CharSequence? = null,
     /** Album artwork. */
-    val artwork: Icon?,
+    val artwork: Icon? = null,
     /** List of generic action buttons for the media player, based on notification actions */
-    val actions: List<MediaAction>,
+    val actions: List<MediaAction> = emptyList(),
     /** Same as above, but shown on smaller versions of the player, like in QQS or keyguard. */
-    val actionsToShowInCompact: List<Int>,
+    val actionsToShowInCompact: List<Int> = emptyList(),
     /**
      * Semantic actions buttons, based on the PlaybackState of the media session. If present, these
      * actions will be preferred in the UI over [actions]
      */
     val semanticActions: MediaButton? = null,
     /** Package name of the app that's posting the media. */
-    val packageName: String,
+    val packageName: String = "INVALID",
     /** Unique media session identifier. */
-    val token: MediaSession.Token?,
+    val token: MediaSession.Token? = null,
     /** Action to perform when the player is tapped. This is unrelated to {@link #actions}. */
-    val clickIntent: PendingIntent?,
+    val clickIntent: PendingIntent? = null,
     /** Where the media is playing: phone, headphones, ear buds, remote session. */
-    val device: MediaDeviceData?,
+    val device: MediaDeviceData? = null,
     /**
      * When active, a player will be displayed on keyguard and quick-quick settings. This is
      * unrelated to the stream being playing or not, a player will not be active if timed out, or in
      * resumption mode.
      */
-    var active: Boolean,
+    var active: Boolean = true,
     /** Action that should be performed to restart a non active session. */
-    var resumeAction: Runnable?,
+    var resumeAction: Runnable? = null,
     /** Playback location: one of PLAYBACK_LOCAL, PLAYBACK_CAST_LOCAL, or PLAYBACK_CAST_REMOTE */
     var playbackLocation: Int = PLAYBACK_LOCAL,
     /**
@@ -88,10 +89,10 @@
     var createdTimestampMillis: Long = 0L,
 
     /** Instance ID for logging purposes */
-    val instanceId: InstanceId,
+    val instanceId: InstanceId = InstanceId.fakeInstanceId(-1),
 
     /** The UID of the app, used for logging */
-    val appUid: Int,
+    val appUid: Int = Process.INVALID_UID,
 
     /** Whether explicit indicator exists */
     val isExplicit: Boolean = false,
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/SmartspaceMediaData.kt b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/SmartspaceMediaData.kt
index 52c605f..b446585 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/SmartspaceMediaData.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/SmartspaceMediaData.kt
@@ -30,23 +30,23 @@
 /** State of a Smartspace media recommendations view. */
 data class SmartspaceMediaData(
     /** Unique id of a Smartspace media target. */
-    val targetId: String,
+    val targetId: String = "INVALID",
     /** Indicates if the status is active. */
-    val isActive: Boolean,
+    val isActive: Boolean = false,
     /** Package name of the media recommendations' provider-app. */
-    val packageName: String,
+    val packageName: String = "INVALID",
     /** Action to perform when the card is tapped. Also contains the target's extra info. */
-    val cardAction: SmartspaceAction?,
+    val cardAction: SmartspaceAction? = null,
     /** List of media recommendations. */
-    val recommendations: List<SmartspaceAction>,
+    val recommendations: List<SmartspaceAction> = emptyList(),
     /** Intent for the user's initiated dismissal. */
-    val dismissIntent: Intent?,
+    val dismissIntent: Intent? = null,
     /** The timestamp in milliseconds that the card was generated */
-    val headphoneConnectionTimeMillis: Long,
+    val headphoneConnectionTimeMillis: Long = 0L,
     /** Instance ID for [MediaUiEventLogger] */
-    val instanceId: InstanceId,
+    val instanceId: InstanceId? = null,
     /** The timestamp in milliseconds indicating when the card should be removed */
-    val expiryTimeMs: Long,
+    val expiryTimeMs: Long = 0L,
 ) {
     /**
      * Indicates if all the data is valid.
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt
index ba7d410..89a9ba7 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt
@@ -27,10 +27,10 @@
 import android.view.ViewGroup
 import androidx.annotation.VisibleForTesting
 import com.android.systemui.Dumpable
-import com.android.systemui.Flags.migrateClocksToBlueprint
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.media.controls.ui.view.MediaHost
 import com.android.systemui.media.controls.ui.view.MediaHostState
 import com.android.systemui.media.dagger.MediaModule.KEYGUARD
@@ -185,7 +185,7 @@
         refreshMediaPosition(reason = "onMediaHostVisibilityChanged")
 
         if (visible) {
-            if (migrateClocksToBlueprint() && useSplitShade) {
+            if (MigrateClocksToBlueprint.isEnabled && useSplitShade) {
                 return
             }
             mediaHost.hostView.layoutParams.apply {
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
index b721236..655e6a5 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
@@ -1163,7 +1163,7 @@
         // Only log media resume card when Smartspace data is available
         if (
             !mediaControlKey.isSsMediaRec &&
-                !mediaManager.smartspaceMediaData.isActive &&
+                !mediaManager.isRecommendationActive() &&
                 MediaPlayerData.smartspaceMediaData == null
         ) {
             return
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt
index f8c816c..2c25fe2 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt
@@ -161,7 +161,7 @@
         logger.log(event)
     }
 
-    fun logRecommendationAdded(packageName: String, instanceId: InstanceId) {
+    fun logRecommendationAdded(packageName: String, instanceId: InstanceId?) {
         logger.logWithInstanceId(
             MediaUiEvent.MEDIA_RECOMMENDATION_ADDED,
             0,
@@ -170,7 +170,7 @@
         )
     }
 
-    fun logRecommendationRemoved(packageName: String, instanceId: InstanceId) {
+    fun logRecommendationRemoved(packageName: String, instanceId: InstanceId?) {
         logger.logWithInstanceId(
             MediaUiEvent.MEDIA_RECOMMENDATION_REMOVED,
             0,
diff --git a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java
index d84e5dd..0fa3605 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java
@@ -19,6 +19,7 @@
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.log.LogBuffer;
 import com.android.systemui.log.LogBufferFactory;
+import com.android.systemui.media.controls.domain.MediaDomainModule;
 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager;
 import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager;
 import com.android.systemui.media.controls.ui.controller.MediaHostStatesManager;
@@ -38,7 +39,11 @@
 import javax.inject.Named;
 
 /** Dagger module for the media package. */
-@Module(subcomponents = {
+@Module(
+        includes = {
+            MediaDomainModule.class
+        },
+        subcomponents = {
         MediaComplicationComponent.class,
 })
 public interface MediaModule {
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
index dfe41eb..d49a513 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
@@ -243,7 +243,7 @@
                 Settings.Secure.getUriFor(Settings.Secure.ASSIST_LONG_PRESS_HOME_ENABLED),
                 false, mAssistContentObserver, UserHandle.USER_ALL);
         mContentResolver.registerContentObserver(
-                Settings.Secure.getUriFor(Secure.SEARCH_LONG_PRESS_HOME_ENABLED),
+                Settings.Secure.getUriFor(Secure.SEARCH_ALL_ENTRYPOINTS_ENABLED),
                 false, mAssistContentObserver, UserHandle.USER_ALL);
         mContentResolver.registerContentObserver(
                 Settings.Secure.getUriFor(Settings.Secure.ASSIST_TOUCH_GESTURE_ENABLED),
@@ -443,10 +443,10 @@
         boolean overrideLongPressHome = mAssistManagerLazy.get()
                 .shouldOverrideAssist(AssistManager.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS);
         boolean longPressDefault = mContext.getResources().getBoolean(overrideLongPressHome
-                ? com.android.internal.R.bool.config_searchLongPressHomeEnabledDefault
+                ? com.android.internal.R.bool.config_searchAllEntrypointsEnabledDefault
                 : com.android.internal.R.bool.config_assistLongPressHomeEnabledDefault);
         mLongPressHomeEnabled = Settings.Secure.getIntForUser(mContentResolver,
-                overrideLongPressHome ? Secure.SEARCH_LONG_PRESS_HOME_ENABLED
+                overrideLongPressHome ? Secure.SEARCH_ALL_ENTRYPOINTS_ENABLED
                         : Settings.Secure.ASSIST_LONG_PRESS_HOME_ENABLED, longPressDefault ? 1 : 0,
                 mUserTracker.getUserId()) != 0;
 
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
index 768bb8e..4fe3a11 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
@@ -934,48 +934,51 @@
 
     private void orientSecondaryHomeHandle() {
         if (!canShowSecondaryHandle()) {
-            if (mStartingQuickSwitchRotation == -1) {
-                resetSecondaryHandle();
-            }
             return;
         }
 
-        int deltaRotation = deltaRotation(mCurrentRotation, mStartingQuickSwitchRotation);
-        if (mStartingQuickSwitchRotation == -1 || deltaRotation == -1) {
-            // Curious if starting quickswitch can change between the if check and our delta
-            Log.d(TAG, "secondary nav delta rotation: " + deltaRotation
-                    + " current: " + mCurrentRotation
-                    + " starting: " + mStartingQuickSwitchRotation);
-        }
-        int height = 0;
-        int width = 0;
-        Rect dispSize = mWindowManager.getCurrentWindowMetrics().getBounds();
-        mOrientationHandle.setDeltaRotation(deltaRotation);
-        switch (deltaRotation) {
-            case Surface.ROTATION_90, Surface.ROTATION_270:
-                height = dispSize.height();
-                width = mView.getHeight();
-                break;
-            case Surface.ROTATION_180, Surface.ROTATION_0:
-                // TODO(b/152683657): Need to determine best UX for this
-                if (!mShowOrientedHandleForImmersiveMode) {
-                    resetSecondaryHandle();
-                    return;
-                }
-                width = dispSize.width();
-                height = mView.getHeight();
-                break;
-        }
+        if (mStartingQuickSwitchRotation == -1) {
+            resetSecondaryHandle();
+        } else {
+            int deltaRotation = deltaRotation(mCurrentRotation, mStartingQuickSwitchRotation);
+            if (mStartingQuickSwitchRotation == -1 || deltaRotation == -1) {
+                // Curious if starting quickswitch can change between the if check and our delta
+                Log.d(TAG, "secondary nav delta rotation: " + deltaRotation
+                        + " current: " + mCurrentRotation
+                        + " starting: " + mStartingQuickSwitchRotation);
+            }
+            int height = 0;
+            int width = 0;
+            Rect dispSize = mWindowManager.getCurrentWindowMetrics().getBounds();
+            mOrientationHandle.setDeltaRotation(deltaRotation);
+            switch (deltaRotation) {
+                case Surface.ROTATION_90:
+                case Surface.ROTATION_270:
+                    height = dispSize.height();
+                    width = mView.getHeight();
+                    break;
+                case Surface.ROTATION_180:
+                case Surface.ROTATION_0:
+                    // TODO(b/152683657): Need to determine best UX for this
+                    if (!mShowOrientedHandleForImmersiveMode) {
+                        resetSecondaryHandle();
+                        return;
+                    }
+                    width = dispSize.width();
+                    height = mView.getHeight();
+                    break;
+            }
 
-        mOrientationParams.gravity =
-                deltaRotation == Surface.ROTATION_0 ? Gravity.BOTTOM :
-                        (deltaRotation == Surface.ROTATION_90 ? Gravity.LEFT : Gravity.RIGHT);
-        mOrientationParams.height = height;
-        mOrientationParams.width = width;
-        mWindowManager.updateViewLayout(mOrientationHandle, mOrientationParams);
-        mView.setVisibility(View.GONE);
-        mOrientationHandle.setVisibility(View.VISIBLE);
-        logNavbarOrientation("orientSecondaryHomeHandle");
+            mOrientationParams.gravity =
+                    deltaRotation == Surface.ROTATION_0 ? Gravity.BOTTOM :
+                            (deltaRotation == Surface.ROTATION_90 ? Gravity.LEFT : Gravity.RIGHT);
+            mOrientationParams.height = height;
+            mOrientationParams.width = width;
+            mWindowManager.updateViewLayout(mOrientationHandle, mOrientationParams);
+            mView.setVisibility(View.GONE);
+            mOrientationHandle.setVisibility(View.VISIBLE);
+            logNavbarOrientation("orientSecondaryHomeHandle");
+        }
     }
 
     private void resetSecondaryHandle() {
@@ -1789,8 +1792,7 @@
     }
 
     private boolean canShowSecondaryHandle() {
-        return mNavBarMode == NAV_BAR_MODE_GESTURAL && mOrientationHandle != null
-                && mStartingQuickSwitchRotation != -1;
+        return mNavBarMode == NAV_BAR_MODE_GESTURAL && mOrientationHandle != null;
     }
 
     private final UserTracker.Callback mUserChangedCallback =
diff --git a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
index 5d2aeef..b34b370 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
@@ -432,6 +432,9 @@
         for (int i = 0; i < NP; i++) {
             mPages.get(i).removeAllViews();
         }
+        if (mPageIndicator != null) {
+            mPageIndicator.setNumPages(numPages);
+        }
         if (NP == numPages) {
             return;
         }
@@ -443,7 +446,6 @@
             mLogger.d("Removing page");
             mPages.remove(mPages.size() - 1);
         }
-        mPageIndicator.setNumPages(mPages.size());
         setAdapter(mAdapter);
         mAdapter.notifyDataSetChanged();
         if (mPageToRestore != NO_PAGE) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index 7a7ee59..00757b7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -127,8 +127,9 @@
 
     }
 
-    void initialize(QSLogger qsLogger) {
+    void initialize(QSLogger qsLogger, boolean usingMediaPlayer) {
         mQsLogger = qsLogger;
+        mUsingMediaPlayer = usingMediaPlayer;
         mTileLayout = getOrCreateTileLayout();
 
         if (mUsingMediaPlayer) {
@@ -163,22 +164,25 @@
     }
 
     protected void setHorizontalContentContainerClipping() {
-        mHorizontalContentContainer.setClipChildren(true);
-        mHorizontalContentContainer.setClipToPadding(false);
-        // Don't clip on the top, that way, secondary pages tiles can animate up
-        // Clipping coordinates should be relative to this view, not absolute (parent coordinates)
-        mHorizontalContentContainer.addOnLayoutChangeListener(
-                (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
-                    if ((right - left) != (oldRight - oldLeft)
-                            || ((bottom - top) != (oldBottom - oldTop))) {
-                        mClippingRect.right = right - left;
-                        mClippingRect.bottom = bottom - top;
-                        mHorizontalContentContainer.setClipBounds(mClippingRect);
-                    }
-                });
-        mClippingRect.left = 0;
-        mClippingRect.top = -1000;
-        mHorizontalContentContainer.setClipBounds(mClippingRect);
+        if (mHorizontalContentContainer != null) {
+            mHorizontalContentContainer.setClipChildren(true);
+            mHorizontalContentContainer.setClipToPadding(false);
+            // Don't clip on the top, that way, secondary pages tiles can animate up
+            // Clipping coordinates should be relative to this view, not absolute
+            // (parent coordinates)
+            mHorizontalContentContainer.addOnLayoutChangeListener(
+                    (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
+                        if ((right - left) != (oldRight - oldLeft)
+                                || ((bottom - top) != (oldBottom - oldTop))) {
+                            mClippingRect.right = right - left;
+                            mClippingRect.bottom = bottom - top;
+                            mHorizontalContentContainer.setClipBounds(mClippingRect);
+                        }
+                    });
+            mClippingRect.left = 0;
+            mClippingRect.top = -1000;
+            mHorizontalContentContainer.setClipBounds(mClippingRect);
+        }
     }
 
     /**
@@ -412,7 +416,7 @@
     }
 
     private void updateHorizontalLinearLayoutMargins() {
-        if (mHorizontalLinearLayout != null && !displayMediaMarginsOnMedia()) {
+        if (mUsingMediaPlayer && mHorizontalLinearLayout != null && !displayMediaMarginsOnMedia()) {
             LayoutParams lp = (LayoutParams) mHorizontalLinearLayout.getLayoutParams();
             lp.bottomMargin = Math.max(mMediaTotalBottomMargin - getPaddingBottom(), 0);
             mHorizontalLinearLayout.setLayoutParams(lp);
@@ -461,6 +465,11 @@
     /** Call when orientation has changed and MediaHost needs to be adjusted. */
     private void reAttachMediaHost(ViewGroup hostView, boolean horizontal) {
         if (!mUsingMediaPlayer) {
+            // If the host view was attached, detach it.
+            ViewGroup parent = (ViewGroup) hostView.getParent();
+            if (parent != null) {
+                parent.removeView(hostView);
+            }
             return;
         }
         mMediaHostView = hostView;
@@ -492,8 +501,10 @@
     public void setExpanded(boolean expanded) {
         if (mExpanded == expanded) return;
         mExpanded = expanded;
-        if (!mExpanded && mTileLayout instanceof PagedTileLayout) {
-            ((PagedTileLayout) mTileLayout).setCurrentItem(0, false);
+        if (!mExpanded && mTileLayout instanceof PagedTileLayout tilesLayout) {
+            // Use post, so it will wait until the view is attached. If the view is not attached,
+            // it will not populate corresponding views (and will not do it later when attached).
+            tilesLayout.post(() -> tilesLayout.setCurrentItem(0, false));
         }
     }
 
@@ -616,7 +627,10 @@
         if (horizontal != mUsingHorizontalLayout || force) {
             Log.d(getDumpableTag(), "setUsingHorizontalLayout: " + horizontal + ", " + force);
             mUsingHorizontalLayout = horizontal;
-            ViewGroup newParent = horizontal ? mHorizontalContentContainer : this;
+            // The tile layout should be reparented if horizontal and we are using media. If not
+            // using media, the parent should always be this.
+            ViewGroup newParent =
+                    horizontal && mUsingMediaPlayer ? mHorizontalContentContainer : this;
             switchAllContentToParent(newParent, mTileLayout);
             reAttachMediaHost(mediaHostView, horizontal);
             if (needsDynamicRowsAndColumns()) {
@@ -624,7 +638,9 @@
                 mTileLayout.setMaxColumns(horizontal ? 2 : 4);
             }
             updateMargins(mediaHostView);
-            mHorizontalLinearLayout.setVisibility(horizontal ? View.VISIBLE : View.GONE);
+            if (mHorizontalLinearLayout != null) {
+                mHorizontalLinearLayout.setVisibility(horizontal ? View.VISIBLE : View.GONE);
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
index 5e12b9d..d8e8187 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
@@ -167,7 +167,7 @@
 
     @Override
     protected void onInit() {
-        mView.initialize(mQSLogger);
+        mView.initialize(mQSLogger, mUsingMediaPlayer);
         mQSLogger.logAllTilesChangeListening(mView.isListening(), mView.getDumpableTag(), "");
         mHost.addCallback(mQSHostCallback);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt
index d82b175..b418a17 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt
@@ -44,6 +44,7 @@
 import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor
 import com.android.systemui.qs.tileimpl.QSTileImpl
 import com.android.systemui.recordissue.IssueRecordingService
+import com.android.systemui.recordissue.IssueRecordingState
 import com.android.systemui.recordissue.RecordIssueDialogDelegate
 import com.android.systemui.res.R
 import com.android.systemui.screenrecord.RecordingService
@@ -69,6 +70,7 @@
     private val dialogTransitionAnimator: DialogTransitionAnimator,
     private val panelInteractor: PanelInteractor,
     private val userContextProvider: UserContextProvider,
+    private val issueRecordingState: IssueRecordingState,
     private val delegateFactory: RecordIssueDialogDelegate.Factory,
 ) :
     QSTileImpl<QSTile.BooleanState>(
@@ -83,7 +85,16 @@
         qsLogger
     ) {
 
-    @VisibleForTesting var isRecording: Boolean = false
+    private val onRecordingChangeListener = Runnable { refreshState() }
+
+    override fun handleSetListening(listening: Boolean) {
+        super.handleSetListening(listening)
+        if (listening) {
+            issueRecordingState.addListener(onRecordingChangeListener)
+        } else {
+            issueRecordingState.removeListener(onRecordingChangeListener)
+        }
+    }
 
     override fun getTileLabel(): CharSequence = mContext.getString(R.string.qs_record_issue_label)
 
@@ -103,13 +114,11 @@
 
     @VisibleForTesting
     public override fun handleClick(view: View?) {
-        if (isRecording) {
-            isRecording = false
+        if (issueRecordingState.isRecording) {
             stopIssueRecordingService()
         } else {
             mUiHandler.post { showPrompt(view) }
         }
-        refreshState()
     }
 
     private fun startIssueRecordingService(screenRecord: Boolean, winscopeTracing: Boolean) =
@@ -138,11 +147,9 @@
         val dialog: AlertDialog =
             delegateFactory
                 .create {
-                    isRecording = true
                     startIssueRecordingService(it.screenRecord, it.winscopeTracing)
                     dialogTransitionAnimator.disableAllCurrentDialogsExitAnimations()
                     panelInteractor.collapsePanels()
-                    refreshState()
                 }
                 .createDialog()
         val dismissAction =
@@ -168,7 +175,7 @@
     @VisibleForTesting
     public override fun handleUpdateState(qsTileState: QSTile.BooleanState, arg: Any?) {
         qsTileState.apply {
-            if (isRecording) {
+            if (issueRecordingState.isRecording) {
                 value = true
                 state = Tile.STATE_ACTIVE
                 forceExpandIcon = false
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapper.kt
index 2b8c335..c0fc52e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapper.kt
@@ -83,6 +83,7 @@
                 }
             }
 
+            sideViewIcon = QSTileState.SideViewIcon.Chevron
             contentDescription = label
             supportedActions = setOf(QSTileState.UserAction.CLICK)
         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegate.kt
index fc42ba4..b25c61c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegate.kt
@@ -39,7 +39,7 @@
         return sysuiDialogFactory.create(this, context)
     }
 
-    override fun onCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
+    override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
         with(dialog) {
             setTitle(R.string.data_saver_enable_title)
             setMessage(R.string.data_saver_description)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileDataInteractor.kt
new file mode 100644
index 0000000..a2a9e87a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileDataInteractor.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.qs.tiles.impl.work.domain.interactor
+
+import android.os.UserHandle
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
+import com.android.systemui.qs.tiles.impl.work.domain.model.WorkModeTileModel
+import com.android.systemui.statusbar.phone.ManagedProfileController
+import com.android.systemui.util.kotlin.hasActiveWorkProfile
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+/** Observes data saver state changes providing the [WorkModeTileModel]. */
+class WorkModeTileDataInteractor
+@Inject
+constructor(
+    private val profileController: ManagedProfileController,
+) : QSTileDataInteractor<WorkModeTileModel> {
+    override fun tileData(
+        user: UserHandle,
+        triggers: Flow<DataUpdateTrigger>
+    ): Flow<WorkModeTileModel> =
+        profileController.hasActiveWorkProfile.map { hasActiveWorkProfile: Boolean ->
+            if (hasActiveWorkProfile) {
+                WorkModeTileModel.HasActiveProfile(profileController.isWorkModeEnabled)
+            } else {
+                WorkModeTileModel.NoActiveProfile
+            }
+        }
+
+    override fun availability(user: UserHandle): Flow<Boolean> =
+        profileController.hasActiveWorkProfile
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileUserActionInteractor.kt
new file mode 100644
index 0000000..f765f8b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileUserActionInteractor.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.qs.tiles.impl.work.domain.interactor
+
+import android.content.Intent
+import android.provider.Settings
+import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.base.interactor.QSTileInput
+import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.work.domain.model.WorkModeTileModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
+import com.android.systemui.statusbar.phone.ManagedProfileController
+import javax.inject.Inject
+
+/** Handles airplane mode tile clicks and long clicks. */
+class WorkModeTileUserActionInteractor
+@Inject
+constructor(
+    private val profileController: ManagedProfileController,
+    private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler,
+) : QSTileUserActionInteractor<WorkModeTileModel> {
+    override suspend fun handleInput(input: QSTileInput<WorkModeTileModel>) =
+        with(input) {
+            when (action) {
+                is QSTileUserAction.Click -> {
+                    if (data is WorkModeTileModel.HasActiveProfile) {
+                        profileController.setWorkModeEnabled(!data.isEnabled)
+                    }
+                }
+                is QSTileUserAction.LongClick -> {
+                    if (data is WorkModeTileModel.HasActiveProfile) {
+                        qsTileIntentUserActionHandler.handle(
+                            action.view,
+                            Intent(Settings.ACTION_MANAGED_PROFILE_SETTINGS)
+                        )
+                    }
+                }
+            }
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/model/WorkModeTileModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/model/WorkModeTileModel.kt
new file mode 100644
index 0000000..ae8382d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/model/WorkModeTileModel.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.qs.tiles.impl.work.domain.model
+
+/** Work mode tile model. */
+sealed interface WorkModeTileModel {
+    /** @param isEnabled is true when the work mode is enabled */
+    data class HasActiveProfile(val isEnabled: Boolean) : WorkModeTileModel
+    data object NoActiveProfile : WorkModeTileModel
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapper.kt
new file mode 100644
index 0000000..55445bb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapper.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.work.ui
+
+import android.app.admin.DevicePolicyManager
+import android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_WORK_PROFILE_LABEL
+import android.content.res.Resources
+import android.service.quicksettings.Tile
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
+import com.android.systemui.qs.tiles.impl.work.domain.model.WorkModeTileModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.res.R
+import javax.inject.Inject
+
+/** Maps [WorkModeTileModel] to [QSTileState]. */
+class WorkModeTileMapper
+@Inject
+constructor(
+    @Main private val resources: Resources,
+    private val theme: Resources.Theme,
+    private val devicePolicyManager: DevicePolicyManager,
+) : QSTileDataToStateMapper<WorkModeTileModel> {
+    override fun map(config: QSTileConfig, data: WorkModeTileModel): QSTileState =
+        QSTileState.build(resources, theme, config.uiConfig) {
+            label = getTileLabel()!!
+            contentDescription = label
+
+            icon = {
+                Icon.Loaded(
+                    resources.getDrawable(
+                        com.android.internal.R.drawable.stat_sys_managed_profile_status,
+                        theme
+                    ),
+                    contentDescription = null
+                )
+            }
+
+            when (data) {
+                is WorkModeTileModel.HasActiveProfile -> {
+                    if (data.isEnabled) {
+                        activationState = QSTileState.ActivationState.ACTIVE
+                        secondaryLabel = ""
+                    } else {
+                        activationState = QSTileState.ActivationState.INACTIVE
+                        secondaryLabel =
+                            resources.getString(R.string.quick_settings_work_mode_paused_state)
+                    }
+                    supportedActions =
+                        setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK)
+                }
+                is WorkModeTileModel.NoActiveProfile -> {
+                    activationState = QSTileState.ActivationState.UNAVAILABLE
+                    secondaryLabel =
+                        resources.getStringArray(R.array.tile_states_work)[Tile.STATE_UNAVAILABLE]
+                    supportedActions = setOf()
+                }
+            }
+
+            sideViewIcon = QSTileState.SideViewIcon.None
+        }
+
+    private fun getTileLabel(): CharSequence? {
+        return devicePolicyManager.resources.getString(QS_WORK_PROFILE_LABEL) {
+            resources.getString(R.string.quick_settings_work_mode_label)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
index c1b2037..6710504 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
@@ -23,16 +23,21 @@
 import androidx.annotation.VisibleForTesting
 import androidx.asynclayoutinflater.view.AsyncLayoutInflater
 import com.android.settingslib.applications.InterestingConfigChanges
+import com.android.systemui.Dumpable
 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.qs.QSContainerController
 import com.android.systemui.qs.QSContainerImpl
 import com.android.systemui.qs.QSImpl
 import com.android.systemui.qs.dagger.QSSceneComponent
 import com.android.systemui.res.R
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.util.kotlin.sample
+import java.io.PrintWriter
 import javax.inject.Inject
 import javax.inject.Provider
 import kotlin.coroutines.resume
@@ -107,11 +112,17 @@
         }
 
         /** State for appearing QQS from Lockscreen or Gone */
-        data class Unsquishing(override val squishiness: Float) : State {
+        data class UnsquishingQQS(override val squishiness: Float) : State {
             override val isVisible = true
             override val expansion = 0f
         }
 
+        /** State for appearing QS from Lockscreen or Gone, used in Split shade */
+        data class UnsquishingQS(override val squishiness: Float) : State {
+            override val isVisible = true
+            override val expansion = 1f
+        }
+
         companion object {
             // These are special cases of the expansion.
             val QQS = Expanding(0f)
@@ -129,22 +140,28 @@
 constructor(
     private val qsSceneComponentFactory: QSSceneComponent.Factory,
     private val qsImplProvider: Provider<QSImpl>,
+    shadeInteractor: ShadeInteractor,
+    dumpManager: DumpManager,
     @Main private val mainDispatcher: CoroutineDispatcher,
     @Application applicationScope: CoroutineScope,
     private val configurationInteractor: ConfigurationInteractor,
     private val asyncLayoutInflaterFactory: (Context) -> AsyncLayoutInflater,
-) : QSContainerController, QSSceneAdapter {
+) : QSContainerController, QSSceneAdapter, Dumpable {
 
     @Inject
     constructor(
         qsSceneComponentFactory: QSSceneComponent.Factory,
         qsImplProvider: Provider<QSImpl>,
+        shadeInteractor: ShadeInteractor,
+        dumpManager: DumpManager,
         @Main dispatcher: CoroutineDispatcher,
         @Application scope: CoroutineScope,
         configurationInteractor: ConfigurationInteractor,
     ) : this(
         qsSceneComponentFactory,
         qsImplProvider,
+        shadeInteractor,
+        dumpManager,
         dispatcher,
         scope,
         configurationInteractor,
@@ -182,6 +199,7 @@
         )
 
     init {
+        dumpManager.registerDumpable(this)
         applicationScope.launch {
             launch {
                 state.sample(_isCustomizing, ::Pair).collect { (state, customizing) ->
@@ -210,6 +228,11 @@
                     it.second.applyBottomNavBarToCustomizerPadding(it.first)
                 }
             }
+            launch {
+                shadeInteractor.shadeMode.collect {
+                    qsImpl.value?.setInSplitShade(it == ShadeMode.Split)
+                }
+            }
         }
     }
 
@@ -256,9 +279,17 @@
 
     private fun QSImpl.applyState(state: QSSceneAdapter.State) {
         setQsVisible(state.isVisible)
-        setExpanded(state.isVisible)
+        setExpanded(state.isVisible && state.expansion > 0f)
         setListening(state.isVisible)
         setQsExpansion(state.expansion, 1f, 0f, state.squishiness)
-        setTransitionToFullShadeProgress(false, 1f, state.squishiness)
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        pw.apply {
+            println("Last state: ${state.value}")
+            println("Customizing: ${isCustomizing.value}")
+            println("QQS height: $qqsHeight")
+            println("QS height: $qsHeight")
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
index 34f66b8..c695d4c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
@@ -48,6 +48,8 @@
         qsSceneAdapter.isCustomizing.map { customizing ->
             if (customizing) {
                 mapOf<UserAction, UserActionResult>(Back to UserActionResult(Scenes.QuickSettings))
+                // TODO(b/330200163) Add an Up from Bottom to be able to collapse the shade
+                // while customizing
             } else {
                 mapOf(
                     Back to UserActionResult(Scenes.Shade),
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt
index 7009816..5e4919d 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt
@@ -59,6 +59,7 @@
     keyguardDismissUtil: KeyguardDismissUtil,
     private val dialogTransitionAnimator: DialogTransitionAnimator,
     private val panelInteractor: PanelInteractor,
+    private val issueRecordingState: IssueRecordingState,
 ) :
     RecordingService(
         controller,
@@ -90,6 +91,7 @@
                     DEFAULT_MAX_TRACE_SIZE,
                     DEFAULT_MAX_TRACE_DURATION_IN_MINUTES
                 )
+                issueRecordingState.isRecording = true
                 if (!intent.getBooleanExtra(EXTRA_SCREEN_RECORD, false)) {
                     // If we don't want to record the screen, the ACTION_SHOW_START_NOTIF action
                     // will circumvent the RecordingService's screen recording start code.
@@ -103,6 +105,7 @@
                 // this line should be removed.
                 getSystemService(LauncherApps::class.java)?.saveViewCaptureData()
                 TraceUtils.traceStop(contentResolver)
+                issueRecordingState.isRecording = false
             }
             ACTION_SHARE -> {
                 shareRecording(intent)
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt
new file mode 100644
index 0000000..394c5c2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.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.recordissue
+
+import com.android.systemui.dagger.SysUISingleton
+import java.util.concurrent.CopyOnWriteArrayList
+import javax.inject.Inject
+
+@SysUISingleton
+class IssueRecordingState @Inject constructor() {
+
+    private val listeners = CopyOnWriteArrayList<Runnable>()
+
+    var isRecording = false
+        set(value) {
+            field = value
+            listeners.forEach(Runnable::run)
+        }
+
+    fun addListener(listener: Runnable) {
+        listeners.add(listener)
+    }
+
+    fun removeListener(listener: Runnable) {
+        listeners.remove(listener)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt
index 7313a49..832fc3f 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.recordissue
 
 import android.annotation.SuppressLint
+import android.app.AlertDialog
 import android.content.Context
 import android.content.res.ColorStateList
 import android.graphics.Color
@@ -74,7 +75,6 @@
 
     @SuppressLint("UseSwitchCompatOrMaterialCode") private lateinit var screenRecordSwitch: Switch
     private lateinit var issueTypeButton: Button
-    private var hasSelectedIssueType: Boolean = false
 
     @MainThread
     override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
@@ -86,15 +86,13 @@
             setPositiveButton(
                 R.string.qs_record_issue_start,
                 { _, _ ->
-                    if (hasSelectedIssueType) {
-                        onStarted.accept(
-                            IssueRecordingConfig(
-                                screenRecordSwitch.isChecked,
-                                true /* TODO: Base this on issueType selected */
-                            )
+                    onStarted.accept(
+                        IssueRecordingConfig(
+                            screenRecordSwitch.isChecked,
+                            true /* TODO: Base this on issueType selected */
                         )
-                        dismiss()
-                    }
+                    )
+                    dismiss()
                 },
                 false
             )
@@ -115,8 +113,12 @@
                     bgExecutor.execute { onScreenRecordSwitchClicked() }
                 }
             }
+            val startButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
             issueTypeButton = requireViewById(R.id.issue_type_button)
-            issueTypeButton.setOnClickListener { onIssueTypeClicked(context) }
+            issueTypeButton.setOnClickListener {
+                onIssueTypeClicked(context) { startButton.isEnabled = true }
+            }
+            startButton.isEnabled = false
         }
     }
 
@@ -159,7 +161,7 @@
     }
 
     @MainThread
-    private fun onIssueTypeClicked(context: Context) {
+    private fun onIssueTypeClicked(context: Context, onIssueTypeSelected: Runnable) {
         val selectedCategory = issueTypeButton.text.toString()
         val popupMenu = PopupMenu(context, issueTypeButton)
 
@@ -174,11 +176,11 @@
         popupMenu.apply {
             setOnMenuItemClickListener {
                 issueTypeButton.text = it.title
+                onIssueTypeSelected.run()
                 true
             }
             setForceShowIcon(true)
             show()
         }
-        hasSelectedIssueType = true
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt
index 467089d..54ec398 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt
@@ -18,18 +18,15 @@
 
 package com.android.systemui.scene.shared.flag
 
-import com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR
-import com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR
-import com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT
 import com.android.systemui.Flags.FLAG_SCENE_CONTAINER
-import com.android.systemui.Flags.keyguardBottomAreaRefactor
-import com.android.systemui.Flags.keyguardWmStateRefactor
-import com.android.systemui.Flags.migrateClocksToBlueprint
 import com.android.systemui.Flags.sceneContainer
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.flags.FlagToken
 import com.android.systemui.flags.Flags.SCENE_CONTAINER_ENABLED
 import com.android.systemui.flags.RefactorFlagUtils
+import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
+import com.android.systemui.keyguard.KeyguardWmStateRefactor
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.keyguard.shared.ComposeLockscreen
 import com.android.systemui.media.controls.util.MediaInSceneContainerFlag
 import dagger.Module
@@ -45,11 +42,11 @@
         get() =
             SCENE_CONTAINER_ENABLED && // mainStaticFlag
             sceneContainer() && // mainAconfigFlag
-                keyguardBottomAreaRefactor() &&
-                migrateClocksToBlueprint() &&
+                KeyguardBottomAreaRefactor.isEnabled &&
+                MigrateClocksToBlueprint.isEnabled &&
                 ComposeLockscreen.isEnabled &&
                 MediaInSceneContainerFlag.isEnabled &&
-                keyguardWmStateRefactor()
+                KeyguardWmStateRefactor.isEnabled
     // NOTE: Changes should also be made in getSecondaryFlags and @EnableSceneContainer
 
     /**
@@ -66,9 +63,9 @@
     /** The set of secondary flags which must be enabled for scene container to work properly */
     inline fun getSecondaryFlags(): Sequence<FlagToken> =
         sequenceOf(
-            FlagToken(FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, keyguardBottomAreaRefactor()),
-            FlagToken(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, migrateClocksToBlueprint()),
-            FlagToken(FLAG_KEYGUARD_WM_STATE_REFACTOR, keyguardWmStateRefactor()),
+            KeyguardBottomAreaRefactor.token,
+            MigrateClocksToBlueprint.token,
+            KeyguardWmStateRefactor.token,
             ComposeLockscreen.token,
             MediaInSceneContainerFlag.token,
             // NOTE: Changes should also be made in isEnabled and @EnableSceneContainer
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt
new file mode 100644
index 0000000..abdbd68
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot
+
+import android.content.Context
+import android.content.Intent
+import android.graphics.drawable.Drawable
+import android.net.Uri
+import android.os.UserHandle
+import androidx.appcompat.content.res.AppCompatResources
+import com.android.systemui.res.R
+import javax.inject.Inject
+
+/**
+ * Provides static actions for screenshots. This class can be overridden by a vendor-specific SysUI
+ * implementation.
+ */
+interface ScreenshotActionsProvider {
+    data class ScreenshotAction(
+        val icon: Drawable?,
+        val text: String?,
+        val overrideTransition: Boolean,
+        val retrieveIntent: (Uri) -> Intent
+    )
+
+    fun getPreviewAction(context: Context, uri: Uri, user: UserHandle): Intent
+    fun getActions(context: Context, user: UserHandle): List<ScreenshotAction>
+}
+
+class DefaultScreenshotActionsProvider @Inject constructor() : ScreenshotActionsProvider {
+    override fun getPreviewAction(context: Context, uri: Uri, user: UserHandle): Intent {
+        return ActionIntentCreator.createEdit(uri, context)
+    }
+
+    override fun getActions(
+        context: Context,
+        user: UserHandle
+    ): List<ScreenshotActionsProvider.ScreenshotAction> {
+        val editAction =
+            ScreenshotActionsProvider.ScreenshotAction(
+                AppCompatResources.getDrawable(context, R.drawable.ic_screenshot_edit),
+                context.resources.getString(R.string.screenshot_edit_label),
+                true
+            ) { uri ->
+                ActionIntentCreator.createEdit(uri, context)
+            }
+        val shareAction =
+            ScreenshotActionsProvider.ScreenshotAction(
+                AppCompatResources.getDrawable(context, R.drawable.ic_screenshot_share),
+                context.resources.getString(R.string.screenshot_share_label),
+                false
+            ) { uri ->
+                ActionIntentCreator.createShare(uri)
+            }
+        return listOf(editAction, shareAction)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt
new file mode 100644
index 0000000..9354fd2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt
@@ -0,0 +1,226 @@
+/*
+ * 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.screenshot
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.app.Notification
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Rect
+import android.view.KeyEvent
+import android.view.LayoutInflater
+import android.view.ScrollCaptureResponse
+import android.view.View
+import android.view.ViewTreeObserver
+import android.view.WindowInsets
+import android.window.OnBackInvokedCallback
+import android.window.OnBackInvokedDispatcher
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.log.DebugLogger.debugLog
+import com.android.systemui.res.R
+import com.android.systemui.screenshot.LogConfig.DEBUG_ACTIONS
+import com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS
+import com.android.systemui.screenshot.LogConfig.DEBUG_INPUT
+import com.android.systemui.screenshot.LogConfig.DEBUG_WINDOW
+import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER
+import com.android.systemui.screenshot.scroll.ScrollCaptureController
+import com.android.systemui.screenshot.ui.ScreenshotAnimationController
+import com.android.systemui.screenshot.ui.ScreenshotShelfView
+import com.android.systemui.screenshot.ui.binder.ScreenshotShelfViewBinder
+import com.android.systemui.screenshot.ui.viewmodel.ActionButtonViewModel
+import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+/** Controls the screenshot view and viewModel. */
+class ScreenshotShelfViewProxy
+@AssistedInject
+constructor(
+    private val logger: UiEventLogger,
+    private val viewModel: ScreenshotViewModel,
+    private val staticActionsProvider: ScreenshotActionsProvider,
+    @Assisted private val context: Context,
+    @Assisted private val displayId: Int
+) : ScreenshotViewProxy {
+    override val view: ScreenshotShelfView =
+        LayoutInflater.from(context).inflate(R.layout.screenshot_shelf, null) as ScreenshotShelfView
+    override val screenshotPreview: View
+    override var packageName: String = ""
+    override var callbacks: ScreenshotView.ScreenshotViewCallback? = null
+    override var screenshot: ScreenshotData? = null
+        set(value) {
+            viewModel.setScreenshotBitmap(value?.bitmap)
+            field = value
+        }
+
+    override val isAttachedToWindow
+        get() = view.isAttachedToWindow
+    override var isDismissing = false
+    override var isPendingSharedTransition = false
+
+    private val animationController = ScreenshotAnimationController(view)
+
+    init {
+        ScreenshotShelfViewBinder.bind(view, viewModel, LayoutInflater.from(context))
+        addPredictiveBackListener { requestDismissal(SCREENSHOT_DISMISSED_OTHER) }
+        setOnKeyListener { requestDismissal(SCREENSHOT_DISMISSED_OTHER) }
+        debugLog(DEBUG_WINDOW) { "adding OnComputeInternalInsetsListener" }
+        screenshotPreview = view.screenshotPreview
+    }
+
+    override fun reset() {
+        animationController.cancel()
+        isPendingSharedTransition = false
+        viewModel.setScreenshotBitmap(null)
+        viewModel.setActions(listOf())
+    }
+    override fun updateInsets(insets: WindowInsets) {}
+    override fun updateOrientation(insets: WindowInsets) {}
+
+    override fun createScreenshotDropInAnimation(screenRect: Rect, showFlash: Boolean): Animator {
+        return animationController.getEntranceAnimation()
+    }
+
+    override fun addQuickShareChip(quickShareAction: Notification.Action) {}
+
+    override fun setChipIntents(imageData: ScreenshotController.SavedImageData) {
+        val staticActions =
+            staticActionsProvider.getActions(context, imageData.owner).map {
+                ActionButtonViewModel(it.icon, it.text) {
+                    val intent = it.retrieveIntent(imageData.uri)
+                    debugLog(DEBUG_ACTIONS) { "Action tapped: $intent" }
+                    isPendingSharedTransition = true
+                    callbacks?.onAction(intent, imageData.owner, it.overrideTransition)
+                }
+            }
+
+        viewModel.setActions(staticActions)
+    }
+
+    override fun requestDismissal(event: ScreenshotEvent) {
+        debugLog(DEBUG_DISMISS) { "screenshot dismissal requested: $event" }
+
+        // If we're already animating out, don't restart the animation
+        if (isDismissing) {
+            debugLog(DEBUG_DISMISS) { "Already dismissing, ignoring duplicate command $event" }
+            return
+        }
+        logger.log(event, 0, packageName)
+        val animator = animationController.getExitAnimation()
+        animator.addListener(
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationStart(animator: Animator) {
+                    isDismissing = true
+                }
+                override fun onAnimationEnd(animator: Animator) {
+                    isDismissing = false
+                    callbacks?.onDismiss()
+                }
+            }
+        )
+        animator.start()
+    }
+
+    override fun showScrollChip(packageName: String, onClick: Runnable) {}
+
+    override fun hideScrollChip() {}
+
+    override fun prepareScrollingTransition(
+        response: ScrollCaptureResponse,
+        screenBitmap: Bitmap,
+        newScreenshot: Bitmap,
+        screenshotTakenInPortrait: Boolean,
+        onTransitionPrepared: Runnable,
+    ) {}
+
+    override fun startLongScreenshotTransition(
+        transitionDestination: Rect,
+        onTransitionEnd: Runnable,
+        longScreenshot: ScrollCaptureController.LongScreenshot
+    ) {}
+
+    override fun restoreNonScrollingUi() {}
+
+    override fun stopInputListening() {}
+
+    override fun requestFocus() {
+        view.requestFocus()
+    }
+
+    override fun announceForAccessibility(string: String) = view.announceForAccessibility(string)
+
+    override fun prepareEntranceAnimation(runnable: Runnable) {
+        view.viewTreeObserver.addOnPreDrawListener(
+            object : ViewTreeObserver.OnPreDrawListener {
+                override fun onPreDraw(): Boolean {
+                    debugLog(DEBUG_WINDOW) { "onPreDraw: startAnimation" }
+                    view.viewTreeObserver.removeOnPreDrawListener(this)
+                    runnable.run()
+                    return true
+                }
+            }
+        )
+    }
+
+    private fun addPredictiveBackListener(onDismissRequested: (ScreenshotEvent) -> Unit) {
+        val onBackInvokedCallback = OnBackInvokedCallback {
+            debugLog(DEBUG_INPUT) { "Predictive Back callback dispatched" }
+            onDismissRequested.invoke(SCREENSHOT_DISMISSED_OTHER)
+        }
+        view.addOnAttachStateChangeListener(
+            object : View.OnAttachStateChangeListener {
+                override fun onViewAttachedToWindow(v: View) {
+                    debugLog(DEBUG_INPUT) { "Registering Predictive Back callback" }
+                    view
+                        .findOnBackInvokedDispatcher()
+                        ?.registerOnBackInvokedCallback(
+                            OnBackInvokedDispatcher.PRIORITY_DEFAULT,
+                            onBackInvokedCallback
+                        )
+                }
+
+                override fun onViewDetachedFromWindow(view: View) {
+                    debugLog(DEBUG_INPUT) { "Unregistering Predictive Back callback" }
+                    view
+                        .findOnBackInvokedDispatcher()
+                        ?.unregisterOnBackInvokedCallback(onBackInvokedCallback)
+                }
+            }
+        )
+    }
+    private fun setOnKeyListener(onDismissRequested: (ScreenshotEvent) -> Unit) {
+        view.setOnKeyListener(
+            object : View.OnKeyListener {
+                override fun onKey(view: View, keyCode: Int, event: KeyEvent): Boolean {
+                    if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) {
+                        debugLog(DEBUG_INPUT) { "onKeyEvent: $keyCode" }
+                        onDismissRequested.invoke(SCREENSHOT_DISMISSED_OTHER)
+                        return true
+                    }
+                    return false
+                }
+            }
+        )
+    }
+
+    @AssistedFactory
+    interface Factory : ScreenshotViewProxy.Factory {
+        override fun getProxy(context: Context, displayId: Int): ScreenshotShelfViewProxy
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
index cdb9abb..9118ee1 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
@@ -16,16 +16,23 @@
 
 package com.android.systemui.screenshot.dagger;
 
-import android.app.Service;
+import static com.android.systemui.Flags.screenshotShelfUi;
 
+import android.app.Service;
+import android.view.accessibility.AccessibilityManager;
+
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.screenshot.DefaultScreenshotActionsProvider;
 import com.android.systemui.screenshot.ImageCapture;
 import com.android.systemui.screenshot.ImageCaptureImpl;
 import com.android.systemui.screenshot.LegacyScreenshotViewProxy;
 import com.android.systemui.screenshot.RequestProcessor;
+import com.android.systemui.screenshot.ScreenshotActionsProvider;
 import com.android.systemui.screenshot.ScreenshotPolicy;
 import com.android.systemui.screenshot.ScreenshotPolicyImpl;
 import com.android.systemui.screenshot.ScreenshotProxyService;
 import com.android.systemui.screenshot.ScreenshotRequestProcessor;
+import com.android.systemui.screenshot.ScreenshotShelfViewProxy;
 import com.android.systemui.screenshot.ScreenshotSoundController;
 import com.android.systemui.screenshot.ScreenshotSoundControllerImpl;
 import com.android.systemui.screenshot.ScreenshotSoundProvider;
@@ -34,6 +41,7 @@
 import com.android.systemui.screenshot.TakeScreenshotService;
 import com.android.systemui.screenshot.appclips.AppClipsScreenshotHelperService;
 import com.android.systemui.screenshot.appclips.AppClipsService;
+import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel;
 
 import dagger.Binds;
 import dagger.Module;
@@ -85,9 +93,25 @@
     abstract ScreenshotSoundController bindScreenshotSoundController(
             ScreenshotSoundControllerImpl screenshotSoundProviderImpl);
 
+    @Binds
+    abstract ScreenshotActionsProvider bindScreenshotActionsProvider(
+            DefaultScreenshotActionsProvider defaultScreenshotActionsProvider);
+
+    @Provides
+    @SysUISingleton
+    static ScreenshotViewModel providesScreenshotViewModel(
+            AccessibilityManager accessibilityManager) {
+        return new ScreenshotViewModel(accessibilityManager);
+    }
+
     @Provides
     static ScreenshotViewProxy.Factory providesScreenshotViewProxyFactory(
+            ScreenshotShelfViewProxy.Factory shelfScreenshotViewProxyFactory,
             LegacyScreenshotViewProxy.Factory legacyScreenshotViewProxyFactory) {
-        return legacyScreenshotViewProxyFactory;
+        if (screenshotShelfUi()) {
+            return shelfScreenshotViewProxyFactory;
+        } else {
+            return legacyScreenshotViewProxyFactory;
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt
new file mode 100644
index 0000000..2c17873
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.screenshot.ui
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.view.View
+
+class ScreenshotAnimationController(private val view: View) {
+    private var animator: Animator? = null
+
+    fun getEntranceAnimation(): Animator {
+        val animator = ValueAnimator.ofFloat(0f, 1f)
+        animator.addUpdateListener { view.alpha = it.animatedFraction }
+        animator.addListener(
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationStart(animator: Animator) {
+                    view.alpha = 0f
+                }
+                override fun onAnimationEnd(animator: Animator) {
+                    view.alpha = 1f
+                }
+            }
+        )
+        this.animator = animator
+        return animator
+    }
+
+    fun getExitAnimation(): Animator {
+        val animator = ValueAnimator.ofFloat(1f, 0f)
+        animator.addUpdateListener { view.alpha = it.animatedValue as Float }
+        animator.addListener(
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationStart(animator: Animator) {
+                    view.alpha = 1f
+                }
+                override fun onAnimationEnd(animator: Animator) {
+                    view.alpha = 0f
+                }
+            }
+        )
+        this.animator = animator
+        return animator
+    }
+
+    fun cancel() {
+        animator?.cancel()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt
new file mode 100644
index 0000000..747ad4f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.screenshot.ui
+
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.ImageView
+import androidx.constraintlayout.widget.ConstraintLayout
+import com.android.systemui.res.R
+
+class ScreenshotShelfView(context: Context, attrs: AttributeSet? = null) :
+    ConstraintLayout(context, attrs) {
+    lateinit var screenshotPreview: ImageView
+
+    override fun onFinishInflate() {
+        super.onFinishInflate()
+        screenshotPreview = requireViewById(R.id.screenshot_preview)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt
new file mode 100644
index 0000000..a5825b5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.screenshot.ui.binder
+
+import android.view.View
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import com.android.systemui.res.R
+import com.android.systemui.screenshot.ui.viewmodel.ActionButtonViewModel
+
+object ActionButtonViewBinder {
+    /** Binds the given view to the given view-model */
+    fun bind(view: View, viewModel: ActionButtonViewModel) {
+        val iconView = view.requireViewById<ImageView>(R.id.overlay_action_chip_icon)
+        val textView = view.requireViewById<TextView>(R.id.overlay_action_chip_text)
+        iconView.setImageDrawable(viewModel.icon)
+        textView.text = viewModel.name
+        setMargins(iconView, textView, viewModel.name?.isNotEmpty() ?: false)
+        if (viewModel.onClicked != null) {
+            view.setOnClickListener { viewModel.onClicked.invoke() }
+        } else {
+            view.setOnClickListener(null)
+        }
+        view.visibility = View.VISIBLE
+        view.alpha = 1f
+    }
+
+    private fun setMargins(iconView: View, textView: View, hasText: Boolean) {
+        val iconParams = iconView.layoutParams as LinearLayout.LayoutParams
+        val textParams = textView.layoutParams as LinearLayout.LayoutParams
+        if (hasText) {
+            iconParams.marginStart = iconView.dpToPx(R.dimen.overlay_action_chip_padding_start)
+            iconParams.marginEnd = iconView.dpToPx(R.dimen.overlay_action_chip_spacing)
+            textParams.marginStart = 0
+            textParams.marginEnd = textView.dpToPx(R.dimen.overlay_action_chip_padding_end)
+        } else {
+            val paddingHorizontal =
+                iconView.dpToPx(R.dimen.overlay_action_chip_icon_only_padding_horizontal)
+            iconParams.marginStart = paddingHorizontal
+            iconParams.marginEnd = paddingHorizontal
+        }
+        iconView.layoutParams = iconParams
+        textView.layoutParams = textParams
+    }
+
+    private fun View.dpToPx(dimenId: Int): Int {
+        return this.resources.getDimensionPixelSize(dimenId)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt
new file mode 100644
index 0000000..3bcd52c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt
@@ -0,0 +1,90 @@
+/*
+ * 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.screenshot.ui.binder
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.LinearLayout
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.res.R
+import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel
+import com.android.systemui.util.children
+import kotlinx.coroutines.launch
+
+object ScreenshotShelfViewBinder {
+    fun bind(
+        view: ViewGroup,
+        viewModel: ScreenshotViewModel,
+        layoutInflater: LayoutInflater,
+    ) {
+        val previewView: ImageView = view.requireViewById(R.id.screenshot_preview)
+        val previewBorder = view.requireViewById<View>(R.id.screenshot_preview_border)
+        previewView.clipToOutline = true
+        val actionsContainer: LinearLayout = view.requireViewById(R.id.screenshot_actions)
+        view.requireViewById<View>(R.id.screenshot_dismiss_button).visibility =
+            if (viewModel.showDismissButton) View.VISIBLE else View.GONE
+
+        view.repeatWhenAttached {
+            lifecycleScope.launch {
+                repeatOnLifecycle(Lifecycle.State.STARTED) {
+                    launch {
+                        viewModel.preview.collect { bitmap ->
+                            if (bitmap != null) {
+                                previewView.setImageBitmap(bitmap)
+                                previewView.visibility = View.VISIBLE
+                                previewBorder.visibility = View.VISIBLE
+                            } else {
+                                previewView.visibility = View.GONE
+                                previewBorder.visibility = View.GONE
+                            }
+                        }
+                    }
+                    launch {
+                        viewModel.actions.collect { actions ->
+                            if (actions.isNotEmpty()) {
+                                view
+                                    .requireViewById<View>(R.id.actions_container_background)
+                                    .visibility = View.VISIBLE
+                            }
+                            val viewPool = actionsContainer.children.toList()
+                            actionsContainer.removeAllViews()
+                            val actionButtons =
+                                List(actions.size) {
+                                    viewPool.getOrElse(it) {
+                                        layoutInflater.inflate(
+                                            R.layout.overlay_action_chip,
+                                            actionsContainer,
+                                            false
+                                        )
+                                    }
+                                }
+                            actionButtons.zip(actions).forEach {
+                                actionsContainer.addView(it.first)
+                                ActionButtonViewBinder.bind(it.first, it.second)
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonViewModel.kt
new file mode 100644
index 0000000..6ee9705
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonViewModel.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.ui.viewmodel
+
+import android.graphics.drawable.Drawable
+
+data class ActionButtonViewModel(
+    val icon: Drawable?,
+    val name: String?,
+    val onClicked: (() -> Unit)?
+)
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt
new file mode 100644
index 0000000..3a652d9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.screenshot.ui.viewmodel
+
+import android.graphics.Bitmap
+import android.view.accessibility.AccessibilityManager
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+class ScreenshotViewModel(private val accessibilityManager: AccessibilityManager) {
+    private val _preview = MutableStateFlow<Bitmap?>(null)
+    val preview: StateFlow<Bitmap?> = _preview
+    private val _actions = MutableStateFlow(emptyList<ActionButtonViewModel>())
+    val actions: StateFlow<List<ActionButtonViewModel>> = _actions
+    val showDismissButton: Boolean
+        get() = accessibilityManager.isEnabled
+
+    fun setScreenshotBitmap(bitmap: Bitmap?) {
+        _preview.value = bitmap
+    }
+
+    fun setActions(actions: List<ActionButtonViewModel>) {
+        _actions.value = actions
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 9cb920a..2de14dd 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -24,8 +24,6 @@
 import static com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE;
 import static com.android.keyguard.KeyguardClockSwitch.LARGE;
 import static com.android.keyguard.KeyguardClockSwitch.SMALL;
-import static com.android.systemui.Flags.keyguardBottomAreaRefactor;
-import static com.android.systemui.Flags.migrateClocksToBlueprint;
 import static com.android.systemui.Flags.predictiveBackAnimateShade;
 import static com.android.systemui.Flags.smartspaceRelocateToBottom;
 import static com.android.systemui.classifier.Classifier.BOUNCER_UNLOCK;
@@ -129,8 +127,10 @@
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
 import com.android.systemui.fragments.FragmentService;
+import com.android.systemui.keyguard.KeyguardBottomAreaRefactor;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.KeyguardViewConfigurator;
+import com.android.systemui.keyguard.MigrateClocksToBlueprint;
 import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
@@ -1018,7 +1018,7 @@
                 instantCollapse();
             } else {
                 mView.animate().cancel();
-                if (!migrateClocksToBlueprint()) {
+                if (!MigrateClocksToBlueprint.isEnabled()) {
                     mView.animate()
                             .alpha(0f)
                             .setStartDelay(0)
@@ -1075,7 +1075,7 @@
         mQsController.init();
         mShadeHeadsUpTracker.addTrackingHeadsUpListener(
                 mNotificationStackScrollLayoutController::setTrackingHeadsUp);
-        if (!keyguardBottomAreaRefactor()) {
+        if (!KeyguardBottomAreaRefactor.isEnabled()) {
             setKeyguardBottomArea(mView.findViewById(R.id.keyguard_bottom_area));
         }
 
@@ -1154,7 +1154,7 @@
         // Occluded->Lockscreen
         collectFlow(mView, mKeyguardTransitionInteractor.getOccludedToLockscreenTransition(),
                 mOccludedToLockscreenTransition, mMainDispatcher);
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled()) {
             collectFlow(mView, mOccludedToLockscreenTransitionViewModel.getLockscreenAlpha(),
                     setTransitionAlpha(mNotificationStackScrollLayoutController), mMainDispatcher);
             collectFlow(mView,
@@ -1165,7 +1165,7 @@
         // Lockscreen->Dreaming
         collectFlow(mView, mKeyguardTransitionInteractor.getLockscreenToDreamingTransition(),
                 mLockscreenToDreamingTransition, mMainDispatcher);
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled()) {
             collectFlow(mView, mLockscreenToDreamingTransitionViewModel.getLockscreenAlpha(),
                     setDreamLockscreenTransitionAlpha(mNotificationStackScrollLayoutController),
                     mMainDispatcher);
@@ -1177,7 +1177,7 @@
         // Gone->Dreaming
         collectFlow(mView, mKeyguardTransitionInteractor.getGoneToDreamingTransition(),
                 mGoneToDreamingTransition, mMainDispatcher);
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled()) {
             collectFlow(mView, mGoneToDreamingTransitionViewModel.getLockscreenAlpha(),
                     setTransitionAlpha(mNotificationStackScrollLayoutController), mMainDispatcher);
         }
@@ -1188,7 +1188,7 @@
         // Lockscreen->Occluded
         collectFlow(mView, mKeyguardTransitionInteractor.getLockscreenToOccludedTransition(),
                 mLockscreenToOccludedTransition, mMainDispatcher);
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled()) {
             collectFlow(mView, mLockscreenToOccludedTransitionViewModel.getLockscreenAlpha(),
                     setTransitionAlpha(mNotificationStackScrollLayoutController), mMainDispatcher);
             collectFlow(mView, mLockscreenToOccludedTransitionViewModel.getLockscreenTranslationY(),
@@ -1196,7 +1196,7 @@
         }
 
         // Primary bouncer->Gone (ensures lockscreen content is not visible on successful auth)
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled()) {
             collectFlow(mView, mPrimaryBouncerToGoneTransitionViewModel.getLockscreenAlpha(),
                     setTransitionAlpha(mNotificationStackScrollLayoutController,
                             /* excludeNotifications=*/ true), mMainDispatcher);
@@ -1280,7 +1280,7 @@
             mKeyguardStatusViewController.onDestroy();
         }
 
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled()) {
             // Need a shared controller until mKeyguardStatusViewController can be removed from
             // here, due to important state being set in that controller. Rebind in order to pick
             // up config changes
@@ -1332,13 +1332,13 @@
 
     private void onSplitShadeEnabledChanged() {
         mShadeLog.logSplitShadeChanged(mSplitShadeEnabled);
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled()) {
             mKeyguardStatusViewController.setSplitShadeEnabled(mSplitShadeEnabled);
         }
         // Reset any left over overscroll state. It is a rare corner case but can happen.
         mQsController.setOverScrollAmount(0);
         mScrimController.setNotificationsOverScrollAmount(0);
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled()) {
             mNotificationStackScrollLayoutController.setOverExpansion(0);
             mNotificationStackScrollLayoutController.setOverScrollAmount(0);
         }
@@ -1359,7 +1359,7 @@
         }
         updateClockAppearance();
         mQsController.updateQsState();
-        if (!migrateClocksToBlueprint() && !FooterViewRefactor.isEnabled()) {
+        if (!MigrateClocksToBlueprint.isEnabled() && !FooterViewRefactor.isEnabled()) {
             mNotificationStackScrollLayoutController.updateFooter();
         }
     }
@@ -1391,7 +1391,7 @@
     void reInflateViews() {
         debugLog("reInflateViews");
         // Re-inflate the status view group.
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled()) {
             KeyguardStatusView keyguardStatusView =
                     mNotificationContainerParent.findViewById(R.id.keyguard_status_view);
             int statusIndex = mNotificationContainerParent.indexOfChild(keyguardStatusView);
@@ -1430,7 +1430,7 @@
 
         updateViewControllers(userAvatarView, keyguardUserSwitcherView);
 
-        if (!keyguardBottomAreaRefactor()) {
+        if (!KeyguardBottomAreaRefactor.isEnabled()) {
             // Update keyguard bottom area
             int index = mView.indexOfChild(mKeyguardBottomArea);
             mView.removeView(mKeyguardBottomArea);
@@ -1449,7 +1449,7 @@
         mStatusBarStateListener.onDozeAmountChanged(mStatusBarStateController.getDozeAmount(),
                 mStatusBarStateController.getInterpolatedDozeAmount());
 
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled()) {
             mKeyguardStatusViewController.setKeyguardStatusViewVisibility(
                     mBarState,
                     false,
@@ -1471,7 +1471,7 @@
                     mBarState);
         }
 
-        if (!keyguardBottomAreaRefactor()) {
+        if (!KeyguardBottomAreaRefactor.isEnabled()) {
             setKeyguardBottomAreaVisibility(mBarState, false);
         }
 
@@ -1480,14 +1480,14 @@
     }
 
     private void attachSplitShadeMediaPlayerContainer(FrameLayout container) {
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled()) {
             return;
         }
         mKeyguardMediaController.attachSplitShadeContainer(container);
     }
 
     private void initBottomArea() {
-        if (!keyguardBottomAreaRefactor()) {
+        if (!KeyguardBottomAreaRefactor.isEnabled()) {
             mKeyguardBottomArea.init(
                 mKeyguardBottomAreaViewModel,
                 mFalsingManager,
@@ -1513,7 +1513,7 @@
     }
 
     private void updateMaxDisplayedNotifications(boolean recompute) {
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled()) {
             return;
         }
 
@@ -1630,7 +1630,7 @@
         int userSwitcherPreferredY = mStatusBarHeaderHeightKeyguard;
         boolean bypassEnabled = mKeyguardBypassController.getBypassEnabled();
         boolean shouldAnimateClockChange = mScreenOffAnimationController.shouldAnimateClockChange();
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled()) {
             mKeyguardClockInteractor.setClockSize(computeDesiredClockSize());
         } else {
             mKeyguardStatusViewController.displayClock(computeDesiredClockSize(),
@@ -1671,11 +1671,11 @@
                 mKeyguardStatusViewController.getClockBottom(mStatusBarHeaderHeightKeyguard),
                 mKeyguardStatusViewController.isClockTopAligned());
         mClockPositionAlgorithm.run(mClockPositionResult);
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled()) {
             mKeyguardStatusViewController.setLockscreenClockY(
                     mClockPositionAlgorithm.getExpandedPreferredClockY());
         }
-        if (!(migrateClocksToBlueprint() || keyguardBottomAreaRefactor())) {
+        if (!(MigrateClocksToBlueprint.isEnabled() || KeyguardBottomAreaRefactor.isEnabled())) {
             mKeyguardBottomAreaInteractor.setClockPosition(
                 mClockPositionResult.clockX, mClockPositionResult.clockY);
         }
@@ -1683,7 +1683,7 @@
         boolean animate = mNotificationStackScrollLayoutController.isAddOrRemoveAnimationPending();
         boolean animateClock = (animate || mAnimateNextPositionUpdate) && shouldAnimateClockChange;
 
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled()) {
             mKeyguardStatusViewController.updatePosition(
                     mClockPositionResult.clockX, mClockPositionResult.clockY,
                     mClockPositionResult.clockScale, animateClock);
@@ -1740,7 +1740,7 @@
         // To prevent the weather clock from overlapping with the notification shelf on AOD, we use
         // the small clock here
         // With migrateClocksToBlueprint, weather clock will have behaviors similar to other clocks
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled()) {
             if (mKeyguardStatusViewController.isLargeClockBlockingNotificationShelf()
                     && hasVisibleNotifications() && isOnAod()) {
                 return SMALL;
@@ -1758,7 +1758,7 @@
 
     private void updateKeyguardStatusViewAlignment(boolean animate) {
         boolean shouldBeCentered = shouldKeyguardStatusViewBeCentered();
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled()) {
             mKeyguardInteractor.setClockShouldBeCentered(shouldBeCentered);
             return;
         }
@@ -1941,7 +1941,7 @@
         }
         float alpha = mClockPositionResult.clockAlpha * mKeyguardOnlyContentAlpha;
         mKeyguardStatusViewController.setAlpha(alpha);
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled()) {
             // TODO (b/296373478) This is for split shade media movement.
         } else {
             mKeyguardStatusViewController
@@ -2498,7 +2498,7 @@
         }
 
         if (!mKeyguardBypassController.getBypassEnabled()) {
-            if (migrateClocksToBlueprint() && !mSplitShadeEnabled) {
+            if (MigrateClocksToBlueprint.isEnabled() && !mSplitShadeEnabled) {
                 return (int) mKeyguardInteractor.getNotificationContainerBounds()
                         .getValue().getTop();
             }
@@ -2531,7 +2531,7 @@
     void requestScrollerTopPaddingUpdate(boolean animate) {
         float padding = mQsController.calculateNotificationsTopPadding(mIsExpandingOrCollapsing,
                 getKeyguardNotificationStaticPadding(), mExpandedFraction);
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled()) {
             mSharedNotificationContainerInteractor.setTopPosition(padding);
         } else {
             mNotificationStackScrollLayoutController.updateTopPadding(padding, animate);
@@ -2712,7 +2712,7 @@
             return;
         }
 
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled()) {
             float alpha = 1f;
             if (mClosingWithAlphaFadeOut && !mExpandingFromHeadsUp
                 && !mHeadsUpManager.hasPinnedHeadsUp()) {
@@ -2748,7 +2748,7 @@
     }
 
     private void updateKeyguardBottomAreaAlpha() {
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled()) {
             return;
         }
         if (mIsOcclusionTransitionRunning) {
@@ -2766,7 +2766,7 @@
 
         float alpha = Math.min(expansionAlpha, 1 - mQsController.computeExpansionFraction());
         alpha *= mBottomAreaShadeAlpha;
-        if (keyguardBottomAreaRefactor()) {
+        if (KeyguardBottomAreaRefactor.isEnabled()) {
             mKeyguardInteractor.setAlpha(alpha);
         } else {
             mKeyguardBottomAreaInteractor.setAlpha(alpha);
@@ -2978,7 +2978,7 @@
     }
 
     private void updateDozingVisibilities(boolean animate) {
-        if (keyguardBottomAreaRefactor()) {
+        if (KeyguardBottomAreaRefactor.isEnabled()) {
             mKeyguardInteractor.setAnimateDozingTransitions(animate);
         } else {
             mKeyguardBottomAreaInteractor.setAnimateDozingTransitions(animate);
@@ -2990,7 +2990,7 @@
 
     @Override
     public void onScreenTurningOn() {
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled()) {
             mKeyguardStatusViewController.dozeTimeTick();
         }
     }
@@ -3189,7 +3189,7 @@
         mDozing = dozing;
         // TODO (b/) make listeners for this
         mNotificationStackScrollLayoutController.setDozing(mDozing, animate);
-        if (keyguardBottomAreaRefactor()) {
+        if (KeyguardBottomAreaRefactor.isEnabled()) {
             mKeyguardInteractor.setAnimateDozingTransitions(animate);
         } else {
             mKeyguardBottomAreaInteractor.setAnimateDozingTransitions(animate);
@@ -3245,7 +3245,7 @@
 
     public void dozeTimeTick() {
         mLockIconViewController.dozeTimeTick();
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled()) {
             mKeyguardStatusViewController.dozeTimeTick();
         }
         if (mInterpolatedDarkAmount > 0) {
@@ -3324,7 +3324,7 @@
         /** Updates the views to the initial state for the fold to AOD animation. */
         @Override
         public void prepareFoldToAodAnimation() {
-            if (migrateClocksToBlueprint()) {
+            if (MigrateClocksToBlueprint.isEnabled()) {
                 return;
             }
             // Force show AOD UI even if we are not locked
@@ -3348,7 +3348,7 @@
         @Override
         public void startFoldToAodAnimation(Runnable startAction, Runnable endAction,
                 Runnable cancelAction) {
-            if (migrateClocksToBlueprint()) {
+            if (MigrateClocksToBlueprint.isEnabled()) {
                 return;
             }
             final ViewPropertyAnimator viewAnimator = mView.animate();
@@ -3386,7 +3386,7 @@
         /** Cancels fold to AOD transition and resets view state. */
         @Override
         public void cancelFoldToAodAnimation() {
-            if (migrateClocksToBlueprint()) {
+            if (MigrateClocksToBlueprint.isEnabled()) {
                 return;
             }
             cancelAnimation();
@@ -4460,7 +4460,7 @@
                     && statusBarState == KEYGUARD) {
                 // This means we're doing the screen off animation - position the keyguard status
                 // view where it'll be on AOD, so we can animate it in.
-                if (!migrateClocksToBlueprint()) {
+                if (!MigrateClocksToBlueprint.isEnabled()) {
                     mKeyguardStatusViewController.updatePosition(
                             mClockPositionResult.clockX,
                             mClockPositionResult.clockYFullyDozing,
@@ -4469,7 +4469,7 @@
                 }
             }
 
-            if (!migrateClocksToBlueprint()) {
+            if (!MigrateClocksToBlueprint.isEnabled()) {
                 mKeyguardStatusViewController.setKeyguardStatusViewVisibility(
                         statusBarState,
                         keyguardFadingAway,
@@ -4477,7 +4477,7 @@
                         mBarState);
             }
 
-            if (!keyguardBottomAreaRefactor()) {
+            if (!KeyguardBottomAreaRefactor.isEnabled()) {
                 setKeyguardBottomAreaVisibility(statusBarState, goingToFullShade);
             }
 
@@ -4582,7 +4582,7 @@
         setDozing(true /* dozing */, false /* animate */);
         mStatusBarStateController.setUpcomingState(KEYGUARD);
 
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled()) {
             mStatusBarStateController.setState(KEYGUARD);
         } else {
             mStatusBarStateListener.onStateChanged(KEYGUARD);
@@ -4645,7 +4645,7 @@
             setIsFullWidth(mNotificationStackScrollLayoutController.getWidth() == mView.getWidth());
 
             // Update Clock Pivot (used by anti-burnin transformations)
-            if (!migrateClocksToBlueprint()) {
+            if (!MigrateClocksToBlueprint.isEnabled()) {
                 mKeyguardStatusViewController.updatePivot(mView.getWidth(), mView.getHeight());
             }
 
@@ -4746,7 +4746,7 @@
                 stackScroller.setMaxAlphaForKeyguard(alpha, "NPVC.setTransitionAlpha()");
             }
 
-            if (keyguardBottomAreaRefactor()) {
+            if (KeyguardBottomAreaRefactor.isEnabled()) {
                 mKeyguardInteractor.setAlpha(alpha);
             } else {
                 mKeyguardBottomAreaInteractor.setAlpha(alpha);
@@ -4765,7 +4765,7 @@
     private Consumer<Float> setTransitionY(
                 NotificationStackScrollLayoutController stackScroller) {
         return (Float translationY) -> {
-            if (!migrateClocksToBlueprint()) {
+            if (!MigrateClocksToBlueprint.isEnabled()) {
                 mKeyguardStatusViewController.setTranslationY(translationY,
                         /* excludeMedia= */false);
                 stackScroller.setTranslationY(translationY);
@@ -4807,7 +4807,7 @@
          */
         @Override
         public boolean onInterceptTouchEvent(MotionEvent event) {
-            if (migrateClocksToBlueprint() && !mUseExternalTouch) {
+            if (MigrateClocksToBlueprint.isEnabled() && !mUseExternalTouch) {
                 return false;
             }
 
@@ -4878,7 +4878,7 @@
 
             switch (event.getActionMasked()) {
                 case MotionEvent.ACTION_DOWN:
-                    if (!migrateClocksToBlueprint()) {
+                    if (!MigrateClocksToBlueprint.isEnabled()) {
                         mCentralSurfaces.userActivity();
                     }
                     mAnimatingOnDown = mHeightAnimator != null && !mIsSpringBackAnimation;
@@ -4979,7 +4979,7 @@
          */
         @Override
         public boolean onTouchEvent(MotionEvent event) {
-            if (migrateClocksToBlueprint() && !mUseExternalTouch) {
+            if (MigrateClocksToBlueprint.isEnabled() && !mUseExternalTouch) {
                 return false;
             }
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index e577178..e8e629c 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -16,7 +16,6 @@
 
 package com.android.systemui.shade;
 
-import static com.android.systemui.Flags.migrateClocksToBlueprint;
 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;
@@ -48,6 +47,7 @@
 import com.android.systemui.flags.Flags;
 import com.android.systemui.keyevent.domain.interactor.SysUIKeyEventHandler;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
+import com.android.systemui.keyguard.MigrateClocksToBlueprint;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
 import com.android.systemui.keyguard.shared.model.TransitionState;
 import com.android.systemui.keyguard.shared.model.TransitionStep;
@@ -320,7 +320,7 @@
                     mTouchActive = true;
                     mTouchCancelled = false;
                     mDownEvent = ev;
-                    if (migrateClocksToBlueprint()) {
+                    if (MigrateClocksToBlueprint.isEnabled()) {
                         mService.userActivity();
                     }
                 } else if (ev.getActionMasked() == MotionEvent.ACTION_UP
@@ -475,7 +475,7 @@
                         && !bouncerShowing
                         && !mStatusBarStateController.isDozing()) {
                     if (mDragDownHelper.isDragDownEnabled()) {
-                        if (migrateClocksToBlueprint()) {
+                        if (MigrateClocksToBlueprint.isEnabled()) {
                             // When on lockscreen, if the touch originates at the top of the screen
                             // go directly to QS and not the shade
                             if (mStatusBarStateController.getState() == KEYGUARD
@@ -488,7 +488,7 @@
 
                         // This handles drag down over lockscreen
                         boolean result = mDragDownHelper.onInterceptTouchEvent(ev);
-                        if (migrateClocksToBlueprint()) {
+                        if (MigrateClocksToBlueprint.isEnabled()) {
                             if (result) {
                                 mLastInterceptWasDragDownHelper = true;
                                 if (ev.getAction() == MotionEvent.ACTION_DOWN) {
@@ -511,7 +511,7 @@
                             return true;
                         }
                     }
-                } else if (migrateClocksToBlueprint()) {
+                } else if (MigrateClocksToBlueprint.isEnabled()) {
                     // This final check handles swipes on HUNs and when Pulsing
                     if (!bouncerShowing && didNotificationPanelInterceptEvent(ev)) {
                         mShadeLogger.d("NSWVC: intercepted for HUN/PULSING");
@@ -526,7 +526,7 @@
                 MotionEvent cancellation = MotionEvent.obtain(ev);
                 cancellation.setAction(MotionEvent.ACTION_CANCEL);
                 mStackScrollLayout.onInterceptTouchEvent(cancellation);
-                if (!migrateClocksToBlueprint()) {
+                if (!MigrateClocksToBlueprint.isEnabled()) {
                     mNotificationPanelViewController.handleExternalInterceptTouch(cancellation);
                 }
                 cancellation.recycle();
@@ -541,7 +541,7 @@
                 if (mStatusBarKeyguardViewManager.onTouch(ev)) {
                     return true;
                 }
-                if (migrateClocksToBlueprint()) {
+                if (MigrateClocksToBlueprint.isEnabled()) {
                     if (mLastInterceptWasDragDownHelper && (mDragDownHelper.isDraggingDown())) {
                         // we still want to finish our drag down gesture when locking the screen
                         handled |= mDragDownHelper.onTouchEvent(ev) || handled;
@@ -631,7 +631,7 @@
     }
 
     private boolean didNotificationPanelInterceptEvent(MotionEvent ev) {
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled()) {
             // Since NotificationStackScrollLayout is now a sibling of notification_panel, we need
             // to also ask NotificationPanelViewController directly, in order to process swipe up
             // events originating from notifications
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt
index 29de688..8b88da1 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt
@@ -28,10 +28,10 @@
 import androidx.constraintlayout.widget.ConstraintSet.TOP
 import androidx.lifecycle.lifecycleScope
 import com.android.systemui.Flags.centralizedStatusBarHeightFix
-import com.android.systemui.Flags.migrateClocksToBlueprint
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.fragments.FragmentService
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.navigationbar.NavigationModeController
 import com.android.systemui.plugins.qs.QS
@@ -52,11 +52,12 @@
 import kotlin.reflect.KMutableProperty0
 import kotlinx.coroutines.launch
 
-@VisibleForTesting
-internal const val INSET_DEBOUNCE_MILLIS = 500L
+@VisibleForTesting internal const val INSET_DEBOUNCE_MILLIS = 500L
 
 @SysUISingleton
-class NotificationsQSContainerController @Inject constructor(
+class NotificationsQSContainerController
+@Inject
+constructor(
     view: NotificationsQuickSettingsContainer,
     private val navigationModeController: NavigationModeController,
     private val overviewProxyService: OverviewProxyService,
@@ -64,8 +65,7 @@
     private val shadeInteractor: ShadeInteractor,
     private val fragmentService: FragmentService,
     @Main private val delayableExecutor: DelayableExecutor,
-    private val
-    notificationStackScrollLayoutController: NotificationStackScrollLayoutController,
+    private val notificationStackScrollLayoutController: NotificationStackScrollLayoutController,
     private val splitShadeStateController: SplitShadeStateController,
     private val largeScreenHeaderHelperLazy: Lazy<LargeScreenHeaderHelper>,
 ) : ViewController<NotificationsQuickSettingsContainer>(view), QSContainerController {
@@ -88,45 +88,48 @@
 
     private var isGestureNavigation = true
     private var taskbarVisible = false
-    private val taskbarVisibilityListener: OverviewProxyListener = object : OverviewProxyListener {
-        override fun onTaskbarStatusUpdated(visible: Boolean, stashed: Boolean) {
-            taskbarVisible = visible
+    private val taskbarVisibilityListener: OverviewProxyListener =
+        object : OverviewProxyListener {
+            override fun onTaskbarStatusUpdated(visible: Boolean, stashed: Boolean) {
+                taskbarVisible = visible
+            }
         }
-    }
 
     // With certain configuration changes (like light/dark changes), the nav bar will disappear
     // for a bit, causing `bottomStableInsets` to be unstable for some time. Debounce the value
     // for 500ms.
     // All interactions with this object happen in the main thread.
-    private val delayedInsetSetter = object : Runnable, Consumer<WindowInsets> {
-        private var canceller: Runnable? = null
-        private var stableInsets = 0
-        private var cutoutInsets = 0
+    private val delayedInsetSetter =
+        object : Runnable, Consumer<WindowInsets> {
+            private var canceller: Runnable? = null
+            private var stableInsets = 0
+            private var cutoutInsets = 0
 
-        override fun accept(insets: WindowInsets) {
-            // when taskbar is visible, stableInsetBottom will include its height
-            stableInsets = insets.stableInsetBottom
-            cutoutInsets = insets.displayCutout?.safeInsetBottom ?: 0
-            canceller?.run()
-            canceller = delayableExecutor.executeDelayed(this, INSET_DEBOUNCE_MILLIS)
-        }
+            override fun accept(insets: WindowInsets) {
+                // when taskbar is visible, stableInsetBottom will include its height
+                stableInsets = insets.stableInsetBottom
+                cutoutInsets = insets.displayCutout?.safeInsetBottom ?: 0
+                canceller?.run()
+                canceller = delayableExecutor.executeDelayed(this, INSET_DEBOUNCE_MILLIS)
+            }
 
-        override fun run() {
-            bottomStableInsets = stableInsets
-            bottomCutoutInsets = cutoutInsets
-            updateBottomSpacing()
+            override fun run() {
+                bottomStableInsets = stableInsets
+                bottomCutoutInsets = cutoutInsets
+                updateBottomSpacing()
+            }
         }
-    }
 
     override fun onInit() {
         mView.repeatWhenAttached {
             lifecycleScope.launch {
-                shadeInteractor.isQsExpanded.collect{ _ -> mView.invalidate() }
+                shadeInteractor.isQsExpanded.collect { _ -> mView.invalidate() }
             }
         }
-        val currentMode: Int = navigationModeController.addListener { mode: Int ->
-            isGestureNavigation = QuickStepContract.isGesturalMode(mode)
-        }
+        val currentMode: Int =
+            navigationModeController.addListener { mode: Int ->
+                isGestureNavigation = QuickStepContract.isGesturalMode(mode)
+            }
         isGestureNavigation = QuickStepContract.isGesturalMode(currentMode)
 
         mView.setStackScroller(notificationStackScrollLayoutController.getView())
@@ -151,30 +154,35 @@
 
     fun updateResources() {
         val newSplitShadeEnabled =
-                splitShadeStateController.shouldUseSplitNotificationShade(resources)
+            splitShadeStateController.shouldUseSplitNotificationShade(resources)
         val splitShadeEnabledChanged = newSplitShadeEnabled != splitShadeEnabled
         splitShadeEnabled = newSplitShadeEnabled
         largeScreenShadeHeaderActive = LargeScreenUtils.shouldUseLargeScreenShadeHeader(resources)
-        notificationsBottomMargin = resources.getDimensionPixelSize(
-                R.dimen.notification_panel_margin_bottom)
+        notificationsBottomMargin =
+            resources.getDimensionPixelSize(R.dimen.notification_panel_margin_bottom)
         largeScreenShadeHeaderHeight = calculateLargeShadeHeaderHeight()
         shadeHeaderHeight = calculateShadeHeaderHeight()
-        panelMarginHorizontal = resources.getDimensionPixelSize(
-                R.dimen.notification_panel_margin_horizontal)
-        topMargin = if (largeScreenShadeHeaderActive) {
-            largeScreenShadeHeaderHeight
-        } else {
-            resources.getDimensionPixelSize(R.dimen.notification_panel_margin_top)
-        }
+        panelMarginHorizontal =
+            resources.getDimensionPixelSize(R.dimen.notification_panel_margin_horizontal)
+        topMargin =
+            if (largeScreenShadeHeaderActive) {
+                largeScreenShadeHeaderHeight
+            } else {
+                resources.getDimensionPixelSize(R.dimen.notification_panel_margin_top)
+            }
         updateConstraints()
 
-        val scrimMarginChanged = ::scrimShadeBottomMargin.setAndReportChange(
-            resources.getDimensionPixelSize(R.dimen.split_shade_notifications_scrim_margin_bottom)
-        )
-        val footerOffsetChanged = ::footerActionsOffset.setAndReportChange(
-            resources.getDimensionPixelSize(R.dimen.qs_footer_action_inset) +
-                resources.getDimensionPixelSize(R.dimen.qs_footer_actions_bottom_padding)
-        )
+        val scrimMarginChanged =
+            ::scrimShadeBottomMargin.setAndReportChange(
+                resources.getDimensionPixelSize(
+                    R.dimen.split_shade_notifications_scrim_margin_bottom
+                )
+            )
+        val footerOffsetChanged =
+            ::footerActionsOffset.setAndReportChange(
+                resources.getDimensionPixelSize(R.dimen.qs_footer_action_inset) +
+                    resources.getDimensionPixelSize(R.dimen.qs_footer_actions_bottom_padding)
+            )
         val dimensChanged = scrimMarginChanged || footerOffsetChanged
 
         if (splitShadeEnabledChanged || dimensChanged) {
@@ -198,7 +206,7 @@
         // 2. carrier_group height (R.dimen.large_screen_shade_header_min_height)
         // 3. date height (R.dimen.new_qs_header_non_clickable_element_height)
         val estimatedHeight =
-                2 * resources.getDimensionPixelSize(R.dimen.large_screen_shade_header_min_height) +
+            2 * resources.getDimensionPixelSize(R.dimen.large_screen_shade_header_min_height) +
                 resources.getDimensionPixelSize(R.dimen.new_qs_header_non_clickable_element_height)
         return estimatedHeight.coerceAtLeast(minHeight)
     }
@@ -250,16 +258,17 @@
             containerPadding = 0
             stackScrollMargin = bottomStableInsets + notificationsBottomMargin
         }
-        val qsContainerPadding = if (!isQSDetailShowing) {
-            // We also want this padding in the bottom in these cases
-            if (splitShadeEnabled) {
-                stackScrollMargin - scrimShadeBottomMargin - footerActionsOffset
+        val qsContainerPadding =
+            if (!isQSDetailShowing) {
+                // We also want this padding in the bottom in these cases
+                if (splitShadeEnabled) {
+                    stackScrollMargin - scrimShadeBottomMargin - footerActionsOffset
+                } else {
+                    bottomStableInsets
+                }
             } else {
-                bottomStableInsets
+                0
             }
-        } else {
-            0
-        }
         return Paddings(containerPadding, stackScrollMargin, qsContainerPadding)
     }
 
@@ -284,7 +293,7 @@
     }
 
     private fun setNotificationsConstraints(constraintSet: ConstraintSet) {
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled) {
             return
         }
         val startConstraintId = if (splitShadeEnabled) R.id.qs_edge_guideline else PARENT_ID
@@ -309,8 +318,8 @@
     }
 
     private fun setKeyguardStatusViewConstraints(constraintSet: ConstraintSet) {
-        val statusViewMarginHorizontal = resources.getDimensionPixelSize(
-                R.dimen.status_view_margin_horizontal)
+        val statusViewMarginHorizontal =
+            resources.getDimensionPixelSize(R.dimen.status_view_margin_horizontal)
         constraintSet.apply {
             setMargin(R.id.keyguard_status_view, START, statusViewMarginHorizontal)
             setMargin(R.id.keyguard_status_view, END, statusViewMarginHorizontal)
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java
index e82f2d3..1333055 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java
@@ -18,8 +18,6 @@
 
 import static androidx.constraintlayout.core.widgets.Optimizer.OPTIMIZATION_GRAPH;
 
-import static com.android.systemui.Flags.migrateClocksToBlueprint;
-
 import android.app.Fragment;
 import android.content.Context;
 import android.content.res.Configuration;
@@ -35,6 +33,7 @@
 import androidx.constraintlayout.widget.ConstraintSet;
 
 import com.android.systemui.fragments.FragmentHostManager.FragmentListener;
+import com.android.systemui.keyguard.MigrateClocksToBlueprint;
 import com.android.systemui.plugins.qs.QS;
 import com.android.systemui.res.R;
 import com.android.systemui.statusbar.notification.AboveShelfObserver;
@@ -190,7 +189,7 @@
 
     @Override
     protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled()) {
             return super.drawChild(canvas, child, drawingTime);
         }
         int layoutIndex = mLayoutDrawingOrder.indexOf(child);
diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
index 8ba0544..3a0e167 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
@@ -21,7 +21,6 @@
 
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE;
 import static com.android.systemui.Flags.centralizedStatusBarHeightFix;
-import static com.android.systemui.Flags.migrateClocksToBlueprint;
 import static com.android.systemui.classifier.Classifier.QS_COLLAPSE;
 import static com.android.systemui.shade.NotificationPanelViewController.COUNTER_PANEL_OPEN_QS;
 import static com.android.systemui.shade.NotificationPanelViewController.FLING_COLLAPSE;
@@ -71,6 +70,7 @@
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.fragments.FragmentHostManager;
+import com.android.systemui.keyguard.MigrateClocksToBlueprint;
 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager;
 import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager;
 import com.android.systemui.plugins.FalsingManager;
@@ -1280,18 +1280,20 @@
 
         mScrimController.setScrimCornerRadius(radius);
 
-        // Convert global clipping coordinates to local ones,
-        // relative to NotificationStackScrollLayout
-        int nsslLeft = calculateNsslLeft(left);
-        int nsslRight = calculateNsslRight(right);
-        int nsslTop = getNotificationsClippingTopBounds(top);
-        int nsslBottom = bottom - mNotificationStackScrollLayoutController.getTop();
-        int bottomRadius = mSplitShadeEnabled ? radius : 0;
-        // TODO (b/265193930): remove dependency on NPVC
-        int topRadius = mSplitShadeEnabled
-                && mPanelViewControllerLazy.get().isExpandingFromHeadsUp() ? 0 : radius;
-        mNotificationStackScrollLayoutController.setRoundedClippingBounds(
-                nsslLeft, nsslTop, nsslRight, nsslBottom, topRadius, bottomRadius);
+        if (!SceneContainerFlag.isEnabled()) {
+            // Convert global clipping coordinates to local ones,
+            // relative to NotificationStackScrollLayout
+            int nsslLeft = calculateNsslLeft(left);
+            int nsslRight = calculateNsslRight(right);
+            int nsslTop = getNotificationsClippingTopBounds(top);
+            int nsslBottom = bottom - mNotificationStackScrollLayoutController.getTop();
+            int bottomRadius = mSplitShadeEnabled ? radius : 0;
+            // TODO (b/265193930): remove dependency on NPVC
+            int topRadius = mSplitShadeEnabled
+                    && mPanelViewControllerLazy.get().isExpandingFromHeadsUp() ? 0 : radius;
+            mNotificationStackScrollLayoutController.setRoundedClippingBounds(
+                    nsslLeft, nsslTop, nsslRight, nsslBottom, topRadius, bottomRadius);
+        }
     }
 
     /**
@@ -1776,7 +1778,7 @@
                     // Dragging down on the lockscreen statusbar should prohibit other interactions
                     // immediately, otherwise we'll wait on the touchslop. This is to allow
                     // dragging down to expanded quick settings directly on the lockscreen.
-                    if (!migrateClocksToBlueprint()) {
+                    if (!MigrateClocksToBlueprint.isEnabled()) {
                         mPanelView.getParent().requestDisallowInterceptTouchEvent(true);
                     }
                 }
@@ -1821,7 +1823,7 @@
                         && Math.abs(h) > Math.abs(x - mInitialTouchX)
                         && shouldQuickSettingsIntercept(
                         mInitialTouchX, mInitialTouchY, h)) {
-                    if (!migrateClocksToBlueprint()) {
+                    if (!MigrateClocksToBlueprint.isEnabled()) {
                         mPanelView.getParent().requestDisallowInterceptTouchEvent(true);
                     }
                     mShadeLog.onQsInterceptMoveQsTrackingEnabled(h);
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
index c20efea..6bb1df7 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
@@ -248,7 +248,7 @@
     }
 
     override fun onStatusBarTouch(event: MotionEvent) {
-        // The only call to this doesn't happen with migrateClocksToBlueprint() enabled
+        // The only call to this doesn't happen with MigrateClocksToBlueprint.isEnabled enabled
         throw UnsupportedOperationException()
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
index ea549f2..24b7533 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
@@ -66,11 +66,13 @@
                 deviceEntryInteractor.isUnlocked,
                 deviceEntryInteractor.canSwipeToEnter,
                 shadeInteractor.shadeMode,
-            ) { isUnlocked, canSwipeToDismiss, shadeMode ->
+                qsSceneAdapter.isCustomizing
+            ) { isUnlocked, canSwipeToDismiss, shadeMode, isCustomizing ->
                 destinationScenes(
                     isUnlocked = isUnlocked,
                     canSwipeToDismiss = canSwipeToDismiss,
                     shadeMode = shadeMode,
+                    isCustomizing = isCustomizing
                 )
             }
             .stateIn(
@@ -81,6 +83,7 @@
                         isUnlocked = deviceEntryInteractor.isUnlocked.value,
                         canSwipeToDismiss = deviceEntryInteractor.canSwipeToEnter.value,
                         shadeMode = shadeInteractor.shadeMode.value,
+                        isCustomizing = qsSceneAdapter.isCustomizing.value,
                     ),
             )
 
@@ -120,6 +123,7 @@
         isUnlocked: Boolean,
         canSwipeToDismiss: Boolean?,
         shadeMode: ShadeMode,
+        isCustomizing: Boolean,
     ): Map<UserAction, UserActionResult> {
         val up =
             when {
@@ -131,7 +135,9 @@
         val down = Scenes.QuickSettings.takeIf { shadeMode is ShadeMode.Single }
 
         return buildMap {
-            this[Swipe(SwipeDirection.Up)] = UserActionResult(up)
+            if (!isCustomizing) {
+                this[Swipe(SwipeDirection.Up)] = UserActionResult(up)
+            } // TODO(b/330200163) Add an else to be able to collapse the shade while customizing
             down?.let { this[Swipe(SwipeDirection.Down)] = UserActionResult(down) }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index 4406813..e7b159a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -529,9 +529,9 @@
         default void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode) {}
 
         /**
-         * @see IStatusBar#enterDesktop(int)
+         * @see IStatusBar#moveFocusedTaskToDesktop(int)
          */
-        default void enterDesktop(int displayId) {}
+        default void moveFocusedTaskToDesktop(int displayId) {}
     }
 
     @VisibleForTesting
@@ -1444,7 +1444,7 @@
     }
 
     @Override
-    public void enterDesktop(int displayId) {
+    public void moveFocusedTaskToDesktop(int displayId) {
         SomeArgs args = SomeArgs.obtain();
         args.arg1 = displayId;
         mHandler.obtainMessage(MSG_ENTER_DESKTOP, args).sendToTarget();
@@ -1960,7 +1960,7 @@
                     args = (SomeArgs) msg.obj;
                     int displayId = args.argi1;
                     for (int i = 0; i < mCallbacks.size(); i++) {
-                        mCallbacks.get(i).enterDesktop(displayId);
+                        mCallbacks.get(i).moveFocusedTaskToDesktop(displayId);
                     }
                     break;
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
index a12b970..d6858ca 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
@@ -560,11 +560,6 @@
                                 Pair.create(
                                         KeyEvent.KEYCODE_TAB,
                                         KeyEvent.META_SHIFT_ON | KeyEvent.META_ALT_ON))),
-                /* Hide and (re)show taskbar: Meta + T */
-                new ShortcutKeyGroupMultiMappingInfo(
-                        context.getString(R.string.group_system_hide_reshow_taskbar),
-                        Arrays.asList(
-                                Pair.create(KeyEvent.KEYCODE_T, KeyEvent.META_META_ON))),
                 /* Access notification shade: Meta + N */
                 new ShortcutKeyGroupMultiMappingInfo(
                         context.getString(R.string.group_system_access_notification_shade),
@@ -636,34 +631,41 @@
         //    Enter Split screen with current app to RHS: Meta + Ctrl + Right arrow
         //    Enter Split screen with current app to LHS: Meta + Ctrl + Left arrow
         //    Switch from Split screen to full screen: Meta + Ctrl + Up arrow
-        String[] shortcutLabels = {
-                context.getString(R.string.system_multitasking_rhs),
-                context.getString(R.string.system_multitasking_lhs),
-                context.getString(R.string.system_multitasking_full_screen),
-        };
-        int[] keyCodes = {
-                KeyEvent.KEYCODE_DPAD_RIGHT,
-                KeyEvent.KEYCODE_DPAD_LEFT,
-                KeyEvent.KEYCODE_DPAD_UP,
-        };
-
-        for (int i = 0; i < shortcutLabels.length; i++) {
-            List<ShortcutKeyGroup> shortcutKeyGroups = Arrays.asList(new ShortcutKeyGroup(
-                    new KeyboardShortcutInfo(
-                            shortcutLabels[i],
-                            keyCodes[i],
-                            KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON),
-                    null));
-            ShortcutMultiMappingInfo shortcutMultiMappingInfo =
-                    new ShortcutMultiMappingInfo(
-                            shortcutLabels[i],
-                            null,
-                            shortcutKeyGroups);
-            systemMultitaskingGroup.addItem(shortcutMultiMappingInfo);
-        }
+        //    Change split screen focus to RHS: Meta + Alt + Right arrow
+        //    Change split screen focus to LHS: Meta + Alt + Left arrow
+        systemMultitaskingGroup.addItem(
+                getMultitaskingShortcut(context.getString(R.string.system_multitasking_rhs),
+                        KeyEvent.KEYCODE_DPAD_RIGHT,
+                        KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON));
+        systemMultitaskingGroup.addItem(
+                getMultitaskingShortcut(context.getString(R.string.system_multitasking_lhs),
+                        KeyEvent.KEYCODE_DPAD_LEFT,
+                        KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON));
+        systemMultitaskingGroup.addItem(
+                getMultitaskingShortcut(context.getString(R.string.system_multitasking_full_screen),
+                        KeyEvent.KEYCODE_DPAD_UP,
+                        KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON));
+        systemMultitaskingGroup.addItem(
+                getMultitaskingShortcut(
+                        context.getString(R.string.system_multitasking_splitscreen_focus_rhs),
+                        KeyEvent.KEYCODE_DPAD_RIGHT,
+                        KeyEvent.META_META_ON | KeyEvent.META_ALT_ON));
+        systemMultitaskingGroup.addItem(
+                getMultitaskingShortcut(
+                        context.getString(R.string.system_multitasking_splitscreen_focus_lhs),
+                        KeyEvent.KEYCODE_DPAD_LEFT,
+                        KeyEvent.META_META_ON | KeyEvent.META_ALT_ON));
         return systemMultitaskingGroup;
     }
 
+    private static ShortcutMultiMappingInfo getMultitaskingShortcut(String shortcutLabel,
+            int keycode, int modifiers) {
+        List<ShortcutKeyGroup> shortcutKeyGroups = Arrays.asList(
+                new ShortcutKeyGroup(new KeyboardShortcutInfo(shortcutLabel, keycode, modifiers),
+                        null));
+        return new ShortcutMultiMappingInfo(shortcutLabel, null, shortcutKeyGroups);
+    }
+
     private static KeyboardShortcutMultiMappingGroup getMultiMappingInputShortcuts(
             Context context) {
         List<ShortcutMultiMappingInfo> shortcutMultiMappingInfoList = Arrays.asList(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
index 4b16126..d974bc44 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
@@ -15,13 +15,13 @@
 import com.android.systemui.Dumpable
 import com.android.systemui.ExpandHelper
 import com.android.systemui.Flags.nsslFalsingFix
-import com.android.systemui.Flags.migrateClocksToBlueprint
 import com.android.systemui.Gefingerpoken
 import com.android.systemui.biometrics.UdfpsKeyguardViewControllerLegacy
 import com.android.systemui.classifier.Classifier
 import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.keyguard.WakefulnessLifecycle
 import com.android.systemui.keyguard.domain.interactor.NaturalScrollingSettingObserver
 import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager
@@ -69,7 +69,7 @@
     private val mediaHierarchyManager: MediaHierarchyManager,
     private val scrimTransitionController: LockscreenShadeScrimTransitionController,
     private val keyguardTransitionControllerFactory:
-    LockscreenShadeKeyguardTransitionController.Factory,
+        LockscreenShadeKeyguardTransitionController.Factory,
     private val depthController: NotificationShadeDepthController,
     private val context: Context,
     private val splitShadeOverScrollerFactory: SplitShadeLockScreenOverScroller.Factory,
@@ -292,8 +292,7 @@
     /** @return true if the interaction is accepted, false if it should be cancelled */
     internal fun canDragDown(): Boolean {
         return (statusBarStateController.state == StatusBarState.KEYGUARD ||
-            nsslController.isInLockedDownShade()) &&
-                (isQsFullyCollapsed || useSplitShade)
+            nsslController.isInLockedDownShade()) && (isQsFullyCollapsed || useSplitShade)
     }
 
     /** Called by the touch helper when when a gesture has completed all the way and released. */
@@ -885,7 +884,7 @@
                     isDraggingDown = false
                     isTrackpadReverseScroll = false
                     shadeRepository.setLegacyLockscreenShadeTracking(false)
-                    if (nsslFalsingFix() || migrateClocksToBlueprint()) {
+                    if (nsslFalsingFix() || MigrateClocksToBlueprint.isEnabled) {
                         return true
                     }
                 } else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
index 5171a5c..9a82ecf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
@@ -863,7 +863,7 @@
         boolean isAppearing = row.isDrawingAppearAnimation() && !row.isInShelf();
         iconState.hidden = isAppearing
                 || (view instanceof ExpandableNotificationRow
-                && ((ExpandableNotificationRow) view).isLowPriority()
+                && ((ExpandableNotificationRow) view).isMinimized()
                 && mShelfIcons.areIconsOverflowing())
                 || (transitionAmount == 0.0f && !iconState.isAnimating(icon))
                 || row.isAboveShelf()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
index e111525..8cdf60b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
@@ -42,7 +42,6 @@
 import android.app.RemoteInput;
 import android.app.RemoteInputHistoryItem;
 import android.content.Context;
-import android.content.pm.ShortcutInfo;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Parcelable;
@@ -133,7 +132,6 @@
     public Uri remoteInputUri;
     public ContentInfo remoteInputAttachment;
     private Notification.BubbleMetadata mBubbleMetadata;
-    private ShortcutInfo mShortcutInfo;
 
     /**
      * If {@link RemoteInput#getEditChoicesBeforeSending} is enabled, and the user is
@@ -168,10 +166,8 @@
     private ListenerSet<OnSensitivityChangedListener> mOnSensitivityChangedListeners =
             new ListenerSet<>();
 
-    private boolean mAutoHeadsUp;
     private boolean mPulseSupressed;
     private int mBucket = BUCKET_ALERTING;
-    @Nullable private Long mPendingAnimationDuration;
     private boolean mIsMarkedForUserTriggeredMovement;
     private boolean mIsHeadsUpEntry;
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java
index dcfccd8..0bbde21 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java
@@ -16,7 +16,7 @@
 
 package com.android.systemui.statusbar.notification.collection.coordinator;
 
-import static com.android.systemui.media.controls.domain.pipeline.MediaDataManagerKt.isMediaNotification;
+import static com.android.systemui.media.controls.domain.pipeline.MediaDataManager.isMediaNotification;
 
 import android.os.RemoteException;
 import android.service.notification.StatusBarNotification;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
index dfb0f9b..7a7b184 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
@@ -363,7 +363,7 @@
 
     NotifInflater.Params getInflaterParams(NotifUiAdjustment adjustment, String reason) {
         return new NotifInflater.Params(
-                /* isLowPriority = */ adjustment.isMinimized(),
+                /* isMinimized = */ adjustment.isMinimized(),
                 /* reason = */ reason,
                 /* showSnooze = */ adjustment.isSnoozeEnabled(),
                 /* isChildInGroup = */ adjustment.isChildInGroup(),
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt
index 7b8a062..ff72888 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt
@@ -56,7 +56,7 @@
 
     /** A class holding parameters used when inflating the notification row */
     class Params(
-        val isLowPriority: Boolean,
+        val isMinimized: Boolean,
         val reason: String,
         val showSnooze: Boolean,
         val isChildInGroup: Boolean = false,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
index 4bbe035..4a895c0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
@@ -243,7 +243,7 @@
             @Nullable NotificationRowContentBinder.InflationCallback inflationCallback) {
         final boolean useIncreasedCollapsedHeight =
                 mMessagingUtil.isImportantMessaging(entry.getSbn(), entry.getImportance());
-        final boolean isLowPriority = inflaterParams.isLowPriority();
+        final boolean isMinimized = inflaterParams.isMinimized();
 
         // Set show snooze action
         row.setShowSnooze(inflaterParams.getShowSnooze());
@@ -252,7 +252,7 @@
         params.requireContentViews(FLAG_CONTENT_VIEW_CONTRACTED);
         params.requireContentViews(FLAG_CONTENT_VIEW_EXPANDED);
         params.setUseIncreasedCollapsedHeight(useIncreasedCollapsedHeight);
-        params.setUseLowPriority(isLowPriority);
+        params.setUseMinimized(isMinimized);
 
         if (screenshareNotificationHiding()
                 ? inflaterParams.getNeedsRedaction()
@@ -275,7 +275,7 @@
         if (AsyncGroupHeaderViewInflation.isEnabled()) {
             if (inflaterParams.isGroupSummary()) {
                 params.requireContentViews(FLAG_GROUP_SUMMARY_HEADER);
-                if (isLowPriority) {
+                if (isMinimized) {
                     params.requireContentViews(FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER);
                 }
             } else {
@@ -288,7 +288,7 @@
         mRowContentBindStage.requestRebind(entry, en -> {
             mLogger.logRebindComplete(entry);
             row.setUsesIncreasedCollapsedHeight(useIncreasedCollapsedHeight);
-            row.setIsLowPriority(isLowPriority);
+            row.setIsMinimized(isMinimized);
             if (inflationCallback != null) {
                 inflationCallback.onAsyncInflationFinished(en);
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index c05c3c3..b8b4a03 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -327,7 +327,7 @@
     private OnClickListener mExpandClickListener = new OnClickListener() {
         @Override
         public void onClick(View v) {
-            if (!shouldShowPublic() && (!mIsLowPriority || isExpanded())
+            if (!shouldShowPublic() && (!mIsMinimized || isExpanded())
                     && mGroupMembershipManager.isGroupSummary(mEntry)) {
                 mGroupExpansionChanging = true;
                 final boolean wasExpanded = mGroupExpansionManager.isGroupExpanded(mEntry);
@@ -382,7 +382,7 @@
     private boolean mAboveShelf;
     private OnUserInteractionCallback mOnUserInteractionCallback;
     private NotificationGutsManager mNotificationGutsManager;
-    private boolean mIsLowPriority;
+    private boolean mIsMinimized;
     private boolean mUseIncreasedCollapsedHeight;
     private boolean mUseIncreasedHeadsUpHeight;
     private float mTranslationWhenRemoved;
@@ -467,7 +467,8 @@
             if (viewWrapper != null) {
                 setIconAnimationRunningForChild(running, viewWrapper.getIcon());
             }
-            NotificationViewWrapper lowPriWrapper = mChildrenContainer.getLowPriorityViewWrapper();
+            NotificationViewWrapper lowPriWrapper = mChildrenContainer
+                    .getMinimizedGroupHeaderWrapper();
             if (lowPriWrapper != null) {
                 setIconAnimationRunningForChild(running, lowPriWrapper.getIcon());
             }
@@ -680,7 +681,7 @@
         if (color != Notification.COLOR_INVALID) {
             return color;
         } else {
-            return mEntry.getContrastedColor(mContext, mIsLowPriority && !isExpanded(),
+            return mEntry.getContrastedColor(mContext, mIsMinimized && !isExpanded(),
                     getBackgroundColorWithoutTint());
         }
     }
@@ -1545,7 +1546,7 @@
      * Set the low-priority group notification header view
      * @param headerView header view to set
      */
-    public void setLowPriorityGroupHeader(NotificationHeaderView headerView) {
+    public void setMinimizedGroupHeader(NotificationHeaderView headerView) {
         NotificationChildrenContainer childrenContainer = getChildrenContainerNonNull();
         childrenContainer.setLowPriorityGroupHeader(
                 /* headerViewLowPriority= */ headerView,
@@ -1664,16 +1665,19 @@
         }
     }
 
-    public void setIsLowPriority(boolean isLowPriority) {
-        mIsLowPriority = isLowPriority;
-        mPrivateLayout.setIsLowPriority(isLowPriority);
+    /**
+     * Set if the row is minimized.
+     */
+    public void setIsMinimized(boolean isMinimized) {
+        mIsMinimized = isMinimized;
+        mPrivateLayout.setIsLowPriority(isMinimized);
         if (mChildrenContainer != null) {
-            mChildrenContainer.setIsLowPriority(isLowPriority);
+            mChildrenContainer.setIsMinimized(isMinimized);
         }
     }
 
-    public boolean isLowPriority() {
-        return mIsLowPriority;
+    public boolean isMinimized() {
+        return mIsMinimized;
     }
 
     public void setUsesIncreasedCollapsedHeight(boolean use) {
@@ -2050,7 +2054,7 @@
         mChildrenContainerStub = findViewById(R.id.child_container_stub);
         mChildrenContainerStub.setOnInflateListener((stub, inflated) -> {
             mChildrenContainer = (NotificationChildrenContainer) inflated;
-            mChildrenContainer.setIsLowPriority(mIsLowPriority);
+            mChildrenContainer.setIsMinimized(mIsMinimized);
             mChildrenContainer.setContainingNotification(ExpandableNotificationRow.this);
             mChildrenContainer.onNotificationUpdated();
             mChildrenContainer.setLogger(mChildrenContainerLogger);
@@ -3435,7 +3439,7 @@
 
     private void onExpansionChanged(boolean userAction, boolean wasExpanded) {
         boolean nowExpanded = isExpanded();
-        if (mIsSummaryWithChildren && (!mIsLowPriority || wasExpanded)) {
+        if (mIsSummaryWithChildren && (!mIsMinimized || wasExpanded)) {
             nowExpanded = mGroupExpansionManager.isGroupExpanded(mEntry);
         }
         if (nowExpanded != wasExpanded) {
@@ -3492,7 +3496,7 @@
         if (!expandable) {
             if (mIsSummaryWithChildren) {
                 expandable = true;
-                if (!mIsLowPriority || isExpanded()) {
+                if (!mIsMinimized || isExpanded()) {
                     isExpanded = isGroupExpanded();
                 }
             } else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java
index 6bc2b2f..ba1cfcc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java
@@ -30,14 +30,21 @@
 import android.widget.TextView;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.widget.ConversationAvatarData;
+import com.android.internal.widget.ConversationAvatarData.GroupConversationAvatarData;
+import com.android.internal.widget.ConversationAvatarData.OneToOneConversationAvatarData;
+import com.android.internal.widget.ConversationHeaderData;
 import com.android.internal.widget.ConversationLayout;
 import com.android.systemui.res.R;
 import com.android.systemui.statusbar.notification.NotificationFadeAware;
 import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation;
+import com.android.systemui.statusbar.notification.row.shared.ConversationStyleSetAvatarAsync;
 import com.android.systemui.statusbar.notification.row.ui.viewmodel.ConversationAvatar;
 import com.android.systemui.statusbar.notification.row.ui.viewmodel.FacePile;
 import com.android.systemui.statusbar.notification.row.ui.viewmodel.SingleIcon;
 
+import java.util.Objects;
+
 /**
  * A hybrid view which may contain information about one ore more conversations.
  */
@@ -103,7 +110,7 @@
 
     @Override
     public void bind(@Nullable CharSequence title, @Nullable CharSequence text,
-                     @Nullable View contentView) {
+            @Nullable View contentView) {
         AsyncHybridViewInflation.assertInLegacyMode();
         if (!(contentView instanceof ConversationLayout)) {
             super.bind(title, text, contentView);
@@ -111,34 +118,7 @@
         }
 
         ConversationLayout conversationLayout = (ConversationLayout) contentView;
-        Icon conversationIcon = conversationLayout.getConversationIcon();
-        if (conversationIcon != null) {
-            mConversationFacePile.setVisibility(GONE);
-            mConversationIconView.setVisibility(VISIBLE);
-            mConversationIconView.setImageIcon(conversationIcon);
-            setSize(mConversationIconView, mSingleAvatarSize);
-        } else {
-            // If there isn't an icon, generate a "face pile" based on the sender avatars
-            mConversationIconView.setVisibility(GONE);
-            mConversationFacePile.setVisibility(VISIBLE);
-
-            mConversationFacePile =
-                    requireViewById(com.android.internal.R.id.conversation_face_pile);
-            ImageView facePileBottomBg = mConversationFacePile.requireViewById(
-                    com.android.internal.R.id.conversation_face_pile_bottom_background);
-            ImageView facePileBottom = mConversationFacePile.requireViewById(
-                    com.android.internal.R.id.conversation_face_pile_bottom);
-            ImageView facePileTop = mConversationFacePile.requireViewById(
-                    com.android.internal.R.id.conversation_face_pile_top);
-            conversationLayout.bindFacePile(facePileBottomBg, facePileBottom, facePileTop);
-            setSize(mConversationFacePile, mFacePileSize);
-            setSize(facePileBottom, mFacePileAvatarSize);
-            setSize(facePileTop, mFacePileAvatarSize);
-            setSize(facePileBottomBg, mFacePileAvatarSize + 2 * mFacePileProtectionWidth);
-            mTransformationHelper.addViewTransformingToSimilar(facePileTop);
-            mTransformationHelper.addViewTransformingToSimilar(facePileBottom);
-            mTransformationHelper.addViewTransformingToSimilar(facePileBottomBg);
-        }
+        loadConversationAvatar(conversationLayout);
         CharSequence conversationTitle = conversationLayout.getConversationTitle();
         if (TextUtils.isEmpty(conversationTitle)) {
             conversationTitle = title;
@@ -156,6 +136,90 @@
         super.bind(conversationTitle, conversationText, conversationLayout);
     }
 
+    private void loadConversationAvatar(ConversationLayout conversationLayout) {
+        AsyncHybridViewInflation.assertInLegacyMode();
+        if (ConversationStyleSetAvatarAsync.isEnabled()) {
+            loadConversationAvatarWithDrawable(conversationLayout);
+        } else {
+            loadConversationAvatarWithIcon(conversationLayout);
+        }
+    }
+
+    @Deprecated
+    private void loadConversationAvatarWithIcon(ConversationLayout conversationLayout) {
+        ConversationStyleSetAvatarAsync.assertInLegacyMode();
+        AsyncHybridViewInflation.assertInLegacyMode();
+        final Icon conversationIcon = conversationLayout.getConversationIcon();
+        if (conversationIcon != null) {
+            mConversationFacePile.setVisibility(GONE);
+            mConversationIconView.setVisibility(VISIBLE);
+            mConversationIconView.setImageIcon(conversationIcon);
+            setSize(mConversationIconView, mSingleAvatarSize);
+        } else {
+            // If there isn't an icon, generate a "face pile" based on the sender avatars
+            mConversationIconView.setVisibility(GONE);
+            mConversationFacePile.setVisibility(VISIBLE);
+
+            mConversationFacePile =
+                    requireViewById(com.android.internal.R.id.conversation_face_pile);
+            final ImageView facePileBottomBg = mConversationFacePile.requireViewById(
+                    com.android.internal.R.id.conversation_face_pile_bottom_background);
+            final ImageView facePileBottom = mConversationFacePile.requireViewById(
+                    com.android.internal.R.id.conversation_face_pile_bottom);
+            final ImageView facePileTop = mConversationFacePile.requireViewById(
+                    com.android.internal.R.id.conversation_face_pile_top);
+            conversationLayout.bindFacePile(facePileBottomBg, facePileBottom, facePileTop);
+            setSize(mConversationFacePile, mFacePileSize);
+            setSize(facePileBottom, mFacePileAvatarSize);
+            setSize(facePileTop, mFacePileAvatarSize);
+            setSize(facePileBottomBg, mFacePileAvatarSize + 2 * mFacePileProtectionWidth);
+            mTransformationHelper.addViewTransformingToSimilar(facePileTop);
+            mTransformationHelper.addViewTransformingToSimilar(facePileBottom);
+            mTransformationHelper.addViewTransformingToSimilar(facePileBottomBg);
+        }
+    }
+
+    private void loadConversationAvatarWithDrawable(ConversationLayout conversationLayout) {
+        AsyncHybridViewInflation.assertInLegacyMode();
+        final ConversationHeaderData conversationHeaderData = Objects.requireNonNull(
+                conversationLayout.getConversationHeaderData(),
+                /* message = */ "conversationHeaderData should not be null");
+        final ConversationAvatarData conversationAvatar =
+                Objects.requireNonNull(conversationHeaderData.getConversationAvatar(),
+                        /* message = */"conversationAvatar should not be null");
+
+        if (conversationAvatar instanceof OneToOneConversationAvatarData oneToOneAvatar) {
+            mConversationFacePile.setVisibility(GONE);
+            mConversationIconView.setVisibility(VISIBLE);
+            mConversationIconView.setImageDrawable(oneToOneAvatar.mDrawable);
+            setSize(mConversationIconView, mSingleAvatarSize);
+        } else {
+            // If there isn't an icon, generate a "face pile" based on the sender avatars
+            mConversationIconView.setVisibility(GONE);
+            mConversationFacePile.setVisibility(VISIBLE);
+
+            final GroupConversationAvatarData groupAvatar =
+                    (GroupConversationAvatarData) conversationAvatar;
+            mConversationFacePile =
+                    requireViewById(com.android.internal.R.id.conversation_face_pile);
+            final ImageView facePileBottomBg = mConversationFacePile.requireViewById(
+                    com.android.internal.R.id.conversation_face_pile_bottom_background);
+            final ImageView facePileBottom = mConversationFacePile.requireViewById(
+                    com.android.internal.R.id.conversation_face_pile_bottom);
+            final ImageView facePileTop = mConversationFacePile.requireViewById(
+                    com.android.internal.R.id.conversation_face_pile_top);
+            conversationLayout.bindFacePileWithDrawable(facePileBottomBg, facePileBottom,
+                    facePileTop, groupAvatar);
+            setSize(mConversationFacePile, mFacePileSize);
+            setSize(facePileBottom, mFacePileAvatarSize);
+            setSize(facePileTop, mFacePileAvatarSize);
+            setSize(facePileBottomBg, mFacePileAvatarSize + 2 * mFacePileProtectionWidth);
+            mTransformationHelper.addViewTransformingToSimilar(facePileTop);
+            mTransformationHelper.addViewTransformingToSimilar(facePileBottom);
+            mTransformationHelper.addViewTransformingToSimilar(facePileBottomBg);
+        }
+    }
+
     /**
      * Set the avatar using ConversationAvatar from SingleLineViewModel
      *
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
index f835cca..ded635c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
@@ -150,7 +150,7 @@
                 entry,
                 mConversationProcessor,
                 row,
-                bindParams.isLowPriority,
+                bindParams.isMinimized,
                 bindParams.usesIncreasedHeight,
                 bindParams.usesIncreasedHeadsUpHeight,
                 callback,
@@ -178,7 +178,7 @@
             SmartReplyStateInflater smartRepliesInflater) {
         InflationProgress result = createRemoteViews(reInflateFlags,
                 builder,
-                bindParams.isLowPriority,
+                bindParams.isMinimized,
                 bindParams.usesIncreasedHeight,
                 bindParams.usesIncreasedHeadsUpHeight,
                 packageContext,
@@ -215,6 +215,7 @@
         apply(
                 mInflationExecutor,
                 inflateSynchronously,
+                bindParams.isMinimized,
                 result,
                 reInflateFlags,
                 mRemoteViewCache,
@@ -365,7 +366,7 @@
     }
 
     private static InflationProgress createRemoteViews(@InflationFlag int reInflateFlags,
-            Notification.Builder builder, boolean isLowPriority, boolean usesIncreasedHeight,
+            Notification.Builder builder, boolean isMinimized, boolean usesIncreasedHeight,
             boolean usesIncreasedHeadsUpHeight, Context packageContext,
             ExpandableNotificationRow row,
             NotifLayoutInflaterFactory.Provider notifLayoutInflaterFactoryProvider,
@@ -376,13 +377,13 @@
 
             if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) {
                 logger.logAsyncTaskProgress(entryForLogging, "creating contracted remote view");
-                result.newContentView = createContentView(builder, isLowPriority,
+                result.newContentView = createContentView(builder, isMinimized,
                         usesIncreasedHeight);
             }
 
             if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0) {
                 logger.logAsyncTaskProgress(entryForLogging, "creating expanded remote view");
-                result.newExpandedView = createExpandedView(builder, isLowPriority);
+                result.newExpandedView = createExpandedView(builder, isMinimized);
             }
 
             if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0) {
@@ -393,7 +394,7 @@
 
             if ((reInflateFlags & FLAG_CONTENT_VIEW_PUBLIC) != 0) {
                 logger.logAsyncTaskProgress(entryForLogging, "creating public remote view");
-                result.newPublicView = builder.makePublicContentView(isLowPriority);
+                result.newPublicView = builder.makePublicContentView(isMinimized);
             }
 
             if (AsyncGroupHeaderViewInflation.isEnabled()) {
@@ -406,7 +407,7 @@
                 if ((reInflateFlags & FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER) != 0) {
                     logger.logAsyncTaskProgress(entryForLogging,
                             "creating low-priority group summary remote view");
-                    result.mNewLowPriorityGroupHeaderView =
+                    result.mNewMinimizedGroupHeaderView =
                             builder.makeLowPriorityContentView(true /* useRegularSubtext */);
                 }
             }
@@ -444,6 +445,7 @@
     private static CancellationSignal apply(
             Executor inflationExecutor,
             boolean inflateSynchronously,
+            boolean isMinimized,
             InflationProgress result,
             @InflationFlag int reInflateFlags,
             NotifRemoteViewCache remoteViewCache,
@@ -475,7 +477,8 @@
                 }
             };
             logger.logAsyncTaskProgress(entry, "applying contracted view");
-            applyRemoteView(inflationExecutor, inflateSynchronously, result, reInflateFlags, flag,
+            applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized, result,
+                    reInflateFlags, flag,
                     remoteViewCache, entry, row, isNewView, remoteViewClickHandler, callback,
                     privateLayout, privateLayout.getContractedChild(),
                     privateLayout.getVisibleWrapper(
@@ -502,7 +505,8 @@
                     }
                 };
                 logger.logAsyncTaskProgress(entry, "applying expanded view");
-                applyRemoteView(inflationExecutor, inflateSynchronously, result, reInflateFlags,
+                applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized, result,
+                        reInflateFlags,
                         flag, remoteViewCache, entry, row, isNewView, remoteViewClickHandler,
                         callback, privateLayout, privateLayout.getExpandedChild(),
                         privateLayout.getVisibleWrapper(VISIBLE_TYPE_EXPANDED), runningInflations,
@@ -529,7 +533,8 @@
                     }
                 };
                 logger.logAsyncTaskProgress(entry, "applying heads up view");
-                applyRemoteView(inflationExecutor, inflateSynchronously, result, reInflateFlags,
+                applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized,
+                        result, reInflateFlags,
                         flag, remoteViewCache, entry, row, isNewView, remoteViewClickHandler,
                         callback, privateLayout, privateLayout.getHeadsUpChild(),
                         privateLayout.getVisibleWrapper(VISIBLE_TYPE_HEADSUP), runningInflations,
@@ -555,7 +560,8 @@
                 }
             };
             logger.logAsyncTaskProgress(entry, "applying public view");
-            applyRemoteView(inflationExecutor, inflateSynchronously, result, reInflateFlags, flag,
+            applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized,
+                    result, reInflateFlags, flag,
                     remoteViewCache, entry, row, isNewView, remoteViewClickHandler, callback,
                     publicLayout, publicLayout.getContractedChild(),
                     publicLayout.getVisibleWrapper(NotificationContentView.VISIBLE_TYPE_CONTRACTED),
@@ -583,11 +589,12 @@
                     }
                 };
                 logger.logAsyncTaskProgress(entry, "applying group header view");
-                applyRemoteView(inflationExecutor, inflateSynchronously, result, reInflateFlags,
+                applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized,
+                        result, reInflateFlags,
                         /* inflationId = */ FLAG_GROUP_SUMMARY_HEADER,
                         remoteViewCache, entry, row, isNewView, remoteViewClickHandler, callback,
                         /* parentLayout = */ childrenContainer,
-                        /* existingView = */ childrenContainer.getNotificationHeader(),
+                        /* existingView = */ childrenContainer.getGroupHeader(),
                         /* existingWrapper = */ childrenContainer.getNotificationHeaderWrapper(),
                         runningInflations, applyCallback, logger);
             }
@@ -595,7 +602,7 @@
             if ((reInflateFlags & FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER) != 0) {
                 boolean isNewView =
                         !canReapplyRemoteView(
-                                /* newView = */ result.mNewLowPriorityGroupHeaderView,
+                                /* newView = */ result.mNewMinimizedGroupHeaderView,
                                 /* oldView = */ remoteViewCache.getCachedView(
                                         entry, FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER));
                 ApplyCallback applyCallback = new ApplyCallback() {
@@ -603,29 +610,30 @@
                     public void setResultView(View v) {
                         logger.logAsyncTaskProgress(entry,
                                 "low-priority group header view applied");
-                        result.mInflatedLowPriorityGroupHeaderView = (NotificationHeaderView) v;
+                        result.mInflatedMinimizedGroupHeaderView = (NotificationHeaderView) v;
                     }
 
                     @Override
                     public RemoteViews getRemoteView() {
-                        return result.mNewLowPriorityGroupHeaderView;
+                        return result.mNewMinimizedGroupHeaderView;
                     }
                 };
                 logger.logAsyncTaskProgress(entry, "applying low priority group header view");
-                applyRemoteView(inflationExecutor, inflateSynchronously, result, reInflateFlags,
+                applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized,
+                        result, reInflateFlags,
                         /* inflationId = */ FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER,
                         remoteViewCache, entry, row, isNewView, remoteViewClickHandler, callback,
                         /* parentLayout = */ childrenContainer,
-                        /* existingView = */ childrenContainer.getNotificationHeaderLowPriority(),
+                        /* existingView = */ childrenContainer.getMinimizedNotificationHeader(),
                         /* existingWrapper = */ childrenContainer
-                                .getLowPriorityViewWrapper(),
+                                .getMinimizedGroupHeaderWrapper(),
                         runningInflations, applyCallback, logger);
             }
         }
 
         // Let's try to finish, maybe nobody is even inflating anything
-        finishIfDone(result, reInflateFlags, remoteViewCache, runningInflations, callback, entry,
-                row, logger);
+        finishIfDone(result, isMinimized, reInflateFlags, remoteViewCache, runningInflations,
+                callback, entry, row, logger);
         CancellationSignal cancellationSignal = new CancellationSignal();
         cancellationSignal.setOnCancelListener(
                 () -> {
@@ -641,6 +649,7 @@
     static void applyRemoteView(
             Executor inflationExecutor,
             boolean inflateSynchronously,
+            boolean isMinimized,
             final InflationProgress result,
             final @InflationFlag int reInflateFlags,
             @InflationFlag int inflationId,
@@ -707,7 +716,8 @@
                     existingWrapper.onReinflated();
                 }
                 runningInflations.remove(inflationId);
-                finishIfDone(result, reInflateFlags, remoteViewCache, runningInflations,
+                finishIfDone(result, isMinimized,
+                        reInflateFlags, remoteViewCache, runningInflations,
                         callback, entry, row, logger);
             }
 
@@ -838,6 +848,7 @@
      * @return true if the inflation was finished
      */
     private static boolean finishIfDone(InflationProgress result,
+            boolean isMinimized,
             @InflationFlag int reInflateFlags, NotifRemoteViewCache remoteViewCache,
             HashMap<Integer, CancellationSignal> runningInflations,
             @Nullable InflationCallback endListener, NotificationEntry entry,
@@ -944,7 +955,9 @@
         if (AsyncGroupHeaderViewInflation.isEnabled()) {
             if ((reInflateFlags & FLAG_GROUP_SUMMARY_HEADER) != 0) {
                 if (result.mInflatedGroupHeaderView != null) {
-                    row.setIsLowPriority(false);
+                    // We need to set if the row is minimized before setting the group header to
+                    // make sure the setting of header view works correctly
+                    row.setIsMinimized(isMinimized);
                     row.setGroupHeader(/* headerView= */ result.mInflatedGroupHeaderView);
                     remoteViewCache.putCachedView(entry, FLAG_GROUP_SUMMARY_HEADER,
                             result.mNewGroupHeaderView);
@@ -957,13 +970,14 @@
             }
 
             if ((reInflateFlags & FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER) != 0) {
-                if (result.mInflatedLowPriorityGroupHeaderView != null) {
-                    // New view case, set row to low priority
-                    row.setIsLowPriority(true);
-                    row.setLowPriorityGroupHeader(
-                            /* headerView= */ result.mInflatedLowPriorityGroupHeaderView);
+                if (result.mInflatedMinimizedGroupHeaderView != null) {
+                    // We need to set if the row is minimized before setting the group header to
+                    // make sure the setting of header view works correctly
+                    row.setIsMinimized(isMinimized);
+                    row.setMinimizedGroupHeader(
+                            /* headerView= */ result.mInflatedMinimizedGroupHeaderView);
                     remoteViewCache.putCachedView(entry, FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER,
-                            result.mNewLowPriorityGroupHeaderView);
+                            result.mNewMinimizedGroupHeaderView);
                 } else if (remoteViewCache.hasCachedView(entry,
                         FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER)) {
                     // Re-inflation case. Only update if it's still cached (i.e. view has not
@@ -984,12 +998,12 @@
     }
 
     private static RemoteViews createExpandedView(Notification.Builder builder,
-            boolean isLowPriority) {
+            boolean isMinimized) {
         RemoteViews bigContentView = builder.createBigContentView();
         if (bigContentView != null) {
             return bigContentView;
         }
-        if (isLowPriority) {
+        if (isMinimized) {
             RemoteViews contentView = builder.createContentView();
             Notification.Builder.makeHeaderExpanded(contentView);
             return contentView;
@@ -998,8 +1012,8 @@
     }
 
     private static RemoteViews createContentView(Notification.Builder builder,
-            boolean isLowPriority, boolean useLarge) {
-        if (isLowPriority) {
+            boolean isMinimized, boolean useLarge) {
+        if (isMinimized) {
             return builder.makeLowPriorityContentView(false /* useRegularSubtext */);
         }
         return builder.createContentView(useLarge);
@@ -1038,7 +1052,7 @@
         private final NotificationEntry mEntry;
         private final Context mContext;
         private final boolean mInflateSynchronously;
-        private final boolean mIsLowPriority;
+        private final boolean mIsMinimized;
         private final boolean mUsesIncreasedHeight;
         private final InflationCallback mCallback;
         private final boolean mUsesIncreasedHeadsUpHeight;
@@ -1063,7 +1077,7 @@
                 NotificationEntry entry,
                 ConversationNotificationProcessor conversationProcessor,
                 ExpandableNotificationRow row,
-                boolean isLowPriority,
+                boolean isMinimized,
                 boolean usesIncreasedHeight,
                 boolean usesIncreasedHeadsUpHeight,
                 InflationCallback callback,
@@ -1080,7 +1094,7 @@
             mRemoteViewCache = cache;
             mSmartRepliesInflater = smartRepliesInflater;
             mContext = mRow.getContext();
-            mIsLowPriority = isLowPriority;
+            mIsMinimized = isMinimized;
             mUsesIncreasedHeight = usesIncreasedHeight;
             mUsesIncreasedHeadsUpHeight = usesIncreasedHeadsUpHeight;
             mRemoteViewClickHandler = remoteViewClickHandler;
@@ -1150,7 +1164,7 @@
                         mEntry, recoveredBuilder, mLogger);
             }
             InflationProgress inflationProgress = createRemoteViews(mReInflateFlags,
-                    recoveredBuilder, mIsLowPriority, mUsesIncreasedHeight,
+                    recoveredBuilder, mIsMinimized, mUsesIncreasedHeight,
                     mUsesIncreasedHeadsUpHeight, packageContext, mRow,
                     mNotifLayoutInflaterFactoryProvider, mLogger);
 
@@ -1209,6 +1223,7 @@
                 mCancellationSignal = apply(
                         mInflationExecutor,
                         mInflateSynchronously,
+                        mIsMinimized,
                         result,
                         mReInflateFlags,
                         mRemoteViewCache,
@@ -1295,7 +1310,7 @@
         private RemoteViews newExpandedView;
         private RemoteViews newPublicView;
         private RemoteViews mNewGroupHeaderView;
-        private RemoteViews mNewLowPriorityGroupHeaderView;
+        private RemoteViews mNewMinimizedGroupHeaderView;
 
         @VisibleForTesting
         Context packageContext;
@@ -1305,7 +1320,7 @@
         private View inflatedExpandedView;
         private View inflatedPublicView;
         private NotificationHeaderView mInflatedGroupHeaderView;
-        private NotificationHeaderView mInflatedLowPriorityGroupHeaderView;
+        private NotificationHeaderView mInflatedMinimizedGroupHeaderView;
         private CharSequence headsUpStatusBarText;
         private CharSequence headsUpStatusBarTextPublic;
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index 8a3e7e8..6f00d96 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -1514,7 +1514,7 @@
         }
         ImageView bubbleButton = layout.findViewById(com.android.internal.R.id.bubble_button);
         View actionContainer = layout.findViewById(com.android.internal.R.id.actions_container);
-        LinearLayout actionListMarginTarget = layout.findViewById(
+        ViewGroup actionListMarginTarget = layout.findViewById(
                 com.android.internal.R.id.notification_action_list_margin_target);
         if (bubbleButton == null || actionContainer == null) {
             return;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java
index b0fd475..33339a7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java
@@ -128,9 +128,9 @@
     class BindParams {
 
         /**
-         * Bind a low priority version of the content views.
+         * Bind a minimized version of the content views.
          */
-        public boolean isLowPriority;
+        public boolean isMinimized;
 
         /**
          * Use increased height when binding contracted view.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java
index 1494c27..bae89fb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java
@@ -26,7 +26,7 @@
  * Parameters for {@link RowContentBindStage}.
  */
 public final class RowContentBindParams {
-    private boolean mUseLowPriority;
+    private boolean mUseMinimized;
     private boolean mUseIncreasedHeight;
     private boolean mUseIncreasedHeadsUpHeight;
     private boolean mViewsNeedReinflation;
@@ -41,17 +41,20 @@
     private @InflationFlag int mDirtyContentViews = mContentViews;
 
     /**
-     * Set whether content should use a low priority version of its content views.
+     * Set whether content should use a minimized version of its content views.
      */
-    public void setUseLowPriority(boolean useLowPriority) {
-        if (mUseLowPriority != useLowPriority) {
+    public void setUseMinimized(boolean useMinimized) {
+        if (mUseMinimized != useMinimized) {
             mDirtyContentViews |= (FLAG_CONTENT_VIEW_CONTRACTED | FLAG_CONTENT_VIEW_EXPANDED);
         }
-        mUseLowPriority = useLowPriority;
+        mUseMinimized = useMinimized;
     }
 
-    public boolean useLowPriority() {
-        return mUseLowPriority;
+    /**
+     * @return Whether the row uses the minimized style.
+     */
+    public boolean useMinimized() {
+        return mUseMinimized;
     }
 
     /**
@@ -149,9 +152,9 @@
     @Override
     public String toString() {
         return String.format("RowContentBindParams[mContentViews=%x mDirtyContentViews=%x "
-                + "mUseLowPriority=%b mUseIncreasedHeight=%b "
+                + "mUseMinimized=%b mUseIncreasedHeight=%b "
                 + "mUseIncreasedHeadsUpHeight=%b mViewsNeedReinflation=%b]",
-                mContentViews, mDirtyContentViews, mUseLowPriority, mUseIncreasedHeight,
+                mContentViews, mDirtyContentViews, mUseMinimized, mUseIncreasedHeight,
                 mUseIncreasedHeadsUpHeight, mViewsNeedReinflation);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java
index f4f8374..89fcda9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java
@@ -73,7 +73,7 @@
         mBinder.unbindContent(entry, row, contentToUnbind);
 
         BindParams bindParams = new BindParams();
-        bindParams.isLowPriority = params.useLowPriority();
+        bindParams.isMinimized = params.useMinimized();
         bindParams.usesIncreasedHeight = params.useIncreasedHeight();
         bindParams.usesIncreasedHeadsUpHeight = params.useIncreasedHeadsUpHeight();
         boolean forceInflate = params.needsReinflation();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/ConversationStyleSetAvatarAsync.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/ConversationStyleSetAvatarAsync.kt
new file mode 100644
index 0000000..3c056c9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/ConversationStyleSetAvatarAsync.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row.shared
+
+import android.widget.flags.Flags
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading or using the conversation style set avatar async flag state. */
+@Suppress("NOTHING_TO_INLINE")
+object ConversationStyleSetAvatarAsync {
+    const val FLAG_NAME = Flags.FLAG_CONVERSATION_STYLE_SET_AVATAR_ASYNC
+
+    /** A token used for dependency declaration */
+    val token: FlagToken
+        get() = FlagToken(FLAG_NAME, isEnabled)
+
+    /** Is async hybrid (single-line) view inflation enabled */
+    @JvmStatic
+    inline val isEnabled
+        get() = Flags.conversationStyleSetAvatarAsync()
+
+    /**
+     * Called to ensure code is only run when the flag is enabled. This protects users from the
+     * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+     * build to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun isUnexpectedlyInLegacyMode() =
+        RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+    /**
+     * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+     * the flag is enabled to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationsHeadsUpRefactor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationsHeadsUpRefactor.kt
new file mode 100644
index 0000000..62641fe
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationsHeadsUpRefactor.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.statusbar.notification.shared
+
+import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading or using the notifications heads up refactor flag state. */
+@Suppress("NOTHING_TO_INLINE")
+object NotificationsHeadsUpRefactor {
+    /** The aconfig flag name */
+    const val FLAG_NAME = Flags.FLAG_NOTIFICATIONS_HEADS_UP_REFACTOR
+
+    /** A token used for dependency declaration */
+    val token: FlagToken
+        get() = FlagToken(FLAG_NAME, isEnabled)
+
+    /** Is the refactor enabled */
+    @JvmStatic
+    inline val isEnabled
+        get() = Flags.notificationsHeadsUpRefactor()
+
+    /**
+     * Called to ensure code is only run when the flag is enabled. This protects users from the
+     * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+     * build to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun isUnexpectedlyInLegacyMode() =
+        RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+    /**
+     * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+     * the flag is enabled to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
index 28f874d..5dc37e0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
@@ -110,14 +110,14 @@
      */
     private boolean mEnableShadowOnChildNotifications;
 
-    private NotificationHeaderView mNotificationHeader;
-    private NotificationHeaderViewWrapper mNotificationHeaderWrapper;
-    private NotificationHeaderView mNotificationHeaderLowPriority;
-    private NotificationHeaderViewWrapper mNotificationHeaderWrapperLowPriority;
+    private NotificationHeaderView mGroupHeader;
+    private NotificationHeaderViewWrapper mGroupHeaderWrapper;
+    private NotificationHeaderView mMinimizedGroupHeader;
+    private NotificationHeaderViewWrapper mMinimizedGroupHeaderWrapper;
     private NotificationGroupingUtil mGroupingUtil;
     private ViewState mHeaderViewState;
     private int mClipBottomAmount;
-    private boolean mIsLowPriority;
+    private boolean mIsMinimized;
     private OnClickListener mHeaderClickListener;
     private ViewGroup mCurrentHeader;
     private boolean mIsConversation;
@@ -217,14 +217,14 @@
             int right = left + mOverflowNumber.getMeasuredWidth();
             mOverflowNumber.layout(left, 0, right, mOverflowNumber.getMeasuredHeight());
         }
-        if (mNotificationHeader != null) {
-            mNotificationHeader.layout(0, 0, mNotificationHeader.getMeasuredWidth(),
-                    mNotificationHeader.getMeasuredHeight());
+        if (mGroupHeader != null) {
+            mGroupHeader.layout(0, 0, mGroupHeader.getMeasuredWidth(),
+                    mGroupHeader.getMeasuredHeight());
         }
-        if (mNotificationHeaderLowPriority != null) {
-            mNotificationHeaderLowPriority.layout(0, 0,
-                    mNotificationHeaderLowPriority.getMeasuredWidth(),
-                    mNotificationHeaderLowPriority.getMeasuredHeight());
+        if (mMinimizedGroupHeader != null) {
+            mMinimizedGroupHeader.layout(0, 0,
+                    mMinimizedGroupHeader.getMeasuredWidth(),
+                    mMinimizedGroupHeader.getMeasuredHeight());
         }
     }
 
@@ -271,11 +271,11 @@
         }
 
         int headerHeightSpec = MeasureSpec.makeMeasureSpec(mHeaderHeight, MeasureSpec.EXACTLY);
-        if (mNotificationHeader != null) {
-            mNotificationHeader.measure(widthMeasureSpec, headerHeightSpec);
+        if (mGroupHeader != null) {
+            mGroupHeader.measure(widthMeasureSpec, headerHeightSpec);
         }
-        if (mNotificationHeaderLowPriority != null) {
-            mNotificationHeaderLowPriority.measure(widthMeasureSpec, headerHeightSpec);
+        if (mMinimizedGroupHeader != null) {
+            mMinimizedGroupHeader.measure(widthMeasureSpec, headerHeightSpec);
         }
 
         setMeasuredDimension(width, height);
@@ -308,11 +308,11 @@
      * appropriately.
      */
     public void setNotificationGroupWhen(long whenMillis) {
-        if (mNotificationHeaderWrapper != null) {
-            mNotificationHeaderWrapper.setNotificationWhen(whenMillis);
+        if (mGroupHeaderWrapper != null) {
+            mGroupHeaderWrapper.setNotificationWhen(whenMillis);
         }
-        if (mNotificationHeaderWrapperLowPriority != null) {
-            mNotificationHeaderWrapperLowPriority.setNotificationWhen(whenMillis);
+        if (mMinimizedGroupHeaderWrapper != null) {
+            mMinimizedGroupHeaderWrapper.setNotificationWhen(whenMillis);
         }
     }
 
@@ -410,28 +410,28 @@
         Trace.beginSection("recreateHeader#makeNotificationGroupHeader");
         RemoteViews header = builder.makeNotificationGroupHeader();
         Trace.endSection();
-        if (mNotificationHeader == null) {
+        if (mGroupHeader == null) {
             Trace.beginSection("recreateHeader#apply");
-            mNotificationHeader = (NotificationHeaderView) header.apply(getContext(), this);
+            mGroupHeader = (NotificationHeaderView) header.apply(getContext(), this);
             Trace.endSection();
-            mNotificationHeader.findViewById(com.android.internal.R.id.expand_button)
+            mGroupHeader.findViewById(com.android.internal.R.id.expand_button)
                     .setVisibility(VISIBLE);
-            mNotificationHeader.setOnClickListener(mHeaderClickListener);
-            mNotificationHeaderWrapper =
+            mGroupHeader.setOnClickListener(mHeaderClickListener);
+            mGroupHeaderWrapper =
                     (NotificationHeaderViewWrapper) NotificationViewWrapper.wrap(
                             getContext(),
-                            mNotificationHeader,
+                            mGroupHeader,
                             mContainingNotification);
-            mNotificationHeaderWrapper.setOnRoundnessChangedListener(this::invalidate);
-            addView(mNotificationHeader, 0);
+            mGroupHeaderWrapper.setOnRoundnessChangedListener(this::invalidate);
+            addView(mGroupHeader, 0);
             invalidate();
         } else {
             Trace.beginSection("recreateHeader#reapply");
-            header.reapply(getContext(), mNotificationHeader);
+            header.reapply(getContext(), mGroupHeader);
             Trace.endSection();
         }
-        mNotificationHeaderWrapper.setExpanded(mChildrenExpanded);
-        mNotificationHeaderWrapper.onContentUpdated(mContainingNotification);
+        mGroupHeaderWrapper.setExpanded(mChildrenExpanded);
+        mGroupHeaderWrapper.onContentUpdated(mContainingNotification);
         recreateLowPriorityHeader(builder, isConversation);
         updateHeaderVisibility(false /* animate */);
         updateChildrenAppearance();
@@ -439,21 +439,21 @@
     }
 
     private void removeGroupHeader() {
-        if (mNotificationHeader == null) {
+        if (mGroupHeader == null) {
             return;
         }
-        removeView(mNotificationHeader);
-        mNotificationHeader = null;
-        mNotificationHeaderWrapper = null;
+        removeView(mGroupHeader);
+        mGroupHeader = null;
+        mGroupHeaderWrapper = null;
     }
 
     private void removeLowPriorityGroupHeader() {
-        if (mNotificationHeaderLowPriority == null) {
+        if (mMinimizedGroupHeader == null) {
             return;
         }
-        removeView(mNotificationHeaderLowPriority);
-        mNotificationHeaderLowPriority = null;
-        mNotificationHeaderWrapperLowPriority = null;
+        removeView(mMinimizedGroupHeader);
+        mMinimizedGroupHeader = null;
+        mMinimizedGroupHeaderWrapper = null;
     }
 
     /**
@@ -474,21 +474,21 @@
             return;
         }
 
-        mNotificationHeader = headerView;
-        mNotificationHeader.findViewById(com.android.internal.R.id.expand_button)
+        mGroupHeader = headerView;
+        mGroupHeader.findViewById(com.android.internal.R.id.expand_button)
                 .setVisibility(VISIBLE);
-        mNotificationHeader.setOnClickListener(mHeaderClickListener);
-        mNotificationHeaderWrapper =
+        mGroupHeader.setOnClickListener(mHeaderClickListener);
+        mGroupHeaderWrapper =
                 (NotificationHeaderViewWrapper) NotificationViewWrapper.wrap(
                         getContext(),
-                        mNotificationHeader,
+                        mGroupHeader,
                         mContainingNotification);
-        mNotificationHeaderWrapper.setOnRoundnessChangedListener(this::invalidate);
-        addView(mNotificationHeader, 0);
+        mGroupHeaderWrapper.setOnRoundnessChangedListener(this::invalidate);
+        addView(mGroupHeader, 0);
         invalidate();
 
-        mNotificationHeaderWrapper.setExpanded(mChildrenExpanded);
-        mNotificationHeaderWrapper.onContentUpdated(mContainingNotification);
+        mGroupHeaderWrapper.setExpanded(mChildrenExpanded);
+        mGroupHeaderWrapper.onContentUpdated(mContainingNotification);
 
         updateHeaderVisibility(false /* animate */);
         updateChildrenAppearance();
@@ -511,20 +511,20 @@
             return;
         }
 
-        mNotificationHeaderLowPriority = headerViewLowPriority;
-        mNotificationHeaderLowPriority.findViewById(com.android.internal.R.id.expand_button)
+        mMinimizedGroupHeader = headerViewLowPriority;
+        mMinimizedGroupHeader.findViewById(com.android.internal.R.id.expand_button)
                 .setVisibility(VISIBLE);
-        mNotificationHeaderLowPriority.setOnClickListener(onClickListener);
-        mNotificationHeaderWrapperLowPriority =
+        mMinimizedGroupHeader.setOnClickListener(onClickListener);
+        mMinimizedGroupHeaderWrapper =
                 (NotificationHeaderViewWrapper) NotificationViewWrapper.wrap(
                         getContext(),
-                        mNotificationHeaderLowPriority,
+                        mMinimizedGroupHeader,
                         mContainingNotification);
-        mNotificationHeaderWrapperLowPriority.setOnRoundnessChangedListener(this::invalidate);
-        addView(mNotificationHeaderLowPriority, 0);
+        mMinimizedGroupHeaderWrapper.setOnRoundnessChangedListener(this::invalidate);
+        addView(mMinimizedGroupHeader, 0);
         invalidate();
 
-        mNotificationHeaderWrapperLowPriority.onContentUpdated(mContainingNotification);
+        mMinimizedGroupHeaderWrapper.onContentUpdated(mContainingNotification);
         updateHeaderVisibility(false /* animate */);
         updateChildrenAppearance();
     }
@@ -539,35 +539,35 @@
         AsyncGroupHeaderViewInflation.assertInLegacyMode();
         RemoteViews header;
         StatusBarNotification notification = mContainingNotification.getEntry().getSbn();
-        if (mIsLowPriority) {
+        if (mIsMinimized) {
             if (builder == null) {
                 builder = Notification.Builder.recoverBuilder(getContext(),
                         notification.getNotification());
             }
             header = builder.makeLowPriorityContentView(true /* useRegularSubtext */);
-            if (mNotificationHeaderLowPriority == null) {
-                mNotificationHeaderLowPriority = (NotificationHeaderView) header.apply(getContext(),
+            if (mMinimizedGroupHeader == null) {
+                mMinimizedGroupHeader = (NotificationHeaderView) header.apply(getContext(),
                         this);
-                mNotificationHeaderLowPriority.findViewById(com.android.internal.R.id.expand_button)
+                mMinimizedGroupHeader.findViewById(com.android.internal.R.id.expand_button)
                         .setVisibility(VISIBLE);
-                mNotificationHeaderLowPriority.setOnClickListener(mHeaderClickListener);
-                mNotificationHeaderWrapperLowPriority =
+                mMinimizedGroupHeader.setOnClickListener(mHeaderClickListener);
+                mMinimizedGroupHeaderWrapper =
                         (NotificationHeaderViewWrapper) NotificationViewWrapper.wrap(
                                 getContext(),
-                                mNotificationHeaderLowPriority,
+                                mMinimizedGroupHeader,
                                 mContainingNotification);
-                mNotificationHeaderWrapper.setOnRoundnessChangedListener(this::invalidate);
-                addView(mNotificationHeaderLowPriority, 0);
+                mGroupHeaderWrapper.setOnRoundnessChangedListener(this::invalidate);
+                addView(mMinimizedGroupHeader, 0);
                 invalidate();
             } else {
-                header.reapply(getContext(), mNotificationHeaderLowPriority);
+                header.reapply(getContext(), mMinimizedGroupHeader);
             }
-            mNotificationHeaderWrapperLowPriority.onContentUpdated(mContainingNotification);
-            resetHeaderVisibilityIfNeeded(mNotificationHeaderLowPriority, calculateDesiredHeader());
+            mMinimizedGroupHeaderWrapper.onContentUpdated(mContainingNotification);
+            resetHeaderVisibilityIfNeeded(mMinimizedGroupHeader, calculateDesiredHeader());
         } else {
-            removeView(mNotificationHeaderLowPriority);
-            mNotificationHeaderLowPriority = null;
-            mNotificationHeaderWrapperLowPriority = null;
+            removeView(mMinimizedGroupHeader);
+            mMinimizedGroupHeader = null;
+            mMinimizedGroupHeaderWrapper = null;
         }
     }
 
@@ -588,8 +588,8 @@
 
     public void updateGroupOverflow() {
         if (mShowGroupCountInExpander) {
-            setExpandButtonNumber(mNotificationHeaderWrapper);
-            setExpandButtonNumber(mNotificationHeaderWrapperLowPriority);
+            setExpandButtonNumber(mGroupHeaderWrapper);
+            setExpandButtonNumber(mMinimizedGroupHeaderWrapper);
             return;
         }
         int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* likeCollapsed */);
@@ -641,9 +641,9 @@
      * @param alpha alpha value to apply to the content
      */
     public void setContentAlpha(float alpha) {
-        if (mNotificationHeader != null) {
-            for (int i = 0; i < mNotificationHeader.getChildCount(); i++) {
-                mNotificationHeader.getChildAt(i).setAlpha(alpha);
+        if (mGroupHeader != null) {
+            for (int i = 0; i < mGroupHeader.getChildCount(); i++) {
+                mGroupHeader.getChildAt(i).setAlpha(alpha);
             }
         }
         for (ExpandableNotificationRow child : getAttachedChildren()) {
@@ -683,7 +683,7 @@
             if (AsyncGroupHeaderViewInflation.isEnabled()) {
                 return mHeaderHeight;
             } else {
-                return mNotificationHeaderLowPriority.getHeight();
+                return mMinimizedGroupHeader.getHeight();
             }
         }
         int intrinsicHeight = mNotificationHeaderMargin + mCurrentHeaderTranslation;
@@ -837,15 +837,15 @@
                 mGroupOverFlowState.setAlpha(0.0f);
             }
         }
-        if (mNotificationHeader != null) {
+        if (mGroupHeader != null) {
             if (mHeaderViewState == null) {
                 mHeaderViewState = new ViewState();
             }
-            mHeaderViewState.initFrom(mNotificationHeader);
+            mHeaderViewState.initFrom(mGroupHeader);
 
             if (mContainingNotification.hasExpandingChild()) {
                 // Not modifying translationZ during expand animation.
-                mHeaderViewState.setZTranslation(mNotificationHeader.getTranslationZ());
+                mHeaderViewState.setZTranslation(mGroupHeader.getTranslationZ());
             } else if (childrenExpandedAndNotAnimating) {
                 mHeaderViewState.setZTranslation(parentState.getZTranslation());
             } else {
@@ -898,7 +898,7 @@
                 && !showingAsLowPriority()) {
             return NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED;
         }
-        if (mIsLowPriority
+        if (mIsMinimized
                 || (!mContainingNotification.isOnKeyguard() && mContainingNotification.isExpanded())
                 || (mContainingNotification.isHeadsUpState()
                 && mContainingNotification.canShowHeadsUp())) {
@@ -946,7 +946,7 @@
             mNeverAppliedGroupState = false;
         }
         if (mHeaderViewState != null) {
-            mHeaderViewState.applyToView(mNotificationHeader);
+            mHeaderViewState.applyToView(mGroupHeader);
         }
         updateChildrenClipping();
     }
@@ -1006,8 +1006,8 @@
         }
 
         if (child instanceof NotificationHeaderView
-                && mNotificationHeaderWrapper.hasRoundedCorner()) {
-            float[] radii = mNotificationHeaderWrapper.getUpdatedRadii();
+                && mGroupHeaderWrapper.hasRoundedCorner()) {
+            float[] radii = mGroupHeaderWrapper.getUpdatedRadii();
             mHeaderPath.reset();
             mHeaderPath.addRoundRect(
                     child.getLeft(),
@@ -1085,8 +1085,8 @@
             }
             mGroupOverFlowState.animateTo(mOverflowNumber, properties);
         }
-        if (mNotificationHeader != null) {
-            mHeaderViewState.applyToView(mNotificationHeader);
+        if (mGroupHeader != null) {
+            mHeaderViewState.applyToView(mGroupHeader);
         }
         updateChildrenClipping();
     }
@@ -1109,8 +1109,8 @@
     public void setChildrenExpanded(boolean childrenExpanded) {
         mChildrenExpanded = childrenExpanded;
         updateExpansionStates();
-        if (mNotificationHeaderWrapper != null) {
-            mNotificationHeaderWrapper.setExpanded(childrenExpanded);
+        if (mGroupHeaderWrapper != null) {
+            mGroupHeaderWrapper.setExpanded(childrenExpanded);
         }
         final int count = mAttachedChildren.size();
         for (int childIdx = 0; childIdx < count; childIdx++) {
@@ -1130,11 +1130,11 @@
     }
 
     public NotificationViewWrapper getNotificationViewWrapper() {
-        return mNotificationHeaderWrapper;
+        return mGroupHeaderWrapper;
     }
 
-    public NotificationViewWrapper getLowPriorityViewWrapper() {
-        return mNotificationHeaderWrapperLowPriority;
+    public NotificationViewWrapper getMinimizedGroupHeaderWrapper() {
+        return mMinimizedGroupHeaderWrapper;
     }
 
     @VisibleForTesting
@@ -1142,12 +1142,12 @@
         return mCurrentHeader;
     }
 
-    public NotificationHeaderView getNotificationHeader() {
-        return mNotificationHeader;
+    public NotificationHeaderView getGroupHeader() {
+        return mGroupHeader;
     }
 
-    public NotificationHeaderView getNotificationHeaderLowPriority() {
-        return mNotificationHeaderLowPriority;
+    public NotificationHeaderView getMinimizedNotificationHeader() {
+        return mMinimizedGroupHeader;
     }
 
     private void updateHeaderVisibility(boolean animate) {
@@ -1171,7 +1171,7 @@
                 NotificationViewWrapper hiddenWrapper = getWrapperForView(currentHeader);
                 visibleWrapper.transformFrom(hiddenWrapper);
                 hiddenWrapper.transformTo(visibleWrapper, () -> updateHeaderVisibility(false));
-                startChildAlphaAnimations(desiredHeader == mNotificationHeader);
+                startChildAlphaAnimations(desiredHeader == mGroupHeader);
             } else {
                 animate = false;
             }
@@ -1192,8 +1192,8 @@
             }
         }
 
-        resetHeaderVisibilityIfNeeded(mNotificationHeader, desiredHeader);
-        resetHeaderVisibilityIfNeeded(mNotificationHeaderLowPriority, desiredHeader);
+        resetHeaderVisibilityIfNeeded(mGroupHeader, desiredHeader);
+        resetHeaderVisibilityIfNeeded(mMinimizedGroupHeader, desiredHeader);
 
         mCurrentHeader = desiredHeader;
     }
@@ -1215,9 +1215,9 @@
     private ViewGroup calculateDesiredHeader() {
         ViewGroup desiredHeader;
         if (showingAsLowPriority()) {
-            desiredHeader = mNotificationHeaderLowPriority;
+            desiredHeader = mMinimizedGroupHeader;
         } else {
-            desiredHeader = mNotificationHeader;
+            desiredHeader = mGroupHeader;
         }
         return desiredHeader;
     }
@@ -1244,20 +1244,20 @@
     private void updateHeaderTransformation() {
         if (mUserLocked && showingAsLowPriority()) {
             float fraction = getGroupExpandFraction();
-            mNotificationHeaderWrapper.transformFrom(mNotificationHeaderWrapperLowPriority,
+            mGroupHeaderWrapper.transformFrom(mMinimizedGroupHeaderWrapper,
                     fraction);
-            mNotificationHeader.setVisibility(VISIBLE);
-            mNotificationHeaderWrapperLowPriority.transformTo(mNotificationHeaderWrapper,
+            mGroupHeader.setVisibility(VISIBLE);
+            mMinimizedGroupHeaderWrapper.transformTo(mGroupHeaderWrapper,
                     fraction);
         }
 
     }
 
     private NotificationViewWrapper getWrapperForView(View visibleHeader) {
-        if (visibleHeader == mNotificationHeader) {
-            return mNotificationHeaderWrapper;
+        if (visibleHeader == mGroupHeader) {
+            return mGroupHeaderWrapper;
         }
-        return mNotificationHeaderWrapperLowPriority;
+        return mMinimizedGroupHeaderWrapper;
     }
 
     /**
@@ -1266,13 +1266,13 @@
      * @param expanded whether the group is expanded.
      */
     public void updateHeaderForExpansion(boolean expanded) {
-        if (mNotificationHeader != null) {
+        if (mGroupHeader != null) {
             if (expanded) {
                 ColorDrawable cd = new ColorDrawable();
                 cd.setColor(mContainingNotification.calculateBgColor());
-                mNotificationHeader.setHeaderBackgroundDrawable(cd);
+                mGroupHeader.setHeaderBackgroundDrawable(cd);
             } else {
-                mNotificationHeader.setHeaderBackgroundDrawable(null);
+                mGroupHeader.setHeaderBackgroundDrawable(null);
             }
         }
     }
@@ -1405,11 +1405,11 @@
             if (AsyncGroupHeaderViewInflation.isEnabled()) {
                 return mHeaderHeight;
             }
-            if (mNotificationHeaderLowPriority == null) {
+            if (mMinimizedGroupHeader == null) {
                 Log.e(TAG, "getMinHeight: low priority header is null", new Exception());
                 return 0;
             }
-            return mNotificationHeaderLowPriority.getHeight();
+            return mMinimizedGroupHeader.getHeight();
         }
         int minExpandHeight = mNotificationHeaderMargin + headerTranslation;
         int visibleChildren = 0;
@@ -1443,20 +1443,20 @@
     }
 
     public boolean showingAsLowPriority() {
-        return mIsLowPriority && !mContainingNotification.isExpanded();
+        return mIsMinimized && !mContainingNotification.isExpanded();
     }
 
     public void reInflateViews(OnClickListener listener, StatusBarNotification notification) {
         if (!AsyncGroupHeaderViewInflation.isEnabled()) {
             // When Async header inflation is enabled, we do not reinflate headers because they are
             // inflated from the background thread
-            if (mNotificationHeader != null) {
-                removeView(mNotificationHeader);
-                mNotificationHeader = null;
+            if (mGroupHeader != null) {
+                removeView(mGroupHeader);
+                mGroupHeader = null;
             }
-            if (mNotificationHeaderLowPriority != null) {
-                removeView(mNotificationHeaderLowPriority);
-                mNotificationHeaderLowPriority = null;
+            if (mMinimizedGroupHeader != null) {
+                removeView(mMinimizedGroupHeader);
+                mMinimizedGroupHeader = null;
             }
             recreateNotificationHeader(listener, mIsConversation);
         }
@@ -1489,8 +1489,8 @@
     }
 
     private void updateHeaderTouchability() {
-        if (mNotificationHeader != null) {
-            mNotificationHeader.setAcceptAllTouches(mChildrenExpanded || mUserLocked);
+        if (mGroupHeader != null) {
+            mGroupHeader.setAcceptAllTouches(mChildrenExpanded || mUserLocked);
         }
     }
 
@@ -1534,8 +1534,11 @@
         updateChildrenClipping();
     }
 
-    public void setIsLowPriority(boolean isLowPriority) {
-        mIsLowPriority = isLowPriority;
+    /**
+     * Set whether the children container is minimized.
+     */
+    public void setIsMinimized(boolean isMinimized) {
+        mIsMinimized = isMinimized;
         if (mContainingNotification != null) { /* we're not yet set up yet otherwise */
             if (!AsyncGroupHeaderViewInflation.isEnabled()) {
                 recreateLowPriorityHeader(null /* existingBuilder */, mIsConversation);
@@ -1552,13 +1555,13 @@
      */
     public NotificationViewWrapper getVisibleWrapper() {
         if (showingAsLowPriority()) {
-            return mNotificationHeaderWrapperLowPriority;
+            return mMinimizedGroupHeaderWrapper;
         }
-        return mNotificationHeaderWrapper;
+        return mGroupHeaderWrapper;
     }
 
     public void onExpansionChanged() {
-        if (mIsLowPriority) {
+        if (mIsMinimized) {
             if (mUserLocked) {
                 setUserLocked(mUserLocked);
             }
@@ -1574,15 +1577,15 @@
     @Override
     public void applyRoundnessAndInvalidate() {
         boolean last = true;
-        if (mNotificationHeaderWrapper != null) {
-            mNotificationHeaderWrapper.requestTopRoundness(
+        if (mGroupHeaderWrapper != null) {
+            mGroupHeaderWrapper.requestTopRoundness(
                     /* value = */ getTopRoundness(),
                     /* sourceType = */ FROM_PARENT,
                     /* animate = */ false
             );
         }
-        if (mNotificationHeaderWrapperLowPriority != null) {
-            mNotificationHeaderWrapperLowPriority.requestTopRoundness(
+        if (mMinimizedGroupHeaderWrapper != null) {
+            mMinimizedGroupHeaderWrapper.requestTopRoundness(
                     /* value = */ getTopRoundness(),
                     /* sourceType = */ FROM_PARENT,
                     /* animate = */ false
@@ -1612,31 +1615,31 @@
      * Shows the given feedback icon, or hides the icon if null.
      */
     public void setFeedbackIcon(@Nullable FeedbackIcon icon) {
-        if (mNotificationHeaderWrapper != null) {
-            mNotificationHeaderWrapper.setFeedbackIcon(icon);
+        if (mGroupHeaderWrapper != null) {
+            mGroupHeaderWrapper.setFeedbackIcon(icon);
         }
-        if (mNotificationHeaderWrapperLowPriority != null) {
-            mNotificationHeaderWrapperLowPriority.setFeedbackIcon(icon);
+        if (mMinimizedGroupHeaderWrapper != null) {
+            mMinimizedGroupHeaderWrapper.setFeedbackIcon(icon);
         }
     }
 
     public void setRecentlyAudiblyAlerted(boolean audiblyAlertedRecently) {
-        if (mNotificationHeaderWrapper != null) {
-            mNotificationHeaderWrapper.setRecentlyAudiblyAlerted(audiblyAlertedRecently);
+        if (mGroupHeaderWrapper != null) {
+            mGroupHeaderWrapper.setRecentlyAudiblyAlerted(audiblyAlertedRecently);
         }
-        if (mNotificationHeaderWrapperLowPriority != null) {
-            mNotificationHeaderWrapperLowPriority.setRecentlyAudiblyAlerted(audiblyAlertedRecently);
+        if (mMinimizedGroupHeaderWrapper != null) {
+            mMinimizedGroupHeaderWrapper.setRecentlyAudiblyAlerted(audiblyAlertedRecently);
         }
     }
 
     @Override
     public void setNotificationFaded(boolean faded) {
         mContainingNotificationIsFaded = faded;
-        if (mNotificationHeaderWrapper != null) {
-            mNotificationHeaderWrapper.setNotificationFaded(faded);
+        if (mGroupHeaderWrapper != null) {
+            mGroupHeaderWrapper.setNotificationFaded(faded);
         }
-        if (mNotificationHeaderWrapperLowPriority != null) {
-            mNotificationHeaderWrapperLowPriority.setNotificationFaded(faded);
+        if (mMinimizedGroupHeaderWrapper != null) {
+            mMinimizedGroupHeaderWrapper.setNotificationFaded(faded);
         }
         for (ExpandableNotificationRow child : mAttachedChildren) {
             child.setNotificationFaded(faded);
@@ -1654,7 +1657,7 @@
     }
 
     public NotificationHeaderViewWrapper getNotificationHeaderWrapper() {
-        return mNotificationHeaderWrapper;
+        return mGroupHeaderWrapper;
     }
 
     public void setLogger(NotificationChildrenContainerLogger logger) {
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 9479762..f2c593d 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
@@ -812,6 +812,10 @@
         } else {
             mDebugTextUsedYPositions.clear();
         }
+
+        mDebugPaint.setColor(Color.DKGRAY);
+        canvas.drawPath(mRoundedClipPath, mDebugPaint);
+
         int y = 0;
         drawDebugInfo(canvas, y, Color.RED, /* label= */ "y = " + y);
 
@@ -843,14 +847,14 @@
         drawDebugInfo(canvas, y, Color.LTGRAY,
                 /* label= */ "mAmbientState.getStackY() + mAmbientState.getStackHeight() = " + y);
 
-        y = (int) mAmbientState.getStackY() + mContentHeight;
-        drawDebugInfo(canvas, y, Color.MAGENTA,
-                /* label= */ "mAmbientState.getStackY() + mContentHeight = " + y);
-
         y = (int) (mAmbientState.getStackY() + mIntrinsicContentHeight);
         drawDebugInfo(canvas, y, Color.YELLOW,
                 /* label= */ "mAmbientState.getStackY() + mIntrinsicContentHeight = " + y);
 
+        y = mContentHeight;
+        drawDebugInfo(canvas, y, Color.MAGENTA,
+                /* label= */ "mContentHeight = " + y);
+
         drawDebugInfo(canvas, mRoundedRectClippingBottom, Color.DKGRAY,
                 /* label= */ "mRoundedRectClippingBottom) = " + y);
     }
@@ -4940,6 +4944,9 @@
             println(pw, "intrinsicPadding", mIntrinsicPadding);
             println(pw, "topPadding", mTopPadding);
             println(pw, "bottomPadding", mBottomPadding);
+            dumpRoundedRectClipping(pw);
+            println(pw, "requestedClipBounds", mRequestedClipBounds);
+            println(pw, "isClipped", mIsClipped);
             println(pw, "translationX", getTranslationX());
             println(pw, "translationY", getTranslationY());
             println(pw, "translationZ", getTranslationZ());
@@ -4994,6 +5001,15 @@
                 });
     }
 
+    private void dumpRoundedRectClipping(IndentingPrintWriter pw) {
+        pw.append("roundedRectClipping{l=").print(mRoundedRectClippingLeft);
+        pw.append(" t=").print(mRoundedRectClippingTop);
+        pw.append(" r=").print(mRoundedRectClippingRight);
+        pw.append(" b=").print(mRoundedRectClippingBottom);
+        pw.append("} topRadius=").print(mBgCornerRadii[0]);
+        pw.append(" bottomRadius=").println(mBgCornerRadii[4]);
+    }
+
     private void dumpFooterViewVisibility(IndentingPrintWriter pw) {
         FooterViewRefactor.assertInLegacyMode();
         final boolean showDismissView = shouldShowDismissView();
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 8ed1ca2..2f577d0 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
@@ -23,7 +23,6 @@
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_SCROLL_FLING;
 import static com.android.server.notification.Flags.screenshareNotificationHiding;
 import static com.android.systemui.Dependency.ALLOW_NOTIFICATION_LONG_PRESS_NAME;
-import static com.android.systemui.Flags.migrateClocksToBlueprint;
 import static com.android.systemui.Flags.nsslFalsingFix;
 import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.OnEmptySpaceClickListener;
@@ -71,6 +70,7 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlagsClassic;
 import com.android.systemui.flags.Flags;
+import com.android.systemui.keyguard.MigrateClocksToBlueprint;
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository;
 import com.android.systemui.keyguard.shared.model.KeyguardState;
 import com.android.systemui.keyguard.shared.model.TransitionStep;
@@ -2090,7 +2090,7 @@
             }
             boolean horizontalSwipeWantsIt = false;
             boolean scrollerWantsIt = false;
-            if (nsslFalsingFix() || migrateClocksToBlueprint()) {
+            if (nsslFalsingFix() || MigrateClocksToBlueprint.isEnabled()) {
                 // Reverse the order relative to the else statement. onScrollTouch will reset on an
                 // UP event, causing horizontalSwipeWantsIt to be set to true on vertical swipes.
                 if (mLongPressedView == null && !mView.isBeingDragged()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index 9b1952b..b42c07d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -53,9 +53,7 @@
     public static final float START_FRACTION = 0.5f;
 
     private static final String TAG = "StackScrollAlgorithm";
-    private static final Boolean DEBUG = false;
     private static final SourceType STACK_SCROLL_ALGO = SourceType.from("StackScrollAlgorithm");
-
     private final ViewGroup mHostView;
     private float mPaddingBetweenElements;
     private float mGapHeight;
@@ -247,13 +245,11 @@
                 >= ambientState.getMaxHeadsUpTranslation();
     }
 
-    public static void log(String s) {
-        if (DEBUG) {
-            android.util.Log.i(TAG, s);
-        }
+    public static void debugLog(String s) {
+        android.util.Log.i(TAG, s);
     }
 
-    public static void logView(View view, String s) {
+    public static void debugLogView(View view, String s) {
         String viewString = "";
         if (view instanceof ExpandableNotificationRow row) {
             if (row.getEntry() == null) {
@@ -274,7 +270,7 @@
         } else {
             viewString = view.toString();
         }
-        log(viewString + " " + s);
+        debugLog(viewString + " " + s);
     }
 
     private void resetChildViewStates() {
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 9efe632..79ba25e 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
@@ -17,8 +17,8 @@
 
 package com.android.systemui.statusbar.notification.stack.data.repository
 
-import com.android.systemui.common.shared.model.NotificationContainerBounds
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds
 import javax.inject.Inject
 import kotlinx.coroutines.flow.MutableStateFlow
 
@@ -26,7 +26,7 @@
 @SysUISingleton
 class NotificationStackAppearanceRepository @Inject constructor() {
     /** The bounds of the notification stack in the current scene. */
-    val stackBounds = MutableStateFlow(NotificationContainerBounds())
+    val stackBounds = MutableStateFlow(StackBounds())
 
     /**
      * The height in px of the contents of notification stack. Depending on the number of
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 08df473..f05d017 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
@@ -17,13 +17,19 @@
 
 package com.android.systemui.statusbar.notification.stack.domain.interactor
 
-import com.android.systemui.common.shared.model.NotificationContainerBounds
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.statusbar.notification.stack.data.repository.NotificationStackAppearanceRepository
+import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds
+import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
 
 /** An interactor which controls the appearance of the NSSL */
 @SysUISingleton
@@ -31,9 +37,30 @@
 @Inject
 constructor(
     private val repository: NotificationStackAppearanceRepository,
+    shadeInteractor: ShadeInteractor,
 ) {
     /** The bounds of the notification stack in the current scene. */
-    val stackBounds: StateFlow<NotificationContainerBounds> = repository.stackBounds.asStateFlow()
+    val stackBounds: StateFlow<StackBounds> = repository.stackBounds.asStateFlow()
+
+    /**
+     * Whether the stack is expanding from GONE-with-HUN to SHADE
+     *
+     * TODO(b/296118689): implement this to match legacy QSController logic
+     */
+    private val isExpandingFromHeadsUp: Flow<Boolean> = flowOf(false)
+
+    /** The rounding of the notification stack. */
+    val stackRounding: Flow<StackRounding> =
+        combine(
+                shadeInteractor.shadeMode,
+                isExpandingFromHeadsUp,
+            ) { shadeMode, isExpandingFromHeadsUp ->
+                StackRounding(
+                    roundTop = !(shadeMode == ShadeMode.Split && isExpandingFromHeadsUp),
+                    roundBottom = shadeMode != ShadeMode.Single,
+                )
+            }
+            .distinctUntilChanged()
 
     /**
      * The height in px of the contents of notification stack. Depending on the number of
@@ -59,7 +86,7 @@
     val syntheticScroll: Flow<Float> = repository.syntheticScroll.asStateFlow()
 
     /** Sets the position of the notification stack in the current scene. */
-    fun setStackBounds(bounds: NotificationContainerBounds) {
+    fun setStackBounds(bounds: StackBounds) {
         check(bounds.top <= bounds.bottom) { "Invalid bounds: $bounds" }
         repository.stackBounds.value = bounds
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackBounds.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackBounds.kt
new file mode 100644
index 0000000..1fc9a18
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackBounds.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack.shared.model
+
+/** Models the bounds of the notification stack. */
+data class StackBounds(
+    /** The position of the left of the stack in its window coordinate system, in pixels. */
+    val left: Float = 0f,
+    /** The position of the top of the stack in its window coordinate system, in pixels. */
+    val top: Float = 0f,
+    /** The position of the right of the stack in its window coordinate system, in pixels. */
+    val right: Float = 0f,
+    /** The position of the bottom of the stack in its window coordinate system, in pixels. */
+    val bottom: Float = 0f,
+) {
+    /** The current height of the notification container. */
+    val height: Float = bottom - top
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackClipping.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackClipping.kt
new file mode 100644
index 0000000..0c92b50
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackClipping.kt
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack.shared.model
+
+/** Models the clipping rounded rectangle of the notification stack */
+data class StackClipping(val bounds: StackBounds, val rounding: StackRounding)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackRounding.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackRounding.kt
new file mode 100644
index 0000000..ddc5d7ea
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackRounding.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack.shared.model
+
+/** Models the corner rounds of the notification stack. */
+data class StackRounding(
+    /** Whether the top corners of the notification stack should be rounded. */
+    val roundTop: Boolean = false,
+    /** Whether the bottom corners of the notification stack should be rounded. */
+    val roundBottom: Boolean = false,
+)
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
deleted file mode 100644
index f10e5f1..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Copyright (C) 2023 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.notification.stack.ui.viewbinder
-
-import android.content.Context
-import android.util.TypedValue
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.repeatOnLifecycle
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.lifecycle.repeatWhenAttached
-import com.android.systemui.statusbar.notification.stack.AmbientState
-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.roundToInt
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.DisposableHandle
-import kotlinx.coroutines.launch
-
-/** Binds the shared notification container to its view-model. */
-object NotificationStackAppearanceViewBinder {
-    const val SCRIM_CORNER_RADIUS = 32f
-
-    @JvmStatic
-    fun bind(
-        context: Context,
-        view: SharedNotificationContainer,
-        viewModel: NotificationStackAppearanceViewModel,
-        ambientState: AmbientState,
-        controller: NotificationStackScrollLayoutController,
-        @Main mainImmediateDispatcher: CoroutineDispatcher,
-    ): DisposableHandle {
-        return view.repeatWhenAttached(mainImmediateDispatcher) {
-            repeatOnLifecycle(Lifecycle.State.CREATED) {
-                launch {
-                    viewModel.stackBounds.collect { bounds ->
-                        val viewLeft = controller.view.left
-                        val viewTop = controller.view.top
-                        controller.setRoundedClippingBounds(
-                            bounds.left.roundToInt() - viewLeft,
-                            bounds.top.roundToInt() - viewTop,
-                            bounds.right.roundToInt() - viewLeft,
-                            bounds.bottom.roundToInt() - viewTop,
-                            SCRIM_CORNER_RADIUS.dpToPx(context),
-                            0,
-                        )
-                    }
-                }
-
-                launch {
-                    viewModel.contentTop.collect {
-                        controller.updateTopPadding(it, controller.isAddOrRemoveAnimationPending)
-                    }
-                }
-
-                launch {
-                    var wasExpanding = false
-                    viewModel.expandFraction.collect { expandFraction ->
-                        val nowExpanding = expandFraction != 0f && expandFraction != 1f
-                        if (nowExpanding && !wasExpanding) {
-                            controller.onExpansionStarted()
-                        }
-                        ambientState.expansionFraction = expandFraction
-                        controller.expandedHeight = expandFraction * controller.view.height
-                        if (!nowExpanding && wasExpanding) {
-                            controller.onExpansionStopped()
-                        }
-                        wasExpanding = nowExpanding
-                    }
-                }
-
-                launch { viewModel.isScrollable.collect { controller.setScrollingEnabled(it) } }
-            }
-        }
-    }
-
-    private fun Float.dpToPx(context: Context): Int {
-        return TypedValue.applyDimension(
-                TypedValue.COMPLEX_UNIT_DIP,
-                this,
-                context.resources.displayMetrics
-            )
-            .roundToInt()
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackViewBinder.kt
new file mode 100644
index 0000000..1a34bb4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackViewBinder.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2023 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.notification.stack.ui.viewbinder
+
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.common.ui.ConfigurationState
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.res.R
+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.ui.viewmodel.NotificationStackAppearanceViewModel
+import javax.inject.Inject
+import kotlin.math.roundToInt
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.launch
+
+/** Binds the NSSL/Controller/AmbientState to their ViewModel. */
+@SysUISingleton
+class NotificationStackViewBinder
+@Inject
+constructor(
+    @Main private val mainImmediateDispatcher: CoroutineDispatcher,
+    private val ambientState: AmbientState,
+    private val view: NotificationStackScrollLayout,
+    private val controller: NotificationStackScrollLayoutController,
+    private val viewModel: NotificationStackAppearanceViewModel,
+    private val configuration: ConfigurationState,
+) {
+
+    fun bindWhileAttached(): DisposableHandle {
+        return view.repeatWhenAttached(mainImmediateDispatcher) {
+            repeatOnLifecycle(Lifecycle.State.CREATED) { bind() }
+        }
+    }
+
+    suspend fun bind() = coroutineScope {
+        launch {
+            combine(viewModel.stackClipping, clipRadius, ::Pair).collect { (clipping, clipRadius) ->
+                val (bounds, rounding) = clipping
+                val viewLeft = controller.view.left
+                val viewTop = controller.view.top
+                controller.setRoundedClippingBounds(
+                    bounds.left.roundToInt() - viewLeft,
+                    bounds.top.roundToInt() - viewTop,
+                    bounds.right.roundToInt() - viewLeft,
+                    bounds.bottom.roundToInt() - viewTop,
+                    if (rounding.roundTop) clipRadius else 0,
+                    if (rounding.roundBottom) clipRadius else 0,
+                )
+            }
+        }
+
+        launch {
+            viewModel.contentTop.collect {
+                controller.updateTopPadding(it, controller.isAddOrRemoveAnimationPending)
+            }
+        }
+
+        launch {
+            var wasExpanding = false
+            viewModel.expandFraction.collect { expandFraction ->
+                val nowExpanding = expandFraction != 0f && expandFraction != 1f
+                if (nowExpanding && !wasExpanding) {
+                    controller.onExpansionStarted()
+                }
+                ambientState.expansionFraction = expandFraction
+                controller.expandedHeight = expandFraction * controller.view.height
+                if (!nowExpanding && wasExpanding) {
+                    controller.onExpansionStopped()
+                }
+                wasExpanding = nowExpanding
+            }
+        }
+
+        launch { viewModel.isScrollable.collect { controller.setScrollingEnabled(it) } }
+    }
+
+    private val clipRadius: Flow<Int>
+        get() = configuration.getDimensionPixelOffset(R.dimen.notification_scrim_corner_radius)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
index 7c76ddb..ecf737a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
@@ -20,6 +20,7 @@
 import android.view.WindowInsets
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
 import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor
@@ -30,6 +31,8 @@
 import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
+import com.android.systemui.util.kotlin.DisposableHandles
+import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.DisposableHandle
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -38,18 +41,24 @@
 import kotlinx.coroutines.launch
 
 /** Binds the shared notification container to its view-model. */
-object SharedNotificationContainerBinder {
+@SysUISingleton
+class SharedNotificationContainerBinder
+@Inject
+constructor(
+    private val sceneContainerFlags: SceneContainerFlags,
+    private val controller: NotificationStackScrollLayoutController,
+    private val notificationStackSizeCalculator: NotificationStackSizeCalculator,
+    private val notificationStackViewBinder: NotificationStackViewBinder,
+    @Main private val mainImmediateDispatcher: CoroutineDispatcher,
+) {
 
-    @JvmStatic
     fun bind(
         view: SharedNotificationContainer,
         viewModel: SharedNotificationContainerViewModel,
-        sceneContainerFlags: SceneContainerFlags,
-        controller: NotificationStackScrollLayoutController,
-        notificationStackSizeCalculator: NotificationStackSizeCalculator,
-        @Main mainImmediateDispatcher: CoroutineDispatcher,
     ): DisposableHandle {
-        val disposableHandle =
+        val disposables = DisposableHandles()
+
+        disposables +=
             view.repeatWhenAttached {
                 repeatOnLifecycle(Lifecycle.State.CREATED) {
                     launch {
@@ -72,24 +81,6 @@
                 }
             }
 
-        // Required to capture keyguard media changes and ensure the notification count is correct
-        val layoutChangeListener =
-            object : View.OnLayoutChangeListener {
-                override fun onLayoutChange(
-                    view: View,
-                    left: Int,
-                    top: Int,
-                    right: Int,
-                    bottom: Int,
-                    oldLeft: Int,
-                    oldTop: Int,
-                    oldRight: Int,
-                    oldBottom: Int
-                ) {
-                    viewModel.notificationStackChanged()
-                }
-            }
-
         val burnInParams = MutableStateFlow(BurnInParameters())
         val viewState =
             ViewStateAccessor(
@@ -100,7 +91,7 @@
          * For animation sensitive coroutines, immediately run just like applicationScope does
          * instead of doing a post() to the main thread. This extra delay can cause visible jitter.
          */
-        val disposableHandleMainImmediate =
+        disposables +=
             view.repeatWhenAttached(mainImmediateDispatcher) {
                 repeatOnLifecycle(Lifecycle.State.CREATED) {
                     launch {
@@ -167,7 +158,12 @@
                 }
             }
 
-        controller.setOnHeightChangedRunnable(Runnable { viewModel.notificationStackChanged() })
+        if (sceneContainerFlags.isEnabled()) {
+            disposables += notificationStackViewBinder.bindWhileAttached()
+        }
+
+        controller.setOnHeightChangedRunnable { viewModel.notificationStackChanged() }
+        disposables += DisposableHandle { controller.setOnHeightChangedRunnable(null) }
 
         view.setOnApplyWindowInsetsListener { v: View, insets: WindowInsets ->
             val insetTypes = WindowInsets.Type.systemBars() or WindowInsets.Type.displayCutout()
@@ -176,16 +172,16 @@
             }
             insets
         }
-        view.addOnLayoutChangeListener(layoutChangeListener)
+        disposables += DisposableHandle { view.setOnApplyWindowInsetsListener(null) }
 
-        return object : DisposableHandle {
-            override fun dispose() {
-                disposableHandle.dispose()
-                disposableHandleMainImmediate.dispose()
-                controller.setOnHeightChangedRunnable(null)
-                view.setOnApplyWindowInsetsListener(null)
-                view.removeOnLayoutChangeListener(layoutChangeListener)
+        // Required to capture keyguard media changes and ensure the notification count is correct
+        val layoutChangeListener =
+            View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
+                viewModel.notificationStackChanged()
             }
-        }
+        view.addOnLayoutChangeListener(layoutChangeListener)
+        disposables += DisposableHandle { view.removeOnLayoutChangeListener(layoutChangeListener) }
+
+        return disposables
     }
 }
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 b6167e1..a7cbc33 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
@@ -18,7 +18,6 @@
 package com.android.systemui.statusbar.notification.stack.ui.viewmodel
 
 import com.android.compose.animation.scene.ObservableTransitionState
-import com.android.systemui.common.shared.model.NotificationContainerBounds
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dump.DumpManager
@@ -27,6 +26,7 @@
 import com.android.systemui.scene.shared.model.Scenes.Shade
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor
+import com.android.systemui.statusbar.notification.stack.shared.model.StackClipping
 import com.android.systemui.util.kotlin.FlowDumperImpl
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
@@ -83,8 +83,13 @@
             .dumpWhileCollecting("expandFraction")
 
     /** The bounds of the notification stack in the current scene. */
-    val stackBounds: Flow<NotificationContainerBounds> =
-        stackAppearanceInteractor.stackBounds.dumpValue("stackBounds")
+    val stackClipping: Flow<StackClipping> =
+        combine(
+                stackAppearanceInteractor.stackBounds,
+                stackAppearanceInteractor.stackRounding,
+                ::StackClipping
+            )
+            .dumpWhileCollecting("stackClipping")
 
     /** The y-coordinate in px of top of the contents of the notification stack. */
     val contentTop: StateFlow<Float> = stackAppearanceInteractor.contentTop.dumpValue("contentTop")
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 9e2497d..bd83121 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
@@ -24,6 +24,8 @@
 import com.android.systemui.scene.shared.flag.SceneContainerFlags
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor
+import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds
+import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 
@@ -61,12 +63,17 @@
         right: Float,
         bottom: Float,
     ) {
-        val notificationContainerBounds =
-            NotificationContainerBounds(top = top, bottom = bottom, left = left, right = right)
-        keyguardInteractor.setNotificationContainerBounds(notificationContainerBounds)
-        interactor.setStackBounds(notificationContainerBounds)
+        keyguardInteractor.setNotificationContainerBounds(
+            NotificationContainerBounds(top = top, bottom = bottom)
+        )
+        interactor.setStackBounds(
+            StackBounds(top = top, bottom = bottom, left = left, right = right)
+        )
     }
 
+    /** Corner rounding of the stack */
+    val stackRounding: Flow<StackRounding> = interactor.stackRounding
+
     /**
      * The height in px of the contents of notification stack. Depending on the number of
      * notifications, this can exceed the space available on screen to show notifications, at which
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 a38840b..ab6c148 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
@@ -386,7 +386,7 @@
         // All transition view models are mututally exclusive, and safe to merge
         val alphaTransitions =
             merge(
-                alternateBouncerToGoneTransitionViewModel.lockscreenAlpha,
+                alternateBouncerToGoneTransitionViewModel.lockscreenAlpha(viewState),
                 aodToLockscreenTransitionViewModel.notificationAlpha,
                 aodToOccludedTransitionViewModel.lockscreenAlpha(viewState),
                 dozingToLockscreenTransitionViewModel.lockscreenAlpha,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index d32e88b..f76de04c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -27,7 +27,6 @@
 
 import static com.android.systemui.Dependency.TIME_TICK_HANDLER_NAME;
 import static com.android.systemui.Flags.lightRevealMigration;
-import static com.android.systemui.Flags.migrateClocksToBlueprint;
 import static com.android.systemui.Flags.newAodTransition;
 import static com.android.systemui.Flags.predictiveBackSysui;
 import static com.android.systemui.Flags.truncatedStatusBarIconsFix;
@@ -142,6 +141,7 @@
 import com.android.systemui.fragments.FragmentService;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.KeyguardViewMediator;
+import com.android.systemui.keyguard.MigrateClocksToBlueprint;
 import com.android.systemui.keyguard.ScreenLifecycle;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.keyguard.ui.binder.LightRevealScrimViewBinder;
@@ -1470,7 +1470,7 @@
         return (v, event) -> {
             mAutoHideController.checkUserAutoHide(event);
             mRemoteInputManager.checkRemoteInputOutside(event);
-            if (!migrateClocksToBlueprint()) {
+            if (!MigrateClocksToBlueprint.isEnabled()) {
                 mShadeController.onStatusBarTouch(event);
             }
             return getNotificationShadeWindowView().onTouchEvent(event);
@@ -2507,7 +2507,7 @@
             mNotificationShadeWindowController.batchApplyWindowLayoutParams(()-> {
                 mDeviceInteractive = true;
 
-                boolean isFlaggedOff = newAodTransition() && migrateClocksToBlueprint();
+                boolean isFlaggedOff = newAodTransition() && MigrateClocksToBlueprint.isEnabled();
                 if (!isFlaggedOff && shouldAnimateDozeWakeup()) {
                     // If this is false, the power button must be physically pressed in order to
                     // trigger fingerprint authentication.
@@ -3147,7 +3147,14 @@
                 public void onDozeAmountChanged(float linear, float eased) {
                     if (!lightRevealMigration()
                             && !(mLightRevealScrim.getRevealEffect() instanceof CircleReveal)) {
-                        mLightRevealScrim.setRevealAmount(1f - linear);
+                        if (DeviceEntryUdfpsRefactor.isEnabled()) {
+                            // If wakeAndUnlocking, this is handled in AuthRippleInteractor
+                            if (!mBiometricUnlockController.isWakeAndUnlock()) {
+                                mLightRevealScrim.setRevealAmount(1f - linear);
+                            }
+                        } else {
+                            mLightRevealScrim.setRevealAmount(1f - linear);
+                        }
                     }
                 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
index 24be3db..86bb844 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
@@ -41,6 +41,7 @@
 import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider;
 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor;
 import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
 import com.android.systemui.statusbar.policy.AnimationStateHandler;
 import com.android.systemui.statusbar.policy.AvalancheController;
@@ -94,6 +95,7 @@
 
         @Override
         public HeadsUpEntryPhone acquire() {
+            NotificationsHeadsUpRefactor.assertInLegacyMode();
             if (!mPoolObjects.isEmpty()) {
                 return mPoolObjects.pop();
             }
@@ -102,6 +104,7 @@
 
         @Override
         public boolean release(@NonNull HeadsUpEntryPhone instance) {
+            NotificationsHeadsUpRefactor.assertInLegacyMode();
             mPoolObjects.push(instance);
             return true;
         }
@@ -371,15 +374,24 @@
     ///////////////////////////////////////////////////////////////////////////////////////////////
     //  HeadsUpManager utility (protected) methods overrides:
 
+    @NonNull
     @Override
-    protected HeadsUpEntry createHeadsUpEntry() {
-        return mEntryPool.acquire();
+    protected HeadsUpEntry createHeadsUpEntry(NotificationEntry entry) {
+        if (NotificationsHeadsUpRefactor.isEnabled()) {
+            return new HeadsUpEntryPhone(entry);
+        } else {
+            HeadsUpEntryPhone headsUpEntry = mEntryPool.acquire();
+            headsUpEntry.setEntry(entry);
+            return headsUpEntry;
+        }
     }
 
     @Override
     protected void onEntryRemoved(HeadsUpEntry headsUpEntry) {
         super.onEntryRemoved(headsUpEntry);
-        mEntryPool.release((HeadsUpEntryPhone) headsUpEntry);
+        if (!NotificationsHeadsUpRefactor.isEnabled()) {
+            mEntryPool.release((HeadsUpEntryPhone) headsUpEntry);
+        }
     }
 
     @Override
@@ -439,14 +451,22 @@
          */
         private boolean extended;
 
-
         @Override
         public boolean isSticky() {
             return super.isSticky() || mGutsShownPinned;
         }
 
-        public void setEntry(@NonNull final NotificationEntry entry) {
-            Runnable removeHeadsUpRunnable = () -> {
+        public HeadsUpEntryPhone() {
+            super();
+        }
+
+        public HeadsUpEntryPhone(NotificationEntry entry) {
+            super(entry);
+        }
+
+        @Override
+        protected Runnable createRemoveRunnable(NotificationEntry entry) {
+            return  () -> {
                 if (!mVisualStabilityProvider.isReorderingAllowed()
                         // We don't want to allow reordering while pulsing, but headsup need to
                         // time out anyway
@@ -460,8 +480,6 @@
                     removeEntry(entry.getKey());
                 }
             };
-
-            setEntry(entry, removeHeadsUpRunnable);
         }
 
         @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java
index 94f62e0..f84efbb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java
@@ -16,7 +16,6 @@
 package com.android.systemui.statusbar.phone;
 
 import static com.android.systemui.Flags.newAodTransition;
-import static com.android.systemui.Flags.migrateClocksToBlueprint;
 
 import android.content.Context;
 import android.content.res.Resources;
@@ -41,6 +40,7 @@
 import com.android.systemui.demomode.DemoMode;
 import com.android.systemui.demomode.DemoModeController;
 import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.keyguard.MigrateClocksToBlueprint;
 import com.android.systemui.plugins.DarkIconDispatcher;
 import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -545,7 +545,7 @@
             return;
         }
         if (mScreenOffAnimationController.shouldAnimateAodIcons()) {
-            if (!migrateClocksToBlueprint()) {
+            if (!MigrateClocksToBlueprint.isEnabled()) {
                 mAodIcons.setTranslationY(-mAodIconAppearTranslation);
             }
             mAodIcons.setAlpha(0);
@@ -557,14 +557,14 @@
                     .start();
         } else {
             mAodIcons.setAlpha(1.0f);
-            if (!migrateClocksToBlueprint()) {
+            if (!MigrateClocksToBlueprint.isEnabled()) {
                 mAodIcons.setTranslationY(0);
             }
         }
     }
 
     private void animateInAodIconTranslation() {
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled()) {
             mAodIcons.animate()
                     .setInterpolator(Interpolators.DECELERATE_QUINT)
                     .translationY(0)
@@ -667,7 +667,7 @@
                 }
             } else {
                 mAodIcons.setAlpha(1.0f);
-                if (!migrateClocksToBlueprint()) {
+                if (!MigrateClocksToBlueprint.isEnabled()) {
                     mAodIcons.setTranslationY(0);
                 }
                 mAodIcons.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
index 67d2299..f3c7090 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
@@ -19,9 +19,9 @@
 import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD
 import com.android.systemui.DejankUtils
 import com.android.systemui.Flags.lightRevealMigration
-import com.android.systemui.Flags.migrateClocksToBlueprint
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.KeyguardViewMediator
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.keyguard.WakefulnessLifecycle
 import com.android.systemui.shade.ShadeViewController
 import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor
@@ -45,9 +45,7 @@
  */
 private const val ANIMATE_IN_KEYGUARD_DELAY = 600L
 
-/**
- * Duration for the light reveal portion of the animation.
- */
+/** Duration for the light reveal portion of the animation. */
 private const val LIGHT_REVEAL_ANIMATION_DURATION = 500L
 
 /**
@@ -58,7 +56,9 @@
  * and then animates in the AOD UI.
  */
 @SysUISingleton
-class UnlockedScreenOffAnimationController @Inject constructor(
+class UnlockedScreenOffAnimationController
+@Inject
+constructor(
     private val context: Context,
     private val wakefulnessLifecycle: WakefulnessLifecycle,
     private val statusBarStateControllerImpl: StatusBarStateControllerImpl,
@@ -95,52 +95,61 @@
      */
     private var decidedToAnimateGoingToSleep: Boolean? = null
 
-    private val lightRevealAnimator = ValueAnimator.ofFloat(1f, 0f).apply {
-        duration = LIGHT_REVEAL_ANIMATION_DURATION
-        interpolator = Interpolators.LINEAR
-        addUpdateListener {
-            if (lightRevealMigration()) return@addUpdateListener
-            if (lightRevealScrim.revealEffect !is CircleReveal) {
-                lightRevealScrim.revealAmount = it.animatedValue as Float
-            }
-            if (lightRevealScrim.isScrimAlmostOccludes &&
-                    interactionJankMonitor.isInstrumenting(CUJ_SCREEN_OFF)) {
-                // ends the instrument when the scrim almost occludes the screen.
-                // because the following janky frames might not be perceptible.
-                interactionJankMonitor.end(CUJ_SCREEN_OFF)
-            }
-        }
-        addListener(object : AnimatorListenerAdapter() {
-            override fun onAnimationCancel(animation: Animator) {
-                if (lightRevealMigration()) return
+    private val lightRevealAnimator =
+        ValueAnimator.ofFloat(1f, 0f).apply {
+            duration = LIGHT_REVEAL_ANIMATION_DURATION
+            interpolator = Interpolators.LINEAR
+            addUpdateListener {
+                if (lightRevealMigration()) return@addUpdateListener
                 if (lightRevealScrim.revealEffect !is CircleReveal) {
-                    lightRevealScrim.revealAmount = 1f
+                    lightRevealScrim.revealAmount = it.animatedValue as Float
+                }
+                if (
+                    lightRevealScrim.isScrimAlmostOccludes &&
+                        interactionJankMonitor.isInstrumenting(CUJ_SCREEN_OFF)
+                ) {
+                    // ends the instrument when the scrim almost occludes the screen.
+                    // because the following janky frames might not be perceptible.
+                    interactionJankMonitor.end(CUJ_SCREEN_OFF)
                 }
             }
+            addListener(
+                object : AnimatorListenerAdapter() {
+                    override fun onAnimationCancel(animation: Animator) {
+                        if (lightRevealMigration()) return
+                        if (lightRevealScrim.revealEffect !is CircleReveal) {
+                            lightRevealScrim.revealAmount = 1f
+                        }
+                    }
 
-            override fun onAnimationEnd(animation: Animator) {
-                lightRevealAnimationPlaying = false
-                interactionJankMonitor.end(CUJ_SCREEN_OFF)
-            }
+                    override fun onAnimationEnd(animation: Animator) {
+                        lightRevealAnimationPlaying = false
+                        interactionJankMonitor.end(CUJ_SCREEN_OFF)
+                    }
 
-            override fun onAnimationStart(animation: Animator) {
-                interactionJankMonitor.begin(
-                        notifShadeWindowControllerLazy.get().windowRootView, CUJ_SCREEN_OFF)
-            }
-        })
-    }
+                    override fun onAnimationStart(animation: Animator) {
+                        interactionJankMonitor.begin(
+                            notifShadeWindowControllerLazy.get().windowRootView,
+                            CUJ_SCREEN_OFF
+                        )
+                    }
+                }
+            )
+        }
 
     // FrameCallback used to delay starting the light reveal animation until the next frame
-    private val startLightRevealCallback = namedRunnable("startLightReveal") {
-        lightRevealAnimationPlaying = true
-        lightRevealAnimator.start()
-    }
-
-    private val animatorDurationScaleObserver = object : ContentObserver(null) {
-        override fun onChange(selfChange: Boolean) {
-            updateAnimatorDurationScale()
+    private val startLightRevealCallback =
+        namedRunnable("startLightReveal") {
+            lightRevealAnimationPlaying = true
+            lightRevealAnimator.start()
         }
-    }
+
+    private val animatorDurationScaleObserver =
+        object : ContentObserver(null) {
+            override fun onChange(selfChange: Boolean) {
+                updateAnimatorDurationScale()
+            }
+        }
 
     override fun initialize(
         centralSurfaces: CentralSurfaces,
@@ -154,22 +163,21 @@
 
         updateAnimatorDurationScale()
         globalSettings.registerContentObserver(
-                Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE),
-                /* notify for descendants */ false,
-                animatorDurationScaleObserver)
+            Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE),
+            /* notify for descendants */ false,
+            animatorDurationScaleObserver
+        )
         wakefulnessLifecycle.addObserver(this)
     }
 
     fun updateAnimatorDurationScale() {
-        animatorDurationScale = fixScale(
-                globalSettings.getFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 1f))
+        animatorDurationScale =
+            fixScale(globalSettings.getFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 1f))
     }
 
-    override fun shouldDelayKeyguardShow(): Boolean =
-        shouldPlayAnimation()
+    override fun shouldDelayKeyguardShow(): Boolean = shouldPlayAnimation()
 
-    override fun isKeyguardShowDelayed(): Boolean =
-        isAnimationPlaying()
+    override fun isKeyguardShowDelayed(): Boolean = isAnimationPlaying()
 
     /**
      * Animates in the provided keyguard view, ending in the same position that it will be in on
@@ -190,15 +198,21 @@
         // We animate the Y properly separately using the PropertyAnimator, as the panel
         // view also needs to update the end position.
         PropertyAnimator.cancelAnimation(keyguardView, AnimatableProperty.Y)
-        PropertyAnimator.setProperty(keyguardView, AnimatableProperty.Y, currentY,
-                AnimationProperties().setDuration(duration.toLong()),
-                true /* animate */)
+        PropertyAnimator.setProperty(
+            keyguardView,
+            AnimatableProperty.Y,
+            currentY,
+            AnimationProperties().setDuration(duration.toLong()),
+            true /* animate */
+        )
 
         // Cancel any existing CUJs before starting the animation
         interactionJankMonitor.cancel(CUJ_SCREEN_OFF_SHOW_AOD)
         PropertyAnimator.cancelAnimation(keyguardView, AnimatableProperty.ALPHA)
         PropertyAnimator.setProperty(
-            keyguardView, AnimatableProperty.ALPHA, 1f,
+            keyguardView,
+            AnimatableProperty.ALPHA,
+            1f,
             AnimationProperties()
                 .setDelay(0)
                 .setDuration(duration.toLong())
@@ -230,13 +244,14 @@
                     interactionJankMonitor.cancel(CUJ_SCREEN_OFF_SHOW_AOD)
                 }
                 .setCustomInterpolator(View.ALPHA, Interpolators.FAST_OUT_SLOW_IN),
-            true /* animate */)
-        val builder = InteractionJankMonitor.Configuration.Builder
-            .withView(
+            true /* animate */
+        )
+        val builder =
+            InteractionJankMonitor.Configuration.Builder.withView(
                     InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD,
                     checkNotNull(notifShadeWindowControllerLazy.get().windowRootView)
-            )
-            .setTag(statusBarStateControllerImpl.getClockId())
+                )
+                .setTag(statusBarStateControllerImpl.getClockId())
 
         interactionJankMonitor.begin(builder)
     }
@@ -284,25 +299,34 @@
             // chance of missing the first frame, so to mitigate this we should start the animation
             // on the next frame.
             DejankUtils.postAfterTraversal(startLightRevealCallback)
-            handler.postDelayed({
-                // Only run this callback if the device is sleeping (not interactive). This callback
-                // is removed in onStartedWakingUp, but since that event is asynchronously
-                // dispatched, a race condition could make it possible for this callback to be run
-                // as the device is waking up. That results in the AOD UI being shown while we wake
-                // up, with unpredictable consequences.
-                if (!powerManager.isInteractive(Display.DEFAULT_DISPLAY) &&
-                        shouldAnimateInKeyguard) {
-                    if (!migrateClocksToBlueprint()) {
-                        // Tracking this state should no longer be relevant, as the isInteractive
-                        // check covers it
-                        aodUiAnimationPlaying = true
-                    }
+            handler.postDelayed(
+                {
+                    // Only run this callback if the device is sleeping (not interactive). This
+                    // callback
+                    // is removed in onStartedWakingUp, but since that event is asynchronously
+                    // dispatched, a race condition could make it possible for this callback to be
+                    // run
+                    // as the device is waking up. That results in the AOD UI being shown while we
+                    // wake
+                    // up, with unpredictable consequences.
+                    if (
+                        !powerManager.isInteractive(Display.DEFAULT_DISPLAY) &&
+                            shouldAnimateInKeyguard
+                    ) {
+                        if (!MigrateClocksToBlueprint.isEnabled) {
+                            // Tracking this state should no longer be relevant, as the
+                            // isInteractive
+                            // check covers it
+                            aodUiAnimationPlaying = true
+                        }
 
-                    // Show AOD. That'll cause the KeyguardVisibilityHelper to call
-                    // #animateInKeyguard.
-                    shadeViewController.showAodUi()
-                }
-            }, (ANIMATE_IN_KEYGUARD_DELAY * animatorDurationScale).toLong())
+                        // Show AOD. That'll cause the KeyguardVisibilityHelper to call
+                        // #animateInKeyguard.
+                        shadeViewController.showAodUi()
+                    }
+                },
+                (ANIMATE_IN_KEYGUARD_DELAY * animatorDurationScale).toLong()
+            )
 
             return true
         } else {
@@ -335,8 +359,12 @@
         }
 
         // If animations are disabled system-wide, don't play this one either.
-        if (Settings.Global.getString(
-                context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE) == "0") {
+        if (
+            Settings.Global.getString(
+                context.contentResolver,
+                Settings.Global.ANIMATOR_DURATION_SCALE
+            ) == "0"
+        ) {
             return false
         }
 
@@ -360,8 +388,10 @@
         // If we're not allowed to rotate the keyguard, it can only be displayed in zero-degree
         // portrait. If we're in another orientation, disable the screen off animation so we don't
         // animate in the keyguard AOD UI sideways or upside down.
-        if (!keyguardStateController.isKeyguardScreenRotationAllowed &&
-            context.display?.rotation != Surface.ROTATION_0) {
+        if (
+            !keyguardStateController.isKeyguardScreenRotationAllowed &&
+                context.display?.rotation != Surface.ROTATION_0
+        ) {
             return false
         }
 
@@ -380,23 +410,18 @@
         return isScreenOffLightRevealAnimationPlaying() || aodUiAnimationPlaying
     }
 
-    override fun shouldAnimateInKeyguard(): Boolean =
-        shouldAnimateInKeyguard
+    override fun shouldAnimateInKeyguard(): Boolean = shouldAnimateInKeyguard
 
-    override fun shouldHideScrimOnWakeUp(): Boolean =
-        isScreenOffLightRevealAnimationPlaying()
+    override fun shouldHideScrimOnWakeUp(): Boolean = isScreenOffLightRevealAnimationPlaying()
 
     override fun overrideNotificationsDozeAmount(): Boolean =
         shouldPlayUnlockedScreenOffAnimation() && isAnimationPlaying()
 
-    override fun shouldShowAodIconsWhenShade(): Boolean =
-        isAnimationPlaying()
+    override fun shouldShowAodIconsWhenShade(): Boolean = isAnimationPlaying()
 
-    override fun shouldAnimateAodIcons(): Boolean =
-        shouldPlayUnlockedScreenOffAnimation()
+    override fun shouldAnimateAodIcons(): Boolean = shouldPlayUnlockedScreenOffAnimation()
 
-    override fun shouldPlayAnimation(): Boolean =
-        shouldPlayUnlockedScreenOffAnimation()
+    override fun shouldPlayAnimation(): Boolean = shouldPlayUnlockedScreenOffAnimation()
 
     /**
      * Whether the light reveal animation is playing. The second part of the screen off animation,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
index 50de3cb..6f7e046 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
@@ -39,6 +39,7 @@
 import com.android.systemui.res.R;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag;
+import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor;
 import com.android.systemui.util.ListenerSet;
 import com.android.systemui.util.concurrency.DelayableExecutor;
 import com.android.systemui.util.settings.GlobalSettings;
@@ -162,11 +163,7 @@
      */
     @Override
     public void showNotification(@NonNull NotificationEntry entry) {
-        HeadsUpEntry headsUpEntry = createHeadsUpEntry();
-
-        // Attach NotificationEntry for AvalancheController to log key and
-        // record mPostTime for AvalancheController sorting
-        headsUpEntry.setEntry(entry);
+        HeadsUpEntry headsUpEntry = createHeadsUpEntry(entry);
 
         Runnable runnable = () -> {
             // TODO(b/315362456) log outside runnable too
@@ -375,7 +372,7 @@
     }
 
     /**
-     * Remove a notification and reset the entry.
+     * Remove a notification from the alerting entries.
      * @param key key of notification to remove
      */
     protected final void removeEntry(@NonNull String key) {
@@ -395,7 +392,11 @@
             mHeadsUpEntryMap.remove(key);
             onEntryRemoved(headsUpEntry);
             entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
-            headsUpEntry.reset();
+            if (NotificationsHeadsUpRefactor.isEnabled()) {
+                headsUpEntry.cancelAutoRemovalCallbacks("removeEntry");
+            } else {
+                headsUpEntry.reset();
+            }
         };
         mAvalancheController.delete(headsUpEntry, runnable, "removeEntry");
     }
@@ -657,8 +658,8 @@
     }
 
     @NonNull
-    protected HeadsUpEntry createHeadsUpEntry() {
-        return new HeadsUpEntry();
+    protected HeadsUpEntry createHeadsUpEntry(NotificationEntry entry) {
+        return new HeadsUpEntry(entry);
     }
 
     /**
@@ -694,11 +695,23 @@
 
         @Nullable private Runnable mCancelRemoveRunnable;
 
-        public void setEntry(@NonNull final NotificationEntry entry) {
-            setEntry(entry, () -> removeEntry(entry.getKey()));
+        public HeadsUpEntry() {
+            NotificationsHeadsUpRefactor.assertInLegacyMode();
         }
 
-        public void setEntry(@NonNull final NotificationEntry entry,
+        public HeadsUpEntry(NotificationEntry entry) {
+            // Attach NotificationEntry for AvalancheController to log key and
+            // record mPostTime for AvalancheController sorting
+            setEntry(entry, createRemoveRunnable(entry));
+        }
+
+        /** Attach a NotificationEntry. */
+        public void setEntry(@NonNull final NotificationEntry entry) {
+            NotificationsHeadsUpRefactor.assertInLegacyMode();
+            setEntry(entry, createRemoveRunnable(entry));
+        }
+
+        private void setEntry(@NonNull final NotificationEntry entry,
                 @Nullable Runnable removeRunnable) {
             mEntry = entry;
             mRemoveRunnable = removeRunnable;
@@ -847,6 +860,7 @@
         }
 
         public void reset() {
+            NotificationsHeadsUpRefactor.assertInLegacyMode();
             cancelAutoRemovalCallbacks("reset()");
             mEntry = null;
             mRemoveRunnable = null;
@@ -919,6 +933,11 @@
             }
         }
 
+        /** Creates a runnable to remove this notification from the alerting entries. */
+        protected Runnable createRemoveRunnable(NotificationEntry entry) {
+            return () -> removeEntry(entry.getKey());
+        }
+
         /**
          * Calculate what the post time of a notification is at some current time.
          * @return the post time
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
index 087e100..7a57027 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
@@ -42,6 +42,10 @@
 import com.android.systemui.qs.tiles.impl.uimodenight.domain.interactor.UiModeNightTileDataInteractor
 import com.android.systemui.qs.tiles.impl.uimodenight.domain.interactor.UiModeNightTileUserActionInteractor
 import com.android.systemui.qs.tiles.impl.uimodenight.domain.model.UiModeNightTileModel
+import com.android.systemui.qs.tiles.impl.work.domain.interactor.WorkModeTileDataInteractor
+import com.android.systemui.qs.tiles.impl.work.domain.interactor.WorkModeTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.work.domain.model.WorkModeTileModel
+import com.android.systemui.qs.tiles.impl.work.ui.WorkModeTileMapper
 import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
 import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig
 import com.android.systemui.qs.tiles.viewmodel.QSTileViewModel
@@ -69,6 +73,7 @@
         const val LOCATION_TILE_SPEC = "location"
         const val ALARM_TILE_SPEC = "alarm"
         const val UIMODENIGHT_TILE_SPEC = "dark"
+        const val WORK_MODE_TILE_SPEC = "work"
 
         /** Inject flashlight config */
         @Provides
@@ -197,6 +202,38 @@
                 stateInteractor,
                 mapper,
             )
+
+        /** Inject work mode tile config */
+        @Provides
+        @IntoMap
+        @StringKey(WORK_MODE_TILE_SPEC)
+        fun provideWorkModeTileConfig(uiEventLogger: QsEventLogger): QSTileConfig =
+            QSTileConfig(
+                tileSpec = TileSpec.create(WORK_MODE_TILE_SPEC),
+                uiConfig =
+                    QSTileUIConfig.Resource(
+                        iconRes = com.android.internal.R.drawable.stat_sys_managed_profile_status,
+                        labelRes = R.string.quick_settings_work_mode_label,
+                    ),
+                instanceId = uiEventLogger.getNewInstanceId(),
+            )
+
+        /** Inject work mode into tileViewModelMap in QSModule */
+        @Provides
+        @IntoMap
+        @StringKey(WORK_MODE_TILE_SPEC)
+        fun provideWorkModeTileViewModel(
+            factory: QSTileViewModelFactory.Static<WorkModeTileModel>,
+            mapper: WorkModeTileMapper,
+            stateInteractor: WorkModeTileDataInteractor,
+            userActionInteractor: WorkModeTileUserActionInteractor
+        ): QSTileViewModel =
+            factory.create(
+                TileSpec.create(WORK_MODE_TILE_SPEC),
+                userActionInteractor,
+                stateInteractor,
+                mapper,
+            )
     }
 
     /** Inject FlashlightTile into tileMap in QSModule */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java
index 18ec68b..1f4c3cd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.policy;
 
+import static android.permission.flags.Flags.sensitiveNotificationAppProtection;
 import static android.provider.Settings.Global.DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS;
 
 import static com.android.server.notification.Flags.screenshareNotificationHiding;
@@ -23,6 +24,7 @@
 import android.annotation.MainThread;
 import android.app.IActivityManager;
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.database.ExecutorContentObserver;
 import android.media.projection.MediaProjectionInfo;
 import android.media.projection.MediaProjectionManager;
@@ -33,6 +35,9 @@
 import android.util.ArraySet;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Background;
@@ -52,6 +57,7 @@
         implements SensitiveNotificationProtectionController {
     private static final String LOG_TAG = "SNPC";
     private final SensitiveNotificationProtectionControllerLogger mLogger;
+    private final PackageManager mPackageManager;
     private final ArraySet<String> mExemptPackages = new ArraySet<>();
     private final ListenerSet<Runnable> mListeners = new ListenerSet<>();
     private volatile MediaProjectionInfo mProjection;
@@ -64,17 +70,7 @@
                 public void onStart(MediaProjectionInfo info) {
                     Trace.beginSection("SNPC.onProjectionStart");
                     try {
-                        if (mDisableScreenShareProtections) {
-                            Log.w(LOG_TAG,
-                                    "Screen share protections disabled, ignoring projectionstart");
-                            mLogger.logProjectionStart(false, info.getPackageName());
-                            return;
-                        }
-
-                        // Only enable sensitive content protection if sharing full screen
-                        // Launch cookie only set (non-null) if sharing single app/task
-                        updateProjectionStateAndNotifyListeners(
-                                (info.getLaunchCookie() == null) ? info : null);
+                        updateProjectionStateAndNotifyListeners(info);
                         mLogger.logProjectionStart(isSensitiveStateActive(), info.getPackageName());
                     } finally {
                         Trace.endSection();
@@ -99,10 +95,12 @@
             GlobalSettings settings,
             MediaProjectionManager mediaProjectionManager,
             IActivityManager activityManager,
+            PackageManager packageManager,
             @Main Handler mainHandler,
             @Background Executor bgExecutor,
             SensitiveNotificationProtectionControllerLogger logger) {
         mLogger = logger;
+        mPackageManager = packageManager;
 
         if (!screenshareNotificationHiding()) {
             return;
@@ -168,7 +166,7 @@
         mExemptPackages.addAll(exemptPackages);
 
         if (mProjection != null) {
-            mListeners.forEach(Runnable::run);
+            updateProjectionStateAndNotifyListeners(mProjection);
         }
     }
 
@@ -177,13 +175,13 @@
      * listeners
      */
     @MainThread
-    private void updateProjectionStateAndNotifyListeners(MediaProjectionInfo info) {
+    private void updateProjectionStateAndNotifyListeners(@Nullable MediaProjectionInfo info) {
         Assert.isMainThread();
         // capture previous state
         boolean wasSensitive = isSensitiveStateActive();
 
         // update internal state
-        mProjection = info;
+        mProjection = getNonExemptProjectionInfo(info);
 
         // if either previous or new state is sensitive, notify listeners.
         if (wasSensitive || isSensitiveStateActive()) {
@@ -191,6 +189,36 @@
         }
     }
 
+    private MediaProjectionInfo getNonExemptProjectionInfo(@Nullable MediaProjectionInfo info) {
+        if (mDisableScreenShareProtections) {
+            Log.w(LOG_TAG, "Screen share protections disabled");
+            return null;
+        } else if (info != null && mExemptPackages.contains(info.getPackageName())) {
+            Log.w(LOG_TAG, "Screen share protections exempt for package " + info.getPackageName());
+            return null;
+        } else if (info != null && canRecordSensitiveContent(info.getPackageName())) {
+            Log.w(LOG_TAG, "Screen share protections exempt for package " + info.getPackageName()
+                    + " via permission");
+            return null;
+        } else if (info != null && info.getLaunchCookie() != null) {
+            // Only enable sensitive content protection if sharing full screen
+            // Launch cookie only set (non-null) if sharing single app/task
+            Log.w(LOG_TAG, "Screen share protections exempt for single app screenshare");
+            return null;
+        }
+        return info;
+    }
+
+    private boolean canRecordSensitiveContent(@NonNull String packageName) {
+        // RECORD_SENSITIVE_CONTENT is flagged api on sensitiveNotificationAppProtection
+        if (sensitiveNotificationAppProtection()) {
+            return mPackageManager.checkPermission(
+                            android.Manifest.permission.RECORD_SENSITIVE_CONTENT, packageName)
+                    == PackageManager.PERMISSION_GRANTED;
+        }
+        return false;
+    }
+
     @Override
     public void registerSensitiveStateListener(Runnable onSensitiveStateChanged) {
         mListeners.addIfAbsent(onSensitiveStateChanged);
@@ -201,15 +229,9 @@
         mListeners.remove(onSensitiveStateChanged);
     }
 
-    // TODO(b/323396693): opportunity for optimization
     @Override
     public boolean isSensitiveStateActive() {
-        MediaProjectionInfo projection = mProjection;
-        if (projection == null) {
-            return false;
-        }
-
-        return !mExemptPackages.contains(projection.getPackageName());
+        return mProjection != null;
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/DisposableHandles.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/DisposableHandles.kt
new file mode 100644
index 0000000..de036ea
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/DisposableHandles.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.util.kotlin
+
+import kotlinx.coroutines.DisposableHandle
+
+/** A mutable collection of [DisposableHandle] objects that is itself a [DisposableHandle] */
+class DisposableHandles : DisposableHandle {
+    private val handles = mutableListOf<DisposableHandle>()
+
+    /** Add the provided handles to this collection. */
+    fun add(vararg handles: DisposableHandle) {
+        this.handles.addAll(handles)
+    }
+
+    /** Same as [add] */
+    operator fun plusAssign(handle: DisposableHandle) {
+        this.handles.add(handle)
+    }
+
+    /** Same as [add] */
+    operator fun plusAssign(handles: Iterable<DisposableHandle>) {
+        this.handles.addAll(handles)
+    }
+
+    /** [dispose] the current contents, then [add] the provided [handles] */
+    fun replaceAll(vararg handles: DisposableHandle) {
+        dispose()
+        add(*handles)
+    }
+
+    /** Dispose of all added handles and empty this collection. */
+    override fun dispose() {
+        handles.forEach { it.dispose() }
+        handles.clear()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/ManagedProfileControllerExt.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/ManagedProfileControllerExt.kt
new file mode 100644
index 0000000..7a2f9b2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/ManagedProfileControllerExt.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.kotlin
+
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.statusbar.phone.ManagedProfileController
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+
+val ManagedProfileController.hasActiveWorkProfile: Flow<Boolean>
+    get() = conflatedCallbackFlow {
+        val callback =
+            object : ManagedProfileController.Callback {
+                override fun onManagedProfileChanged() {
+                    trySend(hasActiveProfile())
+                }
+                override fun onManagedProfileRemoved() {
+                    // no-op, because the other callback will also be called.
+                }
+            }
+        addCallback(callback) // calls onManagedProfileChanged
+        awaitClose { removeCallback(callback) }
+    }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt
index d134e60..155102c9 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt
@@ -21,7 +21,6 @@
 import com.android.settingslib.volume.data.repository.LocalMediaRepository
 import com.android.settingslib.volume.data.repository.MediaControllerRepository
 import com.android.settingslib.volume.data.repository.MediaControllerRepositoryImpl
-import com.android.settingslib.volume.domain.interactor.LocalMediaInteractor
 import com.android.settingslib.volume.shared.AudioManagerEventsReceiver
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
@@ -52,13 +51,6 @@
 
         @Provides
         @SysUISingleton
-        fun provideLocalMediaInteractor(
-            repository: LocalMediaRepository,
-            @Application scope: CoroutineScope,
-        ): LocalMediaInteractor = LocalMediaInteractor(repository, scope)
-
-        @Provides
-        @SysUISingleton
         fun provideMediaDeviceSessionRepository(
             intentsReceiver: AudioManagerEventsReceiver,
             mediaSessionManager: MediaSessionManager,
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt
index 11b4690..e052f24 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt
@@ -15,15 +15,12 @@
  */
 package com.android.systemui.volume.panel.component.mediaoutput.data.repository
 
-import android.media.MediaRouter2Manager
 import com.android.settingslib.volume.data.repository.LocalMediaRepository
 import com.android.settingslib.volume.data.repository.LocalMediaRepositoryImpl
 import com.android.settingslib.volume.shared.AudioManagerEventsReceiver
 import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.media.controls.util.LocalMediaManagerFactory
 import javax.inject.Inject
-import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.CoroutineScope
 
 interface LocalMediaRepositoryFactory {
@@ -35,18 +32,14 @@
 @Inject
 constructor(
     private val eventsReceiver: AudioManagerEventsReceiver,
-    private val mediaRouter2Manager: MediaRouter2Manager,
     private val localMediaManagerFactory: LocalMediaManagerFactory,
     @Application private val coroutineScope: CoroutineScope,
-    @Background private val backgroundCoroutineContext: CoroutineContext,
 ) : LocalMediaRepositoryFactory {
 
     override fun create(packageName: String?): LocalMediaRepository =
         LocalMediaRepositoryImpl(
             eventsReceiver,
             localMediaManagerFactory.create(packageName),
-            mediaRouter2Manager,
             coroutineScope,
-            backgroundCoroutineContext,
         )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractor.kt
new file mode 100644
index 0000000..b0c8a4a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractor.kt
@@ -0,0 +1,108 @@
+/*
+ * 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.volume.panel.component.mediaoutput.domain.interactor
+
+import android.media.session.MediaController
+import android.media.session.PlaybackState
+import android.os.Handler
+import com.android.settingslib.volume.data.repository.MediaControllerChange
+import com.android.settingslib.volume.data.repository.MediaControllerRepository
+import com.android.settingslib.volume.data.repository.stateChanges
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSession
+import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.FlowCollector
+import kotlinx.coroutines.flow.filterIsInstance
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.withContext
+
+/** Allows to observe and change [MediaDeviceSession] state. */
+@OptIn(ExperimentalCoroutinesApi::class)
+@VolumePanelScope
+class MediaDeviceSessionInteractor
+@Inject
+constructor(
+    @Background private val backgroundCoroutineContext: CoroutineContext,
+    @Background private val backgroundHandler: Handler,
+    private val mediaControllerRepository: MediaControllerRepository,
+) {
+
+    /** [PlaybackState] changes for the [MediaDeviceSession]. */
+    fun playbackState(session: MediaDeviceSession): Flow<PlaybackState?> {
+        return stateChanges(session) {
+                emit(MediaControllerChange.PlaybackStateChanged(it.playbackState))
+            }
+            .filterIsInstance(MediaControllerChange.PlaybackStateChanged::class)
+            .map { it.state }
+    }
+
+    /** [MediaController.PlaybackInfo] changes for the [MediaDeviceSession]. */
+    fun playbackInfo(session: MediaDeviceSession): Flow<MediaController.PlaybackInfo?> {
+        return stateChanges(session) {
+                emit(MediaControllerChange.AudioInfoChanged(it.playbackInfo))
+            }
+            .filterIsInstance(MediaControllerChange.AudioInfoChanged::class)
+            .map { it.info }
+    }
+
+    private fun stateChanges(
+        session: MediaDeviceSession,
+        onStart: suspend FlowCollector<MediaControllerChange>.(controller: MediaController) -> Unit,
+    ): Flow<MediaControllerChange?> =
+        mediaControllerRepository.activeSessions
+            .flatMapLatest { controllers ->
+                val controller: MediaController =
+                    findControllerForSession(controllers, session)
+                        ?: return@flatMapLatest flowOf(null)
+                controller.stateChanges(backgroundHandler).onStart { onStart(controller) }
+            }
+            .flowOn(backgroundCoroutineContext)
+
+    /** Set [MediaDeviceSession] volume to [volume]. */
+    suspend fun setSessionVolume(mediaDeviceSession: MediaDeviceSession, volume: Int): Boolean {
+        if (!mediaDeviceSession.canAdjustVolume) {
+            return false
+        }
+        return withContext(backgroundCoroutineContext) {
+            val controller =
+                findControllerForSession(
+                    mediaControllerRepository.activeSessions.value,
+                    mediaDeviceSession,
+                )
+            if (controller == null) {
+                false
+            } else {
+                controller.setVolumeTo(volume, 0)
+                true
+            }
+        }
+    }
+
+    private fun findControllerForSession(
+        controllers: Collection<MediaController>,
+        mediaDeviceSession: MediaDeviceSession,
+    ): MediaController? =
+        controllers.firstOrNull { it.sessionToken == mediaDeviceSession.sessionToken }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt
index cb16abe..ea4c082 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt
@@ -33,23 +33,15 @@
     private val mediaOutputDialogManager: MediaOutputDialogManager,
 ) {
 
-    fun onBarClick(session: MediaDeviceSession, expandable: Expandable) {
-        when (session) {
-            is MediaDeviceSession.Active -> {
-                mediaOutputDialogManager.createAndShowWithController(
-                    session.packageName,
-                    false,
-                    expandable.dialogController()
-                )
-            }
-            is MediaDeviceSession.Inactive -> {
-                mediaOutputDialogManager.createAndShowForSystemRouting(
-                    expandable.dialogController()
-                )
-            }
-            else -> {
-                /* do nothing */
-            }
+    fun onBarClick(session: MediaDeviceSession, isPlaybackActive: Boolean, expandable: Expandable) {
+        if (isPlaybackActive) {
+            mediaOutputDialogManager.createAndShowWithController(
+                session.packageName,
+                false,
+                expandable.dialogController()
+            )
+        } else {
+            mediaOutputDialogManager.createAndShowForSystemRouting(expandable.dialogController())
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt
index 0f53437..e60139e 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt
@@ -17,17 +17,16 @@
 package com.android.systemui.volume.panel.component.mediaoutput.domain.interactor
 
 import android.content.pm.PackageManager
+import android.media.VolumeProvider
 import android.media.session.MediaController
-import android.os.Handler
 import android.util.Log
 import com.android.settingslib.media.MediaDevice
 import com.android.settingslib.volume.data.repository.LocalMediaRepository
-import com.android.settingslib.volume.data.repository.MediaControllerChange
 import com.android.settingslib.volume.data.repository.MediaControllerRepository
-import com.android.settingslib.volume.data.repository.stateChanges
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.volume.panel.component.mediaoutput.data.repository.LocalMediaRepositoryFactory
 import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSession
+import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSessions
 import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
 import javax.inject.Inject
 import kotlin.coroutines.CoroutineContext
@@ -38,12 +37,9 @@
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.filterIsInstance
 import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.shareIn
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.withContext
@@ -58,35 +54,40 @@
     private val packageManager: PackageManager,
     @VolumePanelScope private val coroutineScope: CoroutineScope,
     @Background private val backgroundCoroutineContext: CoroutineContext,
-    @Background private val backgroundHandler: Handler,
-    mediaControllerRepository: MediaControllerRepository
+    mediaControllerRepository: MediaControllerRepository,
 ) {
 
-    /** Current [MediaDeviceSession]. Emits when the session playback changes. */
-    val mediaDeviceSession: StateFlow<MediaDeviceSession> =
-        mediaControllerRepository.activeLocalMediaController
-            .flatMapLatest { it?.mediaDeviceSession() ?: flowOf(MediaDeviceSession.Inactive) }
-            .flowOn(backgroundCoroutineContext)
-            .stateIn(coroutineScope, SharingStarted.Eagerly, MediaDeviceSession.Inactive)
+    private val activeMediaControllers: Flow<MediaControllers> =
+        mediaControllerRepository.activeSessions
+            .map { getMediaControllers(it) }
+            .shareIn(coroutineScope, SharingStarted.Eagerly, replay = 1)
 
-    private fun MediaController.mediaDeviceSession(): Flow<MediaDeviceSession> {
-        return stateChanges(backgroundHandler)
-            .onStart { emit(MediaControllerChange.PlaybackStateChanged(playbackState)) }
-            .filterIsInstance<MediaControllerChange.PlaybackStateChanged>()
+    /** [MediaDeviceSessions] that contains currently active sessions. */
+    val activeMediaDeviceSessions: Flow<MediaDeviceSessions> =
+        activeMediaControllers.map {
+            MediaDeviceSessions(
+                local = it.local?.mediaDeviceSession(),
+                remote = it.remote?.mediaDeviceSession()
+            )
+        }
+
+    /** Returns the default [MediaDeviceSession] from [activeMediaDeviceSessions] */
+    val defaultActiveMediaSession: StateFlow<MediaDeviceSession?> =
+        activeMediaControllers
             .map {
-                MediaDeviceSession.Active(
-                    appLabel = getApplicationLabel(packageName)
-                            ?: return@map MediaDeviceSession.Inactive,
-                    packageName = packageName,
-                    sessionToken = sessionToken,
-                    playbackState = playbackState,
-                )
+                when {
+                    it.local?.playbackState?.isActive == true -> it.local.mediaDeviceSession()
+                    it.remote?.playbackState?.isActive == true -> it.remote.mediaDeviceSession()
+                    it.local != null -> it.local.mediaDeviceSession()
+                    else -> null
+                }
             }
-    }
+            .flowOn(backgroundCoroutineContext)
+            .stateIn(coroutineScope, SharingStarted.Eagerly, null)
 
     private val localMediaRepository: SharedFlow<LocalMediaRepository> =
-        mediaDeviceSession
-            .map { (it as? MediaDeviceSession.Active)?.packageName }
+        defaultActiveMediaSession
+            .map { it?.packageName }
             .distinctUntilChanged()
             .map { localMediaRepositoryFactory.create(it) }
             .shareIn(coroutineScope, SharingStarted.Eagerly, replay = 1)
@@ -111,6 +112,54 @@
         }
     }
 
+    /** Finds local and remote media controllers. */
+    private fun getMediaControllers(
+        controllers: Collection<MediaController>,
+    ): MediaControllers {
+        var localController: MediaController? = null
+        var remoteController: MediaController? = null
+        val remoteMediaSessions: MutableSet<String> = mutableSetOf()
+        for (controller in controllers) {
+            val playbackInfo: MediaController.PlaybackInfo = controller.playbackInfo ?: continue
+            when (playbackInfo.playbackType) {
+                MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE -> {
+                    // MediaController can't be local if there is a remote one for the same package
+                    if (localController?.packageName.equals(controller.packageName)) {
+                        localController = null
+                    }
+                    if (!remoteMediaSessions.contains(controller.packageName)) {
+                        remoteMediaSessions.add(controller.packageName)
+                        if (remoteController == null) {
+                            remoteController = controller
+                        }
+                    }
+                }
+                MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL -> {
+                    if (controller.packageName in remoteMediaSessions) continue
+                    if (localController != null) continue
+                    localController = controller
+                }
+            }
+        }
+        return MediaControllers(local = localController, remote = remoteController)
+    }
+
+    private suspend fun MediaController.mediaDeviceSession(): MediaDeviceSession? {
+        return MediaDeviceSession(
+            packageName = packageName,
+            sessionToken = sessionToken,
+            canAdjustVolume =
+                playbackInfo != null &&
+                    playbackInfo?.volumeControl != VolumeProvider.VOLUME_CONTROL_FIXED,
+            appLabel = getApplicationLabel(packageName) ?: return null
+        )
+    }
+
+    private data class MediaControllers(
+        val local: MediaController?,
+        val remote: MediaController?,
+    )
+
     private companion object {
         const val TAG = "MediaOutputInteractor"
     }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt
index 1bceee9..2a2ce79 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt
@@ -17,26 +17,15 @@
 package com.android.systemui.volume.panel.component.mediaoutput.domain.model
 
 import android.media.session.MediaSession
-import android.media.session.PlaybackState
 
 /** Represents media playing on the connected device. */
-sealed interface MediaDeviceSession {
+data class MediaDeviceSession(
+    val appLabel: CharSequence,
+    val packageName: String,
+    val sessionToken: MediaSession.Token,
+    val canAdjustVolume: Boolean,
+)
 
-    /** Media is playing. */
-    data class Active(
-        val appLabel: CharSequence,
-        val packageName: String,
-        val sessionToken: MediaSession.Token,
-        val playbackState: PlaybackState?,
-    ) : MediaDeviceSession
-
-    /** Media is not playing. */
-    data object Inactive : MediaDeviceSession
-
-    /** Current media state is unknown yet. */
-    data object Unknown : MediaDeviceSession
-}
-
-/** Returns true when the audio is playing for the [MediaDeviceSession]. */
-fun MediaDeviceSession.isPlaying(): Boolean =
-    this is MediaDeviceSession.Active && playbackState?.isActive == true
+/** Returns true when [other] controls the same sessions as [this]. */
+fun MediaDeviceSession.isTheSameSession(other: MediaDeviceSession?): Boolean =
+    sessionToken == other?.sessionToken
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSessions.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSessions.kt
new file mode 100644
index 0000000..ddc0784
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSessions.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.volume.panel.component.mediaoutput.domain.model
+
+/** Models a pair of local and remote [MediaDeviceSession]s. */
+data class MediaDeviceSessions(
+    val local: MediaDeviceSession?,
+    val remote: MediaDeviceSession?,
+) {
+
+    companion object {
+        /** Returns [MediaDeviceSessions.local]. */
+        val Local: (MediaDeviceSessions) -> MediaDeviceSession? = { it.local }
+        /** Returns [MediaDeviceSessions.remote]. */
+        val Remote: (MediaDeviceSessions) -> MediaDeviceSession? = { it.remote }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt
index d49cb1e..2530a3a 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt
@@ -17,24 +17,30 @@
 package com.android.systemui.volume.panel.component.mediaoutput.ui.viewmodel
 
 import android.content.Context
+import android.media.session.PlaybackState
 import com.android.systemui.animation.Expandable
 import com.android.systemui.common.shared.model.Color
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.res.R
+import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaDeviceSessionInteractor
 import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputActionsInteractor
 import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor
 import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSession
-import com.android.systemui.volume.panel.component.mediaoutput.domain.model.isPlaying
 import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
 import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
 
 /** Models the UI of the Media Output Volume Panel component. */
+@OptIn(ExperimentalCoroutinesApi::class)
 @VolumePanelScope
 class MediaOutputViewModel
 @Inject
@@ -43,25 +49,36 @@
     @VolumePanelScope private val coroutineScope: CoroutineScope,
     private val volumePanelViewModel: VolumePanelViewModel,
     private val actionsInteractor: MediaOutputActionsInteractor,
+    private val mediaDeviceSessionInteractor: MediaDeviceSessionInteractor,
     interactor: MediaOutputInteractor,
 ) {
 
-    private val mediaDeviceSession: StateFlow<MediaDeviceSession> =
-        interactor.mediaDeviceSession.stateIn(
-            coroutineScope,
-            SharingStarted.Eagerly,
-            MediaDeviceSession.Unknown,
-        )
+    private val sessionWithPlayback: StateFlow<SessionWithPlayback?> =
+        interactor.defaultActiveMediaSession
+            .flatMapLatest { session ->
+                if (session == null) {
+                    flowOf(null)
+                } else {
+                    mediaDeviceSessionInteractor.playbackState(session).map { playback ->
+                        playback?.let { SessionWithPlayback(session, it) }
+                    }
+                }
+            }
+            .stateIn(
+                coroutineScope,
+                SharingStarted.Eagerly,
+                null,
+            )
 
     val connectedDeviceViewModel: StateFlow<ConnectedDeviceViewModel?> =
-        combine(mediaDeviceSession, interactor.currentConnectedDevice) {
+        combine(sessionWithPlayback, interactor.currentConnectedDevice) {
                 mediaDeviceSession,
                 currentConnectedDevice ->
                 ConnectedDeviceViewModel(
-                    if (mediaDeviceSession.isPlaying()) {
+                    if (mediaDeviceSession?.playback?.isActive == true) {
                         context.getString(
                             R.string.media_output_label_title,
-                            (mediaDeviceSession as MediaDeviceSession.Active).appLabel
+                            mediaDeviceSession.session.appLabel
                         )
                     } else {
                         context.getString(R.string.media_output_title_without_playing)
@@ -76,10 +93,10 @@
             )
 
     val deviceIconViewModel: StateFlow<DeviceIconViewModel?> =
-        combine(mediaDeviceSession, interactor.currentConnectedDevice) {
+        combine(sessionWithPlayback, interactor.currentConnectedDevice) {
                 mediaDeviceSession,
                 currentConnectedDevice ->
-                if (mediaDeviceSession.isPlaying()) {
+                if (mediaDeviceSession?.playback?.isActive == true) {
                     val icon =
                         currentConnectedDevice?.icon?.let { Icon.Loaded(it, null) }
                             ?: Icon.Resource(
@@ -112,7 +129,14 @@
             )
 
     fun onBarClick(expandable: Expandable) {
-        actionsInteractor.onBarClick(mediaDeviceSession.value, expandable)
+        sessionWithPlayback.value?.let {
+            actionsInteractor.onBarClick(it.session, it.playback.isActive, expandable)
+        }
         volumePanelViewModel.dismissPanel()
     }
+
+    private data class SessionWithPlayback(
+        val session: MediaDeviceSession,
+        val playback: PlaybackState,
+    )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/interactor/CastVolumeInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/interactor/CastVolumeInteractor.kt
deleted file mode 100644
index 6b62074..0000000
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/interactor/CastVolumeInteractor.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * 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.volume.panel.component.volume.domain.interactor
-
-import com.android.settingslib.volume.domain.interactor.LocalMediaInteractor
-import com.android.settingslib.volume.domain.model.RoutingSession
-import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
-
-/** Provides a remote media casting state. */
-@VolumePanelScope
-class CastVolumeInteractor
-@Inject
-constructor(
-    @VolumePanelScope private val coroutineScope: CoroutineScope,
-    private val localMediaInteractor: LocalMediaInteractor,
-) {
-
-    /** Returns a list of [RoutingSession] to show in the UI. */
-    val remoteRoutingSessions: StateFlow<List<RoutingSession>> =
-        localMediaInteractor.remoteRoutingSessions
-            .map { it.filter { routingSession -> routingSession.isVolumeSeekBarEnabled } }
-            .stateIn(coroutineScope, SharingStarted.Eagerly, emptyList())
-
-    /** Sets [routingSession] volume to [volume]. */
-    suspend fun setVolume(routingSession: RoutingSession, volume: Int) {
-        localMediaInteractor.adjustSessionVolume(routingSession.routingSessionInfo.id, volume)
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
index 1b73208..3242c28 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
@@ -80,7 +80,7 @@
             ) { model, isEnabled, ringerMode ->
                 model.toState(isEnabled, ringerMode)
             }
-            .stateIn(coroutineScope, SharingStarted.Eagerly, EmptyState)
+            .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty)
 
     override fun onValueChanged(state: SliderState, newValue: Float) {
         val audioViewModel = state as? State
@@ -116,6 +116,7 @@
             isEnabled = isEnabled,
             a11yStep = volumeRange.step,
             audioStreamModel = this,
+            isMutable = audioVolumeInteractor.isMutable(audioStream),
         )
     }
 
@@ -160,20 +161,10 @@
         override val disabledMessage: String?,
         override val isEnabled: Boolean,
         override val a11yStep: Int,
+        override val isMutable: Boolean,
         val audioStreamModel: AudioStreamModel,
     ) : SliderState
 
-    private data object EmptyState : SliderState {
-        override val value: Float = 0f
-        override val valueRange: ClosedFloatingPointRange<Float> = 0f..1f
-        override val icon: Icon? = null
-        override val valueText: String = ""
-        override val label: String = ""
-        override val disabledMessage: String? = null
-        override val a11yStep: Int = 0
-        override val isEnabled: Boolean = true
-    }
-
     @AssistedFactory
     interface Factory {
 
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt
index 86b2d73..73c8bbf 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt
@@ -17,11 +17,11 @@
 package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel
 
 import android.content.Context
-import com.android.settingslib.volume.domain.model.RoutingSession
+import android.media.session.MediaController.PlaybackInfo
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.res.R
-import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor
-import com.android.systemui.volume.panel.component.volume.domain.interactor.CastVolumeInteractor
+import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaDeviceSessionInteractor
+import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSession
 import com.android.systemui.volume.panel.component.volume.domain.interactor.VolumeSliderInteractor
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
@@ -30,30 +30,29 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.mapNotNull
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
 
 class CastVolumeSliderViewModel
 @AssistedInject
 constructor(
-    @Assisted private val routingSession: RoutingSession,
+    @Assisted private val session: MediaDeviceSession,
     @Assisted private val coroutineScope: CoroutineScope,
     private val context: Context,
-    mediaOutputInteractor: MediaOutputInteractor,
+    private val mediaDeviceSessionInteractor: MediaDeviceSessionInteractor,
     private val volumeSliderInteractor: VolumeSliderInteractor,
-    private val castVolumeInteractor: CastVolumeInteractor,
 ) : SliderViewModel {
 
-    private val volumeRange = 0..routingSession.routingSessionInfo.volumeMax
-
     override val slider: StateFlow<SliderState> =
-        combine(mediaOutputInteractor.currentConnectedDevice) { _ -> getCurrentState() }
-            .stateIn(coroutineScope, SharingStarted.Eagerly, getCurrentState())
+        mediaDeviceSessionInteractor
+            .playbackInfo(session)
+            .mapNotNull { it?.getCurrentState() }
+            .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty)
 
     override fun onValueChanged(state: SliderState, newValue: Float) {
         coroutineScope.launch {
-            castVolumeInteractor.setVolume(routingSession, newValue.roundToInt())
+            mediaDeviceSessionInteractor.setSessionVolume(session, newValue.roundToInt())
         }
     }
 
@@ -61,15 +60,16 @@
         // do nothing because this action isn't supported for Cast sliders.
     }
 
-    private fun getCurrentState(): State =
-        State(
-            value = routingSession.routingSessionInfo.volume.toFloat(),
+    private fun PlaybackInfo.getCurrentState(): State {
+        val volumeRange = 0..maxVolume
+        return State(
+            value = currentVolume.toFloat(),
             valueRange = volumeRange.first.toFloat()..volumeRange.last.toFloat(),
             icon = Icon.Resource(R.drawable.ic_cast, null),
             valueText =
                 SliderViewModel.formatValue(
                     volumeSliderInteractor.processVolumeToValue(
-                        volume = routingSession.routingSessionInfo.volume,
+                        volume = currentVolume,
                         volumeRange = volumeRange,
                     )
                 ),
@@ -77,6 +77,7 @@
             isEnabled = true,
             a11yStep = 1
         )
+    }
 
     private data class State(
         override val value: Float,
@@ -89,13 +90,15 @@
     ) : SliderState {
         override val disabledMessage: String?
             get() = null
+        override val isMutable: Boolean
+            get() = false
     }
 
     @AssistedFactory
     interface Factory {
 
         fun create(
-            routingSession: RoutingSession,
+            session: MediaDeviceSession,
             coroutineScope: CoroutineScope,
         ): CastVolumeSliderViewModel
     }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt
index b87d0a7..8eb0b89 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt
@@ -36,4 +36,17 @@
      */
     val a11yStep: Int
     val disabledMessage: String?
+    val isMutable: Boolean
+
+    data object Empty : SliderState {
+        override val value: Float = 0f
+        override val valueRange: ClosedFloatingPointRange<Float> = 0f..1f
+        override val icon: Icon? = null
+        override val valueText: String = ""
+        override val label: String = ""
+        override val disabledMessage: String? = null
+        override val a11yStep: Int = 0
+        override val isEnabled: Boolean = true
+        override val isMutable: Boolean = false
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt
index aaee24b..4e9a456 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt
@@ -18,9 +18,10 @@
 
 import android.media.AudioManager
 import com.android.settingslib.volume.shared.model.AudioStream
+import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaDeviceSessionInteractor
 import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor
-import com.android.systemui.volume.panel.component.mediaoutput.domain.model.isPlaying
-import com.android.systemui.volume.panel.component.volume.domain.interactor.CastVolumeInteractor
+import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSession
+import com.android.systemui.volume.panel.component.mediaoutput.domain.model.isTheSameSession
 import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.AudioStreamSliderViewModel
 import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.CastVolumeSliderViewModel
 import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderViewModel
@@ -29,17 +30,15 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.combineTransform
+import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.merge
-import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.flow.transformLatest
 import kotlinx.coroutines.launch
 
 /**
@@ -52,50 +51,34 @@
 @Inject
 constructor(
     @VolumePanelScope private val scope: CoroutineScope,
-    castVolumeInteractor: CastVolumeInteractor,
     mediaOutputInteractor: MediaOutputInteractor,
+    private val mediaDeviceSessionInteractor: MediaDeviceSessionInteractor,
     private val streamSliderViewModelFactory: AudioStreamSliderViewModel.Factory,
     private val castVolumeSliderViewModelFactory: CastVolumeSliderViewModel.Factory,
 ) {
 
-    private val remoteSessionsViewModels: Flow<List<SliderViewModel>> =
-        castVolumeInteractor.remoteRoutingSessions.transformLatest { routingSessions ->
-            coroutineScope {
-                emit(
-                    routingSessions.map { routingSession ->
-                        castVolumeSliderViewModelFactory.create(routingSession, this)
-                    }
-                )
-            }
-        }
-    private val streamViewModels: Flow<List<SliderViewModel>> =
-        flowOf(
-                listOf(
-                    AudioStream(AudioManager.STREAM_MUSIC),
-                    AudioStream(AudioManager.STREAM_VOICE_CALL),
-                    AudioStream(AudioManager.STREAM_RING),
-                    AudioStream(AudioManager.STREAM_NOTIFICATION),
-                    AudioStream(AudioManager.STREAM_ALARM),
-                )
-            )
-            .transformLatest { streams ->
-                coroutineScope {
-                    emit(
-                        streams.map { stream ->
-                            streamSliderViewModelFactory.create(
-                                AudioStreamSliderViewModel.FactoryAudioStreamWrapper(stream),
-                                this,
-                            )
-                        }
-                    )
-                }
-            }
-
     val sliderViewModels: StateFlow<List<SliderViewModel>> =
-        combine(remoteSessionsViewModels, streamViewModels) {
-                remoteSessionsViewModels,
-                streamViewModels ->
-                remoteSessionsViewModels + streamViewModels
+        combineTransform(
+                mediaOutputInteractor.activeMediaDeviceSessions,
+                mediaOutputInteractor.defaultActiveMediaSession,
+            ) { activeSessions, defaultSession ->
+                coroutineScope {
+                    val viewModels = buildList {
+                        if (defaultSession?.isTheSameSession(activeSessions.remote) == true) {
+                            addRemoteViewModelIfNeeded(this, activeSessions.remote)
+                            addStreamViewModel(this, AudioManager.STREAM_MUSIC)
+                        } else {
+                            addStreamViewModel(this, AudioManager.STREAM_MUSIC)
+                            addRemoteViewModelIfNeeded(this, activeSessions.remote)
+                        }
+
+                        addStreamViewModel(this, AudioManager.STREAM_VOICE_CALL)
+                        addStreamViewModel(this, AudioManager.STREAM_RING)
+                        addStreamViewModel(this, AudioManager.STREAM_NOTIFICATION)
+                        addStreamViewModel(this, AudioManager.STREAM_ALARM)
+                    }
+                    emit(viewModels)
+                }
             }
             .stateIn(scope, SharingStarted.Eagerly, emptyList())
 
@@ -103,12 +86,41 @@
 
     val isExpanded: StateFlow<Boolean> =
         merge(
-                mutableIsExpanded.onStart { emit(false) },
-                mediaOutputInteractor.mediaDeviceSession.map { !it.isPlaying() },
+                mutableIsExpanded,
+                mediaOutputInteractor.defaultActiveMediaSession.flatMapLatest {
+                    if (it == null) flowOf(true)
+                    else mediaDeviceSessionInteractor.playbackState(it).map { it?.isActive != true }
+                },
             )
             .stateIn(scope, SharingStarted.Eagerly, false)
 
     fun onExpandedChanged(isExpanded: Boolean) {
         scope.launch { mutableIsExpanded.emit(isExpanded) }
     }
+
+    private fun CoroutineScope.addRemoteViewModelIfNeeded(
+        list: MutableList<SliderViewModel>,
+        remoteMediaDeviceSession: MediaDeviceSession?
+    ) {
+        if (remoteMediaDeviceSession?.canAdjustVolume == true) {
+            val viewModel =
+                castVolumeSliderViewModelFactory.create(
+                    remoteMediaDeviceSession,
+                    this,
+                )
+            list.add(viewModel)
+        }
+    }
+
+    private fun CoroutineScope.addStreamViewModel(
+        list: MutableList<SliderViewModel>,
+        stream: Int,
+    ) {
+        val viewModel =
+            streamSliderViewModelFactory.create(
+                AudioStreamSliderViewModel.FactoryAudioStreamWrapper(AudioStream(stream)),
+                this,
+            )
+        list.add(viewModel)
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/activity/VolumePanelActivity.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/activity/VolumePanelActivity.kt
index d430e65..c728fef 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/activity/VolumePanelActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/activity/VolumePanelActivity.kt
@@ -42,7 +42,6 @@
     override fun onCreate(savedInstanceState: Bundle?) {
         enableEdgeToEdge()
         super.onCreate(savedInstanceState)
-
         volumePanelFlag.assertNewVolumePanel()
 
         setContent { VolumePanelRoot(viewModel = viewModel, onDismiss = ::finish) }
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
index 7931fab..e48b639 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
@@ -363,8 +363,8 @@
                 }, mSysUiMainExecutor);
         mCommandQueue.addCallback(new CommandQueue.Callbacks() {
             @Override
-            public void enterDesktop(int displayId) {
-                desktopMode.enterDesktop(displayId);
+            public void moveFocusedTaskToDesktop(int displayId) {
+                desktopMode.moveFocusedTaskToDesktop(displayId);
             }
             @Override
             public void moveFocusedTaskToFullscreen(int displayId) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogTransitionAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogTransitionAnimatorTest.kt
index b73e4e6..9182e4101 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogTransitionAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogTransitionAnimatorTest.kt
@@ -36,6 +36,7 @@
 import org.mockito.Mockito.any
 import org.mockito.Mockito.verify
 import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
@@ -44,8 +45,8 @@
     private lateinit var mDialogTransitionAnimator: DialogTransitionAnimator
     private val attachedViews = mutableSetOf<View>()
 
-    val interactionJankMonitor = Kosmos().interactionJankMonitor
-    @get:Rule val rule = MockitoJUnit.rule()
+    private val interactionJankMonitor = Kosmos().interactionJankMonitor
+    @get:Rule val rule: MockitoRule = MockitoJUnit.rule()
 
     @Before
     fun setUp() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationViewModelTransformerTest.java b/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationViewModelTransformerTest.java
index 206babf..09675e2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationViewModelTransformerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationViewModelTransformerTest.java
@@ -23,6 +23,7 @@
 
 import android.testing.AndroidTestingRunner;
 
+import androidx.lifecycle.ViewModel;
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
@@ -56,7 +57,8 @@
         MockitoAnnotations.initMocks(this);
         when(mFactory.create(Mockito.any(), Mockito.any())).thenReturn(mComponent);
         when(mComponent.getViewModelProvider()).thenReturn(mViewModelProvider);
-        when(mViewModelProvider.get(Mockito.any(), Mockito.any())).thenReturn(mViewModel);
+        when(mViewModelProvider.get(Mockito.any(), Mockito.<Class<ViewModel>>any()))
+                .thenReturn(mViewModel);
     }
 
     /**
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt
index 5dd37ae..66aa572 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt
@@ -131,7 +131,6 @@
         whenever(clock.smallClock).thenReturn(smallClock)
         whenever(largeClock.layout).thenReturn(largeClockFaceLayout)
         whenever(smallClock.layout).thenReturn(smallClockFaceLayout)
-        whenever(clockViewModel.clock).thenReturn(clock)
         currentClock.value = clock
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImplTest.kt
similarity index 98%
rename from packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImplTest.kt
index 59eb7bb..e56a253 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImplTest.kt
@@ -66,7 +66,7 @@
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
 @TestableLooper.RunWithLooper
-class MediaDataFilterTest : SysuiTestCase() {
+class LegacyMediaDataFilterImplTest : SysuiTestCase() {
 
     @Mock private lateinit var listener: MediaDataManager.Listener
     @Mock private lateinit var userTracker: UserTracker
@@ -80,7 +80,7 @@
     @Mock private lateinit var mediaFlags: MediaFlags
     @Mock private lateinit var cardAction: SmartspaceAction
 
-    private lateinit var mediaDataFilter: MediaDataFilter
+    private lateinit var mediaDataFilter: LegacyMediaDataFilterImpl
     private lateinit var dataMain: MediaData
     private lateinit var dataGuest: MediaData
     private lateinit var dataPrivateProfile: MediaData
@@ -92,7 +92,7 @@
         MediaPlayerData.clear()
         whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false)
         mediaDataFilter =
-            MediaDataFilter(
+            LegacyMediaDataFilterImpl(
                 context,
                 userTracker,
                 broadcastSender,
@@ -370,7 +370,7 @@
         mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
         mediaDataFilter.onSwipeToDismiss()
 
-        verify(mediaDataManager).setTimedOut(eq(KEY), eq(true), eq(true))
+        verify(mediaDataManager).setInactive(eq(KEY), eq(true), eq(true))
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
similarity index 98%
rename from packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManagerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
index 61bfdb5..5a2d22d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
@@ -114,7 +114,7 @@
 @SmallTest
 @RunWithLooper(setAsMainLooper = true)
 @RunWith(AndroidTestingRunner::class)
-class MediaDataManagerTest : SysuiTestCase() {
+class LegacyMediaDataManagerImplTest : SysuiTestCase() {
 
     @JvmField @Rule val mockito = MockitoJUnit.rule()
     @Mock lateinit var mediaControllerFactory: MediaControllerFactory
@@ -133,7 +133,7 @@
     @Mock lateinit var mediaSessionBasedFilter: MediaSessionBasedFilter
     @Mock lateinit var mediaDeviceManager: MediaDeviceManager
     @Mock lateinit var mediaDataCombineLatest: MediaDataCombineLatest
-    @Mock lateinit var mediaDataFilter: MediaDataFilter
+    @Mock lateinit var mediaDataFilter: LegacyMediaDataFilterImpl
     @Mock lateinit var listener: MediaDataManager.Listener
     @Mock lateinit var pendingIntent: PendingIntent
     @Mock lateinit var activityStarter: ActivityStarter
@@ -146,7 +146,7 @@
     @Mock private lateinit var mediaSmartspaceBaseAction: SmartspaceAction
     @Mock private lateinit var mediaFlags: MediaFlags
     @Mock private lateinit var logger: MediaUiEventLogger
-    lateinit var mediaDataManager: MediaDataManager
+    lateinit var mediaDataManager: LegacyMediaDataManagerImpl
     lateinit var mediaNotification: StatusBarNotification
     lateinit var remoteCastNotification: StatusBarNotification
     @Captor lateinit var mediaDataCaptor: ArgumentCaptor<MediaData>
@@ -189,7 +189,7 @@
             1
         )
         mediaDataManager =
-            MediaDataManager(
+            LegacyMediaDataManagerImpl(
                 context = context,
                 backgroundExecutor = backgroundExecutor,
                 uiExecutor = uiExecutor,
@@ -304,13 +304,13 @@
         val data = mediaDataCaptor.value
         assertThat(data.active).isTrue()
 
-        mediaDataManager.setTimedOut(KEY, timedOut = true)
+        mediaDataManager.setInactive(KEY, timedOut = true)
         assertThat(data.active).isFalse()
         verify(logger).logMediaTimeout(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
     }
 
     @Test
-    fun testSetTimedOut_resume_dismissesMedia() {
+    fun testsetInactive_resume_dismissesMedia() {
         // WHEN resume controls are present, and time out
         val desc =
             MediaDescription.Builder().run {
@@ -339,7 +339,7 @@
                 eq(false)
             )
 
-        mediaDataManager.setTimedOut(PACKAGE_NAME, timedOut = true)
+        mediaDataManager.setInactive(PACKAGE_NAME, timedOut = true)
         verify(logger)
             .logMediaTimeout(anyInt(), eq(PACKAGE_NAME), eq(mediaDataCaptor.value.instanceId))
 
@@ -1485,7 +1485,7 @@
         // WHEN the notification times out
         clock.advanceTime(100)
         val currentTime = clock.elapsedRealtime()
-        mediaDataManager.setTimedOut(KEY, true, true)
+        mediaDataManager.setInactive(KEY, true, true)
 
         // THEN the last active time is changed
         verify(listener)
@@ -1602,7 +1602,7 @@
                 eq(false)
             )
         assertThat(mediaDataCaptor.value.actionsToShowInCompact.size)
-            .isEqualTo(MediaDataManager.MAX_COMPACT_ACTIONS)
+            .isEqualTo(LegacyMediaDataManagerImpl.MAX_COMPACT_ACTIONS)
     }
 
     @Test
@@ -1615,7 +1615,7 @@
                 modifyNotification(context).also {
                     it.setSmallIcon(android.R.drawable.ic_media_pause)
                     it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
-                    for (i in 0..MediaDataManager.MAX_NOTIFICATION_ACTIONS) {
+                    for (i in 0..LegacyMediaDataManagerImpl.MAX_NOTIFICATION_ACTIONS) {
                         it.addAction(action)
                     }
                 }
@@ -1638,7 +1638,7 @@
                 eq(false)
             )
         assertThat(mediaDataCaptor.value.actions.size)
-            .isEqualTo(MediaDataManager.MAX_NOTIFICATION_ACTIONS)
+            .isEqualTo(LegacyMediaDataManagerImpl.MAX_NOTIFICATION_ACTIONS)
     }
 
     @Test
@@ -2040,7 +2040,7 @@
 
         // When a media control based on notification is added, times out, and then removed
         addNotificationAndLoad()
-        mediaDataManager.setTimedOut(KEY, timedOut = true)
+        mediaDataManager.setInactive(KEY, timedOut = true)
         assertThat(mediaDataCaptor.value.active).isFalse()
         mediaDataManager.onNotificationRemoved(KEY)
 
@@ -2070,7 +2070,7 @@
 
         // When a media control based on notification is added and times out
         addNotificationAndLoad()
-        mediaDataManager.setTimedOut(KEY, timedOut = true)
+        mediaDataManager.setInactive(KEY, timedOut = true)
         assertThat(mediaDataCaptor.value.active).isFalse()
 
         // and then the session is destroyed
@@ -2142,7 +2142,7 @@
         addNotificationAndLoad()
         val data = mediaDataCaptor.value
         assertThat(data.active).isTrue()
-        mediaDataManager.setTimedOut(KEY, timedOut = true)
+        mediaDataManager.setInactive(KEY, timedOut = true)
         mediaDataManager.onNotificationRemoved(KEY)
 
         // It remains as a regular player
@@ -2162,7 +2162,7 @@
         addNotificationAndLoad()
         val data = mediaDataCaptor.value
         assertThat(data.active).isTrue()
-        mediaDataManager.setTimedOut(KEY, timedOut = true)
+        mediaDataManager.setInactive(KEY, timedOut = true)
         sessionCallbackCaptor.value.invoke(KEY)
 
         // It is converted to a resume player
@@ -2249,7 +2249,7 @@
         addNotificationAndLoad()
         val data = mediaDataCaptor.value
         assertThat(data.active).isTrue()
-        mediaDataManager.setTimedOut(KEY, timedOut = true)
+        mediaDataManager.setInactive(KEY, timedOut = true)
         sessionCallbackCaptor.value.invoke(KEY)
 
         // It is fully removed.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt
new file mode 100644
index 0000000..564bdc3
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt
@@ -0,0 +1,931 @@
+/*
+ * 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.media.controls.domain.pipeline
+
+import android.app.smartspace.SmartspaceAction
+import android.os.Bundle
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.InstanceId
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.broadcast.BroadcastSender
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.media.controls.MediaTestUtils
+import com.android.systemui.media.controls.data.repository.MediaFilterRepository
+import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_RESUME
+import com.android.systemui.media.controls.shared.model.MediaData
+import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
+import com.android.systemui.media.controls.ui.controller.MediaPlayerData
+import com.android.systemui.media.controls.util.MediaFlags
+import com.android.systemui.media.controls.util.MediaUiEventLogger
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.statusbar.NotificationLockscreenUserManager
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.Executor
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+private const val KEY = "TEST_KEY"
+private const val KEY_ALT = "TEST_KEY_2"
+private const val USER_MAIN = 0
+private const val USER_GUEST = 10
+private const val PRIVATE_PROFILE = 12
+private const val PACKAGE = "PKG"
+private val INSTANCE_ID = InstanceId.fakeInstanceId(123)!!
+private const val APP_UID = 99
+private const val SMARTSPACE_KEY = "SMARTSPACE_KEY"
+private const val SMARTSPACE_PACKAGE = "SMARTSPACE_PKG"
+private val SMARTSPACE_INSTANCE_ID = InstanceId.fakeInstanceId(456)!!
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class MediaDataFilterImplTest : SysuiTestCase() {
+
+    @Mock private lateinit var listener: MediaDataManager.Listener
+    @Mock private lateinit var userTracker: UserTracker
+    @Mock private lateinit var broadcastSender: BroadcastSender
+    @Mock private lateinit var mediaDataManager: MediaDataManager
+    @Mock private lateinit var lockscreenUserManager: NotificationLockscreenUserManager
+    @Mock private lateinit var executor: Executor
+    @Mock private lateinit var smartspaceData: SmartspaceMediaData
+    @Mock private lateinit var smartspaceMediaRecommendationItem: SmartspaceAction
+    @Mock private lateinit var logger: MediaUiEventLogger
+    @Mock private lateinit var mediaFlags: MediaFlags
+    @Mock private lateinit var cardAction: SmartspaceAction
+
+    private lateinit var mediaDataFilter: MediaDataFilterImpl
+    private lateinit var mediaFilterRepository: MediaFilterRepository
+    private lateinit var testScope: TestScope
+    private lateinit var dataMain: MediaData
+    private lateinit var dataGuest: MediaData
+    private lateinit var dataPrivateProfile: MediaData
+    private val clock = FakeSystemClock()
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        MediaPlayerData.clear()
+        whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false)
+        testScope = TestScope()
+        mediaFilterRepository = MediaFilterRepository()
+        mediaDataFilter =
+            MediaDataFilterImpl(
+                context,
+                userTracker,
+                broadcastSender,
+                lockscreenUserManager,
+                executor,
+                clock,
+                logger,
+                mediaFlags,
+                mediaFilterRepository,
+            )
+        mediaDataFilter.mediaDataManager = mediaDataManager
+        mediaDataFilter.addListener(listener)
+
+        // Start all tests as main user
+        setUser(USER_MAIN)
+
+        // Set up test media data
+        dataMain =
+            MediaTestUtils.emptyMediaData.copy(
+                userId = USER_MAIN,
+                packageName = PACKAGE,
+                instanceId = INSTANCE_ID,
+                appUid = APP_UID
+            )
+        dataGuest = dataMain.copy(userId = USER_GUEST)
+        dataPrivateProfile = dataMain.copy(userId = PRIVATE_PROFILE)
+
+        whenever(smartspaceData.targetId).thenReturn(SMARTSPACE_KEY)
+        whenever(smartspaceData.isActive).thenReturn(true)
+        whenever(smartspaceData.isValid()).thenReturn(true)
+        whenever(smartspaceData.packageName).thenReturn(SMARTSPACE_PACKAGE)
+        whenever(smartspaceData.recommendations)
+            .thenReturn(listOf(smartspaceMediaRecommendationItem))
+        whenever(smartspaceData.headphoneConnectionTimeMillis)
+            .thenReturn(clock.currentTimeMillis() - 100)
+        whenever(smartspaceData.instanceId).thenReturn(SMARTSPACE_INSTANCE_ID)
+        whenever(smartspaceData.cardAction).thenReturn(cardAction)
+    }
+
+    private fun setUser(id: Int) {
+        whenever(lockscreenUserManager.isCurrentProfile(anyInt())).thenReturn(false)
+        whenever(lockscreenUserManager.isProfileAvailable(anyInt())).thenReturn(false)
+        whenever(lockscreenUserManager.isCurrentProfile(eq(id))).thenReturn(true)
+        whenever(lockscreenUserManager.isProfileAvailable(eq(id))).thenReturn(true)
+        whenever(lockscreenUserManager.isProfileAvailable(eq(PRIVATE_PROFILE))).thenReturn(true)
+        mediaDataFilter.handleUserSwitched()
+    }
+
+    private fun setPrivateProfileUnavailable() {
+        whenever(lockscreenUserManager.isCurrentProfile(anyInt())).thenReturn(false)
+        whenever(lockscreenUserManager.isCurrentProfile(eq(USER_MAIN))).thenReturn(true)
+        whenever(lockscreenUserManager.isCurrentProfile(eq(PRIVATE_PROFILE))).thenReturn(true)
+        whenever(lockscreenUserManager.isProfileAvailable(eq(PRIVATE_PROFILE))).thenReturn(false)
+        mediaDataFilter.handleProfileChanged()
+    }
+
+    @Test
+    fun testOnDataLoadedForCurrentUser_callsListener() {
+        // GIVEN a media for main user
+        mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
+
+        // THEN we should tell the listener
+        verify(listener)
+            .onMediaDataLoaded(eq(KEY), eq(null), eq(dataMain), eq(true), eq(0), eq(false))
+    }
+
+    @Test
+    fun testOnDataLoadedForGuest_doesNotCallListener() {
+        // GIVEN a media for guest user
+        mediaDataFilter.onMediaDataLoaded(KEY, null, dataGuest)
+
+        // THEN we should NOT tell the listener
+        verify(listener, never())
+            .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean())
+    }
+
+    @Test
+    fun testOnRemovedForCurrent_callsListener() {
+        // GIVEN a media was removed for main user
+        mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
+        mediaDataFilter.onMediaDataRemoved(KEY)
+
+        // THEN we should tell the listener
+        verify(listener).onMediaDataRemoved(eq(KEY))
+    }
+
+    @Test
+    fun testOnRemovedForGuest_doesNotCallListener() {
+        // GIVEN a media was removed for guest user
+        mediaDataFilter.onMediaDataLoaded(KEY, null, dataGuest)
+        mediaDataFilter.onMediaDataRemoved(KEY)
+
+        // THEN we should NOT tell the listener
+        verify(listener, never()).onMediaDataRemoved(eq(KEY))
+    }
+
+    @Test
+    fun testOnUserSwitched_removesOldUserControls() {
+        // GIVEN that we have a media loaded for main user
+        mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
+
+        // and we switch to guest user
+        setUser(USER_GUEST)
+
+        // THEN we should remove the main user's media
+        verify(listener).onMediaDataRemoved(eq(KEY))
+    }
+
+    @Test
+    fun testOnUserSwitched_addsNewUserControls() {
+        // GIVEN that we had some media for both users
+        mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
+        mediaDataFilter.onMediaDataLoaded(KEY_ALT, null, dataGuest)
+        reset(listener)
+
+        // and we switch to guest user
+        setUser(USER_GUEST)
+
+        // THEN we should add back the guest user media
+        verify(listener)
+            .onMediaDataLoaded(eq(KEY_ALT), eq(null), eq(dataGuest), eq(true), eq(0), eq(false))
+
+        // but not the main user's
+        verify(listener, never())
+            .onMediaDataLoaded(eq(KEY), any(), eq(dataMain), anyBoolean(), anyInt(), anyBoolean())
+    }
+
+    @Test
+    fun testOnProfileChanged_profileUnavailable_loadControls() {
+        // GIVEN that we had some media for both profiles
+        mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
+        mediaDataFilter.onMediaDataLoaded(KEY_ALT, null, dataPrivateProfile)
+        reset(listener)
+
+        // and we change profile status
+        setPrivateProfileUnavailable()
+
+        // THEN we should add the private profile media
+        verify(listener).onMediaDataRemoved(eq(KEY_ALT))
+    }
+
+    @Test
+    fun hasAnyMedia_mediaSet_returnsTrue() =
+        testScope.runTest {
+            val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+            mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataMain)
+
+            assertThat(hasAnyMedia(selectedUserEntries)).isTrue()
+        }
+
+    @Test
+    fun hasAnyMedia_recommendationSet_returnsFalse() =
+        testScope.runTest {
+            val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+            mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+            assertThat(hasAnyMedia(selectedUserEntries)).isFalse()
+        }
+
+    @Test
+    fun hasAnyMediaOrRecommendation_mediaSet_returnsTrue() =
+        testScope.runTest {
+            val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+            val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+            mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataMain)
+
+            assertThat(hasAnyMediaOrRecommendation(selectedUserEntries, smartspaceMediaData))
+                .isTrue()
+        }
+
+    @Test
+    fun hasAnyMediaOrRecommendation_recommendationSet_returnsTrue() =
+        testScope.runTest {
+            val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+            val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+            mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+            assertThat(hasAnyMediaOrRecommendation(selectedUserEntries, smartspaceMediaData))
+                .isTrue()
+        }
+
+    @Test
+    fun hasActiveMedia_inactiveMediaSet_returnsFalse() =
+        testScope.runTest {
+            val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+
+            val data = dataMain.copy(active = false)
+            mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data)
+
+            assertThat(hasActiveMedia(selectedUserEntries)).isFalse()
+        }
+
+    @Test
+    fun hasActiveMedia_activeMediaSet_returnsTrue() =
+        testScope.runTest {
+            val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+            val data = dataMain.copy(active = true)
+            mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data)
+
+            assertThat(hasActiveMedia(selectedUserEntries)).isTrue()
+        }
+
+    @Test
+    fun hasActiveMediaOrRecommendation_inactiveMediaSet_returnsFalse() =
+        testScope.runTest {
+            val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+            val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+            val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey)
+            val data = dataMain.copy(active = false)
+            mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data)
+
+            assertThat(
+                    hasActiveMediaOrRecommendation(
+                        selectedUserEntries,
+                        smartspaceMediaData,
+                        reactivatedKey
+                    )
+                )
+                .isFalse()
+        }
+
+    @Test
+    fun hasActiveMediaOrRecommendation_activeMediaSet_returnsTrue() =
+        testScope.runTest {
+            val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+            val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+            val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey)
+            val data = dataMain.copy(active = true)
+            mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data)
+
+            assertThat(
+                    hasActiveMediaOrRecommendation(
+                        selectedUserEntries,
+                        smartspaceMediaData,
+                        reactivatedKey
+                    )
+                )
+                .isTrue()
+        }
+
+    @Test
+    fun hasActiveMediaOrRecommendation_inactiveRecommendationSet_returnsFalse() =
+        testScope.runTest {
+            val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+            val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+            val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey)
+            whenever(smartspaceData.isActive).thenReturn(false)
+            mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+            assertThat(
+                    hasActiveMediaOrRecommendation(
+                        selectedUserEntries,
+                        smartspaceMediaData,
+                        reactivatedKey
+                    )
+                )
+                .isFalse()
+        }
+
+    @Test
+    fun hasActiveMediaOrRecommendation_invalidRecommendationSet_returnsFalse() =
+        testScope.runTest {
+            val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+            val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+            val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey)
+            whenever(smartspaceData.isValid()).thenReturn(false)
+            mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+            assertThat(
+                    hasActiveMediaOrRecommendation(
+                        selectedUserEntries,
+                        smartspaceMediaData,
+                        reactivatedKey
+                    )
+                )
+                .isFalse()
+        }
+
+    @Test
+    fun hasActiveMediaOrRecommendation_activeAndValidRecommendationSet_returnsTrue() =
+        testScope.runTest {
+            val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+            val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+            val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey)
+            whenever(smartspaceData.isActive).thenReturn(true)
+            whenever(smartspaceData.isValid()).thenReturn(true)
+            mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+            assertThat(
+                    hasActiveMediaOrRecommendation(
+                        selectedUserEntries,
+                        smartspaceMediaData,
+                        reactivatedKey
+                    )
+                )
+                .isTrue()
+        }
+
+    @Test
+    fun testHasAnyMediaOrRecommendation_onlyCurrentUser() =
+        testScope.runTest {
+            val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+            val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+            assertThat(hasAnyMediaOrRecommendation(selectedUserEntries, smartspaceMediaData))
+                .isFalse()
+
+            mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataGuest)
+            assertThat(hasAnyMediaOrRecommendation(selectedUserEntries, smartspaceMediaData))
+                .isFalse()
+            assertThat(hasAnyMedia(selectedUserEntries)).isFalse()
+        }
+
+    @Test
+    fun testHasActiveMediaOrRecommendation_onlyCurrentUser() =
+        testScope.runTest {
+            val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+            val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+            val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey)
+            assertThat(
+                    hasActiveMediaOrRecommendation(
+                        selectedUserEntries,
+                        smartspaceMediaData,
+                        reactivatedKey
+                    )
+                )
+                .isFalse()
+            val data = dataGuest.copy(active = true)
+
+            mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data)
+            assertThat(
+                    hasActiveMediaOrRecommendation(
+                        selectedUserEntries,
+                        smartspaceMediaData,
+                        reactivatedKey
+                    )
+                )
+                .isFalse()
+            assertThat(hasAnyMedia(selectedUserEntries)).isFalse()
+        }
+
+    @Test
+    fun testOnNotificationRemoved_doesNotHaveMedia() =
+        testScope.runTest {
+            val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+            val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+
+            mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataMain)
+            mediaDataFilter.onMediaDataRemoved(KEY)
+            assertThat(hasAnyMediaOrRecommendation(selectedUserEntries, smartspaceMediaData))
+                .isFalse()
+            assertThat(hasAnyMedia(selectedUserEntries)).isFalse()
+        }
+
+    @Test
+    fun testOnSwipeToDismiss_setsTimedOut() {
+        mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
+        mediaDataFilter.onSwipeToDismiss()
+
+        verify(mediaDataManager).setInactive(eq(KEY), eq(true), eq(true))
+    }
+
+    @Test
+    fun testOnSmartspaceMediaDataLoaded_noMedia_activeValidRec_prioritizesSmartspace() =
+        testScope.runTest {
+            val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+            val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+            val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey)
+
+            mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+            verify(listener)
+                .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true))
+            assertThat(
+                    hasActiveMediaOrRecommendation(
+                        selectedUserEntries,
+                        smartspaceMediaData,
+                        reactivatedKey
+                    )
+                )
+                .isTrue()
+            assertThat(hasActiveMedia(selectedUserEntries)).isFalse()
+            verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID)
+            verify(logger, never()).logRecommendationActivated(any(), any(), any())
+        }
+
+    @Test
+    fun testOnSmartspaceMediaDataLoaded_noMedia_inactiveRec_showsNothing() =
+        testScope.runTest {
+            val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+            val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+            val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey)
+
+            whenever(smartspaceData.isActive).thenReturn(false)
+
+            mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+            verify(listener, never())
+                .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean())
+            verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
+            assertThat(
+                    hasActiveMediaOrRecommendation(
+                        selectedUserEntries,
+                        smartspaceMediaData,
+                        reactivatedKey
+                    )
+                )
+                .isFalse()
+            assertThat(hasActiveMedia(selectedUserEntries)).isFalse()
+            verify(logger, never()).logRecommendationAdded(any(), any())
+            verify(logger, never()).logRecommendationActivated(any(), any(), any())
+        }
+
+    @Test
+    fun testOnSmartspaceMediaDataLoaded_noRecentMedia_activeValidRec_prioritizesSmartspace() =
+        testScope.runTest {
+            val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+            val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+            val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey)
+            val dataOld = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
+            mediaDataFilter.onMediaDataLoaded(KEY, null, dataOld)
+            clock.advanceTime(MediaDataFilterImpl.SMARTSPACE_MAX_AGE + 100)
+            mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+            verify(listener)
+                .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true))
+            assertThat(
+                    hasActiveMediaOrRecommendation(
+                        selectedUserEntries,
+                        smartspaceMediaData,
+                        reactivatedKey
+                    )
+                )
+                .isTrue()
+            assertThat(hasActiveMedia(selectedUserEntries)).isFalse()
+            verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID)
+            verify(logger, never()).logRecommendationActivated(any(), any(), any())
+        }
+
+    @Test
+    fun testOnSmartspaceMediaDataLoaded_noRecentMedia_inactiveRec_showsNothing() =
+        testScope.runTest {
+            val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+            val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+            val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey)
+            whenever(smartspaceData.isActive).thenReturn(false)
+
+            val dataOld = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
+            mediaDataFilter.onMediaDataLoaded(KEY, null, dataOld)
+            clock.advanceTime(MediaDataFilterImpl.SMARTSPACE_MAX_AGE + 100)
+            mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+            verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
+            assertThat(
+                    hasActiveMediaOrRecommendation(
+                        selectedUserEntries,
+                        smartspaceMediaData,
+                        reactivatedKey
+                    )
+                )
+                .isFalse()
+            assertThat(hasActiveMedia(selectedUserEntries)).isFalse()
+            verify(logger, never()).logRecommendationAdded(any(), any())
+            verify(logger, never()).logRecommendationActivated(any(), any(), any())
+        }
+
+    @Test
+    fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_inactiveRec_showsNothing() =
+        testScope.runTest {
+            val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+            val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+            val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey)
+
+            whenever(smartspaceData.isActive).thenReturn(false)
+
+            // WHEN we have media that was recently played, but not currently active
+            val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
+            mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
+            verify(listener)
+                .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
+
+            // AND we get a smartspace signal
+            mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+            // THEN we should tell listeners to treat the media as not active instead
+            verify(listener, never())
+                .onMediaDataLoaded(eq(KEY), eq(KEY), any(), anyBoolean(), anyInt(), anyBoolean())
+            verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
+            assertThat(
+                    hasActiveMediaOrRecommendation(
+                        selectedUserEntries,
+                        smartspaceMediaData,
+                        reactivatedKey
+                    )
+                )
+                .isFalse()
+            assertThat(hasActiveMedia(selectedUserEntries)).isFalse()
+            verify(logger, never()).logRecommendationAdded(any(), any())
+            verify(logger, never()).logRecommendationActivated(any(), any(), any())
+        }
+
+    @Test
+    fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_activeInvalidRec_usesMedia() =
+        testScope.runTest {
+            val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+            val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+            val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey)
+            whenever(smartspaceData.isValid()).thenReturn(false)
+
+            // WHEN we have media that was recently played, but not currently active
+            val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
+            mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
+            verify(listener)
+                .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
+
+            // AND we get a smartspace signal
+            runCurrent()
+            mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+            // THEN we should tell listeners to treat the media as active instead
+            val dataCurrentAndActive = dataCurrent.copy(active = true)
+            verify(listener)
+                .onMediaDataLoaded(
+                    eq(KEY),
+                    eq(KEY),
+                    eq(dataCurrentAndActive),
+                    eq(true),
+                    eq(100),
+                    eq(true)
+                )
+            assertThat(
+                    hasActiveMediaOrRecommendation(
+                        selectedUserEntries,
+                        smartspaceMediaData,
+                        reactivatedKey
+                    )
+                )
+                .isTrue()
+            // Smartspace update shouldn't be propagated for the empty rec list.
+            verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
+            verify(logger, never()).logRecommendationAdded(any(), any())
+            verify(logger).logRecommendationActivated(eq(APP_UID), eq(PACKAGE), eq(INSTANCE_ID))
+        }
+
+    @Test
+    fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_activeValidRec_usesBoth() =
+        testScope.runTest {
+            val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+            val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+            val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey)
+            // WHEN we have media that was recently played, but not currently active
+            val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
+            mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
+            verify(listener)
+                .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
+
+            // AND we get a smartspace signal
+            runCurrent()
+            mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+            // THEN we should tell listeners to treat the media as active instead
+            val dataCurrentAndActive = dataCurrent.copy(active = true)
+            verify(listener)
+                .onMediaDataLoaded(
+                    eq(KEY),
+                    eq(KEY),
+                    eq(dataCurrentAndActive),
+                    eq(true),
+                    eq(100),
+                    eq(true)
+                )
+            assertThat(
+                    hasActiveMediaOrRecommendation(
+                        selectedUserEntries,
+                        smartspaceMediaData,
+                        reactivatedKey
+                    )
+                )
+                .isTrue()
+            // Smartspace update should also be propagated but not prioritized.
+            verify(listener)
+                .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
+            verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID)
+            verify(logger).logRecommendationActivated(eq(APP_UID), eq(PACKAGE), eq(INSTANCE_ID))
+        }
+
+    @Test
+    fun testOnSmartspaceMediaDataRemoved_usedSmartspace_clearsSmartspace() =
+        testScope.runTest {
+            val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+            val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+            val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey)
+
+            mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+            mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
+
+            verify(listener).onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
+            assertThat(
+                    hasActiveMediaOrRecommendation(
+                        selectedUserEntries,
+                        smartspaceMediaData,
+                        reactivatedKey
+                    )
+                )
+                .isFalse()
+            assertThat(hasActiveMedia(selectedUserEntries)).isFalse()
+        }
+
+    @Test
+    fun testOnSmartspaceMediaDataRemoved_usedMediaAndSmartspace_clearsBoth() =
+        testScope.runTest {
+            val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+            val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+            val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey)
+            val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
+            mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
+            verify(listener)
+                .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
+
+            runCurrent()
+            mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+            val dataCurrentAndActive = dataCurrent.copy(active = true)
+            verify(listener)
+                .onMediaDataLoaded(
+                    eq(KEY),
+                    eq(KEY),
+                    eq(dataCurrentAndActive),
+                    eq(true),
+                    eq(100),
+                    eq(true)
+                )
+
+            mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
+
+            verify(listener).onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
+            assertThat(
+                    hasActiveMediaOrRecommendation(
+                        selectedUserEntries,
+                        smartspaceMediaData,
+                        reactivatedKey
+                    )
+                )
+                .isFalse()
+            assertThat(hasActiveMedia(selectedUserEntries)).isFalse()
+        }
+
+    @Test
+    fun testOnSmartspaceLoaded_persistentEnabled_isInactive_notifiesListeners() =
+        testScope.runTest {
+            val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+            val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+            val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey)
+            whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+            whenever(smartspaceData.isActive).thenReturn(false)
+
+            mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+            verify(listener)
+                .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
+            assertThat(
+                    hasActiveMediaOrRecommendation(
+                        selectedUserEntries,
+                        smartspaceMediaData,
+                        reactivatedKey
+                    )
+                )
+                .isFalse()
+            assertThat(hasAnyMediaOrRecommendation(selectedUserEntries, smartspaceMediaData))
+                .isTrue()
+        }
+
+    @Test
+    fun testOnSmartspaceLoaded_persistentEnabled_inactive_hasRecentMedia_staysInactive() =
+        testScope.runTest {
+            val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+            val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+            val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey)
+
+            whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+            whenever(smartspaceData.isActive).thenReturn(false)
+
+            // If there is media that was recently played but inactive
+            val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
+            mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
+            verify(listener)
+                .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
+
+            // And an inactive recommendation is loaded
+            mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+            // Smartspace is loaded but the media stays inactive
+            verify(listener)
+                .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
+            verify(listener, never())
+                .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean())
+            assertThat(
+                    hasActiveMediaOrRecommendation(
+                        selectedUserEntries,
+                        smartspaceMediaData,
+                        reactivatedKey
+                    )
+                )
+                .isFalse()
+            assertThat(hasAnyMediaOrRecommendation(selectedUserEntries, smartspaceMediaData))
+                .isTrue()
+        }
+
+    @Test
+    fun testOnSwipeToDismiss_persistentEnabled_recommendationSetInactive() {
+        whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
+
+        val data =
+            EMPTY_SMARTSPACE_MEDIA_DATA.copy(
+                targetId = SMARTSPACE_KEY,
+                isActive = true,
+                packageName = SMARTSPACE_PACKAGE,
+                recommendations = listOf(smartspaceMediaRecommendationItem),
+            )
+        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, data)
+        mediaDataFilter.onSwipeToDismiss()
+
+        verify(mediaDataManager).setRecommendationInactive(eq(SMARTSPACE_KEY))
+        verify(mediaDataManager, never())
+            .dismissSmartspaceRecommendation(eq(SMARTSPACE_KEY), anyLong())
+    }
+
+    @Test
+    fun testSmartspaceLoaded_shouldTriggerResume_doesTrigger() =
+        testScope.runTest {
+            val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)
+            val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData)
+            val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey)
+            // WHEN we have media that was recently played, but not currently active
+            val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
+            mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
+            verify(listener)
+                .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
+
+            // AND we get a smartspace signal with extra to trigger resume
+            runCurrent()
+            val extras = Bundle().apply { putBoolean(EXTRA_KEY_TRIGGER_RESUME, true) }
+            whenever(cardAction.extras).thenReturn(extras)
+            mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+            // THEN we should tell listeners to treat the media as active instead
+            val dataCurrentAndActive = dataCurrent.copy(active = true)
+            verify(listener)
+                .onMediaDataLoaded(
+                    eq(KEY),
+                    eq(KEY),
+                    eq(dataCurrentAndActive),
+                    eq(true),
+                    eq(100),
+                    eq(true)
+                )
+            assertThat(
+                    hasActiveMediaOrRecommendation(
+                        selectedUserEntries,
+                        smartspaceMediaData,
+                        reactivatedKey
+                    )
+                )
+                .isTrue()
+            // And send the smartspace data, but not prioritized
+            verify(listener)
+                .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
+        }
+
+    @Test
+    fun testSmartspaceLoaded_notShouldTriggerResume_doesNotTrigger() {
+        // WHEN we have media that was recently played, but not currently active
+        val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
+        mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
+        verify(listener)
+            .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
+
+        // AND we get a smartspace signal with extra to not trigger resume
+        val extras = Bundle().apply { putBoolean(EXTRA_KEY_TRIGGER_RESUME, false) }
+        whenever(cardAction.extras).thenReturn(extras)
+        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+        // THEN listeners are not updated to show media
+        verify(listener, never())
+            .onMediaDataLoaded(eq(KEY), eq(KEY), any(), eq(true), eq(100), eq(true))
+        // But the smartspace update is still propagated
+        verify(listener)
+            .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
+    }
+
+    private fun hasActiveMediaOrRecommendation(
+        entries: Map<String, MediaData>?,
+        smartspaceMediaData: SmartspaceMediaData?,
+        reactivatedKey: String?
+    ): Boolean {
+        if (entries == null || smartspaceMediaData == null) {
+            return false
+        }
+        return entries.any { it.value.active } ||
+            (smartspaceMediaData.isActive &&
+                (smartspaceMediaData.isValid() || reactivatedKey != null))
+    }
+
+    private fun hasActiveMedia(entries: Map<String, MediaData>?): Boolean {
+        return entries?.any { it.value.active } ?: false
+    }
+
+    private fun hasAnyMediaOrRecommendation(
+        entries: Map<String, MediaData>?,
+        smartspaceMediaData: SmartspaceMediaData?
+    ): Boolean {
+        if (entries == null || smartspaceMediaData == null) {
+            return false
+        }
+        return entries.isNotEmpty() ||
+            (if (mediaFlags.isPersistentSsCardEnabled()) {
+                smartspaceMediaData.isValid()
+            } else {
+                smartspaceMediaData.isActive && smartspaceMediaData.isValid()
+            })
+    }
+
+    private fun hasAnyMedia(entries: Map<String, MediaData>?): Boolean {
+        return entries?.isNotEmpty() ?: false
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
similarity index 91%
copy from packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManagerTest.kt
copy to packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
index 61bfdb5..5c275b4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * 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.
@@ -41,6 +41,7 @@
 import android.provider.Settings
 import android.service.notification.StatusBarNotification
 import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
 import android.testing.TestableLooper.RunWithLooper
 import androidx.media.utils.MediaConstants
 import androidx.test.filters.SmallTest
@@ -51,6 +52,9 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.media.controls.data.repository.MediaDataRepository
+import com.android.systemui.media.controls.data.repository.MediaFilterRepository
+import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor
 import com.android.systemui.media.controls.domain.resume.MediaResumeListener
 import com.android.systemui.media.controls.domain.resume.ResumeMediaBrowser
 import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_SOURCE
@@ -64,13 +68,19 @@
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.SbnBuilder
-import com.android.systemui.tuner.TunerService
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.settings.FakeSettings
 import com.android.systemui.util.time.FakeSystemClock
+import com.android.systemui.utils.os.FakeHandler
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import org.junit.After
 import org.junit.Before
 import org.junit.Rule
@@ -87,7 +97,6 @@
 import org.mockito.Mockito.reset
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyNoMoreInteractions
-import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoSession
 import org.mockito.junit.MockitoJUnit
 import org.mockito.quality.Strictness
@@ -111,10 +120,11 @@
     return Mockito.anyObject<T>()
 }
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWithLooper(setAsMainLooper = true)
 @RunWith(AndroidTestingRunner::class)
-class MediaDataManagerTest : SysuiTestCase() {
+class MediaDataProcessorTest : SysuiTestCase() {
 
     @JvmField @Rule val mockito = MockitoJUnit.rule()
     @Mock lateinit var mediaControllerFactory: MediaControllerFactory
@@ -122,9 +132,9 @@
     @Mock lateinit var transportControls: MediaController.TransportControls
     @Mock lateinit var playbackInfo: MediaController.PlaybackInfo
     lateinit var session: MediaSession
-    lateinit var metadataBuilder: MediaMetadata.Builder
+    private lateinit var metadataBuilder: MediaMetadata.Builder
     lateinit var backgroundExecutor: FakeExecutor
-    lateinit var foregroundExecutor: FakeExecutor
+    private lateinit var foregroundExecutor: FakeExecutor
     lateinit var uiExecutor: FakeExecutor
     @Mock lateinit var dumpManager: DumpManager
     @Mock lateinit var broadcastDispatcher: BroadcastDispatcher
@@ -133,32 +143,38 @@
     @Mock lateinit var mediaSessionBasedFilter: MediaSessionBasedFilter
     @Mock lateinit var mediaDeviceManager: MediaDeviceManager
     @Mock lateinit var mediaDataCombineLatest: MediaDataCombineLatest
-    @Mock lateinit var mediaDataFilter: MediaDataFilter
+    @Mock lateinit var mediaDataFilter: MediaDataFilterImpl
     @Mock lateinit var listener: MediaDataManager.Listener
     @Mock lateinit var pendingIntent: PendingIntent
     @Mock lateinit var activityStarter: ActivityStarter
     @Mock lateinit var smartspaceManager: SmartspaceManager
     @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
-    lateinit var smartspaceMediaDataProvider: SmartspaceMediaDataProvider
+    private lateinit var smartspaceMediaDataProvider: SmartspaceMediaDataProvider
     @Mock lateinit var mediaSmartspaceTarget: SmartspaceTarget
     @Mock private lateinit var mediaRecommendationItem: SmartspaceAction
-    lateinit var validRecommendationList: List<SmartspaceAction>
+    private lateinit var validRecommendationList: List<SmartspaceAction>
     @Mock private lateinit var mediaSmartspaceBaseAction: SmartspaceAction
     @Mock private lateinit var mediaFlags: MediaFlags
     @Mock private lateinit var logger: MediaUiEventLogger
-    lateinit var mediaDataManager: MediaDataManager
-    lateinit var mediaNotification: StatusBarNotification
-    lateinit var remoteCastNotification: StatusBarNotification
+    private lateinit var mediaCarouselInteractor: MediaCarouselInteractor
+    private lateinit var mediaDataProcessor: MediaDataProcessor
+    private lateinit var mediaNotification: StatusBarNotification
+    private lateinit var remoteCastNotification: StatusBarNotification
     @Captor lateinit var mediaDataCaptor: ArgumentCaptor<MediaData>
     private val clock = FakeSystemClock()
-    @Mock private lateinit var tunerService: TunerService
-    @Captor lateinit var tunableCaptor: ArgumentCaptor<TunerService.Tunable>
     @Captor lateinit var stateCallbackCaptor: ArgumentCaptor<(String, PlaybackState) -> Unit>
     @Captor lateinit var sessionCallbackCaptor: ArgumentCaptor<(String) -> Unit>
     @Captor lateinit var smartSpaceConfigBuilderCaptor: ArgumentCaptor<SmartspaceConfig>
     @Mock private lateinit var ugm: IUriGrantsManager
     @Mock private lateinit var imageSource: ImageDecoder.Source
+    private lateinit var mediaDataRepository: MediaDataRepository
+    private lateinit var mediaFilterRepository: MediaFilterRepository
+    private lateinit var testScope: TestScope
+    private lateinit var testDispatcher: TestDispatcher
+    private lateinit var testableLooper: TestableLooper
+    private lateinit var fakeHandler: FakeHandler
 
+    private val settings = FakeSettings()
     private val instanceIdSequence = InstanceIdSequenceFake(1 shl 20)
 
     private val originalSmartspaceSetting =
@@ -172,6 +188,8 @@
 
     @Before
     fun setup() {
+        whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true)
+
         staticMockSession =
             ExtendedMockito.mockitoSession()
                 .mockStatic<UriGrantsManager>(UriGrantsManager::class.java)
@@ -182,43 +200,61 @@
         foregroundExecutor = FakeExecutor(clock)
         backgroundExecutor = FakeExecutor(clock)
         uiExecutor = FakeExecutor(clock)
+        testableLooper = TestableLooper.get(this)
+        fakeHandler = FakeHandler(testableLooper.looper)
         smartspaceMediaDataProvider = SmartspaceMediaDataProvider()
         Settings.Secure.putInt(
             context.contentResolver,
             Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
             1
         )
-        mediaDataManager =
-            MediaDataManager(
+        testDispatcher = UnconfinedTestDispatcher()
+        testScope = TestScope(testDispatcher)
+        mediaFilterRepository = MediaFilterRepository()
+        mediaDataRepository = MediaDataRepository(mediaFlags, dumpManager)
+        mediaDataProcessor =
+            MediaDataProcessor(
                 context = context,
+                applicationScope = testScope,
+                backgroundDispatcher = testDispatcher,
                 backgroundExecutor = backgroundExecutor,
                 uiExecutor = uiExecutor,
                 foregroundExecutor = foregroundExecutor,
+                handler = fakeHandler,
                 mediaControllerFactory = mediaControllerFactory,
                 broadcastDispatcher = broadcastDispatcher,
                 dumpManager = dumpManager,
+                activityStarter = activityStarter,
+                smartspaceMediaDataProvider = smartspaceMediaDataProvider,
+                useMediaResumption = true,
+                useQsMediaPlayer = true,
+                systemClock = clock,
+                secureSettings = settings,
+                mediaFlags = mediaFlags,
+                logger = logger,
+                smartspaceManager = smartspaceManager,
+                keyguardUpdateMonitor = keyguardUpdateMonitor,
+                mediaDataRepository = mediaDataRepository,
+            )
+        mediaDataProcessor.start()
+        mediaCarouselInteractor =
+            MediaCarouselInteractor(
+                applicationScope = testScope.backgroundScope,
+                mediaDataRepository = mediaDataRepository,
+                mediaDataProcessor = mediaDataProcessor,
                 mediaTimeoutListener = mediaTimeoutListener,
                 mediaResumeListener = mediaResumeListener,
                 mediaSessionBasedFilter = mediaSessionBasedFilter,
                 mediaDeviceManager = mediaDeviceManager,
                 mediaDataCombineLatest = mediaDataCombineLatest,
                 mediaDataFilter = mediaDataFilter,
-                activityStarter = activityStarter,
-                smartspaceMediaDataProvider = smartspaceMediaDataProvider,
-                useMediaResumption = true,
-                useQsMediaPlayer = true,
-                systemClock = clock,
-                tunerService = tunerService,
-                mediaFlags = mediaFlags,
-                logger = logger,
-                smartspaceManager = smartspaceManager,
-                keyguardUpdateMonitor = keyguardUpdateMonitor,
+                mediaFilterRepository = mediaFilterRepository,
+                mediaFlags = mediaFlags
             )
-        verify(tunerService)
-            .addTunable(capture(tunableCaptor), eq(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION))
+        mediaCarouselInteractor.start()
         verify(mediaTimeoutListener).stateCallback = capture(stateCallbackCaptor)
         verify(mediaTimeoutListener).sessionCallback = capture(sessionCallbackCaptor)
-        session = MediaSession(context, "MediaDataManagerTestSession")
+        session = MediaSession(context, "MediaDataProcessorTestSession")
         mediaNotification =
             SbnBuilder().run {
                 setPkg(PACKAGE_NAME)
@@ -290,7 +326,7 @@
     fun tearDown() {
         staticMockSession.finishMocking()
         session.release()
-        mediaDataManager.destroy()
+        mediaDataProcessor.destroy()
         Settings.Secure.putInt(
             context.contentResolver,
             Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
@@ -299,25 +335,25 @@
     }
 
     @Test
-    fun testSetTimedOut_active_deactivatesMedia() {
+    fun testsetInactive_active_deactivatesMedia() {
         addNotificationAndLoad()
         val data = mediaDataCaptor.value
         assertThat(data.active).isTrue()
 
-        mediaDataManager.setTimedOut(KEY, timedOut = true)
+        mediaDataProcessor.setInactive(KEY, timedOut = true)
         assertThat(data.active).isFalse()
         verify(logger).logMediaTimeout(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
     }
 
     @Test
-    fun testSetTimedOut_resume_dismissesMedia() {
+    fun testsetInactive_resume_dismissesMedia() {
         // WHEN resume controls are present, and time out
         val desc =
             MediaDescription.Builder().run {
                 setTitle(SESSION_TITLE)
                 build()
             }
-        mediaDataManager.addResumptionControls(
+        mediaDataProcessor.addResumptionControls(
             USER_ID,
             desc,
             Runnable {},
@@ -339,7 +375,7 @@
                 eq(false)
             )
 
-        mediaDataManager.setTimedOut(PACKAGE_NAME, timedOut = true)
+        mediaDataProcessor.setInactive(PACKAGE_NAME, timedOut = true)
         verify(logger)
             .logMediaTimeout(anyInt(), eq(PACKAGE_NAME), eq(mediaDataCaptor.value.instanceId))
 
@@ -351,7 +387,7 @@
 
     @Test
     fun testLoadsMetadataOnBackground() {
-        mediaDataManager.onNotificationAdded(KEY, mediaNotification)
+        mediaDataProcessor.onNotificationAdded(KEY, mediaNotification)
         assertThat(backgroundExecutor.numPending()).isEqualTo(1)
     }
 
@@ -367,8 +403,7 @@
                     .build()
             )
 
-        mediaDataManager.addListener(listener)
-        mediaDataManager.onNotificationAdded(KEY, mediaNotification)
+        mediaDataProcessor.onNotificationAdded(KEY, mediaNotification)
 
         assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
         assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
@@ -386,8 +421,7 @@
 
     @Test
     fun testOnMetaDataLoaded_withoutExplicitIndicator() {
-        mediaDataManager.addListener(listener)
-        mediaDataManager.onNotificationAdded(KEY, mediaNotification)
+        mediaDataProcessor.onNotificationAdded(KEY, mediaNotification)
 
         assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
         assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
@@ -418,8 +452,7 @@
     @Test
     fun testOnMetaDataLoaded_conservesActiveFlag() {
         whenever(mediaControllerFactory.create(anyObject())).thenReturn(controller)
-        mediaDataManager.addListener(listener)
-        mediaDataManager.onNotificationAdded(KEY, mediaNotification)
+        mediaDataProcessor.onNotificationAdded(KEY, mediaNotification)
         assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
         assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
         verify(listener)
@@ -464,7 +497,7 @@
                 build()
             }
 
-        mediaDataManager.onNotificationAdded(KEY, notif)
+        mediaDataProcessor.onNotificationAdded(KEY, notif)
         assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
         assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
         verify(listener)
@@ -498,7 +531,7 @@
                 build()
             }
 
-        mediaDataManager.loadMediaDataInBg(KEY, rcn, null)
+        mediaDataProcessor.loadMediaDataInBg(KEY, rcn, null)
         // no crash even though the data structure is incorrect
     }
 
@@ -523,7 +556,7 @@
                 build()
             }
 
-        mediaDataManager.loadMediaDataInBg(KEY, rcn, null)
+        mediaDataProcessor.loadMediaDataInBg(KEY, rcn, null)
         // no crash even though the data structure is incorrect
     }
 
@@ -531,7 +564,7 @@
     fun testOnNotificationRemoved_callsListener() {
         addNotificationAndLoad()
         val data = mediaDataCaptor.value
-        mediaDataManager.onNotificationRemoved(KEY)
+        mediaDataProcessor.onNotificationRemoved(KEY)
         verify(listener).onMediaDataRemoved(eq(KEY))
         verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
     }
@@ -549,7 +582,7 @@
                     .putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_EMPTY_TITLE)
                     .build()
             )
-        mediaDataManager.onNotificationAdded(KEY, mediaNotification)
+        mediaDataProcessor.onNotificationAdded(KEY, mediaNotification)
 
         // Then a media control is created with a placeholder title string
         assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
@@ -580,7 +613,7 @@
                     .putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_BLANK_TITLE)
                     .build()
             )
-        mediaDataManager.onNotificationAdded(KEY, mediaNotification)
+        mediaDataProcessor.onNotificationAdded(KEY, mediaNotification)
 
         // Then a media control is created with a placeholder title string
         assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
@@ -622,7 +655,7 @@
                 }
                 build()
             }
-        mediaDataManager.onNotificationAdded(KEY, mediaNotification)
+        mediaDataProcessor.onNotificationAdded(KEY, mediaNotification)
 
         // Then the media control is added using the notification's title
         assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
@@ -646,7 +679,7 @@
         val data = mediaDataCaptor.value
         val instanceId = data.instanceId
         assertThat(data.resumption).isFalse()
-        mediaDataManager.onMediaDataLoaded(
+        mediaDataProcessor.onMediaDataLoaded(
             KEY,
             null,
             data.copy(song = SESSION_EMPTY_TITLE, resumeAction = Runnable {})
@@ -654,7 +687,7 @@
 
         // WHEN the notification is removed
         reset(listener)
-        mediaDataManager.onNotificationRemoved(KEY)
+        mediaDataProcessor.onNotificationRemoved(KEY)
 
         // THEN active media is not converted to resume.
         verify(listener, never())
@@ -679,7 +712,7 @@
         val data = mediaDataCaptor.value
         val instanceId = data.instanceId
         assertThat(data.resumption).isFalse()
-        mediaDataManager.onMediaDataLoaded(
+        mediaDataProcessor.onMediaDataLoaded(
             KEY,
             null,
             data.copy(song = SESSION_BLANK_TITLE, resumeAction = Runnable {})
@@ -687,7 +720,7 @@
 
         // WHEN the notification is removed
         reset(listener)
-        mediaDataManager.onNotificationRemoved(KEY)
+        mediaDataProcessor.onNotificationRemoved(KEY)
 
         // THEN active media is not converted to resume.
         verify(listener, never())
@@ -711,9 +744,9 @@
         addNotificationAndLoad()
         val data = mediaDataCaptor.value
         assertThat(data.resumption).isFalse()
-        mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
+        mediaDataProcessor.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
         // WHEN the notification is removed
-        mediaDataManager.onNotificationRemoved(KEY)
+        mediaDataProcessor.onNotificationRemoved(KEY)
         // THEN the media data indicates that it is for resumption
         verify(listener)
             .onMediaDataLoaded(
@@ -732,8 +765,8 @@
     @Test
     fun testOnNotificationRemoved_twoWithResumption() {
         // GIVEN that the manager has two notifications with resume actions
-        mediaDataManager.onNotificationAdded(KEY, mediaNotification)
-        mediaDataManager.onNotificationAdded(KEY_2, mediaNotification)
+        mediaDataProcessor.onNotificationAdded(KEY, mediaNotification)
+        mediaDataProcessor.onNotificationAdded(KEY_2, mediaNotification)
         assertThat(backgroundExecutor.runAllReady()).isEqualTo(2)
         assertThat(foregroundExecutor.runAllReady()).isEqualTo(2)
 
@@ -761,11 +794,11 @@
         val data2 = mediaDataCaptor.value
         assertThat(data2.resumption).isFalse()
 
-        mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
-        mediaDataManager.onMediaDataLoaded(KEY_2, null, data2.copy(resumeAction = Runnable {}))
+        mediaDataProcessor.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
+        mediaDataProcessor.onMediaDataLoaded(KEY_2, null, data2.copy(resumeAction = Runnable {}))
         reset(listener)
         // WHEN the first is removed
-        mediaDataManager.onNotificationRemoved(KEY)
+        mediaDataProcessor.onNotificationRemoved(KEY)
         // THEN the data is for resumption and the key is migrated to the package name
         verify(listener)
             .onMediaDataLoaded(
@@ -779,7 +812,7 @@
         assertThat(mediaDataCaptor.value.resumption).isTrue()
         verify(listener, never()).onMediaDataRemoved(eq(KEY))
         // WHEN the second is removed
-        mediaDataManager.onNotificationRemoved(KEY_2)
+        mediaDataProcessor.onNotificationRemoved(KEY_2)
         // THEN the data is for resumption and the second key is removed
         verify(listener)
             .onMediaDataLoaded(
@@ -803,7 +836,7 @@
         val data = mediaDataCaptor.value
         val dataRemoteWithResume =
             data.copy(resumeAction = Runnable {}, playbackLocation = MediaData.PLAYBACK_CAST_LOCAL)
-        mediaDataManager.onMediaDataLoaded(KEY, null, dataRemoteWithResume)
+        mediaDataProcessor.onMediaDataLoaded(KEY, null, dataRemoteWithResume)
         verify(logger)
             .logActiveMediaAdded(
                 anyInt(),
@@ -813,7 +846,7 @@
             )
 
         // WHEN the notification is removed
-        mediaDataManager.onNotificationRemoved(KEY)
+        mediaDataProcessor.onNotificationRemoved(KEY)
 
         // THEN the media data is removed
         verify(listener).onMediaDataRemoved(eq(KEY))
@@ -832,10 +865,10 @@
         val data = mediaDataCaptor.value
         val dataRemoteWithResume =
             data.copy(resumeAction = Runnable {}, playbackLocation = MediaData.PLAYBACK_CAST_LOCAL)
-        mediaDataManager.onMediaDataLoaded(KEY, null, dataRemoteWithResume)
+        mediaDataProcessor.onMediaDataLoaded(KEY, null, dataRemoteWithResume)
 
         // WHEN the notification is removed
-        mediaDataManager.onNotificationRemoved(KEY)
+        mediaDataProcessor.onNotificationRemoved(KEY)
 
         // THEN the media data is converted to a resume state
         verify(listener)
@@ -860,10 +893,10 @@
         val data = mediaDataCaptor.value
         assertThat(data.playbackLocation).isEqualTo(MediaData.PLAYBACK_CAST_REMOTE)
         val dataRemoteWithResume = data.copy(resumeAction = Runnable {})
-        mediaDataManager.onMediaDataLoaded(KEY, null, dataRemoteWithResume)
+        mediaDataProcessor.onMediaDataLoaded(KEY, null, dataRemoteWithResume)
 
         // WHEN the RCN is removed
-        mediaDataManager.onNotificationRemoved(KEY)
+        mediaDataProcessor.onNotificationRemoved(KEY)
 
         // THEN the media data is removed
         verify(listener).onMediaDataRemoved(eq(KEY))
@@ -886,10 +919,10 @@
         addNotificationAndLoad()
         val data = mediaDataCaptor.value
         assertThat(data.resumption).isFalse()
-        mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
+        mediaDataProcessor.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
 
         // When the notification is removed
-        mediaDataManager.onNotificationRemoved(KEY)
+        mediaDataProcessor.onNotificationRemoved(KEY)
 
         // Then it is converted to resumption
         verify(listener)
@@ -913,7 +946,7 @@
 
         addNotificationAndLoad()
         val data = mediaDataCaptor.value
-        mediaDataManager.onNotificationRemoved(KEY)
+        mediaDataProcessor.onNotificationRemoved(KEY)
 
         verify(listener, never()).onMediaDataRemoved(eq(KEY))
         verify(logger, never())
@@ -1076,7 +1109,7 @@
                 setTitle(SESSION_EMPTY_TITLE)
                 build()
             }
-        mediaDataManager.addResumptionControls(
+        mediaDataProcessor.addResumptionControls(
             USER_ID,
             desc,
             Runnable {},
@@ -1110,7 +1143,7 @@
                 setTitle(SESSION_BLANK_TITLE)
                 build()
             }
-        mediaDataManager.addResumptionControls(
+        mediaDataProcessor.addResumptionControls(
             USER_ID,
             desc,
             Runnable {},
@@ -1145,7 +1178,7 @@
         addResumeControlAndLoad(desc)
 
         val data = mediaDataCaptor.value
-        mediaDataManager.setMediaResumptionEnabled(false)
+        mediaDataProcessor.setMediaResumptionEnabled(false)
 
         // THEN the resume controls are dismissed
         verify(listener).onMediaDataRemoved(eq(PACKAGE_NAME))
@@ -1156,7 +1189,7 @@
     fun testDismissMedia_listenerCalled() {
         addNotificationAndLoad()
         val data = mediaDataCaptor.value
-        val removed = mediaDataManager.dismissMediaData(KEY, 0L)
+        val removed = mediaDataProcessor.dismissMediaData(KEY, 0L)
         assertThat(removed).isTrue()
 
         foregroundExecutor.advanceClockToLast()
@@ -1168,7 +1201,7 @@
 
     @Test
     fun testDismissMedia_keyDoesNotExist_returnsFalse() {
-        val removed = mediaDataManager.dismissMediaData(KEY, 0L)
+        val removed = mediaDataProcessor.dismissMediaData(KEY, 0L)
         assertThat(removed).isFalse()
     }
 
@@ -1186,7 +1219,7 @@
                 }
                 build()
             }
-        mediaDataManager.onNotificationAdded(KEY, notif)
+        mediaDataProcessor.onNotificationAdded(KEY, notif)
 
         // THEN it still loads
         assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
@@ -1239,7 +1272,7 @@
             .onSmartspaceMediaDataLoaded(
                 eq(KEY_MEDIA_SMARTSPACE),
                 eq(
-                    EMPTY_SMARTSPACE_MEDIA_DATA.copy(
+                    SmartspaceMediaData(
                         targetId = KEY_MEDIA_SMARTSPACE,
                         isActive = true,
                         dismissIntent = DISMISS_INTENT,
@@ -1271,7 +1304,7 @@
             .onSmartspaceMediaDataLoaded(
                 eq(KEY_MEDIA_SMARTSPACE),
                 eq(
-                    EMPTY_SMARTSPACE_MEDIA_DATA.copy(
+                    SmartspaceMediaData(
                         targetId = KEY_MEDIA_SMARTSPACE,
                         isActive = true,
                         dismissIntent = null,
@@ -1404,7 +1437,7 @@
         smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
         val instanceId = instanceIdSequence.lastInstanceId
 
-        mediaDataManager.setRecommendationInactive(KEY_MEDIA_SMARTSPACE)
+        mediaDataProcessor.setRecommendationInactive(KEY_MEDIA_SMARTSPACE)
         uiExecutor.advanceClockToLast()
         uiExecutor.runAllReady()
 
@@ -1431,12 +1464,7 @@
     @Test
     fun testOnSmartspaceMediaDataLoaded_settingDisabled_doesNothing() {
         // WHEN media recommendation setting is off
-        Settings.Secure.putInt(
-            context.contentResolver,
-            Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
-            0
-        )
-        tunableCaptor.value.onTuningChanged(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, "0")
+        settings.putInt(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 0)
 
         smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
 
@@ -1453,12 +1481,7 @@
             .onSmartspaceMediaDataLoaded(eq(KEY_MEDIA_SMARTSPACE), anyObject(), anyBoolean())
 
         // WHEN the media recommendation setting is turned off
-        Settings.Secure.putInt(
-            context.contentResolver,
-            Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
-            0
-        )
-        tunableCaptor.value.onTuningChanged(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, "0")
+        settings.putInt(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 0)
 
         // THEN listeners are notified
         uiExecutor.advanceClockToLast()
@@ -1478,14 +1501,14 @@
     @Test
     fun testOnMediaDataTimedOut_updatesLastActiveTime() {
         // GIVEN that the manager has a notification
-        mediaDataManager.onNotificationAdded(KEY, mediaNotification)
+        mediaDataProcessor.onNotificationAdded(KEY, mediaNotification)
         assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
         assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
 
         // WHEN the notification times out
         clock.advanceTime(100)
         val currentTime = clock.elapsedRealtime()
-        mediaDataManager.setTimedOut(KEY, true, true)
+        mediaDataProcessor.setInactive(KEY, timedOut = true, forceUpdate = true)
 
         // THEN the last active time is changed
         verify(listener)
@@ -1507,12 +1530,12 @@
         val data = mediaDataCaptor.value
         val instanceId = data.instanceId
         assertThat(data.resumption).isFalse()
-        mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
+        mediaDataProcessor.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
 
         // WHEN the notification is removed
         clock.advanceTime(100)
         val currentTime = clock.elapsedRealtime()
-        mediaDataManager.onNotificationRemoved(KEY)
+        mediaDataProcessor.onNotificationRemoved(KEY)
 
         // THEN the last active time is changed
         verify(listener)
@@ -1539,7 +1562,7 @@
         val data = mediaDataCaptor.value
         val instanceId = data.instanceId
         assertThat(data.resumption).isFalse()
-        mediaDataManager.onMediaDataLoaded(
+        mediaDataProcessor.onMediaDataLoaded(
             KEY,
             null,
             data.copy(resumeAction = Runnable {}, active = false)
@@ -1548,7 +1571,7 @@
         // WHEN the notification is removed
         clock.advanceTime(100)
         val currentTime = clock.elapsedRealtime()
-        mediaDataManager.onNotificationRemoved(KEY)
+        mediaDataProcessor.onNotificationRemoved(KEY)
 
         // THEN the last active time is not changed
         verify(listener)
@@ -1587,7 +1610,7 @@
             }
 
         // WHEN the notification is loaded
-        mediaDataManager.onNotificationAdded(KEY, notif)
+        mediaDataProcessor.onNotificationAdded(KEY, notif)
         assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
         assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
 
@@ -1602,7 +1625,7 @@
                 eq(false)
             )
         assertThat(mediaDataCaptor.value.actionsToShowInCompact.size)
-            .isEqualTo(MediaDataManager.MAX_COMPACT_ACTIONS)
+            .isEqualTo(MediaDataProcessor.MAX_COMPACT_ACTIONS)
     }
 
     @Test
@@ -1615,7 +1638,7 @@
                 modifyNotification(context).also {
                     it.setSmallIcon(android.R.drawable.ic_media_pause)
                     it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
-                    for (i in 0..MediaDataManager.MAX_NOTIFICATION_ACTIONS) {
+                    for (i in 0..MediaDataProcessor.MAX_NOTIFICATION_ACTIONS) {
                         it.addAction(action)
                     }
                 }
@@ -1623,7 +1646,7 @@
             }
 
         // WHEN the notification is loaded
-        mediaDataManager.onNotificationAdded(KEY, notif)
+        mediaDataProcessor.onNotificationAdded(KEY, notif)
         assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
         assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
 
@@ -1638,7 +1661,7 @@
                 eq(false)
             )
         assertThat(mediaDataCaptor.value.actions.size)
-            .isEqualTo(MediaDataManager.MAX_NOTIFICATION_ACTIONS)
+            .isEqualTo(MediaDataProcessor.MAX_NOTIFICATION_ACTIONS)
     }
 
     @Test
@@ -1657,7 +1680,7 @@
                 }
                 build()
             }
-        mediaDataManager.onNotificationAdded(KEY, notifWithAction)
+        mediaDataProcessor.onNotificationAdded(KEY, notifWithAction)
 
         assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
         assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
@@ -1850,7 +1873,7 @@
             )
 
         // update to remote cast
-        mediaDataManager.onNotificationAdded(KEY, remoteCastNotification)
+        mediaDataProcessor.onNotificationAdded(KEY, remoteCastNotification)
         assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
         assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
         verify(logger)
@@ -1901,7 +1924,7 @@
         addNotificationAndLoad()
         val data = mediaDataCaptor.value
         assertThat(data.resumption).isFalse()
-        mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(token = null))
+        mediaDataProcessor.onMediaDataLoaded(KEY, null, data.copy(token = null))
 
         // And then get a state update
         val state = PlaybackState.Builder().build()
@@ -1949,7 +1972,7 @@
 
         // Add resumption controls in order to have semantic actions.
         // To make sure that they are not null after changing state.
-        mediaDataManager.addResumptionControls(
+        mediaDataProcessor.addResumptionControls(
             USER_ID,
             desc,
             Runnable {},
@@ -2040,9 +2063,9 @@
 
         // When a media control based on notification is added, times out, and then removed
         addNotificationAndLoad()
-        mediaDataManager.setTimedOut(KEY, timedOut = true)
+        mediaDataProcessor.setInactive(KEY, timedOut = true)
         assertThat(mediaDataCaptor.value.active).isFalse()
-        mediaDataManager.onNotificationRemoved(KEY)
+        mediaDataProcessor.onNotificationRemoved(KEY)
 
         // It is converted to a resume player
         verify(listener)
@@ -2070,7 +2093,7 @@
 
         // When a media control based on notification is added and times out
         addNotificationAndLoad()
-        mediaDataManager.setTimedOut(KEY, timedOut = true)
+        mediaDataProcessor.setInactive(KEY, timedOut = true)
         assertThat(mediaDataCaptor.value.active).isFalse()
 
         // and then the session is destroyed
@@ -2090,7 +2113,7 @@
         addNotificationAndLoad()
         val data = mediaDataCaptor.value
         assertThat(data.active).isTrue()
-        mediaDataManager.onNotificationRemoved(KEY)
+        mediaDataProcessor.onNotificationRemoved(KEY)
 
         // It is fully removed
         verify(listener).onMediaDataRemoved(eq(KEY))
@@ -2106,10 +2129,10 @@
         // When a media control that supports resumption is added
         addNotificationAndLoad()
         val dataResumable = mediaDataCaptor.value.copy(resumeAction = Runnable {})
-        mediaDataManager.onMediaDataLoaded(KEY, null, dataResumable)
+        mediaDataProcessor.onMediaDataLoaded(KEY, null, dataResumable)
 
         // And then removed while still active
-        mediaDataManager.onNotificationRemoved(KEY)
+        mediaDataProcessor.onNotificationRemoved(KEY)
 
         // It is converted to a resume player
         verify(listener)
@@ -2142,8 +2165,8 @@
         addNotificationAndLoad()
         val data = mediaDataCaptor.value
         assertThat(data.active).isTrue()
-        mediaDataManager.setTimedOut(KEY, timedOut = true)
-        mediaDataManager.onNotificationRemoved(KEY)
+        mediaDataProcessor.setInactive(KEY, timedOut = true)
+        mediaDataProcessor.onNotificationRemoved(KEY)
 
         // It remains as a regular player
         verify(listener, never()).onMediaDataRemoved(eq(KEY))
@@ -2162,7 +2185,7 @@
         addNotificationAndLoad()
         val data = mediaDataCaptor.value
         assertThat(data.active).isTrue()
-        mediaDataManager.setTimedOut(KEY, timedOut = true)
+        mediaDataProcessor.setInactive(KEY, timedOut = true)
         sessionCallbackCaptor.value.invoke(KEY)
 
         // It is converted to a resume player
@@ -2214,7 +2237,7 @@
         // When a media control using session actions and that does allow resumption is added,
         addNotificationAndLoad()
         val dataResumable = mediaDataCaptor.value.copy(resumeAction = Runnable {})
-        mediaDataManager.onMediaDataLoaded(KEY, null, dataResumable)
+        mediaDataProcessor.onMediaDataLoaded(KEY, null, dataResumable)
 
         // And then the session is destroyed without timing out first
         sessionCallbackCaptor.value.invoke(KEY)
@@ -2249,7 +2272,7 @@
         addNotificationAndLoad()
         val data = mediaDataCaptor.value
         assertThat(data.active).isTrue()
-        mediaDataManager.setTimedOut(KEY, timedOut = true)
+        mediaDataProcessor.setInactive(KEY, timedOut = true)
         sessionCallbackCaptor.value.invoke(KEY)
 
         // It is fully removed.
@@ -2293,7 +2316,7 @@
         // When a media control using session actions and that does allow resumption is added,
         addNotificationAndLoad()
         val dataResumable = mediaDataCaptor.value.copy(resumeAction = Runnable {})
-        mediaDataManager.onMediaDataLoaded(KEY, null, dataResumable)
+        mediaDataProcessor.onMediaDataLoaded(KEY, null, dataResumable)
 
         // And then the session is destroyed without timing out first
         sessionCallbackCaptor.value.invoke(KEY)
@@ -2324,9 +2347,9 @@
         whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
 
         // When a notiifcation is added and then removed before it is fully processed
-        mediaDataManager.onNotificationAdded(KEY, mediaNotification)
+        mediaDataProcessor.onNotificationAdded(KEY, mediaNotification)
         backgroundExecutor.runAllReady()
-        mediaDataManager.onNotificationRemoved(KEY)
+        mediaDataProcessor.onNotificationRemoved(KEY)
 
         // We still make sure to remove it
         verify(listener).onMediaDataRemoved(eq(KEY))
@@ -2399,7 +2422,7 @@
 
     /** Helper function to add the given notification and capture the resulting MediaData */
     private fun addNotificationAndLoad(sbn: StatusBarNotification) {
-        mediaDataManager.onNotificationAdded(KEY, sbn)
+        mediaDataProcessor.onNotificationAdded(KEY, sbn)
         assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
         assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
         verify(listener)
@@ -2426,7 +2449,7 @@
         desc: MediaDescription,
         packageName: String = PACKAGE_NAME
     ) {
-        mediaDataManager.addResumptionControls(
+        mediaDataProcessor.addResumptionControls(
             USER_ID,
             desc,
             Runnable {},
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt
index 7f3d79f..a447e44 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt
@@ -41,7 +41,6 @@
 import com.android.settingslib.media.MediaDevice
 import com.android.settingslib.media.PhoneMediaDevice
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.dump.DumpManager
 import com.android.systemui.media.controls.MediaTestUtils
 import com.android.systemui.media.controls.shared.model.MediaData
 import com.android.systemui.media.controls.shared.model.MediaDeviceData
@@ -98,7 +97,6 @@
     @Mock private lateinit var muteAwaitManager: MediaMuteAwaitConnectionManager
     private lateinit var fakeFgExecutor: FakeExecutor
     private lateinit var fakeBgExecutor: FakeExecutor
-    @Mock private lateinit var dumpster: DumpManager
     @Mock private lateinit var listener: MediaDeviceManager.Listener
     @Mock private lateinit var device: MediaDevice
     @Mock private lateinit var icon: Drawable
@@ -133,7 +131,6 @@
                 { localBluetoothManager },
                 fakeFgExecutor,
                 fakeBgExecutor,
-                dumpster,
             )
         manager.addListener(listener)
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
index f755199..59e2696c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
@@ -41,7 +41,6 @@
 import com.android.systemui.media.controls.domain.pipeline.EMPTY_SMARTSPACE_MEDIA_DATA
 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager
 import com.android.systemui.media.controls.shared.model.MediaData
-import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
 import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager.Companion.LOCATION_QS
 import com.android.systemui.media.controls.ui.view.MediaHostState
 import com.android.systemui.media.controls.ui.view.MediaScrollView
@@ -111,7 +110,6 @@
     @Mock lateinit var logger: MediaUiEventLogger
     @Mock lateinit var debugLogger: MediaCarouselControllerLogger
     @Mock lateinit var mediaViewController: MediaViewController
-    @Mock lateinit var smartspaceMediaData: SmartspaceMediaData
     @Mock lateinit var mediaCarousel: MediaScrollView
     @Mock lateinit var pageIndicator: PageIndicator
     @Mock lateinit var mediaFlags: MediaFlags
@@ -165,7 +163,6 @@
         verify(mediaHostStatesManager).addCallback(capture(hostStateCallback))
         whenever(mediaControlPanelFactory.get()).thenReturn(panel)
         whenever(panel.mediaViewController).thenReturn(mediaViewController)
-        whenever(mediaDataManager.smartspaceMediaData).thenReturn(smartspaceMediaData)
         whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false)
         MediaPlayerData.clear()
         verify(globalSettings)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt
index aa54565..6e0919f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt
@@ -28,9 +28,10 @@
 import android.view.ViewConfiguration
 import android.view.WindowManager
 import androidx.test.filters.SmallTest
-import com.android.internal.jank.InteractionJankMonitor
 import com.android.internal.util.LatencyTracker
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.jank.interactionJankMonitor
+import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.plugins.NavigationEdgeBackPlugin
 import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.policy.ConfigurationController
@@ -41,10 +42,8 @@
 import org.mockito.ArgumentMatchers.any
 import org.mockito.ArgumentMatchers.eq
 import org.mockito.Mock
-import org.mockito.Mockito.anyInt
 import org.mockito.Mockito.clearInvocations
 import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
 
 @SmallTest
@@ -62,16 +61,13 @@
     @Mock private lateinit var windowManager: WindowManager
     @Mock private lateinit var configurationController: ConfigurationController
     @Mock private lateinit var latencyTracker: LatencyTracker
-    @Mock private lateinit var interactionJankMonitor: InteractionJankMonitor
+    private val interactionJankMonitor = Kosmos().interactionJankMonitor
     @Mock private lateinit var layoutParams: WindowManager.LayoutParams
     @Mock private lateinit var backCallback: NavigationEdgeBackPlugin.BackCallback
 
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
-        `when`(interactionJankMonitor.begin(any(), anyInt())).thenReturn(true)
-        `when`(interactionJankMonitor.end(anyInt())).thenReturn(true)
-        `when`(interactionJankMonitor.cancel(anyInt())).thenReturn(true)
         mBackPanelController =
             BackPanelController(
                 context,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
index cc48640..5c6ed70 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
@@ -21,6 +21,7 @@
 import android.testing.ViewUtils
 import android.view.ContextThemeWrapper
 import android.view.View
+import android.view.ViewGroup
 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
 import android.view.accessibility.AccessibilityNodeInfo
 import android.widget.FrameLayout
@@ -71,7 +72,7 @@
             qsPanel = QSPanel(themedContext, null)
             qsPanel.mUsingMediaPlayer = true
 
-            qsPanel.initialize(qsLogger)
+            qsPanel.initialize(qsLogger, true)
             // QSPanel inflates a footer inside of it, mocking it here
             footer = LinearLayout(themedContext).apply { id = R.id.qs_footer }
             qsPanel.addView(footer, MATCH_PARENT, 100)
@@ -218,6 +219,62 @@
         verify(tile).addCallback(record.callback)
     }
 
+    @Test
+    fun initializedWithNoMedia_tileLayoutParentIsAlwaysQsPanel() {
+        lateinit var panel: QSPanel
+        lateinit var tileLayout: View
+        testableLooper.runWithLooper {
+            panel = QSPanel(themedContext, null)
+            panel.mUsingMediaPlayer = true
+
+            panel.initialize(qsLogger, /* usingMediaPlayer= */ false)
+            tileLayout = panel.orCreateTileLayout as View
+            // QSPanel inflates a footer inside of it, mocking it here
+            footer = LinearLayout(themedContext).apply { id = R.id.qs_footer }
+            panel.addView(footer, MATCH_PARENT, 100)
+            panel.onFinishInflate()
+            // Provides a parent with non-zero size for QSPanel
+            ViewUtils.attachView(panel)
+        }
+        val mockMediaHost = mock(ViewGroup::class.java)
+
+        panel.setUsingHorizontalLayout(false, mockMediaHost, true)
+
+        assertThat(tileLayout.parent).isSameInstanceAs(panel)
+
+        panel.setUsingHorizontalLayout(true, mockMediaHost, true)
+        assertThat(tileLayout.parent).isSameInstanceAs(panel)
+
+        ViewUtils.detachView(panel)
+    }
+
+    @Test
+    fun initializeWithNoMedia_mediaNeverAttached() {
+        lateinit var panel: QSPanel
+        testableLooper.runWithLooper {
+            panel = QSPanel(themedContext, null)
+            panel.mUsingMediaPlayer = true
+
+            panel.initialize(qsLogger, /* usingMediaPlayer= */ false)
+            panel.orCreateTileLayout as View
+            // QSPanel inflates a footer inside of it, mocking it here
+            footer = LinearLayout(themedContext).apply { id = R.id.qs_footer }
+            panel.addView(footer, MATCH_PARENT, 100)
+            panel.onFinishInflate()
+            // Provides a parent with non-zero size for QSPanel
+            ViewUtils.attachView(panel)
+        }
+        val mockMediaHost = FrameLayout(themedContext)
+
+        panel.setUsingHorizontalLayout(false, mockMediaHost, true)
+        assertThat(mockMediaHost.parent).isNull()
+
+        panel.setUsingHorizontalLayout(true, mockMediaHost, true)
+        assertThat(mockMediaHost.parent).isNull()
+
+        ViewUtils.detachView(panel)
+    }
+
     private infix fun View.isLeftOf(other: View): Boolean {
         val rect = Rect()
         getBoundsOnScreen(rect)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelTest.kt
index 3fba393..e5369fc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelTest.kt
@@ -36,7 +36,7 @@
 
         testableLooper.runWithLooper {
             quickQSPanel = QuickQSPanel(mContext, null)
-            quickQSPanel.initialize(qsLogger)
+            quickQSPanel.initialize(qsLogger, true)
 
             quickQSPanel.onFinishInflate()
             // Provides a parent with non-zero size for QSPanel
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt
index 761c411..37654d5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt
@@ -31,6 +31,7 @@
 import com.android.systemui.qs.QsEventLogger
 import com.android.systemui.qs.logging.QSLogger
 import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor
+import com.android.systemui.recordissue.IssueRecordingState
 import com.android.systemui.recordissue.RecordIssueDialogDelegate
 import com.android.systemui.res.R
 import com.android.systemui.settings.UserContextProvider
@@ -74,6 +75,7 @@
     @Mock private lateinit var dialog: SystemUIDialog
 
     private lateinit var testableLooper: TestableLooper
+    private val issueRecordingState = IssueRecordingState()
     private lateinit var tile: RecordIssueTile
 
     @Before
@@ -100,13 +102,14 @@
                 dialogLauncherAnimator,
                 panelInteractor,
                 userContextProvider,
+                issueRecordingState,
                 delegateFactory,
             )
     }
 
     @Test
     fun qsTileUi_shouldLookCorrect_whenInactive() {
-        tile.isRecording = false
+        issueRecordingState.isRecording = false
 
         val testState = tile.newTileState()
         tile.handleUpdateState(testState, null)
@@ -118,8 +121,7 @@
 
     @Test
     fun qsTileUi_shouldLookCorrect_whenRecording() {
-        tile.isRecording = true
-
+        issueRecordingState.isRecording = true
         val testState = tile.newTileState()
         tile.handleUpdateState(testState, null)
 
@@ -130,7 +132,7 @@
 
     @Test
     fun inActiveQsTile_switchesToActive_whenClicked() {
-        tile.isRecording = false
+        issueRecordingState.isRecording = false
 
         val testState = tile.newTileState()
         tile.handleUpdateState(testState, null)
@@ -140,7 +142,7 @@
 
     @Test
     fun activeQsTile_switchesToInActive_whenClicked() {
-        tile.isRecording = true
+        issueRecordingState.isRecording = true
 
         val testState = tile.newTileState()
         tile.handleUpdateState(testState, null)
@@ -150,7 +152,8 @@
 
     @Test
     fun showPrompt_shouldUseKeyguardDismissUtil_ToShowDialog() {
-        tile.isRecording = false
+        issueRecordingState.isRecording = false
+
         tile.handleClick(null)
         testableLooper.processAllMessages()
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapperTest.kt
new file mode 100644
index 0000000..4215b8c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapperTest.kt
@@ -0,0 +1,145 @@
+/*
+ * 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.qs.tiles.impl.work.ui
+
+import android.app.admin.DevicePolicyResources
+import android.app.admin.DevicePolicyResourcesManager
+import android.app.admin.devicePolicyManager
+import android.graphics.drawable.TestStubDrawable
+import android.service.quicksettings.Tile
+import android.widget.Switch
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.tiles.impl.custom.QSTileStateSubject
+import com.android.systemui.qs.tiles.impl.work.domain.model.WorkModeTileModel
+import com.android.systemui.qs.tiles.impl.work.qsWorkModeTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.res.R
+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 org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class WorkModeTileMapperTest : SysuiTestCase() {
+    private val kosmos = Kosmos()
+    private val qsTileConfig = kosmos.qsWorkModeTileConfig
+    private val devicePolicyManager = kosmos.devicePolicyManager
+    private val testLabel = context.getString(R.string.quick_settings_work_mode_label)
+    private val devicePolicyResourceManager = mock<DevicePolicyResourcesManager>()
+    private lateinit var mapper: WorkModeTileMapper
+
+    @Before
+    fun setup() {
+        whenever(devicePolicyManager.resources).thenReturn(devicePolicyResourceManager)
+        whenever(
+                devicePolicyResourceManager.getString(
+                    eq(DevicePolicyResources.Strings.SystemUi.QS_WORK_PROFILE_LABEL),
+                    any()
+                )
+            )
+            .thenReturn(testLabel)
+        mapper =
+            WorkModeTileMapper(
+                context.orCreateTestableResources
+                    .apply {
+                        addOverride(
+                            com.android.internal.R.drawable.stat_sys_managed_profile_status,
+                            TestStubDrawable()
+                        )
+                    }
+                    .resources,
+                context.theme,
+                devicePolicyManager
+            )
+    }
+
+    @Test
+    fun mapsDisabledDataToInactiveState() {
+        val isEnabled = false
+
+        val actualState: QSTileState =
+            mapper.map(qsTileConfig, WorkModeTileModel.HasActiveProfile(isEnabled))
+
+        val expectedState = createWorkModeTileState(QSTileState.ActivationState.INACTIVE)
+        QSTileStateSubject.assertThat(actualState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun mapsEnabledDataToActiveState() {
+        val isEnabled = true
+
+        val actualState: QSTileState =
+            mapper.map(qsTileConfig, WorkModeTileModel.HasActiveProfile(isEnabled))
+
+        val expectedState = createWorkModeTileState(QSTileState.ActivationState.ACTIVE)
+        QSTileStateSubject.assertThat(actualState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun mapsNoActiveProfileDataToUnavailableState() {
+        val actualState: QSTileState = mapper.map(qsTileConfig, WorkModeTileModel.NoActiveProfile)
+
+        val expectedState = createWorkModeTileState(QSTileState.ActivationState.UNAVAILABLE)
+        QSTileStateSubject.assertThat(actualState).isEqualTo(expectedState)
+    }
+
+    private fun createWorkModeTileState(
+        activationState: QSTileState.ActivationState,
+    ): QSTileState {
+        val label = testLabel
+        return QSTileState(
+            icon = {
+                Icon.Loaded(
+                    context.getDrawable(
+                        com.android.internal.R.drawable.stat_sys_managed_profile_status
+                    )!!,
+                    null
+                )
+            },
+            label = label,
+            activationState = activationState,
+            secondaryLabel =
+                if (activationState == QSTileState.ActivationState.INACTIVE) {
+                    context.getString(R.string.quick_settings_work_mode_paused_state)
+                } else if (activationState == QSTileState.ActivationState.UNAVAILABLE) {
+                    context.resources
+                        .getStringArray(R.array.tile_states_work)[Tile.STATE_UNAVAILABLE]
+                } else {
+                    ""
+                },
+            supportedActions =
+                if (activationState == QSTileState.ActivationState.UNAVAILABLE) {
+                    setOf()
+                } else {
+                    setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK)
+                },
+            contentDescription = label,
+            stateDescription = null,
+            sideViewIcon = QSTileState.SideViewIcon.None,
+            enabledState = QSTileState.EnabledState.ENABLED,
+            expandedAccessibilityClassName = Switch::class.qualifiedName
+        )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
index 2e8160b..1cfca68 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
@@ -222,4 +222,9 @@
             )
         verify(factory, never()).create(any<ScreenCapturePermissionDialogDelegate>())
     }
+
+    @Test
+    fun startButton_isDisabled_beforeIssueTypeIsSelected() {
+        assertThat(dialog.getButton(Dialog.BUTTON_POSITIVE).isEnabled).isFalse()
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index 43fcdf3..c25b910 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -62,7 +62,6 @@
 
 import androidx.constraintlayout.widget.ConstraintSet;
 
-import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.UiEventLogger;
 import com.android.internal.logging.testing.UiEventLoggerFake;
@@ -299,7 +298,6 @@
     @Mock protected RecordingController mRecordingController;
     @Mock protected LockscreenGestureLogger mLockscreenGestureLogger;
     @Mock protected DumpManager mDumpManager;
-    @Mock protected InteractionJankMonitor mInteractionJankMonitor;
     @Mock protected NotificationsQSContainerController mNotificationsQSContainerController;
     @Mock protected QsFrameTranslateController mQsFrameTranslateController;
     @Mock protected StatusBarWindowStateController mStatusBarWindowStateController;
@@ -441,7 +439,7 @@
         SystemClock systemClock = new FakeSystemClock();
         mStatusBarStateController = new StatusBarStateControllerImpl(
                 mUiEventLogger,
-                mInteractionJankMonitor,
+                mKosmos.getInteractionJankMonitor(),
                 mJavaAdapter,
                 () -> mShadeInteractor,
                 () -> mKosmos.getDeviceUnlockedInteractor(),
@@ -459,7 +457,7 @@
                 mDozeParameters,
                 mScreenOffAnimationController,
                 mKeyguardLogger,
-                mInteractionJankMonitor,
+                mKosmos.getInteractionJankMonitor(),
                 mKeyguardInteractor,
                 mDumpManager,
                 mPowerInteractor));
@@ -611,7 +609,7 @@
                         mock(HeadsUpManager.class),
                         new StatusBarStateControllerImpl(
                                 new UiEventLoggerFake(),
-                                mInteractionJankMonitor,
+                                mKosmos.getInteractionJankMonitor(),
                                 mJavaAdapter,
                                 () -> mShadeInteractor,
                                 () -> mKosmos.getDeviceUnlockedInteractor(),
@@ -651,10 +649,6 @@
                 .thenReturn(mKeyguardBottomArea);
         when(mNotificationRemoteInputManager.isRemoteInputActive())
                 .thenReturn(false);
-        when(mInteractionJankMonitor.begin(any(), anyInt()))
-                .thenReturn(true);
-        when(mInteractionJankMonitor.end(anyInt()))
-                .thenReturn(true);
         doAnswer(invocation -> {
             ((Runnable) invocation.getArgument(0)).run();
             return null;
@@ -820,7 +814,7 @@
                 mAccessibilityManager,
                 mLockscreenGestureLogger,
                 mMetricsLogger,
-                mInteractionJankMonitor,
+                mKosmos.getInteractionJankMonitor(),
                 mShadeLog,
                 mDumpManager,
                 mDeviceEntryFaceAuthInteractor,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java
index 419b0fd..118d27a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java
@@ -251,7 +251,7 @@
         mCollectionListener.onEntryInit(mEntry);
         mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
         verify(mNotifInflater).inflateViews(eq(mEntry), mParamsCaptor.capture(), any());
-        assertFalse(mParamsCaptor.getValue().isLowPriority());
+        assertFalse(mParamsCaptor.getValue().isMinimized());
         mNotifInflater.invokeInflateCallbackForEntry(mEntry);
 
         // WHEN notification moves to a min priority section
@@ -260,7 +260,7 @@
 
         // THEN we rebind it
         verify(mNotifInflater).rebindViews(eq(mEntry), mParamsCaptor.capture(), any());
-        assertTrue(mParamsCaptor.getValue().isLowPriority());
+        assertTrue(mParamsCaptor.getValue().isMinimized());
 
         // THEN we do not filter it because it's not the first inflation.
         assertFalse(mUninflatedFilter.shouldFilterOut(mEntry, 0));
@@ -273,7 +273,7 @@
         mCollectionListener.onEntryInit(mEntry);
         mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
         verify(mNotifInflater).inflateViews(eq(mEntry), mParamsCaptor.capture(), any());
-        assertTrue(mParamsCaptor.getValue().isLowPriority());
+        assertTrue(mParamsCaptor.getValue().isMinimized());
         mNotifInflater.invokeInflateCallbackForEntry(mEntry);
 
         // WHEN notification is moved under a parent
@@ -282,7 +282,7 @@
 
         // THEN we rebind it as not-minimized
         verify(mNotifInflater).rebindViews(eq(mEntry), mParamsCaptor.capture(), any());
-        assertFalse(mParamsCaptor.getValue().isLowPriority());
+        assertFalse(mParamsCaptor.getValue().isMinimized());
 
         // THEN we do not filter it because it's not the first inflation.
         assertFalse(mUninflatedFilter.shouldFilterOut(mEntry, 0));
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
index b114e13..ee2eb80 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
@@ -741,7 +741,7 @@
         when(mockViewWrapper.getIcon()).thenReturn(mockIcon);
 
         NotificationViewWrapper mockLowPriorityViewWrapper = mock(NotificationViewWrapper.class);
-        when(mockContainer.getLowPriorityViewWrapper()).thenReturn(mockLowPriorityViewWrapper);
+        when(mockContainer.getMinimizedGroupHeaderWrapper()).thenReturn(mockLowPriorityViewWrapper);
         CachingIconView mockLowPriorityIcon = mock(CachingIconView.class);
         when(mockLowPriorityViewWrapper.getIcon()).thenReturn(mockLowPriorityIcon);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java
index a0d1075..8c22511 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java
@@ -231,6 +231,7 @@
         NotificationContentInflater.applyRemoteView(
                 AsyncTask.SERIAL_EXECUTOR,
                 false /* inflateSynchronously */,
+                /* isMinimized= */ false,
                 result,
                 FLAG_CONTENT_VIEW_EXPANDED,
                 0,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowContentBindStageTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowContentBindStageTest.java
index 76470db..1534c84 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowContentBindStageTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowContentBindStageTest.java
@@ -197,7 +197,7 @@
         params.clearDirtyContentViews();
 
         // WHEN low priority is set and stage executed.
-        params.setUseLowPriority(true);
+        params.setUseMinimized(true);
         mRowContentBindStage.executeStage(mEntry, mRow, (en) -> { });
 
         // THEN binder is called with use low priority and contracted/expanded are called to bind.
@@ -210,7 +210,7 @@
                 anyBoolean(),
                 any());
         BindParams usedParams = bindParamsCaptor.getValue();
-        assertTrue(usedParams.isLowPriority);
+        assertTrue(usedParams.isMinimized);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java
index 1f38a73..3b16f14 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java
@@ -67,7 +67,7 @@
 
     @Test
     public void testGetMaxAllowedVisibleChildren_lowPriority() {
-        mChildrenContainer.setIsLowPriority(true);
+        mChildrenContainer.setIsMinimized(true);
         Assert.assertEquals(mChildrenContainer.getMaxAllowedVisibleChildren(),
                 NotificationChildrenContainer.NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED);
     }
@@ -81,7 +81,7 @@
 
     @Test
     public void testGetMaxAllowedVisibleChildren_lowPriority_expandedChildren() {
-        mChildrenContainer.setIsLowPriority(true);
+        mChildrenContainer.setIsMinimized(true);
         mChildrenContainer.setChildrenExpanded(true);
         Assert.assertEquals(mChildrenContainer.getMaxAllowedVisibleChildren(),
                 NotificationChildrenContainer.NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED);
@@ -89,7 +89,7 @@
 
     @Test
     public void testGetMaxAllowedVisibleChildren_lowPriority_userLocked() {
-        mChildrenContainer.setIsLowPriority(true);
+        mChildrenContainer.setIsMinimized(true);
         mChildrenContainer.setUserLocked(true);
         Assert.assertEquals(mChildrenContainer.getMaxAllowedVisibleChildren(),
                 NotificationChildrenContainer.NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED);
@@ -118,7 +118,7 @@
 
     @Test
     public void testShowingAsLowPriority_lowPriority() {
-        mChildrenContainer.setIsLowPriority(true);
+        mChildrenContainer.setIsMinimized(true);
         Assert.assertTrue(mChildrenContainer.showingAsLowPriority());
     }
 
@@ -129,7 +129,7 @@
 
     @Test
     public void testShowingAsLowPriority_lowPriority_expanded() {
-        mChildrenContainer.setIsLowPriority(true);
+        mChildrenContainer.setIsMinimized(true);
         mGroup.setExpandable(true);
         mGroup.setUserExpanded(true, false);
         Assert.assertFalse(mChildrenContainer.showingAsLowPriority());
@@ -140,7 +140,7 @@
         mGroup.setUserLocked(true);
         mGroup.setExpandable(true);
         mGroup.setUserExpanded(true);
-        mChildrenContainer.setIsLowPriority(true);
+        mChildrenContainer.setIsMinimized(true);
         Assert.assertEquals(mChildrenContainer.getMaxAllowedVisibleChildren(),
                 NotificationChildrenContainer.NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED);
     }
@@ -148,14 +148,14 @@
     @Test
     @DisableFlags(AsyncGroupHeaderViewInflation.FLAG_NAME)
     public void testLowPriorityHeaderCleared() {
-        mGroup.setIsLowPriority(true);
+        mGroup.setIsMinimized(true);
         NotificationHeaderView lowPriorityHeaderView =
-                mChildrenContainer.getLowPriorityViewWrapper().getNotificationHeader();
+                mChildrenContainer.getMinimizedGroupHeaderWrapper().getNotificationHeader();
         Assert.assertEquals(View.VISIBLE, lowPriorityHeaderView.getVisibility());
         Assert.assertSame(mChildrenContainer, lowPriorityHeaderView.getParent());
-        mGroup.setIsLowPriority(false);
+        mGroup.setIsMinimized(false);
         assertNull(lowPriorityHeaderView.getParent());
-        assertNull(mChildrenContainer.getLowPriorityViewWrapper());
+        assertNull(mChildrenContainer.getMinimizedGroupHeaderWrapper());
     }
 
     @Test
@@ -169,7 +169,7 @@
     @Test
     @EnableFlags(AsyncGroupHeaderViewInflation.FLAG_NAME)
     public void testSetLowPriorityWithAsyncInflation_noHeaderReInflation() {
-        mChildrenContainer.setIsLowPriority(true);
+        mChildrenContainer.setIsMinimized(true);
         assertNull("We don't inflate header from the main thread with Async "
                 + "Inflation enabled", mChildrenContainer.getCurrentHeaderView());
     }
@@ -179,21 +179,21 @@
     public void setLowPriorityBeforeLowPriorityHeaderSet() {
 
         //Given: the children container does not have a low-priority header, and is not low-priority
-        assertNull(mChildrenContainer.getLowPriorityViewWrapper());
-        mGroup.setIsLowPriority(false);
+        assertNull(mChildrenContainer.getMinimizedGroupHeaderWrapper());
+        mGroup.setIsMinimized(false);
 
         //When: set the children container to be low-priority and set the low-priority header
-        mGroup.setIsLowPriority(true);
-        mGroup.setLowPriorityGroupHeader(createHeaderView(/* lowPriorityHeader= */ true));
+        mGroup.setIsMinimized(true);
+        mGroup.setMinimizedGroupHeader(createHeaderView(/* lowPriorityHeader= */ true));
 
         //Then: the low-priority group header should be visible
         NotificationHeaderView lowPriorityHeaderView =
-                mChildrenContainer.getLowPriorityViewWrapper().getNotificationHeader();
+                mChildrenContainer.getMinimizedGroupHeaderWrapper().getNotificationHeader();
         Assert.assertEquals(View.VISIBLE, lowPriorityHeaderView.getVisibility());
         Assert.assertSame(mChildrenContainer, lowPriorityHeaderView.getParent());
 
         //When: set the children container to be not low-priority and set the normal header
-        mGroup.setIsLowPriority(false);
+        mGroup.setIsMinimized(false);
         mGroup.setGroupHeader(createHeaderView(/* lowPriorityHeader= */ false));
 
         //Then: the low-priority group header should not be visible , normal header should be
@@ -211,9 +211,9 @@
     public void changeLowPriorityAfterHeaderSet() {
 
         //Given: the children container does not have headers, and is not low-priority
-        assertNull(mChildrenContainer.getLowPriorityViewWrapper());
+        assertNull(mChildrenContainer.getMinimizedGroupHeaderWrapper());
         assertNull(mChildrenContainer.getNotificationHeaderWrapper());
-        mGroup.setIsLowPriority(false);
+        mGroup.setIsMinimized(false);
 
         //When: set the set the normal header
         mGroup.setGroupHeader(createHeaderView(/* lowPriorityHeader= */ false));
@@ -225,14 +225,14 @@
         Assert.assertSame(mChildrenContainer, headerView.getParent());
 
         //When: set the set the row to be low priority, and set the low-priority header
-        mGroup.setIsLowPriority(true);
-        mGroup.setLowPriorityGroupHeader(createHeaderView(/* lowPriorityHeader= */ true));
+        mGroup.setIsMinimized(true);
+        mGroup.setMinimizedGroupHeader(createHeaderView(/* lowPriorityHeader= */ true));
 
         //Then: the header view should not be visible, the low-priority group header should be
         // visible
         Assert.assertEquals(View.INVISIBLE, headerView.getVisibility());
         NotificationHeaderView lowPriorityHeaderView =
-                mChildrenContainer.getLowPriorityViewWrapper().getNotificationHeader();
+                mChildrenContainer.getMinimizedGroupHeaderWrapper().getNotificationHeader();
         Assert.assertEquals(View.VISIBLE, lowPriorityHeaderView.getVisibility());
     }
 
@@ -263,7 +263,7 @@
     @Test
     @DisableFlags(AsyncGroupHeaderViewInflation.FLAG_NAME)
     public void applyRoundnessAndInvalidate_should_be_immediately_applied_on_headerLowPriority() {
-        mChildrenContainer.setIsLowPriority(true);
+        mChildrenContainer.setIsMinimized(true);
 
         NotificationHeaderViewWrapper header = mChildrenContainer.getNotificationHeaderWrapper();
         Assert.assertEquals(0f, header.getTopRoundness(), 0.001f);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
index a4f88fb..10d2191 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
@@ -49,7 +49,6 @@
 
 import androidx.test.filters.SmallTest;
 
-import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.UiEventLogger;
 import com.android.internal.logging.nano.MetricsProto;
@@ -63,6 +62,7 @@
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository;
 import com.android.systemui.keyguard.shared.model.KeyguardState;
 import com.android.systemui.keyguard.shared.model.TransitionStep;
+import com.android.systemui.kosmos.KosmosJavaAdapter;
 import com.android.systemui.media.controls.ui.controller.KeyguardMediaController;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
@@ -130,6 +130,7 @@
 @RunWith(AndroidTestingRunner.class)
 public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase {
 
+    protected KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this);
     private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
     @Mock private NotificationGutsManager mNotificationGutsManager;
     @Mock private NotificationsController mNotificationsController;
@@ -167,7 +168,6 @@
     @Mock private SceneContainerFlags mSceneContainerFlags;
     @Mock private Provider<WindowRootView> mWindowRootView;
     @Mock private NotificationStackAppearanceInteractor mNotificationStackAppearanceInteractor;
-    @Mock private InteractionJankMonitor mJankMonitor;
     private final StackStateLogger mStackLogger = new StackStateLogger(logcatLogBuffer(),
             logcatLogBuffer());
     private final NotificationStackScrollLogger mLogger = new NotificationStackScrollLogger(
@@ -1030,7 +1030,7 @@
                 mSceneContainerFlags,
                 mWindowRootView,
                 mNotificationStackAppearanceInteractor,
-                mJankMonitor,
+                mKosmos.getInteractionJankMonitor(),
                 mStackLogger,
                 mLogger,
                 mNotificationStackSizeCalculator,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerFlagDisabledTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerFlagDisabledTest.kt
index 933b5b5..358709f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerFlagDisabledTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerFlagDisabledTest.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.policy
 
 import android.app.IActivityManager
+import android.content.pm.PackageManager
 import android.media.projection.MediaProjectionManager
 import android.os.Handler
 import android.platform.test.annotations.DisableFlags
@@ -44,6 +45,7 @@
     @Mock private lateinit var handler: Handler
     @Mock private lateinit var activityManager: IActivityManager
     @Mock private lateinit var mediaProjectionManager: MediaProjectionManager
+    @Mock private lateinit var packageManager: PackageManager
     private lateinit var controller: SensitiveNotificationProtectionControllerImpl
 
     @Before
@@ -56,6 +58,7 @@
                 FakeGlobalSettings(),
                 mediaProjectionManager,
                 activityManager,
+                packageManager,
                 handler,
                 FakeExecutor(FakeSystemClock()),
                 logger
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt
index 4b4e315..7dfe6d0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt
@@ -25,9 +25,14 @@
 import android.app.NotificationChannel
 import android.app.NotificationManager.IMPORTANCE_HIGH
 import android.app.NotificationManager.VISIBILITY_NO_OVERRIDE
+import android.content.pm.PackageManager
 import android.media.projection.MediaProjectionInfo
 import android.media.projection.MediaProjectionManager
+import android.permission.flags.Flags.FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION
 import android.platform.test.annotations.EnableFlags
+import android.platform.test.annotations.RequiresFlagsDisabled
+import android.platform.test.annotations.RequiresFlagsEnabled
+import android.platform.test.flag.junit.DeviceFlagsValueProvider
 import android.provider.Settings.Global.DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
@@ -48,9 +53,11 @@
 import org.junit.Assert.assertNotNull
 import org.junit.Assert.assertTrue
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyString
 import org.mockito.Mock
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.times
@@ -64,10 +71,13 @@
 @RunWithLooper
 @EnableFlags(Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING)
 class SensitiveNotificationProtectionControllerTest : SysuiTestCase() {
+    @get:Rule val checkFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
+
     private val logger = SensitiveNotificationProtectionControllerLogger(logcatLogBuffer())
 
     @Mock private lateinit var activityManager: IActivityManager
     @Mock private lateinit var mediaProjectionManager: MediaProjectionManager
+    @Mock private lateinit var packageManager: PackageManager
     @Mock private lateinit var mediaProjectionInfo: MediaProjectionInfo
     @Mock private lateinit var listener1: Runnable
     @Mock private lateinit var listener2: Runnable
@@ -87,6 +97,9 @@
         whenever(activityManager.bugreportWhitelistedPackages)
             .thenReturn(listOf(BUGREPORT_PACKAGE_NAME))
 
+        whenever(packageManager.checkPermission(anyString(), anyString()))
+            .thenReturn(PackageManager.PERMISSION_DENIED)
+
         executor = FakeExecutor(FakeSystemClock())
         globalSettings = FakeGlobalSettings()
         controller =
@@ -95,6 +108,7 @@
                 globalSettings,
                 mediaProjectionManager,
                 activityManager,
+                packageManager,
                 mockExecutorHandler(executor),
                 executor,
                 logger
@@ -237,6 +251,36 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION)
+    fun isSensitiveStateActive_projectionActive_permissionExempt_flagDisabled_true() {
+        whenever(
+                packageManager.checkPermission(
+                    android.Manifest.permission.RECORD_SENSITIVE_CONTENT,
+                    mediaProjectionInfo.packageName
+                )
+            )
+            .thenReturn(PackageManager.PERMISSION_GRANTED)
+        mediaProjectionCallback.onStart(mediaProjectionInfo)
+
+        assertTrue(controller.isSensitiveStateActive)
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION)
+    fun isSensitiveStateActive_projectionActive_permissionExempt_false() {
+        whenever(
+                packageManager.checkPermission(
+                    android.Manifest.permission.RECORD_SENSITIVE_CONTENT,
+                    mediaProjectionInfo.packageName
+                )
+            )
+            .thenReturn(PackageManager.PERMISSION_GRANTED)
+        mediaProjectionCallback.onStart(mediaProjectionInfo)
+
+        assertFalse(controller.isSensitiveStateActive)
+    }
+
+    @Test
     fun isSensitiveStateActive_projectionActive_bugReportHandlerExempt_false() {
         whenever(mediaProjectionInfo.packageName).thenReturn(BUGREPORT_PACKAGE_NAME)
         mediaProjectionCallback.onStart(mediaProjectionInfo)
@@ -309,6 +353,40 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION)
+    fun shouldProtectNotification_projectionActive_permissionExempt_flagDisabled_true() {
+        whenever(
+                packageManager.checkPermission(
+                    android.Manifest.permission.RECORD_SENSITIVE_CONTENT,
+                    mediaProjectionInfo.packageName
+                )
+            )
+            .thenReturn(PackageManager.PERMISSION_GRANTED)
+        mediaProjectionCallback.onStart(mediaProjectionInfo)
+
+        val notificationEntry = setupNotificationEntry(TEST_PACKAGE_NAME, false)
+
+        assertTrue(controller.shouldProtectNotification(notificationEntry))
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION)
+    fun shouldProtectNotification_projectionActive_permissionExempt_false() {
+        whenever(
+                packageManager.checkPermission(
+                    android.Manifest.permission.RECORD_SENSITIVE_CONTENT,
+                    mediaProjectionInfo.packageName
+                )
+            )
+            .thenReturn(PackageManager.PERMISSION_GRANTED)
+        mediaProjectionCallback.onStart(mediaProjectionInfo)
+
+        val notificationEntry = setupNotificationEntry(TEST_PACKAGE_NAME, false)
+
+        assertFalse(controller.shouldProtectNotification(notificationEntry))
+    }
+
+    @Test
     fun shouldProtectNotification_projectionActive_bugReportHandlerExempt_false() {
         whenever(mediaProjectionInfo.packageName).thenReturn(BUGREPORT_PACKAGE_NAME)
         mediaProjectionCallback.onStart(mediaProjectionInfo)
@@ -327,6 +405,7 @@
 
         assertFalse(controller.shouldProtectNotification(notificationEntry))
     }
+
     @Test
     fun shouldProtectNotification_projectionActive_publicNotification_false() {
         mediaProjectionCallback.onStart(mediaProjectionInfo)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryKosmos.kt
index 6ef7419..ba07a84 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryKosmos.kt
@@ -19,4 +19,5 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
 
-val Kosmos.facePropertyRepository by Fixture { FakeFacePropertyRepository() }
+val Kosmos.fakeFacePropertyRepository by Fixture { FakeFacePropertyRepository() }
+val Kosmos.facePropertyRepository by Fixture { fakeFacePropertyRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt
index 27803b2..c065545 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.bouncer.domain.interactor
 
-import android.content.applicationContext
 import com.android.systemui.authentication.domain.interactor.authenticationInteractor
 import com.android.systemui.bouncer.data.repository.bouncerRepository
 import com.android.systemui.classifier.domain.interactor.falsingInteractor
@@ -29,12 +28,10 @@
 val Kosmos.bouncerInteractor by Fixture {
     BouncerInteractor(
         applicationScope = testScope.backgroundScope,
-        applicationContext = applicationContext,
         repository = bouncerRepository,
         authenticationInteractor = authenticationInteractor,
         deviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor,
         falsingInteractor = falsingInteractor,
         powerInteractor = powerInteractor,
-        simBouncerInteractor = simBouncerInteractor,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorKosmos.kt
index 8ed9f45..02b79af 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorKosmos.kt
@@ -38,7 +38,7 @@
         telephonyManager = telephonyManager,
         resources = mainResources,
         keyguardUpdateMonitor = keyguardUpdateMonitor,
-        euiccManager = applicationContext.getSystemService(Context.EUICC_SERVICE) as EuiccManager,
+        euiccManager = applicationContext.getSystemService(Context.EUICC_SERVICE) as EuiccManager?,
         mobileConnectionsRepository = mobileConnectionsRepository,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.kt
new file mode 100644
index 0000000..4b64416
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.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.bouncer.ui.viewmodel
+
+import android.content.applicationContext
+import com.android.systemui.authentication.domain.interactor.authenticationInteractor
+import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor
+import com.android.systemui.bouncer.shared.flag.composeBouncerFlags
+import com.android.systemui.deviceentry.domain.interactor.biometricMessageInteractor
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryFingerprintAuthInteractor
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.user.ui.viewmodel.userSwitcherViewModel
+import com.android.systemui.util.time.systemClock
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+@ExperimentalCoroutinesApi
+val Kosmos.bouncerMessageViewModel by
+    Kosmos.Fixture {
+        BouncerMessageViewModel(
+            applicationContext = applicationContext,
+            applicationScope = testScope.backgroundScope,
+            bouncerInteractor = bouncerInteractor,
+            simBouncerInteractor = simBouncerInteractor,
+            authenticationInteractor = authenticationInteractor,
+            selectedUser = userSwitcherViewModel.selectedUser,
+            clock = systemClock,
+            biometricMessageInteractor = biometricMessageInteractor,
+            faceAuthInteractor = deviceEntryFaceAuthInteractor,
+            deviceEntryInteractor = deviceEntryInteractor,
+            fingerprintInteractor = deviceEntryFingerprintAuthInteractor,
+            flags = composeBouncerFlags,
+        )
+    }
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 6d97238..0f6c7cf 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
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
 package com.android.systemui.bouncer.ui.viewmodel
 
 import android.content.applicationContext
@@ -30,7 +32,7 @@
 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
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 
 val Kosmos.bouncerViewModel by Fixture {
     BouncerViewModel(
@@ -47,7 +49,7 @@
         users = userSwitcherViewModel.users,
         userSwitcherMenu = userSwitcherViewModel.menu,
         actionButton = bouncerActionButtonInteractor.actionButton,
-        clock = systemClock,
         devicePolicyManager = mock(),
+        bouncerMessageViewModel = bouncerMessageViewModel,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardClockRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardClockRepository.kt
index 5b642ea..eba5a11 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardClockRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardClockRepository.kt
@@ -45,15 +45,9 @@
     private val _currentClock: MutableStateFlow<ClockController?> = MutableStateFlow(null)
     override val currentClock = _currentClock
 
-    private val _previewClockPair =
-        MutableStateFlow(
-            Pair(
-                Mockito.mock(ClockController::class.java),
-                Mockito.mock(ClockController::class.java)
-            )
-        )
-    override val previewClockPair: StateFlow<Pair<ClockController, ClockController>> =
-        _previewClockPair
+    private val _previewClock = MutableStateFlow(Mockito.mock(ClockController::class.java))
+    override val previewClock: Flow<ClockController>
+        get() = _previewClock
     override val clockEventController: ClockEventController
         get() = mock()
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
index dcbd577..de6bfb2 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
@@ -18,7 +18,6 @@
 package com.android.systemui.keyguard.data.repository
 
 import android.annotation.FloatRange
-import android.util.Log
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionInfo
@@ -48,21 +47,8 @@
     override val transitions: SharedFlow<TransitionStep> = _transitions
 
     init {
-        _transitions.tryEmit(
-            TransitionStep(
-                transitionState = TransitionState.STARTED,
-                from = KeyguardState.OFF,
-                to = KeyguardState.LOCKSCREEN,
-            )
-        )
-
-        _transitions.tryEmit(
-            TransitionStep(
-                transitionState = TransitionState.FINISHED,
-                from = KeyguardState.OFF,
-                to = KeyguardState.LOCKSCREEN,
-            )
-        )
+        // Seed the fake repository with the same initial steps the actual repository uses.
+        KeyguardTransitionRepositoryImpl.initialTransitionSteps.forEach { _transitions.tryEmit(it) }
     }
 
     /**
@@ -207,16 +193,15 @@
     suspend fun sendTransitionSteps(
         steps: List<TransitionStep>,
         testScope: TestScope,
-        validateStep: Boolean = true
+        validateSteps: Boolean = true
     ) {
         steps.forEach {
-            sendTransitionStep(step = it, validateStep = validateStep)
+            sendTransitionStep(step = it, validateStep = validateSteps)
             testScope.testScheduler.runCurrent()
         }
     }
 
     override fun startTransition(info: TransitionInfo): UUID? {
-        Log.i("TEST", "Start transition: ", Exception())
         return if (info.animator == null) UUID.randomUUID() else null
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt
index 73fd999..709f864 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.testScope
 import com.android.systemui.scene.shared.flag.sceneContainerFlags
 import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.statusbar.phone.statusBarKeyguardViewManager
@@ -50,5 +51,6 @@
         keyguardViewController = { statusBarKeyguardViewManager },
         deviceEntryInteractor = deviceEntryInteractor,
         deviceEntrySourceInteractor = deviceEntrySourceInteractor,
+        scope = testScope.backgroundScope,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGoneTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGoneTransitionViewModelKosmos.kt
new file mode 100644
index 0000000..f389142
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGoneTransitionViewModelKosmos.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.keyguard.ui.viewmodel
+
+import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
+import com.android.systemui.kosmos.Kosmos
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+@ExperimentalCoroutinesApi
+val Kosmos.dreamingToGoneTransitionViewModel by
+    Kosmos.Fixture {
+        DreamingToGoneTransitionViewModel(
+            animationFlow = keyguardTransitionAnimationFlow,
+        )
+    }
\ No newline at end of file
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToLockscreenTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToLockscreenTransitionViewModelKosmos.kt
new file mode 100644
index 0000000..1b6fa06
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToLockscreenTransitionViewModelKosmos.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+var Kosmos.goneToLockscreenTransitionViewModel by Fixture {
+    GoneToLockscreenTransitionViewModel(
+        animationFlow = keyguardTransitionAnimationFlow,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
index a863edf..b91aafe 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
@@ -46,10 +46,13 @@
         dozingToGoneTransitionViewModel = dozingToGoneTransitionViewModel,
         dozingToLockscreenTransitionViewModel = dozingToLockscreenTransitionViewModel,
         dozingToOccludedTransitionViewModel = dozingToOccludedTransitionViewModel,
+        dreamingToGoneTransitionViewModel = dreamingToGoneTransitionViewModel,
         dreamingToLockscreenTransitionViewModel = dreamingToLockscreenTransitionViewModel,
         glanceableHubToLockscreenTransitionViewModel = glanceableHubToLockscreenTransitionViewModel,
         goneToAodTransitionViewModel = goneToAodTransitionViewModel,
         goneToDozingTransitionViewModel = goneToDozingTransitionViewModel,
+        goneToDreamingTransitionViewModel = goneToDreamingTransitionViewModel,
+        goneToLockscreenTransitionViewModel = goneToLockscreenTransitionViewModel,
         lockscreenToAodTransitionViewModel = lockscreenToAodTransitionViewModel,
         lockscreenToDozingTransitionViewModel = lockscreenToDozingTransitionViewModel,
         lockscreenToDreamingTransitionViewModel = lockscreenToDreamingTransitionViewModel,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelKosmos.kt
index 8566251..370afc3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelKosmos.kt
@@ -18,7 +18,6 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
-import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor
 import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
@@ -26,7 +25,6 @@
 
 val Kosmos.primaryBouncerToLockscreenTransitionViewModel by Fixture {
     PrimaryBouncerToLockscreenTransitionViewModel(
-        deviceEntryUdfpsInteractor = deviceEntryUdfpsInteractor,
         animationFlow = keyguardTransitionAnimationFlow,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/data/repository/MediaDataRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/data/repository/MediaDataRepositoryKosmos.kt
new file mode 100644
index 0000000..5c17cb9
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/data/repository/MediaDataRepositoryKosmos.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.media.controls.data.repository
+
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.media.controls.util.mediaFlags
+
+val Kosmos.mediaDataRepository by Fixture {
+    MediaDataRepository(mediaFlags = mediaFlags, dumpManager = dumpManager)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryKosmos.kt
new file mode 100644
index 0000000..7ce810e
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryKosmos.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.media.controls.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.mediaFilterRepository by Kosmos.Fixture { MediaFilterRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataCombineLatestKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataCombineLatestKosmos.kt
new file mode 100644
index 0000000..12a6325
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataCombineLatestKosmos.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.media.controls.domain.pipeline
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.mediaDataCombineLatest by Kosmos.Fixture { MediaDataCombineLatest() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterKosmos.kt
new file mode 100644
index 0000000..d56222e
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterKosmos.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.media.controls.domain.pipeline
+
+import android.content.applicationContext
+import com.android.systemui.broadcast.BroadcastSender
+import com.android.systemui.concurrency.fakeExecutor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.media.controls.data.repository.mediaFilterRepository
+import com.android.systemui.media.controls.util.mediaFlags
+import com.android.systemui.media.controls.util.mediaUiEventLogger
+import com.android.systemui.settings.userTracker
+import com.android.systemui.statusbar.notificationLockscreenUserManager
+import com.android.systemui.util.time.systemClock
+import com.android.systemui.util.wakelock.WakeLockFake
+
+val Kosmos.mediaDataFilter by
+    Kosmos.Fixture {
+        MediaDataFilterImpl(
+            context = applicationContext,
+            userTracker = userTracker,
+            broadcastSender =
+                BroadcastSender(
+                    applicationContext,
+                    WakeLockFake.Builder(applicationContext),
+                    fakeExecutor
+                ),
+            lockscreenUserManager = notificationLockscreenUserManager,
+            executor = fakeExecutor,
+            systemClock = systemClock,
+            logger = mediaUiEventLogger,
+            mediaFlags = mediaFlags,
+            mediaFilterRepository = mediaFilterRepository,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt
new file mode 100644
index 0000000..cc1ad1f
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.media.controls.domain.pipeline
+
+import android.app.smartspace.SmartspaceManager
+import android.content.applicationContext
+import android.os.fakeExecutorHandler
+import com.android.keyguard.keyguardUpdateMonitor
+import com.android.systemui.broadcast.broadcastDispatcher
+import com.android.systemui.concurrency.fakeExecutor
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.media.controls.data.repository.mediaDataRepository
+import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvider
+import com.android.systemui.media.controls.util.mediaControllerFactory
+import com.android.systemui.media.controls.util.mediaFlags
+import com.android.systemui.media.controls.util.mediaUiEventLogger
+import com.android.systemui.plugins.activityStarter
+import com.android.systemui.util.Utils
+import com.android.systemui.util.settings.fakeSettings
+import com.android.systemui.util.time.systemClock
+
+val Kosmos.mediaDataProcessor by
+    Kosmos.Fixture {
+        MediaDataProcessor(
+            context = applicationContext,
+            applicationScope = applicationCoroutineScope,
+            backgroundDispatcher = testDispatcher,
+            backgroundExecutor = fakeExecutor,
+            uiExecutor = fakeExecutor,
+            foregroundExecutor = fakeExecutor,
+            handler = fakeExecutorHandler,
+            mediaControllerFactory = mediaControllerFactory,
+            broadcastDispatcher = broadcastDispatcher,
+            dumpManager = dumpManager,
+            activityStarter = activityStarter,
+            smartspaceMediaDataProvider = SmartspaceMediaDataProvider(),
+            useMediaResumption = Utils.useMediaResumption(applicationContext),
+            useQsMediaPlayer = Utils.useQsMediaPlayer(applicationContext),
+            systemClock = systemClock,
+            secureSettings = fakeSettings,
+            mediaFlags = mediaFlags,
+            logger = mediaUiEventLogger,
+            smartspaceManager = SmartspaceManager(applicationContext),
+            keyguardUpdateMonitor = keyguardUpdateMonitor,
+            mediaDataRepository = mediaDataRepository,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerKosmos.kt
new file mode 100644
index 0000000..b98f557
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerKosmos.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.media.controls.domain.pipeline
+
+import android.content.applicationContext
+import android.media.MediaRouter2Manager
+import android.os.fakeExecutorHandler
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.systemui.concurrency.fakeExecutor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.media.controls.util.localMediaManagerFactory
+import com.android.systemui.media.controls.util.mediaControllerFactory
+import com.android.systemui.media.muteawait.mediaMuteAwaitConnectionManagerFactory
+import com.android.systemui.statusbar.policy.configurationController
+
+val Kosmos.mediaDeviceManager by
+    Kosmos.Fixture {
+        MediaDeviceManager(
+            context = applicationContext,
+            controllerFactory = mediaControllerFactory,
+            localMediaManagerFactory = localMediaManagerFactory,
+            mr2manager = { MediaRouter2Manager.getInstance(applicationContext) },
+            muteAwaitConnectionManagerFactory = mediaMuteAwaitConnectionManagerFactory,
+            configurationController = configurationController,
+            localBluetoothManager = {
+                LocalBluetoothManager.create(applicationContext, fakeExecutorHandler)
+            },
+            fgExecutor = fakeExecutor,
+            bgExecutor = fakeExecutor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaResumeListenerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaResumeListenerKosmos.kt
new file mode 100644
index 0000000..2a3e84b
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaResumeListenerKosmos.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.media.controls.domain.pipeline
+
+import android.content.applicationContext
+import com.android.systemui.broadcast.broadcastDispatcher
+import com.android.systemui.concurrency.fakeExecutor
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.media.controls.domain.resume.MediaResumeListener
+import com.android.systemui.media.controls.domain.resume.resumeMediaBrowserFactory
+import com.android.systemui.media.controls.util.mediaFlags
+import com.android.systemui.settings.userTracker
+import com.android.systemui.tuner.TunerService
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.time.systemClock
+
+val Kosmos.mediaResumeListener by
+    Kosmos.Fixture {
+        MediaResumeListener(
+            context = applicationContext,
+            broadcastDispatcher = broadcastDispatcher,
+            userTracker = userTracker,
+            mainExecutor = fakeExecutor,
+            backgroundExecutor = fakeExecutor,
+            tunerService = mock<TunerService> {},
+            mediaBrowserFactory = resumeMediaBrowserFactory,
+            dumpManager = dumpManager,
+            systemClock = systemClock,
+            mediaFlags = mediaFlags,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaSessionBasedFilterKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaSessionBasedFilterKosmos.kt
new file mode 100644
index 0000000..9b02a5b
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaSessionBasedFilterKosmos.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.media.controls.domain.pipeline
+
+import android.content.applicationContext
+import android.media.session.MediaSessionManager
+import com.android.systemui.concurrency.fakeExecutor
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.mediaSessionBasedFilter by
+    Kosmos.Fixture {
+        MediaSessionBasedFilter(
+            context = applicationContext,
+            sessionManager = MediaSessionManager(applicationContext),
+            foregroundExecutor = fakeExecutor,
+            backgroundExecutor = fakeExecutor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerKosmos.kt
new file mode 100644
index 0000000..6ec6378
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerKosmos.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.media.controls.domain.pipeline
+
+import com.android.systemui.concurrency.fakeExecutor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.log.logcatLogBuffer
+import com.android.systemui.media.controls.util.mediaControllerFactory
+import com.android.systemui.media.controls.util.mediaFlags
+import com.android.systemui.plugins.statusbar.statusBarStateController
+import com.android.systemui.util.time.systemClock
+
+val Kosmos.mediaTimeoutListener by
+    Kosmos.Fixture {
+        MediaTimeoutListener(
+            mediaControllerFactory = mediaControllerFactory,
+            mainExecutor = fakeExecutor,
+            logger = MediaTimeoutLogger(logcatLogBuffer("MediaTimeoutLogBuffer")),
+            statusBarStateController = statusBarStateController,
+            systemClock = systemClock,
+            mediaFlags = mediaFlags,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractorKosmos.kt
new file mode 100644
index 0000000..e5e2aff
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractorKosmos.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.media.controls.domain.pipeline.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.media.controls.data.repository.mediaDataRepository
+import com.android.systemui.media.controls.data.repository.mediaFilterRepository
+import com.android.systemui.media.controls.domain.pipeline.mediaDataCombineLatest
+import com.android.systemui.media.controls.domain.pipeline.mediaDataFilter
+import com.android.systemui.media.controls.domain.pipeline.mediaDataProcessor
+import com.android.systemui.media.controls.domain.pipeline.mediaDeviceManager
+import com.android.systemui.media.controls.domain.pipeline.mediaResumeListener
+import com.android.systemui.media.controls.domain.pipeline.mediaSessionBasedFilter
+import com.android.systemui.media.controls.domain.pipeline.mediaTimeoutListener
+import com.android.systemui.media.controls.util.mediaFlags
+
+val Kosmos.mediaCarouselInteractor by
+    Kosmos.Fixture {
+        MediaCarouselInteractor(
+            applicationScope = applicationCoroutineScope,
+            mediaDataRepository = mediaDataRepository,
+            mediaDataProcessor = mediaDataProcessor,
+            mediaTimeoutListener = mediaTimeoutListener,
+            mediaResumeListener = mediaResumeListener,
+            mediaSessionBasedFilter = mediaSessionBasedFilter,
+            mediaDeviceManager = mediaDeviceManager,
+            mediaDataCombineLatest = mediaDataCombineLatest,
+            mediaDataFilter = mediaDataFilter,
+            mediaFilterRepository = mediaFilterRepository,
+            mediaFlags = mediaFlags,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/resume/MediaBrowserFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/resume/MediaBrowserFactoryKosmos.kt
new file mode 100644
index 0000000..2621869
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/resume/MediaBrowserFactoryKosmos.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.media.controls.domain.resume
+
+import android.content.applicationContext
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.mediaBrowserFactory by Kosmos.Fixture { MediaBrowserFactory(applicationContext) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/resume/ResumeMediaBrowserFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/resume/ResumeMediaBrowserFactoryKosmos.kt
new file mode 100644
index 0000000..ed720bd
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/resume/ResumeMediaBrowserFactoryKosmos.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.media.controls.domain.resume
+
+import android.content.applicationContext
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.log.logcatLogBuffer
+
+val Kosmos.resumeMediaBrowserFactory by
+    Kosmos.Fixture {
+        ResumeMediaBrowserFactory(
+            applicationContext,
+            mediaBrowserFactory,
+            ResumeMediaBrowserLogger(logcatLogBuffer("ResumeMediaLogBuffer"))
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/LocalMediaManagerFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/LocalMediaManagerFactoryKosmos.kt
new file mode 100644
index 0000000..2e0c9b8
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/LocalMediaManagerFactoryKosmos.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.media.controls.util
+
+import android.content.applicationContext
+import android.os.fakeExecutorHandler
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.localMediaManagerFactory by
+    Kosmos.Fixture {
+        LocalMediaManagerFactory(
+            context = applicationContext,
+            localBluetoothManager =
+                LocalBluetoothManager.create(applicationContext, fakeExecutorHandler),
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaControllerFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaControllerFactoryKosmos.kt
new file mode 100644
index 0000000..1ce6e82
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaControllerFactoryKosmos.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.media.controls.util
+
+import android.content.applicationContext
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.mediaControllerFactory by Kosmos.Fixture { MediaControllerFactory(applicationContext) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaFlagsKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaFlagsKosmos.kt
new file mode 100644
index 0000000..6f652f2
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaFlagsKosmos.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.media.controls.util
+
+import com.android.systemui.flags.featureFlagsClassic
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.scene.shared.flag.sceneContainerFlags
+
+val Kosmos.mediaFlags by
+    Kosmos.Fixture {
+        MediaFlags(featureFlags = featureFlagsClassic, sceneContainerFlags = sceneContainerFlags)
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaUiEventLoggerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaUiEventLoggerKosmos.kt
new file mode 100644
index 0000000..b01876d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaUiEventLoggerKosmos.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.media.controls.util
+
+import com.android.internal.logging.uiEventLogger
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.mediaUiEventLogger by Kosmos.Fixture { MediaUiEventLogger(uiEventLogger) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactoryKosmos.kt
new file mode 100644
index 0000000..b78bd58
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactoryKosmos.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.media.muteawait
+
+import android.content.applicationContext
+import com.android.systemui.concurrency.fakeExecutor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.log.logcatLogBuffer
+
+val Kosmos.mediaMuteAwaitConnectionManagerFactory by
+    Kosmos.Fixture {
+        MediaMuteAwaitConnectionManagerFactory(
+            context = applicationContext,
+            logger = MediaMuteAwaitLogger(logcatLogBuffer("MediaMuteAwaitLogBuffer")),
+            mainExecutor = fakeExecutor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/work/WorkModeTileKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/work/WorkModeTileKosmos.kt
new file mode 100644
index 0000000..c04c5ed
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/work/WorkModeTileKosmos.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.qs.tiles.impl.work
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.qsEventLogger
+import com.android.systemui.statusbar.policy.PolicyModule
+
+val Kosmos.qsWorkModeTileConfig by
+    Kosmos.Fixture { PolicyModule.provideWorkModeTileConfig(qsEventLogger) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorKosmos.kt
index 546a1e0..5605d10 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorKosmos.kt
@@ -18,10 +18,12 @@
 
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.statusbar.notification.stack.data.repository.notificationStackAppearanceRepository
 
 val Kosmos.notificationStackAppearanceInteractor by Fixture {
     NotificationStackAppearanceInteractor(
         repository = notificationStackAppearanceRepository,
+        shadeInteractor = shadeInteractor,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeManagedProfileController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeManagedProfileController.java
index 18b07cf..59adb11 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeManagedProfileController.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeManagedProfileController.java
@@ -19,24 +19,65 @@
 import com.android.systemui.statusbar.phone.ManagedProfileController;
 import com.android.systemui.statusbar.phone.ManagedProfileController.Callback;
 
+import java.util.ArrayList;
+import java.util.List;
+
 public class FakeManagedProfileController extends BaseLeakChecker<Callback> implements
         ManagedProfileController {
+
+    private List<Callback> mCallbackList = new ArrayList<>();
+    private boolean mIsEnabled = false;
+    private boolean mHasActiveProfile = false;
+
     public FakeManagedProfileController(LeakCheck test) {
         super(test, "profile");
     }
 
     @Override
+    public void addCallback(Callback cb) {
+        mCallbackList.add(cb);
+        cb.onManagedProfileChanged();
+    }
+
+    @Override
+    public void removeCallback(Callback cb) {
+        mCallbackList.remove(cb);
+    }
+
+    @Override
     public void setWorkModeEnabled(boolean enabled) {
+        if (mIsEnabled != enabled) {
+            mIsEnabled = enabled;
+            for (Callback cb: mCallbackList) {
+                cb.onManagedProfileChanged();
+            }
+        }
 
     }
 
     @Override
     public boolean hasActiveProfile() {
-        return false;
+        return mHasActiveProfile;
+    }
+
+    /**
+     * Triggers onManagedProfileChanged on callbacks when value flips.
+     */
+    public void setHasActiveProfile(boolean hasActiveProfile) {
+        if (mHasActiveProfile != hasActiveProfile) {
+            mHasActiveProfile = hasActiveProfile;
+            for (Callback cb: mCallbackList) {
+                cb.onManagedProfileChanged();
+                if (!hasActiveProfile) {
+                    cb.onManagedProfileRemoved();
+                }
+            }
+        }
+
     }
 
     @Override
     public boolean isWorkModeEnabled() {
-        return false;
+        return mIsEnabled;
     }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaControllerKosmos.kt
new file mode 100644
index 0000000..5db1724
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaControllerKosmos.kt
@@ -0,0 +1,82 @@
+/*
+ * 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.volume
+
+import android.content.packageManager
+import android.content.pm.ApplicationInfo
+import android.media.AudioAttributes
+import android.media.session.MediaController
+import android.media.session.MediaSession
+import com.android.systemui.kosmos.Kosmos
+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
+
+private const val LOCAL_PACKAGE = "local.test.pkg"
+var Kosmos.localMediaController: MediaController by
+    Kosmos.Fixture {
+        val appInfo: ApplicationInfo = mock {
+            whenever(loadLabel(any())).thenReturn("local_media_controller_label")
+        }
+        whenever(packageManager.getApplicationInfo(eq(LOCAL_PACKAGE), any<Int>()))
+            .thenReturn(appInfo)
+
+        val localSessionToken: MediaSession.Token = MediaSession.Token(0, mock {})
+        mock {
+            whenever(packageName).thenReturn(LOCAL_PACKAGE)
+            whenever(playbackInfo)
+                .thenReturn(
+                    MediaController.PlaybackInfo(
+                        MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL,
+                        0,
+                        0,
+                        0,
+                        AudioAttributes.Builder().build(),
+                        "",
+                    )
+                )
+            whenever(sessionToken).thenReturn(localSessionToken)
+        }
+    }
+
+private const val REMOTE_PACKAGE = "remote.test.pkg"
+var Kosmos.remoteMediaController: MediaController by
+    Kosmos.Fixture {
+        val appInfo: ApplicationInfo = mock {
+            whenever(loadLabel(any())).thenReturn("remote_media_controller_label")
+        }
+        whenever(packageManager.getApplicationInfo(eq(REMOTE_PACKAGE), any<Int>()))
+            .thenReturn(appInfo)
+
+        val remoteSessionToken: MediaSession.Token = MediaSession.Token(0, mock {})
+        mock {
+            whenever(packageName).thenReturn(REMOTE_PACKAGE)
+            whenever(playbackInfo)
+                .thenReturn(
+                    MediaController.PlaybackInfo(
+                        MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE,
+                        0,
+                        0,
+                        0,
+                        AudioAttributes.Builder().build(),
+                        "",
+                    )
+                )
+            whenever(sessionToken).thenReturn(remoteSessionToken)
+        }
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt
index 3938f77..fa3a19b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt
@@ -18,7 +18,6 @@
 
 import android.content.packageManager
 import android.content.pm.ApplicationInfo
-import android.media.session.MediaController
 import android.os.Handler
 import android.testing.TestableLooper
 import com.android.systemui.kosmos.Kosmos
@@ -32,11 +31,10 @@
 import com.android.systemui.volume.data.repository.FakeMediaControllerRepository
 import com.android.systemui.volume.panel.component.mediaoutput.data.repository.FakeLocalMediaRepositoryFactory
 import com.android.systemui.volume.panel.component.mediaoutput.data.repository.LocalMediaRepositoryFactory
+import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaDeviceSessionInteractor
 import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputActionsInteractor
 import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor
 
-var Kosmos.mediaController: MediaController by Kosmos.Fixture { mock {} }
-
 val Kosmos.localMediaRepository by Kosmos.Fixture { FakeLocalMediaRepository() }
 val Kosmos.localMediaRepositoryFactory: LocalMediaRepositoryFactory by
     Kosmos.Fixture { FakeLocalMediaRepositoryFactory { localMediaRepository } }
@@ -56,6 +54,14 @@
             },
             testScope.backgroundScope,
             testScope.testScheduler,
+            mediaControllerRepository,
+        )
+    }
+
+val Kosmos.mediaDeviceSessionInteractor by
+    Kosmos.Fixture {
+        MediaDeviceSessionInteractor(
+            testScope.testScheduler,
             Handler(TestableLooper.get(testCase).looper),
             mediaControllerRepository,
         )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeLocalMediaRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeLocalMediaRepository.kt
index 284bd55..909be75 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeLocalMediaRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeLocalMediaRepository.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.volume.data.repository
 
 import com.android.settingslib.media.MediaDevice
-import com.android.settingslib.volume.data.model.RoutingSession
 import com.android.settingslib.volume.data.repository.LocalMediaRepository
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
@@ -25,35 +24,11 @@
 
 class FakeLocalMediaRepository : LocalMediaRepository {
 
-    private val volumeBySession: MutableMap<String?, Int> = mutableMapOf()
-
-    private val mutableMediaDevices = MutableStateFlow<List<MediaDevice>>(emptyList())
-    override val mediaDevices: StateFlow<List<MediaDevice>>
-        get() = mutableMediaDevices.asStateFlow()
-
     private val mutableCurrentConnectedDevice = MutableStateFlow<MediaDevice?>(null)
     override val currentConnectedDevice: StateFlow<MediaDevice?>
         get() = mutableCurrentConnectedDevice.asStateFlow()
 
-    private val mutableRemoteRoutingSessions = MutableStateFlow<List<RoutingSession>>(emptyList())
-    override val remoteRoutingSessions: StateFlow<List<RoutingSession>>
-        get() = mutableRemoteRoutingSessions.asStateFlow()
-
-    fun updateMediaDevices(devices: List<MediaDevice>) {
-        mutableMediaDevices.value = devices
-    }
-
     fun updateCurrentConnectedDevice(device: MediaDevice?) {
         mutableCurrentConnectedDevice.value = device
     }
-
-    fun updateRemoteRoutingSessions(sessions: List<RoutingSession>) {
-        mutableRemoteRoutingSessions.value = sessions
-    }
-
-    fun getSessionVolume(sessionId: String?): Int = volumeBySession.getOrDefault(sessionId, 0)
-
-    override suspend fun adjustSessionVolume(sessionId: String?, volume: Int) {
-        volumeBySession[sessionId] = volume
-    }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeMediaControllerRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeMediaControllerRepository.kt
index 6d52e52..8ab5bd90 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeMediaControllerRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeMediaControllerRepository.kt
@@ -24,11 +24,11 @@
 
 class FakeMediaControllerRepository : MediaControllerRepository {
 
-    private val mutableActiveLocalMediaController = MutableStateFlow<MediaController?>(null)
-    override val activeLocalMediaController: StateFlow<MediaController?> =
-        mutableActiveLocalMediaController.asStateFlow()
+    private val mutableActiveSessions = MutableStateFlow<List<MediaController>>(emptyList())
+    override val activeSessions: StateFlow<List<MediaController>>
+        get() = mutableActiveSessions.asStateFlow()
 
-    fun setActiveLocalMediaController(controller: MediaController?) {
-        mutableActiveLocalMediaController.value = controller
+    fun setActiveSessions(sessions: List<MediaController>) {
+        mutableActiveSessions.value = sessions
     }
 }
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java
index 81ad31e..61ec7b4 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java
+++ b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java
@@ -383,9 +383,21 @@
         // Assume false for now, because we don't support writing FDs yet.
         return false;
     }
+
     public static boolean nativeHasFileDescriptorsInRange(
             long nativePtr, int offset, int length) {
         // Assume false for now, because we don't support writing FDs yet.
         return false;
     }
+
+    public static boolean nativeHasBinders(long nativePtr) {
+        // Assume false for now, because we don't support adding binders.
+        return false;
+    }
+
+    public static boolean nativeHasBindersInRange(
+            long nativePtr, int offset, int length) {
+        // Assume false for now, because we don't support writing FDs yet.
+        return false;
+    }
 }
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 880a687..3e7682a 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -2703,11 +2703,13 @@
         Map<ComponentName, AccessibilityServiceConnection> componentNameToServiceMap =
                 userState.mComponentNameToServiceMap;
         boolean isUnlockingOrUnlocked = mUmi.isUserUnlockingOrUnlocked(userState.mUserId);
+        Set<ComponentName> installedComponentNames = new HashSet<>();
 
         for (int i = 0, count = userState.mInstalledServices.size(); i < count; i++) {
             AccessibilityServiceInfo installedService = userState.mInstalledServices.get(i);
             ComponentName componentName = ComponentName.unflattenFromString(
                     installedService.getId());
+            installedComponentNames.add(componentName);
 
             AccessibilityServiceConnection service = componentNameToServiceMap.get(componentName);
 
@@ -2767,6 +2769,28 @@
             audioManager.setAccessibilityServiceUids(mTempIntArray);
         }
         mActivityTaskManagerService.setAccessibilityServiceUids(mTempIntArray);
+        final Iterator<ComponentName> it = userState.mEnabledServices.iterator();
+        boolean anyServiceRemoved = false;
+        while (it.hasNext()) {
+            final ComponentName comp = it.next();
+            if (!installedComponentNames.contains(comp)) {
+                it.remove();
+                userState.mTouchExplorationGrantedServices.remove(comp);
+                anyServiceRemoved = true;
+            }
+        }
+        if (anyServiceRemoved) {
+            // Update the enabled services setting.
+            persistComponentNamesToSettingLocked(
+                    Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
+                    userState.mEnabledServices,
+                    userState.mUserId);
+            // Update the touch exploration granted services setting.
+            persistComponentNamesToSettingLocked(
+                    Settings.Secure.TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES,
+                    userState.mTouchExplorationGrantedServices,
+                    userState.mUserId);
+        }
         updateAccessibilityEnabledSettingLocked(userState);
     }
 
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java
index c570d65..d307484 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java
@@ -79,6 +79,8 @@
 
     private static int sNextWindowId;
 
+    private final Region mTmpRegion = new Region();
+
     private final Object mLock;
     private final Handler mHandler;
     private final WindowManagerInternal mWindowManagerInternal;
@@ -613,7 +615,7 @@
             }
 
             // If the window is completely covered by other windows - ignore.
-            if (unaccountedSpace.quickReject(regionInScreen)) {
+            if (!mTmpRegion.op(unaccountedSpace, regionInScreen, Region.Op.INTERSECT)) {
                 return false;
             }
 
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
index e4f1d3a..07fcb50 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
@@ -718,7 +718,9 @@
                         + ", mPccUseFallbackDetection=" + mPccUseFallbackDetection
                         + ", mPccProviderHints=" + mPccProviderHints
                         + ", mAutofillCredmanIntegrationEnabled="
-                        + mAutofillCredmanIntegrationEnabled);
+                        + mAutofillCredmanIntegrationEnabled
+                        + ", mIsFillFieldsFromCurrentSessionOnly="
+                        + mIsFillFieldsFromCurrentSessionOnly);
             }
         }
     }
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
index e1291e5..14a331c 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
@@ -1672,9 +1672,10 @@
 
         @Override // from InlineSuggestionRenderCallbacksImpl
         public void onServiceDied(@NonNull RemoteInlineSuggestionRenderService service) {
-            // Don't do anything; eventually the system will bind to it again...
             Slog.w(TAG, "remote service died: " + service);
-            mRemoteInlineSuggestionRenderService = null;
+            synchronized (mLock) {
+                resetExtServiceLocked();
+            }
         }
     }
 
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index 8244d20..3ec6e47 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -23,6 +23,7 @@
 import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
 import static android.companion.virtual.VirtualDeviceParams.NAVIGATION_POLICY_DEFAULT_ALLOWED;
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_ACTIVITY;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO;
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CAMERA;
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CLIPBOARD;
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_RECENTS;
@@ -82,6 +83,8 @@
 import android.hardware.input.VirtualStylusMotionEvent;
 import android.hardware.input.VirtualTouchEvent;
 import android.hardware.input.VirtualTouchscreenConfig;
+import android.media.AudioManager;
+import android.media.audiopolicy.AudioMix;
 import android.os.Binder;
 import android.os.IBinder;
 import android.os.LocaleList;
@@ -1063,6 +1066,37 @@
     }
 
     @Override
+    public boolean hasCustomAudioInputSupport() throws RemoteException {
+        if (!Flags.vdmPublicApis()) {
+            return false;
+        }
+
+        if (!android.media.audiopolicy.Flags.audioMixTestApi()) {
+            return false;
+        }
+        if (!android.media.audiopolicy.Flags.recordAudioDeviceAwarePermission()) {
+            return false;
+        }
+
+        if (getDevicePolicy(POLICY_TYPE_AUDIO) == VirtualDeviceParams.DEVICE_POLICY_DEFAULT) {
+            return false;
+        }
+        final long token = Binder.clearCallingIdentity();
+        try {
+            AudioManager audioManager = mContext.getSystemService(AudioManager.class);
+            for (AudioMix mix : audioManager.getRegisteredPolicyMixes()) {
+                if (mix.matchesVirtualDeviceId(getDeviceId())
+                        && mix.getMixType() == AudioMix.MIX_TYPE_RECORDERS) {
+                    return true;
+                }
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+        return false;
+    }
+
+    @Override
     protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
         String indent = "    ";
         fout.println("  VirtualDevice: ");
diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
index 6b5ba96..2607ed3 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
@@ -1297,15 +1297,19 @@
 
         @Override
         public void onLoginDetected(@NonNull ParceledListSlice<ContentCaptureEvent> events) {
-            RemoteContentProtectionService service = createRemoteContentProtectionService();
-            if (service == null) {
-                return;
-            }
-            try {
-                service.onLoginDetected(events);
-            } catch (Exception ex) {
-                Slog.e(TAG, "Failed to call remote service", ex);
-            }
+            Binder.withCleanCallingIdentity(
+                    () -> {
+                        RemoteContentProtectionService service =
+                                createRemoteContentProtectionService();
+                        if (service == null) {
+                            return;
+                        }
+                        try {
+                            service.onLoginDetected(events);
+                        } catch (Exception ex) {
+                            Slog.e(TAG, "Failed to call remote service", ex);
+                        }
+                    });
         }
     }
 
diff --git a/services/core/Android.bp b/services/core/Android.bp
index d1d7ee7..7f5867f 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -242,6 +242,7 @@
         "apache-commons-math",
         "backstage_power_flags_lib",
         "notification_flags_lib",
+        "power_hint_flags_lib",
         "biometrics_flags_lib",
         "am_flags_lib",
         "com_android_server_accessibility_flags_lib",
diff --git a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
index cc40940..589d8b3 100644
--- a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
+++ b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
@@ -69,6 +69,8 @@
 
     final Object mSensitiveContentProtectionLock = new Object();
 
+    private final ArraySet<PackageInfo> mPackagesShowingSensitiveContent = new ArraySet<>();
+
     @GuardedBy("mSensitiveContentProtectionLock")
     private boolean mProjectionActive = false;
 
@@ -205,6 +207,10 @@
             if (sensitiveNotificationAppProtection()) {
                 updateAppsThatShouldBlockScreenCapture();
             }
+
+            if (sensitiveContentAppProtection() && mPackagesShowingSensitiveContent.size() > 0) {
+                mWindowManager.addBlockScreenCaptureForApps(mPackagesShowingSensitiveContent);
+            }
         }
     }
 
@@ -354,17 +360,27 @@
     void setSensitiveContentProtection(IBinder windowToken, String packageName, int uid,
             boolean isShowingSensitiveContent) {
         synchronized (mSensitiveContentProtectionLock) {
+            // The window token distinguish this package from packages added for notifications.
+            PackageInfo packageInfo = new PackageInfo(packageName, uid, windowToken);
+            // track these packages to protect when screen share starts.
+            if (isShowingSensitiveContent) {
+                mPackagesShowingSensitiveContent.add(packageInfo);
+                if (mPackagesShowingSensitiveContent.size() > 100) {
+                    Log.w(TAG, "Unexpectedly large number of sensitive windows, count: "
+                            + mPackagesShowingSensitiveContent.size());
+                }
+            } else {
+                mPackagesShowingSensitiveContent.remove(packageInfo);
+            }
             if (!mProjectionActive) {
                 return;
             }
             if (DEBUG) {
-                Log.d(TAG, "setSensitiveContentProtection - windowToken=" + windowToken
-                        + ", package=" + packageName + ", uid=" + uid
-                        + ", isShowingSensitiveContent=" + isShowingSensitiveContent);
+                Log.d(TAG, "setSensitiveContentProtection - current package=" + packageInfo
+                        + ", isShowingSensitiveContent=" + isShowingSensitiveContent
+                        + ", sensitive packages=" + mPackagesShowingSensitiveContent);
             }
 
-            // The window token distinguish this package from packages added for notifications.
-            PackageInfo packageInfo = new PackageInfo(packageName, uid, windowToken);
             ArraySet<PackageInfo> packageInfos = new ArraySet<>();
             packageInfos.add(packageInfo);
             if (isShowingSensitiveContent) {
@@ -392,6 +408,12 @@
                 verifyCallingPackage(callingUid, packageName);
                 final long identity = Binder.clearCallingIdentity();
                 try {
+                    if (isShowingSensitiveContent
+                            && mWindowManager.getWindowName(windowToken) == null) {
+                        Log.e(TAG, "window token is not know to WMS, can't apply protection,"
+                                + " token: " + windowToken + ", package: " + packageName);
+                        return;
+                    }
                     SensitiveContentProtectionManagerService.this.setSensitiveContentProtection(
                             windowToken, packageName, callingUid, isShowingSensitiveContent);
                 } finally {
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 03ab5b3..4364f16 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -9973,7 +9973,7 @@
                         "getHistoricalProcessStartReasons");
             if (uid != INVALID_UID) {
                 mProcessList.getAppStartInfoTracker().getStartInfo(
-                        packageName, userId, callingPid, maxNum, results);
+                        packageName, uid, callingPid, maxNum, results);
             }
         } else {
             // If no package name is given, use the caller's uid as the filter uid.
diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index 4ebabdc..5a97e87 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -1164,8 +1164,7 @@
         synchronized (mInternal) {
             synchronized (mInternal.mProcLock) {
                 app.mOptRecord.setFreezeSticky(isSticky);
-                mInternal.mOomAdjuster.mCachedAppOptimizer.freezeAppAsyncInternalLSP(
-                        app, 0 /* delayMillis */, true /* force */, false /* immediate */);
+                mInternal.mOomAdjuster.mCachedAppOptimizer.forceFreezeAppAsyncLSP(app);
             }
         }
         return 0;
diff --git a/services/core/java/com/android/server/am/BroadcastConstants.java b/services/core/java/com/android/server/am/BroadcastConstants.java
index 91cfb8f..e676b1f 100644
--- a/services/core/java/com/android/server/am/BroadcastConstants.java
+++ b/services/core/java/com/android/server/am/BroadcastConstants.java
@@ -281,7 +281,7 @@
      * For {@link BroadcastQueueModernImpl}: Maximum number of outgoing broadcasts from a
      * freezable process that will be allowed before killing the process.
      */
-    public long MAX_FROZEN_OUTGOING_BROADCASTS = DEFAULT_MAX_FROZEN_OUTGOING_BROADCASTS;
+    public int MAX_FROZEN_OUTGOING_BROADCASTS = DEFAULT_MAX_FROZEN_OUTGOING_BROADCASTS;
     private static final String KEY_MAX_FROZEN_OUTGOING_BROADCASTS =
             "max_frozen_outgoing_broadcasts";
     private static final int DEFAULT_MAX_FROZEN_OUTGOING_BROADCASTS = 32;
diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
index e98e1ba..ed3cd1e 100644
--- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java
+++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
@@ -277,6 +277,10 @@
         mOutgoingBroadcasts.clear();
     }
 
+    public void clearOutgoingBroadcasts() {
+        mOutgoingBroadcasts.clear();
+    }
+
     /**
      * Enqueue the given broadcast to be dispatched to this process at some
      * future point in time. The target receiver is indicated by the given index
diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
index a6f6b34..c082889 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
@@ -166,7 +166,7 @@
     /**
      * Map from UID to per-process broadcast queues. If a UID hosts more than
      * one process, each additional process is stored as a linked list using
-     * {@link BroadcastProcessQueue#next}.
+     * {@link BroadcastProcessQueue#processNameNext}.
      *
      * @see #getProcessQueue
      * @see #getOrCreateProcessQueue
@@ -661,6 +661,10 @@
         final BroadcastProcessQueue queue = getProcessQueue(app);
         if (queue != null) {
             setQueueProcess(queue, app);
+            // Outgoing broadcasts should be cleared when the process dies but there have been
+            // issues due to AMS not always informing the BroadcastQueue of process deaths.
+            // So, clear them when a new process starts as well.
+            queue.clearOutgoingBroadcasts();
         }
 
         boolean didSomething = false;
@@ -730,6 +734,8 @@
                 demoteFromRunningLocked(queue);
             }
 
+            queue.clearOutgoingBroadcasts();
+
             // Skip any pending registered receivers, since the old process
             // would never be around to receive them
             boolean didSomething = queue.forEachMatchingBroadcast((r, i) -> {
@@ -781,8 +787,11 @@
             final BroadcastProcessQueue queue = getOrCreateProcessQueue(
                     r.callerApp.processName, r.callerApp.uid);
             if (queue.getOutgoingBroadcastCount() >= mConstants.MAX_FROZEN_OUTGOING_BROADCASTS) {
-                // TODO: Kill the process if the outgoing broadcasts count is
-                // beyond a certain limit.
+                r.callerApp.killLocked("Too many outgoing broadcasts in cached state",
+                        ApplicationExitInfo.REASON_OTHER,
+                        ApplicationExitInfo.SUBREASON_EXCESSIVE_OUTGOING_BROADCASTS_WHILE_CACHED,
+                        true /* noisy */);
+                return;
             }
             queue.enqueueOutgoingBroadcast(r);
             mHistory.onBroadcastFrozenLocked(r);
diff --git a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java
index 66abb42..b8ef03f 100644
--- a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java
+++ b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java
@@ -19,6 +19,7 @@
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_PERMISSIONS_REVIEW;
 import static com.android.server.am.ActivityManagerService.checkComponentPermission;
 import static com.android.server.am.BroadcastQueue.TAG;
+import static com.android.server.am.Flags.usePermissionManagerForBroadcastDeliveryCheck;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -27,6 +28,7 @@
 import android.app.AppOpsManager;
 import android.app.BroadcastOptions;
 import android.app.PendingIntent;
+import android.content.AttributionSource;
 import android.content.ComponentName;
 import android.content.IIntentSender;
 import android.content.Intent;
@@ -39,6 +41,7 @@
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.permission.IPermissionManager;
+import android.permission.PermissionManager;
 import android.util.Slog;
 
 import com.android.internal.util.ArrayUtils;
@@ -54,6 +57,9 @@
 public class BroadcastSkipPolicy {
     private final ActivityManagerService mService;
 
+    @Nullable
+    private PermissionManager mPermissionManager;
+
     public BroadcastSkipPolicy(@NonNull ActivityManagerService service) {
         mService = Objects.requireNonNull(service);
     }
@@ -283,14 +289,35 @@
 
         if (info.activityInfo.applicationInfo.uid != Process.SYSTEM_UID &&
                 r.requiredPermissions != null && r.requiredPermissions.length > 0) {
+            final AttributionSource attributionSource;
+            if (usePermissionManagerForBroadcastDeliveryCheck()) {
+                attributionSource =
+                        new AttributionSource.Builder(info.activityInfo.applicationInfo.uid)
+                                .setPackageName(info.activityInfo.packageName)
+                                .build();
+            } else {
+                attributionSource = null;
+            }
             for (int i = 0; i < r.requiredPermissions.length; i++) {
                 String requiredPermission = r.requiredPermissions[i];
                 try {
-                    perm = AppGlobals.getPackageManager().
-                            checkPermission(requiredPermission,
-                                    info.activityInfo.applicationInfo.packageName,
-                                    UserHandle
-                                    .getUserId(info.activityInfo.applicationInfo.uid));
+                    if (usePermissionManagerForBroadcastDeliveryCheck()) {
+                        final PermissionManager permissionManager = getPermissionManager();
+                        if (permissionManager != null) {
+                            perm = permissionManager.checkPermissionForDataDelivery(
+                                    requiredPermission, attributionSource, null /* message */);
+                        } else {
+                            // Assume permission denial if PermissionManager is not yet available.
+                            perm = PackageManager.PERMISSION_DENIED;
+                        }
+                    } else {
+                        perm = AppGlobals.getPackageManager()
+                                .checkPermission(
+                                        requiredPermission,
+                                        info.activityInfo.applicationInfo.packageName,
+                                        UserHandle
+                                                .getUserId(info.activityInfo.applicationInfo.uid));
+                    }
                 } catch (RemoteException e) {
                     perm = PackageManager.PERMISSION_DENIED;
                 }
@@ -302,11 +329,13 @@
                             + " due to sender " + r.callerPackage
                             + " (uid " + r.callingUid + ")";
                 }
-                int appOp = AppOpsManager.permissionToOpCode(requiredPermission);
-                if (appOp != AppOpsManager.OP_NONE && appOp != r.appOp) {
-                    if (!noteOpForManifestReceiver(appOp, r, info, component)) {
-                        return "Skipping delivery to " + info.activityInfo.packageName
-                                + " due to required appop " + appOp;
+                if (!usePermissionManagerForBroadcastDeliveryCheck()) {
+                    int appOp = AppOpsManager.permissionToOpCode(requiredPermission);
+                    if (appOp != AppOpsManager.OP_NONE && appOp != r.appOp) {
+                        if (!noteOpForManifestReceiver(appOp, r, info, component)) {
+                            return "Skipping delivery to " + info.activityInfo.packageName
+                                    + " due to required appop " + appOp;
+                        }
                     }
                 }
             }
@@ -694,4 +723,11 @@
 
         return false;
     }
+
+    private PermissionManager getPermissionManager() {
+        if (mPermissionManager == null) {
+            mPermissionManager = mService.mContext.getSystemService(PermissionManager.class);
+        }
+        return mPermissionManager;
+    }
 }
diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java
index 0cf5575..6e20f6c 100644
--- a/services/core/java/com/android/server/am/CachedAppOptimizer.java
+++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java
@@ -1414,8 +1414,13 @@
     }
 
     @GuardedBy({"mAm", "mProcLock"})
+    void forceFreezeAppAsyncLSP(ProcessRecord app) {
+        freezeAppAsyncInternalLSP(app, 0 /* delayMillis */, true /* force */);
+    }
+
+    @GuardedBy({"mAm", "mProcLock"})
     private void freezeAppAsyncLSP(ProcessRecord app, @UptimeMillisLong long delayMillis) {
-        freezeAppAsyncInternalLSP(app, delayMillis, false, false);
+        freezeAppAsyncInternalLSP(app, delayMillis, false /* force */);
     }
 
     @GuardedBy({"mAm", "mProcLock"})
@@ -1427,17 +1432,18 @@
     // and remove this method.
     @GuardedBy({"mAm", "mProcLock"})
     void freezeAppAsyncImmediateLSP(ProcessRecord app) {
-        freezeAppAsyncInternalLSP(app, 0, false, true);
+        freezeAppAsyncInternalLSP(app, 0 /* delayMillis */, false /* force */);
     }
 
-    // TODO: Update this method to be private and have the existing clients call different methods.
-    // This "internal" method should not be directly triggered by clients outside this class.
     @GuardedBy({"mAm", "mProcLock"})
-    void freezeAppAsyncInternalLSP(ProcessRecord app, @UptimeMillisLong long delayMillis,
-            boolean force, boolean immediate) {
+    private void freezeAppAsyncInternalLSP(ProcessRecord app, @UptimeMillisLong long delayMillis,
+            boolean force) {
         final ProcessCachedOptimizerRecord opt = app.mOptRecord;
         if (opt.isPendingFreeze()) {
-            if (immediate) {
+            if (delayMillis == 0) {
+                // Caller is requesting to freeze the process without delay, so remove
+                // any already posted messages which would have been handled with a delay and
+                // post a new message without a delay.
                 mFreezeHandler.removeMessages(SET_FROZEN_PROCESS_MSG, app);
                 mFreezeHandler.sendMessage(mFreezeHandler.obtainMessage(
                         SET_FROZEN_PROCESS_MSG, DO_FREEZE, 0, app));
diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig
index 0209944..fd847f1 100644
--- a/services/core/java/com/android/server/am/flags.aconfig
+++ b/services/core/java/com/android/server/am/flags.aconfig
@@ -86,3 +86,11 @@
         purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    namespace: "backstage_power"
+    name: "use_permission_manager_for_broadcast_delivery_check"
+    description: "Use PermissionManager API for broadcast delivery permission checks."
+    bug: "315468967"
+    is_fixed_read_only: true
+}
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index e8c05c6..53c0f58 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -73,6 +73,7 @@
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHeadset;
 import android.bluetooth.BluetoothProfile;
+import android.content.AttributionSource;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.ContentResolver;
@@ -6853,15 +6854,6 @@
                         ringerMode = RINGER_MODE_SILENT;
                     }
                 }
-            } else if (mIsSingleVolume && (direction == AudioManager.ADJUST_TOGGLE_MUTE
-                    || direction == AudioManager.ADJUST_MUTE)) {
-                if (mHasVibrator) {
-                    ringerMode = RINGER_MODE_VIBRATE;
-                } else {
-                    ringerMode = RINGER_MODE_SILENT;
-                }
-                // Setting the ringer mode will toggle mute
-                result &= ~FLAG_ADJUST_VOLUME;
             }
             break;
         case RINGER_MODE_VIBRATE:
@@ -6870,11 +6862,8 @@
                         "but no vibrator is present");
                 break;
             }
-            if ((direction == AudioManager.ADJUST_LOWER)) {
-                // This is the case we were muted with the volume turned up
-                if (mIsSingleVolume && oldIndex >= 2 * step && isMuted) {
-                    ringerMode = RINGER_MODE_NORMAL;
-                } else if (mPrevVolDirection != AudioManager.ADJUST_LOWER) {
+            if (direction == AudioManager.ADJUST_LOWER) {
+                if (mPrevVolDirection != AudioManager.ADJUST_LOWER) {
                     if (mVolumePolicy.volumeDownToEnterSilent) {
                         final long diff = SystemClock.uptimeMillis()
                                 - mLoweredFromNormalToVibrateTime;
@@ -6894,10 +6883,7 @@
             result &= ~FLAG_ADJUST_VOLUME;
             break;
         case RINGER_MODE_SILENT:
-            if (mIsSingleVolume && direction == AudioManager.ADJUST_LOWER && oldIndex >= 2 * step && isMuted) {
-                // This is the case we were muted with the volume turned up
-                ringerMode = RINGER_MODE_NORMAL;
-            } else if (direction == AudioManager.ADJUST_RAISE
+            if (direction == AudioManager.ADJUST_RAISE
                     || direction == AudioManager.ADJUST_TOGGLE_MUTE
                     || direction == AudioManager.ADJUST_UNMUTE) {
                 if (!mVolumePolicy.volumeUpToExitSilent) {
@@ -12207,7 +12193,9 @@
     //==========================================================================================
     public String registerAudioPolicy(AudioPolicyConfig policyConfig, IAudioPolicyCallback pcb,
             boolean hasFocusListener, boolean isFocusPolicy, boolean isTestFocusPolicy,
-            boolean isVolumeController, IMediaProjection projection) {
+            boolean isVolumeController, IMediaProjection projection,
+            AttributionSource attributionSource) {
+        Objects.requireNonNull(attributionSource);
         AudioSystem.setDynamicPolicyCallback(mDynPolicyCallback);
 
         if (!isPolicyRegisterAllowed(policyConfig,
@@ -12228,7 +12216,8 @@
             }
             try {
                 AudioPolicyProxy app = new AudioPolicyProxy(policyConfig, pcb, hasFocusListener,
-                        isFocusPolicy, isTestFocusPolicy, isVolumeController, projection);
+                        isFocusPolicy, isTestFocusPolicy, isVolumeController, projection,
+                        attributionSource);
                 pcb.asBinder().linkToDeath(app, 0/*flags*/);
 
                 // logging after registration so we have the registration id
@@ -13200,6 +13189,7 @@
     public class AudioPolicyProxy extends AudioPolicyConfig implements IBinder.DeathRecipient {
         private static final String TAG = "AudioPolicyProxy";
         final IAudioPolicyCallback mPolicyCallback;
+        final AttributionSource mAttributionSource;
         final boolean mHasFocusListener;
         final boolean mIsVolumeController;
         final HashMap<Integer, AudioDeviceArray> mUidDeviceAffinities =
@@ -13239,10 +13229,12 @@
 
         AudioPolicyProxy(AudioPolicyConfig config, IAudioPolicyCallback token,
                 boolean hasFocusListener, boolean isFocusPolicy, boolean isTestFocusPolicy,
-                boolean isVolumeController, IMediaProjection projection) {
+                boolean isVolumeController, IMediaProjection projection,
+                AttributionSource attributionSource) {
             super(config);
             setRegistration(new String(config.hashCode() + ":ap:" + mAudioPolicyCounter++));
             mPolicyCallback = token;
+            mAttributionSource = attributionSource;
             mHasFocusListener = hasFocusListener;
             mIsVolumeController = isVolumeController;
             mProjection = projection;
@@ -13370,6 +13362,7 @@
                 if (android.media.audiopolicy.Flags.audioMixOwnership()) {
                     for (AudioMix mix : mixes) {
                         setMixRegistration(mix);
+                        mix.setVirtualDeviceId(mAttributionSource.getDeviceId());
                     }
 
                     int result = mAudioSystem.registerPolicyMixes(mixes, true);
@@ -13393,6 +13386,9 @@
         @AudioSystem.AudioSystemError int connectMixes() {
             final long identity = Binder.clearCallingIdentity();
             try {
+                for (AudioMix mix : mMixes) {
+                    mix.setVirtualDeviceId(mAttributionSource.getDeviceId());
+                }
                 return mAudioSystem.registerPolicyMixes(mMixes, true);
             } finally {
                 Binder.restoreCallingIdentity(identity);
@@ -13406,6 +13402,9 @@
             Objects.requireNonNull(mixesToUpdate);
             Objects.requireNonNull(updatedMixingRules);
 
+            for (AudioMix mix : mixesToUpdate) {
+                mix.setVirtualDeviceId(mAttributionSource.getDeviceId());
+            }
             if (mixesToUpdate.length != updatedMixingRules.length) {
                 Log.e(TAG, "Provided list of audio mixes to update and corresponding mixing rules "
                         + "have mismatching length (mixesToUpdate.length = " + mixesToUpdate.length
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index b7ece2ea..5905b7d 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -366,7 +366,6 @@
 
     private PendingIntent mStatusIntent;
     private volatile boolean mEnableTeardown = true;
-    private final INetworkManagementService mNms;
     private final INetd mNetd;
     @VisibleForTesting
     @GuardedBy("this")
@@ -626,7 +625,6 @@
         mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class);
 
         mDeps = deps;
-        mNms = netService;
         mNetd = netd;
         mUserId = userId;
         mLooper = looper;
diff --git a/services/core/java/com/android/server/display/AutomaticBrightnessController.java b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
index 4032514..4aab9d2 100644
--- a/services/core/java/com/android/server/display/AutomaticBrightnessController.java
+++ b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
@@ -54,6 +54,7 @@
 import com.android.internal.os.BackgroundThread;
 import com.android.server.EventLogTags;
 import com.android.server.display.brightness.BrightnessEvent;
+import com.android.server.display.brightness.clamper.BrightnessClamperController;
 
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
@@ -252,6 +253,7 @@
 
     // Controls Brightness range (including High Brightness Mode).
     private final BrightnessRangeController mBrightnessRangeController;
+    private final BrightnessClamperController mBrightnessClamperController;
 
     // Throttles (caps) maximum allowed brightness
     private final BrightnessThrottler mBrightnessThrottler;
@@ -287,7 +289,8 @@
             HysteresisLevels screenBrightnessThresholdsIdle, Context context,
             BrightnessRangeController brightnessModeController,
             BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
-            int ambientLightHorizonLong, float userLux, float userNits) {
+            int ambientLightHorizonLong, float userLux, float userNits,
+            BrightnessClamperController brightnessClamperController) {
         this(new Injector(), callbacks, looper, sensorManager, lightSensor,
                 brightnessMappingStrategyMap, lightSensorWarmUpTime, brightnessMin, brightnessMax,
                 dozeScaleFactor, lightSensorRate, initialLightSensorRate,
@@ -297,7 +300,7 @@
                 screenBrightnessThresholds, ambientBrightnessThresholdsIdle,
                 screenBrightnessThresholdsIdle, context, brightnessModeController,
                 brightnessThrottler, ambientLightHorizonShort, ambientLightHorizonLong, userLux,
-                userNits
+                userNits, brightnessClamperController
         );
     }
 
@@ -313,9 +316,10 @@
             HysteresisLevels screenBrightnessThresholds,
             HysteresisLevels ambientBrightnessThresholdsIdle,
             HysteresisLevels screenBrightnessThresholdsIdle, Context context,
-            BrightnessRangeController brightnessModeController,
+            BrightnessRangeController brightnessRangeController,
             BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
-            int ambientLightHorizonLong, float userLux, float userNits) {
+            int ambientLightHorizonLong, float userLux, float userNits,
+            BrightnessClamperController brightnessClamperController) {
         mInjector = injector;
         mClock = injector.createClock();
         mContext = context;
@@ -358,7 +362,8 @@
         mPendingForegroundAppPackageName = null;
         mForegroundAppCategory = ApplicationInfo.CATEGORY_UNDEFINED;
         mPendingForegroundAppCategory = ApplicationInfo.CATEGORY_UNDEFINED;
-        mBrightnessRangeController = brightnessModeController;
+        mBrightnessRangeController = brightnessRangeController;
+        mBrightnessClamperController = brightnessClamperController;
         mBrightnessThrottler = brightnessThrottler;
         mBrightnessMappingStrategyMap = brightnessMappingStrategyMap;
 
@@ -791,7 +796,7 @@
                     mAmbientBrightnessThresholds.getDarkeningThreshold(lux);
         }
         mBrightnessRangeController.onAmbientLuxChange(mAmbientLux);
-
+        mBrightnessClamperController.onAmbientLuxChange(mAmbientLux);
 
         // If the short term model was invalidated and the change is drastic enough, reset it.
         mShortTermModel.maybeReset(mAmbientLux);
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index 4116669..04e7f77 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -61,6 +61,7 @@
 import com.android.server.display.config.IdleScreenRefreshRateTimeoutLuxThresholdPoint;
 import com.android.server.display.config.IdleScreenRefreshRateTimeoutLuxThresholds;
 import com.android.server.display.config.IntegerArray;
+import com.android.server.display.config.LowBrightnessData;
 import com.android.server.display.config.LuxThrottling;
 import com.android.server.display.config.NitsMap;
 import com.android.server.display.config.NonNegativeFloatToFloatPoint;
@@ -555,6 +556,24 @@
  *         <majorVersion>2</majorVersion>
  *         <minorVersion>0</minorVersion>
  *     </usiVersion>
+ *     <lowBrightness enabled="true">
+ *       <transitionPoint>0.1</transitionPoint>
+ *
+ *       <nits>0.2</nits>
+ *       <nits>2.0</nits>
+ *       <nits>500.0</nits>
+ *       <nits>1000.0</nits>
+ *
+ *       <backlight>0</backlight>
+ *       <backlight>0.0001</backlight>
+ *       <backlight>0.5</backlight>
+ *       <backlight>1.0</backlight>
+ *
+ *       <brightness>0</brightness>
+ *       <brightness>0.1</brightness>
+ *       <brightness>0.5</brightness>
+ *       <brightness>1.0</brightness>
+ *     </lowBrightness>
  *     <screenBrightnessCapForWearBedtimeMode>0.1</screenBrightnessCapForWearBedtimeMode>
  *     <idleScreenRefreshRateTimeout>
  *          <luxThresholds>
@@ -568,6 +587,8 @@
  *              </point>
  *          </luxThresholds>
  *     </idleScreenRefreshRateTimeout>
+ *
+ *
  *    </displayConfiguration>
  *  }
  *  </pre>
@@ -732,6 +753,7 @@
     private Spline mBacklightToBrightnessSpline;
     private Spline mBacklightToNitsSpline;
     private Spline mNitsToBacklightSpline;
+
     private List<String> mQuirks;
     private boolean mIsHighBrightnessModeEnabled = false;
     private HighBrightnessModeData mHbmData;
@@ -872,6 +894,9 @@
     @Nullable
     private HdrBrightnessData mHdrBrightnessData;
 
+    @Nullable
+    public LowBrightnessData mLowBrightnessData;
+
     /**
      * Maximum screen brightness setting when screen brightness capped in Wear Bedtime mode.
      */
@@ -1814,6 +1839,15 @@
     }
 
     /**
+     *
+     * @return true if low brightness mode is enabled
+     */
+    @VisibleForTesting
+    public boolean getLbmEnabled() {
+        return mLowBrightnessData != null;
+    }
+
+    /**
      * @return Maximum screen brightness setting when screen brightness capped in Wear Bedtime mode.
      */
     public float getBrightnessCapForWearBedtimeMode() {
@@ -1952,6 +1986,8 @@
                 + "mUsiVersion= " + mHostUsiVersion + "\n"
                 + "mHdrBrightnessData= " + mHdrBrightnessData + "\n"
                 + "mBrightnessCapForWearBedtimeMode= " + mBrightnessCapForWearBedtimeMode
+                + "\n"
+                + (mLowBrightnessData != null ? mLowBrightnessData.toString() : "")
                 + "}";
     }
 
@@ -2002,6 +2038,9 @@
                 loadDensityMapping(config);
                 loadBrightnessDefaultFromDdcXml(config);
                 loadBrightnessConstraintsFromConfigXml();
+                if (mFlags.isEvenDimmerEnabled()) {
+                    mLowBrightnessData = LowBrightnessData.loadConfig(config);
+                }
                 loadBrightnessMap(config);
                 loadThermalThrottlingConfig(config);
                 loadPowerThrottlingConfigData(config);
@@ -2793,6 +2832,18 @@
     // These splines are used to convert from the system brightness value to the HAL backlight
     // value
     private void createBacklightConversionSplines() {
+        if (mLowBrightnessData != null) {
+            mBrightnessToBacklightSpline = mLowBrightnessData.mBrightnessToBacklight;
+            mBacklightToBrightnessSpline = mLowBrightnessData.mBacklightToBrightness;
+            mBacklightToNitsSpline = mLowBrightnessData.mBacklightToNits;
+            mNitsToBacklightSpline = mLowBrightnessData.mNitsToBacklight;
+
+            mNits = mLowBrightnessData.mNits;
+            mBrightness = mLowBrightnessData.mBrightness;
+            mBacklight = mLowBrightnessData.mBacklight;
+            return;
+        }
+
         mBrightness = new float[mBacklight.length];
         for (int i = 0; i < mBrightness.length; i++) {
             mBrightness[i] = MathUtils.map(mBacklight[0],
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 87d017c..90ad8c0 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -1165,7 +1165,8 @@
                     screenBrightnessThresholds, ambientBrightnessThresholdsIdle,
                     screenBrightnessThresholdsIdle, mContext, mBrightnessRangeController,
                     mBrightnessThrottler, mDisplayDeviceConfig.getAmbientHorizonShort(),
-                    mDisplayDeviceConfig.getAmbientHorizonLong(), userLux, userNits);
+                    mDisplayDeviceConfig.getAmbientHorizonLong(), userLux, userNits,
+                    mBrightnessClamperController);
             mDisplayBrightnessController.setAutomaticBrightnessController(
                     mAutomaticBrightnessController);
 
@@ -2479,6 +2480,7 @@
     public void setBrightnessToFollow(float leadDisplayBrightness, float nits, float ambientLux,
             boolean slowChange) {
         mBrightnessRangeController.onAmbientLuxChange(ambientLux);
+        mBrightnessClamperController.onAmbientLuxChange(ambientLux);
         if (nits == BrightnessMappingStrategy.INVALID_NITS) {
             mDisplayBrightnessController.setBrightnessToFollow(leadDisplayBrightness, slowChange);
         } else {
@@ -3176,7 +3178,9 @@
                 HysteresisLevels screenBrightnessThresholdsIdle, Context context,
                 BrightnessRangeController brightnessModeController,
                 BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
-                int ambientLightHorizonLong, float userLux, float userNits) {
+                int ambientLightHorizonLong, float userLux, float userNits,
+                BrightnessClamperController brightnessClamperController) {
+
             return new AutomaticBrightnessController(callbacks, looper, sensorManager, lightSensor,
                     brightnessMappingStrategyMap, lightSensorWarmUpTime, brightnessMin,
                     brightnessMax, dozeScaleFactor, lightSensorRate, initialLightSensorRate,
@@ -3186,7 +3190,7 @@
                     screenBrightnessThresholds, ambientBrightnessThresholdsIdle,
                     screenBrightnessThresholdsIdle, context, brightnessModeController,
                     brightnessThrottler, ambientLightHorizonShort, ambientLightHorizonLong, userLux,
-                    userNits);
+                    userNits, brightnessClamperController);
         }
 
         BrightnessMappingStrategy getDefaultModeBrightnessMapper(Context context,
diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
index b2fd9ed..3b3a03b 100644
--- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
@@ -37,6 +37,7 @@
 import android.os.Trace;
 import android.util.DisplayUtils;
 import android.util.LongSparseArray;
+import android.util.MathUtils;
 import android.util.Slog;
 import android.util.SparseArray;
 import android.view.Display;
@@ -78,6 +79,13 @@
     private static final String UNIQUE_ID_PREFIX = "local:";
 
     private static final String PROPERTY_EMULATOR_CIRCULAR = "ro.boot.emulator.circular";
+    // Min and max strengths for even dimmer feature.
+    private static final float EVEN_DIMMER_MIN_STRENGTH = 0.0f;
+    private static final float EVEN_DIMMER_MAX_STRENGTH = 70.0f; // not too dim yet.
+    private static final float BRIGHTNESS_MIN = 0.0f;
+    // The brightness at which we start using color matrices rather than backlight,
+    // to dim the display
+    private static final float BACKLIGHT_COLOR_TRANSITION_POINT = 0.1f;
 
     private final LongSparseArray<LocalDisplayDevice> mDevices = new LongSparseArray<>();
 
@@ -91,6 +99,8 @@
 
     private Context mOverlayContext;
 
+    private int mEvenDimmerStrength = -1;
+
     // Called with SyncRoot lock held.
     LocalDisplayAdapter(DisplayManagerService.SyncRoot syncRoot, Context context,
             Handler handler, Listener listener, DisplayManagerFlags flags,
@@ -928,6 +938,10 @@
                             final float nits = backlightToNits(backlight);
                             final float sdrNits = backlightToNits(sdrBacklight);
 
+                            if (getFeatureFlags().isEvenDimmerEnabled()) {
+                                applyColorMatrixBasedDimming(brightnessState);
+                            }
+
                             mBacklightAdapter.setBacklight(sdrBacklight, sdrNits, backlight, nits);
                             Trace.traceCounter(Trace.TRACE_TAG_POWER,
                                     "ScreenBrightness",
@@ -974,6 +988,22 @@
                             }
                         }
                     }
+
+                    private void applyColorMatrixBasedDimming(float brightnessState) {
+                        int strength = (int) (MathUtils.constrainedMap(
+                                EVEN_DIMMER_MAX_STRENGTH, EVEN_DIMMER_MIN_STRENGTH, // to this range
+                                BRIGHTNESS_MIN, BACKLIGHT_COLOR_TRANSITION_POINT, // from this range
+                                brightnessState) + 0.5); // map this (+ rounded up)
+
+                        if (mEvenDimmerStrength < 0 // uninitialised
+                                || MathUtils.abs(mEvenDimmerStrength - strength) > 1
+                                || strength <= 1) {
+                            mEvenDimmerStrength = strength;
+                        }
+
+                        // TODO: use `enabled` and `mRbcStrength` to set color matrices here
+                        // TODO: boolean enabled = mEvenDimmerStrength > 0.0f;
+                    }
                 };
             }
             return null;
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
index 18e8fab..d8a4500 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
@@ -189,6 +189,13 @@
         mModifiers.forEach(BrightnessStateModifier::stop);
     }
 
+    /**
+     * Notifies modifiers that ambient lux has changed.
+     * @param ambientLux current lux, debounced
+     */
+    public void onAmbientLuxChange(float ambientLux) {
+        mModifiers.forEach(modifier -> modifier.onAmbientLuxChange(ambientLux));
+    }
 
     // Called in DisplayControllerHandler
     private void recalculateBrightnessCap() {
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java
index 7f1f7a9..a91bb59 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java
@@ -39,21 +39,21 @@
  * Class used to prevent the screen brightness dipping below a certain value, based on current
  * lux conditions and user preferred minimum.
  */
-public class BrightnessLowLuxModifier implements
-        BrightnessStateModifier {
+public class BrightnessLowLuxModifier extends BrightnessModifier {
 
     // To enable these logs, run:
     // 'adb shell setprop persist.log.tag.BrightnessLowLuxModifier DEBUG && adb reboot'
     private static final String TAG = "BrightnessLowLuxModifier";
     private static final boolean DEBUG = DebugUtils.isDebuggable(TAG);
+    private static final float MIN_NITS = 2.0f;
     private final SettingsObserver mSettingsObserver;
     private final ContentResolver mContentResolver;
     private final Handler mHandler;
     private final BrightnessClamperController.ClamperChangeListener mChangeListener;
-    protected float mSettingNitsLowerBound = PowerManager.BRIGHTNESS_MIN;
     private int mReason;
     private float mBrightnessLowerBound;
     private boolean mIsActive;
+    private float mAmbientLux;
 
     @VisibleForTesting
     BrightnessLowLuxModifier(Handler handler,
@@ -78,17 +78,17 @@
         int userId = UserHandle.USER_CURRENT;
         float settingNitsLowerBound = Settings.Secure.getFloatForUser(
                 mContentResolver, Settings.Secure.EVEN_DIMMER_MIN_NITS,
-                /* def= */ PowerManager.BRIGHTNESS_MIN, userId);
+                /* def= */ MIN_NITS, userId);
 
-        boolean isActive = Settings.Secure.getIntForUser(mContentResolver,
+        boolean isActive = Settings.Secure.getFloatForUser(mContentResolver,
                 Settings.Secure.EVEN_DIMMER_ACTIVATED,
-                /* def= */ 0, userId) == 1;
+                /* def= */ 0, userId) == 1.0f;
 
-        // TODO: luxBasedNitsLowerBound = mMinNitsToLuxSpline(currentLux);
-        float luxBasedNitsLowerBound = 0.0f;
+        // TODO: luxBasedNitsLowerBound = mMinLuxToNitsSpline(currentLux);
+        float luxBasedNitsLowerBound = 2.0f;
 
-        // TODO: final float nitsLowerBound = isActive ? Math.max(settingNitsLowerBound,
-                // luxBasedNitsLowerBound) : PowerManager.BRIGHTNESS_MIN;
+        final float nitsLowerBound = isActive ? Math.max(settingNitsLowerBound,
+                 luxBasedNitsLowerBound) : MIN_NITS;
 
         final int reason = settingNitsLowerBound > luxBasedNitsLowerBound
                 ? BrightnessReason.MODIFIER_MIN_USER_SET_LOWER_BOUND
@@ -104,8 +104,13 @@
             mReason = reason;
             if (DEBUG) {
                 Slog.i(TAG, "isActive: " + isActive
-                        + ", settingNitsLowerBound: " + settingNitsLowerBound
-                        + ", lowerBound: " + brightnessLowerBound);
+                        + ", brightnessLowerBound: " + brightnessLowerBound
+                        + ", mAmbientLux: " + mAmbientLux
+                        + ", mReason: " + (
+                        mReason == BrightnessReason.MODIFIER_MIN_USER_SET_LOWER_BOUND ? "minSetting"
+                                : "lux")
+                        + ", nitsLowerBound: " + nitsLowerBound
+                );
             }
             mBrightnessLowerBound = brightnessLowerBound;
             mChangeListener.onChanged();
@@ -132,6 +137,22 @@
     }
 
     @Override
+    boolean shouldApply(DisplayManagerInternal.DisplayPowerRequest request) {
+        return mIsActive;
+    }
+
+    @Override
+    float getBrightnessAdjusted(float currentBrightness,
+            DisplayManagerInternal.DisplayPowerRequest request) {
+        return Math.max(mBrightnessLowerBound, currentBrightness);
+    }
+
+    @Override
+    int getModifier() {
+        return mReason;
+    }
+
+    @Override
     public void apply(DisplayManagerInternal.DisplayPowerRequest request,
             DisplayBrightnessState.Builder stateBuilder) {
         stateBuilder.setMinBrightness(mBrightnessLowerBound);
@@ -150,10 +171,16 @@
     }
 
     @Override
+    public void onAmbientLuxChange(float ambientLux) {
+        mAmbientLux = ambientLux;
+        recalculateLowerBound();
+    }
+
+    @Override
     public void dump(PrintWriter pw) {
         pw.println("BrightnessLowLuxModifier:");
-        pw.println("  mBrightnessLowerBound=" + mBrightnessLowerBound);
         pw.println("  mIsActive=" + mIsActive);
+        pw.println("  mBrightnessLowerBound=" + mBrightnessLowerBound);
         pw.println("  mReason=" + mReason);
     }
 
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessModifier.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessModifier.java
index be8fa5a..2a3dd87 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessModifier.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessModifier.java
@@ -68,4 +68,9 @@
     public void stop() {
         // do nothing
     }
+
+    @Override
+    public void onAmbientLuxChange(float ambientLux) {
+        // do nothing
+    }
 }
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessStateModifier.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessStateModifier.java
index 441ba8f..2234258 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessStateModifier.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessStateModifier.java
@@ -42,4 +42,10 @@
      * Called when stopped. Listeners can be unregistered here.
      */
     void stop();
+
+    /**
+     * Allows modifiers to react to ambient lux changes.
+     * @param ambientLux current debounced lux.
+     */
+    void onAmbientLuxChange(float ambientLux);
 }
diff --git a/services/core/java/com/android/server/display/config/LowBrightnessData.java b/services/core/java/com/android/server/display/config/LowBrightnessData.java
new file mode 100644
index 0000000..aa82533
--- /dev/null
+++ b/services/core/java/com/android/server/display/config/LowBrightnessData.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display.config;
+
+import android.annotation.Nullable;
+import android.util.Slog;
+import android.util.Spline;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Brightness config for low brightness mode
+ */
+public class LowBrightnessData {
+    private static final String TAG = "LowBrightnessData";
+
+    /**
+     * Brightness value at which lower brightness methods are used.
+     */
+    public final float mTransitionPoint;
+
+    /**
+     * Nits array, maps to mBacklight
+     */
+    public final float[] mNits;
+
+    /**
+     * Backlight array, maps to mBrightness and mNits
+     */
+    public final float[] mBacklight;
+
+    /**
+     * Brightness array, maps to mBacklight
+     */
+    public final float[] mBrightness;
+    /**
+     * Spline, mapping between backlight and nits
+     */
+    public final Spline mBacklightToNits;
+    /**
+     * Spline, mapping between nits and backlight
+     */
+    public final Spline mNitsToBacklight;
+    /**
+     * Spline, mapping between brightness and backlight
+     */
+    public final Spline mBrightnessToBacklight;
+    /**
+     * Spline, mapping between backlight and brightness
+     */
+    public final Spline mBacklightToBrightness;
+
+    @VisibleForTesting
+    public LowBrightnessData(float transitionPoint, float[] nits,
+            float[] backlight, float[] brightness, Spline backlightToNits,
+            Spline nitsToBacklight, Spline brightnessToBacklight, Spline backlightToBrightness) {
+        mTransitionPoint = transitionPoint;
+        mNits = nits;
+        mBacklight = backlight;
+        mBrightness = brightness;
+        mBacklightToNits = backlightToNits;
+        mNitsToBacklight = nitsToBacklight;
+        mBrightnessToBacklight = brightnessToBacklight;
+        mBacklightToBrightness = backlightToBrightness;
+    }
+
+    @Override
+    public String toString() {
+        return "LowBrightnessData {"
+                + "mTransitionPoint: " + mTransitionPoint
+                + ", mNits: " + Arrays.toString(mNits)
+                + ", mBacklight: " + Arrays.toString(mBacklight)
+                + ", mBrightness: " + Arrays.toString(mBrightness)
+                + ", mBacklightToNits: " + mBacklightToNits
+                + ", mNitsToBacklight: " + mNitsToBacklight
+                + ", mBrightnessToBacklight: " + mBrightnessToBacklight
+                + ", mBacklightToBrightness: " + mBacklightToBrightness
+                + "} ";
+    }
+
+    /**
+     * Loads LowBrightnessData from DisplayConfiguration
+     */
+    @Nullable
+    public static LowBrightnessData loadConfig(DisplayConfiguration config) {
+        final LowBrightnessMode lbm = config.getLowBrightness();
+        if (lbm == null) {
+            return null;
+        }
+
+        boolean lbmIsEnabled = lbm.getEnabled();
+        if (!lbmIsEnabled) {
+            return null;
+        }
+
+        List<Float> nitsList = lbm.getNits();
+        List<Float> backlightList = lbm.getBacklight();
+        List<Float> brightnessList = lbm.getBrightness();
+        float transitionPoints = lbm.getTransitionPoint().floatValue();
+
+        if (nitsList.isEmpty()
+                || backlightList.size() != brightnessList.size()
+                || backlightList.size() != nitsList.size()) {
+            Slog.e(TAG, "Invalid low brightness array lengths");
+            return null;
+        }
+
+        float[] nits = new float[nitsList.size()];
+        float[] backlight = new float[nitsList.size()];
+        float[] brightness = new float[nitsList.size()];
+
+        for (int i = 0; i < nitsList.size(); i++) {
+            nits[i] = nitsList.get(i);
+            backlight[i] = backlightList.get(i);
+            brightness[i] = brightnessList.get(i);
+        }
+
+        return new LowBrightnessData(transitionPoints, nits, backlight, brightness,
+                Spline.createSpline(backlight, nits),
+                Spline.createSpline(nits, backlight),
+                Spline.createSpline(brightness, backlight),
+                Spline.createSpline(backlight, brightness)
+                );
+    }
+}
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 05b1cb69..468b902 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -2604,6 +2604,19 @@
         mBatteryController.notifyStylusGestureStarted(deviceId, eventTime);
     }
 
+    // Native callback.
+    @SuppressWarnings("unused")
+    private int getPackageUid(String pkg) {
+        if (TextUtils.isEmpty(pkg)) {
+            return Process.INVALID_UID;
+        }
+        try {
+            return mContext.getPackageManager().getPackageUid(pkg, 0 /*flags*/);
+        } catch (PackageManager.NameNotFoundException e) {
+            return Process.INVALID_UID;
+        }
+    }
+
     /**
      * Flatten a map into a string list, with value positioned directly next to the
      * key.
diff --git a/services/core/java/com/android/server/input/KeyboardLayoutManager.java b/services/core/java/com/android/server/input/KeyboardLayoutManager.java
index 283e692..6610081 100644
--- a/services/core/java/com/android/server/input/KeyboardLayoutManager.java
+++ b/services/core/java/com/android/server/input/KeyboardLayoutManager.java
@@ -459,13 +459,16 @@
         for (ResolveInfo resolveInfo : pm.queryBroadcastReceiversAsUser(intent,
                 PackageManager.GET_META_DATA | PackageManager.MATCH_DIRECT_BOOT_AWARE
                         | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, UserHandle.USER_SYSTEM)) {
+            if (resolveInfo == null || resolveInfo.activityInfo == null) {
+                continue;
+            }
             final ActivityInfo activityInfo = resolveInfo.activityInfo;
             final int priority = resolveInfo.priority;
             visitKeyboardLayoutsInPackage(pm, activityInfo, null, priority, visitor);
         }
     }
 
-    private void visitKeyboardLayout(String keyboardLayoutDescriptor,
+    private void visitKeyboardLayout(@NonNull String keyboardLayoutDescriptor,
             KeyboardLayoutVisitor visitor) {
         KeyboardLayoutDescriptor d = KeyboardLayoutDescriptor.parse(keyboardLayoutDescriptor);
         if (d != null) {
@@ -482,8 +485,8 @@
         }
     }
 
-    private void visitKeyboardLayoutsInPackage(PackageManager pm, ActivityInfo receiver,
-            String keyboardName, int requestedPriority, KeyboardLayoutVisitor visitor) {
+    private void visitKeyboardLayoutsInPackage(PackageManager pm, @NonNull ActivityInfo receiver,
+            @Nullable String keyboardName, int requestedPriority, KeyboardLayoutVisitor visitor) {
         Bundle metaData = receiver.metaData;
         if (metaData == null) {
             return;
@@ -1415,7 +1418,7 @@
             return packageName + "/" + receiverName + "/" + keyboardName;
         }
 
-        public static KeyboardLayoutDescriptor parse(String descriptor) {
+        public static KeyboardLayoutDescriptor parse(@NonNull String descriptor) {
             int pos = descriptor.indexOf('/');
             if (pos < 0 || pos + 1 == descriptor.length()) {
                 return null;
diff --git a/services/core/java/com/android/server/inputmethod/HandwritingModeController.java b/services/core/java/com/android/server/inputmethod/HandwritingModeController.java
index 23fe5cc..dbdac41 100644
--- a/services/core/java/com/android/server/inputmethod/HandwritingModeController.java
+++ b/services/core/java/com/android/server/inputmethod/HandwritingModeController.java
@@ -16,6 +16,8 @@
 
 package com.android.server.inputmethod;
 
+import static com.android.text.flags.Flags.handwritingEndOfLineTap;
+
 import android.Manifest;
 import android.annotation.AnyThread;
 import android.annotation.NonNull;
@@ -30,6 +32,7 @@
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
+import android.os.SystemClock;
 import android.text.TextUtils;
 import android.util.Slog;
 import android.view.BatchedInputEventReceiver;
@@ -66,6 +69,7 @@
     // Use getHandwritingBufferSize() and not this value directly.
     private static final int LONG_EVENT_BUFFER_SIZE = EVENT_BUFFER_SIZE * 20;
     private static final long HANDWRITING_DELEGATION_IDLE_TIMEOUT_MS = 3000;
+    private static final long AFTER_STYLUS_UP_ALLOW_PERIOD_MS = 200L;
 
     private final Context mContext;
     // This must be the looper for the UiThread.
@@ -78,6 +82,7 @@
     private InputEventReceiver mHandwritingEventReceiver;
     private Runnable mInkWindowInitRunnable;
     private boolean mRecordingGesture;
+    private boolean mRecordingGestureAfterStylusUp;
     private int mCurrentDisplayId;
     // when set, package names are used for handwriting delegation.
     private @Nullable String mDelegatePackageName;
@@ -155,6 +160,15 @@
     }
 
     boolean isStylusGestureOngoing() {
+        if (mRecordingGestureAfterStylusUp && !mHandwritingBuffer.isEmpty()) {
+            // If it is less than AFTER_STYLUS_UP_ALLOW_PERIOD_MS after the stylus up event, return
+            // true so that handwriting can start.
+            MotionEvent lastEvent = mHandwritingBuffer.get(mHandwritingBuffer.size() - 1);
+            if (lastEvent.getActionMasked() == MotionEvent.ACTION_UP) {
+                return SystemClock.uptimeMillis() - lastEvent.getEventTime()
+                        < AFTER_STYLUS_UP_ALLOW_PERIOD_MS;
+            }
+        }
         return mRecordingGesture;
     }
 
@@ -277,7 +291,7 @@
             Slog.e(TAG, "Cannot start handwriting session: Invalid request id: " + requestId);
             return null;
         }
-        if (!mRecordingGesture || mHandwritingBuffer.isEmpty()) {
+        if (!isStylusGestureOngoing()) {
             Slog.e(TAG, "Cannot start handwriting session: No stylus gesture is being recorded.");
             return null;
         }
@@ -300,6 +314,7 @@
         mHandwritingEventReceiver.dispose();
         mHandwritingEventReceiver = null;
         mRecordingGesture = false;
+        mRecordingGestureAfterStylusUp = false;
 
         if (mHandwritingSurface.isIntercepting()) {
             throw new IllegalStateException(
@@ -362,6 +377,7 @@
             clearPendingHandwritingDelegation();
         }
         mRecordingGesture = false;
+        mRecordingGestureAfterStylusUp = false;
     }
 
     private boolean onInputEvent(InputEvent ev) {
@@ -412,15 +428,20 @@
         if ((TextUtils.isEmpty(mDelegatePackageName) || mDelegationConnectionlessFlow)
                 && (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL)) {
             mRecordingGesture = false;
-            mHandwritingBuffer.clear();
-            return;
+            if (handwritingEndOfLineTap() && action == MotionEvent.ACTION_UP) {
+                mRecordingGestureAfterStylusUp = true;
+            } else {
+                mHandwritingBuffer.clear();
+                return;
+            }
         }
 
         if (action == MotionEvent.ACTION_DOWN) {
+            clearBufferIfRecordingAfterStylusUp();
             mRecordingGesture = true;
         }
 
-        if (!mRecordingGesture) {
+        if (!mRecordingGesture && !mRecordingGestureAfterStylusUp) {
             return;
         }
 
@@ -430,12 +451,20 @@
                         + " The rest of the gesture will not be recorded.");
             }
             mRecordingGesture = false;
+            clearBufferIfRecordingAfterStylusUp();
             return;
         }
 
         mHandwritingBuffer.add(MotionEvent.obtain(event));
     }
 
+    private void clearBufferIfRecordingAfterStylusUp() {
+        if (mRecordingGestureAfterStylusUp) {
+            mHandwritingBuffer.clear();
+            mRecordingGestureAfterStylusUp = false;
+        }
+    }
+
     static final class HandwritingSession {
         private final int mRequestId;
         private final InputChannel mHandwritingChannel;
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index d0a83a6..cfd64c4 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -1248,7 +1248,15 @@
             mService.publishLocalService();
             IInputMethodManager.Stub service;
             if (Flags.useZeroJankProxy()) {
-                service = new ZeroJankProxy(mService.mHandler::post, mService);
+                service =
+                        new ZeroJankProxy(
+                                mService.mHandler::post,
+                                mService,
+                                () -> {
+                                    synchronized (ImfLock.class) {
+                                        return mService.isInputShown();
+                                    }
+                                });
             } else {
                 service = mService;
             }
diff --git a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
index 396192e..31ce630 100644
--- a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
+++ b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
@@ -46,7 +46,6 @@
 import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.os.ShellCallback;
-import android.util.ExceptionUtils;
 import android.util.Slog;
 import android.view.WindowManager;
 import android.view.inputmethod.CursorAnchorInfo;
@@ -77,6 +76,7 @@
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executor;
+import java.util.function.BooleanSupplier;
 
 /**
  * A proxy that processes all {@link IInputMethodManager} calls asynchronously.
@@ -86,10 +86,12 @@
 
     private final IInputMethodManager mInner;
     private final Executor mExecutor;
+    private final BooleanSupplier mIsInputShown;
 
-    ZeroJankProxy(Executor executor, IInputMethodManager inner) {
+    ZeroJankProxy(Executor executor, IInputMethodManager inner, BooleanSupplier isInputShown) {
         mInner = inner;
         mExecutor = executor;
+        mIsInputShown = isInputShown;
     }
 
     private void offload(ThrowingRunnable r) {
@@ -163,8 +165,19 @@
             int lastClickTooType, ResultReceiver resultReceiver,
             @SoftInputShowHideReason int reason)
             throws RemoteException {
-        offload(() -> mInner.showSoftInput(client, windowToken, statsToken, flags, lastClickTooType,
-                resultReceiver, reason));
+        offload(
+                () -> {
+                    if (!mInner.showSoftInput(
+                            client,
+                            windowToken,
+                            statsToken,
+                            flags,
+                            lastClickTooType,
+                            resultReceiver,
+                            reason)) {
+                        sendResultReceiverFailure(resultReceiver);
+                    }
+                });
         return true;
     }
 
@@ -173,11 +186,24 @@
             @Nullable ImeTracker.Token statsToken, @InputMethodManager.HideFlags int flags,
             ResultReceiver resultReceiver, @SoftInputShowHideReason int reason)
             throws RemoteException {
-        offload(() -> mInner.hideSoftInput(client, windowToken, statsToken, flags, resultReceiver,
-                reason));
+        offload(
+                () -> {
+                    if (!mInner.hideSoftInput(
+                            client, windowToken, statsToken, flags, resultReceiver, reason)) {
+                        sendResultReceiverFailure(resultReceiver);
+                    }
+                });
         return true;
     }
 
+    private void sendResultReceiverFailure(ResultReceiver resultReceiver) {
+        resultReceiver.send(
+                mIsInputShown.getAsBoolean()
+                        ? InputMethodManager.RESULT_UNCHANGED_SHOWN
+                        : InputMethodManager.RESULT_UNCHANGED_HIDDEN,
+                null);
+    }
+
     @Override
     @EnforcePermission(Manifest.permission.TEST_INPUT_METHOD)
     public void hideSoftInputFromServerForTest() throws RemoteException {
@@ -415,14 +441,17 @@
 
     private void sendOnStartInputResult(
             IInputMethodClient client, InputBindResult res, int startInputSeq) {
-        InputMethodManagerService service = (InputMethodManagerService) mInner;
-        final ClientState cs = service.getClientState(client);
-        if (cs != null && cs.mClient != null) {
-            cs.mClient.onStartInputResult(res, startInputSeq);
-        } else {
-            // client is unbound.
-            Slog.i(TAG, "Client that requested startInputOrWindowGainedFocus is no longer"
-                    + " bound. InputBindResult: " + res + " for startInputSeq: " + startInputSeq);
+        synchronized (ImfLock.class) {
+            InputMethodManagerService service = (InputMethodManagerService) mInner;
+            final ClientState cs = service.getClientState(client);
+            if (cs != null && cs.mClient != null) {
+                cs.mClient.onStartInputResult(res, startInputSeq);
+            } else {
+                // client is unbound.
+                Slog.i(TAG, "Client that requested startInputOrWindowGainedFocus is no longer"
+                        + " bound. InputBindResult: " + res + " for startInputSeq: "
+                        + startInputSeq);
+            }
         }
     }
 }
diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java
index a9a8272..5b3934e 100644
--- a/services/core/java/com/android/server/media/MediaSessionRecord.java
+++ b/services/core/java/com/android/server/media/MediaSessionRecord.java
@@ -687,27 +687,20 @@
 
     private static String toVolumeControlTypeString(
             @VolumeProvider.ControlType int volumeControlType) {
-        switch (volumeControlType) {
-            case VOLUME_CONTROL_FIXED:
-                return "FIXED";
-            case VOLUME_CONTROL_RELATIVE:
-                return "RELATIVE";
-            case VOLUME_CONTROL_ABSOLUTE:
-                return "ABSOLUTE";
-            default:
-                return TextUtils.formatSimple("unknown(%d)", volumeControlType);
-        }
+        return switch (volumeControlType) {
+            case VOLUME_CONTROL_FIXED -> "FIXED";
+            case VOLUME_CONTROL_RELATIVE -> "RELATIVE";
+            case VOLUME_CONTROL_ABSOLUTE -> "ABSOLUTE";
+            default -> TextUtils.formatSimple("unknown(%d)", volumeControlType);
+        };
     }
 
     private static String toVolumeTypeString(@PlaybackInfo.PlaybackType int volumeType) {
-        switch (volumeType) {
-            case PLAYBACK_TYPE_LOCAL:
-                return "LOCAL";
-            case PLAYBACK_TYPE_REMOTE:
-                return "REMOTE";
-            default:
-                return TextUtils.formatSimple("unknown(%d)", volumeType);
-        }
+        return switch (volumeType) {
+            case PLAYBACK_TYPE_LOCAL -> "LOCAL";
+            case PLAYBACK_TYPE_REMOTE -> "REMOTE";
+            default -> TextUtils.formatSimple("unknown(%d)", volumeType);
+        };
     }
 
     @Override
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index 4f3cdbc..50ca984 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -310,6 +310,7 @@
                     parser.getAttributeInt(null, ATT_VISIBILITY, DEFAULT_VISIBILITY),
                     parser.getAttributeBoolean(null, ATT_SHOW_BADGE, DEFAULT_SHOW_BADGE),
                     bubblePref);
+            r.bubblePreference = bubblePref;
             r.priority = parser.getAttributeInt(null, ATT_PRIORITY, DEFAULT_PRIORITY);
             r.visibility = parser.getAttributeInt(null, ATT_VISIBILITY, DEFAULT_VISIBILITY);
             r.showBadge = parser.getAttributeBoolean(null, ATT_SHOW_BADGE, DEFAULT_SHOW_BADGE);
@@ -676,7 +677,7 @@
      * @param bubblePreference whether bubbles are allowed.
      */
     public void setBubblesAllowed(String pkg, int uid, int bubblePreference) {
-        boolean changed = false;
+        boolean changed;
         synchronized (mPackagePreferences) {
             PackagePreferences p = getOrCreatePackagePreferencesLocked(pkg, uid);
             changed = p.bubblePreference != bubblePreference;
diff --git a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
index 37023e1..953300a 100644
--- a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
+++ b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
@@ -163,7 +163,7 @@
             }
 
             @Override
-            public void getVersion(RemoteCallback remoteCallback) throws RemoteException {
+            public void getVersion(RemoteCallback remoteCallback) {
                 Slog.i(TAG, "OnDeviceIntelligenceManagerInternal getVersion");
                 Objects.requireNonNull(remoteCallback);
                 mContext.enforceCallingOrSelfPermission(
@@ -244,7 +244,7 @@
 
             @Override
             public void requestFeatureDownload(Feature feature,
-                    ICancellationSignal cancellationSignal,
+                    AndroidFuture cancellationSignalFuture,
                     IDownloadCallback downloadCallback) throws RemoteException {
                 Slog.i(TAG, "OnDeviceIntelligenceManagerInternal requestFeatureDownload");
                 Objects.requireNonNull(feature);
@@ -261,16 +261,17 @@
                 ensureRemoteIntelligenceServiceInitialized();
                 mRemoteOnDeviceIntelligenceService.run(
                         service -> service.requestFeatureDownload(Binder.getCallingUid(), feature,
-                                cancellationSignal,
+                                cancellationSignalFuture,
                                 downloadCallback));
             }
 
 
             @Override
             public void requestTokenInfo(Feature feature,
-                    Bundle request, ICancellationSignal cancellationSignal,
+                    Bundle request,
+                    AndroidFuture cancellationSignalFuture,
                     ITokenInfoCallback tokenInfoCallback) throws RemoteException {
-                Slog.i(TAG, "OnDeviceIntelligenceManagerInternal prepareFeatureProcessing");
+                Slog.i(TAG, "OnDeviceIntelligenceManagerInternal requestTokenInfo");
                 Objects.requireNonNull(feature);
                 Objects.requireNonNull(request);
                 Objects.requireNonNull(tokenInfoCallback);
@@ -285,10 +286,11 @@
                             PersistableBundle.EMPTY);
                 }
                 ensureRemoteInferenceServiceInitialized();
+
                 mRemoteInferenceService.run(
                         service -> service.requestTokenInfo(Binder.getCallingUid(), feature,
                                 request,
-                                cancellationSignal,
+                                cancellationSignalFuture,
                                 tokenInfoCallback));
             }
 
@@ -296,8 +298,8 @@
             public void processRequest(Feature feature,
                     Bundle request,
                     int requestType,
-                    ICancellationSignal cancellationSignal,
-                    IProcessingSignal processingSignal,
+                    AndroidFuture cancellationSignalFuture,
+                    AndroidFuture processingSignalFuture,
                     IResponseCallback responseCallback)
                     throws RemoteException {
                 Slog.i(TAG, "OnDeviceIntelligenceManagerInternal processRequest");
@@ -316,7 +318,7 @@
                 mRemoteInferenceService.run(
                         service -> service.processRequest(Binder.getCallingUid(), feature, request,
                                 requestType,
-                                cancellationSignal, processingSignal,
+                                cancellationSignalFuture, processingSignalFuture,
                                 responseCallback));
             }
 
@@ -324,8 +326,8 @@
             public void processRequestStreaming(Feature feature,
                     Bundle request,
                     int requestType,
-                    ICancellationSignal cancellationSignal,
-                    IProcessingSignal processingSignal,
+                    AndroidFuture cancellationSignalFuture,
+                    AndroidFuture processingSignalFuture,
                     IStreamingResponseCallback streamingCallback) throws RemoteException {
                 Slog.i(TAG, "OnDeviceIntelligenceManagerInternal processRequestStreaming");
                 Objects.requireNonNull(feature);
@@ -343,7 +345,7 @@
                 mRemoteInferenceService.run(
                         service -> service.processRequestStreaming(Binder.getCallingUid(), feature,
                                 request, requestType,
-                                cancellationSignal, processingSignal,
+                                cancellationSignalFuture, processingSignalFuture,
                                 streamingCallback));
             }
 
@@ -356,7 +358,7 @@
         };
     }
 
-    private void ensureRemoteIntelligenceServiceInitialized() throws RemoteException {
+    private void ensureRemoteIntelligenceServiceInitialized() {
         synchronized (mLock) {
             if (mRemoteOnDeviceIntelligenceService == null) {
                 String serviceName = getServiceNames()[0];
@@ -388,25 +390,15 @@
             public void updateProcessingState(
                     Bundle processingState,
                     IProcessingUpdateStatusCallback callback) {
-                try {
-                    ensureRemoteInferenceServiceInitialized();
-                    mRemoteInferenceService.run(
-                            service -> service.updateProcessingState(
-                                    processingState, callback));
-                } catch (RemoteException unused) {
-                    try {
-                        callback.onFailure(
-                                OnDeviceIntelligenceException.PROCESSING_UPDATE_STATUS_CONNECTION_FAILED,
-                                "Received failure invoking the remote processing service.");
-                    } catch (RemoteException ex) {
-                        Slog.w(TAG, "Failed to send failure status.", ex);
-                    }
-                }
+                ensureRemoteInferenceServiceInitialized();
+                mRemoteInferenceService.run(
+                        service -> service.updateProcessingState(
+                                processingState, callback));
             }
         };
     }
 
-    private void ensureRemoteInferenceServiceInitialized() throws RemoteException {
+    private void ensureRemoteInferenceServiceInitialized() {
         synchronized (mLock) {
             if (mRemoteInferenceService == null) {
                 String serviceName = getServiceNames()[1];
@@ -457,34 +449,38 @@
         };
     }
 
-    private static void validateServiceElevated(String serviceName, boolean checkIsolated)
-            throws RemoteException {
-        if (TextUtils.isEmpty(serviceName)) {
-            throw new IllegalArgumentException("Received null/empty service name : " + serviceName);
-        }
-        ComponentName serviceComponent = ComponentName.unflattenFromString(
-                serviceName);
-        ServiceInfo serviceInfo = AppGlobals.getPackageManager().getServiceInfo(
-                serviceComponent,
-                PackageManager.MATCH_DIRECT_BOOT_AWARE
-                        | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, 0);
-        if (serviceInfo != null) {
-            if (!checkIsolated) {
-                checkServiceRequiresPermission(serviceInfo,
-                        Manifest.permission.BIND_ON_DEVICE_INTELLIGENCE_SERVICE);
-                return;
+    private void validateServiceElevated(String serviceName, boolean checkIsolated) {
+        try {
+            if (TextUtils.isEmpty(serviceName)) {
+                throw new IllegalStateException(
+                        "Remote service is not configured to complete the request");
             }
+            ComponentName serviceComponent = ComponentName.unflattenFromString(
+                    serviceName);
+            ServiceInfo serviceInfo = AppGlobals.getPackageManager().getServiceInfo(
+                    serviceComponent,
+                    PackageManager.MATCH_DIRECT_BOOT_AWARE
+                            | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, 0);
+            if (serviceInfo != null) {
+                if (!checkIsolated) {
+                    checkServiceRequiresPermission(serviceInfo,
+                            Manifest.permission.BIND_ON_DEVICE_INTELLIGENCE_SERVICE);
+                    return;
+                }
 
-            checkServiceRequiresPermission(serviceInfo,
-                    Manifest.permission.BIND_ON_DEVICE_SANDBOXED_INFERENCE_SERVICE);
-            if (!isIsolatedService(serviceInfo)) {
-                throw new SecurityException(
-                        "Call required an isolated service, but the configured service: "
-                                + serviceName + ", is not isolated");
+                checkServiceRequiresPermission(serviceInfo,
+                        Manifest.permission.BIND_ON_DEVICE_SANDBOXED_INFERENCE_SERVICE);
+                if (!isIsolatedService(serviceInfo)) {
+                    throw new SecurityException(
+                            "Call required an isolated service, but the configured service: "
+                                    + serviceName + ", is not isolated");
+                }
+            } else {
+                throw new IllegalStateException(
+                        "Remote service is not configured to complete the request.");
             }
-        } else {
-            throw new RuntimeException(
-                    "Could not find service info for serviceName: " + serviceName);
+        } catch (RemoteException e) {
+            throw new IllegalStateException("Could not fetch service info for remote services", e);
         }
     }
 
@@ -542,7 +538,8 @@
                 Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG);
         synchronized (mLock) {
             mTemporaryServiceNames = componentNames;
-
+            mRemoteOnDeviceIntelligenceService = null;
+            mRemoteInferenceService = null;
             if (mTemporaryHandler == null) {
                 mTemporaryHandler = new Handler(Looper.getMainLooper(), null, true) {
                     @Override
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index 9480c8e..2005b17 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -137,6 +137,7 @@
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.Preconditions;
 import com.android.modules.utils.TypedXmlSerializer;
+import com.android.server.ondeviceintelligence.OnDeviceIntelligenceManagerInternal;
 import com.android.server.pm.dex.DexManager;
 import com.android.server.pm.dex.PackageDexUsage;
 import com.android.server.pm.parsing.PackageInfoUtils;
@@ -4353,9 +4354,8 @@
         if (Process.isSdkSandboxUid(uid)) {
             uid = getBaseSdkSandboxUid();
         }
-        if (Process.isIsolatedUid(uid)
-                && mPermissionManager.getHotwordDetectionServiceProvider() != null
-                && uid == mPermissionManager.getHotwordDetectionServiceProvider().getUid()) {
+        final int callingUserId = UserHandle.getUserId(callingUid);
+        if (isKnownIsolatedComputeApp(uid, callingUserId)) {
             try {
                 uid = getIsolatedOwner(uid);
             } catch (IllegalStateException e) {
@@ -4363,7 +4363,6 @@
                 Slog.wtf(TAG, "Expected isolated uid " + uid + " to have an owner", e);
             }
         }
-        final int callingUserId = UserHandle.getUserId(callingUid);
         final int appId = UserHandle.getAppId(uid);
         final Object obj = mSettings.getSettingBase(appId);
         if (obj instanceof SharedUserSetting) {
@@ -4399,9 +4398,7 @@
             if (Process.isSdkSandboxUid(uid)) {
                 uid = getBaseSdkSandboxUid();
             }
-            if (Process.isIsolatedUid(uid)
-                    && mPermissionManager.getHotwordDetectionServiceProvider() != null
-                    && uid == mPermissionManager.getHotwordDetectionServiceProvider().getUid()) {
+            if (isKnownIsolatedComputeApp(uid, callingUserId)) {
                 try {
                     uid = getIsolatedOwner(uid);
                 } catch (IllegalStateException e) {
@@ -5802,6 +5799,43 @@
         return getPackage(mService.getSdkSandboxPackageName()).getUid();
     }
 
+
+    private boolean isKnownIsolatedComputeApp(int uid, int callingUserId) {
+        if (!Process.isIsolatedUid(uid)) {
+            return false;
+        }
+        final boolean isHotword =
+                mPermissionManager.getHotwordDetectionServiceProvider() != null
+                        && uid
+                        == mPermissionManager.getHotwordDetectionServiceProvider().getUid();
+        if (isHotword) {
+            return true;
+        }
+        OnDeviceIntelligenceManagerInternal onDeviceIntelligenceManagerInternal =
+                mInjector.getLocalService(OnDeviceIntelligenceManagerInternal.class);
+        if (onDeviceIntelligenceManagerInternal == null) {
+            return false;
+        }
+
+        String onDeviceIntelligencePackage =
+                onDeviceIntelligenceManagerInternal.getRemoteServicePackageName();
+        if (onDeviceIntelligencePackage == null) {
+            return false;
+        }
+
+        try {
+            if (getIsolatedOwner(uid) == getPackageUid(onDeviceIntelligencePackage, 0,
+                    callingUserId)) {
+                return true;
+            }
+        } catch (IllegalStateException e) {
+            // If the owner uid doesn't exist, just use the current uid
+            Slog.wtf(TAG, "Expected isolated uid " + uid + " to have an owner", e);
+        }
+
+        return false;
+    }
+
     @Nullable
     @Override
     public SharedUserApi getSharedUser(int sharedUserAppId) {
diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java
index c6bb99e..20b669b 100644
--- a/services/core/java/com/android/server/pm/LauncherAppsService.java
+++ b/services/core/java/com/android/server/pm/LauncherAppsService.java
@@ -18,12 +18,12 @@
 
 import static android.Manifest.permission.READ_FRAME_BUFFER;
 import static android.app.ActivityOptions.KEY_SPLASH_SCREEN_THEME;
+import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED;
+import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED;
 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.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED;
-import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED;
 import static android.app.PendingIntent.FLAG_IMMUTABLE;
 import static android.app.PendingIntent.FLAG_MUTABLE;
 import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
@@ -555,12 +555,6 @@
                     return false;
                 }
 
-                if (!mRoleManager
-                        .getRoleHoldersAsUser(
-                                RoleManager.ROLE_HOME, UserHandle.getUserHandleForUid(callingUid))
-                        .contains(callingPackage.getPackageName())) {
-                    return false;
-                }
                 if (mContext.checkPermission(
                                 Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL,
                                 callingPid,
@@ -569,6 +563,13 @@
                     return true;
                 }
 
+                if (!mRoleManager
+                        .getRoleHoldersAsUser(
+                                RoleManager.ROLE_HOME, UserHandle.getUserHandleForUid(callingUid))
+                        .contains(callingPackage.getPackageName())) {
+                    return false;
+                }
+
                 // TODO(b/321988638): add option to disable with a flag
                 return mContext.checkPermission(
                                 android.Manifest.permission.ACCESS_HIDDEN_PROFILES,
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 7a83eb5..76bf8fd 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -530,6 +530,14 @@
     // TODO(b/178103325): Track sleep/requested sleep for every display.
     volatile boolean mRequestedOrSleepingDefaultDisplay;
 
+    /**
+     * This is used to check whether to invoke {@link #updateScreenOffSleepToken} when screen is
+     * turned off. E.g. if it is false when screen is turned off and the display is swapping, it
+     * is expected that the screen will be on in a short time. Then it is unnecessary to acquire
+     * screen-off-sleep-token, so it can avoid intermediate visibility or lifecycle changes.
+     */
+    volatile boolean mIsGoingToSleepDefaultDisplay;
+
     volatile boolean mRecentsVisible;
     volatile boolean mNavBarVirtualKeyHapticFeedbackEnabled = true;
     volatile boolean mPictureInPictureVisible;
@@ -3479,13 +3487,6 @@
                     return true;
                 }
                 break;
-            case KeyEvent.KEYCODE_T:
-                if (firstDown && event.isMetaPressed()) {
-                    toggleTaskbar();
-                    logKeyboardSystemsEvent(event, KeyboardLogEvent.TOGGLE_TASKBAR);
-                    return true;
-                }
-                break;
             case KeyEvent.KEYCODE_DEL:
             case KeyEvent.KEYCODE_ESCAPE:
                 if (firstDown && event.isMetaPressed()) {
@@ -3507,7 +3508,7 @@
                 if (firstDown && event.isMetaPressed() && event.isCtrlPressed()) {
                     StatusBarManagerInternal statusbar = getStatusBarManagerInternal();
                     if (statusbar != null) {
-                        statusbar.enterDesktop(getTargetDisplayIdForKeyEvent(event));
+                        statusbar.moveFocusedTaskToDesktop(getTargetDisplayIdForKeyEvent(event));
                         logKeyboardSystemsEvent(event, KeyboardLogEvent.DESKTOP_MODE);
                         return true;
                     }
@@ -5477,6 +5478,15 @@
         }
 
         mRequestedOrSleepingDefaultDisplay = true;
+        mIsGoingToSleepDefaultDisplay = true;
+
+        // In case startedGoingToSleep is called after screenTurnedOff (the source caller is in
+        // order but the methods run on different threads) and updateScreenOffSleepToken was
+        // skipped. Then acquire sleep token if screen was off.
+        if (!mDefaultDisplayPolicy.isScreenOnFully() && !mDefaultDisplayPolicy.isScreenOnEarly()
+                && com.android.window.flags.Flags.skipSleepingWhenSwitchingDisplay()) {
+            updateScreenOffSleepToken(true /* acquire */, false /* isSwappingDisplay */);
+        }
 
         if (mKeyguardDelegate != null) {
             mKeyguardDelegate.onStartedGoingToSleep(pmSleepReason);
@@ -5500,6 +5510,7 @@
         MetricsLogger.histogram(mContext, "screen_timeout", mLockScreenTimeout / 1000);
 
         mRequestedOrSleepingDefaultDisplay = false;
+        mIsGoingToSleepDefaultDisplay = false;
         mDefaultDisplayPolicy.setAwake(false);
 
         // We must get this work done here because the power manager will drop
@@ -5535,7 +5546,7 @@
         }
         EventLogTags.writeScreenToggled(1);
 
-
+        mIsGoingToSleepDefaultDisplay = false;
         mDefaultDisplayPolicy.setAwake(true);
 
         // Since goToSleep performs these functions synchronously, we must
@@ -5637,7 +5648,10 @@
         if (DEBUG_WAKEUP) Slog.i(TAG, "Display" + displayId + " turned off...");
 
         if (displayId == DEFAULT_DISPLAY) {
-            updateScreenOffSleepToken(true, isSwappingDisplay);
+            if (!isSwappingDisplay || mIsGoingToSleepDefaultDisplay
+                    || !com.android.window.flags.Flags.skipSleepingWhenSwitchingDisplay()) {
+                updateScreenOffSleepToken(true /* acquire */, isSwappingDisplay);
+            }
             mRequestedOrSleepingDefaultDisplay = false;
             mDefaultDisplayPolicy.screenTurnedOff();
             synchronized (mLock) {
diff --git a/services/core/java/com/android/server/power/hint/Android.bp b/services/core/java/com/android/server/power/hint/Android.bp
new file mode 100644
index 0000000..8a98de6
--- /dev/null
+++ b/services/core/java/com/android/server/power/hint/Android.bp
@@ -0,0 +1,12 @@
+aconfig_declarations {
+    name: "power_hint_flags",
+    package: "com.android.server.power.hint",
+    srcs: [
+        "flags.aconfig",
+    ],
+}
+
+java_aconfig_library {
+    name: "power_hint_flags_lib",
+    aconfig_declarations: "power_hint_flags",
+}
diff --git a/services/core/java/com/android/server/power/hint/HintManagerService.java b/services/core/java/com/android/server/power/hint/HintManagerService.java
index aa1a41e..3f1b1c1 100644
--- a/services/core/java/com/android/server/power/hint/HintManagerService.java
+++ b/services/core/java/com/android/server/power/hint/HintManagerService.java
@@ -17,6 +17,7 @@
 package com.android.server.power.hint;
 
 import static com.android.internal.util.ConcurrentUtils.DIRECT_EXECUTOR;
+import static com.android.server.power.hint.Flags.powerhintThreadCleanup;
 
 import android.annotation.NonNull;
 import android.app.ActivityManager;
@@ -26,9 +27,12 @@
 import android.content.Context;
 import android.hardware.power.WorkDuration;
 import android.os.Binder;
+import android.os.Handler;
 import android.os.IBinder;
 import android.os.IHintManager;
 import android.os.IHintSession;
+import android.os.Looper;
+import android.os.Message;
 import android.os.PerformanceHintManager;
 import android.os.Process;
 import android.os.RemoteException;
@@ -36,6 +40,8 @@
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
+import android.util.IntArray;
+import android.util.Slog;
 import android.util.SparseIntArray;
 import android.util.StatsEvent;
 
@@ -46,20 +52,31 @@
 import com.android.internal.util.Preconditions;
 import com.android.server.FgThread;
 import com.android.server.LocalServices;
+import com.android.server.ServiceThread;
 import com.android.server.SystemService;
 import com.android.server.utils.Slogf;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
 
 /** An hint service implementation that runs in System Server process. */
 public final class HintManagerService extends SystemService {
     private static final String TAG = "HintManagerService";
     private static final boolean DEBUG = false;
+
+    private static final int EVENT_CLEAN_UP_UID = 3;
+    @VisibleForTesting  static final int CLEAN_UP_UID_DELAY_MILLIS = 1000;
+
+
     @VisibleForTesting final long mHintSessionPreferredRate;
 
     // Multi-level map storing all active AppHintSessions.
@@ -73,9 +90,15 @@
     /** Lock to protect HAL handles and listen list. */
     private final Object mLock = new Object();
 
+    @GuardedBy("mNonIsolatedTidsLock")
+    private final Map<Integer, Set<Long>> mNonIsolatedTids;
+
+    private final Object mNonIsolatedTidsLock = new Object();
+
     @VisibleForTesting final MyUidObserver mUidObserver;
 
     private final NativeWrapper mNativeWrapper;
+    private final CleanUpHandler mCleanUpHandler;
 
     private final ActivityManagerInternal mAmInternal;
 
@@ -94,6 +117,13 @@
     HintManagerService(Context context, Injector injector) {
         super(context);
         mContext = context;
+        if (powerhintThreadCleanup()) {
+            mCleanUpHandler = new CleanUpHandler(createCleanUpThread().getLooper());
+            mNonIsolatedTids = new HashMap<>();
+        } else {
+            mCleanUpHandler = null;
+            mNonIsolatedTids = null;
+        }
         mActiveSessions = new ArrayMap<>();
         mNativeWrapper = injector.createNativeWrapper();
         mNativeWrapper.halInit();
@@ -103,6 +133,13 @@
                 LocalServices.getService(ActivityManagerInternal.class));
     }
 
+    private ServiceThread createCleanUpThread() {
+        final ServiceThread handlerThread = new ServiceThread(TAG,
+                Process.THREAD_PRIORITY_LOWEST, true /*allowIo*/);
+        handlerThread.start();
+        return handlerThread;
+    }
+
     @VisibleForTesting
     static class Injector {
         NativeWrapper createNativeWrapper() {
@@ -306,7 +343,18 @@
         public void onUidStateChanged(int uid, int procState, long procStateSeq, int capability) {
             FgThread.getHandler().post(() -> {
                 synchronized (mCacheLock) {
-                    mProcStatesCache.put(uid, procState);
+                    if (powerhintThreadCleanup()) {
+                        final boolean before = isUidForeground(uid);
+                        mProcStatesCache.put(uid, procState);
+                        final boolean after = isUidForeground(uid);
+                        if (before != after) {
+                            final Message msg = mCleanUpHandler.obtainMessage(EVENT_CLEAN_UP_UID,
+                                    uid);
+                            mCleanUpHandler.sendMessageDelayed(msg, CLEAN_UP_UID_DELAY_MILLIS);
+                        }
+                    } else {
+                        mProcStatesCache.put(uid, procState);
+                    }
                 }
                 boolean shouldAllowUpdate = isUidForeground(uid);
                 synchronized (mLock) {
@@ -314,9 +362,10 @@
                     if (tokenMap == null) {
                         return;
                     }
-                    for (ArraySet<AppHintSession> sessionSet : tokenMap.values()) {
-                        for (AppHintSession s : sessionSet) {
-                            s.onProcStateChanged(shouldAllowUpdate);
+                    for (int i = tokenMap.size() - 1; i >= 0; i--) {
+                        final ArraySet<AppHintSession> sessionSet = tokenMap.valueAt(i);
+                        for (int j = sessionSet.size() - 1; j >= 0; j--) {
+                            sessionSet.valueAt(j).onProcStateChanged(shouldAllowUpdate);
                         }
                     }
                 }
@@ -324,52 +373,237 @@
         }
     }
 
+    final class CleanUpHandler extends Handler {
+        // status of processed tid used for caching
+        private static final int TID_NOT_CHECKED = 0;
+        private static final int TID_PASSED_CHECK = 1;
+        private static final int TID_EXITED = 2;
+
+        CleanUpHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            if (msg.what == EVENT_CLEAN_UP_UID) {
+                if (hasEqualMessages(msg.what, msg.obj)) {
+                    removeEqualMessages(msg.what, msg.obj);
+                    final Message newMsg = obtainMessage(msg.what, msg.obj);
+                    sendMessageDelayed(newMsg, CLEAN_UP_UID_DELAY_MILLIS);
+                    return;
+                }
+                final int uid = (int) msg.obj;
+                boolean isForeground = mUidObserver.isUidForeground(uid);
+                // store all sessions in a list and release the global lock
+                // we don't need to worry about stale data or racing as the session is synchronized
+                // itself and will perform its own closed status check in setThreads call
+                final List<AppHintSession> sessions;
+                synchronized (mLock) {
+                    final ArrayMap<IBinder, ArraySet<AppHintSession>> tokenMap =
+                            mActiveSessions.get(uid);
+                    if (tokenMap == null || tokenMap.isEmpty()) {
+                        return;
+                    }
+                    sessions = new ArrayList<>(tokenMap.size());
+                    for (int i = tokenMap.size() - 1; i >= 0; i--) {
+                        final ArraySet<AppHintSession> set = tokenMap.valueAt(i);
+                        for (int j = set.size() - 1; j >= 0; j--) {
+                            sessions.add(set.valueAt(j));
+                        }
+                    }
+                }
+                final long[] durationList = new long[sessions.size()];
+                final int[] invalidTidCntList = new int[sessions.size()];
+                final SparseIntArray checkedTids = new SparseIntArray();
+                int[] totalTidCnt = new int[1];
+                for (int i = sessions.size() - 1; i >= 0; i--) {
+                    final AppHintSession session = sessions.get(i);
+                    final long start = System.nanoTime();
+                    try {
+                        final int invalidCnt = cleanUpSession(session, checkedTids, totalTidCnt);
+                        final long elapsed = System.nanoTime() - start;
+                        invalidTidCntList[i] = invalidCnt;
+                        durationList[i] = elapsed;
+                    } catch (Exception e) {
+                        Slog.e(TAG, "Failed to clean up session " + session.mHalSessionPtr
+                                + " for UID " + session.mUid);
+                    }
+                }
+                logCleanUpMetrics(uid, invalidTidCntList, durationList, sessions.size(),
+                        totalTidCnt[0], isForeground);
+            }
+        }
+
+        private void logCleanUpMetrics(int uid, int[] count, long[] durationNsList, int sessionCnt,
+                int totalTidCnt, boolean isForeground) {
+            int maxInvalidTidCnt = Integer.MIN_VALUE;
+            int totalInvalidTidCnt = 0;
+            for (int i = 0; i < count.length; i++) {
+                totalInvalidTidCnt += count[i];
+                maxInvalidTidCnt = Math.max(maxInvalidTidCnt, count[i]);
+            }
+            if (DEBUG || totalInvalidTidCnt > 0) {
+                Arrays.sort(durationNsList);
+                long totalDurationNs = 0;
+                for (int i = 0; i < durationNsList.length; i++) {
+                    totalDurationNs += durationNsList[i];
+                }
+                int totalDurationUs = (int) TimeUnit.NANOSECONDS.toMicros(totalDurationNs);
+                int maxDurationUs = (int) TimeUnit.NANOSECONDS.toMicros(
+                        durationNsList[durationNsList.length - 1]);
+                int minDurationUs = (int) TimeUnit.NANOSECONDS.toMicros(durationNsList[0]);
+                int avgDurationUs = (int) TimeUnit.NANOSECONDS.toMicros(
+                        totalDurationNs / durationNsList.length);
+                int th90DurationUs = (int) TimeUnit.NANOSECONDS.toMicros(
+                        durationNsList[(int) (durationNsList.length * 0.9)]);
+                Slog.d(TAG,
+                        "Invalid tid found for UID" + uid + " in " + totalDurationUs + "us:\n\t"
+                                + "count("
+                                + " session: " + sessionCnt
+                                + " totalTid: " + totalTidCnt
+                                + " maxInvalidTid: " + maxInvalidTidCnt
+                                + " totalInvalidTid: " + totalInvalidTidCnt + ")\n\t"
+                                + "time per session("
+                                + " min: " + minDurationUs + "us"
+                                + " max: " + maxDurationUs + "us"
+                                + " avg: " + avgDurationUs + "us"
+                                + " 90%: " + th90DurationUs + "us" + ")\n\t"
+                                + "isForeground: " + isForeground);
+            }
+        }
+
+        // This will check if each TID currently linked to the session still exists. If it's
+        // previously registered as not an isolated process, then it will run tkill(pid, tid, 0) to
+        // verify that it's still running under the same pid. Otherwise, it will run
+        // kill(tid, 0) to only check if it exists. The result will be cached in checkedTids
+        // map with tid as the key and checked status as value.
+        public int cleanUpSession(AppHintSession session, SparseIntArray checkedTids, int[] total) {
+            if (session.isClosed()) {
+                return 0;
+            }
+            final int pid = session.mPid;
+            final int[] tids = session.getTidsInternal();
+            if (total != null && total.length == 1) {
+                total[0] += tids.length;
+            }
+            final IntArray filtered = new IntArray(tids.length);
+            for (int i = 0; i < tids.length; i++) {
+                int tid = tids[i];
+                if (checkedTids.get(tid, 0) != TID_NOT_CHECKED) {
+                    if (checkedTids.get(tid) == TID_PASSED_CHECK) {
+                        filtered.add(tid);
+                    }
+                    continue;
+                }
+                // if it was registered as a non-isolated then we perform more restricted check
+                final boolean isNotIsolated;
+                synchronized (mNonIsolatedTidsLock) {
+                    isNotIsolated = mNonIsolatedTids.containsKey(tid);
+                }
+                try {
+                    if (isNotIsolated) {
+                        Process.checkTid(pid, tid);
+                    } else {
+                        Process.checkPid(tid);
+                    }
+                    checkedTids.put(tid, TID_PASSED_CHECK);
+                    filtered.add(tid);
+                } catch (NoSuchElementException e) {
+                    checkedTids.put(tid, TID_EXITED);
+                } catch (Exception e) {
+                    Slog.w(TAG, "Unexpected exception when checking TID " + tid + " under PID "
+                            + pid + "(isolated: " + !isNotIsolated + ")", e);
+                    // if anything unexpected happens then we keep it, but don't store it as checked
+                    filtered.add(tid);
+                }
+            }
+            final int diff = tids.length - filtered.size();
+            if (diff > 0) {
+                synchronized (session) {
+                    // in case thread list is updated during the cleanup then we skip updating
+                    // the session but just return the number for reporting purpose
+                    final int[] newTids = session.getTidsInternal();
+                    if (newTids.length != tids.length) {
+                        Slog.d(TAG, "Skipped cleaning up the session as new tids are added");
+                        return diff;
+                    }
+                    Arrays.sort(newTids);
+                    Arrays.sort(tids);
+                    if (!Arrays.equals(newTids, tids)) {
+                        Slog.d(TAG, "Skipped cleaning up the session as new tids are updated");
+                        return diff;
+                    }
+                    Slog.d(TAG, "Cleaned up " + diff + " invalid tids for session "
+                            + session.mHalSessionPtr + " with UID " + session.mUid + "\n\t"
+                            + "before: " + Arrays.toString(tids) + "\n\t"
+                            + "after: " + filtered);
+                    final int[] filteredTids = filtered.toArray();
+                    if (filteredTids.length == 0) {
+                        session.mShouldForcePause = true;
+                        if (session.mUpdateAllowed) {
+                            session.pause();
+                        }
+                    } else {
+                        session.setThreadsInternal(filteredTids, false);
+                    }
+                }
+            }
+            return diff;
+        }
+    }
+
     @VisibleForTesting
     IHintManager.Stub getBinderServiceInstance() {
         return mService;
     }
 
     // returns the first invalid tid or null if not found
-    private Integer checkTidValid(int uid, int tgid, int [] tids) {
+    private Integer checkTidValid(int uid, int tgid, int [] tids, IntArray nonIsolated) {
         // Make sure all tids belongs to the same UID (including isolated UID),
         // tids can belong to different application processes.
         List<Integer> isolatedPids = null;
-        for (int threadId : tids) {
+        for (int i = 0; i < tids.length; i++) {
+            int tid = tids[i];
             final String[] procStatusKeys = new String[] {
                     "Uid:",
                     "Tgid:"
             };
             long[] output = new long[procStatusKeys.length];
-            Process.readProcLines("/proc/" + threadId + "/status", procStatusKeys, output);
+            Process.readProcLines("/proc/" + tid + "/status", procStatusKeys, output);
             int uidOfThreadId = (int) output[0];
             int pidOfThreadId = (int) output[1];
 
-            // use PID check for isolated processes, use UID check for non-isolated processes.
-            if (pidOfThreadId == tgid || uidOfThreadId == uid) {
+            // use PID check for non-isolated processes
+            if (nonIsolated != null && pidOfThreadId == tgid) {
+                nonIsolated.add(tid);
+                continue;
+            }
+            // use UID check for isolated processes.
+            if (uidOfThreadId == uid) {
                 continue;
             }
             // Only call into AM if the tid is either isolated or invalid
             if (isolatedPids == null) {
                 // To avoid deadlock, do not call into AMS if the call is from system.
                 if (uid == Process.SYSTEM_UID) {
-                    return threadId;
+                    return tid;
                 }
                 isolatedPids = mAmInternal.getIsolatedProcesses(uid);
                 if (isolatedPids == null) {
-                    return threadId;
+                    return tid;
                 }
             }
             if (isolatedPids.contains(pidOfThreadId)) {
                 continue;
             }
-            return threadId;
+            return tid;
         }
         return null;
     }
 
     private String formatTidCheckErrMsg(int callingUid, int[] tids, Integer invalidTid) {
         return "Tid" + invalidTid + " from list " + Arrays.toString(tids)
-                + " doesn't belong to the calling application" + callingUid;
+                + " doesn't belong to the calling application " + callingUid;
     }
 
     @VisibleForTesting
@@ -387,7 +621,10 @@
             final int callingTgid = Process.getThreadGroupLeader(Binder.getCallingPid());
             final long identity = Binder.clearCallingIdentity();
             try {
-                final Integer invalidTid = checkTidValid(callingUid, callingTgid, tids);
+                final IntArray nonIsolated = powerhintThreadCleanup() ? new IntArray(tids.length)
+                        : null;
+                final Integer invalidTid = checkTidValid(callingUid, callingTgid, tids,
+                        nonIsolated);
                 if (invalidTid != null) {
                     final String errMsg = formatTidCheckErrMsg(callingUid, tids, invalidTid);
                     Slogf.w(TAG, errMsg);
@@ -396,6 +633,14 @@
 
                 long halSessionPtr = mNativeWrapper.halCreateHintSession(callingTgid, callingUid,
                         tids, durationNanos);
+                if (powerhintThreadCleanup()) {
+                    synchronized (mNonIsolatedTidsLock) {
+                        for (int i = nonIsolated.size() - 1; i >= 0; i--) {
+                            mNonIsolatedTids.putIfAbsent(nonIsolated.get(i), new ArraySet<>());
+                            mNonIsolatedTids.get(nonIsolated.get(i)).add(halSessionPtr);
+                        }
+                    }
+                }
                 if (halSessionPtr == 0) {
                     return null;
                 }
@@ -482,6 +727,7 @@
         protected boolean mUpdateAllowed;
         protected int[] mNewThreadIds;
         protected boolean mPowerEfficient;
+        protected boolean mShouldForcePause;
 
         private enum SessionModes {
             POWER_EFFICIENCY,
@@ -498,6 +744,7 @@
             mTargetDurationNanos = durationNanos;
             mUpdateAllowed = true;
             mPowerEfficient = false;
+            mShouldForcePause = false;
             final boolean allowed = mUidObserver.isUidForeground(mUid);
             updateHintAllowed(allowed);
             try {
@@ -511,7 +758,7 @@
         @VisibleForTesting
         boolean updateHintAllowed(boolean allowed) {
             synchronized (this) {
-                if (allowed && !mUpdateAllowed) resume();
+                if (allowed && !mUpdateAllowed && !mShouldForcePause) resume();
                 if (!allowed && mUpdateAllowed) pause();
                 mUpdateAllowed = allowed;
                 return mUpdateAllowed;
@@ -521,7 +768,7 @@
         @Override
         public void updateTargetWorkDuration(long targetDurationNanos) {
             synchronized (this) {
-                if (mHalSessionPtr == 0 || !mUpdateAllowed) {
+                if (mHalSessionPtr == 0 || !mUpdateAllowed || mShouldForcePause) {
                     return;
                 }
                 Preconditions.checkArgument(targetDurationNanos > 0, "Expected"
@@ -534,7 +781,7 @@
         @Override
         public void reportActualWorkDuration(long[] actualDurationNanos, long[] timeStampNanos) {
             synchronized (this) {
-                if (mHalSessionPtr == 0 || !mUpdateAllowed) {
+                if (mHalSessionPtr == 0 || !mUpdateAllowed || mShouldForcePause) {
                     return;
                 }
                 Preconditions.checkArgument(actualDurationNanos.length != 0, "the count"
@@ -581,12 +828,25 @@
                 if (sessionSet.isEmpty()) tokenMap.remove(mToken);
                 if (tokenMap.isEmpty()) mActiveSessions.remove(mUid);
             }
+            if (powerhintThreadCleanup()) {
+                synchronized (mNonIsolatedTidsLock) {
+                    final int[] tids = getTidsInternal();
+                    for (int tid : tids) {
+                        if (mNonIsolatedTids.containsKey(tid)) {
+                            mNonIsolatedTids.get(tid).remove(mHalSessionPtr);
+                            if (mNonIsolatedTids.get(tid).isEmpty()) {
+                                mNonIsolatedTids.remove(tid);
+                            }
+                        }
+                    }
+                }
+            }
         }
 
         @Override
         public void sendHint(@PerformanceHintManager.Session.Hint int hint) {
             synchronized (this) {
-                if (mHalSessionPtr == 0 || !mUpdateAllowed) {
+                if (mHalSessionPtr == 0 || !mUpdateAllowed || mShouldForcePause) {
                     return;
                 }
                 Preconditions.checkArgument(hint >= 0, "the hint ID value should be"
@@ -596,33 +856,60 @@
         }
 
         public void setThreads(@NonNull int[] tids) {
+            setThreadsInternal(tids, true);
+        }
+
+        private void setThreadsInternal(int[] tids, boolean checkTid) {
+            if (tids.length == 0) {
+                throw new IllegalArgumentException("Thread id list can't be empty.");
+            }
+
             synchronized (this) {
                 if (mHalSessionPtr == 0) {
                     return;
                 }
-                if (tids.length == 0) {
-                    throw new IllegalArgumentException("Thread id list can't be empty.");
-                }
-                final int callingUid = Binder.getCallingUid();
-                final int callingTgid = Process.getThreadGroupLeader(Binder.getCallingPid());
-                final long identity = Binder.clearCallingIdentity();
-                try {
-                    final Integer invalidTid = checkTidValid(callingUid, callingTgid, tids);
-                    if (invalidTid != null) {
-                        final String errMsg = formatTidCheckErrMsg(callingUid, tids, invalidTid);
-                        Slogf.w(TAG, errMsg);
-                        throw new SecurityException(errMsg);
-                    }
-                } finally {
-                    Binder.restoreCallingIdentity(identity);
-                }
                 if (!mUpdateAllowed) {
                     Slogf.v(TAG, "update hint not allowed, storing tids.");
                     mNewThreadIds = tids;
+                    mShouldForcePause = false;
                     return;
                 }
+                if (checkTid) {
+                    final int callingUid = Binder.getCallingUid();
+                    final int callingTgid = Process.getThreadGroupLeader(Binder.getCallingPid());
+                    final IntArray nonIsolated = powerhintThreadCleanup() ? new IntArray() : null;
+                    final long identity = Binder.clearCallingIdentity();
+                    try {
+                        final Integer invalidTid = checkTidValid(callingUid, callingTgid, tids,
+                                nonIsolated);
+                        if (invalidTid != null) {
+                            final String errMsg = formatTidCheckErrMsg(callingUid, tids,
+                                    invalidTid);
+                            Slogf.w(TAG, errMsg);
+                            throw new SecurityException(errMsg);
+                        }
+                        if (powerhintThreadCleanup()) {
+                            synchronized (mNonIsolatedTidsLock) {
+                                for (int i = nonIsolated.size() - 1; i >= 0; i--) {
+                                    mNonIsolatedTids.putIfAbsent(nonIsolated.get(i),
+                                            new ArraySet<>());
+                                    mNonIsolatedTids.get(nonIsolated.get(i)).add(mHalSessionPtr);
+                                }
+                            }
+                        }
+                    } finally {
+                        Binder.restoreCallingIdentity(identity);
+                    }
+                }
                 mNativeWrapper.halSetThreads(mHalSessionPtr, tids);
                 mThreadIds = tids;
+                mNewThreadIds = null;
+                // if the update is allowed but the session is force paused by tid clean up, then
+                // it's waiting for this tid update to resume
+                if (mShouldForcePause) {
+                    resume();
+                    mShouldForcePause = false;
+                }
             }
         }
 
@@ -632,10 +919,24 @@
             }
         }
 
+        @VisibleForTesting
+        int[] getTidsInternal() {
+            synchronized (this) {
+                return mNewThreadIds != null ? Arrays.copyOf(mNewThreadIds, mNewThreadIds.length)
+                        : Arrays.copyOf(mThreadIds, mThreadIds.length);
+            }
+        }
+
+        boolean isClosed() {
+            synchronized (this) {
+                return mHalSessionPtr == 0;
+            }
+        }
+
         @Override
         public void setMode(int mode, boolean enabled) {
             synchronized (this) {
-                if (mHalSessionPtr == 0 || !mUpdateAllowed) {
+                if (mHalSessionPtr == 0 || !mUpdateAllowed || mShouldForcePause) {
                     return;
                 }
                 Preconditions.checkArgument(mode >= 0, "the mode Id value should be"
@@ -650,13 +951,13 @@
         @Override
         public void reportActualWorkDuration2(WorkDuration[] workDurations) {
             synchronized (this) {
-                if (mHalSessionPtr == 0 || !mUpdateAllowed) {
+                if (mHalSessionPtr == 0 || !mUpdateAllowed || mShouldForcePause) {
                     return;
                 }
                 Preconditions.checkArgument(workDurations.length != 0, "the count"
                         + " of work durations shouldn't be 0.");
-                for (WorkDuration workDuration : workDurations) {
-                    validateWorkDuration(workDuration);
+                for (int i = 0; i < workDurations.length; i++) {
+                    validateWorkDuration(workDurations[i]);
                 }
                 mNativeWrapper.halReportActualWorkDuration(mHalSessionPtr, workDurations);
             }
@@ -743,6 +1044,7 @@
                 pw.println(prefix + "SessionTIDs: " + Arrays.toString(mThreadIds));
                 pw.println(prefix + "SessionTargetDurationNanos: " + mTargetDurationNanos);
                 pw.println(prefix + "SessionAllowed: " + mUpdateAllowed);
+                pw.println(prefix + "SessionForcePaused: " + mShouldForcePause);
                 pw.println(prefix + "PowerEfficient: " + (mPowerEfficient ? "true" : "false"));
             }
         }
diff --git a/services/core/java/com/android/server/power/hint/flags.aconfig b/services/core/java/com/android/server/power/hint/flags.aconfig
new file mode 100644
index 0000000..f4afcb1
--- /dev/null
+++ b/services/core/java/com/android/server/power/hint/flags.aconfig
@@ -0,0 +1,8 @@
+package: "com.android.server.power.hint"
+
+flag {
+    name: "powerhint_thread_cleanup"
+    namespace: "game"
+    description: "Feature flag for auto PowerHintSession dead thread cleanup"
+    bug: "296160319"
+}
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
index f7c236a..2ff3861 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
@@ -267,7 +267,7 @@
     void removeQsTile(ComponentName tile);
 
     /**
-     * Called when requested to enter desktop from an app.
+     * Called when requested to enter desktop from a focused app.
      */
-    void enterDesktop(int displayId);
+    void moveFocusedTaskToDesktop(int displayId);
 }
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index 7b3e237..cca5beb 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -838,15 +838,17 @@
                 } catch (RemoteException ex) { }
             }
         }
+
         @Override
-        public void enterDesktop(int displayId) {
+        public void moveFocusedTaskToDesktop(int displayId) {
             IStatusBar bar = mBar;
             if (bar != null) {
                 try {
-                    bar.enterDesktop(displayId);
+                    bar.moveFocusedTaskToDesktop(displayId);
                 } catch (RemoteException ex) { }
             }
         }
+
         @Override
         public void showMediaOutputSwitcher(String packageName) {
             IStatusBar bar = mBar;
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 060f1c8..6af496f 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -5682,29 +5682,6 @@
         throw e;
     }
 
-    /**
-     * Sets the corresponding {@link DisplayArea} information for the process global
-     * configuration. To be called when we need to show IME on a different {@link DisplayArea}
-     * or display.
-     *
-     * @param pid The process id associated with the IME window.
-     * @param imeContainer The DisplayArea that contains the IME window.
-     */
-    void onImeWindowSetOnDisplayArea(final int pid, @NonNull final DisplayArea imeContainer) {
-        if (pid == MY_PID || pid < 0) {
-            ProtoLog.w(WM_DEBUG_CONFIGURATION,
-                    "Trying to update display configuration for system/invalid process.");
-            return;
-        }
-        final WindowProcessController process = mProcessMap.getProcess(pid);
-        if (process == null) {
-            ProtoLog.w(WM_DEBUG_CONFIGURATION, "Trying to update display "
-                    + "configuration for invalid process, pid=%d", pid);
-            return;
-        }
-        process.registerDisplayAreaConfigurationListener(imeContainer);
-    }
-
     @Override
     public void setRunningRemoteTransitionDelegate(IApplicationThread delegate) {
         final TransitionController controller = getTransitionController();
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index eb1f052..46d4ce4 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -4171,11 +4171,6 @@
      */
     void setInputMethodWindowLocked(WindowState win) {
         mInputMethodWindow = win;
-        // Update display configuration for IME process.
-        if (mInputMethodWindow != null) {
-            final int imePid = mInputMethodWindow.mSession.mPid;
-            mAtmService.onImeWindowSetOnDisplayArea(imePid, mImeWindowsContainer);
-        }
         mInsetsStateController.getImeSourceProvider().setWindowContainer(win,
                 mDisplayPolicy.getImeSourceFrameProvider(), null);
         computeImeTarget(true /* updateImeTarget */);
diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java
index 30134d8..e157318 100644
--- a/services/core/java/com/android/server/wm/Session.java
+++ b/services/core/java/com/android/server/wm/Session.java
@@ -283,14 +283,14 @@
             int lastSyncSeqId, ClientWindowFrames outFrames,
             MergedConfiguration mergedConfiguration, SurfaceControl outSurfaceControl,
             InsetsState outInsetsState, InsetsSourceControl.Array outActiveControls,
-            Bundle outSyncSeqIdBundle) {
+            Bundle outBundle) {
         if (false) Slog.d(TAG_WM, ">>>>>> ENTERED relayout from "
                 + Binder.getCallingPid());
         Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, mRelayoutTag);
         int res = mService.relayoutWindow(this, window, attrs,
                 requestedWidth, requestedHeight, viewFlags, flags, seq,
                 lastSyncSeqId, outFrames, mergedConfiguration, outSurfaceControl, outInsetsState,
-                outActiveControls, outSyncSeqIdBundle);
+                outActiveControls, outBundle);
         Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
         if (false) Slog.d(TAG_WM, "<<<<<< EXITING relayout to "
                 + Binder.getCallingPid());
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 4c282bd..18d2718 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -6822,8 +6822,8 @@
      * A decor surface is requested by a {@link TaskFragmentOrganizer} and is placed below children
      * windows in the Task except for own Activities and TaskFragments in fully trusted mode. The
      * decor surface is created and shared with the client app with
-     * {@link android.window.TaskFragmentOperation#OP_TYPE_CREATE_TASK_FRAGMENT_DECOR_SURFACE} and
-     * be removed with
+     * {@link android.window.TaskFragmentOperation#OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE}
+     * and be removed with
      * {@link android.window.TaskFragmentOperation#OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE}.
      *
      * When boosted with
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 66c2e53..319e2b0 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -2468,7 +2468,15 @@
             for (WindowContainer<?> p = getAnimatableParent(wc); p != null;
                     p = getAnimatableParent(p)) {
                 final ChangeInfo parentChange = changes.get(p);
-                if (parentChange == null || !parentChange.hasChanged()) break;
+                if (parentChange == null) {
+                    break;
+                }
+                if (!parentChange.hasChanged()) {
+                    // In case the target is collected after the parent has been changed, it could
+                    // be too late to snapshot the parent change. Skip to see if there is any
+                    // parent window further up to be considered as change parent.
+                    continue;
+                }
                 if (p.mRemoteToken == null) {
                     // Intermediate parents must be those that has window to be managed by Shell.
                     continue;
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 2934574..0effa6c 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -304,6 +304,7 @@
 import android.view.displayhash.DisplayHash;
 import android.view.displayhash.VerifiedDisplayHash;
 import android.view.inputmethod.ImeTracker;
+import android.window.ActivityWindowInfo;
 import android.window.AddToSurfaceSyncGroupResult;
 import android.window.ClientWindowFrames;
 import android.window.IGlobalDragListener;
@@ -794,6 +795,8 @@
                 Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE);
         private final Uri mImmersiveModeConfirmationsUri =
                 Settings.Secure.getUriFor(Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS);
+        private final Uri mDisableSecureWindowsUri =
+                Settings.Secure.getUriFor(Settings.Secure.DISABLE_SECURE_WINDOWS);
         private final Uri mPolicyControlUri =
                 Settings.Global.getUriFor(Settings.Global.POLICY_CONTROL);
         private final Uri mForceDesktopModeOnExternalDisplaysUri = Settings.Global.getUriFor(
@@ -822,6 +825,8 @@
                     UserHandle.USER_ALL);
             resolver.registerContentObserver(mImmersiveModeConfirmationsUri, false, this,
                     UserHandle.USER_ALL);
+            resolver.registerContentObserver(mDisableSecureWindowsUri, false, this,
+                    UserHandle.USER_ALL);
             resolver.registerContentObserver(mPolicyControlUri, false, this, UserHandle.USER_ALL);
             resolver.registerContentObserver(mForceDesktopModeOnExternalDisplaysUri, false, this,
                     UserHandle.USER_ALL);
@@ -876,6 +881,11 @@
                 return;
             }
 
+            if (mDisableSecureWindowsUri.equals(uri)) {
+                updateDisableSecureWindows();
+                return;
+            }
+
             @UpdateAnimationScaleMode
             final int mode;
             if (mWindowAnimationScaleUri.equals(uri)) {
@@ -895,6 +905,7 @@
         void loadSettings() {
             updateSystemUiSettings(false /* handleChange */);
             updateMaximumObscuringOpacityForTouch();
+            updateDisableSecureWindows();
         }
 
         void updateMaximumObscuringOpacityForTouch() {
@@ -977,6 +988,28 @@
                 });
             }
         }
+
+        void updateDisableSecureWindows() {
+            if (!SystemProperties.getBoolean(SYSTEM_DEBUGGABLE, false)) {
+                return;
+            }
+
+            final boolean disableSecureWindows;
+            try {
+                disableSecureWindows = Settings.Secure.getIntForUser(mContext.getContentResolver(),
+                        Settings.Secure.DISABLE_SECURE_WINDOWS, 0) != 0;
+            } catch (Settings.SettingNotFoundException e) {
+                return;
+            }
+            if (mDisableSecureWindows == disableSecureWindows) {
+                return;
+            }
+
+            synchronized (mGlobalLock) {
+                mDisableSecureWindows = disableSecureWindows;
+                mRoot.refreshSecureSurfaceState();
+            }
+        }
     }
 
     PowerManager mPowerManager;
@@ -1115,6 +1148,8 @@
 
     private final ScreenRecordingCallbackController mScreenRecordingCallbackController;
 
+    private volatile boolean mDisableSecureWindows = false;
+
     public static WindowManagerService main(final Context context, final InputManagerService im,
             final boolean showBootMsgs, WindowManagerPolicy policy,
             ActivityTaskManagerService atm) {
@@ -2213,7 +2248,7 @@
             int lastSyncSeqId, ClientWindowFrames outFrames,
             MergedConfiguration outMergedConfiguration, SurfaceControl outSurfaceControl,
             InsetsState outInsetsState, InsetsSourceControl.Array outActiveControls,
-            Bundle outSyncIdBundle) {
+            Bundle outBundle) {
         if (outActiveControls != null) {
             outActiveControls.set(null);
         }
@@ -2544,6 +2579,13 @@
             if (outFrames != null && outMergedConfiguration != null) {
                 win.fillClientWindowFramesAndConfiguration(outFrames, outMergedConfiguration,
                         false /* useLatestConfig */, shouldRelayout);
+                if (Flags.activityWindowInfoFlag() && outBundle != null
+                        && win.mActivityRecord != null) {
+                    final ActivityWindowInfo activityWindowInfo = win.mActivityRecord
+                            .getActivityWindowInfo();
+                    outBundle.putParcelable(IWindowSession.KEY_RELAYOUT_BUNDLE_ACTIVITY_WINDOW_INFO,
+                            activityWindowInfo);
+                }
 
                 // Set resize-handled here because the values are sent back to the client.
                 win.onResizeHandled();
@@ -2573,7 +2615,7 @@
                         win.isVisible() /* visible */, false /* removed */);
             }
 
-            if (outSyncIdBundle != null) {
+            if (outBundle != null) {
                 final int maybeSyncSeqId;
                 if (win.syncNextBuffer() && viewVisibility == View.VISIBLE
                         && win.mSyncSeqId > lastSyncSeqId) {
@@ -2582,7 +2624,7 @@
                 } else {
                     maybeSyncSeqId = -1;
                 }
-                outSyncIdBundle.putInt("seqid", maybeSyncSeqId);
+                outBundle.putInt(IWindowSession.KEY_RELAYOUT_BUNDLE_SEQID, maybeSyncSeqId);
             }
 
             if (configChanged) {
@@ -6897,6 +6939,7 @@
                     pw.print(mLastFinishedFreezeSource);
                 }
                 pw.println();
+        pw.print("  mDisableSecureWindows="); pw.println(mDisableSecureWindows);
 
         mInputManagerCallback.dump(pw, "  ");
         mSnapshotController.dump(pw, " ");
@@ -10068,4 +10111,8 @@
             mDragDropController.setGlobalDragListener(listener);
         }
     }
+
+    boolean getDisableSecureWindows() {
+        return mDisableSecureWindows;
+    }
 }
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index d967cde..14ec41f 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -23,7 +23,7 @@
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.window.TaskFragmentOperation.OP_TYPE_CLEAR_ADJACENT_TASK_FRAGMENTS;
 import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_TASK_FRAGMENT;
-import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_TASK_FRAGMENT_DECOR_SURFACE;
+import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE;
 import static android.window.TaskFragmentOperation.OP_TYPE_DELETE_TASK_FRAGMENT;
 import static android.window.TaskFragmentOperation.OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE;
 import static android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_BOTTOM_OF_TASK;
@@ -1558,7 +1558,7 @@
                 }
                 break;
             }
-            case OP_TYPE_CREATE_TASK_FRAGMENT_DECOR_SURFACE: {
+            case OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE: {
                 taskFragment.getTask().moveOrCreateDecorSurfaceFor(taskFragment);
                 break;
             }
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index c0cf97d..37b2d0e 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -240,6 +240,7 @@
 import android.view.animation.AnimationUtils;
 import android.view.animation.Interpolator;
 import android.view.inputmethod.ImeTracker;
+import android.window.ActivityWindowInfo;
 import android.window.ClientWindowFrames;
 import android.window.OnBackInvokedCallbackInfo;
 
@@ -1898,6 +1899,10 @@
     }
 
     boolean isSecureLocked() {
+        if (mWmService.getDisableSecureWindows()) {
+            return false;
+        }
+
         if ((mAttrs.flags & WindowManager.LayoutParams.FLAG_SECURE) != 0) {
             return true;
         }
@@ -3692,19 +3697,32 @@
 
         markRedrawForSyncReported();
 
+        // App window resize may trigger Activity#onConfigurationChanged, so we need to update
+        // ActivityWindowInfo as well.
+        final IBinder activityToken;
+        final ActivityWindowInfo activityWindowInfo;
+        if (Flags.activityWindowInfoFlag() && mActivityRecord != null) {
+            activityToken = mActivityRecord.token;
+            activityWindowInfo = mActivityRecord.getActivityWindowInfo();
+        } else {
+            activityToken = null;
+            activityWindowInfo = null;
+        }
+
         if (Flags.bundleClientTransactionFlag()) {
             getProcess().scheduleClientTransactionItem(
                     WindowStateResizeItem.obtain(mClient, mClientWindowFrames, reportDraw,
                             mLastReportedConfiguration, getCompatInsetsState(), forceRelayout,
                             alwaysConsumeSystemBars, displayId,
-                            syncWithBuffers ? mSyncSeqId : -1, isDragResizing));
+                            syncWithBuffers ? mSyncSeqId : -1, isDragResizing,
+                            activityToken, activityWindowInfo));
             onResizePostDispatched(drawPending, prevRotation, displayId);
         } else {
             // TODO(b/301870955): cleanup after launch
             try {
                 mClient.resized(mClientWindowFrames, reportDraw, mLastReportedConfiguration,
                         getCompatInsetsState(), forceRelayout, alwaysConsumeSystemBars, displayId,
-                        syncWithBuffers ? mSyncSeqId : -1, isDragResizing);
+                        syncWithBuffers ? mSyncSeqId : -1, isDragResizing, activityWindowInfo);
                 onResizePostDispatched(drawPending, prevRotation, displayId);
             } catch (RemoteException e) {
                 // Cancel orientation change of this window to avoid blocking unfreeze display.
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 610fcb5..70224db 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -143,6 +143,7 @@
     jmethodID getTouchCalibrationForInputDevice;
     jmethodID notifyDropWindow;
     jmethodID getParentSurfaceForPointers;
+    jmethodID getPackageUid;
 } gServiceClassInfo;
 
 static struct {
@@ -362,6 +363,7 @@
     void notifyDropWindow(const sp<IBinder>& token, float x, float y) override;
     void notifyDeviceInteraction(int32_t deviceId, nsecs_t timestamp,
                                  const std::set<gui::Uid>& uids) override;
+    gui::Uid getPackageUid(std::string package) override;
 
     /* --- PointerControllerPolicyInterface implementation --- */
 
@@ -1116,6 +1118,21 @@
     mInputManager->getMetricsCollector().notifyDeviceInteraction(deviceId, timestamp, uids);
 }
 
+gui::Uid NativeInputManager::getPackageUid(std::string package) {
+    ATRACE_CALL();
+    JNIEnv* env = jniEnv();
+    ScopedLocalFrame localFrame(env);
+
+    ScopedLocalRef<jstring> javaPackage(env, env->NewStringUTF(package.c_str()));
+    const jint uid =
+            env->CallIntMethod(mServiceObj, gServiceClassInfo.getPackageUid, javaPackage.get());
+    if (checkAndClearExceptionFromCallback(env, "getPackageUid")) {
+        LOG(FATAL) << __func__ << ": Failed to get UID for package: " << package;
+    }
+
+    return gui::Uid{static_cast<uint32_t>(uid)};
+}
+
 void NativeInputManager::notifySensorEvent(int32_t deviceId, InputDeviceSensorType sensorType,
                                            InputDeviceSensorAccuracy accuracy, nsecs_t timestamp,
                                            const std::vector<float>& values) {
@@ -3101,6 +3118,8 @@
     GET_METHOD_ID(gServiceClassInfo.getParentSurfaceForPointers, clazz,
                   "getParentSurfaceForPointers", "(I)J");
 
+    GET_METHOD_ID(gServiceClassInfo.getPackageUid, clazz, "getPackageUid", "(Ljava/lang/String;)I");
+
     // InputDevice
 
     FIND_CLASS(gInputDeviceClassInfo.clazz, "android/view/InputDevice");
diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd
index d0df2b2..1f54518 100644
--- a/services/core/xsd/display-device-config/display-device-config.xsd
+++ b/services/core/xsd/display-device-config/display-device-config.xsd
@@ -162,6 +162,10 @@
                 <xs:element type="usiVersion" name="usiVersion">
                     <xs:annotation name="final"/>
                 </xs:element>
+                <xs:element type="lowBrightnessMode" name="lowBrightness">
+                    <xs:attribute name="enabled" type="xs:boolean" use="optional"/>
+                    <xs:annotation name="final"/>
+                </xs:element>
                 <!-- Maximum screen brightness setting when screen brightness capped in
                 Wear Bedtime mode. This must be a non-negative decimal within the range defined by
                 the first and the last brightness value in screenBrightnessMap. -->
@@ -172,6 +176,7 @@
                 <xs:element type="idleScreenRefreshRateTimeout" name="idleScreenRefreshRateTimeout" minOccurs="0">
                     <xs:annotation name="final"/>
                 </xs:element>
+
             </xs:sequence>
         </xs:complexType>
     </xs:element>
@@ -216,6 +221,21 @@
         </xs:restriction>
     </xs:simpleType>
 
+    <xs:complexType name="lowBrightnessMode">
+        <xs:sequence>
+            <xs:element name="transitionPoint" type="nonNegativeDecimal" minOccurs="1"
+                maxOccurs="1">
+            </xs:element>
+            <xs:element name="nits" type="xs:float" maxOccurs="unbounded">
+            </xs:element>
+            <xs:element name="backlight" type="xs:float" maxOccurs="unbounded">
+            </xs:element>
+            <xs:element name="brightness" type="xs:float" maxOccurs="unbounded">
+            </xs:element>
+        </xs:sequence>
+        <xs:attribute name="enabled" type="xs:boolean" use="optional"/>
+    </xs:complexType>
+
     <xs:complexType name="highBrightnessMode">
         <xs:all>
             <xs:element name="transitionPoint" type="nonNegativeDecimal" minOccurs="1"
diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt
index 00dc908..c39c3d7 100644
--- a/services/core/xsd/display-device-config/schema/current.txt
+++ b/services/core/xsd/display-device-config/schema/current.txt
@@ -113,6 +113,7 @@
     method public com.android.server.display.config.HighBrightnessMode getHighBrightnessMode();
     method public final com.android.server.display.config.IdleScreenRefreshRateTimeout getIdleScreenRefreshRateTimeout();
     method public final com.android.server.display.config.SensorDetails getLightSensor();
+    method public final com.android.server.display.config.LowBrightnessMode getLowBrightness();
     method public com.android.server.display.config.LuxThrottling getLuxThrottling();
     method @Nullable public final String getName();
     method public com.android.server.display.config.PowerThrottlingConfig getPowerThrottlingConfig();
@@ -149,6 +150,7 @@
     method public void setHighBrightnessMode(com.android.server.display.config.HighBrightnessMode);
     method public final void setIdleScreenRefreshRateTimeout(com.android.server.display.config.IdleScreenRefreshRateTimeout);
     method public final void setLightSensor(com.android.server.display.config.SensorDetails);
+    method public final void setLowBrightness(com.android.server.display.config.LowBrightnessMode);
     method public void setLuxThrottling(com.android.server.display.config.LuxThrottling);
     method public final void setName(@Nullable String);
     method public void setPowerThrottlingConfig(com.android.server.display.config.PowerThrottlingConfig);
@@ -248,6 +250,17 @@
     method public java.util.List<java.math.BigInteger> getItem();
   }
 
+  public class LowBrightnessMode {
+    ctor public LowBrightnessMode();
+    method public java.util.List<java.lang.Float> getBacklight();
+    method public java.util.List<java.lang.Float> getBrightness();
+    method public boolean getEnabled();
+    method public java.util.List<java.lang.Float> getNits();
+    method public java.math.BigDecimal getTransitionPoint();
+    method public void setEnabled(boolean);
+    method public void setTransitionPoint(java.math.BigDecimal);
+  }
+
   public class LuxThrottling {
     ctor public LuxThrottling();
     method @NonNull public final java.util.List<com.android.server.display.config.BrightnessLimitMap> getBrightnessLimitMap();
diff --git a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
index 173cb36..cac42b1 100644
--- a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
@@ -112,7 +112,8 @@
                                     Manifest.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS),
                             /*defaultProviderId=*/flattenedPrimaryProviders,
                             /*isShowAllOptionsRequested=*/ false),
-                    providerDataList);
+                    providerDataList,
+                    mRequestSessionMetric);
             mClientCallback.onPendingIntent(mPendingIntent);
         } catch (RemoteException e) {
             mRequestSessionMetric.collectUiReturnedFinalPhase(/*uiReturned=*/ false);
diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java
index f5e1e41..24f6697 100644
--- a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java
+++ b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java
@@ -25,6 +25,7 @@
 import android.credentials.CredentialManager;
 import android.credentials.CredentialProviderInfo;
 import android.credentials.selection.DisabledProviderData;
+import android.credentials.selection.IntentCreationResult;
 import android.credentials.selection.IntentFactory;
 import android.credentials.selection.ProviderData;
 import android.credentials.selection.RequestInfo;
@@ -37,6 +38,8 @@
 import android.os.UserHandle;
 import android.service.credentials.CredentialProviderInfoFactory;
 
+import com.android.server.credentials.metrics.RequestSessionMetric;
+
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
@@ -159,7 +162,8 @@
      * @param providerDataList       the list of provider data from remote providers
      */
     public PendingIntent createPendingIntent(
-            RequestInfo requestInfo, ArrayList<ProviderData> providerDataList) {
+            RequestInfo requestInfo, ArrayList<ProviderData> providerDataList,
+            RequestSessionMetric requestSessionMetric) {
         List<CredentialProviderInfo> allProviders =
                 CredentialProviderInfoFactory.getCredentialProviderServices(
                         mContext,
@@ -174,10 +178,12 @@
                 .map(disabledProvider -> new DisabledProviderData(
                         disabledProvider.getComponentName().flattenToString())).toList();
 
-        Intent intent;
-        intent = IntentFactory.createCredentialSelectorIntent(
-                mContext, requestInfo, providerDataList,
-                new ArrayList<>(disabledProviderDataList), mResultReceiver);
+        IntentCreationResult intentCreationResult = IntentFactory
+                .createCredentialSelectorIntentForCredMan(mContext, requestInfo, providerDataList,
+                        new ArrayList<>(disabledProviderDataList), mResultReceiver);
+        requestSessionMetric.collectUiConfigurationResults(
+                mContext, intentCreationResult, mUserId);
+        Intent intent = intentCreationResult.getIntent();
         intent.setAction(UUID.randomUUID().toString());
         //TODO: Create unique pending intent using request code and cancel any pre-existing pending
         // intents
@@ -197,10 +203,15 @@
      * of the pinned entry.
      *
      * @param requestInfo            the information about the request
+     * @param requestSessionMetric   the metric object for logging
      */
-    public Intent createIntentForAutofill(RequestInfo requestInfo) {
-        return IntentFactory.createCredentialSelectorIntentForAutofill(
-                mContext, requestInfo, new ArrayList<>(),
-                mResultReceiver);
+    public Intent createIntentForAutofill(RequestInfo requestInfo,
+            RequestSessionMetric requestSessionMetric) {
+        IntentCreationResult intentCreationResult = IntentFactory
+                .createCredentialSelectorIntentForAutofill(mContext, requestInfo, new ArrayList<>(),
+                        mResultReceiver);
+        requestSessionMetric.collectUiConfigurationResults(
+                mContext, intentCreationResult, mUserId);
+        return intentCreationResult.getIntent();
     }
 }
diff --git a/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java b/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java
index eff53de..fd2a9a2 100644
--- a/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java
@@ -122,7 +122,8 @@
                         mRequestId, mClientRequest, mClientAppInfo.getPackageName(),
                         PermissionUtils.hasPermission(mContext, mClientAppInfo.getPackageName(),
                                 Manifest.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS),
-                        /*isShowAllOptionsRequested=*/ true));
+                        /*isShowAllOptionsRequested=*/ true),
+                mRequestSessionMetric);
 
         List<GetCredentialProviderData> candidateProviderDataList = new ArrayList<>();
         for (ProviderData providerData : providerDataList) {
diff --git a/services/credentials/java/com/android/server/credentials/GetRequestSession.java b/services/credentials/java/com/android/server/credentials/GetRequestSession.java
index 6513ae1a..d55d8ef 100644
--- a/services/credentials/java/com/android/server/credentials/GetRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/GetRequestSession.java
@@ -111,7 +111,8 @@
                                         Manifest.permission
                                                 .CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS),
                                 /*isShowAllOptionsRequested=*/ false),
-                        providerDataList);
+                        providerDataList,
+                        mRequestSessionMetric);
                 mClientCallback.onPendingIntent(mPendingIntent);
             } catch (RemoteException e) {
                 mRequestSessionMetric.collectUiReturnedFinalPhase(/*uiReturned=*/ false);
diff --git a/services/credentials/java/com/android/server/credentials/MetricUtilities.java b/services/credentials/java/com/android/server/credentials/MetricUtilities.java
index bdea4f9..16bf1778 100644
--- a/services/credentials/java/com/android/server/credentials/MetricUtilities.java
+++ b/services/credentials/java/com/android/server/credentials/MetricUtilities.java
@@ -16,6 +16,7 @@
 
 package com.android.server.credentials;
 
+import android.annotation.UserIdInt;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.PackageManager;
@@ -68,17 +69,27 @@
      *
      * @return the uid of a given package
      */
-    protected static int getPackageUid(Context context, ComponentName componentName) {
-        int sessUid = -1;
-        try {
-            // Only for T and above, which is fine for our use case
-            sessUid = context.getPackageManager().getApplicationInfo(
-                    componentName.getPackageName(),
-                    PackageManager.ApplicationInfoFlags.of(0)).uid;
-        } catch (Throwable t) {
-            Slog.i(TAG, "Couldn't find required uid");
+    protected static int getPackageUid(Context context, ComponentName componentName,
+            @UserIdInt int userId) {
+        if (componentName == null) {
+            return -1;
         }
-        return sessUid;
+        return getPackageUid(context, componentName.getPackageName(), userId);
+    }
+
+    /** Returns the package uid, or -1 if not found. */
+    public static int getPackageUid(Context context, String packageName,
+            @UserIdInt int userId) {
+        if (packageName == null) {
+            return -1;
+        }
+        try {
+            return context.getPackageManager().getPackageUidAsUser(packageName,
+                    PackageManager.PackageInfoFlags.of(0), userId);
+        } catch (Throwable t) {
+            Slog.i(TAG, "Couldn't find uid for " + packageName + ": " + t);
+            return -1;
+        }
     }
 
     /**
diff --git a/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java b/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java
index 6e8f7c8..e4b5c77 100644
--- a/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java
@@ -193,7 +193,8 @@
                             PermissionUtils.hasPermission(mContext, mClientAppInfo.getPackageName(),
                                     Manifest.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS),
                             /*isShowAllOptionsRequested=*/ false),
-                    providerDataList);
+                    providerDataList,
+                    mRequestSessionMetric);
         } else {
             return null;
         }
diff --git a/services/credentials/java/com/android/server/credentials/ProviderSession.java b/services/credentials/java/com/android/server/credentials/ProviderSession.java
index c16e232..dfc08f0 100644
--- a/services/credentials/java/com/android/server/credentials/ProviderSession.java
+++ b/services/credentials/java/com/android/server/credentials/ProviderSession.java
@@ -153,7 +153,7 @@
         mUserId = userId;
         mComponentName = componentName;
         mRemoteCredentialService = remoteCredentialService;
-        mProviderSessionUid = MetricUtilities.getPackageUid(mContext, mComponentName);
+        mProviderSessionUid = MetricUtilities.getPackageUid(mContext, mComponentName, userId);
         mProviderSessionMetric = new ProviderSessionMetric(
                 ((RequestSession) mCallbacks).mRequestSessionMetric.getSessionIdTrackTwo());
     }
diff --git a/services/credentials/java/com/android/server/credentials/metrics/OemUiUsageStatus.java b/services/credentials/java/com/android/server/credentials/metrics/OemUiUsageStatus.java
index 2fd3a86..80ce354 100644
--- a/services/credentials/java/com/android/server/credentials/metrics/OemUiUsageStatus.java
+++ b/services/credentials/java/com/android/server/credentials/metrics/OemUiUsageStatus.java
@@ -22,7 +22,12 @@
 import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_FINAL_NO_UID_REPORTED__OEM_UI_USAGE_STATUS__OEM_UI_USAGE_STATUS_SPECIFIED_BUT_NOT_FOUND;
 import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_FINAL_NO_UID_REPORTED__OEM_UI_USAGE_STATUS__OEM_UI_USAGE_STATUS_SPECIFIED_BUT_NOT_ENABLED;
 
+import android.credentials.selection.IntentCreationResult;
 
+/**
+ * Result of attempting to use the config_oemCredentialManagerDialogComponent as the Credential
+ * Manager UI.
+ */
 public enum OemUiUsageStatus {
     UNKNOWN(CREDENTIAL_MANAGER_FINAL_NO_UID_REPORTED__OEM_UI_USAGE_STATUS__OEM_UI_USAGE_STATUS_UNKNOWN),
     SUCCESS(CREDENTIAL_MANAGER_FINAL_NO_UID_REPORTED__OEM_UI_USAGE_STATUS__OEM_UI_USAGE_STATUS_SUCCESS),
@@ -39,4 +44,21 @@
     public int getLoggingInt() {
         return mLoggingInt;
     }
+
+    /** Factory method. */
+    public static OemUiUsageStatus createFrom(IntentCreationResult.OemUiUsageStatus from) {
+        switch (from) {
+            case UNKNOWN:
+                return OemUiUsageStatus.UNKNOWN;
+            case SUCCESS:
+                return OemUiUsageStatus.SUCCESS;
+            case OEM_UI_CONFIG_NOT_SPECIFIED:
+                return OemUiUsageStatus.FAILURE_NOT_SPECIFIED;
+            case OEM_UI_CONFIG_SPECIFIED_BUT_NOT_FOUND:
+                return OemUiUsageStatus.FAILURE_SPECIFIED_BUT_NOT_FOUND;
+            case OEM_UI_CONFIG_SPECIFIED_FOUND_BUT_NOT_ENABLED:
+                return OemUiUsageStatus.FAILURE_SPECIFIED_BUT_NOT_ENABLED;
+        }
+        return OemUiUsageStatus.UNKNOWN;
+    }
 }
diff --git a/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java b/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java
index a77bd3e..619a568 100644
--- a/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java
+++ b/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java
@@ -30,9 +30,12 @@
 import static com.android.server.credentials.metrics.ApiName.GET_CREDENTIAL_VIA_REGISTRY;
 
 import android.annotation.NonNull;
+import android.annotation.UserIdInt;
 import android.content.ComponentName;
+import android.content.Context;
 import android.credentials.CreateCredentialRequest;
 import android.credentials.GetCredentialRequest;
+import android.credentials.selection.IntentCreationResult;
 import android.credentials.selection.UserSelectionDialogResult;
 import android.util.Slog;
 
@@ -270,6 +273,21 @@
         }
     }
 
+    /** Log results of the device Credential Manager UI configuration. */
+    public void collectUiConfigurationResults(Context context, IntentCreationResult result,
+            @UserIdInt int userId) {
+        try {
+            mChosenProviderFinalPhaseMetric.setOemUiUid(MetricUtilities.getPackageUid(
+                    context, result.getOemUiPackageName(), userId));
+            mChosenProviderFinalPhaseMetric.setFallbackUiUid(MetricUtilities.getPackageUid(
+                    context, result.getFallbackUiPackageName(), userId));
+            mChosenProviderFinalPhaseMetric.setOemUiUsageStatus(
+                    OemUiUsageStatus.createFrom(result.getOemUiUsageStatus()));
+        } catch (Exception e) {
+            Slog.w(TAG, "Unexpected error during ui configuration result collection: " + e);
+        }
+    }
+
     /**
      * Allows encapsulating the overall final phase metric status from the chosen and final
      * provider.
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 3b2a3dd..e202bbf 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -1230,10 +1230,6 @@
         mSystemServiceManager.startService(ThermalManagerService.class);
         t.traceEnd();
 
-        t.traceBegin("StartHintManager");
-        mSystemServiceManager.startService(HintManagerService.class);
-        t.traceEnd();
-
         // Now that the power manager has been started, let the activity manager
         // initialize power management features.
         t.traceBegin("InitPowerManagement");
@@ -1614,6 +1610,10 @@
                 t.traceEnd();
             }
 
+            t.traceBegin("StartHintManager");
+            mSystemServiceManager.startService(HintManagerService.class);
+            t.traceEnd();
+
             // Grants default permissions and defines roles
             t.traceBegin("StartRoleManagerService");
             LocalManagerRegistry.addManager(RoleServicePlatformHelper.class,
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
index b9c5b36..b4cf799 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
@@ -203,6 +203,7 @@
                 .thenReturn(new int[] {0});
         when(mMockUserManagerInternal.getUserIds()).thenReturn(new int[] {0});
         when(mMockActivityManagerInternal.isSystemReady()).thenReturn(true);
+        when(mMockActivityManagerInternal.getCurrentUserId()).thenReturn(mCallingUserId);
         when(mMockPackageManagerInternal.getPackageUid(anyString(), anyLong(), anyInt()))
                 .thenReturn(Binder.getCallingUid());
         when(mMockPackageManagerInternal.isSameApp(anyString(), anyLong(), anyInt(), anyInt()))
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java
index cea65b5..9f46d0ba 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java
@@ -198,7 +198,9 @@
 
     @Test
     public void startInputOrWindowGainedFocus_userNotRunning() throws RemoteException {
-        when(mMockUserManagerInternal.isUserRunning(anyInt())).thenReturn(false);
+        // Run blockingly on ServiceThread to avoid that interfering with our stubbing.
+        mServiceThread.getThreadHandler().runWithScissors(
+                () -> when(mMockUserManagerInternal.isUserRunning(anyInt())).thenReturn(false), 0);
 
         assertThat(
                         startInputOrWindowGainedFocus(
diff --git a/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
index b0f7bfa..54de64e 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
@@ -52,6 +52,7 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.server.display.brightness.clamper.BrightnessClamperController;
 import com.android.server.testutils.OffsettableClock;
 
 import org.junit.After;
@@ -96,6 +97,8 @@
     @Mock HysteresisLevels mScreenBrightnessThresholdsIdle;
     @Mock Handler mNoOpHandler;
     @Mock BrightnessRangeController mBrightnessRangeController;
+    @Mock
+    BrightnessClamperController mBrightnessClamperController;
     @Mock BrightnessThrottler mBrightnessThrottler;
 
     @Before
@@ -161,7 +164,8 @@
                 mAmbientBrightnessThresholdsIdle, mScreenBrightnessThresholdsIdle,
                 mContext, mBrightnessRangeController, mBrightnessThrottler,
                 useHorizon ? AMBIENT_LIGHT_HORIZON_SHORT : 1,
-                useHorizon ? AMBIENT_LIGHT_HORIZON_LONG : 10000, userLux, userNits
+                useHorizon ? AMBIENT_LIGHT_HORIZON_LONG : 10000, userLux, userNits,
+                mBrightnessClamperController
         );
 
         when(mBrightnessRangeController.getCurrentBrightnessMax()).thenReturn(
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
index 35b69f8..73a2f65 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
@@ -44,6 +44,7 @@
 import android.hardware.display.DisplayManagerInternal;
 import android.os.PowerManager;
 import android.os.Temperature;
+import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.provider.Settings;
 import android.util.SparseArray;
 import android.util.Spline;
@@ -57,6 +58,7 @@
 import com.android.server.display.config.IdleScreenRefreshRateTimeoutLuxThresholdPoint;
 import com.android.server.display.config.ThermalStatus;
 import com.android.server.display.feature.DisplayManagerFlags;
+import com.android.server.display.feature.flags.Flags;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -380,7 +382,7 @@
     public void testInvalidLuxThrottling() throws Exception {
         setupDisplayDeviceConfigFromDisplayConfigFile(
                 getContent(getInvalidLuxThrottling(), getValidProxSensor(),
-                        /* includeIdleMode= */ true));
+                        /* includeIdleMode= */ true, /* enableEvenDimmer */ false));
 
         Map<DisplayDeviceConfig.BrightnessLimitMapType, Map<Float, Float>> luxThrottlingData =
                 mDisplayDeviceConfig.getLuxThrottlingData();
@@ -588,7 +590,7 @@
     public void testProximitySensorWithEmptyValuesFromDisplayConfig() throws IOException {
         setupDisplayDeviceConfigFromDisplayConfigFile(
                 getContent(getValidLuxThrottling(), getProxSensorWithEmptyValues(),
-                        /* includeIdleMode= */ true));
+                        /* includeIdleMode= */ true, /* enableEvenDimmer */ false));
         assertNull(mDisplayDeviceConfig.getProximitySensor());
     }
 
@@ -596,7 +598,7 @@
     public void testProximitySensorWithRefreshRatesFromDisplayConfig() throws IOException {
         setupDisplayDeviceConfigFromDisplayConfigFile(
                 getContent(getValidLuxThrottling(), getValidProxSensorWithRefreshRateAndVsyncRate(),
-                        /* includeIdleMode= */ true));
+                        /* includeIdleMode= */ true, /* enableEvenDimmer */ false));
         assertEquals("test_proximity_sensor",
                 mDisplayDeviceConfig.getProximitySensor().type);
         assertEquals("Test Proximity Sensor",
@@ -784,7 +786,7 @@
     @Test
     public void testBrightnessRamps_IdleFallsBackToConfigInteractive() throws IOException {
         setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(),
-                getValidProxSensor(), /* includeIdleMode= */ false));
+                getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ false));
 
         assertEquals(mDisplayDeviceConfig.getBrightnessRampDecreaseMaxMillis(), 3000);
         assertEquals(mDisplayDeviceConfig.getBrightnessRampIncreaseMaxMillis(), 2000);
@@ -801,14 +803,14 @@
     @Test
     public void testBrightnessCapForWearBedtimeMode() throws IOException {
         setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(),
-                getValidProxSensor(), /* includeIdleMode= */ false));
+                getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ false));
         assertEquals(0.1f, mDisplayDeviceConfig.getBrightnessCapForWearBedtimeMode(), ZERO_DELTA);
     }
 
     @Test
     public void testAutoBrightnessBrighteningLevels() throws IOException {
         setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(),
-                getValidProxSensor(), /* includeIdleMode= */ false));
+                getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ false));
 
         assertArrayEquals(new float[]{0.0f, 80},
                 mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(
@@ -871,7 +873,7 @@
         when(mFlags.areAutoBrightnessModesEnabled()).thenReturn(false);
         setupDisplayDeviceConfigFromConfigResourceFile();
         setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(),
-                getValidProxSensor(), /* includeIdleMode= */ false));
+                getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ false));
 
         assertArrayEquals(new float[]{brightnessIntToFloat(50), brightnessIntToFloat(100),
                         brightnessIntToFloat(150)},
@@ -904,6 +906,18 @@
         assertFalse(mDisplayDeviceConfig.isAutoBrightnessAvailable());
     }
 
+    @RequiresFlagsEnabled(Flags.FLAG_EVEN_DIMMER)
+    @Test
+    public void testEvenDimmer() throws IOException {
+        when(mFlags.isEvenDimmerEnabled()).thenReturn(true);
+        setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(),
+                getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ true));
+
+        assertTrue(mDisplayDeviceConfig.getLbmEnabled());
+        assertEquals(0.0001f, mDisplayDeviceConfig.getBacklightFromBrightness(0.1f), ZERO_DELTA);
+        assertEquals(0.2f, mDisplayDeviceConfig.getNitsFromBacklight(0.0f), ZERO_DELTA);
+    }
+
     private String getValidLuxThrottling() {
         return "<luxThrottling>\n"
                 + "    <brightnessLimitMap>\n"
@@ -1229,11 +1243,11 @@
 
     private String getContent() {
         return getContent(getValidLuxThrottling(), getValidProxSensor(),
-                /* includeIdleMode= */ true);
+                /* includeIdleMode= */ true, false);
     }
 
     private String getContent(String brightnessCapConfig, String proxSensor,
-            boolean includeIdleMode) {
+            boolean includeIdleMode, boolean enableEvenDimmer) {
         return "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
                 + "<displayConfiguration>\n"
                 +   "<name>Example Display</name>\n"
@@ -1603,6 +1617,7 @@
                 +       "<majorVersion>2</majorVersion>\n"
                 +       "<minorVersion>0</minorVersion>\n"
                 +   "</usiVersion>\n"
+                + evenDimmerConfig(enableEvenDimmer)
                 +   "<screenBrightnessCapForWearBedtimeMode>"
                 +       "0.1"
                 +   "</screenBrightnessCapForWearBedtimeMode>"
@@ -1621,6 +1636,24 @@
                 + "</displayConfiguration>\n";
     }
 
+    private String evenDimmerConfig(boolean enabled) {
+        return (enabled ? "<lowBrightness enabled=\"true\">" : "<lowBrightness enabled=\"false\">")
+                + "  <transitionPoint>0.1</transitionPoint>\n"
+                + "  <nits>0.2</nits>\n"
+                + "  <nits>2.0</nits>\n"
+                + "  <nits>500.0</nits>\n"
+                + "  <nits>1000.0</nits>\n"
+                + "  <backlight>0</backlight>\n"
+                + "  <backlight>0.0001</backlight>\n"
+                + "  <backlight>0.5</backlight>\n"
+                + "  <backlight>1.0</backlight>\n"
+                + "  <brightness>0</brightness>\n"
+                + "  <brightness>0.1</brightness>\n"
+                + "  <brightness>0.5</brightness>\n"
+                + "  <brightness>1.0</brightness>\n"
+                + "</lowBrightness>";
+    }
+
     private void mockDeviceConfigs() {
         when(mResources.getFloat(com.android.internal.R.dimen
                 .config_screenBrightnessSettingDefaultFloat)).thenReturn(0.5f);
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
index 01598ae..740ffc9 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -1184,7 +1184,8 @@
                 /* ambientLightHorizonShort= */ anyInt(),
                 /* ambientLightHorizonLong= */ anyInt(),
                 eq(lux),
-                eq(nits)
+                eq(nits),
+                any(BrightnessClamperController.class)
         );
     }
 
@@ -2121,7 +2122,8 @@
                 HysteresisLevels screenBrightnessThresholdsIdle, Context context,
                 BrightnessRangeController brightnessRangeController,
                 BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
-                int ambientLightHorizonLong, float userLux, float userNits) {
+                int ambientLightHorizonLong, float userLux, float userNits,
+                BrightnessClamperController brightnessClamperController) {
             return mAutomaticBrightnessController;
         }
 
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt
index ac7d1f5..e4a7d98 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt
@@ -65,7 +65,7 @@
         Settings.Secure.putIntForUser(context.contentResolver,
                 Settings.Secure.EVEN_DIMMER_ACTIVATED, 1, userId)
         Settings.Secure.putFloatForUser(context.contentResolver,
-                Settings.Secure.EVEN_DIMMER_MIN_NITS, 0.7f, userId)
+                Settings.Secure.EVEN_DIMMER_MIN_NITS, 30.0f, userId)
         modifier.recalculateLowerBound()
         testHandler.flush()
         assertThat(modifier.isActive).isTrue()
@@ -81,11 +81,22 @@
                 Settings.Secure.EVEN_DIMMER_ACTIVATED, 1, userId)
         Settings.Secure.putFloatForUser(context.contentResolver,
                 Settings.Secure.EVEN_DIMMER_MIN_NITS, 0.0f, userId)
-        modifier.recalculateLowerBound()
+        modifier.onAmbientLuxChange(3000.0f)
         testHandler.flush()
         assertThat(modifier.isActive).isTrue()
 
         // Test restriction from lux setting
         assertThat(modifier.brightnessReason).isEqualTo(BrightnessReason.MODIFIER_MIN_LUX)
     }
+
+    @Test
+    fun testSettingOffDisablesModifier() {
+        Settings.Secure.putIntForUser(context.contentResolver,
+            Settings.Secure.EVEN_DIMMER_ACTIVATED, 0, userId)
+        assertThat(modifier.brightnessLowerBound).isEqualTo(PowerManager.BRIGHTNESS_MIN)
+        modifier.onAmbientLuxChange(3000.0f)
+        testHandler.flush()
+        assertThat(modifier.isActive).isFalse()
+        assertThat(modifier.brightnessLowerBound).isEqualTo(PowerManager.BRIGHTNESS_MIN)
+    }
 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java b/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java
index c30ac2d..682569f 100644
--- a/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java
@@ -26,6 +26,7 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
 import static com.android.server.RescueParty.LEVEL_FACTORY_RESET;
+import static com.android.server.RescueParty.RESCUE_LEVEL_FACTORY_RESET;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -41,9 +42,11 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.VersionedPackage;
+import android.crashrecovery.flags.Flags;
 import android.os.RecoverySystem;
 import android.os.SystemProperties;
 import android.os.UserHandle;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.provider.DeviceConfig;
 import android.provider.Settings;
 import android.util.ArraySet;
@@ -55,6 +58,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.mockito.Answers;
 import org.mockito.ArgumentCaptor;
@@ -69,6 +73,7 @@
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.Executor;
 import java.util.concurrent.TimeUnit;
 
@@ -100,6 +105,9 @@
 
     private static final int THROTTLING_DURATION_MIN = 10;
 
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     private MockitoSession mSession;
     private HashMap<String, String> mSystemSettingsMap;
     private HashMap<String, String> mCrashRecoveryPropertiesMap;
@@ -267,6 +275,42 @@
     }
 
     @Test
+    public void testBootLoopDetectionWithExecutionForAllRescueLevelsRecoverabilityDetection() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+        RescueParty.onSettingsProviderPublished(mMockContext);
+        verify(() -> DeviceConfig.setMonitorCallback(eq(mMockContentResolver),
+                any(Executor.class),
+                mMonitorCallbackCaptor.capture()));
+        HashMap<String, Integer> verifiedTimesMap = new HashMap<String, Integer>();
+
+        // Record DeviceConfig accesses
+        DeviceConfig.MonitorCallback monitorCallback = mMonitorCallbackCaptor.getValue();
+        monitorCallback.onDeviceConfigAccess(CALLING_PACKAGE1, NAMESPACE1);
+        monitorCallback.onDeviceConfigAccess(CALLING_PACKAGE1, NAMESPACE2);
+
+        final String[] expectedAllResetNamespaces = new String[]{NAMESPACE1, NAMESPACE2};
+
+        noteBoot(1);
+        verifyDeviceConfigReset(expectedAllResetNamespaces, verifiedTimesMap);
+
+        noteBoot(2);
+        assertTrue(RescueParty.isRebootPropertySet());
+
+        noteBoot(3);
+        verifyOnlySettingsReset(Settings.RESET_MODE_UNTRUSTED_DEFAULTS);
+
+        noteBoot(4);
+        verifyOnlySettingsReset(Settings.RESET_MODE_UNTRUSTED_CHANGES);
+
+        noteBoot(5);
+        verifyOnlySettingsReset(Settings.RESET_MODE_TRUSTED_DEFAULTS);
+
+        setCrashRecoveryPropAttemptingReboot(false);
+        noteBoot(6);
+        assertTrue(RescueParty.isFactoryResetPropertySet());
+    }
+
+    @Test
     public void testPersistentAppCrashDetectionWithExecutionForAllRescueLevels() {
         noteAppCrash(1, true);
 
@@ -292,6 +336,47 @@
     }
 
     @Test
+    public void testPersistentAppCrashDetectionWithExecutionForAllRescueLevelsRecoverability() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+        RescueParty.onSettingsProviderPublished(mMockContext);
+        verify(() -> DeviceConfig.setMonitorCallback(eq(mMockContentResolver),
+                any(Executor.class),
+                mMonitorCallbackCaptor.capture()));
+        HashMap<String, Integer> verifiedTimesMap = new HashMap<String, Integer>();
+
+        // Record DeviceConfig accesses
+        DeviceConfig.MonitorCallback monitorCallback = mMonitorCallbackCaptor.getValue();
+        monitorCallback.onDeviceConfigAccess(PERSISTENT_PACKAGE, NAMESPACE1);
+        monitorCallback.onDeviceConfigAccess(CALLING_PACKAGE1, NAMESPACE1);
+        monitorCallback.onDeviceConfigAccess(CALLING_PACKAGE1, NAMESPACE2);
+
+        final String[] expectedResetNamespaces = new String[]{NAMESPACE1};
+        final String[] expectedAllResetNamespaces = new String[]{NAMESPACE1, NAMESPACE2};
+
+        noteAppCrash(1, true);
+        verifyDeviceConfigReset(expectedResetNamespaces, verifiedTimesMap);
+
+        noteAppCrash(2, true);
+        verifyDeviceConfigReset(expectedAllResetNamespaces, verifiedTimesMap);
+
+        noteAppCrash(3, true);
+        assertTrue(RescueParty.isRebootPropertySet());
+
+        noteAppCrash(4, true);
+        verifyOnlySettingsReset(Settings.RESET_MODE_UNTRUSTED_DEFAULTS);
+
+        noteAppCrash(5, true);
+        verifyOnlySettingsReset(Settings.RESET_MODE_UNTRUSTED_CHANGES);
+
+        noteAppCrash(6, true);
+        verifyOnlySettingsReset(Settings.RESET_MODE_TRUSTED_DEFAULTS);
+
+        setCrashRecoveryPropAttemptingReboot(false);
+        noteAppCrash(7, true);
+        assertTrue(RescueParty.isFactoryResetPropertySet());
+    }
+
+    @Test
     public void testNonPersistentAppOnlyPerformsFlagResets() {
         noteAppCrash(1, false);
 
@@ -316,6 +401,45 @@
     }
 
     @Test
+    public void testNonPersistentAppOnlyPerformsFlagResetsRecoverabilityDetection() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+        RescueParty.onSettingsProviderPublished(mMockContext);
+        verify(() -> DeviceConfig.setMonitorCallback(eq(mMockContentResolver),
+                any(Executor.class),
+                mMonitorCallbackCaptor.capture()));
+        HashMap<String, Integer> verifiedTimesMap = new HashMap<String, Integer>();
+
+        // Record DeviceConfig accesses
+        DeviceConfig.MonitorCallback monitorCallback = mMonitorCallbackCaptor.getValue();
+        monitorCallback.onDeviceConfigAccess(NON_PERSISTENT_PACKAGE, NAMESPACE1);
+        monitorCallback.onDeviceConfigAccess(CALLING_PACKAGE1, NAMESPACE1);
+        monitorCallback.onDeviceConfigAccess(CALLING_PACKAGE1, NAMESPACE2);
+
+        final String[] expectedResetNamespaces = new String[]{NAMESPACE1};
+        final String[] expectedAllResetNamespaces = new String[]{NAMESPACE1, NAMESPACE2};
+
+        noteAppCrash(1, false);
+        verifyDeviceConfigReset(expectedResetNamespaces, verifiedTimesMap);
+
+        noteAppCrash(2, false);
+        verifyDeviceConfigReset(expectedAllResetNamespaces, verifiedTimesMap);
+
+        noteAppCrash(3, false);
+        assertFalse(RescueParty.isRebootPropertySet());
+
+        noteAppCrash(4, false);
+        verifyNoSettingsReset(Settings.RESET_MODE_UNTRUSTED_DEFAULTS);
+        noteAppCrash(5, false);
+        verifyNoSettingsReset(Settings.RESET_MODE_UNTRUSTED_CHANGES);
+        noteAppCrash(6, false);
+        verifyNoSettingsReset(Settings.RESET_MODE_TRUSTED_DEFAULTS);
+
+        setCrashRecoveryPropAttemptingReboot(false);
+        noteAppCrash(7, false);
+        assertFalse(RescueParty.isFactoryResetPropertySet());
+    }
+
+    @Test
     public void testNonPersistentAppCrashDetectionWithScopedResets() {
         RescueParty.onSettingsProviderPublished(mMockContext);
         verify(() -> DeviceConfig.setMonitorCallback(eq(mMockContentResolver),
@@ -451,6 +575,19 @@
     }
 
     @Test
+    public void testIsRecoveryTriggeredRebootRecoverabilityDetection() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+        for (int i = 0; i < RESCUE_LEVEL_FACTORY_RESET; i++) {
+            noteBoot(i + 1);
+        }
+        assertFalse(RescueParty.isFactoryResetPropertySet());
+        setCrashRecoveryPropAttemptingReboot(false);
+        noteBoot(RESCUE_LEVEL_FACTORY_RESET + 1);
+        assertTrue(RescueParty.isRecoveryTriggeredReboot());
+        assertTrue(RescueParty.isFactoryResetPropertySet());
+    }
+
+    @Test
     public void testIsRecoveryTriggeredRebootOnlyAfterRebootCompleted() {
         for (int i = 0; i < LEVEL_FACTORY_RESET; i++) {
             noteBoot(i + 1);
@@ -469,6 +606,25 @@
     }
 
     @Test
+    public void testIsRecoveryTriggeredRebootOnlyAfterRebootCompletedRecoverabilityDetection() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+        for (int i = 0; i < RESCUE_LEVEL_FACTORY_RESET; i++) {
+            noteBoot(i + 1);
+        }
+        int mitigationCount = RESCUE_LEVEL_FACTORY_RESET + 1;
+        assertFalse(RescueParty.isFactoryResetPropertySet());
+        noteBoot(mitigationCount++);
+        assertFalse(RescueParty.isFactoryResetPropertySet());
+        noteBoot(mitigationCount++);
+        assertFalse(RescueParty.isFactoryResetPropertySet());
+        noteBoot(mitigationCount++);
+        setCrashRecoveryPropAttemptingReboot(false);
+        noteBoot(mitigationCount + 1);
+        assertTrue(RescueParty.isRecoveryTriggeredReboot());
+        assertTrue(RescueParty.isFactoryResetPropertySet());
+    }
+
+    @Test
     public void testThrottlingOnBootFailures() {
         setCrashRecoveryPropAttemptingReboot(false);
         long now = System.currentTimeMillis();
@@ -481,6 +637,19 @@
     }
 
     @Test
+    public void testThrottlingOnBootFailuresRecoverabilityDetection() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+        setCrashRecoveryPropAttemptingReboot(false);
+        long now = System.currentTimeMillis();
+        long beforeTimeout = now - TimeUnit.MINUTES.toMillis(THROTTLING_DURATION_MIN - 1);
+        setCrashRecoveryPropLastFactoryReset(beforeTimeout);
+        for (int i = 1; i <= RESCUE_LEVEL_FACTORY_RESET; i++) {
+            noteBoot(i);
+        }
+        assertFalse(RescueParty.isRecoveryTriggeredReboot());
+    }
+
+    @Test
     public void testThrottlingOnAppCrash() {
         setCrashRecoveryPropAttemptingReboot(false);
         long now = System.currentTimeMillis();
@@ -493,6 +662,19 @@
     }
 
     @Test
+    public void testThrottlingOnAppCrashRecoverabilityDetection() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+        setCrashRecoveryPropAttemptingReboot(false);
+        long now = System.currentTimeMillis();
+        long beforeTimeout = now - TimeUnit.MINUTES.toMillis(THROTTLING_DURATION_MIN - 1);
+        setCrashRecoveryPropLastFactoryReset(beforeTimeout);
+        for (int i = 0; i <= RESCUE_LEVEL_FACTORY_RESET; i++) {
+            noteAppCrash(i + 1, true);
+        }
+        assertFalse(RescueParty.isRecoveryTriggeredReboot());
+    }
+
+    @Test
     public void testNotThrottlingAfterTimeoutOnBootFailures() {
         setCrashRecoveryPropAttemptingReboot(false);
         long now = System.currentTimeMillis();
@@ -503,6 +685,20 @@
         }
         assertTrue(RescueParty.isRecoveryTriggeredReboot());
     }
+
+    @Test
+    public void testNotThrottlingAfterTimeoutOnBootFailuresRecoverabilityDetection() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+        setCrashRecoveryPropAttemptingReboot(false);
+        long now = System.currentTimeMillis();
+        long afterTimeout = now - TimeUnit.MINUTES.toMillis(THROTTLING_DURATION_MIN + 1);
+        setCrashRecoveryPropLastFactoryReset(afterTimeout);
+        for (int i = 1; i <= RESCUE_LEVEL_FACTORY_RESET; i++) {
+            noteBoot(i);
+        }
+        assertTrue(RescueParty.isRecoveryTriggeredReboot());
+    }
+
     @Test
     public void testNotThrottlingAfterTimeoutOnAppCrash() {
         setCrashRecoveryPropAttemptingReboot(false);
@@ -516,6 +712,19 @@
     }
 
     @Test
+    public void testNotThrottlingAfterTimeoutOnAppCrashRecoverabilityDetection() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+        setCrashRecoveryPropAttemptingReboot(false);
+        long now = System.currentTimeMillis();
+        long afterTimeout = now - TimeUnit.MINUTES.toMillis(THROTTLING_DURATION_MIN + 1);
+        setCrashRecoveryPropLastFactoryReset(afterTimeout);
+        for (int i = 0; i <= RESCUE_LEVEL_FACTORY_RESET; i++) {
+            noteAppCrash(i + 1, true);
+        }
+        assertTrue(RescueParty.isRecoveryTriggeredReboot());
+    }
+
+    @Test
     public void testNativeRescuePartyResets() {
         doReturn(true).when(() -> SettingsToPropertiesMapper.isNativeFlagsResetPerformed());
         doReturn(FAKE_RESET_NATIVE_NAMESPACES).when(
@@ -531,6 +740,7 @@
 
     @Test
     public void testExplicitlyEnablingAndDisablingRescue() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
         SystemProperties.set(RescueParty.PROP_ENABLE_RESCUE, Boolean.toString(false));
         SystemProperties.set(PROP_DISABLE_RESCUE, Boolean.toString(true));
         assertEquals(RescuePartyObserver.getInstance(mMockContext).execute(sFailingPackage,
@@ -543,6 +753,7 @@
 
     @Test
     public void testDisablingRescueByDeviceConfigFlag() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
         SystemProperties.set(RescueParty.PROP_ENABLE_RESCUE, Boolean.toString(false));
         SystemProperties.set(PROP_DEVICE_CONFIG_DISABLE_FLAG, Boolean.toString(true));
 
@@ -568,6 +779,20 @@
     }
 
     @Test
+    public void testDisablingFactoryResetByDeviceConfigFlagRecoverabilityDetection() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+        SystemProperties.set(PROP_DISABLE_FACTORY_RESET_FLAG, Boolean.toString(true));
+
+        for (int i = 0; i < RESCUE_LEVEL_FACTORY_RESET; i++) {
+            noteBoot(i + 1);
+        }
+        assertFalse(RescueParty.isFactoryResetPropertySet());
+
+        // Restore the property value initialized in SetUp()
+        SystemProperties.set(PROP_DISABLE_FACTORY_RESET_FLAG, "");
+    }
+
+    @Test
     public void testHealthCheckLevels() {
         RescuePartyObserver observer = RescuePartyObserver.getInstance(mMockContext);
 
@@ -594,6 +819,46 @@
     }
 
     @Test
+    public void testHealthCheckLevelsRecoverabilityDetection() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+        RescuePartyObserver observer = RescuePartyObserver.getInstance(mMockContext);
+
+        // Ensure that no action is taken for cases where the failure reason is unknown
+        assertEquals(observer.onHealthCheckFailed(sFailingPackage,
+                PackageWatchdog.FAILURE_REASON_UNKNOWN, 1),
+                PackageHealthObserverImpact.USER_IMPACT_LEVEL_0);
+
+        // Ensure the correct user impact is returned for each mitigation count.
+        assertEquals(observer.onHealthCheckFailed(sFailingPackage,
+                PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING, 1),
+                PackageHealthObserverImpact.USER_IMPACT_LEVEL_10);
+
+        assertEquals(observer.onHealthCheckFailed(sFailingPackage,
+                PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING, 2),
+                PackageHealthObserverImpact.USER_IMPACT_LEVEL_20);
+
+        assertEquals(observer.onHealthCheckFailed(sFailingPackage,
+                PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING, 3),
+                PackageHealthObserverImpact.USER_IMPACT_LEVEL_20);
+
+        assertEquals(observer.onHealthCheckFailed(sFailingPackage,
+                PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING, 4),
+                PackageHealthObserverImpact.USER_IMPACT_LEVEL_20);
+
+        assertEquals(observer.onHealthCheckFailed(sFailingPackage,
+                PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING, 5),
+                PackageHealthObserverImpact.USER_IMPACT_LEVEL_20);
+
+        assertEquals(observer.onHealthCheckFailed(sFailingPackage,
+                PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING, 6),
+                PackageHealthObserverImpact.USER_IMPACT_LEVEL_20);
+
+        assertEquals(observer.onHealthCheckFailed(sFailingPackage,
+                PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING, 7),
+                PackageHealthObserverImpact.USER_IMPACT_LEVEL_20);
+    }
+
+    @Test
     public void testBootLoopLevels() {
         RescuePartyObserver observer = RescuePartyObserver.getInstance(mMockContext);
 
@@ -606,6 +871,19 @@
     }
 
     @Test
+    public void testBootLoopLevelsRecoverabilityDetection() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+        RescuePartyObserver observer = RescuePartyObserver.getInstance(mMockContext);
+
+        assertEquals(observer.onBootLoop(1), PackageHealthObserverImpact.USER_IMPACT_LEVEL_20);
+        assertEquals(observer.onBootLoop(2), PackageHealthObserverImpact.USER_IMPACT_LEVEL_50);
+        assertEquals(observer.onBootLoop(3), PackageHealthObserverImpact.USER_IMPACT_LEVEL_71);
+        assertEquals(observer.onBootLoop(4), PackageHealthObserverImpact.USER_IMPACT_LEVEL_75);
+        assertEquals(observer.onBootLoop(5), PackageHealthObserverImpact.USER_IMPACT_LEVEL_80);
+        assertEquals(observer.onBootLoop(6), PackageHealthObserverImpact.USER_IMPACT_LEVEL_100);
+    }
+
+    @Test
     public void testResetDeviceConfigForPackagesOnlyRuntimeMap() {
         RescueParty.onSettingsProviderPublished(mMockContext);
         verify(() -> DeviceConfig.setMonitorCallback(eq(mMockContentResolver),
@@ -727,11 +1005,26 @@
 
     private void verifySettingsResets(int resetMode, String[] resetNamespaces,
             HashMap<String, Integer> configResetVerifiedTimesMap) {
+        verifyOnlySettingsReset(resetMode);
+        verifyDeviceConfigReset(resetNamespaces, configResetVerifiedTimesMap);
+    }
+
+    private void verifyOnlySettingsReset(int resetMode) {
         verify(() -> Settings.Global.resetToDefaultsAsUser(mMockContentResolver, null,
                 resetMode, UserHandle.USER_SYSTEM));
         verify(() -> Settings.Secure.resetToDefaultsAsUser(eq(mMockContentResolver), isNull(),
                 eq(resetMode), anyInt()));
-        // Verify DeviceConfig resets
+    }
+
+    private void verifyNoSettingsReset(int resetMode) {
+        verify(() -> Settings.Global.resetToDefaultsAsUser(mMockContentResolver, null,
+                resetMode, UserHandle.USER_SYSTEM), never());
+        verify(() -> Settings.Secure.resetToDefaultsAsUser(eq(mMockContentResolver), isNull(),
+                eq(resetMode), anyInt()), never());
+    }
+
+    private void verifyDeviceConfigReset(String[] resetNamespaces,
+            Map<String, Integer> configResetVerifiedTimesMap) {
         if (resetNamespaces == null) {
             verify(() -> DeviceConfig.resetToDefaults(anyInt(), anyString()), never());
         } else {
@@ -818,9 +1111,16 @@
 
         // mock properties in BootThreshold
         try {
-            mSpyBootThreshold = spy(watchdog.new BootThreshold(
-                PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT,
-                PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS));
+            if (Flags.recoverabilityDetection()) {
+                mSpyBootThreshold = spy(watchdog.new BootThreshold(
+                    PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT,
+                    PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS,
+                    PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT));
+            } else {
+                mSpyBootThreshold = spy(watchdog.new BootThreshold(
+                    PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT,
+                    PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS));
+            }
             mCrashRecoveryPropertiesMap = new HashMap<>();
 
             doAnswer((Answer<Integer>) invocationOnMock -> {
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
index 420af86..1b2c0e4 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
@@ -41,6 +41,7 @@
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.same;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
@@ -57,6 +58,7 @@
 import android.app.Activity;
 import android.app.ActivityManager;
 import android.app.AppOpsManager;
+import android.app.ApplicationExitInfo;
 import android.app.BackgroundStartPrivileges;
 import android.app.BroadcastOptions;
 import android.app.IApplicationThread;
@@ -239,6 +241,7 @@
         mConstants.TIMEOUT = 200;
         mConstants.ALLOW_BG_ACTIVITY_START_TIMEOUT = 0;
         mConstants.PENDING_COLD_START_CHECK_INTERVAL_MILLIS = 500;
+        mConstants.MAX_FROZEN_OUTGOING_BROADCASTS = 10;
     }
 
     @After
@@ -2368,6 +2371,34 @@
         verifyScheduleReceiver(times(1), receiverYellowApp, timeTick);
     }
 
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DEFER_OUTGOING_BROADCASTS)
+    public void testKillProcess_excessiveOutgoingBroadcastsWhileCached() throws Exception {
+        final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
+        setProcessFreezable(callerApp, true /* pendingFreeze */, false /* frozen */);
+        waitForIdle();
+
+        final int count = mConstants.MAX_FROZEN_OUTGOING_BROADCASTS + 1;
+        for (int i = 0; i < count; ++i) {
+            final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK + "_" + i);
+            enqueueBroadcast(makeBroadcastRecord(timeTick, callerApp, List.of(
+                    makeManifestReceiver(PACKAGE_BLUE, CLASS_BLUE))));
+        }
+        // Verify that we invoke the call to freeze the caller app.
+        verify(mAms.mOomAdjuster.mCachedAppOptimizer, atLeastOnce())
+                .freezeAppAsyncImmediateLSP(callerApp);
+
+        // Verify that the caller process is killed
+        assertTrue(callerApp.isKilled());
+        verify(mProcessList).noteAppKill(same(callerApp),
+                eq(ApplicationExitInfo.REASON_OTHER),
+                eq(ApplicationExitInfo.SUBREASON_EXCESSIVE_OUTGOING_BROADCASTS_WHILE_CACHED),
+                any(String.class));
+
+        waitForIdle();
+        assertNull(mAms.getProcessRecordLocked(PACKAGE_BLUE, getUidForPackage(PACKAGE_BLUE)));
+    }
+
     private long getReceiverScheduledTime(@NonNull BroadcastRecord r, @NonNull Object receiver) {
         for (int i = 0; i < r.receivers.size(); ++i) {
             if (isReceiverEquals(receiver, r.receivers.get(i))) {
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ServiceBindingOomAdjPolicyTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ServiceBindingOomAdjPolicyTest.java
index 97b7af8..680ab16 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/ServiceBindingOomAdjPolicyTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/ServiceBindingOomAdjPolicyTest.java
@@ -36,7 +36,6 @@
 
 import static org.junit.Assert.assertNotEquals;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
@@ -185,8 +184,8 @@
         doReturn(false).when(mAms.mAtmInternal).hasSystemAlertWindowPermission(anyInt(), anyInt(),
                 any());
         doReturn(true).when(mAms.mOomAdjuster.mCachedAppOptimizer).useFreezer();
-        doNothing().when(mAms.mOomAdjuster.mCachedAppOptimizer).freezeAppAsyncInternalLSP(
-                any(), anyLong(), anyBoolean(), anyBoolean());
+        doNothing().when(mAms.mOomAdjuster.mCachedAppOptimizer).freezeAppAsyncAtEarliestLSP(
+                any());
         doReturn(false).when(mAms.mAppProfiler).updateLowMemStateLSP(anyInt(), anyInt(),
                 anyInt(), anyLong());
 
@@ -503,7 +502,7 @@
         if (clientApp.isFreezable()) {
             verify(mAms.mOomAdjuster.mCachedAppOptimizer,
                     times(Flags.serviceBindingOomAdjPolicy() ? 1 : 0))
-                    .freezeAppAsyncInternalLSP(eq(clientApp), eq(0L), anyBoolean(), anyBoolean());
+                    .freezeAppAsyncAtEarliestLSP(eq(clientApp));
             clearInvocations(mAms.mOomAdjuster.mCachedAppOptimizer);
         }
 
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
index 53c460c..9d32ed8 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
@@ -21,7 +21,6 @@
 import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN;
 import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_NONE;
 import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW;
-import static android.view.accessibility.Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG;
 import static android.view.accessibility.Flags.FLAG_SKIP_ACCESSIBILITY_WARNING_DIALOG_FOR_TRUSTED_SERVICES;
 
 import static com.android.internal.accessibility.AccessibilityShortcutController.ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME;
@@ -885,7 +884,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG)
     public void testIsAccessibilityServiceWarningRequired_requiredByDefault() {
         mockManageAccessibilityGranted(mTestableContext);
         final AccessibilityServiceInfo info = mockAccessibilityServiceInfo(COMPONENT_NAME);
@@ -894,7 +892,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG)
     public void testIsAccessibilityServiceWarningRequired_notRequiredIfAlreadyEnabled() {
         mockManageAccessibilityGranted(mTestableContext);
         final AccessibilityServiceInfo info_a = mockAccessibilityServiceInfo(COMPONENT_NAME);
@@ -909,7 +906,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG)
     public void testIsAccessibilityServiceWarningRequired_notRequiredIfExistingShortcut() {
         mockManageAccessibilityGranted(mTestableContext);
         final AccessibilityServiceInfo info_a = mockAccessibilityServiceInfo(
@@ -930,9 +926,7 @@
     }
 
     @Test
-    @RequiresFlagsEnabled({
-            FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG,
-            FLAG_SKIP_ACCESSIBILITY_WARNING_DIALOG_FOR_TRUSTED_SERVICES})
+    @RequiresFlagsEnabled(FLAG_SKIP_ACCESSIBILITY_WARNING_DIALOG_FOR_TRUSTED_SERVICES)
     public void testIsAccessibilityServiceWarningRequired_notRequiredIfAllowlisted() {
         mockManageAccessibilityGranted(mTestableContext);
         final AccessibilityServiceInfo info_a = mockAccessibilityServiceInfo(
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java
index 6e8d6dc..f44879f 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java
@@ -470,6 +470,27 @@
     }
 
     @Test
+    public void onWindowsChanged_shouldNotReportfullyOccludedWindow() {
+        final AccessibilityWindow frontWindow = mWindows.get(Display.DEFAULT_DISPLAY).get(0);
+        setRegionForMockAccessibilityWindow(frontWindow, new Region(100, 100, 300, 300));
+        final int frontWindowId = mA11yWindowManager.findWindowIdLocked(
+                USER_SYSTEM_ID, frontWindow.getWindowInfo().token);
+
+        // index 1 is focused. Let's use the next one for this test.
+        final AccessibilityWindow occludedWindow = mWindows.get(Display.DEFAULT_DISPLAY).get(2);
+        setRegionForMockAccessibilityWindow(occludedWindow, new Region(150, 150, 250, 250));
+        final int occludedWindowId = mA11yWindowManager.findWindowIdLocked(
+                USER_SYSTEM_ID, occludedWindow.getWindowInfo().token);
+
+        onAccessibilityWindowsChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES);
+
+        final List<AccessibilityWindowInfo> a11yWindows =
+                mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY);
+        assertThat(a11yWindows, hasItem(windowId(frontWindowId)));
+        assertThat(a11yWindows, not(hasItem(windowId(occludedWindowId))));
+    }
+
+    @Test
     public void onWindowsChangedAndForceSend_shouldUpdateWindows() {
         assertNotEquals("new title",
                 toString(mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY)
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java
index a4628ee..4d1d17f 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java
@@ -141,6 +141,7 @@
     @Test
     public void virtualDevice_hasCustomAudioInputSupport() throws Exception {
         mSetFlagsRule.enableFlags(Flags.FLAG_VDM_PUBLIC_APIS);
+        mSetFlagsRule.enableFlags(android.media.audiopolicy.Flags.FLAG_AUDIO_MIX_TEST_API);
 
         VirtualDevice virtualDevice =
                 new VirtualDevice(
@@ -150,6 +151,10 @@
         assertThat(virtualDevice.hasCustomAudioInputSupport()).isFalse();
 
         when(mVirtualDevice.getDevicePolicy(POLICY_TYPE_AUDIO)).thenReturn(DEVICE_POLICY_CUSTOM);
+        when(mVirtualDevice.hasCustomAudioInputSupport()).thenReturn(false);
+        assertThat(virtualDevice.hasCustomAudioInputSupport()).isFalse();
+
+        when(mVirtualDevice.hasCustomAudioInputSupport()).thenReturn(true);
         assertThat(virtualDevice.hasCustomAudioInputSupport()).isTrue();
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java
index 66599e9..510e7c4 100644
--- a/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java
@@ -17,6 +17,8 @@
 package com.android.server.power.hint;
 
 
+import static com.android.server.power.hint.HintManagerService.CLEAN_UP_UID_DELAY_MILLIS;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertArrayEquals;
@@ -45,6 +47,9 @@
 import android.os.IHintSession;
 import android.os.PerformanceHintManager;
 import android.os.Process;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.util.Log;
 
 import com.android.server.FgThread;
@@ -54,11 +59,13 @@
 import com.android.server.power.hint.HintManagerService.NativeWrapper;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
@@ -71,7 +78,7 @@
  * Tests for {@link com.android.server.power.hint.HintManagerService}.
  *
  * Build/Install/Run:
- *  atest FrameworksServicesTests:HintManagerServiceTest
+ * atest FrameworksServicesTests:HintManagerServiceTest
  */
 public class HintManagerServiceTest {
     private static final String TAG = "HintManagerServiceTest";
@@ -110,9 +117,15 @@
         makeWorkDuration(2L, 13L, 2L, 8L, 0L),
     };
 
-    @Mock private Context mContext;
-    @Mock private HintManagerService.NativeWrapper mNativeWrapperMock;
-    @Mock private ActivityManagerInternal mAmInternalMock;
+    @Mock
+    private Context mContext;
+    @Mock
+    private HintManagerService.NativeWrapper mNativeWrapperMock;
+    @Mock
+    private ActivityManagerInternal mAmInternalMock;
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
 
     private HintManagerService mService;
 
@@ -122,12 +135,11 @@
         when(mNativeWrapperMock.halGetHintSessionPreferredRate())
                 .thenReturn(DEFAULT_HINT_PREFERRED_RATE);
         when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(SESSION_TIDS_A),
-              eq(DEFAULT_TARGET_DURATION))).thenReturn(1L);
+                eq(DEFAULT_TARGET_DURATION))).thenReturn(1L);
         when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(SESSION_TIDS_B),
-              eq(DEFAULT_TARGET_DURATION))).thenReturn(2L);
+                eq(DEFAULT_TARGET_DURATION))).thenReturn(2L);
         when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(SESSION_TIDS_C),
-              eq(0L))).thenReturn(1L);
-        when(mAmInternalMock.getIsolatedProcesses(anyInt())).thenReturn(null);
+                eq(0L))).thenReturn(1L);
         LocalServices.removeServiceForTest(ActivityManagerInternal.class);
         LocalServices.addService(ActivityManagerInternal.class, mAmInternalMock);
     }
@@ -434,6 +446,163 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(Flags.FLAG_POWERHINT_THREAD_CLEANUP)
+    public void testCleanupDeadThreads() throws Exception {
+        HintManagerService service = createService();
+        IBinder token = new Binder();
+        CountDownLatch stopLatch1 = new CountDownLatch(1);
+        int threadCount = 3;
+        int[] tids1 = createThreads(threadCount, stopLatch1);
+        long sessionPtr1 = 111;
+        when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(tids1),
+                eq(DEFAULT_TARGET_DURATION))).thenReturn(sessionPtr1);
+        AppHintSession session1 = (AppHintSession) service.getBinderServiceInstance()
+                .createHintSession(token, tids1, DEFAULT_TARGET_DURATION);
+        assertNotNull(session1);
+
+        // for test only to avoid conflicting with any real thread that exists on device
+        int isoProc1 = -100;
+        int isoProc2 = 9999;
+        when(mAmInternalMock.getIsolatedProcesses(eq(UID))).thenReturn(List.of(0));
+
+        CountDownLatch stopLatch2 = new CountDownLatch(1);
+        int[] tids2 = createThreads(threadCount, stopLatch2);
+        int[] tids2WithIsolated = Arrays.copyOf(tids2, tids2.length + 2);
+        int[] expectedTids2 = Arrays.copyOf(tids2, tids2.length + 1);
+        expectedTids2[tids2.length] = isoProc1;
+        tids2WithIsolated[threadCount] = isoProc1;
+        tids2WithIsolated[threadCount + 1] = isoProc2;
+        long sessionPtr2 = 222;
+        when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(tids2WithIsolated),
+                eq(DEFAULT_TARGET_DURATION))).thenReturn(sessionPtr2);
+        AppHintSession session2 = (AppHintSession) service.getBinderServiceInstance()
+                .createHintSession(token, tids2WithIsolated, DEFAULT_TARGET_DURATION);
+        assertNotNull(session2);
+
+        // trigger clean up through UID state change by making the process background
+        service.mUidObserver.onUidStateChanged(UID,
+                ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND, 0, 0);
+        LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500) + TimeUnit.MILLISECONDS.toNanos(
+                CLEAN_UP_UID_DELAY_MILLIS));
+        verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr1), any());
+        verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr2), any());
+        // the new TIDs pending list should be updated
+        assertArrayEquals(session2.getTidsInternal(), expectedTids2);
+        reset(mNativeWrapperMock);
+
+        // this should resume and update the threads so those never-existed invalid isolated
+        // processes should be cleaned up
+        service.mUidObserver.onUidStateChanged(UID,
+                ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND, 0, 0);
+        // wait for the async uid state change to trigger resume and setThreads
+        LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500));
+        verify(mNativeWrapperMock, times(1)).halSetThreads(eq(sessionPtr2), eq(expectedTids2));
+        reset(mNativeWrapperMock);
+
+        // let all session 1 threads to exit and the cleanup should force pause the session
+        stopLatch1.countDown();
+        LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(100));
+        service.mUidObserver.onUidStateChanged(UID,
+                ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND, 0, 0);
+        LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500) + TimeUnit.MILLISECONDS.toNanos(
+                CLEAN_UP_UID_DELAY_MILLIS));
+        verify(mNativeWrapperMock, times(1)).halPauseHintSession(eq(sessionPtr1));
+        verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr1), any());
+        verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr2), any());
+        // all hints will have no effect as the session is force paused while proc in foreground
+        verifyAllHintsEnabled(session1, false);
+        verifyAllHintsEnabled(session2, true);
+        reset(mNativeWrapperMock);
+
+        // in foreground, set new tids for session 1 then it should be resumed and all hints allowed
+        stopLatch1 = new CountDownLatch(1);
+        tids1 = createThreads(threadCount, stopLatch1);
+        session1.setThreads(tids1);
+        verify(mNativeWrapperMock, times(1)).halSetThreads(eq(sessionPtr1), eq(tids1));
+        verify(mNativeWrapperMock, times(1)).halResumeHintSession(eq(sessionPtr1));
+        verifyAllHintsEnabled(session1, true);
+        reset(mNativeWrapperMock);
+
+        // let all session 1 and 2 non isolated threads to exit
+        stopLatch1.countDown();
+        stopLatch2.countDown();
+        expectedTids2 = new int[]{isoProc1};
+        LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(100));
+        service.mUidObserver.onUidStateChanged(UID,
+                ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND, 0, 0);
+        LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500) + TimeUnit.MILLISECONDS.toNanos(
+                CLEAN_UP_UID_DELAY_MILLIS));
+        verify(mNativeWrapperMock, times(1)).halPauseHintSession(eq(sessionPtr1));
+        verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr1), any());
+        verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr2), any());
+        // in background, set threads for session 1 then it should not be force paused next time
+        session1.setThreads(SESSION_TIDS_A);
+        // the new TIDs pending list should be updated
+        assertArrayEquals(session1.getTidsInternal(), SESSION_TIDS_A);
+        assertArrayEquals(session2.getTidsInternal(), expectedTids2);
+        verifyAllHintsEnabled(session1, false);
+        verifyAllHintsEnabled(session2, false);
+        reset(mNativeWrapperMock);
+
+        service.mUidObserver.onUidStateChanged(UID,
+                ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND, 0, 0);
+        LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500) + TimeUnit.MILLISECONDS.toNanos(
+                CLEAN_UP_UID_DELAY_MILLIS));
+        verify(mNativeWrapperMock, times(1)).halSetThreads(eq(sessionPtr1),
+                eq(SESSION_TIDS_A));
+        verify(mNativeWrapperMock, times(1)).halSetThreads(eq(sessionPtr2),
+                eq(expectedTids2));
+        verifyAllHintsEnabled(session1, true);
+        verifyAllHintsEnabled(session2, true);
+    }
+
+    private void verifyAllHintsEnabled(AppHintSession session, boolean verifyEnabled) {
+        session.reportActualWorkDuration2(new WorkDuration[]{makeWorkDuration(1, 3, 2, 1, 1000)});
+        session.reportActualWorkDuration(new long[]{1}, new long[]{2});
+        session.updateTargetWorkDuration(3);
+        session.setMode(0, true);
+        session.sendHint(1);
+        if (verifyEnabled) {
+            verify(mNativeWrapperMock, times(1)).halReportActualWorkDuration(
+                    eq(session.mHalSessionPtr), any());
+            verify(mNativeWrapperMock, times(1)).halSetMode(eq(session.mHalSessionPtr), anyInt(),
+                    anyBoolean());
+            verify(mNativeWrapperMock, times(1)).halUpdateTargetWorkDuration(
+                    eq(session.mHalSessionPtr), anyLong());
+            verify(mNativeWrapperMock, times(1)).halSendHint(eq(session.mHalSessionPtr), anyInt());
+        } else {
+            verify(mNativeWrapperMock, never()).halReportActualWorkDuration(
+                    eq(session.mHalSessionPtr), any());
+            verify(mNativeWrapperMock, never()).halSetMode(eq(session.mHalSessionPtr), anyInt(),
+                    anyBoolean());
+            verify(mNativeWrapperMock, never()).halUpdateTargetWorkDuration(
+                    eq(session.mHalSessionPtr), anyLong());
+            verify(mNativeWrapperMock, never()).halSendHint(eq(session.mHalSessionPtr), anyInt());
+        }
+    }
+
+    private int[] createThreads(int threadCount, CountDownLatch stopLatch)
+            throws InterruptedException {
+        int[] tids = new int[threadCount];
+        AtomicInteger k = new AtomicInteger(0);
+        CountDownLatch latch = new CountDownLatch(threadCount);
+        for (int j = 0; j < threadCount; j++) {
+            Thread thread = new Thread(() -> {
+                try {
+                    tids[k.getAndIncrement()] = android.os.Process.myTid();
+                    latch.countDown();
+                    stopLatch.await();
+                } catch (InterruptedException e) {
+                    throw new RuntimeException(e);
+                }
+            });
+            thread.start();
+        }
+        latch.await();
+        return tids;
+    }
+
+    @Test
     public void testSetMode() throws Exception {
         HintManagerService service = createService();
         IBinder token = new Binder();
@@ -457,7 +626,8 @@
         // Set session to background, then the duration would not be updated.
         service.mUidObserver.onUidStateChanged(
                 a.mUid, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
-        FgThread.getHandler().runWithScissors(() -> { }, 500);
+        FgThread.getHandler().runWithScissors(() -> {
+        }, 500);
         assertFalse(service.mUidObserver.isUidForeground(a.mUid));
         a.setMode(0, true);
         verify(mNativeWrapperMock, never()).halSetMode(anyLong(), anyInt(), anyBoolean());
@@ -519,7 +689,10 @@
                     LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500));
                     service.mUidObserver.onUidStateChanged(UID,
                             ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
-                    LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500));
+                    // let the cleanup work proceed
+                    LockSupport.parkNanos(
+                            TimeUnit.MILLISECONDS.toNanos(500) + TimeUnit.MILLISECONDS.toNanos(
+                                    CLEAN_UP_UID_DELAY_MILLIS));
                 }
                 Log.d(TAG, "notifier thread min " + min + " max " + max + " avg " + sum / count);
                 service.mUidObserver.onUidGone(UID, true);
diff --git a/services/tests/servicestests/src/com/android/server/power/hint/TEST_MAPPING b/services/tests/servicestests/src/com/android/server/power/hint/TEST_MAPPING
new file mode 100644
index 0000000..2d5df07
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/power/hint/TEST_MAPPING
@@ -0,0 +1,15 @@
+{
+  "postsubmit": [
+    {
+      "name": "FrameworksServicesTests",
+      "options": [
+        {
+          "include-filter": "com.android.server.power.hint"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.FlakyTest"
+        }
+      ]
+    }
+  ]
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index bfc47fd..cee6cdb 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -3962,6 +3962,20 @@
     }
 
     @Test
+    public void testReadXml_existingPackage_bubblePrefsRestored() throws Exception {
+        mHelper.setBubblesAllowed(PKG_O, UID_O, BUBBLE_PREFERENCE_ALL);
+        assertEquals(BUBBLE_PREFERENCE_ALL, mHelper.getBubblePreference(PKG_O, UID_O));
+
+        mXmlHelper.setBubblesAllowed(PKG_O, UID_O, BUBBLE_PREFERENCE_NONE);
+        assertEquals(BUBBLE_PREFERENCE_NONE, mXmlHelper.getBubblePreference(PKG_O, UID_O));
+
+        ByteArrayOutputStream stream = writeXmlAndPurge(PKG_O, UID_O, false, UserHandle.USER_ALL);
+        loadStreamXml(stream, true, UserHandle.USER_ALL);
+
+        assertEquals(BUBBLE_PREFERENCE_ALL, mXmlHelper.getBubblePreference(PKG_O, UID_O));
+    }
+
+    @Test
     public void testUpdateNotificationChannel_fixedPermission() {
         List<UserInfo> users = ImmutableList.of(new UserInfo(UserHandle.USER_SYSTEM, "user0", 0));
         when(mPermissionHelper.isPermissionFixed(PKG_O, 0)).thenReturn(true);
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
index 8cbcc22..5861d88 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
@@ -500,7 +500,8 @@
         InOrder batteryVerifier = inOrder(mBatteryStatsMock);
         batteryVerifier.verify(mBatteryStatsMock)
                 .noteVibratorOn(UID, oneShotDuration + mVibrationConfig.getRampDownDurationMs());
-        batteryVerifier.verify(mBatteryStatsMock).noteVibratorOff(UID);
+        batteryVerifier
+                .verify(mBatteryStatsMock, timeout(TEST_TIMEOUT_MILLIS)).noteVibratorOff(UID);
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java
index 29467f2..a80e2f8 100644
--- a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java
@@ -16,10 +16,14 @@
 
 package com.android.server.policy;
 
+import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
 import static android.view.WindowManagerGlobal.ADD_OKAY;
 
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
@@ -33,18 +37,27 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
 
 import android.app.ActivityManager;
 import android.app.AppOpsManager;
+import android.content.Context;
+import android.os.PowerManager;
 import android.platform.test.flag.junit.SetFlagsRule;
 
 import androidx.test.filters.SmallTest;
 
+import com.android.server.LocalServices;
 import com.android.server.pm.UserManagerInternal;
 import com.android.server.wm.ActivityTaskManagerInternal;
+import com.android.server.wm.DisplayPolicy;
+import com.android.server.wm.DisplayRotation;
+import com.android.server.wm.WindowManagerInternal;
 
 import org.junit.After;
 import org.junit.Before;
@@ -64,16 +77,27 @@
     public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 
     PhoneWindowManager mPhoneWindowManager;
+    private ActivityTaskManagerInternal mAtmInternal;
+    private Context mContext;
 
     @Before
     public void setUp() {
         mPhoneWindowManager = spy(new PhoneWindowManager());
         spyOn(ActivityManager.getService());
+        mContext = getInstrumentation().getTargetContext();
+        spyOn(mContext);
+        mAtmInternal = mock(ActivityTaskManagerInternal.class);
+        LocalServices.addService(ActivityTaskManagerInternal.class, mAtmInternal);
+        mPhoneWindowManager.mActivityTaskManagerInternal = mAtmInternal;
+        LocalServices.addService(WindowManagerInternal.class, mock(WindowManagerInternal.class));
     }
 
     @After
     public void tearDown() {
         reset(ActivityManager.getService());
+        reset(mContext);
+        LocalServices.removeServiceForTest(ActivityTaskManagerInternal.class);
+        LocalServices.removeServiceForTest(WindowManagerInternal.class);
     }
 
     @Test
@@ -99,6 +123,60 @@
     }
 
     @Test
+    public void testScreenTurnedOff() {
+        mSetFlagsRule.enableFlags(com.android.window.flags.Flags
+                .FLAG_SKIP_SLEEPING_WHEN_SWITCHING_DISPLAY);
+        doNothing().when(mPhoneWindowManager).updateSettings(any());
+        doNothing().when(mPhoneWindowManager).initializeHdmiState();
+        final boolean[] isScreenTurnedOff = { false };
+        final DisplayPolicy displayPolicy = mock(DisplayPolicy.class);
+        doAnswer(invocation -> isScreenTurnedOff[0] = true).when(displayPolicy).screenTurnedOff();
+        doAnswer(invocation -> !isScreenTurnedOff[0]).when(displayPolicy).isScreenOnEarly();
+        doAnswer(invocation -> !isScreenTurnedOff[0]).when(displayPolicy).isScreenOnFully();
+
+        mPhoneWindowManager.mDefaultDisplayPolicy = displayPolicy;
+        mPhoneWindowManager.mDefaultDisplayRotation = mock(DisplayRotation.class);
+        final ActivityTaskManagerInternal.SleepTokenAcquirer tokenAcquirer =
+                mock(ActivityTaskManagerInternal.SleepTokenAcquirer.class);
+        doReturn(tokenAcquirer).when(mAtmInternal).createSleepTokenAcquirer(anyString());
+        final PowerManager pm = mock(PowerManager.class);
+        doReturn(true).when(pm).isInteractive();
+        doReturn(pm).when(mContext).getSystemService(eq(Context.POWER_SERVICE));
+
+        mContext.getMainThreadHandler().runWithScissors(() -> mPhoneWindowManager.init(
+                new PhoneWindowManager.Injector(mContext,
+                        mock(WindowManagerPolicy.WindowManagerFuncs.class))), 0);
+        assertThat(isScreenTurnedOff[0]).isFalse();
+        assertThat(mPhoneWindowManager.mIsGoingToSleepDefaultDisplay).isFalse();
+
+        // Skip sleep-token for non-sleep-screen-off.
+        clearInvocations(tokenAcquirer);
+        mPhoneWindowManager.screenTurnedOff(DEFAULT_DISPLAY, true /* isSwappingDisplay */);
+        verify(tokenAcquirer, never()).acquire(anyInt(), anyBoolean());
+        assertThat(isScreenTurnedOff[0]).isTrue();
+
+        // Apply sleep-token for sleep-screen-off.
+        mPhoneWindowManager.startedGoingToSleep(DEFAULT_DISPLAY, 0 /* reason */);
+        assertThat(mPhoneWindowManager.mIsGoingToSleepDefaultDisplay).isTrue();
+        mPhoneWindowManager.screenTurnedOff(DEFAULT_DISPLAY, true /* isSwappingDisplay */);
+        verify(tokenAcquirer).acquire(eq(DEFAULT_DISPLAY), eq(true));
+
+        mPhoneWindowManager.finishedGoingToSleep(DEFAULT_DISPLAY, 0 /* reason */);
+        assertThat(mPhoneWindowManager.mIsGoingToSleepDefaultDisplay).isFalse();
+
+        // Simulate unexpected reversed order: screenTurnedOff -> startedGoingToSleep. The sleep
+        // token can still be acquired.
+        isScreenTurnedOff[0] = false;
+        clearInvocations(tokenAcquirer);
+        mPhoneWindowManager.screenTurnedOff(DEFAULT_DISPLAY, true /* isSwappingDisplay */);
+        verify(tokenAcquirer, never()).acquire(anyInt(), anyBoolean());
+        assertThat(displayPolicy.isScreenOnEarly()).isFalse();
+        assertThat(displayPolicy.isScreenOnFully()).isFalse();
+        mPhoneWindowManager.startedGoingToSleep(DEFAULT_DISPLAY, 0 /* reason */);
+        verify(tokenAcquirer).acquire(eq(DEFAULT_DISPLAY), eq(false));
+    }
+
+    @Test
     public void testCheckAddPermission_withoutAccessibilityOverlay_noAccessibilityAppOpLogged() {
         mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags
                 .FLAG_CREATE_ACCESSIBILITY_OVERLAY_APP_OP_ENABLED);
@@ -130,11 +208,8 @@
 
     private void mockStartDockOrHome() throws Exception {
         doNothing().when(ActivityManager.getService()).stopAppSwitches();
-        ActivityTaskManagerInternal mMockActivityTaskManagerInternal =
-                mock(ActivityTaskManagerInternal.class);
-        when(mMockActivityTaskManagerInternal.startHomeOnDisplay(
+        when(mAtmInternal.startHomeOnDisplay(
                 anyInt(), anyString(), anyInt(), anyBoolean(), anyBoolean())).thenReturn(false);
-        mPhoneWindowManager.mActivityTaskManagerInternal = mMockActivityTaskManagerInternal;
         mPhoneWindowManager.mUserManagerInternal = mock(UserManagerInternal.class);
     }
 }
diff --git a/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java b/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java
index 0a29dfb..60716cb 100644
--- a/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java
@@ -95,8 +95,6 @@
                         new int[]{KeyEvent.KEYCODE_NOTIFICATION},
                         KeyboardLogEvent.TOGGLE_NOTIFICATION_PANEL, KeyEvent.KEYCODE_NOTIFICATION,
                         0},
-                {"Meta + T -> Toggle Taskbar", new int[]{META_KEY, KeyEvent.KEYCODE_T},
-                        KeyboardLogEvent.TOGGLE_TASKBAR, KeyEvent.KEYCODE_T, META_ON},
                 {"Meta + Ctrl + S -> Take Screenshot",
                         new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_S},
                         KeyboardLogEvent.TAKE_SCREENSHOT, KeyEvent.KEYCODE_S, META_ON | CTRL_ON},
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index daa5a5a..82e55711 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -3346,7 +3346,7 @@
         } else {
             verify(app2.mClient, atLeastOnce()).resized(any(), anyBoolean(), any(),
                     insetsStateCaptor.capture(), anyBoolean(), anyBoolean(), anyInt(), anyInt(),
-                    anyBoolean());
+                    anyBoolean(), any());
         }
         assertFalse(app2.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime()));
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index 4e360d0..2c88ed2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -1068,16 +1068,6 @@
                 mDisplayContent.getImeTarget(IME_TARGET_LAYERING));
     }
 
-    @SetupWindows(addWindows = W_INPUT_METHOD)
-    @Test
-    public void testInputMethodSet_listenOnDisplayAreaConfigurationChanged() {
-        spyOn(mAtm);
-        mDisplayContent.setInputMethodWindowLocked(mImeWindow);
-
-        verify(mAtm).onImeWindowSetOnDisplayArea(
-                mImeWindow.mSession.mPid, mDisplayContent.getImeContainer());
-    }
-
     @Test
     public void testAllowsTopmostFullscreenOrientation() {
         final DisplayContent dc = createNewDisplay();
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
index 897a3da..52485ee 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -25,7 +25,7 @@
 import static android.view.WindowManager.TRANSIT_NONE;
 import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_TASK_FRAGMENT;
-import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_TASK_FRAGMENT_DECOR_SURFACE;
+import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE;
 import static android.window.TaskFragmentOperation.OP_TYPE_DELETE_TASK_FRAGMENT;
 import static android.window.TaskFragmentOperation.OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE;
 import static android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_BOTTOM_OF_TASK;
@@ -1835,7 +1835,7 @@
 
         final TaskFragment tf = createTaskFragment(task);
         final TaskFragmentOperation operation = new TaskFragmentOperation.Builder(
-                OP_TYPE_CREATE_TASK_FRAGMENT_DECOR_SURFACE).build();
+                OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE).build();
         mTransaction.addTaskFragmentOperation(tf.getFragmentToken(), operation);
 
         assertApplyTransactionAllowed(mTransaction);
diff --git a/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java b/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java
index 3f8acc6..37de51e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java
@@ -28,6 +28,7 @@
 import android.view.InsetsState;
 import android.view.ScrollCaptureResponse;
 import android.view.inputmethod.ImeTracker;
+import android.window.ActivityWindowInfo;
 import android.window.ClientWindowFrames;
 
 import com.android.internal.os.IResultReceiver;
@@ -46,8 +47,8 @@
     @Override
     public void resized(ClientWindowFrames frames, boolean reportDraw,
             MergedConfiguration mergedConfig, InsetsState insetsState, boolean forceLayout,
-            boolean alwaysConsumeSystemBars, int displayId, int seqId, boolean dragResizing)
-            throws RemoteException {
+            boolean alwaysConsumeSystemBars, int displayId, int seqId, boolean dragResizing,
+            @Nullable ActivityWindowInfo activityWindowInfo) throws RemoteException {
     }
 
     @Override
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
index 12f46df..48b12f7 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
@@ -90,6 +90,7 @@
 import android.util.MergedConfiguration;
 import android.view.ContentRecordingSession;
 import android.view.IWindow;
+import android.view.IWindowSession;
 import android.view.InputChannel;
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
@@ -99,6 +100,7 @@
 import android.view.WindowInsets;
 import android.view.WindowManager;
 import android.view.WindowManager.LayoutParams;
+import android.window.ActivityWindowInfo;
 import android.window.ClientWindowFrames;
 import android.window.InputTransferToken;
 import android.window.ScreenCapture;
@@ -1216,6 +1218,35 @@
         mWm.reportKeepClearAreasChanged(session, window, new ArrayList<>(), new ArrayList<>());
     }
 
+    @Test
+    public void testRelayout_appWindowSendActivityWindowInfo() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ACTIVITY_WINDOW_INFO_FLAG);
+
+        // Skip unnecessary operations of relayout.
+        spyOn(mWm.mWindowPlacerLocked);
+        doNothing().when(mWm.mWindowPlacerLocked).performSurfacePlacement(anyBoolean());
+
+        final Task task = createTask(mDisplayContent);
+        final WindowState win = createAppWindow(task, ACTIVITY_TYPE_STANDARD, "appWindow");
+        mWm.mWindowMap.put(win.mClient.asBinder(), win);
+
+        final int w = 100;
+        final int h = 200;
+        final ClientWindowFrames outFrames = new ClientWindowFrames();
+        final MergedConfiguration outConfig = new MergedConfiguration();
+        final SurfaceControl outSurfaceControl = new SurfaceControl();
+        final InsetsState outInsetsState = new InsetsState();
+        final InsetsSourceControl.Array outControls = new InsetsSourceControl.Array();
+        final Bundle outBundle = new Bundle();
+
+        mWm.relayoutWindow(win.mSession, win.mClient, win.mAttrs, w, h, View.GONE, 0, 0, 0,
+                outFrames, outConfig, outSurfaceControl, outInsetsState, outControls, outBundle);
+
+        final ActivityWindowInfo activityWindowInfo = outBundle.getParcelable(
+                IWindowSession.KEY_RELAYOUT_BUNDLE_ACTIVITY_WINDOW_INFO, ActivityWindowInfo.class);
+        assertEquals(win.mActivityRecord.getActivityWindowInfo(), activityWindowInfo);
+    }
+
     class TestResultReceiver implements IResultReceiver {
         public android.os.Bundle resultData;
         private final IBinder mBinder = mock(IBinder.class);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
index c8ad4bd..e20f822 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
@@ -804,7 +804,8 @@
                     anyBoolean() /* reportDraw */, any() /* mergedConfig */,
                     any() /* insetsState */, anyBoolean() /* forceLayout */,
                     anyBoolean() /* alwaysConsumeSystemBars */, anyInt() /* displayId */,
-                    anyInt() /* seqId */, anyBoolean() /* dragResizing */);
+                    anyInt() /* seqId */, anyBoolean() /* dragResizing */,
+                    any() /* activityWindowInfo */);
         } catch (RemoteException ignored) {
         }
         win.reportResized();
diff --git a/telephony/java/android/telephony/satellite/stub/ISatellite.aidl b/telephony/java/android/telephony/satellite/stub/ISatellite.aidl
index 9441fb5..36485c6 100644
--- a/telephony/java/android/telephony/satellite/stub/ISatellite.aidl
+++ b/telephony/java/android/telephony/satellite/stub/ISatellite.aidl
@@ -347,28 +347,6 @@
             in IIntegerConsumer callback);
 
     /**
-     * Request to get whether satellite communication is allowed for the current location.
-     *
-     * @param resultCallback The callback to receive the error code result of the operation.
-     *                       This must only be sent when the result is not
-     *                       SatelliteResult#SATELLITE_RESULT_SUCCESS.
-     * @param callback If the result is SatelliteResult#SATELLITE_RESULT_SUCCESS, the callback to
-     *                 receive whether satellite communication is allowed for the current location.
-     *
-     * Valid result codes returned:
-     *   SatelliteResult:SATELLITE_RESULT_SUCCESS
-     *   SatelliteResult:SATELLITE_RESULT_SERVICE_ERROR
-     *   SatelliteResult:SATELLITE_RESULT_MODEM_ERROR
-     *   SatelliteResult:SATELLITE_RESULT_INVALID_MODEM_STATE
-     *   SatelliteResult:SATELLITE_RESULT_INVALID_ARGUMENTS
-     *   SatelliteResult:SATELLITE_RESULT_RADIO_NOT_AVAILABLE
-     *   SatelliteResult:SATELLITE_RESULT_REQUEST_NOT_SUPPORTED
-     *   SatelliteResult:SATELLITE_RESULT_NO_RESOURCES
-     */
-    void requestIsSatelliteCommunicationAllowedForCurrentLocation(
-            in IIntegerConsumer resultCallback, in IBooleanConsumer callback);
-
-    /**
      * Request to get the time after which the satellite will be visible. This is an int
      * representing the duration in seconds after which the satellite will be visible.
      * This will return 0 if the satellite is currently visible.
diff --git a/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java b/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java
index f17ff17..b7dc79f 100644
--- a/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java
+++ b/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java
@@ -194,17 +194,6 @@
         }
 
         @Override
-        public void requestIsSatelliteCommunicationAllowedForCurrentLocation(
-                IIntegerConsumer resultCallback, IBooleanConsumer callback)
-                throws RemoteException {
-            executeMethodAsync(
-                    () -> SatelliteImplBase.this
-                            .requestIsSatelliteCommunicationAllowedForCurrentLocation(
-                                    resultCallback, callback),
-                    "requestIsCommunicationAllowedForCurrentLocation");
-        }
-
-        @Override
         public void requestTimeForNextSatelliteVisibility(IIntegerConsumer resultCallback,
                 IIntegerConsumer callback) throws RemoteException {
             executeMethodAsync(
@@ -638,30 +627,6 @@
     }
 
     /**
-     * Request to get whether satellite communication is allowed for the current location.
-     *
-     * @param resultCallback The callback to receive the error code result of the operation.
-     *                       This must only be sent when the result is not
-     *                       SatelliteResult#SATELLITE_RESULT_SUCCESS.
-     * @param callback If the result is SatelliteResult#SATELLITE_RESULT_SUCCESS, the callback to
-     *                 receive whether satellite communication is allowed for the current location.
-     *
-     * Valid result codes returned:
-     *   SatelliteResult:SATELLITE_RESULT_SUCCESS
-     *   SatelliteResult:SATELLITE_RESULT_SERVICE_ERROR
-     *   SatelliteResult:SATELLITE_RESULT_MODEM_ERROR
-     *   SatelliteResult:SATELLITE_RESULT_INVALID_MODEM_STATE
-     *   SatelliteResult:SATELLITE_RESULT_INVALID_ARGUMENTS
-     *   SatelliteResult:SATELLITE_RESULT_RADIO_NOT_AVAILABLE
-     *   SatelliteResult:SATELLITE_RESULT_REQUEST_NOT_SUPPORTED
-     *   SatelliteResult:SATELLITE_RESULT_NO_RESOURCES
-     */
-    public void requestIsSatelliteCommunicationAllowedForCurrentLocation(
-            @NonNull IIntegerConsumer resultCallback, @NonNull IBooleanConsumer callback) {
-        // stub implementation
-    }
-
-    /**
      * Request to get the time after which the satellite will be visible. This is an int
      * representing the duration in seconds after which the satellite will be visible.
      * This will return 0 if the satellite is currently visible.
diff --git a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java
index caaee63..4d48276 100644
--- a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java
+++ b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java
@@ -30,10 +30,12 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+@Ignore // b/330376055: Write tests for functionality for both dVRR and MRR devices.
 @RunWith(AndroidJUnit4.class)
 public class SurfaceControlTest {
     private static final String TAG = "SurfaceControlTest";
diff --git a/tests/PackageWatchdog/Android.bp b/tests/PackageWatchdog/Android.bp
index e0e6c4c..2c5fdd3 100644
--- a/tests/PackageWatchdog/Android.bp
+++ b/tests/PackageWatchdog/Android.bp
@@ -28,8 +28,10 @@
     static_libs: [
         "junit",
         "mockito-target-extended-minus-junit4",
+        "flag-junit",
         "frameworks-base-testutils",
         "androidx.test.rules",
+        "PlatformProperties",
         "services.core",
         "services.net",
         "truth",
diff --git a/tests/PackageWatchdog/src/com/android/server/CrashRecoveryTest.java b/tests/PackageWatchdog/src/com/android/server/CrashRecoveryTest.java
new file mode 100644
index 0000000..081da11
--- /dev/null
+++ b/tests/PackageWatchdog/src/com/android/server/CrashRecoveryTest.java
@@ -0,0 +1,644 @@
+/*
+ * 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.server;
+
+import static android.service.watchdog.ExplicitHealthCheckService.PackageConfig;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertFalse;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.VersionedPackage;
+import android.content.rollback.PackageRollbackInfo;
+import android.content.rollback.RollbackInfo;
+import android.content.rollback.RollbackManager;
+import android.crashrecovery.flags.Flags;
+import android.net.ConnectivityModuleConnector;
+import android.net.ConnectivityModuleConnector.ConnectivityModuleHealthListener;
+import android.os.Handler;
+import android.os.SystemProperties;
+import android.os.test.TestLooper;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.provider.DeviceConfig;
+import android.util.AtomicFile;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.server.RescueParty.RescuePartyObserver;
+import com.android.server.pm.ApexManager;
+import com.android.server.rollback.RollbackPackageHealthObserver;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Answers;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+import org.mockito.stubbing.Answer;
+
+import java.io.File;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+/**
+ * Test CrashRecovery, integration tests that include PackageWatchdog, RescueParty and
+ * RollbackPackageHealthObserver
+ */
+public class CrashRecoveryTest {
+    private static final String PROP_DEVICE_CONFIG_DISABLE_FLAG =
+            "persist.device_config.configuration.disable_rescue_party";
+
+    private static final String APP_A = "com.package.a";
+    private static final String APP_B = "com.package.b";
+    private static final String APP_C = "com.package.c";
+    private static final long VERSION_CODE = 1L;
+    private static final long SHORT_DURATION = TimeUnit.SECONDS.toMillis(1);
+
+    private static final RollbackInfo ROLLBACK_INFO_LOW = getRollbackInfo(APP_A, VERSION_CODE, 1,
+                PackageManager.ROLLBACK_USER_IMPACT_LOW);
+    private static final RollbackInfo ROLLBACK_INFO_HIGH = getRollbackInfo(APP_B, VERSION_CODE, 2,
+            PackageManager.ROLLBACK_USER_IMPACT_HIGH);
+    private static final RollbackInfo ROLLBACK_INFO_MANUAL = getRollbackInfo(APP_C, VERSION_CODE, 3,
+            PackageManager.ROLLBACK_USER_IMPACT_ONLY_MANUAL);
+
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+    private final TestClock mTestClock = new TestClock();
+    private TestLooper mTestLooper;
+    private Context mSpyContext;
+    // Keep track of all created watchdogs to apply device config changes
+    private List<PackageWatchdog> mAllocatedWatchdogs;
+    @Mock
+    private ConnectivityModuleConnector mConnectivityModuleConnector;
+    @Mock
+    private PackageManager mMockPackageManager;
+    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+    private ApexManager mApexManager;
+    @Mock
+    RollbackManager mRollbackManager;
+    // Mock only sysprop apis
+    private PackageWatchdog.BootThreshold mSpyBootThreshold;
+    @Captor
+    private ArgumentCaptor<ConnectivityModuleHealthListener> mConnectivityModuleCallbackCaptor;
+    private MockitoSession mSession;
+    private HashMap<String, String> mSystemSettingsMap;
+    private HashMap<String, String> mCrashRecoveryPropertiesMap;
+
+    @Before
+    public void setUp() throws Exception {
+        mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+        MockitoAnnotations.initMocks(this);
+        new File(InstrumentationRegistry.getContext().getFilesDir(),
+                "package-watchdog.xml").delete();
+        adoptShellPermissions(Manifest.permission.READ_DEVICE_CONFIG,
+                Manifest.permission.WRITE_DEVICE_CONFIG);
+        mTestLooper = new TestLooper();
+        mSpyContext = spy(InstrumentationRegistry.getContext());
+        when(mSpyContext.getPackageManager()).thenReturn(mMockPackageManager);
+        when(mMockPackageManager.getPackageInfo(anyString(), anyInt())).then(inv -> {
+            final PackageInfo res = new PackageInfo();
+            res.packageName = inv.getArgument(0);
+            res.setLongVersionCode(VERSION_CODE);
+            return res;
+        });
+        mSession = ExtendedMockito.mockitoSession()
+                .initMocks(this)
+                .strictness(Strictness.LENIENT)
+                .spyStatic(SystemProperties.class)
+                .spyStatic(RescueParty.class)
+                .startMocking();
+        mSystemSettingsMap = new HashMap<>();
+
+        // Mock SystemProperties setter and various getters
+        doAnswer((Answer<Void>) invocationOnMock -> {
+                    String key = invocationOnMock.getArgument(0);
+                    String value = invocationOnMock.getArgument(1);
+
+                    mSystemSettingsMap.put(key, value);
+                    return null;
+                }
+        ).when(() -> SystemProperties.set(anyString(), anyString()));
+
+        doAnswer((Answer<Integer>) invocationOnMock -> {
+                    String key = invocationOnMock.getArgument(0);
+                    int defaultValue = invocationOnMock.getArgument(1);
+
+                    String storedValue = mSystemSettingsMap.get(key);
+                    return storedValue == null ? defaultValue : Integer.parseInt(storedValue);
+                }
+        ).when(() -> SystemProperties.getInt(anyString(), anyInt()));
+
+        doAnswer((Answer<Long>) invocationOnMock -> {
+                    String key = invocationOnMock.getArgument(0);
+                    long defaultValue = invocationOnMock.getArgument(1);
+
+                    String storedValue = mSystemSettingsMap.get(key);
+                    return storedValue == null ? defaultValue : Long.parseLong(storedValue);
+                }
+        ).when(() -> SystemProperties.getLong(anyString(), anyLong()));
+
+        doAnswer((Answer<Boolean>) invocationOnMock -> {
+                    String key = invocationOnMock.getArgument(0);
+                    boolean defaultValue = invocationOnMock.getArgument(1);
+
+                    String storedValue = mSystemSettingsMap.get(key);
+                    return storedValue == null ? defaultValue : Boolean.parseBoolean(storedValue);
+                }
+        ).when(() -> SystemProperties.getBoolean(anyString(), anyBoolean()));
+
+        SystemProperties.set(RescueParty.PROP_ENABLE_RESCUE, Boolean.toString(true));
+        SystemProperties.set(PROP_DEVICE_CONFIG_DISABLE_FLAG, Boolean.toString(false));
+
+        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ROLLBACK,
+                PackageWatchdog.PROPERTY_WATCHDOG_EXPLICIT_HEALTH_CHECK_ENABLED,
+                Boolean.toString(true), false);
+
+        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ROLLBACK,
+                PackageWatchdog.PROPERTY_WATCHDOG_TRIGGER_FAILURE_COUNT,
+                Integer.toString(PackageWatchdog.DEFAULT_TRIGGER_FAILURE_COUNT), false);
+
+        mAllocatedWatchdogs = new ArrayList<>();
+        RescuePartyObserver.reset();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        dropShellPermissions();
+        mSession.finishMocking();
+        // Clean up listeners since too many listeners will delay notifications significantly
+        for (PackageWatchdog watchdog : mAllocatedWatchdogs) {
+            watchdog.removePropertyChangedListener();
+        }
+        mAllocatedWatchdogs.clear();
+    }
+
+    @Test
+    public void testBootLoopWithRescueParty() throws Exception {
+        PackageWatchdog watchdog = createWatchdog();
+        RescuePartyObserver rescuePartyObserver = setUpRescuePartyObserver(watchdog);
+
+        verify(rescuePartyObserver, never()).executeBootLoopMitigation(1);
+        int bootCounter = 0;
+        for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT; i++) {
+            watchdog.noteBoot();
+            bootCounter += 1;
+        }
+        verify(rescuePartyObserver).executeBootLoopMitigation(1);
+        verify(rescuePartyObserver, never()).executeBootLoopMitigation(2);
+
+        for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) {
+            watchdog.noteBoot();
+            bootCounter += 1;
+        }
+        verify(rescuePartyObserver).executeBootLoopMitigation(2);
+        verify(rescuePartyObserver, never()).executeBootLoopMitigation(3);
+
+        int bootLoopThreshold = PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD - bootCounter;
+        for (int i = 0; i < bootLoopThreshold; i++) {
+            watchdog.noteBoot();
+        }
+        verify(rescuePartyObserver).executeBootLoopMitigation(3);
+        verify(rescuePartyObserver, never()).executeBootLoopMitigation(4);
+
+        for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) {
+            watchdog.noteBoot();
+        }
+        verify(rescuePartyObserver).executeBootLoopMitigation(4);
+        verify(rescuePartyObserver, never()).executeBootLoopMitigation(5);
+
+        for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) {
+            watchdog.noteBoot();
+        }
+        verify(rescuePartyObserver).executeBootLoopMitigation(5);
+        verify(rescuePartyObserver, never()).executeBootLoopMitigation(6);
+
+        for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) {
+            watchdog.noteBoot();
+        }
+        verify(rescuePartyObserver).executeBootLoopMitigation(6);
+        verify(rescuePartyObserver, never()).executeBootLoopMitigation(7);
+    }
+
+    @Test
+    public void testBootLoopWithRollbackPackageHealthObserver() throws Exception {
+        PackageWatchdog watchdog = createWatchdog();
+        RollbackPackageHealthObserver rollbackObserver =
+                setUpRollbackPackageHealthObserver(watchdog);
+
+        verify(rollbackObserver, never()).executeBootLoopMitigation(1);
+        int bootCounter = 0;
+        for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT; i++) {
+            watchdog.noteBoot();
+            bootCounter += 1;
+        }
+        verify(rollbackObserver).executeBootLoopMitigation(1);
+        verify(rollbackObserver, never()).executeBootLoopMitigation(2);
+
+        // Update the list of available rollbacks after executing bootloop mitigation once
+        when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(ROLLBACK_INFO_HIGH,
+                ROLLBACK_INFO_MANUAL));
+
+        int bootLoopThreshold = PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD - bootCounter;
+        for (int i = 0; i < bootLoopThreshold; i++) {
+            watchdog.noteBoot();
+        }
+        verify(rollbackObserver).executeBootLoopMitigation(2);
+        verify(rollbackObserver, never()).executeBootLoopMitigation(3);
+
+        // Update the list of available rollbacks after executing bootloop mitigation once
+        when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(ROLLBACK_INFO_MANUAL));
+
+        for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) {
+            watchdog.noteBoot();
+        }
+        verify(rollbackObserver, never()).executeBootLoopMitigation(3);
+    }
+
+    @Test
+    public void testBootLoopWithRescuePartyAndRollbackPackageHealthObserver() throws Exception {
+        PackageWatchdog watchdog = createWatchdog();
+        RescuePartyObserver rescuePartyObserver = setUpRescuePartyObserver(watchdog);
+        RollbackPackageHealthObserver rollbackObserver =
+                setUpRollbackPackageHealthObserver(watchdog);
+
+        verify(rescuePartyObserver, never()).executeBootLoopMitigation(1);
+        verify(rollbackObserver, never()).executeBootLoopMitigation(1);
+        int bootCounter = 0;
+        for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT; i++) {
+            watchdog.noteBoot();
+            bootCounter += 1;
+        }
+        verify(rescuePartyObserver).executeBootLoopMitigation(1);
+        verify(rescuePartyObserver, never()).executeBootLoopMitigation(2);
+        verify(rollbackObserver, never()).executeBootLoopMitigation(1);
+
+        for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) {
+            watchdog.noteBoot();
+            bootCounter += 1;
+        }
+        verify(rescuePartyObserver).executeBootLoopMitigation(2);
+        verify(rescuePartyObserver, never()).executeBootLoopMitigation(3);
+        verify(rollbackObserver, never()).executeBootLoopMitigation(2);
+
+        for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) {
+            watchdog.noteBoot();
+            bootCounter += 1;
+        }
+        verify(rescuePartyObserver, never()).executeBootLoopMitigation(3);
+        verify(rollbackObserver).executeBootLoopMitigation(1);
+        verify(rollbackObserver, never()).executeBootLoopMitigation(2);
+        // Update the list of available rollbacks after executing bootloop mitigation once
+        when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(ROLLBACK_INFO_HIGH,
+                ROLLBACK_INFO_MANUAL));
+
+        int bootLoopThreshold = PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD - bootCounter;
+        for (int i = 0; i < bootLoopThreshold; i++) {
+            watchdog.noteBoot();
+        }
+        verify(rescuePartyObserver).executeBootLoopMitigation(3);
+        verify(rescuePartyObserver, never()).executeBootLoopMitigation(4);
+        verify(rollbackObserver, never()).executeBootLoopMitigation(2);
+
+        for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) {
+            watchdog.noteBoot();
+        }
+        verify(rescuePartyObserver).executeBootLoopMitigation(4);
+        verify(rescuePartyObserver, never()).executeBootLoopMitigation(5);
+        verify(rollbackObserver, never()).executeBootLoopMitigation(2);
+
+        for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) {
+            watchdog.noteBoot();
+        }
+        verify(rescuePartyObserver).executeBootLoopMitigation(5);
+        verify(rescuePartyObserver, never()).executeBootLoopMitigation(6);
+        verify(rollbackObserver, never()).executeBootLoopMitigation(2);
+
+        for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) {
+            watchdog.noteBoot();
+        }
+        verify(rescuePartyObserver, never()).executeBootLoopMitigation(6);
+        verify(rollbackObserver).executeBootLoopMitigation(2);
+        verify(rollbackObserver, never()).executeBootLoopMitigation(3);
+        // Update the list of available rollbacks after executing bootloop mitigation
+        when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(ROLLBACK_INFO_MANUAL));
+
+        for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) {
+            watchdog.noteBoot();
+        }
+        verify(rescuePartyObserver).executeBootLoopMitigation(6);
+        verify(rescuePartyObserver, never()).executeBootLoopMitigation(7);
+        verify(rollbackObserver, never()).executeBootLoopMitigation(3);
+    }
+
+    RollbackPackageHealthObserver setUpRollbackPackageHealthObserver(PackageWatchdog watchdog) {
+        RollbackPackageHealthObserver rollbackObserver =
+                spy(new RollbackPackageHealthObserver(mSpyContext, mApexManager));
+        when(mSpyContext.getSystemService(RollbackManager.class)).thenReturn(mRollbackManager);
+        when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(ROLLBACK_INFO_LOW,
+                ROLLBACK_INFO_HIGH, ROLLBACK_INFO_MANUAL));
+        when(mSpyContext.getPackageManager()).thenReturn(mMockPackageManager);
+
+        watchdog.registerHealthObserver(rollbackObserver);
+        return rollbackObserver;
+    }
+
+    RescuePartyObserver setUpRescuePartyObserver(PackageWatchdog watchdog) {
+        setCrashRecoveryPropRescueBootCount(0);
+        RescuePartyObserver rescuePartyObserver = spy(RescuePartyObserver.getInstance(mSpyContext));
+        assertFalse(RescueParty.isRebootPropertySet());
+        watchdog.registerHealthObserver(rescuePartyObserver);
+        return rescuePartyObserver;
+    }
+
+    private static RollbackInfo getRollbackInfo(String packageName, long versionCode,
+            int rollbackId, int rollbackUserImpact) {
+        VersionedPackage appFrom = new VersionedPackage(packageName, versionCode + 1);
+        VersionedPackage appTo = new VersionedPackage(packageName, versionCode);
+        PackageRollbackInfo packageRollbackInfo = new PackageRollbackInfo(appFrom, appTo, null,
+                null, false, false, null);
+        RollbackInfo rollbackInfo = new RollbackInfo(rollbackId, List.of(packageRollbackInfo),
+                false, null, 111, rollbackUserImpact);
+        return rollbackInfo;
+    }
+
+    private void adoptShellPermissions(String... permissions) {
+        androidx.test.platform.app.InstrumentationRegistry
+                .getInstrumentation()
+                .getUiAutomation()
+                .adoptShellPermissionIdentity(permissions);
+    }
+
+    private void dropShellPermissions() {
+        androidx.test.platform.app.InstrumentationRegistry
+                .getInstrumentation()
+                .getUiAutomation()
+                .dropShellPermissionIdentity();
+    }
+
+
+    private PackageWatchdog createWatchdog() {
+        return createWatchdog(new TestController(), true /* withPackagesReady */);
+    }
+
+    private PackageWatchdog createWatchdog(TestController controller, boolean withPackagesReady) {
+        AtomicFile policyFile =
+                new AtomicFile(new File(mSpyContext.getFilesDir(), "package-watchdog.xml"));
+        Handler handler = new Handler(mTestLooper.getLooper());
+        PackageWatchdog watchdog =
+                new PackageWatchdog(mSpyContext, policyFile, handler, handler, controller,
+                        mConnectivityModuleConnector, mTestClock);
+        mockCrashRecoveryProperties(watchdog);
+
+        // Verify controller is not automatically started
+        assertThat(controller.mIsEnabled).isFalse();
+        if (withPackagesReady) {
+            // Only capture the NetworkStack callback for the latest registered watchdog
+            reset(mConnectivityModuleConnector);
+            watchdog.onPackagesReady();
+            // Verify controller by default is started when packages are ready
+            assertThat(controller.mIsEnabled).isTrue();
+
+            verify(mConnectivityModuleConnector).registerHealthListener(
+                    mConnectivityModuleCallbackCaptor.capture());
+        }
+        mAllocatedWatchdogs.add(watchdog);
+        return watchdog;
+    }
+
+    // Mock CrashRecoveryProperties as they cannot be accessed due to SEPolicy restrictions
+    private void mockCrashRecoveryProperties(PackageWatchdog watchdog) {
+        mCrashRecoveryPropertiesMap = new HashMap<>();
+
+        // mock properties in RescueParty
+        try {
+
+            doAnswer((Answer<Boolean>) invocationOnMock -> {
+                String storedValue = mCrashRecoveryPropertiesMap
+                        .getOrDefault("crashrecovery.attempting_factory_reset", "false");
+                return Boolean.parseBoolean(storedValue);
+            }).when(() -> RescueParty.isFactoryResetPropertySet());
+            doAnswer((Answer<Void>) invocationOnMock -> {
+                boolean value = invocationOnMock.getArgument(0);
+                mCrashRecoveryPropertiesMap.put("crashrecovery.attempting_factory_reset",
+                        Boolean.toString(value));
+                return null;
+            }).when(() -> RescueParty.setFactoryResetProperty(anyBoolean()));
+
+            doAnswer((Answer<Boolean>) invocationOnMock -> {
+                String storedValue = mCrashRecoveryPropertiesMap
+                        .getOrDefault("crashrecovery.attempting_reboot", "false");
+                return Boolean.parseBoolean(storedValue);
+            }).when(() -> RescueParty.isRebootPropertySet());
+            doAnswer((Answer<Void>) invocationOnMock -> {
+                boolean value = invocationOnMock.getArgument(0);
+                setCrashRecoveryPropAttemptingReboot(value);
+                return null;
+            }).when(() -> RescueParty.setRebootProperty(anyBoolean()));
+
+            doAnswer((Answer<Long>) invocationOnMock -> {
+                String storedValue = mCrashRecoveryPropertiesMap
+                        .getOrDefault("persist.crashrecovery.last_factory_reset", "0");
+                return Long.parseLong(storedValue);
+            }).when(() -> RescueParty.getLastFactoryResetTimeMs());
+            doAnswer((Answer<Void>) invocationOnMock -> {
+                long value = invocationOnMock.getArgument(0);
+                setCrashRecoveryPropLastFactoryReset(value);
+                return null;
+            }).when(() -> RescueParty.setLastFactoryResetTimeMs(anyLong()));
+
+            doAnswer((Answer<Integer>) invocationOnMock -> {
+                String storedValue = mCrashRecoveryPropertiesMap
+                        .getOrDefault("crashrecovery.max_rescue_level_attempted", "0");
+                return Integer.parseInt(storedValue);
+            }).when(() -> RescueParty.getMaxRescueLevelAttempted());
+            doAnswer((Answer<Void>) invocationOnMock -> {
+                int value = invocationOnMock.getArgument(0);
+                mCrashRecoveryPropertiesMap.put("crashrecovery.max_rescue_level_attempted",
+                        Integer.toString(value));
+                return null;
+            }).when(() -> RescueParty.setMaxRescueLevelAttempted(anyInt()));
+
+        } catch (Exception e) {
+            // tests will fail, just printing the error
+            System.out.println("Error while mocking crashrecovery properties " + e.getMessage());
+        }
+
+        try {
+            if (Flags.recoverabilityDetection()) {
+                mSpyBootThreshold = spy(watchdog.new BootThreshold(
+                    PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT,
+                    PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS,
+                    PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT));
+            } else {
+                mSpyBootThreshold = spy(watchdog.new BootThreshold(
+                    PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT,
+                    PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS));
+            }
+
+            doAnswer((Answer<Integer>) invocationOnMock -> {
+                String storedValue = mCrashRecoveryPropertiesMap
+                        .getOrDefault("crashrecovery.rescue_boot_count", "0");
+                return Integer.parseInt(storedValue);
+            }).when(mSpyBootThreshold).getCount();
+            doAnswer((Answer<Void>) invocationOnMock -> {
+                int count = invocationOnMock.getArgument(0);
+                mCrashRecoveryPropertiesMap.put("crashrecovery.rescue_boot_count",
+                        Integer.toString(count));
+                return null;
+            }).when(mSpyBootThreshold).setCount(anyInt());
+
+            doAnswer((Answer<Integer>) invocationOnMock -> {
+                String storedValue = mCrashRecoveryPropertiesMap
+                        .getOrDefault("crashrecovery.boot_mitigation_count", "0");
+                return Integer.parseInt(storedValue);
+            }).when(mSpyBootThreshold).getMitigationCount();
+            doAnswer((Answer<Void>) invocationOnMock -> {
+                int count = invocationOnMock.getArgument(0);
+                mCrashRecoveryPropertiesMap.put("crashrecovery.boot_mitigation_count",
+                        Integer.toString(count));
+                return null;
+            }).when(mSpyBootThreshold).setMitigationCount(anyInt());
+
+            doAnswer((Answer<Long>) invocationOnMock -> {
+                String storedValue = mCrashRecoveryPropertiesMap
+                        .getOrDefault("crashrecovery.rescue_boot_start", "0");
+                return Long.parseLong(storedValue);
+            }).when(mSpyBootThreshold).getStart();
+            doAnswer((Answer<Void>) invocationOnMock -> {
+                long count = invocationOnMock.getArgument(0);
+                mCrashRecoveryPropertiesMap.put("crashrecovery.rescue_boot_start",
+                        Long.toString(count));
+                return null;
+            }).when(mSpyBootThreshold).setStart(anyLong());
+
+            doAnswer((Answer<Long>) invocationOnMock -> {
+                String storedValue = mCrashRecoveryPropertiesMap
+                        .getOrDefault("crashrecovery.boot_mitigation_start", "0");
+                return Long.parseLong(storedValue);
+            }).when(mSpyBootThreshold).getMitigationStart();
+            doAnswer((Answer<Void>) invocationOnMock -> {
+                long count = invocationOnMock.getArgument(0);
+                mCrashRecoveryPropertiesMap.put("crashrecovery.boot_mitigation_start",
+                        Long.toString(count));
+                return null;
+            }).when(mSpyBootThreshold).setMitigationStart(anyLong());
+
+            Field mBootThresholdField = watchdog.getClass().getDeclaredField("mBootThreshold");
+            mBootThresholdField.setAccessible(true);
+            mBootThresholdField.set(watchdog, mSpyBootThreshold);
+        } catch (Exception e) {
+            // tests will fail, just printing the error
+            System.out.println("Error detected while spying BootThreshold" + e.getMessage());
+        }
+    }
+
+    private void setCrashRecoveryPropRescueBootCount(int count) {
+        mCrashRecoveryPropertiesMap.put("crashrecovery.rescue_boot_count",
+                Integer.toString(count));
+    }
+
+    private void setCrashRecoveryPropAttemptingReboot(boolean value) {
+        mCrashRecoveryPropertiesMap.put("crashrecovery.attempting_reboot",
+                Boolean.toString(value));
+    }
+
+    private void setCrashRecoveryPropLastFactoryReset(long value) {
+        mCrashRecoveryPropertiesMap.put("persist.crashrecovery.last_factory_reset",
+                Long.toString(value));
+    }
+
+    private static class TestController extends ExplicitHealthCheckController {
+        TestController() {
+            super(null /* controller */);
+        }
+
+        private boolean mIsEnabled;
+        private List<String> mSupportedPackages = new ArrayList<>();
+        private List<String> mRequestedPackages = new ArrayList<>();
+        private Consumer<List<PackageConfig>> mSupportedConsumer;
+        private List<Set> mSyncRequests = new ArrayList<>();
+
+        @Override
+        public void setEnabled(boolean enabled) {
+            mIsEnabled = enabled;
+            if (!mIsEnabled) {
+                mSupportedPackages.clear();
+            }
+        }
+
+        @Override
+        public void setCallbacks(Consumer<String> passedConsumer,
+                Consumer<List<PackageConfig>> supportedConsumer, Runnable notifySyncRunnable) {
+            mSupportedConsumer = supportedConsumer;
+        }
+
+        @Override
+        public void syncRequests(Set<String> packages) {
+            mSyncRequests.add(packages);
+            mRequestedPackages.clear();
+            if (mIsEnabled) {
+                packages.retainAll(mSupportedPackages);
+                mRequestedPackages.addAll(packages);
+                List<PackageConfig> packageConfigs = new ArrayList<>();
+                for (String packageName: packages) {
+                    packageConfigs.add(new PackageConfig(packageName, SHORT_DURATION));
+                }
+                mSupportedConsumer.accept(packageConfigs);
+            } else {
+                mSupportedConsumer.accept(Collections.emptyList());
+            }
+        }
+    }
+
+    private static class TestClock implements PackageWatchdog.SystemClock {
+        // Note 0 is special to the internal clock of PackageWatchdog. We need to start from
+        // a non-zero value in order not to disrupt the logic of PackageWatchdog.
+        private long mUpTimeMillis = 1;
+        @Override
+        public long uptimeMillis() {
+            return mUpTimeMillis;
+        }
+    }
+}
diff --git a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
index 75284c7..4f27e06 100644
--- a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
+++ b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
@@ -36,11 +36,13 @@
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.VersionedPackage;
+import android.crashrecovery.flags.Flags;
 import android.net.ConnectivityModuleConnector;
 import android.net.ConnectivityModuleConnector.ConnectivityModuleHealthListener;
 import android.os.Handler;
 import android.os.SystemProperties;
 import android.os.test.TestLooper;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.provider.DeviceConfig;
 import android.util.AtomicFile;
 import android.util.Xml;
@@ -54,11 +56,13 @@
 import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.PackageWatchdog.HealthCheckState;
 import com.android.server.PackageWatchdog.MonitoredPackage;
+import com.android.server.PackageWatchdog.ObserverInternal;
 import com.android.server.PackageWatchdog.PackageHealthObserver;
 import com.android.server.PackageWatchdog.PackageHealthObserverImpact;
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Captor;
@@ -99,6 +103,10 @@
     private static final String OBSERVER_NAME_4 = "observer4";
     private static final long SHORT_DURATION = TimeUnit.SECONDS.toMillis(1);
     private static final long LONG_DURATION = TimeUnit.SECONDS.toMillis(5);
+
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     private final TestClock mTestClock = new TestClock();
     private TestLooper mTestLooper;
     private Context mSpyContext;
@@ -128,6 +136,7 @@
 
     @Before
     public void setUp() throws Exception {
+        mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
         MockitoAnnotations.initMocks(this);
         new File(InstrumentationRegistry.getContext().getFilesDir(),
                 "package-watchdog.xml").delete();
@@ -444,6 +453,7 @@
      */
     @Test
     public void testPackageFailureNotifyAllDifferentImpacts() throws Exception {
+        mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
         PackageWatchdog watchdog = createWatchdog();
         TestObserver observerNone = new TestObserver(OBSERVER_NAME_1,
                 PackageHealthObserverImpact.USER_IMPACT_LEVEL_0);
@@ -488,6 +498,52 @@
         assertThat(observerLowPackages).containsExactly(APP_A);
     }
 
+    @Test
+    public void testPackageFailureNotifyAllDifferentImpactsRecoverability() throws Exception {
+        PackageWatchdog watchdog = createWatchdog();
+        TestObserver observerNone = new TestObserver(OBSERVER_NAME_1,
+                PackageHealthObserverImpact.USER_IMPACT_LEVEL_0);
+        TestObserver observerHigh = new TestObserver(OBSERVER_NAME_2,
+                PackageHealthObserverImpact.USER_IMPACT_LEVEL_50);
+        TestObserver observerMid = new TestObserver(OBSERVER_NAME_3,
+                PackageHealthObserverImpact.USER_IMPACT_LEVEL_30);
+        TestObserver observerLow = new TestObserver(OBSERVER_NAME_4,
+                PackageHealthObserverImpact.USER_IMPACT_LEVEL_10);
+
+        // Start observing for all impact observers
+        watchdog.startObservingHealth(observerNone, Arrays.asList(APP_A, APP_B, APP_C, APP_D),
+                SHORT_DURATION);
+        watchdog.startObservingHealth(observerHigh, Arrays.asList(APP_A, APP_B, APP_C),
+                SHORT_DURATION);
+        watchdog.startObservingHealth(observerMid, Arrays.asList(APP_A, APP_B),
+                SHORT_DURATION);
+        watchdog.startObservingHealth(observerLow, Arrays.asList(APP_A),
+                SHORT_DURATION);
+
+        // Then fail all apps above the threshold
+        raiseFatalFailureAndDispatch(watchdog,
+                Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE),
+                        new VersionedPackage(APP_B, VERSION_CODE),
+                        new VersionedPackage(APP_C, VERSION_CODE),
+                        new VersionedPackage(APP_D, VERSION_CODE)),
+                PackageWatchdog.FAILURE_REASON_UNKNOWN);
+
+        // Verify least impact observers are notifed of package failures
+        List<String> observerNonePackages = observerNone.mMitigatedPackages;
+        List<String> observerHighPackages = observerHigh.mMitigatedPackages;
+        List<String> observerMidPackages = observerMid.mMitigatedPackages;
+        List<String> observerLowPackages = observerLow.mMitigatedPackages;
+
+        // APP_D failure observed by only observerNone is not caught cos its impact is none
+        assertThat(observerNonePackages).isEmpty();
+        // APP_C failure is caught by observerHigh cos it's the lowest impact observer
+        assertThat(observerHighPackages).containsExactly(APP_C);
+        // APP_B failure is caught by observerMid cos it's the lowest impact observer
+        assertThat(observerMidPackages).containsExactly(APP_B);
+        // APP_A failure is caught by observerLow cos it's the lowest impact observer
+        assertThat(observerLowPackages).containsExactly(APP_A);
+    }
+
     /**
      * Test package failure and least impact observers are notified successively.
      * State transistions:
@@ -501,6 +557,7 @@
      */
     @Test
     public void testPackageFailureNotifyLeastImpactSuccessively() throws Exception {
+        mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
         PackageWatchdog watchdog = createWatchdog();
         TestObserver observerFirst = new TestObserver(OBSERVER_NAME_1,
                 PackageHealthObserverImpact.USER_IMPACT_LEVEL_10);
@@ -563,11 +620,76 @@
         assertThat(observerSecond.mMitigatedPackages).isEmpty();
     }
 
+    @Test
+    public void testPackageFailureNotifyLeastImpactSuccessivelyRecoverability() throws Exception {
+        PackageWatchdog watchdog = createWatchdog();
+        TestObserver observerFirst = new TestObserver(OBSERVER_NAME_1,
+                PackageHealthObserverImpact.USER_IMPACT_LEVEL_10);
+        TestObserver observerSecond = new TestObserver(OBSERVER_NAME_2,
+                PackageHealthObserverImpact.USER_IMPACT_LEVEL_30);
+
+        // Start observing for observerFirst and observerSecond with failure handling
+        watchdog.startObservingHealth(observerFirst, Arrays.asList(APP_A), LONG_DURATION);
+        watchdog.startObservingHealth(observerSecond, Arrays.asList(APP_A), LONG_DURATION);
+
+        // Then fail APP_A above the threshold
+        raiseFatalFailureAndDispatch(watchdog,
+                Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)),
+                PackageWatchdog.FAILURE_REASON_UNKNOWN);
+
+        // Verify only observerFirst is notifed
+        assertThat(observerFirst.mMitigatedPackages).containsExactly(APP_A);
+        assertThat(observerSecond.mMitigatedPackages).isEmpty();
+
+        // After observerFirst handles failure, next action it has is high impact
+        observerFirst.mImpact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_50;
+        observerFirst.mMitigatedPackages.clear();
+        observerSecond.mMitigatedPackages.clear();
+
+        // Then fail APP_A again above the threshold
+        raiseFatalFailureAndDispatch(watchdog,
+                Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)),
+                PackageWatchdog.FAILURE_REASON_UNKNOWN);
+
+        // Verify only observerSecond is notifed cos it has least impact
+        assertThat(observerSecond.mMitigatedPackages).containsExactly(APP_A);
+        assertThat(observerFirst.mMitigatedPackages).isEmpty();
+
+        // After observerSecond handles failure, it has no further actions
+        observerSecond.mImpact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
+        observerFirst.mMitigatedPackages.clear();
+        observerSecond.mMitigatedPackages.clear();
+
+        // Then fail APP_A again above the threshold
+        raiseFatalFailureAndDispatch(watchdog,
+                Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)),
+                PackageWatchdog.FAILURE_REASON_UNKNOWN);
+
+        // Verify only observerFirst is notifed cos it has the only action
+        assertThat(observerFirst.mMitigatedPackages).containsExactly(APP_A);
+        assertThat(observerSecond.mMitigatedPackages).isEmpty();
+
+        // After observerFirst handles failure, it too has no further actions
+        observerFirst.mImpact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
+        observerFirst.mMitigatedPackages.clear();
+        observerSecond.mMitigatedPackages.clear();
+
+        // Then fail APP_A again above the threshold
+        raiseFatalFailureAndDispatch(watchdog,
+                Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)),
+                PackageWatchdog.FAILURE_REASON_UNKNOWN);
+
+        // Verify no observer is notified cos no actions left
+        assertThat(observerFirst.mMitigatedPackages).isEmpty();
+        assertThat(observerSecond.mMitigatedPackages).isEmpty();
+    }
+
     /**
      * Test package failure and notifies only one observer even with observer impact tie.
      */
     @Test
     public void testPackageFailureNotifyOneSameImpact() throws Exception {
+        mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
         PackageWatchdog watchdog = createWatchdog();
         TestObserver observer1 = new TestObserver(OBSERVER_NAME_1,
                 PackageHealthObserverImpact.USER_IMPACT_LEVEL_100);
@@ -588,6 +710,28 @@
         assertThat(observer2.mMitigatedPackages).isEmpty();
     }
 
+    @Test
+    public void testPackageFailureNotifyOneSameImpactRecoverabilityDetection() throws Exception {
+        PackageWatchdog watchdog = createWatchdog();
+        TestObserver observer1 = new TestObserver(OBSERVER_NAME_1,
+                PackageHealthObserverImpact.USER_IMPACT_LEVEL_50);
+        TestObserver observer2 = new TestObserver(OBSERVER_NAME_2,
+                PackageHealthObserverImpact.USER_IMPACT_LEVEL_50);
+
+        // Start observing for observer1 and observer2 with failure handling
+        watchdog.startObservingHealth(observer2, Arrays.asList(APP_A), SHORT_DURATION);
+        watchdog.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION);
+
+        // Then fail APP_A above the threshold
+        raiseFatalFailureAndDispatch(watchdog,
+                Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)),
+                PackageWatchdog.FAILURE_REASON_UNKNOWN);
+
+        // Verify only one observer is notifed
+        assertThat(observer1.mMitigatedPackages).containsExactly(APP_A);
+        assertThat(observer2.mMitigatedPackages).isEmpty();
+    }
+
     /**
      * Test package passing explicit health checks does not fail and vice versa.
      */
@@ -818,6 +962,7 @@
 
     @Test
     public void testNetworkStackFailure() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
         final PackageWatchdog wd = createWatchdog();
 
         // Start observing with failure handling
@@ -835,6 +980,25 @@
         assertThat(observer.mMitigatedPackages).containsExactly(APP_A);
     }
 
+    @Test
+    public void testNetworkStackFailureRecoverabilityDetection() {
+        final PackageWatchdog wd = createWatchdog();
+
+        // Start observing with failure handling
+        TestObserver observer = new TestObserver(OBSERVER_NAME_1,
+                PackageHealthObserverImpact.USER_IMPACT_LEVEL_100);
+        wd.startObservingHealth(observer, Collections.singletonList(APP_A), SHORT_DURATION);
+
+        // Notify of NetworkStack failure
+        mConnectivityModuleCallbackCaptor.getValue().onNetworkStackFailure(APP_A);
+
+        // Run handler so package failures are dispatched to observers
+        mTestLooper.dispatchAll();
+
+        // Verify the NetworkStack observer is notified
+        assertThat(observer.mMitigatedPackages).isEmpty();
+    }
+
     /** Test default values are used when device property is invalid. */
     @Test
     public void testInvalidConfig_watchdogTriggerFailureCount() {
@@ -1045,6 +1209,7 @@
     /** Ensure that boot loop mitigation is done when the number of boots meets the threshold. */
     @Test
     public void testBootLoopDetection_meetsThreshold() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
         PackageWatchdog watchdog = createWatchdog();
         TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1);
         watchdog.registerHealthObserver(bootObserver);
@@ -1054,6 +1219,16 @@
         assertThat(bootObserver.mitigatedBootLoop()).isTrue();
     }
 
+    @Test
+    public void testBootLoopDetection_meetsThresholdRecoverability() {
+        PackageWatchdog watchdog = createWatchdog();
+        TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1);
+        watchdog.registerHealthObserver(bootObserver);
+        for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD; i++) {
+            watchdog.noteBoot();
+        }
+        assertThat(bootObserver.mitigatedBootLoop()).isTrue();
+    }
 
     /**
      * Ensure that boot loop mitigation is not done when the number of boots does not meet the
@@ -1071,10 +1246,43 @@
     }
 
     /**
+     * Ensure that boot loop mitigation is not done when the number of boots does not meet the
+     * threshold.
+     */
+    @Test
+    public void testBootLoopDetection_doesNotMeetThresholdRecoverabilityLowImpact() {
+        PackageWatchdog watchdog = createWatchdog();
+        TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1,
+                PackageHealthObserverImpact.USER_IMPACT_LEVEL_30);
+        watchdog.registerHealthObserver(bootObserver);
+        for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT - 1; i++) {
+            watchdog.noteBoot();
+        }
+        assertThat(bootObserver.mitigatedBootLoop()).isFalse();
+    }
+
+    /**
+     * Ensure that boot loop mitigation is not done when the number of boots does not meet the
+     * threshold.
+     */
+    @Test
+    public void testBootLoopDetection_doesNotMeetThresholdRecoverabilityHighImpact() {
+        PackageWatchdog watchdog = createWatchdog();
+        TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1,
+                PackageHealthObserverImpact.USER_IMPACT_LEVEL_80);
+        watchdog.registerHealthObserver(bootObserver);
+        for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD - 1; i++) {
+            watchdog.noteBoot();
+        }
+        assertThat(bootObserver.mitigatedBootLoop()).isFalse();
+    }
+
+    /**
      * Ensure that boot loop mitigation is done for the observer with the lowest user impact
      */
     @Test
     public void testBootLoopMitigationDoneForLowestUserImpact() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
         PackageWatchdog watchdog = createWatchdog();
         TestObserver bootObserver1 = new TestObserver(OBSERVER_NAME_1);
         bootObserver1.setImpact(PackageHealthObserverImpact.USER_IMPACT_LEVEL_10);
@@ -1089,11 +1297,28 @@
         assertThat(bootObserver2.mitigatedBootLoop()).isFalse();
     }
 
+    @Test
+    public void testBootLoopMitigationDoneForLowestUserImpactRecoverability() {
+        PackageWatchdog watchdog = createWatchdog();
+        TestObserver bootObserver1 = new TestObserver(OBSERVER_NAME_1);
+        bootObserver1.setImpact(PackageHealthObserverImpact.USER_IMPACT_LEVEL_10);
+        TestObserver bootObserver2 = new TestObserver(OBSERVER_NAME_2);
+        bootObserver2.setImpact(PackageHealthObserverImpact.USER_IMPACT_LEVEL_30);
+        watchdog.registerHealthObserver(bootObserver1);
+        watchdog.registerHealthObserver(bootObserver2);
+        for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD; i++) {
+            watchdog.noteBoot();
+        }
+        assertThat(bootObserver1.mitigatedBootLoop()).isTrue();
+        assertThat(bootObserver2.mitigatedBootLoop()).isFalse();
+    }
+
     /**
      * Ensure that the correct mitigation counts are sent to the boot loop observer.
      */
     @Test
     public void testMultipleBootLoopMitigation() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
         PackageWatchdog watchdog = createWatchdog();
         TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1);
         watchdog.registerHealthObserver(bootObserver);
@@ -1114,6 +1339,64 @@
         assertThat(bootObserver.mBootMitigationCounts).isEqualTo(List.of(1, 2, 3, 4, 1, 2, 3, 4));
     }
 
+    @Test
+    public void testMultipleBootLoopMitigationRecoverabilityLowImpact() {
+        PackageWatchdog watchdog = createWatchdog();
+        TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1,
+                PackageHealthObserverImpact.USER_IMPACT_LEVEL_30);
+        watchdog.registerHealthObserver(bootObserver);
+        for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT - 1; j++) {
+            watchdog.noteBoot();
+        }
+        for (int i = 0; i < 4; i++) {
+            for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; j++) {
+                watchdog.noteBoot();
+            }
+        }
+
+        moveTimeForwardAndDispatch(PackageWatchdog.DEFAULT_DEESCALATION_WINDOW_MS + 1);
+
+        for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT - 1; j++) {
+            watchdog.noteBoot();
+        }
+        for (int i = 0; i < 4; i++) {
+            for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; j++) {
+                watchdog.noteBoot();
+            }
+        }
+
+        assertThat(bootObserver.mBootMitigationCounts).isEqualTo(List.of(1, 2, 3, 4, 1, 2, 3, 4));
+    }
+
+    @Test
+    public void testMultipleBootLoopMitigationRecoverabilityHighImpact() {
+        PackageWatchdog watchdog = createWatchdog();
+        TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1,
+                PackageHealthObserverImpact.USER_IMPACT_LEVEL_80);
+        watchdog.registerHealthObserver(bootObserver);
+        for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD - 1; j++) {
+            watchdog.noteBoot();
+        }
+        for (int i = 0; i < 4; i++) {
+            for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; j++) {
+                watchdog.noteBoot();
+            }
+        }
+
+        moveTimeForwardAndDispatch(PackageWatchdog.DEFAULT_DEESCALATION_WINDOW_MS + 1);
+
+        for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD - 1; j++) {
+            watchdog.noteBoot();
+        }
+        for (int i = 0; i < 4; i++) {
+            for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; j++) {
+                watchdog.noteBoot();
+            }
+        }
+
+        assertThat(bootObserver.mBootMitigationCounts).isEqualTo(List.of(1, 2, 3, 4, 1, 2, 3, 4));
+    }
+
     /**
      * Ensure that passing a null list of failed packages does not cause any mitigation logic to
      * execute.
@@ -1304,6 +1587,78 @@
     }
 
     /**
+     * Ensure that a {@link ObserverInternal} may be correctly written and read in order to persist
+     * across reboots.
+     */
+    @Test
+    @SuppressWarnings("GuardedBy")
+    public void testWritingAndReadingObserverInternalRecoverability() throws Exception {
+        PackageWatchdog watchdog = createWatchdog();
+
+        LongArrayQueue mitigationCalls = new LongArrayQueue();
+        mitigationCalls.addLast(1000);
+        mitigationCalls.addLast(2000);
+        mitigationCalls.addLast(3000);
+        MonitoredPackage writePkg = watchdog.newMonitoredPackage(
+                "test.package", 1000, 2000, true, mitigationCalls);
+        final int bootMitigationCount = 4;
+        ObserverInternal writeObserver = new ObserverInternal("test", List.of(writePkg),
+                bootMitigationCount);
+
+        // Write the observer
+        File tmpFile = File.createTempFile("observer-watchdog-test", ".xml");
+        AtomicFile testFile = new AtomicFile(tmpFile);
+        FileOutputStream stream = testFile.startWrite();
+        TypedXmlSerializer outputSerializer = Xml.resolveSerializer(stream);
+        outputSerializer.startDocument(null, true);
+        writeObserver.writeLocked(outputSerializer);
+        outputSerializer.endDocument();
+        testFile.finishWrite(stream);
+
+        // Read the observer
+        TypedXmlPullParser parser = Xml.resolvePullParser(testFile.openRead());
+        XmlUtils.beginDocument(parser, "observer");
+        ObserverInternal readObserver = ObserverInternal.read(parser, watchdog);
+
+        assertThat(readObserver.name).isEqualTo(writeObserver.name);
+        assertThat(readObserver.getBootMitigationCount()).isEqualTo(bootMitigationCount);
+    }
+
+    /**
+     * Ensure that boot mitigation counts may be correctly written and read as metadata
+     * in order to persist across reboots.
+     */
+    @Test
+    @SuppressWarnings("GuardedBy")
+    public void testWritingAndReadingMetadataBootMitigationCountRecoverability() throws Exception {
+        PackageWatchdog watchdog = createWatchdog();
+        String filePath = InstrumentationRegistry.getContext().getFilesDir().toString()
+                + "metadata_file.txt";
+
+        ObserverInternal observer1 = new ObserverInternal("test1", List.of(), 1);
+        ObserverInternal observer2 = new ObserverInternal("test2", List.of(), 2);
+        watchdog.registerObserverInternal(observer1);
+        watchdog.registerObserverInternal(observer2);
+
+        mSpyBootThreshold = spy(watchdog.new BootThreshold(
+                PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT,
+                PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS,
+                PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT));
+
+        watchdog.saveAllObserversBootMitigationCountToMetadata(filePath);
+
+        observer1.setBootMitigationCount(0);
+        observer2.setBootMitigationCount(0);
+        assertThat(observer1.getBootMitigationCount()).isEqualTo(0);
+        assertThat(observer2.getBootMitigationCount()).isEqualTo(0);
+
+        mSpyBootThreshold.readAllObserversBootMitigationCountIfNecessary(filePath);
+
+        assertThat(observer1.getBootMitigationCount()).isEqualTo(1);
+        assertThat(observer2.getBootMitigationCount()).isEqualTo(2);
+    }
+
+    /**
      * Tests device config changes are propagated correctly.
      */
     @Test
@@ -1440,11 +1795,19 @@
 
     // Mock CrashRecoveryProperties as they cannot be accessed due to SEPolicy restrictions
     private void mockCrashRecoveryProperties(PackageWatchdog watchdog) {
+        mCrashRecoveryPropertiesMap = new HashMap<>();
+
         try {
-            mSpyBootThreshold = spy(watchdog.new BootThreshold(
-                PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT,
-                PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS));
-            mCrashRecoveryPropertiesMap = new HashMap<>();
+            if (Flags.recoverabilityDetection()) {
+                mSpyBootThreshold = spy(watchdog.new BootThreshold(
+                    PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT,
+                    PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS,
+                    PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT));
+            } else {
+                mSpyBootThreshold = spy(watchdog.new BootThreshold(
+                    PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT,
+                    PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS));
+            }
 
             doAnswer((Answer<Integer>) invocationOnMock -> {
                 String storedValue = mCrashRecoveryPropertiesMap
diff --git a/tests/inputmethod/ConcurrentMultiSessionImeTest/Android.bp b/tests/inputmethod/ConcurrentMultiSessionImeTest/Android.bp
index 0e0d212..8d05a97 100644
--- a/tests/inputmethod/ConcurrentMultiSessionImeTest/Android.bp
+++ b/tests/inputmethod/ConcurrentMultiSessionImeTest/Android.bp
@@ -26,11 +26,6 @@
         "platform-test-annotations",
         "platform-test-rules",
         "truth",
-
-        // beadstead
-        "Nene",
-        "Harrier",
-        "TestApp",
     ],
     test_suites: [
         "general-tests",
diff --git a/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java b/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java
index 867c0a6..b66ceba 100644
--- a/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java
+++ b/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java
@@ -23,20 +23,14 @@
 
 import androidx.test.platform.app.InstrumentationRegistry;
 
-import com.android.bedstead.harrier.BedsteadJUnit4;
-import com.android.bedstead.harrier.DeviceState;
-
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
-@RunWith(BedsteadJUnit4.class)
+@RunWith(JUnit4.class)
 public final class ConcurrentMultiUserTest {
 
-    @Rule
-    public static final DeviceState sDeviceState = new DeviceState();
-
     @Before
     public void doBeforeEachTest() {
         // No op
diff --git a/tools/app_metadata_bundles/Android.bp b/tools/app_metadata_bundles/Android.bp
new file mode 100644
index 0000000..be6bea6
--- /dev/null
+++ b/tools/app_metadata_bundles/Android.bp
@@ -0,0 +1,26 @@
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+java_library_host {
+    name: "asllib",
+    srcs: [
+        "src/lib/java/**/*.java",
+    ],
+}
+
+java_binary_host {
+    name: "aslgen",
+    manifest: "src/aslgen/aslgen.mf",
+    srcs: [
+        "src/aslgen/java/**/*.java",
+    ],
+    static_libs: [
+        "asllib",
+    ],
+}
diff --git a/tools/app_metadata_bundles/OWNERS b/tools/app_metadata_bundles/OWNERS
new file mode 100644
index 0000000..a2a250b
--- /dev/null
+++ b/tools/app_metadata_bundles/OWNERS
@@ -0,0 +1,2 @@
+wenhaowang@google.com
+mloh@google.com
diff --git a/tools/app_metadata_bundles/README.md b/tools/app_metadata_bundles/README.md
new file mode 100644
index 0000000..6e8d287
--- /dev/null
+++ b/tools/app_metadata_bundles/README.md
@@ -0,0 +1,9 @@
+# App metadata bundles
+
+This project delivers a comprehensive toolchain solution for developers
+to efficiently manage app metadata bundles.
+
+The project consists of two subprojects:
+
+  * A pure Java library, and
+  * A pure Java command-line tool.
diff --git a/tools/app_metadata_bundles/src/aslgen/aslgen.mf b/tools/app_metadata_bundles/src/aslgen/aslgen.mf
new file mode 100644
index 0000000..fc656e2
--- /dev/null
+++ b/tools/app_metadata_bundles/src/aslgen/aslgen.mf
@@ -0,0 +1 @@
+Main-Class: com.android.aslgen.Main
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/aslgen/java/com/android/aslgen/Main.java b/tools/app_metadata_bundles/src/aslgen/java/com/android/aslgen/Main.java
new file mode 100644
index 0000000..df003b6
--- /dev/null
+++ b/tools/app_metadata_bundles/src/aslgen/java/com/android/aslgen/Main.java
@@ -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.aslgen;
+
+import com.android.asllib.AndroidSafetyLabel;
+import com.android.asllib.AndroidSafetyLabel.Format;
+
+import org.xml.sax.SAXException;
+
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.TransformerException;
+
+public class Main {
+
+    /** Takes the options to make file conversion. */
+    public static void main(String[] args)
+            throws IOException, ParserConfigurationException, SAXException, TransformerException {
+
+        String inFile = null;
+        String outFile = null;
+        Format inFormat = Format.NULL;
+        Format outFormat = Format.NULL;
+
+
+        // Except for "--help", all arguments require a value currently.
+        // So just make sure we have an even number and
+        // then process them all two at a time.
+        if (args.length == 1 && "--help".equals(args[0])) {
+            showUsage();
+            return;
+        }
+        if (args.length % 2 != 0) {
+            throw new IllegalArgumentException("Argument is missing corresponding value");
+        }
+        for (int i = 0; i < args.length - 1; i += 2) {
+            final String arg = args[i].trim();
+            final String argValue = args[i + 1].trim();
+            if ("--in-path".equals(arg)) {
+                inFile = argValue;
+            } else if ("--out-path".equals(arg)) {
+                outFile = argValue;
+            } else if ("--in-format".equals(arg)) {
+                inFormat = getFormat(argValue);
+            } else if ("--out-format".equals(arg)) {
+                outFormat = getFormat(argValue);
+            } else {
+                throw new IllegalArgumentException("Unknown argument: " + arg);
+            }
+        }
+
+        if (inFile == null) {
+            throw new IllegalArgumentException("input file is required");
+        }
+
+        if (outFile == null) {
+            throw new IllegalArgumentException("output file is required");
+        }
+
+        if (inFormat == Format.NULL) {
+            throw new IllegalArgumentException("input format is required");
+        }
+
+        if (outFormat == Format.NULL) {
+            throw new IllegalArgumentException("output format is required");
+        }
+
+        System.out.println("in path: " + inFile);
+        System.out.println("out path: " + outFile);
+        System.out.println("in format: " + inFormat);
+        System.out.println("out format: " + outFormat);
+
+        var asl = AndroidSafetyLabel.readFromStream(new FileInputStream(inFile), inFormat);
+        asl.writeToStream(new FileOutputStream(outFile), outFormat);
+    }
+
+    private static Format getFormat(String argValue) {
+        if ("hr".equals(argValue)) {
+            return Format.HUMAN_READABLE;
+        } else if ("od".equals(argValue)) {
+            return Format.ON_DEVICE;
+        } else {
+            return Format.NULL;
+        }
+    }
+
+    private static void showUsage() {
+        AndroidSafetyLabel.test();
+        System.err.println(
+                "Usage:\n"
+        );
+    }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabel.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabel.java
new file mode 100644
index 0000000..0f7ce68
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabel.java
@@ -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.asllib;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.List;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+
+public class AndroidSafetyLabel implements AslMarshallable {
+
+    public enum Format {
+        NULL, HUMAN_READABLE, ON_DEVICE;
+    }
+
+    private final SafetyLabels mSafetyLabels;
+
+    public SafetyLabels getSafetyLabels() {
+        return mSafetyLabels;
+    }
+
+    public AndroidSafetyLabel(SafetyLabels safetyLabels) {
+        this.mSafetyLabels = safetyLabels;
+    }
+
+    /** Reads a {@link AndroidSafetyLabel} from an {@link InputStream}. */
+    // TODO(b/329902686): Support parsing from on-device.
+    public static AndroidSafetyLabel readFromStream(InputStream in, Format format)
+            throws IOException, ParserConfigurationException, SAXException {
+        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+        factory.setNamespaceAware(true);
+        Document document = factory.newDocumentBuilder().parse(in);
+
+        switch (format) {
+            case HUMAN_READABLE:
+                Element appMetadataBundles =
+                        XmlUtils.getSingleElement(document, XmlUtils.HR_TAG_APP_METADATA_BUNDLES);
+
+                return new AndroidSafetyLabelFactory()
+                        .createFromHrElements(
+                                XmlUtils.asElementList(
+                                        document.getElementsByTagName(
+                                                XmlUtils.HR_TAG_APP_METADATA_BUNDLES)));
+            case ON_DEVICE:
+                throw new IllegalArgumentException(
+                        "Parsing from on-device format is not supported at this time.");
+            default:
+                throw new IllegalStateException("Unrecognized input format.");
+        }
+    }
+
+    /** Write the content of the {@link AndroidSafetyLabel} to a {@link OutputStream}. */
+    // TODO(b/329902686): Support outputting human-readable format.
+    public void writeToStream(OutputStream out, Format format)
+            throws IOException, ParserConfigurationException, TransformerException {
+        var docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
+        var document = docBuilder.newDocument();
+
+        switch (format) {
+            case HUMAN_READABLE:
+                throw new IllegalArgumentException(
+                        "Outputting human-readable format is not supported at this time.");
+            case ON_DEVICE:
+                for (var child : this.toOdDomElements(document)) {
+                    document.appendChild(child);
+                }
+                break;
+            default:
+                throw new IllegalStateException("Unrecognized input format.");
+        }
+
+        TransformerFactory transformerFactory = TransformerFactory.newInstance();
+        Transformer transformer = transformerFactory.newTransformer();
+        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+        transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
+        transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
+        StreamResult streamResult = new StreamResult(out); // out
+        DOMSource domSource = new DOMSource(document);
+        transformer.transform(domSource, streamResult);
+    }
+
+    /** Creates an on-device DOM element from an {@link AndroidSafetyLabel} */
+    @Override
+    public List<Element> toOdDomElements(Document doc) {
+        Element aslEle = doc.createElement(XmlUtils.OD_TAG_BUNDLE);
+        XmlUtils.appendChildren(aslEle, mSafetyLabels.toOdDomElements(doc));
+        return List.of(aslEle);
+    }
+
+    public static void test() {
+        // TODO(b/329902686): Add tests.
+    }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabelFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabelFactory.java
new file mode 100644
index 0000000..9b0f05b
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabelFactory.java
@@ -0,0 +1,36 @@
+/*
+ * 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.asllib;
+
+import org.w3c.dom.Element;
+
+import java.util.List;
+
+public class AndroidSafetyLabelFactory implements AslMarshallableFactory<AndroidSafetyLabel> {
+
+    /** Creates an {@link AndroidSafetyLabel} from human-readable DOM element */
+    @Override
+    public AndroidSafetyLabel createFromHrElements(List<Element> appMetadataBundles) {
+        Element appMetadataBundlesEle = XmlUtils.getSingleElement(appMetadataBundles);
+        Element safetyLabelsEle =
+                XmlUtils.getSingleChildElement(
+                        appMetadataBundlesEle, XmlUtils.HR_TAG_SAFETY_LABELS);
+        SafetyLabels safetyLabels =
+                new SafetyLabelsFactory().createFromHrElements(List.of(safetyLabelsEle));
+        return new AndroidSafetyLabel(safetyLabels);
+    }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallable.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallable.java
new file mode 100644
index 0000000..4e64ab0
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallable.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.asllib;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.util.List;
+
+public interface AslMarshallable {
+
+    /** Creates the on-device DOM element from the AslMarshallable Java Object. */
+    List<Element> toOdDomElements(Document doc);
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallableFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallableFactory.java
new file mode 100644
index 0000000..b607353
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallableFactory.java
@@ -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.asllib;
+
+import org.w3c.dom.Element;
+
+import java.util.List;
+
+public interface AslMarshallableFactory<T extends AslMarshallable> {
+
+    /** Creates an {@link AslMarshallableFactory} from human-readable DOM element */
+    T createFromHrElements(List<Element> elements);
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategory.java
new file mode 100644
index 0000000..e5ed63b
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategory.java
@@ -0,0 +1,58 @@
+/*
+ * 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.asllib;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Data usage category representation containing one or more {@link DataType}. Valid category keys
+ * are defined in {@link DataCategoryConstants}, each category has a valid set of types {@link
+ * DataType}, which are mapped in {@link DataTypeConstants}
+ */
+public class DataCategory implements AslMarshallable {
+    private final String mCategoryName;
+    private final Map<String, DataType> mDataTypes;
+
+    public DataCategory(String categoryName, Map<String, DataType> dataTypes) {
+        this.mCategoryName = categoryName;
+        this.mDataTypes = dataTypes;
+    }
+
+    public String getCategoryName() {
+        return mCategoryName;
+    }
+
+    /** Return the type {@link Map} of String type key to {@link DataType} */
+
+    public Map<String, DataType> getDataTypes() {
+        return mDataTypes;
+    }
+
+    /** Creates on-device DOM element(s) from the {@link DataCategory}. */
+    @Override
+    public List<Element> toOdDomElements(Document doc) {
+        Element dataCategoryEle = XmlUtils.createPbundleEleWithName(doc, this.getCategoryName());
+        for (DataType dataType : mDataTypes.values()) {
+            XmlUtils.appendChildren(dataCategoryEle, dataType.toOdDomElements(doc));
+        }
+        return List.of(dataCategoryEle);
+    }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryConstants.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryConstants.java
new file mode 100644
index 0000000..b364c8b
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryConstants.java
@@ -0,0 +1,74 @@
+/*
+ * 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.asllib;
+
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Constants for determining valid {@link String} data types for usage within {@link SafetyLabels},
+ * {@link DataCategory}, and {@link DataType}
+ */
+public class DataCategoryConstants {
+
+    public static final String CATEGORY_PERSONAL = "personal";
+    public static final String CATEGORY_FINANCIAL = "financial";
+    public static final String CATEGORY_LOCATION = "location";
+    public static final String CATEGORY_EMAIL_TEXT_MESSAGE = "email_text_message";
+    public static final String CATEGORY_PHOTO_VIDEO = "photo_video";
+    public static final String CATEGORY_AUDIO = "audio";
+    public static final String CATEGORY_STORAGE = "storage";
+    public static final String CATEGORY_HEALTH_FITNESS = "health_fitness";
+    public static final String CATEGORY_CONTACTS = "contacts";
+    public static final String CATEGORY_CALENDAR = "calendar";
+    public static final String CATEGORY_IDENTIFIERS = "identifiers";
+    public static final String CATEGORY_APP_PERFORMANCE = "app_performance";
+    public static final String CATEGORY_ACTIONS_IN_APP = "actions_in_app";
+    public static final String CATEGORY_SEARCH_AND_BROWSING = "search_and_browsing";
+
+    /** Set of valid categories */
+    public static final Set<String> VALID_CATEGORIES =
+            Collections.unmodifiableSet(
+                    new HashSet<>(
+                            Arrays.asList(
+                                    CATEGORY_PERSONAL,
+                                    CATEGORY_FINANCIAL,
+                                    CATEGORY_LOCATION,
+                                    CATEGORY_EMAIL_TEXT_MESSAGE,
+                                    CATEGORY_PHOTO_VIDEO,
+                                    CATEGORY_AUDIO,
+                                    CATEGORY_STORAGE,
+                                    CATEGORY_HEALTH_FITNESS,
+                                    CATEGORY_CONTACTS,
+                                    CATEGORY_CALENDAR,
+                                    CATEGORY_IDENTIFIERS,
+                                    CATEGORY_APP_PERFORMANCE,
+                                    CATEGORY_ACTIONS_IN_APP,
+                                    CATEGORY_SEARCH_AND_BROWSING)));
+
+    /** Returns {@link Set} of valid {@link String} category keys */
+    public static Set<String> getValidDataCategories() {
+        return VALID_CATEGORIES;
+    }
+
+    private DataCategoryConstants() {
+        /* do nothing - hide constructor */
+    }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryFactory.java
new file mode 100644
index 0000000..5a52591
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryFactory.java
@@ -0,0 +1,38 @@
+/*
+ * 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.asllib;
+
+import org.w3c.dom.Element;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class DataCategoryFactory implements AslMarshallableFactory<DataCategory> {
+    @Override
+    public DataCategory createFromHrElements(List<Element> elements) {
+        String categoryName = null;
+        Map<String, DataType> dataTypeMap = new HashMap<String, DataType>();
+        for (Element ele : elements) {
+            categoryName = ele.getAttribute(XmlUtils.HR_ATTR_DATA_CATEGORY);
+            String dataTypeName = ele.getAttribute(XmlUtils.HR_ATTR_DATA_TYPE);
+            dataTypeMap.put(dataTypeName, new DataTypeFactory().createFromHrElements(List.of(ele)));
+        }
+
+        return new DataCategory(categoryName, dataTypeMap);
+    }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabels.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabels.java
new file mode 100644
index 0000000..d2fffc0
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabels.java
@@ -0,0 +1,101 @@
+/*
+ * 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.asllib;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Data label representation with data shared and data collected maps containing zero or more {@link
+ * DataCategory}
+ */
+public class DataLabels implements AslMarshallable {
+    private final Map<String, DataCategory> mDataAccessed;
+    private final Map<String, DataCategory> mDataCollected;
+    private final Map<String, DataCategory> mDataShared;
+
+    public DataLabels(
+            Map<String, DataCategory> dataAccessed,
+            Map<String, DataCategory> dataCollected,
+            Map<String, DataCategory> dataShared) {
+        mDataAccessed = dataAccessed;
+        mDataCollected = dataCollected;
+        mDataShared = dataShared;
+    }
+
+    /**
+     * Returns the data accessed {@link Map} of {@link com.android.asllib.DataCategoryConstants} to
+     * {@link DataCategory}
+     */
+    public Map<String, DataCategory> getDataAccessed() {
+        return mDataAccessed;
+    }
+
+    /**
+     * Returns the data collected {@link Map} of {@link com.android.asllib.DataCategoryConstants} to
+     * {@link DataCategory}
+     */
+    public Map<String, DataCategory> getDataCollected() {
+        return mDataCollected;
+    }
+
+    /**
+     * Returns the data shared {@link Map} of {@link com.android.asllib.DataCategoryConstants} to
+     * {@link DataCategory}
+     */
+    public Map<String, DataCategory> getDataShared() {
+        return mDataShared;
+    }
+
+    /** Gets the on-device DOM element for the {@link DataLabels}. */
+    @Override
+    public List<Element> toOdDomElements(Document doc) {
+        Element dataLabelsEle =
+                XmlUtils.createPbundleEleWithName(doc, XmlUtils.OD_NAME_DATA_LABELS);
+
+        maybeAppendDataUsages(doc, dataLabelsEle, mDataCollected, XmlUtils.OD_NAME_DATA_ACCESSED);
+        maybeAppendDataUsages(doc, dataLabelsEle, mDataCollected, XmlUtils.OD_NAME_DATA_COLLECTED);
+        maybeAppendDataUsages(doc, dataLabelsEle, mDataShared, XmlUtils.OD_NAME_DATA_SHARED);
+
+        return List.of(dataLabelsEle);
+    }
+
+    private void maybeAppendDataUsages(
+            Document doc,
+            Element dataLabelsEle,
+            Map<String, DataCategory> dataCategoriesMap,
+            String dataUsageTypeName) {
+        if (dataCategoriesMap.isEmpty()) {
+            return;
+        }
+        Element dataUsageEle = XmlUtils.createPbundleEleWithName(doc, dataUsageTypeName);
+
+        for (String dataCategoryName : dataCategoriesMap.keySet()) {
+            Element dataCategoryEle = XmlUtils.createPbundleEleWithName(doc, dataCategoryName);
+            DataCategory dataCategory = dataCategoriesMap.get(dataCategoryName);
+            for (String dataTypeName : dataCategory.getDataTypes().keySet()) {
+                DataType dataType = dataCategory.getDataTypes().get(dataTypeName);
+                XmlUtils.appendChildren(dataCategoryEle, dataType.toOdDomElements(doc));
+            }
+            dataUsageEle.appendChild(dataCategoryEle);
+        }
+        dataLabelsEle.appendChild(dataUsageEle);
+    }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabelsFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabelsFactory.java
new file mode 100644
index 0000000..c758ab9
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabelsFactory.java
@@ -0,0 +1,70 @@
+/*
+ * 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.asllib;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class DataLabelsFactory implements AslMarshallableFactory<DataLabels> {
+
+    /** Creates a {@link DataLabels} from the human-readable DOM element. */
+    @Override
+    public DataLabels createFromHrElements(List<Element> elements) {
+        Element ele = XmlUtils.getSingleElement(elements);
+        Map<String, DataCategory> dataAccessed =
+                getDataCategoriesWithTag(ele, XmlUtils.HR_TAG_DATA_ACCESSED);
+        Map<String, DataCategory> dataCollected =
+                getDataCategoriesWithTag(ele, XmlUtils.HR_TAG_DATA_COLLECTED);
+        Map<String, DataCategory> dataShared =
+                getDataCategoriesWithTag(ele, XmlUtils.HR_TAG_DATA_SHARED);
+        return new DataLabels(dataAccessed, dataCollected, dataShared);
+    }
+
+    private static Map<String, DataCategory> getDataCategoriesWithTag(
+            Element dataLabelsEle, String dataCategoryUsageTypeTag) {
+        Map<String, Map<String, DataType>> dataTypeMap =
+                new HashMap<String, Map<String, DataType>>();
+        NodeList dataUsedNodeList = dataLabelsEle.getElementsByTagName(dataCategoryUsageTypeTag);
+        Map<String, DataCategory> dataCategoryMap = new HashMap<String, DataCategory>();
+
+        Set<String> dataCategoryNames = new HashSet<String>();
+        for (int i = 0; i < dataUsedNodeList.getLength(); i++) {
+            Element dataUsedEle = (Element) dataUsedNodeList.item(i);
+            String dataCategoryName = dataUsedEle.getAttribute(XmlUtils.HR_ATTR_DATA_CATEGORY);
+            dataCategoryNames.add(dataCategoryName);
+        }
+        for (String dataCategoryName : dataCategoryNames) {
+            var dataCategoryElements =
+                    XmlUtils.asElementList(dataUsedNodeList).stream()
+                            .filter(
+                                    ele ->
+                                            ele.getAttribute(XmlUtils.HR_ATTR_DATA_CATEGORY)
+                                                    .equals(dataCategoryName))
+                            .toList();
+            DataCategory dataCategory =
+                    new DataCategoryFactory().createFromHrElements(dataCategoryElements);
+            dataCategoryMap.put(dataCategoryName, dataCategory);
+        }
+        return dataCategoryMap;
+    }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataType.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataType.java
new file mode 100644
index 0000000..5ba2975
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataType.java
@@ -0,0 +1,176 @@
+/*
+ * 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.asllib;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Data usage type representation. Types are specific to a {@link DataCategory} and contains
+ * metadata related to the data usage purpose.
+ */
+public class DataType implements AslMarshallable {
+
+    public enum Purpose {
+        PURPOSE_APP_FUNCTIONALITY(1),
+        PURPOSE_ANALYTICS(2),
+        PURPOSE_DEVELOPER_COMMUNICATIONS(3),
+        PURPOSE_FRAUD_PREVENTION_SECURITY(4),
+        PURPOSE_ADVERTISING(5),
+        PURPOSE_PERSONALIZATION(6),
+        PURPOSE_ACCOUNT_MANAGEMENT(7);
+
+        private static final String PURPOSE_PREFIX = "PURPOSE_";
+
+        private final int mValue;
+
+        Purpose(int value) {
+            this.mValue = value;
+        }
+
+        /** Get the int value associated with the Purpose. */
+        public int getValue() {
+            return mValue;
+        }
+
+        /** Get the Purpose associated with the int value. */
+        public static Purpose forValue(int value) {
+            for (Purpose e : values()) {
+                if (e.getValue() == value) {
+                    return e;
+                }
+            }
+            throw new IllegalArgumentException("No enum for value: " + value);
+        }
+
+        /** Get the Purpose associated with the human-readable String. */
+        public static Purpose forString(String s) {
+            for (Purpose e : values()) {
+                if (e.toString().equals(s)) {
+                    return e;
+                }
+            }
+            throw new IllegalArgumentException("No enum for str: " + s);
+        }
+
+        /** Human-readable String representation of Purpose. */
+        public String toString() {
+            if (!this.name().startsWith(PURPOSE_PREFIX)) {
+                return this.name();
+            }
+            return this.name().substring(PURPOSE_PREFIX.length()).toLowerCase();
+        }
+    }
+
+    private final String mDataTypeName;
+
+    private final Set<Purpose> mPurposeSet;
+    private final Boolean mIsCollectionOptional;
+    private final Boolean mIsSharingOptional;
+    private final Boolean mEphemeral;
+
+    public DataType(
+            String dataTypeName,
+            Set<Purpose> purposeSet,
+            Boolean isCollectionOptional,
+            Boolean isSharingOptional,
+            Boolean ephemeral) {
+        this.mDataTypeName = dataTypeName;
+        this.mPurposeSet = purposeSet;
+        this.mIsCollectionOptional = isCollectionOptional;
+        this.mIsSharingOptional = isSharingOptional;
+        this.mEphemeral = ephemeral;
+    }
+
+    public String getDataTypeName() {
+        return mDataTypeName;
+    }
+
+    /**
+     * Returns {@link Set} of valid {@link Integer} purposes for using the associated data category
+     * and type
+     */
+    public Set<Purpose> getPurposeSet() {
+        return mPurposeSet;
+    }
+
+    /**
+     * For data-collected, returns {@code true} if data usage is user optional and {@code false} if
+     * data usage is required. Should return {@code null} for data-accessed and data-shared.
+     */
+    public Boolean getIsCollectionOptional() {
+        return mIsCollectionOptional;
+    }
+
+    /**
+     * For data-shared, returns {@code true} if data usage is user optional and {@code false} if
+     * data usage is required. Should return {@code null} for data-accessed and data-collected.
+     */
+    public Boolean getIsSharingOptional() {
+        return mIsSharingOptional;
+    }
+
+    /**
+     * For data-collected, returns {@code true} if data usage is user optional and {@code false} if
+     * data usage is processed ephemerally. Should return {@code null} for data-shared.
+     */
+    public Boolean getEphemeral() {
+        return mEphemeral;
+    }
+
+    @Override
+    public List<Element> toOdDomElements(Document doc) {
+        Element dataTypeEle = XmlUtils.createPbundleEleWithName(doc, this.getDataTypeName());
+        if (!this.getPurposeSet().isEmpty()) {
+            Element purposesEle = doc.createElement(XmlUtils.OD_TAG_INT_ARRAY);
+            purposesEle.setAttribute(XmlUtils.OD_ATTR_NAME, XmlUtils.OD_NAME_PURPOSES);
+            purposesEle.setAttribute(
+                    XmlUtils.OD_ATTR_NUM, String.valueOf(this.getPurposeSet().size()));
+            for (DataType.Purpose purpose : this.getPurposeSet()) {
+                Element purposeEle = doc.createElement(XmlUtils.OD_TAG_ITEM);
+                purposeEle.setAttribute(XmlUtils.OD_ATTR_VALUE, String.valueOf(purpose.getValue()));
+                purposesEle.appendChild(purposeEle);
+            }
+            dataTypeEle.appendChild(purposesEle);
+        }
+
+        maybeAddBoolToOdElement(
+                doc,
+                dataTypeEle,
+                this.getIsCollectionOptional(),
+                XmlUtils.OD_NAME_IS_COLLECTION_OPTIONAL);
+        maybeAddBoolToOdElement(
+                doc,
+                dataTypeEle,
+                this.getIsSharingOptional(),
+                XmlUtils.OD_NAME_IS_SHARING_OPTIONAL);
+        maybeAddBoolToOdElement(doc, dataTypeEle, this.getEphemeral(), XmlUtils.OD_NAME_EPHEMERAL);
+        return List.of(dataTypeEle);
+    }
+
+    private static void maybeAddBoolToOdElement(
+            Document doc, Element parentEle, Boolean b, String odName) {
+        if (b == null) {
+            return;
+        }
+        Element ele = XmlUtils.createOdBooleanEle(doc, odName, b);
+        parentEle.appendChild(ele);
+    }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeConstants.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeConstants.java
new file mode 100644
index 0000000..a0a7537
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeConstants.java
@@ -0,0 +1,156 @@
+/*
+ * 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.asllib;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Constants for determining valid {@link String} data types for usage within {@link SafetyLabels},
+ * {@link DataCategory}, and {@link DataType}
+ */
+public class DataTypeConstants {
+    /** Data types for {@link DataCategoryConstants.CATEGORY_PERSONAL} */
+    public static final String TYPE_NAME = "name";
+
+    public static final String TYPE_EMAIL_ADDRESS = "email_address";
+    public static final String TYPE_PHONE_NUMBER = "phone_number";
+    public static final String TYPE_RACE_ETHNICITY = "race_ethnicity";
+    public static final String TYPE_POLITICAL_OR_RELIGIOUS_BELIEFS =
+            "political_or_religious_beliefs";
+    public static final String TYPE_SEXUAL_ORIENTATION_OR_GENDER_IDENTITY =
+            "sexual_orientation_or_gender_identity";
+    public static final String TYPE_PERSONAL_IDENTIFIERS = "personal_identifiers";
+    public static final String TYPE_OTHER = "other";
+
+    /** Data types for {@link DataCategoryConstants.CATEGORY_FINANCIAL} */
+    public static final String TYPE_CARD_BANK_ACCOUNT = "card_bank_account";
+
+    public static final String TYPE_PURCHASE_HISTORY = "purchase_history";
+    public static final String TYPE_CREDIT_SCORE = "credit_score";
+    public static final String TYPE_FINANCIAL_OTHER = "other";
+
+    /** Data types for {@link DataCategoryConstants.CATEGORY_LOCATION} */
+    public static final String TYPE_APPROX_LOCATION = "approx_location";
+
+    public static final String TYPE_PRECISE_LOCATION = "precise_location";
+
+    /** Data types for {@link DataCategoryConstants.CATEGORY_EMAIL_TEXT_MESSAGE} */
+    public static final String TYPE_EMAILS = "emails";
+
+    public static final String TYPE_TEXT_MESSAGES = "text_messages";
+    public static final String TYPE_EMAIL_TEXT_MESSAGE_OTHER = "other";
+
+    /** Data types for {@link DataCategoryConstants.CATEGORY_PHOTO_VIDEO} */
+    public static final String TYPE_PHOTOS = "photos";
+
+    public static final String TYPE_VIDEOS = "videos";
+
+    /** Data types for {@link DataCategoryConstants.CATEGORY_AUDIO} */
+    public static final String TYPE_SOUND_RECORDINGS = "sound_recordings";
+
+    public static final String TYPE_MUSIC_FILES = "music_files";
+    public static final String TYPE_AUDIO_OTHER = "other";
+
+    /** Data types for {@link DataCategoryConstants.CATEGORY_STORAGE} */
+    public static final String TYPE_FILES_DOCS = "files_docs";
+
+    /** Data types for {@link DataCategoryConstants.CATEGORY_HEALTH_FITNESS} */
+    public static final String TYPE_HEALTH = "health";
+
+    public static final String TYPE_FITNESS = "fitness";
+
+    /** Data types for {@link DataCategoryConstants.CATEGORY_CONTACTS} */
+    public static final String TYPE_CONTACTS = "contacts";
+
+    /** Data types for {@link DataCategoryConstants.CATEGORY_CALENDAR} */
+    public static final String TYPE_CALENDAR = "calendar";
+
+    /** Data types for {@link DataCategoryConstants.CATEGORY_IDENTIFIERS} */
+    public static final String TYPE_IDENTIFIERS_OTHER = "other";
+
+    /** Data types for {@link DataCategoryConstants.CATEGORY_APP_PERFORMANCE} */
+    public static final String TYPE_CRASH_LOGS = "crash_logs";
+
+    public static final String TYPE_PERFORMANCE_DIAGNOSTICS = "performance_diagnostics";
+    public static final String TYPE_APP_PERFORMANCE_OTHER = "other";
+
+    /** Data types for {@link DataCategoryConstants.CATEGORY_ACTIONS_IN_APP} */
+    public static final String TYPE_USER_INTERACTION = "user_interaction";
+
+    public static final String TYPE_IN_APP_SEARCH_HISTORY = "in_app_search_history";
+    public static final String TYPE_INSTALLED_APPS = "installed_apps";
+    public static final String TYPE_USER_GENERATED_CONTENT = "user_generated_content";
+    public static final String TYPE_ACTIONS_IN_APP_OTHER = "other";
+
+    /** Data types for {@link DataCategoryConstants.CATEGORY_SEARCH_AND_BROWSING} */
+    public static final String TYPE_WEB_BROWSING_HISTORY = "web_browsing_history";
+
+    /** Set of valid categories */
+    public static final Set<String> VALID_TYPES =
+            Collections.unmodifiableSet(
+                    new HashSet<>(
+                            Arrays.asList(
+                                    TYPE_NAME,
+                                    TYPE_EMAIL_ADDRESS,
+                                    TYPE_PHONE_NUMBER,
+                                    TYPE_RACE_ETHNICITY,
+                                    TYPE_POLITICAL_OR_RELIGIOUS_BELIEFS,
+                                    TYPE_SEXUAL_ORIENTATION_OR_GENDER_IDENTITY,
+                                    TYPE_PERSONAL_IDENTIFIERS,
+                                    TYPE_OTHER,
+                                    TYPE_CARD_BANK_ACCOUNT,
+                                    TYPE_PURCHASE_HISTORY,
+                                    TYPE_CREDIT_SCORE,
+                                    TYPE_FINANCIAL_OTHER,
+                                    TYPE_APPROX_LOCATION,
+                                    TYPE_PRECISE_LOCATION,
+                                    TYPE_EMAILS,
+                                    TYPE_TEXT_MESSAGES,
+                                    TYPE_EMAIL_TEXT_MESSAGE_OTHER,
+                                    TYPE_PHOTOS,
+                                    TYPE_VIDEOS,
+                                    TYPE_SOUND_RECORDINGS,
+                                    TYPE_MUSIC_FILES,
+                                    TYPE_AUDIO_OTHER,
+                                    TYPE_FILES_DOCS,
+                                    TYPE_HEALTH,
+                                    TYPE_FITNESS,
+                                    TYPE_CONTACTS,
+                                    TYPE_CALENDAR,
+                                    TYPE_IDENTIFIERS_OTHER,
+                                    TYPE_CRASH_LOGS,
+                                    TYPE_PERFORMANCE_DIAGNOSTICS,
+                                    TYPE_APP_PERFORMANCE_OTHER,
+                                    TYPE_USER_INTERACTION,
+                                    TYPE_IN_APP_SEARCH_HISTORY,
+                                    TYPE_INSTALLED_APPS,
+                                    TYPE_USER_GENERATED_CONTENT,
+                                    TYPE_ACTIONS_IN_APP_OTHER,
+                                    TYPE_WEB_BROWSING_HISTORY)));
+
+    /** Returns {@link Set} of valid {@link String} category keys */
+    public static Set<String> getValidDataTypes() {
+        return VALID_TYPES;
+    }
+
+    private DataTypeConstants() {
+        /* do nothing - hide constructor */
+    }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeFactory.java
new file mode 100644
index 0000000..99f8a8b
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeFactory.java
@@ -0,0 +1,47 @@
+/*
+ * 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.asllib;
+
+import org.w3c.dom.Element;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class DataTypeFactory implements AslMarshallableFactory<DataType> {
+    /** Creates a {@link DataType} from the human-readable DOM element. */
+    @Override
+    public DataType createFromHrElements(List<Element> elements) {
+        Element hrDataTypeEle = XmlUtils.getSingleElement(elements);
+        String dataTypeName = hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_DATA_TYPE);
+        Set<DataType.Purpose> purposeSet =
+                Arrays.stream(hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_PURPOSES).split("\\|"))
+                        .map(DataType.Purpose::forString)
+                        .collect(Collectors.toUnmodifiableSet());
+        Boolean isCollectionOptional =
+                XmlUtils.fromString(
+                        hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_IS_SHARING_OPTIONAL));
+        Boolean isSharingOptional =
+                XmlUtils.fromString(
+                        hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_IS_COLLECTION_OPTIONAL));
+        Boolean ephemeral =
+                XmlUtils.fromString(hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_EPHEMERAL));
+        return new DataType(
+                dataTypeName, purposeSet, isCollectionOptional, isSharingOptional, ephemeral);
+    }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabels.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabels.java
new file mode 100644
index 0000000..f06522f
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabels.java
@@ -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.asllib;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.util.List;
+
+/** Safety Label representation containing zero or more {@link DataCategory} for data shared */
+public class SafetyLabels implements AslMarshallable {
+
+    private final Long mVersion;
+    private final DataLabels mDataLabels;
+
+    public SafetyLabels(Long version, DataLabels dataLabels) {
+        this.mVersion = version;
+        this.mDataLabels = dataLabels;
+    }
+
+    /** Returns the data label for the safety label */
+    public DataLabels getDataLabel() {
+        return mDataLabels;
+    }
+
+    /** Gets the version of the {@link SafetyLabels}. */
+    public Long getVersion() {
+        return mVersion;
+    }
+
+    /** Creates an on-device DOM element from the {@link SafetyLabels}. */
+    @Override
+    public List<Element> toOdDomElements(Document doc) {
+        Element safetyLabelsEle =
+                XmlUtils.createPbundleEleWithName(doc, XmlUtils.OD_NAME_SAFETY_LABELS);
+        XmlUtils.appendChildren(safetyLabelsEle, mDataLabels.toOdDomElements(doc));
+        return List.of(safetyLabelsEle);
+    }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabelsFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabelsFactory.java
new file mode 100644
index 0000000..68e83fe
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabelsFactory.java
@@ -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.asllib;
+
+import org.w3c.dom.Element;
+
+import java.util.List;
+
+public class SafetyLabelsFactory implements AslMarshallableFactory<SafetyLabels> {
+
+    /** Creates a {@link SafetyLabels} from the human-readable DOM element. */
+    @Override
+    public SafetyLabels createFromHrElements(List<Element> elements) {
+        Element safetyLabelsEle = XmlUtils.getSingleElement(elements);
+        Long version;
+        try {
+            version = Long.parseLong(safetyLabelsEle.getAttribute(XmlUtils.HR_ATTR_VERSION));
+        } catch (Exception e) {
+            throw new IllegalArgumentException(
+                    "Malformed or missing required version in safety labels.");
+        }
+
+        DataLabels dataLabels =
+                new DataLabelsFactory()
+                        .createFromHrElements(
+                                List.of(
+                                        XmlUtils.getSingleChildElement(
+                                                safetyLabelsEle, XmlUtils.HR_TAG_DATA_LABELS)));
+        return new SafetyLabels(version, dataLabels);
+    }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/XmlUtils.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/XmlUtils.java
new file mode 100644
index 0000000..3c89a30
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/XmlUtils.java
@@ -0,0 +1,152 @@
+/*
+ * 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.asllib;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class XmlUtils {
+    public static final String HR_TAG_APP_METADATA_BUNDLES = "app-metadata-bundles";
+    public static final String HR_TAG_SAFETY_LABELS = "safety-labels";
+    public static final String HR_TAG_DATA_LABELS = "data-labels";
+    public static final String HR_TAG_DATA_ACCESSED = "data-accessed";
+    public static final String HR_TAG_DATA_COLLECTED = "data-collected";
+    public static final String HR_TAG_DATA_SHARED = "data-shared";
+
+    public static final String HR_ATTR_DATA_CATEGORY = "dataCategory";
+    public static final String HR_ATTR_DATA_TYPE = "dataType";
+    public static final String HR_ATTR_IS_COLLECTION_OPTIONAL = "isCollectionOptional";
+    public static final String HR_ATTR_IS_SHARING_OPTIONAL = "isSharingOptional";
+    public static final String HR_ATTR_EPHEMERAL = "ephemeral";
+    public static final String HR_ATTR_PURPOSES = "purposes";
+    public static final String HR_ATTR_VERSION = "version";
+
+    public static final String OD_TAG_BUNDLE = "bundle";
+    public static final String OD_TAG_PBUNDLE_AS_MAP = "pbundle_as_map";
+    public static final String OD_TAG_BOOLEAN = "boolean";
+    public static final String OD_TAG_INT_ARRAY = "int-array";
+    public static final String OD_TAG_ITEM = "item";
+    public static final String OD_ATTR_NAME = "name";
+    public static final String OD_ATTR_VALUE = "value";
+    public static final String OD_ATTR_NUM = "num";
+    public static final String OD_NAME_SAFETY_LABELS = "safety_labels";
+    public static final String OD_NAME_DATA_LABELS = "data_labels";
+    public static final String OD_NAME_DATA_ACCESSED = "data_accessed";
+    public static final String OD_NAME_DATA_COLLECTED = "data_collected";
+    public static final String OD_NAME_DATA_SHARED = "data_shared";
+    public static final String OD_NAME_PURPOSES = "purposes";
+    public static final String OD_NAME_IS_COLLECTION_OPTIONAL = "is_collection_optional";
+    public static final String OD_NAME_IS_SHARING_OPTIONAL = "is_sharing_optional";
+    public static final String OD_NAME_EPHEMERAL = "ephemeral";
+
+    public static final String TRUE_STR = "true";
+    public static final String FALSE_STR = "false";
+
+    /** Gets the single top-level {@link Element} having the {@param tagName}. */
+    public static Element getSingleElement(Document doc, String tagName) {
+        var elements = doc.getElementsByTagName(tagName);
+        return getSingleElement(elements);
+    }
+
+    /**
+     * Gets the single {@link Element} within {@param parentEle} and having the {@param tagName}.
+     */
+    public static Element getSingleChildElement(Element parentEle, String tagName) {
+        var elements = parentEle.getElementsByTagName(tagName);
+        return getSingleElement(elements);
+    }
+
+    /** Gets the single {@link Element} from {@param elements} */
+    public static Element getSingleElement(NodeList elements) {
+        if (elements.getLength() != 1) {
+            throw new IllegalArgumentException(
+                    String.format(
+                            "Expected 1 element in NodeList but got %s.", elements.getLength()));
+        }
+        var elementAsNode = elements.item(0);
+        if (!(elementAsNode instanceof Element)) {
+            throw new IllegalStateException(
+                    String.format("%s was not an element.", elementAsNode.getNodeName()));
+        }
+        return ((Element) elementAsNode);
+    }
+
+    /** Gets the single {@link Element} within {@param elements}. */
+    public static Element getSingleElement(List<Element> elements) {
+        if (elements.size() != 1) {
+            throw new IllegalStateException(
+                    String.format("Expected 1 element in list but got %s.", elements.size()));
+        }
+        return elements.get(0);
+    }
+
+    /** Converts {@param nodeList} into List of {@link Element}. */
+    public static List<Element> asElementList(NodeList nodeList) {
+        List<Element> elementList = new ArrayList<Element>();
+        for (int i = 0; i < nodeList.getLength(); i++) {
+            var elementAsNode = nodeList.item(0);
+            if (elementAsNode instanceof Element) {
+                elementList.add(((Element) elementAsNode));
+            }
+        }
+        return elementList;
+    }
+
+    /** Appends {@param children} to the {@param ele}. */
+    public static void appendChildren(Element ele, List<Element> children) {
+        for (Element c : children) {
+            ele.appendChild(c);
+        }
+    }
+
+    /** Gets the Boolean from the String value. */
+    public static Boolean fromString(String s) {
+        if (s == null) {
+            return null;
+        }
+        if (s.equals(TRUE_STR)) {
+            return true;
+        } else if (s.equals(FALSE_STR)) {
+            return false;
+        }
+        return null;
+    }
+
+    /** Creates an on-device PBundle DOM Element with the given attribute name. */
+    public static Element createPbundleEleWithName(Document doc, String name) {
+        var ele = doc.createElement(XmlUtils.OD_TAG_PBUNDLE_AS_MAP);
+        ele.setAttribute(XmlUtils.OD_ATTR_NAME, name);
+        return ele;
+    }
+
+    /** Create an on-device Boolean DOM Element with the given attribute name. */
+    public static Element createOdBooleanEle(Document doc, String name, boolean b) {
+        var ele = doc.createElement(XmlUtils.OD_TAG_BOOLEAN);
+        ele.setAttribute(XmlUtils.OD_ATTR_NAME, name);
+        ele.setAttribute(XmlUtils.OD_ATTR_VALUE, String.valueOf(b));
+        return ele;
+    }
+
+    /** Returns whether the String is null or empty. */
+    public static boolean isNullOrEmpty(String s) {
+        return s == null || s.isEmpty();
+    }
+}