Merge "Switch to callback version of ScreenshotActionsProvider" into main
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/apex/jobscheduler/framework/aconfig/job.aconfig b/apex/jobscheduler/framework/aconfig/job.aconfig
index 788e824..2c1a853 100644
--- a/apex/jobscheduler/framework/aconfig/job.aconfig
+++ b/apex/jobscheduler/framework/aconfig/job.aconfig
@@ -9,6 +9,7 @@
 
 flag {
     name: "job_debug_info_apis"
+    is_exported: true
     namespace: "backstage_power"
     description: "Add APIs to let apps attach debug information to jobs"
     bug: "293491637"
@@ -16,6 +17,7 @@
 
 flag {
     name: "backup_jobs_exemption"
+    is_exported: true
     namespace: "backstage_power"
     description: "Introduce a new RUN_BACKUP_JOBS permission and exemption logic allowing for longer running jobs for apps whose primary purpose is to backup or sync content."
     bug: "318731461"
diff --git a/core/api/current.txt b/core/api/current.txt
index 8a61f4a..93c34cd 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -10731,6 +10731,7 @@
     field public static final String DROPBOX_SERVICE = "dropbox";
     field public static final String EUICC_SERVICE = "euicc";
     field public static final String FILE_INTEGRITY_SERVICE = "file_integrity";
+    field public static final String FINGERPRINT_SERVICE = "fingerprint";
     field public static final String GAME_SERVICE = "game";
     field public static final String GRAMMATICAL_INFLECTION_SERVICE = "grammatical_inflection";
     field public static final String HARDWARE_PROPERTIES_SERVICE = "hardware_properties";
@@ -20387,6 +20388,54 @@
 
 }
 
+package android.hardware.fingerprint {
+
+  @Deprecated public class FingerprintManager {
+    method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.USE_BIOMETRIC, android.Manifest.permission.USE_FINGERPRINT}) public void authenticate(@Nullable android.hardware.fingerprint.FingerprintManager.CryptoObject, @Nullable android.os.CancellationSignal, int, @NonNull android.hardware.fingerprint.FingerprintManager.AuthenticationCallback, @Nullable android.os.Handler);
+    method @Deprecated @RequiresPermission(android.Manifest.permission.USE_FINGERPRINT) public boolean hasEnrolledFingerprints();
+    method @Deprecated @RequiresPermission(android.Manifest.permission.USE_FINGERPRINT) public boolean isHardwareDetected();
+    field public static final int FINGERPRINT_ACQUIRED_GOOD = 0; // 0x0
+    field public static final int FINGERPRINT_ACQUIRED_IMAGER_DIRTY = 3; // 0x3
+    field public static final int FINGERPRINT_ACQUIRED_INSUFFICIENT = 2; // 0x2
+    field public static final int FINGERPRINT_ACQUIRED_PARTIAL = 1; // 0x1
+    field public static final int FINGERPRINT_ACQUIRED_TOO_FAST = 5; // 0x5
+    field public static final int FINGERPRINT_ACQUIRED_TOO_SLOW = 4; // 0x4
+    field public static final int FINGERPRINT_ERROR_CANCELED = 5; // 0x5
+    field public static final int FINGERPRINT_ERROR_HW_NOT_PRESENT = 12; // 0xc
+    field public static final int FINGERPRINT_ERROR_HW_UNAVAILABLE = 1; // 0x1
+    field public static final int FINGERPRINT_ERROR_LOCKOUT = 7; // 0x7
+    field public static final int FINGERPRINT_ERROR_LOCKOUT_PERMANENT = 9; // 0x9
+    field public static final int FINGERPRINT_ERROR_NO_FINGERPRINTS = 11; // 0xb
+    field public static final int FINGERPRINT_ERROR_NO_SPACE = 4; // 0x4
+    field public static final int FINGERPRINT_ERROR_TIMEOUT = 3; // 0x3
+    field public static final int FINGERPRINT_ERROR_UNABLE_TO_PROCESS = 2; // 0x2
+    field public static final int FINGERPRINT_ERROR_USER_CANCELED = 10; // 0xa
+    field public static final int FINGERPRINT_ERROR_VENDOR = 8; // 0x8
+  }
+
+  @Deprecated public abstract static class FingerprintManager.AuthenticationCallback {
+    ctor @Deprecated public FingerprintManager.AuthenticationCallback();
+    method @Deprecated public void onAuthenticationError(int, CharSequence);
+    method @Deprecated public void onAuthenticationFailed();
+    method @Deprecated public void onAuthenticationHelp(int, CharSequence);
+    method @Deprecated public void onAuthenticationSucceeded(android.hardware.fingerprint.FingerprintManager.AuthenticationResult);
+  }
+
+  @Deprecated public static class FingerprintManager.AuthenticationResult {
+    method @Deprecated public android.hardware.fingerprint.FingerprintManager.CryptoObject getCryptoObject();
+  }
+
+  @Deprecated public static final class FingerprintManager.CryptoObject {
+    ctor @Deprecated public FingerprintManager.CryptoObject(@NonNull java.security.Signature);
+    ctor @Deprecated public FingerprintManager.CryptoObject(@NonNull javax.crypto.Cipher);
+    ctor @Deprecated public FingerprintManager.CryptoObject(@NonNull javax.crypto.Mac);
+    method @Deprecated public javax.crypto.Cipher getCipher();
+    method @Deprecated public javax.crypto.Mac getMac();
+    method @Deprecated public java.security.Signature getSignature();
+  }
+
+}
+
 package android.hardware.input {
 
   public final class HostUsiVersion implements android.os.Parcelable {
diff --git a/core/api/removed.txt b/core/api/removed.txt
index c61f163..3c7c0d6 100644
--- a/core/api/removed.txt
+++ b/core/api/removed.txt
@@ -35,7 +35,6 @@
     method @Deprecated @Nullable public String getFeatureId();
     method public abstract android.content.SharedPreferences getSharedPreferences(java.io.File, int);
     method public abstract java.io.File getSharedPreferencesPath(String);
-    field public static final String FINGERPRINT_SERVICE = "fingerprint";
   }
 
   public class ContextWrapper extends android.content.Context {
@@ -146,54 +145,6 @@
 
 }
 
-package android.hardware.fingerprint {
-
-  @Deprecated public class FingerprintManager {
-    method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.USE_BIOMETRIC, android.Manifest.permission.USE_FINGERPRINT}) public void authenticate(@Nullable android.hardware.fingerprint.FingerprintManager.CryptoObject, @Nullable android.os.CancellationSignal, int, @NonNull android.hardware.fingerprint.FingerprintManager.AuthenticationCallback, @Nullable android.os.Handler);
-    method @Deprecated @RequiresPermission(android.Manifest.permission.USE_FINGERPRINT) public boolean hasEnrolledFingerprints();
-    method @Deprecated @RequiresPermission(android.Manifest.permission.USE_FINGERPRINT) public boolean isHardwareDetected();
-    field public static final int FINGERPRINT_ACQUIRED_GOOD = 0; // 0x0
-    field public static final int FINGERPRINT_ACQUIRED_IMAGER_DIRTY = 3; // 0x3
-    field public static final int FINGERPRINT_ACQUIRED_INSUFFICIENT = 2; // 0x2
-    field public static final int FINGERPRINT_ACQUIRED_PARTIAL = 1; // 0x1
-    field public static final int FINGERPRINT_ACQUIRED_TOO_FAST = 5; // 0x5
-    field public static final int FINGERPRINT_ACQUIRED_TOO_SLOW = 4; // 0x4
-    field public static final int FINGERPRINT_ERROR_CANCELED = 5; // 0x5
-    field public static final int FINGERPRINT_ERROR_HW_NOT_PRESENT = 12; // 0xc
-    field public static final int FINGERPRINT_ERROR_HW_UNAVAILABLE = 1; // 0x1
-    field public static final int FINGERPRINT_ERROR_LOCKOUT = 7; // 0x7
-    field public static final int FINGERPRINT_ERROR_LOCKOUT_PERMANENT = 9; // 0x9
-    field public static final int FINGERPRINT_ERROR_NO_FINGERPRINTS = 11; // 0xb
-    field public static final int FINGERPRINT_ERROR_NO_SPACE = 4; // 0x4
-    field public static final int FINGERPRINT_ERROR_TIMEOUT = 3; // 0x3
-    field public static final int FINGERPRINT_ERROR_UNABLE_TO_PROCESS = 2; // 0x2
-    field public static final int FINGERPRINT_ERROR_USER_CANCELED = 10; // 0xa
-    field public static final int FINGERPRINT_ERROR_VENDOR = 8; // 0x8
-  }
-
-  @Deprecated public abstract static class FingerprintManager.AuthenticationCallback {
-    ctor public FingerprintManager.AuthenticationCallback();
-    method public void onAuthenticationError(int, CharSequence);
-    method public void onAuthenticationFailed();
-    method public void onAuthenticationHelp(int, CharSequence);
-    method public void onAuthenticationSucceeded(android.hardware.fingerprint.FingerprintManager.AuthenticationResult);
-  }
-
-  @Deprecated public static class FingerprintManager.AuthenticationResult {
-    method public android.hardware.fingerprint.FingerprintManager.CryptoObject getCryptoObject();
-  }
-
-  @Deprecated public static final class FingerprintManager.CryptoObject {
-    ctor public FingerprintManager.CryptoObject(@NonNull java.security.Signature);
-    ctor public FingerprintManager.CryptoObject(@NonNull javax.crypto.Cipher);
-    ctor public FingerprintManager.CryptoObject(@NonNull javax.crypto.Mac);
-    method public javax.crypto.Cipher getCipher();
-    method public javax.crypto.Mac getMac();
-    method public java.security.Signature getSignature();
-  }
-
-}
-
 package android.media {
 
   public final class AudioFormat implements android.os.Parcelable {
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 0023e2a..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
@@ -4355,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..a76aa67 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -1721,6 +1721,15 @@
 
 }
 
+package android.hardware.fingerprint {
+
+  @Deprecated public class FingerprintManager {
+    method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.TEST_BIOMETRIC) public android.hardware.biometrics.BiometricTestSession createTestSession(int);
+    method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.TEST_BIOMETRIC) public java.util.List<android.hardware.biometrics.SensorProperties> getSensorProperties();
+  }
+
+}
+
 package android.hardware.hdmi {
 
   public final class HdmiControlServiceWrapper {
@@ -2460,6 +2469,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/api/test-removed.txt b/core/api/test-removed.txt
index 2e44176..d802177 100644
--- a/core/api/test-removed.txt
+++ b/core/api/test-removed.txt
@@ -1,10 +1 @@
 // Signature format: 2.0
-package android.hardware.fingerprint {
-
-  @Deprecated public class FingerprintManager {
-    method @NonNull @RequiresPermission(android.Manifest.permission.TEST_BIOMETRIC) public android.hardware.biometrics.BiometricTestSession createTestSession(int);
-    method @NonNull @RequiresPermission(android.Manifest.permission.TEST_BIOMETRIC) public java.util.List<android.hardware.biometrics.SensorProperties> getSensorProperties();
-  }
-
-}
-
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/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/activity_manager.aconfig b/core/java/android/app/activity_manager.aconfig
index 350b1ed..b9aa18c 100644
--- a/core/java/android/app/activity_manager.aconfig
+++ b/core/java/android/app/activity_manager.aconfig
@@ -3,6 +3,7 @@
 flag {
      namespace: "system_performance"
      name: "app_start_info"
+     is_exported: true
      description: "Control collecting of ApplicationStartInfo records and APIs."
      bug: "247814855"
 }
@@ -10,6 +11,7 @@
 flag {
      namespace: "backstage_power"
      name: "get_binding_uid_importance"
+     is_exported: true
      description: "API to get importance of UID that's binding to the caller"
      bug: "292533010"
 }
@@ -17,6 +19,7 @@
 flag {
     namespace: "backstage_power"
     name: "app_restrictions_api"
+    is_exported: true
     description: "API to track and query restrictions applied to apps"
     bug: "320150834"
 }
@@ -24,6 +27,7 @@
 flag {
      namespace: "backstage_power"
      name: "uid_importance_listener_for_uids"
+     is_exported: true
      description: "API to add OnUidImportanceListener with targetted UIDs"
      bug: "286258140"
 }
@@ -31,12 +35,14 @@
 flag {
      namespace: "backstage_power"
      name: "introduce_new_service_ontimeout_callback"
+     is_exported: true
      description: "Add a new callback in Service to indicate a FGS has reached its timeout."
      bug: "317799821"
 }
 
 flag {
     name: "bcast_event_timestamps"
+    is_exported: true
     namespace: "backstage_power"
     description: "Add APIs for clients to provide broadcast event trigger timestamps"
     bug: "325136414"
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/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig
index 3ec6fe7..4fa45be 100644
--- a/core/java/android/app/admin/flags/flags.aconfig
+++ b/core/java/android/app/admin/flags/flags.aconfig
@@ -5,6 +5,7 @@
 
 flag {
   name: "policy_engine_migration_v2_enabled"
+  is_exported: true
   namespace: "enterprise"
   description: "V2 of the policy engine migrations for Android V"
   bug: "289520697"
@@ -12,6 +13,7 @@
 
 flag {
   name: "device_policy_size_tracking_enabled"
+  is_exported: true
   namespace: "enterprise"
   description: "Add feature to track the total policy size and have a max threshold - public API changes"
   bug: "281543351"
@@ -26,6 +28,7 @@
 
 flag {
   name: "onboarding_bugreport_v2_enabled"
+  is_exported: true
   namespace: "enterprise"
   description: "Add feature to track required changes for enabled V2 of auto-capturing of onboarding bug reports."
   bug: "302517677"
@@ -47,6 +50,7 @@
 
 flag {
   name: "dedicated_device_control_api_enabled"
+  is_exported: true
   namespace: "enterprise"
   description: "(API) Allow the device management role holder to control which platform features are available on dedicated devices."
   bug: "281964214"
@@ -54,6 +58,7 @@
 
 flag {
   name: "permission_migration_for_zero_trust_api_enabled"
+  is_exported: true
   namespace: "enterprise"
   description: "(API) Migrate existing APIs to permission based, and enable DMRH to call them to collect Zero Trust signals."
   bug: "289520697"
@@ -68,6 +73,7 @@
 
 flag {
   name: "device_theft_api_enabled"
+  is_exported: true
   namespace: "enterprise"
   description: "Add new API for theft detection."
   bug: "325073410"
@@ -89,6 +95,7 @@
 
 flag {
   name: "security_log_v2_enabled"
+  is_exported: true
   namespace: "enterprise"
   description: "Improve access to security logging in the context of Zero Trust."
   bug: "295324350"
@@ -103,6 +110,7 @@
 
 flag {
     name: "allow_querying_profile_type"
+    is_exported: true
     namespace: "enterprise"
     description: "Public APIs to query if a user is a profile and what kind of profile type it is."
     bug: "323001115"
@@ -117,6 +125,7 @@
 
 flag {
   name: "assist_content_user_restriction_enabled"
+  is_exported: true
   namespace: "enterprise"
   description: "Prevent work data leakage by sending assist content to privileged apps."
   bug: "322975406"
@@ -134,6 +143,7 @@
 
 flag {
   name: "backup_service_security_log_event_enabled"
+  is_exported: true
   namespace: "enterprise"
   description: "Emit a security log event when DPM.setBackupServiceEnabled is called"
   bug: "304999634"
@@ -141,6 +151,7 @@
 
 flag {
   name: "esim_management_enabled"
+  is_exported: true
   namespace: "enterprise"
   description: "Enable APIs to provision and manage eSIMs"
   bug: "295301164"
@@ -148,6 +159,7 @@
 
 flag {
   name: "headless_device_owner_single_user_enabled"
+  is_exported: true
   namespace: "enterprise"
   description: "Add Headless DO support."
   bug: "289515470"
@@ -155,6 +167,7 @@
 
 flag {
   name: "is_mte_policy_enforced"
+  is_exported: true
   namespace: "enterprise"
   description: "Allow to query whether MTE is enabled or not to check for compliance for enterprise policy"
   bug: "322777918"
diff --git a/core/java/android/app/background_install_control_manager.aconfig b/core/java/android/app/background_install_control_manager.aconfig
index 4473b95..5f3bb07 100644
--- a/core/java/android/app/background_install_control_manager.aconfig
+++ b/core/java/android/app/background_install_control_manager.aconfig
@@ -3,6 +3,7 @@
 flag {
      namespace: "preload_safety"
      name: "bic_client"
+     is_exported: true
      description: "System API for background install control."
      is_fixed_read_only: true
      bug: "287507984"
diff --git a/core/java/android/app/grammatical_inflection_manager.aconfig b/core/java/android/app/grammatical_inflection_manager.aconfig
index 68d12ba..0d7bf65 100644
--- a/core/java/android/app/grammatical_inflection_manager.aconfig
+++ b/core/java/android/app/grammatical_inflection_manager.aconfig
@@ -2,6 +2,7 @@
 
 flag {
     name: "system_terms_of_address_enabled"
+    is_exported: true
     namespace: "globalintl"
     description: "Feature flag for System Terms of Address"
     bug: "297798866"
diff --git a/core/java/android/app/multitasking.aconfig b/core/java/android/app/multitasking.aconfig
index ab00891..dbf3173 100644
--- a/core/java/android/app/multitasking.aconfig
+++ b/core/java/android/app/multitasking.aconfig
@@ -2,6 +2,7 @@
 
 flag {
     name: "enable_pip_ui_state_callback_on_entering"
+    is_exported: true
     namespace: "multitasking"
     description: "Enables PiP UI state callback on entering"
     bug: "303718131"
diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig
index 274d02a..e9a7460 100644
--- a/core/java/android/app/notification.aconfig
+++ b/core/java/android/app/notification.aconfig
@@ -2,6 +2,7 @@
 
 flag {
   name: "modes_api"
+  is_exported: true
   namespace: "systemui"
   description: "This flag controls new and updated DND apis"
   bug: "300477976"
@@ -16,6 +17,7 @@
 
 flag {
   name: "api_tvextender"
+  is_exported: true
   namespace: "systemui"
   description: "Guards new android.app.Notification.TvExtender api"
   bug: "308164892"
@@ -24,6 +26,7 @@
 
 flag {
   name: "lifetime_extension_refactor"
+  is_exported: true
   namespace: "systemui"
   description: "Enables moving notification lifetime extension management from SystemUI to "
       "Notification Manager Service"
@@ -46,6 +49,7 @@
 
 flag {
   name: "category_voicemail"
+  is_exported: true
   namespace: "wear_sysui"
   description: "Adds a new voicemail category for notifications"
   bug: "322806700"
@@ -53,6 +57,7 @@
 
 flag {
   name: "notification_channel_vibration_effect_api"
+  is_exported: true
   namespace: "systemui"
   description: "This flag enables the API to allow setting VibrationEffect for NotificationChannels"
   bug: "241732519"
diff --git a/core/java/android/app/ondeviceintelligence/flags/ondevice_intelligence.aconfig b/core/java/android/app/ondeviceintelligence/flags/ondevice_intelligence.aconfig
index 44f3329..dd9210f 100644
--- a/core/java/android/app/ondeviceintelligence/flags/ondevice_intelligence.aconfig
+++ b/core/java/android/app/ondeviceintelligence/flags/ondevice_intelligence.aconfig
@@ -2,6 +2,7 @@
 
 flag {
     name: "enable_on_device_intelligence"
+    is_exported: true
     namespace: "ondeviceintelligence"
     description: "Make methods on OnDeviceIntelligenceManager available for local inference."
     bug: "304755128"
diff --git a/core/java/android/app/pinner-client.aconfig b/core/java/android/app/pinner-client.aconfig
index b60ad9e..0f7fa14 100644
--- a/core/java/android/app/pinner-client.aconfig
+++ b/core/java/android/app/pinner-client.aconfig
@@ -3,6 +3,7 @@
 flag {
      namespace: "system_performance"
      name: "pinner_service_client_api"
+     is_exported: true
      description: "Control exposing PinnerService APIs."
      bug: "307594624"
 }
\ No newline at end of file
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/app/smartspace/flags.aconfig b/core/java/android/app/smartspace/flags.aconfig
index 12af888..e90ba67 100644
--- a/core/java/android/app/smartspace/flags.aconfig
+++ b/core/java/android/app/smartspace/flags.aconfig
@@ -2,6 +2,7 @@
 
 flag {
   name: "remote_views"
+  is_exported: true
   namespace: "sysui_integrations"
   description: "Flag to enable the FlaggedApi to include RemoteViews in SmartspaceTarget"
   bug: "300157758"
@@ -9,6 +10,7 @@
 
 flag {
   name: "access_smartspace"
+  is_exported: true
   namespace: "sysui_integrations"
   description: "Flag to enable the ACCESS_SMARTSPACE check in SmartspaceManagerService"
   bug: "297207196"
diff --git a/core/java/android/app/usage/flags.aconfig b/core/java/android/app/usage/flags.aconfig
index 4d9d911..9a2d2e5 100644
--- a/core/java/android/app/usage/flags.aconfig
+++ b/core/java/android/app/usage/flags.aconfig
@@ -2,6 +2,7 @@
 
 flag {
     name: "user_interaction_type_api"
+    is_exported: true
     namespace: "backstage_power"
     description: "Feature flag for user interaction event report/query API"
     bug: "296061232"
@@ -9,6 +10,7 @@
 
 flag {
     name: "report_usage_stats_permission"
+    is_exported: true
     namespace: "backstage_power"
     description: "Feature flag for the new REPORT_USAGE_STATS permission."
     bug: "296056771"
@@ -31,6 +33,7 @@
 
 flag {
     name: "filter_based_event_query_api"
+    is_exported: true
     namespace: "backstage_power"
     description: " Feature flag to support filter based event query API"
     bug: "194321117"
@@ -38,6 +41,7 @@
 
 flag {
     name: "get_app_bytes_by_data_type_api"
+    is_exported: true
     namespace: "system_performance"
     description: "Feature flag for collecting app data size by file type API"
     bug: "294088945"
diff --git a/core/java/android/app/wearable/flags.aconfig b/core/java/android/app/wearable/flags.aconfig
index b4f628f..d1d7b5d 100644
--- a/core/java/android/app/wearable/flags.aconfig
+++ b/core/java/android/app/wearable/flags.aconfig
@@ -2,6 +2,7 @@
 
 flag {
     name: "enable_unsupported_operation_status_code"
+    is_exported: true
     namespace: "machine_learning"
     description: "This flag enables the WearableSensingManager#STATUS_UNSUPPORTED_OPERATION status code API."
     bug: "301427767"
@@ -9,6 +10,7 @@
 
 flag {
     name: "enable_data_request_observer_api"
+    is_exported: true
     namespace: "machine_learning"
     description: "This flag enables the API to register a data request observer on WearableSensingManager."
     bug: "301427767"
@@ -16,6 +18,7 @@
 
 flag {
     name: "enable_provide_wearable_connection_api"
+    is_exported: true
     namespace: "machine_learning"
     description: "This flag enables the WearableSensingManager#provideWearableConnection API."
     bug: "301427767"
@@ -30,6 +33,7 @@
 
 flag {
     name: "enable_hotword_wearable_sensing_api"
+    is_exported: true
     namespace: "machine_learning"
     description: "This flag enables the APIs related to hotword in WearableSensingManager and WearableSensingService."
     bug: "310055381"
diff --git a/core/java/android/appwidget/flags.aconfig b/core/java/android/appwidget/flags.aconfig
index 822f02f..4511954 100644
--- a/core/java/android/appwidget/flags.aconfig
+++ b/core/java/android/appwidget/flags.aconfig
@@ -2,6 +2,7 @@
 
 flag {
   name: "generated_previews"
+  is_exported: true
   namespace: "app_widgets"
   description: "Enable support for generated previews in AppWidgetManager"
   bug: "306546610"
@@ -26,6 +27,7 @@
 
 flag {
   name: "draw_data_parcel"
+  is_exported: true
   namespace: "app_widgets"
   description: "Enable support for transporting draw instructions as data parcel"
   bug: "286130467"
diff --git a/core/java/android/companion/flags.aconfig b/core/java/android/companion/flags.aconfig
index d634b64..ecc5e1b 100644
--- a/core/java/android/companion/flags.aconfig
+++ b/core/java/android/companion/flags.aconfig
@@ -2,6 +2,7 @@
 
 flag {
     name: "new_association_builder"
+    is_exported: true
     namespace: "companion"
     description: "Controls if the new Builder is exposed to test apis."
     bug: "296251481"
@@ -16,6 +17,7 @@
 
 flag {
     name: "association_tag"
+    is_exported: true
     namespace: "companion"
     description: "Enable Association tag APIs "
     bug: "289241123"
@@ -23,6 +25,7 @@
 
 flag {
     name: "device_presence"
+    is_exported: true
     namespace: "companion"
     description: "Enable device presence APIs"
     bug: "283000075"
@@ -30,6 +33,7 @@
 
 flag {
     name: "perm_sync_user_consent"
+    is_exported: true
     namespace: "companion"
     description: "Expose perm sync user consent API"
     bug: "309528663"
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/flags.aconfig b/core/java/android/companion/virtual/flags.aconfig
index 588e4fc..a6a4f5e 100644
--- a/core/java/android/companion/virtual/flags.aconfig
+++ b/core/java/android/companion/virtual/flags.aconfig
@@ -19,6 +19,7 @@
 
 flag {
   name: "dynamic_policy"
+  is_exported: true
   namespace: "virtual_devices"
   description: "Enable dynamic policy API"
   bug: "298401780"
@@ -26,6 +27,7 @@
 
 flag {
   name: "cross_device_clipboard"
+  is_exported: true
   namespace: "virtual_devices"
   description: "Enable cross-device clipboard API"
   bug: "306622082"
@@ -40,6 +42,7 @@
 
 flag {
   name: "vdm_custom_ime"
+  is_exported: true
   namespace: "virtual_devices"
   description: "Enable custom IME API"
   bug: "287269288"
@@ -47,6 +50,7 @@
 
 flag {
   name: "vdm_custom_home"
+  is_exported: true
   namespace: "virtual_devices"
   description: "Enable custom home API"
   bug: "297168328"
@@ -54,6 +58,7 @@
 
 flag {
   name: "vdm_public_apis"
+  is_exported: true
   namespace: "virtual_devices"
   description: "Enable public VDM API for device capabilities"
   bug: "297253526"
@@ -61,6 +66,7 @@
 
 flag {
   name: "virtual_camera"
+  is_exported: true
   namespace: "virtual_devices"
   description: "Enable Virtual Camera"
   bug: "270352264"
@@ -82,6 +88,7 @@
 
 flag {
   name: "persistent_device_id_api"
+  is_exported: true
   namespace: "virtual_devices"
   description: "Enable persistent device ID notification API"
   bug: "295258915"
@@ -96,6 +103,7 @@
 
 flag {
   name: "interactive_screen_mirror"
+  is_exported: true
   namespace: "virtual_devices"
   description: "Enable interactive screen mirroring using Virtual Devices"
   bug: "292212199"
@@ -103,6 +111,7 @@
 
 flag {
   name: "virtual_stylus"
+  is_exported: true
   namespace: "virtual_devices"
   description: "Enable virtual stylus input"
   bug: "304829446"
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 284e318..c0c91cb 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -5067,7 +5067,6 @@
      * {@link android.hardware.fingerprint.FingerprintManager} for handling management
      * of fingerprints.
      *
-     * @removed See {@link android.hardware.biometrics.BiometricPrompt}
      * @see #getSystemService(String)
      * @see android.hardware.fingerprint.FingerprintManager
      */
diff --git a/core/java/android/content/flags/flags.aconfig b/core/java/android/content/flags/flags.aconfig
index 3445fb5..27bce5b 100644
--- a/core/java/android/content/flags/flags.aconfig
+++ b/core/java/android/content/flags/flags.aconfig
@@ -2,6 +2,7 @@
 
 flag {
     name: "enable_bind_package_isolated_process"
+    is_exported: true
     namespace: "machine_learning"
     description: "This flag enables the newly added flag for binding package-private isolated processes."
     bug: "312706530"
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/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig
index 92cb9cc..cde565b 100644
--- a/core/java/android/content/pm/flags.aconfig
+++ b/core/java/android/content/pm/flags.aconfig
@@ -2,6 +2,7 @@
 
 flag {
     name: "quarantined_enabled"
+    is_exported: true
     namespace: "package_manager_service"
     description: "Feature flag for Quarantined state"
     bug: "269127435"
@@ -9,6 +10,7 @@
 
 flag {
     name: "archiving"
+    is_exported: true
     namespace: "package_manager_service"
     description: "Feature flag to enable the archiving feature."
     bug: "278553670"
@@ -24,6 +26,7 @@
 
 flag {
     name: "stay_stopped"
+    is_exported: true
     namespace: "backstage_power"
     description: "Feature flag to improve stopped state enforcement"
     bug: "296644915"
@@ -39,6 +42,7 @@
 
 flag {
     name: "get_package_info"
+    is_exported: true
     namespace: "package_manager_service"
     description: "Feature flag to enable the feature to retrieve package info without installation."
     bug: "269149275"
@@ -54,6 +58,7 @@
 
 flag {
     name: "sdk_lib_independence"
+    is_exported: true
     namespace: "package_manager_service"
     description: "Feature flag to keep app working even if its declared sdk-library dependency is unavailable."
     bug: "295827951"
@@ -78,6 +83,7 @@
 
 flag {
     name: "get_resolved_apk_path"
+    is_exported: true
     namespace: "package_manager_service"
     description: "Feature flag to retrieve resolved path of the base APK during an app install."
     bug: "269728874"
@@ -92,6 +98,7 @@
 
 flag {
     name: "read_install_info"
+    is_exported: true
     namespace: "package_manager_service"
     description: "Feature flag to read install related information from an APK."
     bug: "275658500"
@@ -113,6 +120,7 @@
 
 flag {
     name: "relative_reference_intent_filters"
+    is_exported: true
     namespace: "package_manager_service"
     description: "Feature flag to enable relative reference intent filters"
     bug: "307556883"
@@ -121,6 +129,7 @@
 
 flag {
     name: "fix_duplicated_flags"
+    is_exported: true
     namespace: "package_manager_service"
     description: "Feature flag to fix duplicated PackageManager flag values"
     bug: "314815969"
@@ -128,6 +137,7 @@
 
 flag {
     name: "provide_info_of_apk_in_apex"
+    is_exported: true
     namespace: "package_manager_service"
     description: "Feature flag to provide the information of APK-in-APEX"
     bug: "306329516"
@@ -144,6 +154,7 @@
 
 flag {
     name: "introduce_media_processing_type"
+    is_exported: true
     namespace: "backstage_power"
     description: "Add a new FGS type for media processing use cases."
     bug: "317788011"
@@ -182,6 +193,7 @@
 
 flag {
     name: "emergency_install_permission"
+    is_exported: true
     namespace: "permissions"
     description: "Feature flag to enable permission EMERGENCY_INSTALL_PACKAGES"
     bug: "321080601"
@@ -189,6 +201,7 @@
 
 flag {
     name: "asl_in_apk_app_metadata_source"
+    is_exported: true
     namespace: "package_manager_service"
     description: "Feature flag to allow to know if the Android Safety Label (ASL) of an app is provided by the app's APK itself, or provided by an installer."
     bug: "287487923"
@@ -205,6 +218,7 @@
 
 flag {
     name: "set_pre_verified_domains"
+    is_exported: true
     namespace: "package_manager_service"
     description: "Feature flag to enable pre-verified domains"
     bug: "307327678"
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index 2d32aed..4963a4f 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -24,6 +24,7 @@
 
 flag {
     name: "support_communal_profile"
+    is_exported: true
     namespace: "multiuser"
     description: "Framework support for communal profile."
     bug: "285426179"
@@ -31,6 +32,7 @@
 
 flag {
     name: "support_communal_profile_nextgen"
+    is_exported: true
     namespace: "multiuser"
     description: "Further framework support for communal profile, beyond the basics, for later releases."
     bug: "285426179"
@@ -59,6 +61,7 @@
 
 flag {
     name: "enable_biometrics_to_unlock_private_space"
+    is_exported: true
     namespace: "profile_experiences"
     description: "Add support to unlock the private space using biometrics"
     bug: "312184187"
@@ -102,6 +105,7 @@
 
 flag {
     name: "enable_system_user_only_for_services_and_providers"
+    is_exported: true
     namespace: "multiuser"
     description: "Enable systemUserOnly manifest attribute for services and providers."
     bug: "302354856"
@@ -118,6 +122,7 @@
 
 flag {
     name: "enable_permission_to_access_hidden_profiles"
+    is_exported: true
     namespace: "profile_experiences"
     description: "Add permission to access API hidden users data via system APIs"
     bug: "321988638"
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/content/res/flags.aconfig b/core/java/android/content/res/flags.aconfig
index 7fd0b03..8f5c912 100644
--- a/core/java/android/content/res/flags.aconfig
+++ b/core/java/android/content/res/flags.aconfig
@@ -2,6 +2,7 @@
 
 flag {
     name: "default_locale"
+    is_exported: true
     namespace: "resource_manager"
     description: "Feature flag for default locale in LocaleConfig"
     bug: "117306409"
@@ -11,6 +12,7 @@
 
 flag {
     name: "font_scale_converter_public"
+    is_exported: true
     namespace: "accessibility"
     description: "Enables the public API for FontScaleConverter, including enabling thread-safe caching."
     bug: "239736383"
@@ -20,6 +22,7 @@
 
 flag {
     name: "asset_file_descriptor_frro"
+    is_exported: true
     namespace: "resource_manager"
     description: "Feature flag for passing in an AssetFileDescriptor to create an frro"
     bug: "304478666"
@@ -27,6 +30,7 @@
 
 flag {
     name: "manifest_flagging"
+    is_exported: true
     namespace: "resource_manager"
     description: "Feature flag for flagging manifest entries"
     bug: "297373084"
@@ -36,6 +40,7 @@
 
 flag {
     name: "nine_patch_frro"
+    is_exported: true
     namespace: "resource_manager"
     description: "Feature flag for creating an frro from a 9-patch"
     bug: "296324826"
@@ -43,6 +48,7 @@
 
 flag {
     name: "register_resource_paths"
+    is_exported: true
     namespace: "resource_manager"
     description: "Feature flag for register resource paths for shared library"
     bug: "306202569"
diff --git a/core/java/android/credentials/flags.aconfig b/core/java/android/credentials/flags.aconfig
index 16ca31f..d077329 100644
--- a/core/java/android/credentials/flags.aconfig
+++ b/core/java/android/credentials/flags.aconfig
@@ -3,6 +3,7 @@
 flag {
     namespace: "credential_manager"
     name: "settings_activity_enabled"
+    is_exported: true
     description: "Enable the Credential Manager Settings Activity APIs"
     bug: "300014059"
 }
@@ -24,6 +25,7 @@
 flag {
     namespace: "credential_manager"
     name: "new_settings_intents"
+    is_exported: true
     description: "Enables settings intents to redirect to new settings page"
     bug: "307587989"
 }
@@ -45,6 +47,7 @@
 flag {
     namespace: "credential_manager"
     name: "configurable_selector_ui_enabled"
+    is_exported: true
     description: "Enables OEM configurable Credential Selector UI"
     bug: "319448437"
     is_exported: true
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/database/sqlite/flags.aconfig b/core/java/android/database/sqlite/flags.aconfig
index 92ef9c2..7ecffaf 100644
--- a/core/java/android/database/sqlite/flags.aconfig
+++ b/core/java/android/database/sqlite/flags.aconfig
@@ -2,6 +2,7 @@
 
 flag {
      name: "sqlite_apis_35"
+     is_exported: true
      namespace: "system_performance"
      is_fixed_read_only: true
      description: "SQLite APIs held back for Android 15"
diff --git a/core/java/android/hardware/biometrics/flags.aconfig b/core/java/android/hardware/biometrics/flags.aconfig
index ff07498..9836eec 100644
--- a/core/java/android/hardware/biometrics/flags.aconfig
+++ b/core/java/android/hardware/biometrics/flags.aconfig
@@ -18,6 +18,7 @@
 
 flag {
   name: "get_op_id_crypto_object"
+  is_exported: true
   namespace: "biometrics_framework"
   description: "Feature flag for adding a get operation id api to CryptoObject."
   bug: "307601768"
@@ -25,8 +26,8 @@
 
 flag {
   name: "custom_biometric_prompt"
+  is_exported: true
   namespace: "biometrics_framework"
   description: "Feature flag for adding a custom content view API to BiometricPrompt.Builder."
   bug: "302735104"
 }
-
diff --git a/core/java/android/hardware/devicestate/feature/flags.aconfig b/core/java/android/hardware/devicestate/feature/flags.aconfig
index 73a9e34..e474603 100644
--- a/core/java/android/hardware/devicestate/feature/flags.aconfig
+++ b/core/java/android/hardware/devicestate/feature/flags.aconfig
@@ -2,6 +2,7 @@
 
 flag {
     name: "device_state_property_api"
+    is_exported: true
     namespace: "windowing_sdk"
     description: "Updated DeviceState hasProperty API"
     bug: "293636629"
diff --git a/core/java/android/hardware/fingerprint/FingerprintManager.java b/core/java/android/hardware/fingerprint/FingerprintManager.java
index b0f69f5..81e321d 100644
--- a/core/java/android/hardware/fingerprint/FingerprintManager.java
+++ b/core/java/android/hardware/fingerprint/FingerprintManager.java
@@ -83,8 +83,7 @@
 
 /**
  * A class that coordinates access to the fingerprint hardware.
- *
- * @removed See {@link BiometricPrompt} which shows a system-provided dialog upon starting
+ * @deprecated See {@link BiometricPrompt} which shows a system-provided dialog upon starting
  * authentication. In a world where devices may have different types of biometric authentication,
  * it's much more realistic to have a system-provided authentication dialog since the method may
  * vary by vendor/device.
@@ -95,6 +94,7 @@
 @RequiresFeature(PackageManager.FEATURE_FINGERPRINT)
 public class FingerprintManager implements BiometricAuthenticator, BiometricFingerprintConstants {
     private static final String TAG = "FingerprintManager";
+    private static final boolean DEBUG = true;
     private static final int MSG_ENROLL_RESULT = 100;
     private static final int MSG_ACQUIRED = 101;
     private static final int MSG_AUTHENTICATION_SUCCEEDED = 102;
@@ -196,7 +196,6 @@
 
     /**
      * Retrieves a test session for FingerprintManager.
-     *
      * @hide
      */
     @TestApi
@@ -255,10 +254,9 @@
     }
 
     /**
-     * A wrapper class for the crypto objects supported by FingerprintManager. Currently, the
+     * A wrapper class for the crypto objects supported by FingerprintManager. Currently the
      * framework supports {@link Signature}, {@link Cipher} and {@link Mac} objects.
-     *
-     * @removed See {@link android.hardware.biometrics.BiometricPrompt.CryptoObject}
+     * @deprecated See {@link android.hardware.biometrics.BiometricPrompt.CryptoObject}
      */
     @Deprecated
     public static final class CryptoObject extends android.hardware.biometrics.CryptoObject {
@@ -332,8 +330,7 @@
     /**
      * Container for callback data from {@link FingerprintManager#authenticate(CryptoObject,
      *     CancellationSignal, int, AuthenticationCallback, Handler)}.
-     *
-     * @removed See {@link android.hardware.biometrics.BiometricPrompt.AuthenticationResult}
+     * @deprecated See {@link android.hardware.biometrics.BiometricPrompt.AuthenticationResult}
      */
     @Deprecated
     public static class AuthenticationResult {
@@ -395,8 +392,7 @@
      * FingerprintManager#authenticate(CryptoObject, CancellationSignal,
      * int, AuthenticationCallback, Handler) } must provide an implementation of this for listening to
      * fingerprint events.
-     *
-     * @removed See {@link android.hardware.biometrics.BiometricPrompt.AuthenticationCallback}
+     * @deprecated See {@link android.hardware.biometrics.BiometricPrompt.AuthenticationCallback}
      */
     @Deprecated
     public static abstract class AuthenticationCallback
@@ -459,7 +455,6 @@
     /**
      * Callback structure provided for {@link #detectFingerprint(CancellationSignal,
      * FingerprintDetectionCallback, int, Surface)}.
-     *
      * @hide
      */
     public interface FingerprintDetectionCallback {
@@ -613,8 +608,7 @@
      *         by <a href="{@docRoot}training/articles/keystore.html">Android Keystore
      *         facility</a>.
      * @throws IllegalStateException if the crypto primitive is not initialized.
-     *
-     * @removed See {@link BiometricPrompt#authenticate(CancellationSignal, Executor,
+     * @deprecated See {@link BiometricPrompt#authenticate(CancellationSignal, Executor,
      * BiometricPrompt.AuthenticationCallback)} and {@link BiometricPrompt#authenticate(
      * BiometricPrompt.CryptoObject, CancellationSignal, Executor,
      * BiometricPrompt.AuthenticationCallback)}
@@ -629,7 +623,6 @@
     /**
      * Per-user version of authenticate.
      * @deprecated use {@link #authenticate(CryptoObject, CancellationSignal, AuthenticationCallback, Handler, FingerprintAuthenticateOptions)}.
-     *
      * @hide
      */
     @Deprecated
@@ -642,7 +635,6 @@
     /**
      * Per-user and per-sensor version of authenticate.
      * @deprecated use {@link #authenticate(CryptoObject, CancellationSignal, AuthenticationCallback, Handler, FingerprintAuthenticateOptions)}.
-     *
      * @hide
      */
     @Deprecated
@@ -659,7 +651,6 @@
 
     /**
      * Version of authenticate with additional options.
-     *
      * @hide
      */
     @RequiresPermission(anyOf = {USE_BIOMETRIC, USE_FINGERPRINT})
@@ -707,7 +698,6 @@
     /**
      * Uses the fingerprint hardware to detect for the presence of a finger, without giving details
      * about accept/reject/lockout.
-     *
      * @hide
      */
     @RequiresPermission(USE_BIOMETRIC_INTERNAL)
@@ -750,7 +740,6 @@
      * @param callback an object to receive enrollment events
      * @param shouldLogMetrics a flag that indicates if enrollment failure/success metrics
      * should be logged.
-     *
      * @hide
      */
     @RequiresPermission(MANAGE_FINGERPRINT)
@@ -821,7 +810,6 @@
     /**
      * Same as {@link #generateChallenge(int, GenerateChallengeCallback)}, but assumes the first
      * enumerated sensor.
-     *
      * @hide
      */
     @RequiresPermission(MANAGE_FINGERPRINT)
@@ -836,7 +824,6 @@
 
     /**
      * Revokes the specified challenge.
-     *
      * @hide
      */
     @RequiresPermission(MANAGE_FINGERPRINT)
@@ -862,7 +849,6 @@
      * @param sensorId Sensor ID that this operation takes effect for
      * @param userId User ID that this operation takes effect for.
      * @param hardwareAuthToken An opaque token returned by password confirmation.
-     *
      * @hide
      */
     @RequiresPermission(RESET_FINGERPRINT_LOCKOUT)
@@ -900,7 +886,6 @@
 
     /**
      * Removes all fingerprint templates for the given user.
-     *
      * @hide
      */
     @RequiresPermission(MANAGE_FINGERPRINT)
@@ -1020,7 +1005,6 @@
     /**
      * Forwards BiometricStateListener to FingerprintService
      * @param listener new BiometricStateListener being added
-     *
      * @hide
      */
     public void registerBiometricStateListener(@NonNull BiometricStateListener listener) {
@@ -1172,8 +1156,7 @@
     }
 
     /**
-     * This is triggered by SideFpsEventHandler.
-     *
+     * This is triggered by SideFpsEventHandler
      * @hide
      */
     @RequiresPermission(USE_BIOMETRIC_INTERNAL)
@@ -1186,8 +1169,7 @@
      * Determine if there is at least one fingerprint enrolled.
      *
      * @return true if at least one fingerprint is enrolled, false otherwise
-     *
-     * @removed See {@link BiometricPrompt} and
+     * @deprecated See {@link BiometricPrompt} and
      * {@link FingerprintManager#FINGERPRINT_ERROR_NO_FINGERPRINTS}
      */
     @Deprecated
@@ -1221,8 +1203,7 @@
      * Determine if fingerprint hardware is present and functional.
      *
      * @return true if hardware is present and functional, false otherwise.
-     *
-     * @removed See {@link BiometricPrompt} and
+     * @deprecated See {@link BiometricPrompt} and
      * {@link FingerprintManager#FINGERPRINT_ERROR_HW_UNAVAILABLE}
      */
     @Deprecated
@@ -1248,7 +1229,6 @@
 
     /**
      * Get statically configured sensor properties.
-     *
      * @hide
      */
     @RequiresPermission(USE_BIOMETRIC_INTERNAL)
@@ -1267,7 +1247,6 @@
     /**
      * Returns whether the device has a power button fingerprint sensor.
      * @return boolean indicating whether power button is fingerprint sensor
-     *
      * @hide
      */
     public boolean isPowerbuttonFps() {
diff --git a/core/java/android/hardware/flags/overlayproperties_flags.aconfig b/core/java/android/hardware/flags/overlayproperties_flags.aconfig
index c6a352e..1165e65 100644
--- a/core/java/android/hardware/flags/overlayproperties_flags.aconfig
+++ b/core/java/android/hardware/flags/overlayproperties_flags.aconfig
@@ -2,6 +2,7 @@
 
 flag {
     name: "overlayproperties_class_api"
+    is_exported: true
     namespace: "core_graphics"
     description: "public OverlayProperties class, OverlayProperties#supportMixedColorSpaces and Display#getOverlaySupport API"
     bug: "267234573"
diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig
index e070fe5..9684e64 100644
--- a/core/java/android/hardware/input/input_framework.aconfig
+++ b/core/java/android/hardware/input/input_framework.aconfig
@@ -27,6 +27,7 @@
 flag {
     namespace: "input_native"
     name: "pointer_coords_is_resampled_api"
+    is_exported: true
     description: "Makes MotionEvent.PointerCoords#isResampled() a public API"
     bug: "298197511"
 }
@@ -34,6 +35,7 @@
 flag {
     namespace: "input_native"
     name: "emoji_and_screenshot_keycodes_available"
+    is_exported: true
     description: "Add new KeyEvent keycodes for opening Emoji Picker and Taking Screenshots"
     bug: "315307777"
 }
diff --git a/core/java/android/hardware/radio/flags.aconfig b/core/java/android/hardware/radio/flags.aconfig
index dbc1a4b..d0d10c1 100644
--- a/core/java/android/hardware/radio/flags.aconfig
+++ b/core/java/android/hardware/radio/flags.aconfig
@@ -2,6 +2,7 @@
 
 flag {
     name: "hd_radio_improved"
+    is_exported: true
     namespace: "car_framework"
     description: "Feature flag for improved HD radio support with less vendor extensions"
     bug: "280300929"
diff --git a/core/java/android/hardware/usb/flags/system_sw_usb_flags.aconfig b/core/java/android/hardware/usb/flags/system_sw_usb_flags.aconfig
index 9e487e1..fac02ce 100644
--- a/core/java/android/hardware/usb/flags/system_sw_usb_flags.aconfig
+++ b/core/java/android/hardware/usb/flags/system_sw_usb_flags.aconfig
@@ -2,6 +2,7 @@
 
 flag {
     name: "enable_usb_data_compliance_warning"
+    is_exported: true
     namespace: "system_sw_usb"
     description: "Enable USB data compliance warnings when set"
     bug: "296119135"
diff --git a/core/java/android/hardware/usb/flags/usb_framework_flags.aconfig b/core/java/android/hardware/usb/flags/usb_framework_flags.aconfig
index a495631..3dd746c 100644
--- a/core/java/android/hardware/usb/flags/usb_framework_flags.aconfig
+++ b/core/java/android/hardware/usb/flags/usb_framework_flags.aconfig
@@ -2,6 +2,7 @@
 
 flag {
     name: "enable_is_pd_compliant_api"
+    is_exported: true
     namespace: "usb"
     description: "Feature flag for the api to check if a port is PD compliant"
     bug: "323470419"
@@ -9,6 +10,7 @@
 
 flag {
     name: "enable_is_mode_change_supported_api"
+    is_exported: true
     namespace: "usb"
     description: "Feature flag for the api to check if a port supports mode change"
     bug: "323470419"
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/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/os/flags.aconfig b/core/java/android/os/flags.aconfig
index 375d729..311e991 100644
--- a/core/java/android/os/flags.aconfig
+++ b/core/java/android/os/flags.aconfig
@@ -26,6 +26,7 @@
 
 flag {
     name: "remove_app_profiler_pss_collection"
+    is_exported: true
     namespace: "backstage_power"
     description: "Replaces background PSS collection in AppProfiler with RSS"
     bug: "297542292"
@@ -33,6 +34,7 @@
 
 flag {
     name: "allow_thermal_headroom_thresholds"
+    is_exported: true
     namespace: "game"
     description: "Enable thermal headroom thresholds API"
     bug: "288119641"
@@ -41,6 +43,7 @@
 # This flag guards the private space feature, its APIs, and some of the feature implementations. The flag android.multiuser.Flags.enable_private_space_features exclusively guards all the implementations.
 flag {
     name: "allow_private_profile"
+    is_exported: true
     namespace: "profile_experiences"
     description: "Guards a new Private Profile type in UserManager - everything from its setup to config to deletion."
     bug: "299069460"
@@ -49,6 +52,7 @@
 
 flag {
     name: "bugreport_mode_max_value"
+    is_exported: true
     namespace: "telephony"
     description: "Introduce a constant as maximum value of bugreport mode."
     bug: "305067125"
@@ -56,6 +60,7 @@
 
 flag {
     name: "adpf_prefer_power_efficiency"
+    is_exported: true
     namespace: "game"
     description: "Guards the ADPF power efficiency API"
     bug: "288117936"
@@ -63,6 +68,7 @@
 
 flag {
     name: "security_state_service"
+    is_exported: true
     namespace: "dynamic_spl"
     description: "Guards the Security State API."
     bug: "302189431"
@@ -70,6 +76,7 @@
 
 flag {
     name: "battery_saver_supported_check_api"
+    is_exported: true
     namespace: "backstage_power"
     description: "Guards a new API in PowerManager to check if battery saver is supported or not."
     bug: "305067031"
@@ -77,6 +84,7 @@
 
 flag {
     name: "adpf_gpu_report_actual_work_duration"
+    is_exported: true
     namespace: "game"
     description: "Guards the ADPF GPU APIs."
     bug: "284324521"
@@ -114,6 +122,7 @@
 
 flag {
     name: "battery_part_status_api"
+    is_exported: true
     namespace: "phoenix"
     description: "Feature flag for adding Health HAL v3 APIs."
     is_fixed_read_only: true
@@ -122,6 +131,7 @@
 
 flag {
     name: "storage_lifetime_api"
+    is_exported: true
     namespace: "phoenix"
     description: "Feature flag for adding storage component health APIs."
     is_fixed_read_only: true
@@ -131,6 +141,7 @@
 flag {
      namespace: "system_performance"
      name: "telemetry_apis_framework_initialization"
+     is_exported: true
      description: "Control framework initialization APIs of telemetry APIs feature."
      is_fixed_read_only: true
      bug: "324241334"
diff --git a/core/java/android/os/vibrator/flags.aconfig b/core/java/android/os/vibrator/flags.aconfig
index d485eca..bb0498e 100644
--- a/core/java/android/os/vibrator/flags.aconfig
+++ b/core/java/android/os/vibrator/flags.aconfig
@@ -10,6 +10,7 @@
 flag {
     namespace: "haptics"
     name: "haptics_customization_enabled"
+    is_exported: true
     description: "Enables the haptics customization feature"
     bug: "241918098"
 }
diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig
index 999bc99..2710df2 100644
--- a/core/java/android/permission/flags.aconfig
+++ b/core/java/android/permission/flags.aconfig
@@ -2,6 +2,7 @@
 
 flag {
   name: "device_aware_permission_apis_enabled"
+  is_exported: true
   is_fixed_read_only: true
   namespace: "permissions"
   description: "enable device aware permission APIs"
@@ -10,6 +11,7 @@
 
 flag {
   name: "voice_activation_permission_apis"
+  is_exported: true
   namespace: "permissions"
   description: "enable voice activation permission APIs"
   bug: "287264308"
@@ -17,6 +19,7 @@
 
 flag {
     name: "system_server_role_controller_enabled"
+    is_exported: true
     is_fixed_read_only: true
     namespace: "permissions"
     description: "enable role controller in system server"
@@ -25,6 +28,7 @@
 
 flag {
   name: "set_next_attribution_source"
+  is_exported: true
   namespace: "permissions"
   description: "enable AttributionSource.setNextAttributionSource"
   bug: "304478648"
@@ -32,6 +36,7 @@
 
 flag {
     name: "should_register_attribution_source"
+    is_exported: true
     namespace: "permissions"
     description: "enable the shouldRegisterAttributionSource API"
     bug: "305057691"
@@ -39,6 +44,7 @@
 
 flag {
   name: "attribution_source_constructor"
+  is_exported: true
   namespace: "permissions"
   description: "enable AttributionSource(int, int, String, String, IBinder, String[], AttributionSource)"
   bug: "304478648"
@@ -46,6 +52,7 @@
 
 flag {
     name: "enhanced_confirmation_mode_apis_enabled"
+    is_exported: true
     is_fixed_read_only: true
     namespace: "permissions"
     description: "enable enhanced confirmation mode apis"
@@ -54,6 +61,7 @@
 
 flag {
   name: "op_enable_mobile_data_by_user"
+  is_exported: true
   namespace: "permissions"
   description: "enables logging of the OP_ENABLE_MOBILE_DATA_BY_USER"
   bug: "222650148"
@@ -61,6 +69,7 @@
 
 flag {
   name: "factory_reset_prep_permission_apis"
+  is_exported: true
   namespace: "wallet_integration"
   description: "enable Permission PREPARE_FACTORY_RESET."
   bug: "302016478"
@@ -68,6 +77,7 @@
 
 flag {
     name: "retail_demo_role_enabled"
+    is_exported: true
     namespace: "permissions"
     description: "default retail demo role holder"
     bug: "274132354"
@@ -82,6 +92,7 @@
 
 flag {
     name: "wallet_role_enabled"
+    is_exported: true
     namespace: "wallet_integration"
     description: "This flag is used to enabled the Wallet Role for all users on the device"
     bug: "283989236"
@@ -114,6 +125,7 @@
 
 flag {
      name: "get_emergency_role_holder_api_enabled"
+     is_exported: true
      is_fixed_read_only: true
      namespace: "permissions"
      description: "Enables the getEmergencyRoleHolder API."
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index aad2b4e..d0593e7 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -12386,6 +12386,17 @@
          */
         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.
+         *
+         * <p>When this setting is set to a non-zero value, all windows are treated as non-secure.
+         * Content in windows with {@link android.view.WindowManager.LayoutParams#FLAG_SECURE} will
+         * appear in screenshots and recordings.
+         *
+         * @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/provider/flags.aconfig b/core/java/android/provider/flags.aconfig
index ea1ac27..9245557 100644
--- a/core/java/android/provider/flags.aconfig
+++ b/core/java/android/provider/flags.aconfig
@@ -2,6 +2,7 @@
 
 flag {
     name: "system_settings_default"
+    is_exported: true
     namespace: "package_manager_service"
     description: "Enable Settings.System.resetToDefault APIs."
     bug: "279083734"
@@ -9,6 +10,7 @@
 
 flag {
     name: "user_keys"
+    is_exported: true
     namespace: "privacy_infra_policy"
     description: "This flag controls new E2EE contact keys API"
     bug: "290696572"
@@ -16,6 +18,7 @@
 
 flag {
     name: "backup_tasks_settings_screen"
+    is_exported: true
     namespace: "backstage_power"
     description: "Add a new settings page for the RUN_BACKUP_JOBS permission."
     bug: "320563660"
diff --git a/core/java/android/security/flags.aconfig b/core/java/android/security/flags.aconfig
index 3c77c44..7f5b550 100644
--- a/core/java/android/security/flags.aconfig
+++ b/core/java/android/security/flags.aconfig
@@ -10,6 +10,7 @@
 
 flag {
     name: "fsverity_api"
+    is_exported: true
     namespace: "hardware_backed_security"
     description: "Feature flag for fs-verity API"
     bug: "285185747"
@@ -64,6 +65,7 @@
 
 flag {
     name: "frp_enforcement"
+    is_exported: true
     namespace: "hardware_backed_security"
     description: "This flag controls whether PDB enforces FRP"
     bug: "290312729"
diff --git a/core/java/android/security/responsible_apis_flags.aconfig b/core/java/android/security/responsible_apis_flags.aconfig
index 0bae459..548f8aa 100644
--- a/core/java/android/security/responsible_apis_flags.aconfig
+++ b/core/java/android/security/responsible_apis_flags.aconfig
@@ -9,6 +9,7 @@
 
 flag {
     name: "asm_restrictions_enabled"
+    is_exported: true
     namespace: "responsible_apis"
     description: "Enables ASM restrictions for activity starts and finishes"
     bug: "230590090"
@@ -23,6 +24,7 @@
 
 flag {
     name: "content_uri_permission_apis"
+    is_exported: true
     namespace: "responsible_apis"
     description: "Enables the content URI permission APIs"
     bug: "293467489"
@@ -30,6 +32,7 @@
 
 flag {
     name: "enforce_intent_filter_match"
+    is_exported: true
     namespace: "responsible_apis"
     description: "Make delivered intents match components' intent filters"
     bug: "293560872"
diff --git a/core/java/android/service/appprediction/flags/flags.aconfig b/core/java/android/service/appprediction/flags/flags.aconfig
index c7e47d4..7f9764e 100644
--- a/core/java/android/service/appprediction/flags/flags.aconfig
+++ b/core/java/android/service/appprediction/flags/flags.aconfig
@@ -2,6 +2,7 @@
 
 flag {
   name: "service_features_api"
+  is_exported: true
   namespace: "systemui"
   description: "Guards the new requestServiceFeatures api"
   bug: "292565550"
diff --git a/core/java/android/service/chooser/flags.aconfig b/core/java/android/service/chooser/flags.aconfig
index d72441f..a3eff3b 100644
--- a/core/java/android/service/chooser/flags.aconfig
+++ b/core/java/android/service/chooser/flags.aconfig
@@ -2,6 +2,7 @@
 
 flag {
   name: "chooser_album_text"
+  is_exported: true
   namespace: "intentresolver"
   description: "Flag controlling the album text subtype hint for sharesheet"
   bug: "323380224"
@@ -9,6 +10,7 @@
 
 flag {
     name: "enable_sharesheet_metadata_extra"
+    is_exported: true
     namespace: "intentresolver"
     description: "This flag enables sharesheet metadata to be displayed to users."
     bug: "318942069"
@@ -16,6 +18,7 @@
 
 flag {
   name: "chooser_payload_toggling"
+  is_exported: true
   namespace: "intentresolver"
   description: "This flag controls content toggling in Chooser"
   bug: "302691505"
@@ -23,18 +26,8 @@
 
 flag {
   name: "enable_chooser_result"
+  is_exported: true
   namespace: "intentresolver"
   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/controls/flags/flags.aconfig b/core/java/android/service/controls/flags/flags.aconfig
index 3a28844..197f1bc 100644
--- a/core/java/android/service/controls/flags/flags.aconfig
+++ b/core/java/android/service/controls/flags/flags.aconfig
@@ -2,6 +2,7 @@
 
 flag {
     name: "home_panel_dream"
+    is_exported: true
     namespace: "systemui"
     description: "Enables the home controls dream feature."
     bug: "298025023"
diff --git a/core/java/android/service/notification/flags.aconfig b/core/java/android/service/notification/flags.aconfig
index c5acc2c..35cd3ed 100644
--- a/core/java/android/service/notification/flags.aconfig
+++ b/core/java/android/service/notification/flags.aconfig
@@ -10,6 +10,7 @@
 
 flag {
   name: "redact_sensitive_notifications_from_untrusted_listeners"
+  is_exported: true
   namespace: "systemui"
   description: "This flag controls the redacting of sensitive notifications from untrusted NotificationListenerServices"
   bug: "306271190"
@@ -18,6 +19,7 @@
 
 flag {
   name: "callstyle_callback_api"
+  is_exported: true
   namespace: "systemui"
   description: "Guards the new CallStyleNotificationEventsCallback"
   bug: "305095040"
diff --git a/core/java/android/service/voice/flags/flags.aconfig b/core/java/android/service/voice/flags/flags.aconfig
index 22e8cdd..633304b 100644
--- a/core/java/android/service/voice/flags/flags.aconfig
+++ b/core/java/android/service/voice/flags/flags.aconfig
@@ -2,6 +2,7 @@
 
 flag {
     name: "allow_training_data_egress_from_hds"
+    is_exported: true
     namespace: "machine_learning"
     description: "This flag allows the hotword detection service to egress training data to the default assistant."
     bug: "296074924"
@@ -9,6 +10,7 @@
 
 flag {
     name: "allow_hotword_bump_egress"
+    is_exported: true
     namespace: "machine_learning"
     description: "This flag allows hotword detection service to egress reason code for hotword bump."
     bug: "290951024"
@@ -16,6 +18,7 @@
 
 flag {
     name: "allow_foreground_activities_in_on_show"
+    is_exported: true
     namespace: "machine_learning"
     description: "This flag allows providing foreground app component along with onShow args."
     bug: "319409708"
@@ -23,6 +26,7 @@
 
 flag {
     name: "allow_various_attention_types"
+    is_exported: true
     namespace: "visual_query"
     description: "This flag allows visual query detection service to set different attention types."
     bug: "318617199"
@@ -30,6 +34,7 @@
 
 flag {
     name: "allow_complex_results_egress_from_vqds"
+    is_exported: true
     namespace: "visual_query"
     description: "This flag allows visual query detection service egress detailed results. "
     bug: "318617199"
@@ -37,6 +42,7 @@
 
 flag {
     name: "allow_speaker_id_egress"
+    is_exported: true
     namespace: "machine_learning"
     description: "This flag allows hotword detection service and visual query detection service to egress current speaker profile id."
     bug: "318617199"
diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java
index cd486d0..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;
 
@@ -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/speech/flags/speech_flags.aconfig b/core/java/android/speech/flags/speech_flags.aconfig
index fd80127..fa33592 100644
--- a/core/java/android/speech/flags/speech_flags.aconfig
+++ b/core/java/android/speech/flags/speech_flags.aconfig
@@ -2,6 +2,7 @@
 
 flag {
     name: "multilang_extra_launch"
+    is_exported: true
     namespace: "machine_learning"
     description: "Feature flag for adding new extra for multi-lang feature"
     bug: "312489931"
diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig
index 30b1a2e..8e1ac63 100644
--- a/core/java/android/text/flags/flags.aconfig
+++ b/core/java/android/text/flags/flags.aconfig
@@ -10,6 +10,7 @@
 
 flag {
   name: "new_fonts_fallback_xml"
+  is_exported: true
   namespace: "text"
   description: "Feature flag for deprecating fonts.xml. By setting true for this feature flag, the new font configuration XML, /system/etc/font_fallback.xml is used. The new XML has a new syntax and flexibility of variable font declarations, but it is not compatible with the apps that reads fonts.xml. So, fonts.xml is maintained as a subset of the font_fallback.xml"
   # Make read only, as it could be used before the Settings provider is initialized.
@@ -26,6 +27,7 @@
 
 flag {
   name: "fix_line_height_for_locale"
+  is_exported: true
   namespace: "text"
   description: "Feature flag that preserve the line height of the TextView and EditText even if the the locale is different from Latin"
   bug: "303326708"
@@ -33,6 +35,7 @@
 
 flag {
   name: "no_break_no_hyphenation_span"
+  is_exported: true
   namespace: "text"
   description: "A feature flag that adding new spans that prevents line breaking and hyphenation."
   bug: "283193586"
@@ -57,6 +60,7 @@
 
 flag {
   name: "use_bounds_for_width"
+  is_exported: true
   namespace: "text"
   description: "Feature flag for preventing horizontal clipping."
   bug: "63938206"
@@ -71,6 +75,7 @@
 
 flag {
   name: "word_style_auto"
+  is_exported: true
   namespace: "text"
   description: "A feature flag that implements line break word style auto."
   bug: "280005585"
@@ -78,6 +83,7 @@
 
 flag {
   name: "letter_spacing_justification"
+  is_exported: true
   namespace: "text"
   description: "A feature flag that implement inter character justification."
   bug: "283193133"
diff --git a/core/java/android/view/HandwritingInitiator.java b/core/java/android/view/HandwritingInitiator.java
index 192b2ec..f4dadbb 100644
--- a/core/java/android/view/HandwritingInitiator.java
+++ b/core/java/android/view/HandwritingInitiator.java
@@ -225,6 +225,7 @@
                     View candidateView = findBestCandidateView(mState.mStylusDownX,
                             mState.mStylusDownY, /* isHover */ false);
                     if (candidateView != null && candidateView.isEnabled()) {
+                        boolean candidateHasFocus = candidateView.hasFocus();
                         if (shouldShowHandwritingUnavailableMessageForView(candidateView)) {
                             int messagesResId = (candidateView instanceof TextView tv
                                     && tv.isAnyPasswordInputType())
@@ -243,23 +244,24 @@
                                     | MotionEvent.ACTION_CANCEL);
                             candidateView.getRootView().dispatchTouchEvent(motionEvent);
                         } else if (candidateView == getConnectedOrFocusedView()) {
-                            if (!mInitiateWithoutConnection && !candidateView.hasFocus()) {
+                            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);
+                                }
                             }
                         }
                     }
@@ -285,6 +287,9 @@
      * gained focus.
      */
     public void onDelegateViewFocused(@NonNull View view) {
+        if (mInitiateWithoutConnection) {
+            onEditorFocused(view);
+        }
         if (view == getConnectedView()) {
             tryAcceptStylusHandwritingDelegation(view);
         }
@@ -332,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.
@@ -378,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;
@@ -390,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;
     }
 
@@ -861,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. */
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/View.java b/core/java/android/view/View.java
index 41bfb24..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);
                 }
             }
         }
@@ -16701,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 304e43e..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);
@@ -8938,6 +8995,14 @@
             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);
@@ -9358,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));
@@ -9571,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;
@@ -11029,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
@@ -11048,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.
@@ -11060,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
@@ -12716,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/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
index 5b99c71f..91bd4ea 100644
--- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
+++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
@@ -4,6 +4,7 @@
 
 flag {
     name: "a11y_overlay_callbacks"
+    is_exported: true
     namespace: "accessibility"
     description: "Whether to allow the passing of result callbacks when attaching a11y overlays."
     bug: "304478691"
@@ -26,6 +27,7 @@
 flag {
     namespace: "accessibility"
     name: "braille_display_hid"
+    is_exported: true
     description: "Enables new APIs for an AccessibilityService to communicate with a HID Braille display"
     bug: "303522222"
 }
@@ -40,6 +42,7 @@
 flag {
     namespace: "accessibility"
     name: "collection_info_item_counts"
+    is_exported: true
     description: "Fields for total items and the number of important for accessibility items in a collection"
     bug: "302376158"
 }
@@ -61,6 +64,7 @@
 flag {
     namespace: "accessibility"
     name: "flash_notification_system_api"
+    is_exported: true
     description: "Makes flash notification APIs as system APIs for calling from mainline module"
     bug: "303131332"
 }
@@ -74,6 +78,7 @@
 
 flag {
     name: "motion_event_observing"
+    is_exported: true
     namespace: "accessibility"
     description: "Allows accessibility services to intercept but not consume motion events from specified sources."
     bug: "297595990"
@@ -82,6 +87,7 @@
 flag {
     namespace: "accessibility"
     name: "granular_scrolling"
+    is_exported: true
     description: "Allow the use of granular scrolling. This allows scrollable nodes to scroll by increments other than a full screen"
     bug: "302376158"
 }
@@ -103,6 +109,7 @@
 flag {
     namespace: "accessibility"
     name: "add_type_window_control"
+    is_exported: true
     description: "adds new TYPE_WINDOW_CONTROL to AccessibilityWindowInfo for detecting Window Decorations"
     bug: "320445550"
 }
diff --git a/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig b/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig
index 5d3153c..4de0f29 100644
--- a/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig
+++ b/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig
@@ -23,6 +23,7 @@
 
 flag {
     name: "create_accessibility_overlay_app_op_enabled"
+    is_exported: true
     namespace: "content_protection"
     description: "If true, an appop is logged on creation of accessibility overlays."
     bug: "289081465"
@@ -30,6 +31,7 @@
 
 flag {
     name: "rapid_clear_notifications_by_listener_app_op_enabled"
+    is_exported: true
     namespace: "content_protection"
     description: "If true, an appop is logged when a notification is rapidly cleared by a notification listener."
     bug: "289080543"
@@ -37,6 +39,7 @@
 
 flag {
     name: "manage_device_policy_enabled"
+    is_exported: true
     namespace: "content_protection"
     description: "If true, the APIs to manage content protection device policy will be enabled."
     bug: "319477846"
diff --git a/core/java/android/view/flags/refresh_rate_flags.aconfig b/core/java/android/view/flags/refresh_rate_flags.aconfig
index 05cabd5..06598b3 100644
--- a/core/java/android/view/flags/refresh_rate_flags.aconfig
+++ b/core/java/android/view/flags/refresh_rate_flags.aconfig
@@ -2,6 +2,7 @@
 
 flag {
     name: "view_velocity_api"
+    is_exported: true
     namespace: "toolkit"
     description: "Feature flag for view content velocity api"
     bug: "293513816"
@@ -16,6 +17,7 @@
 
 flag {
     name: "toolkit_set_frame_rate_read_only"
+    is_exported: true
     namespace: "toolkit"
     description: "Feature flag for toolkit to set frame rate"
     bug: "293512962"
@@ -24,6 +26,7 @@
 
 flag {
     name: "expected_presentation_time_api"
+    is_exported: true
     namespace: "toolkit"
     description: "Feature flag for using expected presentation time of the Choreographer"
     bug: "278730197"
@@ -31,6 +34,7 @@
 
 flag {
     name: "expected_presentation_time_read_only"
+    is_exported: true
     namespace: "toolkit"
     description: "Feature flag for using expected presentation time of the Choreographer"
     bug: "278730197"
diff --git a/core/java/android/view/flags/scroll_feedback_flags.aconfig b/core/java/android/view/flags/scroll_feedback_flags.aconfig
index d1d871c2..a7c4104 100644
--- a/core/java/android/view/flags/scroll_feedback_flags.aconfig
+++ b/core/java/android/view/flags/scroll_feedback_flags.aconfig
@@ -3,6 +3,7 @@
 flag {
     namespace: "toolkit"
     name: "scroll_feedback_api"
+    is_exported: true
     description: "Enable the scroll feedback APIs"
     bug: "239594271"
 }
diff --git a/core/java/android/view/flags/view_flags.aconfig b/core/java/android/view/flags/view_flags.aconfig
index 6cf89d6..c482f8b 100644
--- a/core/java/android/view/flags/view_flags.aconfig
+++ b/core/java/android/view/flags/view_flags.aconfig
@@ -28,6 +28,7 @@
 
 flag {
   name: "sensitive_content_app_protection_api"
+  is_exported: true
   namespace: "permissions"
   description: "This flag controls the new sensitive content protection API,"
     " The API will be used by other ui toolkits (i.e. compose, webview, custom virtual views)."
diff --git a/core/java/android/view/flags/window_insets.aconfig b/core/java/android/view/flags/window_insets.aconfig
index 201b7ad..bf6df5c 100644
--- a/core/java/android/view/flags/window_insets.aconfig
+++ b/core/java/android/view/flags/window_insets.aconfig
@@ -2,6 +2,7 @@
 
 flag {
     name: "customizable_window_headers"
+    is_exported: true
     namespace: "lse_desktop_experience"
     description: "Flag to control the caption bar appearance and to fit app content in its empty space"
     bug: "316387515"
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/view/inputmethod/flags.aconfig b/core/java/android/view/inputmethod/flags.aconfig
index 8d3920f..be74a65 100644
--- a/core/java/android/view/inputmethod/flags.aconfig
+++ b/core/java/android/view/inputmethod/flags.aconfig
@@ -10,6 +10,7 @@
 
 flag {
     name: "editorinfo_handwriting_enabled"
+    is_exported: true
     namespace: "input_method"
     description: "Feature flag for adding EditorInfo#mStylusHandwritingEnabled"
     bug: "293898187"
@@ -18,6 +19,7 @@
 
 flag {
     name: "imm_userhandle_hostsidetests"
+    is_exported: true
     namespace: "input_method"
     description: "Feature flag for replacing UserIdInt with UserHandle in some helper IMM functions"
     bug: "301713309"
@@ -26,6 +28,7 @@
 
 flag {
     name: "concurrent_input_methods"
+    is_exported: true
     namespace: "input_method"
     description: "Feature flag for concurrent multi-session IME"
     bug: "284527000"
@@ -34,6 +37,7 @@
 
 flag {
     name: "home_screen_handwriting_delegator"
+    is_exported: true
     namespace: "input_method"
     description: "Feature flag for supporting stylus handwriting delegation from RemoteViews on the home screen"
     bug: "279959705"
@@ -49,6 +53,7 @@
 
 flag {
     name: "use_zero_jank_proxy"
+    is_exported: true
     namespace: "input_method"
     description: "Feature flag for using a proxy that uses async calls to achieve zero jank for IMMS calls."
     bug: "293640003"
@@ -57,6 +62,7 @@
 
 flag {
     name: "ime_switcher_revamp"
+    is_exported: true
     namespace: "input_method"
     description: "Feature flag for revamping the Input Method Switcher menu"
     bug: "311791923"
@@ -73,6 +79,7 @@
 
 flag {
     name: "connectionless_handwriting"
+    is_exported: true
     namespace: "input_method"
     description: "Feature flag for connectionless stylus handwriting APIs"
     bug: "300979854"
diff --git a/core/java/android/webkit/flags.aconfig b/core/java/android/webkit/flags.aconfig
index 6938b29..2d834a8 100644
--- a/core/java/android/webkit/flags.aconfig
+++ b/core/java/android/webkit/flags.aconfig
@@ -2,6 +2,7 @@
 
 flag {
     name: "update_service_ipc_wrapper"
+    is_exported: true
     namespace: "webview"
     description: "New API: proper wrapper for IWebViewUpdateService"
     bug: "319292658"
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index dc060ba..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);
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/flags/large_screen_experiences_app_compat.aconfig b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig
index 254f4f7..7fbec67 100644
--- a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig
+++ b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig
@@ -18,6 +18,7 @@
 
 flag {
   name: "density_390_api"
+  is_exported: true
   namespace: "large_screen_experiences_app_compat"
   description: "Whether the API DisplayMetrics.DENSITY_390 is available"
   bug: "297550533"
@@ -26,6 +27,7 @@
 
 flag {
   name: "app_compat_properties_api"
+  is_exported: true
   namespace: "large_screen_experiences_app_compat"
   description: "Whether app compat property APIs are public. Which includes: /n"
                  "WindowManager.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE,/n"
diff --git a/core/java/android/window/flags/wallpaper_manager.aconfig b/core/java/android/window/flags/wallpaper_manager.aconfig
index ea9da96..dea9497 100644
--- a/core/java/android/window/flags/wallpaper_manager.aconfig
+++ b/core/java/android/window/flags/wallpaper_manager.aconfig
@@ -2,6 +2,7 @@
 
 flag {
     name: "always_update_wallpaper_permission"
+    is_exported: true
     namespace: "wear_frameworks"
     description: "Allow out of focus process to update wallpaper complications"
     bug: "271132915"
diff --git a/core/java/android/window/flags/window_surfaces.aconfig b/core/java/android/window/flags/window_surfaces.aconfig
index 3f48341..00b600c 100644
--- a/core/java/android/window/flags/window_surfaces.aconfig
+++ b/core/java/android/window/flags/window_surfaces.aconfig
@@ -45,6 +45,7 @@
 flag {
     namespace: "window_surfaces"
     name: "trusted_presentation_listener_for_window"
+    is_exported: true
     description: "Enable trustedPresentationListener on windows public API"
     is_fixed_read_only: true
     bug: "278027319"
@@ -53,6 +54,7 @@
 flag {
     namespace: "window_surfaces"
     name: "sdk_desired_present_time"
+    is_exported: true
     description: "Feature flag for the new SDK API to set desired present time"
     is_fixed_read_only: true
     bug: "295038072"
@@ -61,6 +63,7 @@
 flag {
     namespace: "window_surfaces"
     name: "surface_control_input_receiver"
+    is_exported: true
     description: "Enable public API to register an InputReceiver for a SurfaceControl"
     is_fixed_read_only: true
     bug: "278757236"
@@ -69,6 +72,7 @@
 flag {
     namespace: "window_surfaces"
     name: "screen_recording_callbacks"
+    is_exported: true
     description: "Enable screen recording callbacks public API"
     is_fixed_read_only: true
     bug: "304574518"
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index 65bf241..247f28c 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -16,6 +16,7 @@
 
 flag {
   name: "enforce_edge_to_edge"
+  is_exported: true
   namespace: "windowing_frontend"
   description: "Make app go edge-to-edge when targeting SDK level 35 or greater"
   bug: "309578419"
@@ -88,6 +89,7 @@
 
 flag {
     name: "supports_multi_instance_system_ui"
+    is_exported: true
     namespace: "multitasking"
     description: "Feature flag to enable a multi-instance system ui component property."
     bug: "262864589"
@@ -96,6 +98,7 @@
 
 flag {
   name: "delegate_unhandled_drags"
+  is_exported: true
   namespace: "multitasking"
   description: "Enables delegating unhandled drags to SystemUI"
   bug: "320797628"
diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig
index 82e613e..4b3d8e8 100644
--- a/core/java/android/window/flags/windowing_sdk.aconfig
+++ b/core/java/android/window/flags/windowing_sdk.aconfig
@@ -43,6 +43,7 @@
 flag {
     namespace: "windowing_sdk"
     name: "untrusted_embedding_any_app_permission"
+    is_exported: true
     description: "Feature flag to enable the permission to embed any app in untrusted mode."
     bug: "293647332"
     is_fixed_read_only: true
@@ -59,6 +60,7 @@
 flag {
     namespace: "windowing_sdk"
     name: "untrusted_embedding_state_sharing"
+    is_exported: true
     description: "Feature flag to enable state sharing in untrusted embedding when apps opt in."
     bug: "293647332"
     is_fixed_read_only: true
@@ -74,6 +76,7 @@
 flag {
     namespace: "windowing_sdk"
     name: "cover_display_opt_in"
+    is_exported: true
     description: "Properties to allow apps and activities to opt-in to cover display rendering"
     bug: "312530526"
     is_fixed_read_only: true
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/SuspendedAppActivity.java b/core/java/com/android/internal/app/SuspendedAppActivity.java
index 467cd49..751368f 100644
--- a/core/java/com/android/internal/app/SuspendedAppActivity.java
+++ b/core/java/com/android/internal/app/SuspendedAppActivity.java
@@ -16,6 +16,7 @@
 
 package com.android.internal.app;
 
+import static android.app.admin.flags.Flags.crossUserSuspensionEnabled;
 import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
 import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
 import static android.content.pm.SuspendDialogInfo.BUTTON_ACTION_MORE_DETAILS;
@@ -59,6 +60,7 @@
     public static final String EXTRA_SUSPENDED_PACKAGE = PACKAGE_NAME + ".extra.SUSPENDED_PACKAGE";
     public static final String EXTRA_SUSPENDING_PACKAGE =
             PACKAGE_NAME + ".extra.SUSPENDING_PACKAGE";
+    public static final String EXTRA_SUSPENDING_USER = PACKAGE_NAME + ".extra.SUSPENDING_USER";
     public static final String EXTRA_DIALOG_INFO = PACKAGE_NAME + ".extra.DIALOG_INFO";
     public static final String EXTRA_ACTIVITY_OPTIONS = PACKAGE_NAME + ".extra.ACTIVITY_OPTIONS";
     public static final String EXTRA_UNSUSPEND_INTENT = PACKAGE_NAME + ".extra.UNSUSPEND_INTENT";
@@ -67,6 +69,7 @@
     private IntentSender mOnUnsuspend;
     private String mSuspendedPackage;
     private String mSuspendingPackage;
+    private int mSuspendingUserId;
     private int mNeutralButtonAction;
     private int mUserId;
     private PackageManager mPm;
@@ -117,7 +120,7 @@
                 .setPackage(mSuspendingPackage);
         final String requiredPermission = Manifest.permission.SEND_SHOW_SUSPENDED_APP_DETAILS;
         final ResolveInfo resolvedInfo = mPm.resolveActivityAsUser(moreDetailsIntent,
-                MATCH_DIRECT_BOOT_UNAWARE | MATCH_DIRECT_BOOT_AWARE, mUserId);
+                MATCH_DIRECT_BOOT_UNAWARE | MATCH_DIRECT_BOOT_AWARE, mSuspendingUserId);
         if (resolvedInfo != null && resolvedInfo.activityInfo != null
                 && requiredPermission.equals(resolvedInfo.activityInfo.permission)) {
             moreDetailsIntent.putExtra(Intent.EXTRA_PACKAGE_NAME, mSuspendedPackage)
@@ -231,12 +234,17 @@
         }
         mSuspendedPackage = intent.getStringExtra(EXTRA_SUSPENDED_PACKAGE);
         mSuspendingPackage = intent.getStringExtra(EXTRA_SUSPENDING_PACKAGE);
+        if (crossUserSuspensionEnabled()) {
+            mSuspendingUserId = intent.getIntExtra(EXTRA_SUSPENDING_USER, mUserId);
+        } else {
+            mSuspendingUserId = mUserId;
+        }
         mSuppliedDialogInfo = intent.getParcelableExtra(EXTRA_DIALOG_INFO, android.content.pm.SuspendDialogInfo.class);
         mOnUnsuspend = intent.getParcelableExtra(EXTRA_UNSUSPEND_INTENT, android.content.IntentSender.class);
         if (mSuppliedDialogInfo != null) {
             try {
                 mSuspendingAppResources = createContextAsUser(
-                        UserHandle.of(mUserId), /* flags */ 0).getPackageManager()
+                        UserHandle.of(mSuspendingUserId), /* flags */ 0).getPackageManager()
                         .getResourcesForApplication(mSuspendingPackage);
             } catch (PackageManager.NameNotFoundException ne) {
                 Slog.e(TAG, "Could not find resources for " + mSuspendingPackage, ne);
@@ -299,7 +307,7 @@
                     case BUTTON_ACTION_MORE_DETAILS:
                         if (mMoreDetailsIntent != null) {
                             startActivityAsUser(mMoreDetailsIntent, mOptions,
-                                    UserHandle.of(mUserId));
+                                    UserHandle.of(mSuspendingUserId));
                         } else {
                             Slog.wtf(TAG, "Neutral button should not have existed!");
                         }
@@ -324,7 +332,7 @@
                                 .putExtra(Intent.EXTRA_PACKAGE_NAME, mSuspendedPackage)
                                 .setPackage(mSuspendingPackage)
                                 .addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
-                        sendBroadcastAsUser(reportUnsuspend, UserHandle.of(mUserId));
+                        sendBroadcastAsUser(reportUnsuspend, UserHandle.of(mSuspendingUserId));
 
                         if (mOnUnsuspend != null) {
                             Bundle activityOptions =
@@ -365,6 +373,9 @@
                 .putExtra(Intent.EXTRA_USER_ID, userId)
                 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
                         | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+        if (crossUserSuspensionEnabled()) {
+            intent.putExtra(EXTRA_SUSPENDING_USER, suspendingPackage.userId);
+        }
         return intent;
     }
 }
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/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/proto/android/service/package.proto b/core/proto/android/service/package.proto
index 068f4dd..d30f195 100644
--- a/core/proto/android/service/package.proto
+++ b/core/proto/android/service/package.proto
@@ -142,6 +142,7 @@
         // UTC timestamp of first install for the user
         optional int32 first_install_time_ms = 11;
         optional ArchiveState archive_state = 12;
+        repeated int32 suspending_user = 13;
     }
 
     message InstallSourceProto {
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/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 6c00fd8..faad472 100644
--- a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java
+++ b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java
@@ -52,6 +52,7 @@
 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;
 
@@ -135,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);
@@ -172,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);
@@ -202,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);
@@ -246,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);
@@ -284,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);
     }
@@ -312,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);
@@ -351,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;
@@ -371,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;
@@ -393,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
@@ -431,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);
@@ -446,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);
@@ -465,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);
@@ -481,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);
@@ -497,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;
@@ -553,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);
@@ -608,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.
@@ -653,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.
@@ -679,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.
@@ -702,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.
@@ -724,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.
@@ -735,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.
@@ -754,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;
@@ -974,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/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/graphics/java/android/framework_graphics.aconfig b/graphics/java/android/framework_graphics.aconfig
index 6c81a60..1e41b4d 100644
--- a/graphics/java/android/framework_graphics.aconfig
+++ b/graphics/java/android/framework_graphics.aconfig
@@ -2,6 +2,7 @@
 
 flag {
      name: "exact_compute_bounds"
+     is_exported: true
      namespace: "core_graphics"
      description: "Add a function without unused exact param for computeBounds."
      bug: "304478551"
@@ -9,6 +10,7 @@
 
 flag {
      name: "yuv_image_compress_to_ultra_hdr"
+     is_exported: true
      namespace: "core_graphics"
      description: "Feature flag for YUV image compress to Ultra HDR."
      bug: "308978825"
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/MultiInstanceHelper.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiInstanceHelper.kt
index 4c34971..9e8dfb5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiInstanceHelper.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiInstanceHelper.kt
@@ -21,11 +21,9 @@
 import android.content.pm.LauncherApps
 import android.content.pm.PackageManager
 import android.os.UserHandle
-import android.view.WindowManager
 import android.view.WindowManager.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI
 import com.android.internal.annotations.VisibleForTesting
 import com.android.wm.shell.R
-import com.android.wm.shell.protolog.ShellProtoLogGroup
 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL
 import com.android.wm.shell.util.KtProtoLog
 import java.util.Arrays
@@ -37,7 +35,8 @@
     private val context: Context,
     private val packageManager: PackageManager,
     private val staticAppsSupportingMultiInstance: Array<String> = context.resources
-            .getStringArray(R.array.config_appsSupportMultiInstancesSplit)) {
+            .getStringArray(R.array.config_appsSupportMultiInstancesSplit),
+    private val supportsMultiInstanceProperty: Boolean) {
 
     /**
      * Returns whether a specific component desires to be launched in multiple instances.
@@ -59,6 +58,11 @@
             }
         }
 
+        if (!supportsMultiInstanceProperty) {
+            // If not checking the multi-instance properties, then return early
+            return false;
+        }
+
         // Check the activity property first
         try {
             val activityProp = packageManager.getProperty(
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/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index 8d489e1..5122114 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -29,6 +29,7 @@
 
 import com.android.internal.logging.UiEventLogger;
 import com.android.launcher3.icons.IconProvider;
+import com.android.window.flags.Flags;
 import com.android.wm.shell.ProtoLogController;
 import com.android.wm.shell.R;
 import com.android.wm.shell.RootDisplayAreaOrganizer;
@@ -326,7 +327,8 @@
     @WMSingleton
     @Provides
     static MultiInstanceHelper provideMultiInstanceHelper(Context context) {
-        return new MultiInstanceHelper(context, context.getPackageManager());
+        return new MultiInstanceHelper(context, context.getPackageManager(),
+                Flags.supportsMultiInstanceSystemUi());
     }
 
     //
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/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index 6a53d33..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
@@ -495,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());
             }
@@ -506,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;
             }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiInstanceHelperTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiInstanceHelperTest.kt
index 2f5fe11..bec91e9 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiInstanceHelperTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiInstanceHelperTest.kt
@@ -32,9 +32,12 @@
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers
 import org.mockito.ArgumentMatchers.eq
+import org.mockito.kotlin.any
 import org.mockito.kotlin.doReturn
 import org.mockito.kotlin.doThrow
 import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
 
 @RunWith(AndroidJUnit4::class)
@@ -77,7 +80,7 @@
     @Test
     fun supportsMultiInstanceSplit_inStaticAllowList() {
         val allowList = arrayOf(TEST_PACKAGE)
-        val helper = MultiInstanceHelper(mContext, context.packageManager, allowList)
+        val helper = MultiInstanceHelper(mContext, context.packageManager, allowList, true)
         val component = ComponentName(TEST_PACKAGE, TEST_ACTIVITY)
         assertEquals(true, helper.supportsMultiInstanceSplit(component))
     }
@@ -85,7 +88,7 @@
     @Test
     fun supportsMultiInstanceSplit_notInStaticAllowList() {
         val allowList = arrayOf(TEST_PACKAGE)
-        val helper = MultiInstanceHelper(mContext, context.packageManager, allowList)
+        val helper = MultiInstanceHelper(mContext, context.packageManager, allowList, true)
         val component = ComponentName(TEST_NOT_ALLOWED_PACKAGE, TEST_ACTIVITY)
         assertEquals(false, helper.supportsMultiInstanceSplit(component))
     }
@@ -104,7 +107,7 @@
             eq(component.packageName)))
                 .thenReturn(appProp)
 
-        val helper = MultiInstanceHelper(mContext, pm, emptyArray())
+        val helper = MultiInstanceHelper(mContext, pm, emptyArray(), true)
         // Expect activity property to override application property
         assertEquals(true, helper.supportsMultiInstanceSplit(component))
     }
@@ -123,7 +126,7 @@
             eq(component.packageName)))
                 .thenReturn(appProp)
 
-        val helper = MultiInstanceHelper(mContext, pm, emptyArray())
+        val helper = MultiInstanceHelper(mContext, pm, emptyArray(), true)
         // Expect activity property to override application property
         assertEquals(false, helper.supportsMultiInstanceSplit(component))
     }
@@ -141,7 +144,7 @@
             eq(component.packageName)))
                 .thenReturn(appProp)
 
-        val helper = MultiInstanceHelper(mContext, pm, emptyArray())
+        val helper = MultiInstanceHelper(mContext, pm, emptyArray(), true)
         // Expect fall through to app property
         assertEquals(true, helper.supportsMultiInstanceSplit(component))
     }
@@ -158,10 +161,30 @@
             eq(component.packageName)))
                 .thenThrow(PackageManager.NameNotFoundException())
 
-        val helper = MultiInstanceHelper(mContext, pm, emptyArray())
+        val helper = MultiInstanceHelper(mContext, pm, emptyArray(), true)
         assertEquals(false, helper.supportsMultiInstanceSplit(component))
     }
 
+    @Test
+    @Throws(PackageManager.NameNotFoundException::class)
+    fun checkNoMultiInstancePropertyFlag_ignoreProperty() {
+        val component = ComponentName(TEST_PACKAGE, TEST_ACTIVITY)
+        val pm = mock<PackageManager>()
+        val activityProp = PackageManager.Property("", true, "", "")
+        whenever(pm.getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI),
+            eq(component)))
+            .thenReturn(activityProp)
+        val appProp = PackageManager.Property("", true, "", "")
+        whenever(pm.getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI),
+            eq(component.packageName)))
+            .thenReturn(appProp)
+
+        val helper = MultiInstanceHelper(mContext, pm, emptyArray(), false)
+        // Expect we only check the static list and not the property
+        assertEquals(false, helper.supportsMultiInstanceSplit(component))
+        verify(pm, never()).getProperty(any(), any<ComponentName>())
+    }
+
     companion object {
         val TEST_PACKAGE = "com.android.wm.shell.common"
         val TEST_NOT_ALLOWED_PACKAGE = "com.android.wm.shell.common.fake";
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/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig
index 76a0a64..659bcdc 100644
--- a/libs/hwui/aconfig/hwui_flags.aconfig
+++ b/libs/hwui/aconfig/hwui_flags.aconfig
@@ -2,6 +2,7 @@
 
 flag {
   name: "clip_shader"
+  is_exported: true
   namespace: "core_graphics"
   description: "API for canvas shader clipping operations"
   bug: "280116960"
@@ -9,6 +10,7 @@
 
 flag {
   name: "matrix_44"
+  is_exported: true
   namespace: "core_graphics"
   description: "API for 4x4 matrix and related canvas functions"
   bug: "280116960"
@@ -16,6 +18,7 @@
 
 flag {
   name: "limited_hdr"
+  is_exported: true
   namespace: "core_graphics"
   description: "API to enable apps to restrict the amount of HDR headroom that is used"
   bug: "234181960"
@@ -44,6 +47,7 @@
 
 flag {
   name: "gainmap_animations"
+  is_exported: true
   namespace: "core_graphics"
   description: "APIs to help enable animations involving gainmaps"
   bug: "296482289"
@@ -51,6 +55,7 @@
 
 flag {
   name: "gainmap_constructor_with_metadata"
+  is_exported: true
   namespace: "core_graphics"
   description: "APIs to create a new gainmap with a bitmap for metadata."
   bug: "304478551"
@@ -65,6 +70,7 @@
 
 flag {
   name: "requested_formats_v"
+  is_exported: true
   namespace: "core_graphics"
   description: "Enable r_8, r_16_uint, rg_1616_uint, and rgba_10101010 in the SDK"
   bug: "292545615"
diff --git a/location/java/android/location/flags/location.aconfig b/location/java/android/location/flags/location.aconfig
index f33bcb7..ce689ac 100644
--- a/location/java/android/location/flags/location.aconfig
+++ b/location/java/android/location/flags/location.aconfig
@@ -9,6 +9,7 @@
 
 flag {
     name: "location_bypass"
+    is_exported: true
     namespace: "location"
     description: "Enable location bypass appops behavior"
     bug: "329151785"
diff --git a/media/java/android/media/flags/editing.aconfig b/media/java/android/media/flags/editing.aconfig
index c3997e9..5bf1b4e 100644
--- a/media/java/android/media/flags/editing.aconfig
+++ b/media/java/android/media/flags/editing.aconfig
@@ -2,6 +2,7 @@
 
 flag {
   name: "add_media_metrics_editing"
+  is_exported: true
   namespace: "media_solutions"
   description: "Add media metrics for transcoding/editing events."
   bug: "297487694"
diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig
index bf39425..40929f7 100644
--- a/media/java/android/media/flags/media_better_together.aconfig
+++ b/media/java/android/media/flags/media_better_together.aconfig
@@ -2,6 +2,7 @@
 
 flag {
     name: "enable_rlp_callbacks_in_media_router2"
+    is_exported: true
     namespace: "media_solutions"
     description: "Make RouteListingPreference getter and callbacks public in MediaRouter2."
     bug: "281067101"
@@ -16,6 +17,7 @@
 
 flag {
     name: "enable_audio_policies_device_and_bluetooth_controller"
+    is_exported: true
     namespace: "media_solutions"
     description: "Use Audio Policies implementation for device and Bluetooth route controllers."
     bug: "280576228"
@@ -44,6 +46,7 @@
 
 flag {
     name: "enable_new_media_route_2_info_types"
+    is_exported: true
     namespace: "media_solutions"
     description: "Enables the following type constants in MediaRoute2Info: CAR, COMPUTER, GAME_CONSOLE, SMARTPHONE, SMARTWATCH, TABLET, TABLET_DOCKED. Note that this doesn't gate any behavior. It only guards some API int symbols."
     bug: "301713440"
@@ -51,6 +54,7 @@
 
 flag {
     name: "enable_privileged_routing_for_media_routing_control"
+    is_exported: true
     namespace: "media_solutions"
     description: "Allow access to privileged routing capabilities to MEDIA_ROUTING_CONTROL holders."
     bug: "305919655"
@@ -58,6 +62,7 @@
 
 flag {
     name: "enable_cross_user_routing_in_media_router2"
+    is_exported: true
     namespace: "media_solutions"
     description: "Allows clients of privileged MediaRouter2 that hold INTERACT_ACROSS_USERS_FULL to control routing across users."
     bug: "288580225"
@@ -72,6 +77,7 @@
 
 flag {
      name: "enable_built_in_speaker_route_suitability_statuses"
+     is_exported: true
      namespace: "media_solutions"
      description: "Make MediaRoute2Info provide information about routes suitability for transfer."
      bug: "279555229"
@@ -79,6 +85,7 @@
 
 flag {
     name: "enable_notifying_activity_manager_with_media_session_status_change"
+    is_exported: true
     namespace: "media_solutions"
     description: "Notify ActivityManager with the changes in playback state of the media session."
     bug: "295518668"
@@ -86,6 +93,7 @@
 
 flag {
     name: "enable_get_transferable_routes"
+    is_exported: true
     namespace: "media_solutions"
     description: "Exposes RoutingController#getTransferableRoutes() (previously hidden) to the public API."
     bug: "323154573"
@@ -100,6 +108,7 @@
 
 flag {
     name: "enable_screen_off_scanning"
+    is_exported: true
     namespace: "media_solutions"
     description: "Enable new MediaRouter2 API to enable watch companion apps to scan while the phone screen is off."
     bug: "281072508"
diff --git a/media/java/android/media/tv/flags/media_tv.aconfig b/media/java/android/media/tv/flags/media_tv.aconfig
index f110705..1731e5e 100644
--- a/media/java/android/media/tv/flags/media_tv.aconfig
+++ b/media/java/android/media/tv/flags/media_tv.aconfig
@@ -2,6 +2,7 @@
 
 flag {
     name: "broadcast_visibility_types"
+    is_exported: true
     namespace: "media_tv"
     description: "Constants for standardizing broadcast visibility types."
     bug: "222402395"
@@ -9,6 +10,7 @@
 
 flag {
     name: "enable_ad_service_fw"
+    is_exported: true
     namespace: "media_tv"
     description: "Enable the TV client-side AD framework."
     bug: "303506816"
@@ -16,6 +18,7 @@
 
 flag {
     name: "tiaf_v_apis"
+    is_exported: true
     namespace: "media_tv"
     description: "TIAF V3.0 APIs for Android V"
     bug: "303323657"
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 d178abc..a84ec73 100644
--- a/native/android/surface_control_input_receiver.cpp
+++ b/native/android/surface_control_input_receiver.cpp
@@ -192,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/java/android/nfc/flags.aconfig b/nfc/java/android/nfc/flags.aconfig
index ba084c0..6d4a17c 100644
--- a/nfc/java/android/nfc/flags.aconfig
+++ b/nfc/java/android/nfc/flags.aconfig
@@ -63,6 +63,7 @@
 
 flag {
     name: "enable_nfc_charging"
+    is_exported: true
     namespace: "nfc"
     description: "Flag for NFC charging changes"
     bug: "292143899"
diff --git a/packages/CrashRecovery/aconfig/flags.aconfig b/packages/CrashRecovery/aconfig/flags.aconfig
index 572a669..8627eac 100644
--- a/packages/CrashRecovery/aconfig/flags.aconfig
+++ b/packages/CrashRecovery/aconfig/flags.aconfig
@@ -10,6 +10,7 @@
 
 flag {
     name: "enable_crashrecovery"
+    is_exported: true
     namespace: "crashrecovery"
     description: "Enables various dependencies of crashrecovery module"
     bug: "289203818"
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_english_uk.kcm b/packages/InputDevices/res/raw/keyboard_layout_english_uk.kcm
index 071f9f4..854c2fd 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_english_uk.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_english_uk.kcm
@@ -23,8 +23,8 @@
 ### ROW 1
 
 key GRAVE {
-    label:                              '`'
-    base:                               '`'
+    label:                              '\u0300'
+    base:                               '\u0300'
     shift:                              '\u00AC'
     ralt:                               '\u00A6'
 }
@@ -39,6 +39,7 @@
     label:                              '2'
     base:                               '2'
     shift:                              '"'
+    ralt:                               '\u0308'
 }
 
 key 3 {
@@ -64,6 +65,7 @@
     label:                              '6'
     base:                               '6'
     shift:                              '^'
+    ralt:                               '\u0302'
 }
 
 key 7 {
@@ -202,6 +204,7 @@
     label:                              ']'
     base:                               ']'
     shift:                              '}'
+    shift+ralt:                         '|'
 }
 
 ### ROW 3
@@ -282,14 +285,16 @@
     label:                              '\''
     base:                               '\''
     shift:                              '@'
+    ralt:                               '\u0301'
+    shift+ralt:                         '`'
 }
 
 key POUND {
     label:                              '#'
     base:                               '#'
     shift:                              '~'
-    ralt:                               '\\'
-    shift+ralt:                         '|'
+    ralt:                               '\u0303'
+    shift+ralt:                         '\\'
 }
 
 ### ROW 4
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..7c313e8 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,6 +76,7 @@
             return;
         }
         mPackageManager = getPackageManager();
+        mPackageInstaller = mPackageManager.getPackageInstaller();
         mUserManager = getSystemService(UserManager.class);
 
         Intent intent = getIntent();
@@ -94,12 +96,11 @@
         // If the activity was started via a PackageInstaller session, we retrieve the calling
         // package from that session
         final int sessionId = (isSessionInstall
-                ? intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1)
-                : -1);
+                ? intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, SessionInfo.INVALID_ID)
+                : SessionInfo.INVALID_ID);
         int originatingUidFromSession = callingUid;
-        if (callingPackage == null && sessionId != -1) {
-            PackageInstaller packageInstaller = getPackageManager().getPackageInstaller();
-            PackageInstaller.SessionInfo sessionInfo = packageInstaller.getSessionInfo(sessionId);
+        if (callingPackage == null && sessionId != SessionInfo.INVALID_ID) {
+            PackageInstaller.SessionInfo sessionInfo = mPackageInstaller.getSessionInfo(sessionId);
             if (sessionInfo != null) {
                 callingPackage = sessionInfo.getInstallerPackageName();
                 callingAttributionTag = sessionInfo.getInstallerAttributionTag();
@@ -188,6 +189,7 @@
         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));
@@ -295,8 +295,7 @@
         if (originatingUid == 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;
         }
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
index 45bfe54..1b93c10 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
@@ -86,6 +86,7 @@
     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";
 
@@ -304,21 +305,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
@@ -557,7 +543,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/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/bluetooth/BluetoothCallback.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothCallback.java
index 416b369..baccda7e 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothCallback.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothCallback.java
@@ -163,6 +163,16 @@
     default void onAclConnectionStateChanged(
             @NonNull CachedBluetoothDevice cachedDevice, int state) {}
 
+    /**
+     * Called when the Auto-on state is changed for any user. Listens to intent
+     * {@link android.bluetooth.BluetoothAdapter#ACTION_AUTO_ON_STATE_CHANGED }
+     *
+     * @param state        the Auto-on state, the possible values are:
+     *                     {@link android.bluetooth.BluetoothAdapter#AUTO_ON_STATE_ENABLED},
+     *                     {@link android.bluetooth.BluetoothAdapter#AUTO_ON_STATE_DISABLED}
+     */
+    default void onAutoOnStateChanged(int state) {}
+
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(prefix = { "STATE_" }, value = {
             STATE_DISCONNECTED,
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
index 647fcb9..0996d52 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
@@ -133,6 +133,8 @@
         addHandler(BluetoothDevice.ACTION_ACL_CONNECTED, new AclStateChangedHandler());
         addHandler(BluetoothDevice.ACTION_ACL_DISCONNECTED, new AclStateChangedHandler());
 
+        addHandler(BluetoothAdapter.ACTION_AUTO_ON_STATE_CHANGED, new AutoOnStateChangedHandler());
+
         registerAdapterIntentReceiver();
     }
 
@@ -552,4 +554,21 @@
             dispatchAudioModeChanged();
         }
     }
+
+    private class AutoOnStateChangedHandler implements Handler {
+
+        @Override
+        public void onReceive(Context context, Intent intent, BluetoothDevice device) {
+            String action = intent.getAction();
+            if (action == null) {
+                Log.w(TAG, "AutoOnStateChangedHandler() action is null");
+                return;
+            }
+            int state = intent.getIntExtra(BluetoothAdapter.EXTRA_AUTO_ON_STATE,
+                    BluetoothAdapter.ERROR);
+            for (BluetoothCallback callback : mCallbacks) {
+                callback.onAutoOnStateChanged(state);
+            }
+        }
+    }
 }
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/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/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/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java
index 13635c3..48bbf4e 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java
@@ -18,6 +18,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -489,4 +490,17 @@
         verify(mErrorListener).onShowError(any(Context.class), eq(DEVICE_NAME),
                 eq(R.string.bluetooth_pairing_pin_error_message));
     }
+
+    /**
+     * Intent ACTION_AUTO_ON_STATE_CHANGED should dispatch to callback.
+     */
+    @Test
+    public void intentWithExtraState_autoOnStateChangedShouldDispatchToRegisterCallback() {
+        mBluetoothEventManager.registerCallback(mBluetoothCallback);
+        mIntent = new Intent(BluetoothAdapter.ACTION_AUTO_ON_STATE_CHANGED);
+
+        mContext.sendBroadcast(mIntent);
+
+        verify(mBluetoothCallback).onAutoOnStateChanged(anyInt());
+    }
 }
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/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index a155dc4..f057acc 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -25,6 +25,16 @@
 }
 
 flag {
+   name: "notification_minimalism_prototype"
+   namespace: "systemui"
+   description: "Prototype of notification minimalism; the new 'Intermediate' lockscreen customization proposal."
+   bug: "330387368"
+   metadata {
+        purpose: PURPOSE_BUGFIX
+   }
+}
+
+flag {
    name: "notification_view_flipper_pausing"
    namespace: "systemui"
    description: "Pause ViewFlippers inside Notification custom layouts when the shade is closed."
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/PlatformSlider.kt b/packages/SystemUI/compose/core/src/com/android/compose/PlatformSlider.kt
index 596a297..4a89e31 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/PlatformSlider.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/PlatformSlider.kt
@@ -18,6 +18,7 @@
 
 package com.android.compose
 
+import androidx.compose.animation.animateColorAsState
 import androidx.compose.animation.core.animateDpAsState
 import androidx.compose.animation.core.animateFloatAsState
 import androidx.compose.foundation.Canvas
@@ -266,8 +267,17 @@
             label = "PlatformSliderCornersAnimation",
         )
 
-    val trackColor = colors.getTrackColor(enabled)
-    val indicatorColor = colors.getIndicatorColor(enabled)
+    val trackColor by
+        animateColorAsState(
+            colors.getTrackColor(enabled),
+            label = "PlatformSliderTrackColorAnimation",
+        )
+
+    val indicatorColor by
+        animateColorAsState(
+            colors.getIndicatorColor(enabled),
+            label = "PlatformSliderIndicatorColorAnimation",
+        )
     Canvas(modifier.fillMaxSize()) {
         val trackCornerRadius = CornerRadius(size.height / 2, size.height / 2)
         val trackPath = Path()
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 1da6c1e..0f3d3dc2 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
@@ -484,6 +484,7 @@
         onChangeScene = {},
         transitions = SceneTransitions,
         modifier = modifier,
+        enableInterruptions = false,
     ) {
         scene(SceneKeys.ContiguousSceneKey) {
             FoldableScene(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
index d0c4984..a1d8c29 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
@@ -71,6 +71,7 @@
             currentScene,
             onChangeScene = { viewModel.onSceneChanged(it) },
             transitions = sceneTransitions,
+            enableInterruptions = false,
         )
     val touchesAllowed by viewModel.touchesAllowed.collectAsState(initial = false)
 
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt
index bc4e555..1178cc8 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt
@@ -74,6 +74,7 @@
             transitions =
                 transitions { sceneKeyByBlueprintId.values.forEach { sceneKey -> to(sceneKey) } },
             modifier = modifier,
+            enableInterruptions = false,
         ) {
             sceneKeyByBlueprint.entries.forEach { (blueprint, sceneKey) ->
                 scene(sceneKey) { with(blueprint) { Content(Modifier.fillMaxSize()) } }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ClockTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ClockTransition.kt
index d9ed497..a12f099 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ClockTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ClockTransition.kt
@@ -80,5 +80,14 @@
 object ClockElementKeys {
     val largeClockElementKey = ElementKey("large-clock")
     val smallClockElementKey = ElementKey("small-clock")
+    val weatherSmallClockElementKey = ElementKey("weather-small-clock")
     val smartspaceElementKey = ElementKey("smart-space")
 }
+
+object WeatherClockElementKeys {
+    val timeElementKey = ElementKey("weather-large-clock-time")
+    val dateElementKey = ElementKey("weather-large-clock-date")
+    val weatherIconElementKey = ElementKey("weather-large-clock-weather-icon")
+    val temperatureElementKey = ElementKey("weather-large-clock-temperature")
+    val dndAlarmElementKey = ElementKey("weather-large-clock-dnd-alarm")
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/WeatherClockBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/WeatherClockBlueprint.kt
index ee4e2d6..fe774a0 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/WeatherClockBlueprint.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/WeatherClockBlueprint.kt
@@ -23,6 +23,8 @@
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.Layout
@@ -33,11 +35,14 @@
 import androidx.compose.ui.unit.dp
 import com.android.compose.animation.scene.SceneScope
 import com.android.compose.modifiers.padding
+import com.android.keyguard.KeyguardClockSwitch.LARGE
 import com.android.systemui.Flags
+import com.android.systemui.customization.R as customizationR
 import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor.Companion.SPLIT_SHADE_WEATHER_CLOCK_BLUEPRINT_ID
 import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor.Companion.WEATHER_CLOCK_BLUEPRINT_ID
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.keyguard.ui.composable.LockscreenLongPress
+import com.android.systemui.keyguard.ui.composable.modifier.onTopPlacementChanged
 import com.android.systemui.keyguard.ui.composable.section.AmbientIndicationSection
 import com.android.systemui.keyguard.ui.composable.section.BottomAreaSection
 import com.android.systemui.keyguard.ui.composable.section.LockSection
@@ -47,8 +52,8 @@
 import com.android.systemui.keyguard.ui.composable.section.SmartSpaceSection
 import com.android.systemui.keyguard.ui.composable.section.StatusBarSection
 import com.android.systemui.keyguard.ui.composable.section.WeatherClockSection
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
 import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
-import com.android.systemui.media.controls.ui.composable.MediaCarousel
 import com.android.systemui.res.R
 import com.android.systemui.shade.LargeScreenHeaderHelper
 import dagger.Binds
@@ -71,6 +76,7 @@
     private val settingsMenuSection: SettingsMenuSection,
     private val clockInteractor: KeyguardClockInteractor,
     private val mediaCarouselSection: MediaCarouselSection,
+    private val clockViewModel: KeyguardClockViewModel,
 ) : ComposableLockscreenSceneBlueprint {
 
     override val id: String = WEATHER_CLOCK_BLUEPRINT_ID
@@ -79,7 +85,7 @@
         val isUdfpsVisible = viewModel.isUdfpsVisible
         val burnIn = rememberBurnIn(clockInteractor)
         val resources = LocalContext.current.resources
-
+        val currentClockState = clockViewModel.currentClock.collectAsState()
         LockscreenLongPress(
             viewModel = viewModel.longPress,
             modifier = modifier,
@@ -91,7 +97,34 @@
                         modifier = Modifier.fillMaxWidth(),
                     ) {
                         with(statusBarSection) { StatusBar(modifier = Modifier.fillMaxWidth()) }
-                        // TODO: Add weather clock for small and large clock
+                        val currentClock = currentClockState.value
+                        val clockSize by clockViewModel.clockSize.collectAsState()
+                        with(weatherClockSection) {
+                            if (currentClock == null) {
+                                return@with
+                            }
+
+                            if (clockSize == LARGE) {
+                                Time(
+                                    clock = currentClock,
+                                    modifier =
+                                        Modifier.padding(
+                                            start =
+                                                dimensionResource(
+                                                    customizationR.dimen.clock_padding_start
+                                                )
+                                        )
+                                )
+                            } else {
+                                SmallClock(
+                                    burnInParams = burnIn.parameters,
+                                    modifier =
+                                        Modifier.align(Alignment.Start)
+                                            .onTopPlacementChanged(burnIn.onSmallClockTopChanged),
+                                    clock = currentClock
+                                )
+                            }
+                        }
                         with(smartSpaceSection) {
                             SmartSpace(
                                 burnInParams = burnIn.parameters,
@@ -119,6 +152,12 @@
                                 )
                             }
                         }
+                        with(weatherClockSection) {
+                            if (currentClock == null || clockSize != LARGE) {
+                                return@with
+                            }
+                            LargeClockSectionBelowSmartspace(clock = currentClock)
+                        }
 
                         if (!isUdfpsVisible && ambientIndicationSectionOptional.isPresent) {
                             with(ambientIndicationSectionOptional.get()) {
@@ -234,6 +273,7 @@
     private val largeScreenHeaderHelper: LargeScreenHeaderHelper,
     private val weatherClockSection: WeatherClockSection,
     private val mediaCarouselSection: MediaCarouselSection,
+    private val clockViewModel: KeyguardClockViewModel,
 ) : ComposableLockscreenSceneBlueprint {
     override val id: String = SPLIT_SHADE_WEATHER_CLOCK_BLUEPRINT_ID
 
@@ -242,7 +282,7 @@
         val isUdfpsVisible = viewModel.isUdfpsVisible
         val burnIn = rememberBurnIn(clockInteractor)
         val resources = LocalContext.current.resources
-
+        val currentClockState = clockViewModel.currentClock.collectAsState()
         LockscreenLongPress(
             viewModel = viewModel.longPress,
             modifier = modifier,
@@ -257,11 +297,42 @@
                         Row(
                             modifier = Modifier.fillMaxSize(),
                         ) {
-                            // TODO: Add weather clock for small and large clock
                             Column(
                                 modifier = Modifier.fillMaxHeight().weight(weight = 1f),
                                 horizontalAlignment = Alignment.CenterHorizontally,
                             ) {
+                                val currentClock = currentClockState.value
+                                val clockSize by clockViewModel.clockSize.collectAsState()
+                                with(weatherClockSection) {
+                                    if (currentClock == null) {
+                                        return@with
+                                    }
+
+                                    if (clockSize == LARGE) {
+                                        Time(
+                                            clock = currentClock,
+                                            modifier =
+                                                Modifier.align(Alignment.Start)
+                                                    .padding(
+                                                        start =
+                                                            dimensionResource(
+                                                                customizationR.dimen
+                                                                    .clock_padding_start
+                                                            )
+                                                    )
+                                        )
+                                    } else {
+                                        SmallClock(
+                                            burnInParams = burnIn.parameters,
+                                            modifier =
+                                                Modifier.align(Alignment.Start)
+                                                    .onTopPlacementChanged(
+                                                        burnIn.onSmallClockTopChanged
+                                                    ),
+                                            clock = currentClock,
+                                        )
+                                    }
+                                }
                                 with(smartSpaceSection) {
                                     SmartSpace(
                                         burnInParams = burnIn.parameters,
@@ -284,6 +355,14 @@
                                 }
 
                                 with(mediaCarouselSection) { MediaCarousel() }
+
+                                with(weatherClockSection) {
+                                    if (currentClock == null || clockSize != LARGE) {
+                                        return@with
+                                    }
+
+                                    LargeClockSectionBelowSmartspace(currentClock)
+                                }
                             }
                             with(notificationSection) {
                                 val splitShadeTopMargin: Dp =
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 1d86b15..2781f39 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
@@ -20,6 +20,7 @@
 import android.view.ViewGroup
 import android.widget.FrameLayout
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.collectAsState
@@ -32,6 +33,7 @@
 import com.android.compose.animation.scene.SceneScope
 import com.android.compose.modifiers.padding
 import com.android.systemui.customization.R as customizationR
+import com.android.systemui.customization.R
 import com.android.systemui.keyguard.ui.composable.blueprint.ClockElementKeys.largeClockElementKey
 import com.android.systemui.keyguard.ui.composable.blueprint.ClockElementKeys.smallClockElementKey
 import com.android.systemui.keyguard.ui.composable.modifier.burnInAware
@@ -58,19 +60,21 @@
         if (currentClock?.smallClock?.view == null) {
             return
         }
-
         val context = LocalContext.current
         MovableElement(key = smallClockElementKey, modifier = modifier) {
             content {
                 AndroidView(
                     factory = { context ->
                         FrameLayout(context).apply {
-                            addClockView(checkNotNull(currentClock).smallClock.view)
+                            ensureClockViewExists(checkNotNull(currentClock).smallClock.view)
                         }
                     },
-                    update = { it.addClockView(checkNotNull(currentClock).smallClock.view) },
+                    update = {
+                        it.ensureClockViewExists(checkNotNull(currentClock).smallClock.view)
+                    },
                     modifier =
-                        Modifier.padding(
+                        Modifier.height(dimensionResource(R.dimen.small_clock_height))
+                            .padding(
                                 horizontal =
                                     dimensionResource(customizationR.dimen.clock_padding_start)
                             )
@@ -91,23 +95,24 @@
         if (currentClock?.largeClock?.view == null) {
             return
         }
-
         MovableElement(key = largeClockElementKey, modifier = modifier) {
             content {
                 AndroidView(
                     factory = { context ->
                         FrameLayout(context).apply {
-                            addClockView(checkNotNull(currentClock).largeClock.view)
+                            ensureClockViewExists(checkNotNull(currentClock).largeClock.view)
                         }
                     },
-                    update = { it.addClockView(checkNotNull(currentClock).largeClock.view) },
+                    update = {
+                        it.ensureClockViewExists(checkNotNull(currentClock).largeClock.view)
+                    },
                     modifier = Modifier.fillMaxSize()
                 )
             }
         }
     }
 
-    private fun FrameLayout.addClockView(clockView: View) {
+    private fun FrameLayout.ensureClockViewExists(clockView: View) {
         if (contains(clockView)) {
             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 525ad16..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
@@ -20,13 +20,11 @@
 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.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.NotificationStackScrollLayout
 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
-import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackViewBinder
 import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
@@ -37,17 +35,15 @@
 @Inject
 constructor(
     private val viewModel: NotificationsPlaceholderViewModel,
-    sceneContainerFlags: SceneContainerFlags,
     sharedNotificationContainer: SharedNotificationContainer,
     sharedNotificationContainerViewModel: SharedNotificationContainerViewModel,
     stackScrollLayout: NotificationStackScrollLayout,
     sharedNotificationContainerBinder: SharedNotificationContainerBinder,
-    notificationStackViewBinder: NotificationStackViewBinder,
 ) {
 
     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
@@ -65,10 +61,6 @@
             sharedNotificationContainer,
             sharedNotificationContainerViewModel,
         )
-
-        if (sceneContainerFlags.isEnabled()) {
-            notificationStackViewBinder.bindWhileAttached()
-        }
     }
 
     @Composable
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt
index 7635841..d72d5ca 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt
@@ -92,6 +92,7 @@
             currentScene = currentScene,
             onChangeScene = {},
             transitions = ClockTransition.defaultClockTransitions,
+            enableInterruptions = false,
         ) {
             scene(ClockScenes.splitShadeLargeClockScene) {
                 Row(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/WeatherClockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/WeatherClockSection.kt
index 2e7bc2a..d358453 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/WeatherClockSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/WeatherClockSection.kt
@@ -16,45 +16,177 @@
 
 package com.android.systemui.keyguard.ui.composable.section
 
+import android.view.ViewGroup
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.viewinterop.AndroidView
+import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.SceneScope
+import com.android.compose.modifiers.padding
+import com.android.systemui.customization.R
+import com.android.systemui.keyguard.ui.composable.blueprint.ClockElementKeys.weatherSmallClockElementKey
+import com.android.systemui.keyguard.ui.composable.blueprint.WeatherClockElementKeys
+import com.android.systemui.keyguard.ui.composable.modifier.burnInAware
+import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel
+import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
+import com.android.systemui.plugins.clocks.ClockController
 import javax.inject.Inject
 
 /** Provides small clock and large clock composables for the weather clock layout. */
-class WeatherClockSection @Inject constructor() {
+class WeatherClockSection
+@Inject
+constructor(
+    private val viewModel: KeyguardClockViewModel,
+    private val aodBurnInViewModel: AodBurnInViewModel,
+) {
     @Composable
     fun SceneScope.Time(
+        clock: ClockController,
         modifier: Modifier = Modifier,
     ) {
-        // TODO: compose view
+        WeatherElement(
+            weatherClockElementViewId = R.id.weather_clock_time,
+            clock = clock,
+            elementKey = WeatherClockElementKeys.timeElementKey,
+            modifier = modifier.wrapContentSize(),
+        )
     }
 
     @Composable
-    fun SceneScope.Date(
+    private fun SceneScope.Date(
+        clock: ClockController,
         modifier: Modifier = Modifier,
     ) {
-        // TODO: compose view
+        WeatherElement(
+            weatherClockElementViewId = R.id.weather_clock_date,
+            clock = clock,
+            elementKey = WeatherClockElementKeys.dateElementKey,
+            modifier = modifier,
+        )
     }
 
     @Composable
-    fun SceneScope.Weather(
+    private fun SceneScope.Weather(
+        clock: ClockController,
         modifier: Modifier = Modifier,
     ) {
-        // TODO: compose view
+        WeatherElement(
+            weatherClockElementViewId = R.id.weather_clock_weather_icon,
+            clock = clock,
+            elementKey = WeatherClockElementKeys.weatherIconElementKey,
+            modifier = modifier.wrapContentSize(),
+        )
     }
 
     @Composable
-    fun SceneScope.DndAlarmStatus(
+    private fun SceneScope.DndAlarmStatus(
+        clock: ClockController,
         modifier: Modifier = Modifier,
     ) {
-        // TODO: compose view
+        WeatherElement(
+            weatherClockElementViewId = R.id.weather_clock_alarm_dnd,
+            clock = clock,
+            elementKey = WeatherClockElementKeys.dndAlarmElementKey,
+            modifier = modifier.wrapContentSize(),
+        )
     }
 
     @Composable
-    fun SceneScope.Temperature(
+    private fun SceneScope.Temperature(
+        clock: ClockController,
         modifier: Modifier = Modifier,
     ) {
-        // TODO: compose view
+        WeatherElement(
+            weatherClockElementViewId = R.id.weather_clock_temperature,
+            clock = clock,
+            elementKey = WeatherClockElementKeys.temperatureElementKey,
+            modifier = modifier.wrapContentSize(),
+        )
+    }
+
+    @Composable
+    private fun SceneScope.WeatherElement(
+        weatherClockElementViewId: Int,
+        clock: ClockController,
+        elementKey: ElementKey,
+        modifier: Modifier
+    ) {
+        MovableElement(key = elementKey, modifier) {
+            content {
+                AndroidView(
+                    factory = {
+                        val view =
+                            clock.largeClock.layout.views.first {
+                                it.id == weatherClockElementViewId
+                            }
+                        (view.parent as? ViewGroup)?.removeView(view)
+                        view
+                    },
+                    update = {},
+                    modifier = modifier
+                )
+            }
+        }
+    }
+
+    @Composable
+    fun SceneScope.LargeClockSectionBelowSmartspace(
+        clock: ClockController,
+    ) {
+        Row(
+            modifier =
+                Modifier.height(IntrinsicSize.Max)
+                    .padding(horizontal = dimensionResource(R.dimen.clock_padding_start))
+        ) {
+            Date(clock = clock, modifier = Modifier.wrapContentSize())
+            Box(modifier = Modifier.fillMaxSize()) {
+                Weather(clock = clock, modifier = Modifier.align(Alignment.TopStart))
+                Temperature(clock = clock, modifier = Modifier.align(Alignment.BottomEnd))
+                DndAlarmStatus(clock = clock, modifier = Modifier.align(Alignment.TopEnd))
+            }
+        }
+    }
+
+    @Composable
+    fun SceneScope.SmallClock(
+        burnInParams: BurnInParameters,
+        modifier: Modifier = Modifier,
+        clock: ClockController,
+    ) {
+        val localContext = LocalContext.current
+        MovableElement(key = weatherSmallClockElementKey, modifier) {
+            content {
+                AndroidView(
+                    factory = {
+                        val view = clock.smallClock.view
+                        if (view.parent != null) {
+                            (view.parent as? ViewGroup)?.removeView(view)
+                        }
+                        view
+                    },
+                    modifier =
+                        modifier
+                            .height(dimensionResource(R.dimen.small_clock_height))
+                            .padding(start = dimensionResource(R.dimen.clock_padding_start))
+                            .padding(top = { viewModel.getSmallClockTopMargin(localContext) })
+                            .burnInAware(
+                                viewModel = aodBurnInViewModel,
+                                params = burnInParams,
+                            ),
+                    update = {},
+                )
+            }
+        }
     }
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
index 0fdaabe..fe6701c 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
@@ -79,6 +79,7 @@
             initialScene = currentSceneKey,
             canChangeScene = { toScene -> viewModel.canChangeScene(toScene) },
             transitions = SceneContainerTransitions,
+            enableInterruptions = false,
         )
     }
 
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..d31064a 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
@@ -17,16 +17,20 @@
 package com.android.systemui.volume.panel.component.volume.ui.composable
 
 import androidx.compose.animation.core.animateFloatAsState
-import androidx.compose.foundation.basicMarquee
-import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.size
 import androidx.compose.material3.IconButton
 import androidx.compose.material3.IconButtonColors
 import androidx.compose.material3.LocalContentColor
-import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
 import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.semantics.ProgressBarRangeInfo
@@ -38,6 +42,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
 
@@ -49,16 +54,15 @@
     modifier: Modifier = Modifier,
     sliderColors: PlatformSliderColors,
 ) {
-    val value by
-        animateFloatAsState(targetValue = state.value, label = "VolumeSliderValueAnimation")
+    val value by valueState(state)
     PlatformSlider(
         modifier =
             modifier.clearAndSetSemantics {
                 if (!state.isEnabled) disabled()
                 contentDescription = state.label
 
-                // provide a not animated value to the a11y because it fails to announce the settled
-                // value when it changes rapidly.
+                // provide a not animated value to the a11y because it fails to announce the
+                // settled value when it changes rapidly.
                 progressBarRangeInfo = ProgressBarRangeInfo(state.value, state.valueRange)
                 setProgress { targetValue ->
                     val targetDirection =
@@ -86,44 +90,64 @@
                 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,
+                    )
                 }
             }
         },
         colors = sliderColors,
         label = {
-            Column(modifier = Modifier) {
-                Text(
-                    modifier = Modifier.basicMarquee(),
-                    text = state.label,
-                    style = MaterialTheme.typography.titleMedium,
-                    color = LocalContentColor.current,
-                    maxLines = 1,
-                )
-
-                if (!state.isEnabled) {
-                    state.disabledMessage?.let { message ->
-                        Text(
-                            modifier = Modifier.basicMarquee(),
-                            text = message,
-                            style = MaterialTheme.typography.bodySmall,
-                            color = LocalContentColor.current,
-                            maxLines = 1,
-                        )
-                    }
-                }
-            }
+            VolumeSliderContent(
+                modifier = Modifier,
+                label = state.label,
+                isEnabled = state.isEnabled,
+                disabledMessage = state.disabledMessage,
+            )
         }
     )
 }
+
+@Composable
+private fun valueState(state: SliderState): State<Float> {
+    var prevState by remember { mutableStateOf(state) }
+    // Don't animate slider value when receive the first value and when changing isEnabled state
+    val shouldSkipAnimation =
+        prevState is SliderState.Empty || prevState.isEnabled != state.isEnabled
+    val value =
+        if (shouldSkipAnimation) mutableFloatStateOf(state.value)
+        else animateFloatAsState(targetValue = state.value, label = "VolumeSliderValueAnimation")
+    prevState = state
+    return value
+}
+
+@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/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSliderContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSliderContent.kt
new file mode 100644
index 0000000..6b9af23
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSliderContent.kt
@@ -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.systemui.volume.panel.component.volume.ui.composable
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.shrinkVertically
+import androidx.compose.foundation.basicMarquee
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasurePolicy
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.layout.layoutId
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.util.fastFirst
+import androidx.compose.ui.util.fastFirstOrNull
+import kotlinx.coroutines.launch
+
+private enum class VolumeSliderContentComponent {
+    Label,
+    DisabledMessage,
+}
+
+/** Shows label of the [VolumeSlider]. Also shows [disabledMessage] when not [isEnabled]. */
+@Composable
+fun VolumeSliderContent(
+    label: String,
+    isEnabled: Boolean,
+    disabledMessage: String?,
+    modifier: Modifier = Modifier,
+) {
+    Layout(
+        modifier = modifier.animateContentHeight(),
+        content = {
+            Text(
+                modifier = Modifier.layoutId(VolumeSliderContentComponent.Label).basicMarquee(),
+                text = label,
+                style = MaterialTheme.typography.titleMedium,
+                color = LocalContentColor.current,
+                maxLines = 1,
+            )
+
+            disabledMessage?.let { message ->
+                AnimatedVisibility(
+                    modifier = Modifier.layoutId(VolumeSliderContentComponent.DisabledMessage),
+                    visible = !isEnabled,
+                    enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(),
+                    exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(),
+                ) {
+                    Text(
+                        modifier = Modifier.basicMarquee(),
+                        text = message,
+                        style = MaterialTheme.typography.bodySmall,
+                        color = LocalContentColor.current,
+                        maxLines = 1,
+                    )
+                }
+            }
+        },
+        measurePolicy = VolumeSliderContentMeasurePolicy(isEnabled)
+    )
+}
+
+/**
+ * Uses [VolumeSliderContentComponent.Label] width when [isEnabled] and max available width
+ * otherwise. This ensures that the slider always have the correct measurement to position the
+ * content.
+ */
+private class VolumeSliderContentMeasurePolicy(private val isEnabled: Boolean) : MeasurePolicy {
+
+    override fun MeasureScope.measure(
+        measurables: List<Measurable>,
+        constraints: Constraints
+    ): MeasureResult {
+        val labelPlaceable =
+            measurables
+                .fastFirst { it.layoutId == VolumeSliderContentComponent.Label }
+                .measure(constraints)
+        val layoutWidth: Int = constraints.maxWidth
+        val fullLayoutWidth: Int =
+            if (isEnabled) {
+                // PlatformSlider uses half of the available space for the enabled state.
+                // This is using it to allow disabled message to take whole space when animating to
+                // prevent it from jumping left to right
+                layoutWidth * 2
+            } else {
+                layoutWidth
+            }
+
+        val disabledMessagePlaceable =
+            measurables
+                .fastFirstOrNull { it.layoutId == VolumeSliderContentComponent.DisabledMessage }
+                ?.measure(constraints.copy(maxWidth = fullLayoutWidth))
+
+        val layoutHeight = labelPlaceable.height + (disabledMessagePlaceable?.height ?: 0)
+        return layout(layoutWidth, layoutHeight) {
+            labelPlaceable.placeRelative(0, 0, 0f)
+            disabledMessagePlaceable?.placeRelative(0, labelPlaceable.height, 0f)
+        }
+    }
+}
+
+/** Animates composable height changes. */
+@Composable
+private fun Modifier.animateContentHeight(): Modifier {
+    var heightAnimation by remember { mutableStateOf<Animatable<Int, AnimationVector1D>?>(null) }
+    val coroutineScope = rememberCoroutineScope()
+    return layout { measurable, constraints ->
+        val placeable = measurable.measure(constraints)
+        val currentAnimation = heightAnimation
+        val anim =
+            if (currentAnimation == null) {
+                Animatable(placeable.height, Int.VectorConverter).also { heightAnimation = it }
+            } else {
+                coroutineScope.launch { currentAnimation.animateTo(placeable.height) }
+                currentAnimation
+            }
+        layout(placeable.width, anim.value) { placeable.place(0, 0) }
+    }
+}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt
index 6cff30c..da07f6d 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt
@@ -138,8 +138,9 @@
     // that will actually animate it.
     layoutState.startTransition(transition, transitionKey)
 
-    // The transformation now contains the spec that we should use to instantiate the Animatable.
-    val animationSpec = layoutState.transformationSpec.progressSpec
+    // The transition now contains the transformation spec that we should use to instantiate the
+    // Animatable.
+    val animationSpec = transition.transformationSpec.progressSpec
     val visibilityThreshold =
         (animationSpec as? SpringSpec)?.visibilityThreshold ?: ProgressVisibilityThreshold
     val animatable =
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
index 82083f9..1b06275 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
@@ -18,10 +18,8 @@
 
 package com.android.compose.animation.scene
 
-import android.util.Log
 import androidx.compose.animation.core.Animatable
 import androidx.compose.animation.core.AnimationVector1D
-import androidx.compose.animation.core.SpringSpec
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableFloatStateOf
@@ -145,16 +143,6 @@
         }
 
         val transitionState = layoutImpl.state.transitionState
-        if (transitionState is TransitionState.Transition) {
-            // TODO(b/290184746): Better handle interruptions here if state != idle.
-            Log.w(
-                TAG,
-                "start from TransitionState.Transition is not fully supported: from" +
-                    " ${transitionState.fromScene} to ${transitionState.toScene} " +
-                    "(progress ${transitionState.progress})"
-            )
-        }
-
         val fromScene = layoutImpl.scene(transitionState.currentScene)
         val swipes = computeSwipes(fromScene, startedPosition, pointersDown)
         val result =
@@ -269,19 +257,6 @@
     fun updateTransition(newTransition: SwipeTransition, force: Boolean = false) {
         if (isDrivingTransition || force) {
             layoutState.startTransition(newTransition, newTransition.key)
-
-            // Initialize SwipeTransition.transformationSpec and .swipeSpec. Note that this must be
-            // called right after layoutState.startTransition() is called, because it computes the
-            // current layoutState.transformationSpec().
-            val transformationSpec = layoutState.transformationSpec
-            newTransition.transformationSpec = transformationSpec
-            newTransition.swipeSpec =
-                transformationSpec.swipeSpec ?: layoutState.transitions.defaultSwipeSpec
-        } else {
-            // We were not driving the transition and we don't force the update, so the specs won't
-            // be used and it doesn't matter which ones we set here.
-            newTransition.transformationSpec = TransformationSpec.Empty
-            newTransition.swipeSpec = SceneTransitions.DefaultSwipeSpec
         }
 
         swipeTransition = newTransition
@@ -616,18 +591,6 @@
     override val isUserInputOngoing: Boolean
         get() = offsetAnimation == null
 
-    /**
-     * The [TransformationSpecImpl] associated to this transition.
-     *
-     * Note: This is lateinit because this [SwipeTransition] is needed by
-     * [BaseSceneTransitionLayoutState] to compute the [TransitionSpec], and it will be set right
-     * after [BaseSceneTransitionLayoutState.startTransition] is called with this transition.
-     */
-    lateinit var transformationSpec: TransformationSpecImpl
-
-    /** The spec to use when animating this transition to either [fromScene] or [toScene]. */
-    lateinit var swipeSpec: SpringSpec<Float>
-
     override val overscrollScope: OverscrollScope =
         object : OverscrollScope {
             override val absoluteDistance: Float
@@ -701,6 +664,9 @@
                 coroutineScope
                     .launch {
                         try {
+                            val swipeSpec =
+                                transformationSpec.swipeSpec
+                                    ?: layoutState.transitions.defaultSwipeSpec
                             animatable.animateTo(
                                 targetValue = targetOffset,
                                 animationSpec = swipeSpec,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
index 15712b5..69f1d45 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
@@ -203,7 +203,7 @@
         measurable: Measurable,
         constraints: Constraints,
     ): MeasureResult {
-        val overscrollScene = layoutImpl.state.currentOverscrollSpec?.scene
+        val overscrollScene = layoutImpl.state.currentTransition?.currentOverscrollSpec?.scene
         if (overscrollScene != null && overscrollScene != scene.key) {
             // There is an overscroll in progress on another scene
             // By measuring composable elements, Compose can cache relevant information.
@@ -269,13 +269,12 @@
         transition == null ||
             transition.fromScene !in element.sceneStates ||
             transition.toScene !in element.sceneStates ||
-            layoutImpl.state.currentOverscrollSpec?.scene == scene.key
+            transition.currentOverscrollSpec?.scene == scene.key
     ) {
         return true
     }
 
-    val sharedTransformation =
-        sharedElementTransformation(layoutImpl.state, transition, element.key)
+    val sharedTransformation = sharedElementTransformation(transition, element.key)
     if (sharedTransformation?.enabled == false) {
         return true
     }
@@ -305,23 +304,21 @@
             fromSceneZIndex = layoutImpl.scenes.getValue(fromScene).zIndex,
             toSceneZIndex = layoutImpl.scenes.getValue(toScene).zIndex,
         ) == scene
-    return chosenByPicker || layoutImpl.state.currentOverscrollSpec?.scene == scene
+    return chosenByPicker || transition.currentOverscrollSpec?.scene == scene
 }
 
 private fun isSharedElementEnabled(
-    layoutState: BaseSceneTransitionLayoutState,
     transition: TransitionState.Transition,
     element: ElementKey,
 ): Boolean {
-    return sharedElementTransformation(layoutState, transition, element)?.enabled ?: true
+    return sharedElementTransformation(transition, element)?.enabled ?: true
 }
 
 internal fun sharedElementTransformation(
-    layoutState: BaseSceneTransitionLayoutState,
     transition: TransitionState.Transition,
     element: ElementKey,
 ): SharedElementTransformation? {
-    val transformationSpec = layoutState.transformationSpec
+    val transformationSpec = transition.transformationSpec
     val sharedInFromScene = transformationSpec.transformations(element, transition.fromScene).shared
     val sharedInToScene = transformationSpec.transformations(element, transition.toScene).shared
 
@@ -360,11 +357,11 @@
     }
 
     val isSharedElement = fromState != null && toState != null
-    if (isSharedElement && isSharedElementEnabled(layoutImpl.state, transition, element.key)) {
+    if (isSharedElement && isSharedElementEnabled(transition, element.key)) {
         return true
     }
 
-    return layoutImpl.state.transformationSpec.transformations(element.key, scene.key).alpha == null
+    return transition.transformationSpec.transformations(element.key, scene.key).alpha == null
 }
 
 /**
@@ -559,7 +556,7 @@
     }
 
     if (transition is TransitionState.HasOverscrollProperties) {
-        val overscroll = layoutImpl.state.currentOverscrollSpec
+        val overscroll = transition.currentOverscrollSpec
         if (overscroll?.scene == scene.key) {
             val elementSpec = overscroll.transformationSpec.transformations(element.key, scene.key)
             val propertySpec = transformation(elementSpec) ?: return currentValue()
@@ -597,7 +594,7 @@
     // TODO(b/290184746): Support non linear shared paths as well as a way to make sure that shared
     // elements follow the finger direction.
     val isSharedElement = fromState != null && toState != null
-    if (isSharedElement && isSharedElementEnabled(layoutImpl.state, transition, element.key)) {
+    if (isSharedElement && isSharedElementEnabled(transition, element.key)) {
         val start = sceneValue(fromState!!)
         val end = sceneValue(toState!!)
 
@@ -607,7 +604,7 @@
     }
 
     val transformation =
-        transformation(layoutImpl.state.transformationSpec.transformations(element.key, scene.key))
+        transformation(transition.transformationSpec.transformations(element.key, scene.key))
         // If there is no transformation explicitly associated to this element value, let's use
         // the value given by the system (like the current position and size given by the layout
         // pass).
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 ebc9099..c7c874c 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
@@ -96,9 +96,17 @@
     modifier: Modifier = Modifier,
     swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector,
     @FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0f,
+    enableInterruptions: Boolean = DEFAULT_INTERRUPTIONS_ENABLED,
     scenes: SceneTransitionLayoutScope.() -> Unit,
 ) {
-    val state = updateSceneTransitionLayoutState(currentScene, onChangeScene, transitions)
+    val state =
+        updateSceneTransitionLayoutState(
+            currentScene,
+            onChangeScene,
+            transitions,
+            enableInterruptions = enableInterruptions,
+        )
+
     SceneTransitionLayout(
         state,
         modifier,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
index 617a8ea..f13c016 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
@@ -16,15 +16,16 @@
 
 package com.android.compose.animation.scene
 
+import android.util.Log
+import androidx.annotation.VisibleForTesting
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.SideEffect
 import androidx.compose.runtime.Stable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.SnapshotStateList
+import androidx.compose.ui.util.fastAll
 import androidx.compose.ui.util.fastFilter
 import androidx.compose.ui.util.fastForEach
 import com.android.compose.animation.scene.transition.link.LinkedTransition
@@ -50,10 +51,21 @@
      */
     val transitionState: TransitionState
 
-    /** The current transition, or `null` if we are idle. */
+    /**
+     * The current transition, or `null` if we are idle.
+     *
+     * Note: If you need to handle interruptions and multiple transitions running in parallel, use
+     * [currentTransitions] instead.
+     */
     val currentTransition: TransitionState.Transition?
         get() = transitionState as? TransitionState.Transition
 
+    /**
+     * The list of [TransitionState.Transition] currently running. This will be the empty list if we
+     * are idle.
+     */
+    val currentTransitions: List<TransitionState.Transition>
+
     /** The [SceneTransitions] used when animating this state. */
     val transitions: SceneTransitions
 
@@ -120,12 +132,14 @@
     transitions: SceneTransitions = SceneTransitions.Empty,
     canChangeScene: (SceneKey) -> Boolean = { true },
     stateLinks: List<StateLink> = emptyList(),
+    enableInterruptions: Boolean = DEFAULT_INTERRUPTIONS_ENABLED,
 ): MutableSceneTransitionLayoutState {
     return MutableSceneTransitionLayoutStateImpl(
         initialScene,
         transitions,
         canChangeScene,
         stateLinks,
+        enableInterruptions,
     )
 }
 
@@ -154,6 +168,7 @@
     transitions: SceneTransitions = SceneTransitions.Empty,
     canChangeScene: (SceneKey) -> Boolean = { true },
     stateLinks: List<StateLink> = emptyList(),
+    enableInterruptions: Boolean = DEFAULT_INTERRUPTIONS_ENABLED,
 ): SceneTransitionLayoutState {
     return remember {
             HoistedSceneTransitionLayoutState(
@@ -162,9 +177,19 @@
                 onChangeScene,
                 canChangeScene,
                 stateLinks,
+                enableInterruptions,
             )
         }
-        .apply { update(currentScene, onChangeScene, canChangeScene, transitions, stateLinks) }
+        .apply {
+            update(
+                currentScene,
+                onChangeScene,
+                canChangeScene,
+                transitions,
+                stateLinks,
+                enableInterruptions,
+            )
+        }
 }
 
 @Stable
@@ -204,6 +229,30 @@
         /** Whether user input is currently driving the transition. */
         abstract val isUserInputOngoing: Boolean
 
+        /**
+         * The current [TransformationSpecImpl] and [OverscrollSpecImpl] associated to this
+         * transition.
+         *
+         * Important: These will be set exactly once, when this transition is
+         * [started][BaseSceneTransitionLayoutState.startTransition].
+         */
+        internal var transformationSpec: TransformationSpecImpl = TransformationSpec.Empty
+        private var fromOverscrollSpec: OverscrollSpecImpl? = null
+        private var toOverscrollSpec: OverscrollSpecImpl? = null
+
+        /** The current [OverscrollSpecImpl], if this transition is currently overscrolling. */
+        internal val currentOverscrollSpec: OverscrollSpecImpl?
+            get() {
+                if (this !is HasOverscrollProperties) return null
+                val progress = progress
+                val bouncingScene = bouncingScene
+                return when {
+                    progress < 0f || bouncingScene == fromScene -> fromOverscrollSpec
+                    progress > 1f || bouncingScene == toScene -> toOverscrollSpec
+                    else -> null
+                }
+            }
+
         init {
             check(fromScene != toScene)
         }
@@ -232,6 +281,14 @@
             return isTransitioning(from = scene, to = other) ||
                 isTransitioning(from = other, to = scene)
         }
+
+        internal fun updateOverscrollSpecs(
+            fromSpec: OverscrollSpecImpl?,
+            toSpec: OverscrollSpecImpl?,
+        ) {
+            fromOverscrollSpec = fromSpec
+            toOverscrollSpec = toSpec
+        }
     }
 
     interface HasOverscrollProperties {
@@ -270,38 +327,41 @@
 internal abstract class BaseSceneTransitionLayoutState(
     initialScene: SceneKey,
     protected var stateLinks: List<StateLink>,
+
+    // TODO(b/290930950): Remove this flag.
+    internal var enableInterruptions: Boolean,
 ) : SceneTransitionLayoutState {
-    override var transitionState: TransitionState by
-        mutableStateOf(TransitionState.Idle(initialScene))
-        protected set
-
     /**
-     * The current [transformationSpec] associated to [transitionState]. Accessing this value makes
-     * sense only if [transitionState] is a [TransitionState.Transition].
+     * The current [TransitionState]. This list will either be:
+     * 1. A list with a single [TransitionState.Idle] element, when we are idle.
+     * 2. A list with one or more [TransitionState.Transition], when we are transitioning.
      */
-    internal var transformationSpec: TransformationSpecImpl = TransformationSpec.Empty
+    @VisibleForTesting
+    internal val transitionStates: MutableList<TransitionState> =
+        SnapshotStateList<TransitionState>().apply { add(TransitionState.Idle(initialScene)) }
 
-    private var fromOverscrollSpec: OverscrollSpecImpl? = null
-    private var toOverscrollSpec: OverscrollSpecImpl? = null
+    override val transitionState: TransitionState
+        get() = transitionStates.last()
 
-    /**
-     * @return the overscroll [OverscrollSpecImpl] if it is defined for the current
-     *   [transitionState] and we are currently over scrolling.
-     */
-    internal val currentOverscrollSpec: OverscrollSpecImpl?
+    private val activeTransitionLinks = mutableMapOf<StateLink, LinkedTransition>()
+
+    override val currentTransitions: List<TransitionState.Transition>
         get() {
-            val transition = currentTransition ?: return null
-            if (transition !is TransitionState.HasOverscrollProperties) return null
-            val progress = transition.progress
-            val bouncingScene = transition.bouncingScene
-            return when {
-                progress < 0f || bouncingScene == transition.fromScene -> fromOverscrollSpec
-                progress > 1f || bouncingScene == transition.toScene -> toOverscrollSpec
-                else -> null
+            if (transitionStates.last() is TransitionState.Idle) {
+                check(transitionStates.size == 1)
+                return emptyList()
+            } else {
+                @Suppress("UNCHECKED_CAST")
+                return transitionStates as List<TransitionState.Transition>
             }
         }
 
-    private val activeTransitionLinks = mutableMapOf<StateLink, LinkedTransition>()
+    /**
+     * The mapping of transitions that are finished, i.e. for which [finishTransition] was called,
+     * to their idle scene.
+     */
+    @VisibleForTesting
+    internal val finishedTransitions = mutableMapOf<TransitionState.Transition, SceneKey>()
 
     /** Whether we can transition to the given [scene]. */
     internal abstract fun canChangeScene(scene: SceneKey): Boolean
@@ -324,7 +384,11 @@
         return transition.isTransitioningBetween(scene, other)
     }
 
-    /** Start a new [transition], instantly interrupting any ongoing transition if there was one. */
+    /**
+     * Start a new [transition], instantly interrupting any ongoing transition if there was one.
+     *
+     * Important: you *must* call [finishTransition] once the transition is finished.
+     */
     internal fun startTransition(
         transition: TransitionState.Transition,
         transitionKey: TransitionKey?,
@@ -333,13 +397,81 @@
         val fromScene = transition.fromScene
         val toScene = transition.toScene
         val orientation = (transition as? TransitionState.HasOverscrollProperties)?.orientation
-        transformationSpec =
+
+        // Update the transition specs.
+        transition.transformationSpec =
             transitions.transitionSpec(fromScene, toScene, key = transitionKey).transformationSpec()
-        fromOverscrollSpec = orientation?.let { transitions.overscrollSpec(fromScene, it) }
-        toOverscrollSpec = orientation?.let { transitions.overscrollSpec(toScene, it) }
+        if (orientation != null) {
+            transition.updateOverscrollSpecs(
+                fromSpec = transitions.overscrollSpec(fromScene, orientation),
+                toSpec = transitions.overscrollSpec(toScene, orientation),
+            )
+        } else {
+            transition.updateOverscrollSpecs(fromSpec = null, toSpec = null)
+        }
+
+        // Handle transition links.
         cancelActiveTransitionLinks()
         setupTransitionLinks(transition)
-        transitionState = transition
+
+        if (!enableInterruptions) {
+            // Set the current transition.
+            check(transitionStates.size == 1)
+            transitionStates[0] = transition
+            return
+        }
+
+        when (val currentState = transitionStates.last()) {
+            is TransitionState.Idle -> {
+                // Replace [Idle] by [transition].
+                check(transitionStates.size == 1)
+                transitionStates[0] = transition
+            }
+            is TransitionState.Transition -> {
+                // Force the current transition to finish to currentScene.
+                currentState.finish().invokeOnCompletion {
+                    // Make sure [finishTransition] is called at the end of the transition.
+                    finishTransition(currentState, currentState.currentScene)
+                }
+
+                // Check that we don't have too many concurrent transitions.
+                if (transitionStates.size >= MAX_CONCURRENT_TRANSITIONS) {
+                    Log.wtf(
+                        TAG,
+                        buildString {
+                            appendLine("Potential leak detected in SceneTransitionLayoutState!")
+                            appendLine(
+                                "  Some transition(s) never called STLState.finishTransition()."
+                            )
+                            appendLine("  Transitions (size=${transitionStates.size}):")
+                            transitionStates.fastForEach { state ->
+                                val transition = state as TransitionState.Transition
+                                val from = transition.fromScene
+                                val to = transition.toScene
+                                val indicator =
+                                    if (finishedTransitions.contains(transition)) "x" else " "
+                                appendLine("  [$indicator] $from => $to ($transition)")
+                            }
+                        }
+                    )
+
+                    // Force finish all transitions.
+                    while (currentTransitions.isNotEmpty()) {
+                        val transition = transitionStates[0] as TransitionState.Transition
+                        finishTransition(transition, transition.currentScene)
+                    }
+
+                    // We finished all transitions, so we are now idle. We remove this state so that
+                    // we end up only with the new transition after appending it.
+                    check(transitionStates.size == 1)
+                    check(transitionStates[0] is TransitionState.Idle)
+                    transitionStates.clear()
+                }
+
+                // Append the new transition.
+                transitionStates.add(transition)
+            }
+        }
     }
 
     private fun cancelActiveTransitionLinks() {
@@ -379,13 +511,54 @@
      * nothing if [transition] was interrupted since it was started.
      */
     internal fun finishTransition(transition: TransitionState.Transition, idleScene: SceneKey) {
-        resolveActiveTransitionLinks(idleScene)
-        if (transitionState == transition) {
-            transitionState = TransitionState.Idle(idleScene)
+        val existingIdleScene = finishedTransitions[transition]
+        if (existingIdleScene != null) {
+            // This transition was already finished.
+            check(idleScene == existingIdleScene) {
+                "Transition $transition was finished multiple times with different " +
+                    "idleScene ($existingIdleScene != $idleScene)"
+            }
+            return
+        }
+
+        if (!transitionStates.contains(transition)) {
+            // This transition was already removed from transitionStates.
+            return
+        }
+
+        check(transitionStates.fastAll { it is TransitionState.Transition })
+
+        // Mark this transition as finished and save the scene it is settling at.
+        finishedTransitions[transition] = idleScene
+
+        // Finish all linked transitions.
+        finishActiveTransitionLinks(idleScene)
+
+        // Keep a reference to the idle scene of the last removed transition, in case we remove all
+        // transitions and should settle to Idle.
+        var lastRemovedIdleScene: SceneKey? = null
+
+        // Remove all first n finished transitions.
+        while (transitionStates.isNotEmpty()) {
+            val firstTransition = transitionStates[0]
+            if (!finishedTransitions.contains(firstTransition)) {
+                // Stop here.
+                break
+            }
+
+            // Remove the transition from the list and from the set of finished transitions.
+            transitionStates.removeAt(0)
+            lastRemovedIdleScene = finishedTransitions.remove(firstTransition)
+        }
+
+        // If all transitions are finished, we are idle.
+        if (transitionStates.isEmpty()) {
+            check(finishedTransitions.isEmpty())
+            transitionStates.add(TransitionState.Idle(checkNotNull(lastRemovedIdleScene)))
         }
     }
 
-    private fun resolveActiveTransitionLinks(idleScene: SceneKey) {
+    private fun finishActiveTransitionLinks(idleScene: SceneKey) {
         val previousTransition = this.transitionState as? TransitionState.Transition ?: return
         for ((link, linkedTransition) in activeTransitionLinks) {
             if (previousTransition.fromScene == idleScene) {
@@ -406,20 +579,39 @@
      * Check if a transition is in progress. If the progress value is near 0 or 1, immediately snap
      * to the closest scene.
      *
+     * Important: Snapping to the closest scene will instantly finish *all* ongoing transitions,
+     * only the progress of the last transition will be checked.
+     *
      * @return true if snapped to the closest scene.
      */
     internal fun snapToIdleIfClose(threshold: Float): Boolean {
         val transition = currentTransition ?: return false
         val progress = transition.progress
+
         fun isProgressCloseTo(value: Float) = (progress - value).absoluteValue <= threshold
 
+        fun finishAllTransitions(lastTransitionIdleScene: SceneKey) {
+            // Force finish all transitions.
+            while (currentTransitions.isNotEmpty()) {
+                val transition = transitionStates[0] as TransitionState.Transition
+                val idleScene =
+                    if (transitionStates.size == 1) {
+                        lastTransitionIdleScene
+                    } else {
+                        transition.currentScene
+                    }
+
+                finishTransition(transition, idleScene)
+            }
+        }
+
         return when {
             isProgressCloseTo(0f) -> {
-                finishTransition(transition, transition.fromScene)
+                finishAllTransitions(transition.fromScene)
                 true
             }
             isProgressCloseTo(1f) -> {
-                finishTransition(transition, transition.toScene)
+                finishAllTransitions(transition.toScene)
                 true
             }
             else -> false
@@ -437,7 +629,8 @@
     private var changeScene: (SceneKey) -> Unit,
     private var canChangeScene: (SceneKey) -> Boolean,
     stateLinks: List<StateLink> = emptyList(),
-) : BaseSceneTransitionLayoutState(initialScene, stateLinks) {
+    enableInterruptions: Boolean = DEFAULT_INTERRUPTIONS_ENABLED,
+) : BaseSceneTransitionLayoutState(initialScene, stateLinks, enableInterruptions) {
     private val targetSceneChannel = Channel<SceneKey>(Channel.CONFLATED)
 
     override fun canChangeScene(scene: SceneKey): Boolean = canChangeScene.invoke(scene)
@@ -451,12 +644,14 @@
         canChangeScene: (SceneKey) -> Boolean,
         transitions: SceneTransitions,
         stateLinks: List<StateLink>,
+        enableInterruptions: Boolean,
     ) {
         SideEffect {
             this.changeScene = onChangeScene
             this.canChangeScene = canChangeScene
             this.transitions = transitions
             this.stateLinks = stateLinks
+            this.enableInterruptions = enableInterruptions
 
             targetSceneChannel.trySend(currentScene)
         }
@@ -482,7 +677,10 @@
     override var transitions: SceneTransitions,
     private val canChangeScene: (SceneKey) -> Boolean = { true },
     stateLinks: List<StateLink> = emptyList(),
-) : MutableSceneTransitionLayoutState, BaseSceneTransitionLayoutState(initialScene, stateLinks) {
+    enableInterruptions: Boolean = DEFAULT_INTERRUPTIONS_ENABLED,
+) :
+    MutableSceneTransitionLayoutState,
+    BaseSceneTransitionLayoutState(initialScene, stateLinks, enableInterruptions) {
     override fun setTargetScene(
         targetScene: SceneKey,
         coroutineScope: CoroutineScope,
@@ -501,3 +699,15 @@
         setTargetScene(scene, coroutineScope = this)
     }
 }
+
+private const val TAG = "SceneTransitionLayoutState"
+
+/** Whether support for interruptions in enabled by default. */
+internal const val DEFAULT_INTERRUPTIONS_ENABLED = true
+
+/**
+ * The max number of concurrent transitions. If the number of transitions goes past this number,
+ * this probably means that there is a leak and we will Log.wtf before clearing the list of
+ * transitions.
+ */
+private const val MAX_CONCURRENT_TRANSITIONS = 100
diff --git a/packages/SystemUI/compose/scene/tests/Android.bp b/packages/SystemUI/compose/scene/tests/Android.bp
index 59cc63a..af13896 100644
--- a/packages/SystemUI/compose/scene/tests/Android.bp
+++ b/packages/SystemUI/compose/scene/tests/Android.bp
@@ -26,7 +26,6 @@
     name: "PlatformComposeSceneTransitionLayoutTests",
     manifest: "AndroidManifest.xml",
     test_suites: ["device-tests"],
-    sdk_version: "current",
     certificate: "platform",
 
     srcs: [
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
index 1e9a7e2..2ed51eb 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
@@ -984,14 +984,14 @@
         val scene = layoutState.transitionState.currentScene
         // We should have overscroll spec for scene C
         assertThat(layoutState.transitions.overscrollSpec(scene, Orientation.Vertical)).isNotNull()
-        assertThat(layoutState.currentOverscrollSpec).isNull()
+        assertThat(layoutState.currentTransition?.currentOverscrollSpec).isNull()
 
         val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeAlways)
         nestedScroll.scroll(available = downOffset(fractionOfScreen = 0.1f))
 
         // We scrolled down, under scene C there is nothing, so we can use the overscroll spec
-        assertThat(layoutState.currentOverscrollSpec).isNotNull()
-        assertThat(layoutState.currentOverscrollSpec?.scene).isEqualTo(SceneC)
+        assertThat(layoutState.currentTransition?.currentOverscrollSpec).isNotNull()
+        assertThat(layoutState.currentTransition?.currentOverscrollSpec?.scene).isEqualTo(SceneC)
         val transition = layoutState.currentTransition
         assertThat(transition).isNotNull()
         assertThat(transition!!.progress).isEqualTo(-0.1f)
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
index 597da9e..2453e25 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
@@ -595,7 +595,7 @@
         }
 
         assertThat(state.currentTransition).isNull()
-        assertThat(state.currentOverscrollSpec).isNull()
+        assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
 
         // Swipe by half of verticalSwipeDistance.
         rule.onRoot().performTouchInput {
@@ -643,7 +643,7 @@
 
         // Scroll 150% (Scene B overscroll by 50%)
         assertThat(transition.progress).isEqualTo(1.5f)
-        assertThat(state.currentOverscrollSpec).isNotNull()
+        assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
         fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 0.5f)
         // animatedFloat cannot overflow (canOverflow = false)
         assertThat(animatedFloat).isEqualTo(100f)
@@ -655,7 +655,7 @@
 
         // Scroll 250% (Scene B overscroll by 150%)
         assertThat(transition.progress).isEqualTo(2.5f)
-        assertThat(state.currentOverscrollSpec).isNotNull()
+        assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
         fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 1.5f)
         assertThat(animatedFloat).isEqualTo(100f)
     }
@@ -707,7 +707,7 @@
         }
 
         assertThat(state.currentTransition).isNull()
-        assertThat(state.currentOverscrollSpec).isNull()
+        assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
         val fooElement = rule.onNodeWithTag(TestElements.Foo.testTag, useUnmergedTree = true)
         fooElement.assertTopPositionInRootIsEqualTo(0.dp)
 
@@ -720,7 +720,7 @@
         }
 
         val transition = state.currentTransition
-        assertThat(state.currentOverscrollSpec).isNotNull()
+        assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
         assertThat(transition).isNotNull()
         assertThat(transition!!.progress).isEqualTo(-0.5f)
         fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 0.5f)
@@ -732,7 +732,7 @@
 
         // Scroll 150% (Scene B overscroll by 50%)
         assertThat(transition.progress).isEqualTo(-1.5f)
-        assertThat(state.currentOverscrollSpec).isNotNull()
+        assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
         fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 1.5f)
     }
 
@@ -771,7 +771,7 @@
 
         // Scroll 150% (100% scroll + 50% overscroll)
         assertThat(transition!!.progress).isEqualTo(1.5f)
-        assertThat(state.currentOverscrollSpec).isNotNull()
+        assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
         fooElement.assertTopPositionInRootIsEqualTo(layoutHeight * 0.5f)
         assertThat(animatedFloat).isEqualTo(100f)
 
@@ -782,7 +782,7 @@
 
         // Scroll 250% (100% scroll + 150% overscroll)
         assertThat(transition.progress).isEqualTo(2.5f)
-        assertThat(state.currentOverscrollSpec).isNotNull()
+        assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
         fooElement.assertTopPositionInRootIsEqualTo(layoutHeight * 1.5f)
         assertThat(animatedFloat).isEqualTo(100f)
     }
@@ -828,7 +828,7 @@
 
         // Scroll 150% (100% scroll + 50% overscroll)
         assertThat(transition.progress).isEqualTo(1.5f)
-        assertThat(state.currentOverscrollSpec).isNotNull()
+        assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
         fooElement.assertTopPositionInRootIsEqualTo(layoutHeight * (transition.progress - 1f))
         assertThat(animatedFloat).isEqualTo(100f)
 
@@ -840,7 +840,7 @@
         rule.waitUntil(timeoutMillis = 10_000) { transition.progress < 1f }
 
         assertThat(transition.progress).isLessThan(1f)
-        assertThat(state.currentOverscrollSpec).isNotNull()
+        assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
         assertThat(transition.bouncingScene).isEqualTo(transition.toScene)
         assertThat(animatedFloat).isEqualTo(100f)
     }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
index 9baabc3..93e94f8 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.compose.animation.scene
 
+import android.util.Log
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.ui.test.junit4.createComposeRule
@@ -28,9 +29,12 @@
 import com.android.compose.test.runMonotonicClockTest
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Job
 import kotlinx.coroutines.cancelAndJoin
-import kotlinx.coroutines.job
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.test.runTest
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -271,11 +275,21 @@
     }
 
     @Test
-    fun linkedTransition_startsLinkButLinkedStateIsTakenOver() {
+    fun linkedTransition_startsLinkButLinkedStateIsTakenOver() = runTest {
         val (parentState, childState) = setupLinkedStates()
 
-        val childTransition = transition(SceneA, SceneB)
-        val parentTransition = transition(SceneC, SceneA)
+        val childTransition =
+            transition(
+                SceneA,
+                SceneB,
+                onFinish = { launch { /* Do nothing. */} },
+            )
+        val parentTransition =
+            transition(
+                SceneC,
+                SceneA,
+                onFinish = { launch { /* Do nothing. */} },
+            )
         childState.startTransition(childTransition, null)
         parentState.startTransition(parentTransition, null)
 
@@ -303,7 +317,7 @@
 
         // Default transition from A to B.
         assertThat(state.setTargetScene(SceneB, coroutineScope = this)).isNotNull()
-        assertThat(state.transformationSpec.transformations).hasSize(1)
+        assertThat(state.currentTransition?.transformationSpec?.transformations).hasSize(1)
 
         // Go back to A.
         state.setTargetScene(SceneA, coroutineScope = this)
@@ -320,14 +334,14 @@
                 )
             )
             .isNotNull()
-        assertThat(state.transformationSpec.transformations).hasSize(2)
+        assertThat(state.currentTransition?.transformationSpec?.transformations).hasSize(2)
     }
 
     @Test
     fun snapToIdleIfClose_snapToStart() = runMonotonicClockTest {
         val state = MutableSceneTransitionLayoutStateImpl(SceneA, SceneTransitions.Empty)
         state.startTransition(
-            transition(from = SceneA, to = TestScenes.SceneB, progress = { 0.2f }),
+            transition(from = SceneA, to = SceneB, progress = { 0.2f }),
             transitionKey = null
         )
         assertThat(state.isTransitioning()).isTrue()
@@ -346,7 +360,7 @@
     fun snapToIdleIfClose_snapToEnd() = runMonotonicClockTest {
         val state = MutableSceneTransitionLayoutStateImpl(SceneA, SceneTransitions.Empty)
         state.startTransition(
-            transition(from = SceneA, to = TestScenes.SceneB, progress = { 0.8f }),
+            transition(from = SceneA, to = SceneB, progress = { 0.8f }),
             transitionKey = null
         )
         assertThat(state.isTransitioning()).isTrue()
@@ -358,7 +372,35 @@
         // Go to the final scene if it is close to 1.
         assertThat(state.snapToIdleIfClose(threshold = 0.2f)).isTrue()
         assertThat(state.isTransitioning()).isFalse()
-        assertThat(state.transitionState).isEqualTo(TransitionState.Idle(TestScenes.SceneB))
+        assertThat(state.transitionState).isEqualTo(TransitionState.Idle(SceneB))
+    }
+
+    @Test
+    fun snapToIdleIfClose_multipleTransitions() = runMonotonicClockTest {
+        val state = MutableSceneTransitionLayoutStateImpl(SceneA, SceneTransitions.Empty)
+
+        val aToB =
+            transition(
+                from = SceneA,
+                to = SceneB,
+                progress = { 0.5f },
+                onFinish = { launch { /* do nothing */} },
+            )
+        state.startTransition(aToB, transitionKey = null)
+        assertThat(state.currentTransitions).containsExactly(aToB).inOrder()
+
+        val bToC = transition(from = SceneB, to = SceneC, progress = { 0.8f })
+        state.startTransition(bToC, transitionKey = null)
+        assertThat(state.currentTransitions).containsExactly(aToB, bToC).inOrder()
+
+        // Ignore the request if the progress is not close to 0 or 1, using the threshold.
+        assertThat(state.snapToIdleIfClose(threshold = 0.1f)).isFalse()
+        assertThat(state.currentTransitions).containsExactly(aToB, bToC).inOrder()
+
+        // Go to the final scene if it is close to 1.
+        assertThat(state.snapToIdleIfClose(threshold = 0.2f)).isTrue()
+        assertThat(state.transitionState).isEqualTo(TransitionState.Idle(SceneC))
+        assertThat(state.currentTransitions).isEmpty()
     }
 
     @Test
@@ -435,23 +477,23 @@
                         overscroll(SceneB, Orientation.Vertical) { fade(TestElements.Foo) }
                     }
             )
-        assertThat(state.currentOverscrollSpec).isNull()
+        assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
 
         // overscroll for SceneA is NOT defined
         progress.value = -0.1f
-        assertThat(state.currentOverscrollSpec).isNull()
+        assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
 
         // scroll from SceneA to SceneB
         progress.value = 0.5f
-        assertThat(state.currentOverscrollSpec).isNull()
+        assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
 
         progress.value = 1f
-        assertThat(state.currentOverscrollSpec).isNull()
+        assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
 
         // overscroll for SceneB is defined
         progress.value = 1.1f
-        assertThat(state.currentOverscrollSpec).isNotNull()
-        assertThat(state.currentOverscrollSpec?.scene).isEqualTo(SceneB)
+        assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
+        assertThat(state.currentTransition?.currentOverscrollSpec?.scene).isEqualTo(SceneB)
     }
 
     @Test
@@ -465,23 +507,23 @@
                         overscroll(SceneA, Orientation.Vertical) { fade(TestElements.Foo) }
                     }
             )
-        assertThat(state.currentOverscrollSpec).isNull()
+        assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
 
         // overscroll for SceneA is defined
         progress.value = -0.1f
-        assertThat(state.currentOverscrollSpec).isNotNull()
-        assertThat(state.currentOverscrollSpec?.scene).isEqualTo(SceneA)
+        assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
+        assertThat(state.currentTransition?.currentOverscrollSpec?.scene).isEqualTo(SceneA)
 
         // scroll from SceneA to SceneB
         progress.value = 0.5f
-        assertThat(state.currentOverscrollSpec).isNull()
+        assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
 
         progress.value = 1f
-        assertThat(state.currentOverscrollSpec).isNull()
+        assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
 
         // overscroll for SceneB is NOT defined
         progress.value = 1.1f
-        assertThat(state.currentOverscrollSpec).isNull()
+        assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
     }
 
     @Test
@@ -492,21 +534,99 @@
                 progress = { progress.value },
                 sceneTransitions = transitions {}
             )
-        assertThat(state.currentOverscrollSpec).isNull()
+        assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
 
         // overscroll for SceneA is NOT defined
         progress.value = -0.1f
-        assertThat(state.currentOverscrollSpec).isNull()
+        assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
 
         // scroll from SceneA to SceneB
         progress.value = 0.5f
-        assertThat(state.currentOverscrollSpec).isNull()
+        assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
 
         progress.value = 1f
-        assertThat(state.currentOverscrollSpec).isNull()
+        assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
 
         // overscroll for SceneB is NOT defined
         progress.value = 1.1f
-        assertThat(state.currentOverscrollSpec).isNull()
+        assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
+    }
+
+    @Test
+    fun multipleTransitions() = runTest {
+        val finishingTransitions = mutableSetOf<TransitionState.Transition>()
+        fun onFinish(transition: TransitionState.Transition): Job {
+            // Instead of letting the transition finish, we put the transition in the
+            // finishingTransitions set so that we can verify that finish() is called when expected
+            // and then we call state STLState.finishTransition() ourselves.
+            finishingTransitions.add(transition)
+
+            return backgroundScope.launch {
+                // Try to acquire a locked mutex so that this code never completes.
+                Mutex(locked = true).withLock {}
+            }
+        }
+
+        val state = MutableSceneTransitionLayoutStateImpl(SceneA, EmptyTestTransitions)
+        val aToB = transition(SceneA, SceneB, onFinish = ::onFinish)
+        val bToC = transition(SceneB, SceneC, onFinish = ::onFinish)
+        val cToA = transition(SceneC, SceneA, onFinish = ::onFinish)
+
+        // Starting state.
+        assertThat(finishingTransitions).isEmpty()
+        assertThat(state.currentTransitions).isEmpty()
+
+        // A => B.
+        state.startTransition(aToB, transitionKey = null)
+        assertThat(finishingTransitions).isEmpty()
+        assertThat(state.finishedTransitions).isEmpty()
+        assertThat(state.currentTransitions).containsExactly(aToB).inOrder()
+
+        // B => C. This should automatically call finish() on aToB.
+        state.startTransition(bToC, transitionKey = null)
+        assertThat(finishingTransitions).containsExactly(aToB)
+        assertThat(state.finishedTransitions).isEmpty()
+        assertThat(state.currentTransitions).containsExactly(aToB, bToC).inOrder()
+
+        // C => A. This should automatically call finish() on bToC.
+        state.startTransition(cToA, transitionKey = null)
+        assertThat(finishingTransitions).containsExactly(aToB, bToC)
+        assertThat(state.finishedTransitions).isEmpty()
+        assertThat(state.currentTransitions).containsExactly(aToB, bToC, cToA).inOrder()
+
+        // Mark bToC as finished. The list of current transitions does not change because aToB is
+        // still not marked as finished.
+        state.finishTransition(bToC, idleScene = bToC.currentScene)
+        assertThat(state.finishedTransitions).containsExactly(bToC, bToC.currentScene)
+        assertThat(state.currentTransitions).containsExactly(aToB, bToC, cToA).inOrder()
+
+        // Mark aToB as finished. This will remove both aToB and bToC from the list of transitions.
+        state.finishTransition(aToB, idleScene = aToB.currentScene)
+        assertThat(state.finishedTransitions).isEmpty()
+        assertThat(state.currentTransitions).containsExactly(cToA).inOrder()
+    }
+
+    @Test
+    fun tooManyTransitionsLogsWtfAndClearsTransitions() = runTest {
+        val state = MutableSceneTransitionLayoutStateImpl(SceneA, EmptyTestTransitions)
+
+        fun startTransition() {
+            val transition = transition(SceneA, SceneB, onFinish = { launch { /* do nothing */} })
+            state.startTransition(transition, transitionKey = null)
+        }
+
+        var hasLoggedWtf = false
+        val originalHandler = Log.setWtfHandler { _, _, _ -> hasLoggedWtf = true }
+        try {
+            repeat(100) { startTransition() }
+            assertThat(hasLoggedWtf).isFalse()
+            assertThat(state.currentTransitions).hasSize(100)
+
+            startTransition()
+            assertThat(hasLoggedWtf).isTrue()
+            assertThat(state.currentTransitions).hasSize(1)
+        } finally {
+            Log.setWtfHandler(originalHandler)
+        }
     }
 }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
index efaea71..723a182 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
@@ -299,6 +299,11 @@
             .isWithin(DpOffsetSubject.DefaultTolerance)
             .of(DpOffset(expectedOffset, expectedOffset))
 
+        // Wait for the transition to C to finish.
+        rule.mainClock.advanceTimeBy(TestTransitionDuration)
+        assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+        assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
+
         // Go back to scene A. This should happen instantly (once the animation started, i.e. after
         // 2 frames) given that we use a snap() animation spec.
         currentScene = TestScenes.SceneA
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
index 99372a5..f034c18 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
@@ -547,12 +547,12 @@
         }
 
         assertThat(state.isTransitioning(from = TestScenes.SceneA, to = TestScenes.SceneB)).isTrue()
-        assertThat(state.transformationSpec.transformations).hasSize(1)
+        assertThat(state.currentTransition?.transformationSpec?.transformations).hasSize(1)
 
         // Move the pointer up to swipe to scene B using the new transition.
         rule.onRoot().performTouchInput { moveBy(Offset(0f, -1.dp.toPx()), delayMillis = 1_000) }
         assertThat(state.isTransitioning(from = TestScenes.SceneA, to = TestScenes.SceneB)).isTrue()
-        assertThat(state.transformationSpec.transformations).hasSize(2)
+        assertThat(state.currentTransition?.transformationSpec?.transformations).hasSize(2)
     }
 
     @Test
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/Transition.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/Transition.kt
index a32fe22..767057b 100644
--- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/Transition.kt
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/Transition.kt
@@ -29,6 +29,7 @@
     isUpOrLeft: Boolean = false,
     bouncingScene: SceneKey? = null,
     orientation: Orientation = Orientation.Horizontal,
+    onFinish: ((TransitionState.Transition) -> Job)? = null,
 ): TransitionState.Transition {
     return object : TransitionState.Transition(from, to), TransitionState.HasOverscrollProperties {
         override val currentScene: SceneKey = from
@@ -46,7 +47,13 @@
             }
 
         override fun finish(): Job {
-            error("finish() is not supported in test transitions")
+            val onFinish =
+                onFinish
+                    ?: error(
+                        "onFinish() must be provided if finish() is called on test transitions"
+                    )
+
+            return onFinish(this)
         }
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt
index 69ff5ab..b4f87c4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt
@@ -21,6 +21,8 @@
 import android.view.View
 import android.widget.FrameLayout
 import android.widget.RemoteViews.RemoteResponse
+import androidx.core.util.component1
+import androidx.core.util.component2
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -29,6 +31,7 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.refEq
 import org.mockito.Mock
 import org.mockito.Mockito.isNull
 import org.mockito.Mockito.notNull
@@ -62,6 +65,7 @@
         val parent = FrameLayout(context)
         val view = CommunalAppWidgetHostView(context)
         parent.addView(view)
+        val (fillInIntent, activityOptions) = testResponse.getLaunchOptions(view)
 
         underTest.onInteraction(view, testIntent, testResponse)
 
@@ -70,6 +74,8 @@
                 eq(testIntent),
                 isNull(),
                 notNull(),
+                refEq(fillInIntent),
+                refEq(activityOptions.toBundle()),
             )
     }
 
@@ -78,10 +84,17 @@
         val parent = FrameLayout(context)
         val view = View(context)
         parent.addView(view)
+        val (fillInIntent, activityOptions) = testResponse.getLaunchOptions(view)
 
         underTest.onInteraction(view, testIntent, testResponse)
 
         verify(activityStarter)
-            .startPendingIntentMaybeDismissingKeyguard(eq(testIntent), isNull(), isNull())
+            .startPendingIntentMaybeDismissingKeyguard(
+                eq(testIntent),
+                isNull(),
+                isNull(),
+                refEq(fillInIntent),
+                refEq(activityOptions.toBundle()),
+            )
     }
 }
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/LockscreenToGoneTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelTest.kt
index e7aaddd..857b9f8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelTest.kt
@@ -68,9 +68,7 @@
             repository.sendTransitionStep(step(0f))
             assertThat(alpha).isEqualTo(0.5f)
 
-            repository.sendTransitionStep(step(0.25f))
-            assertThat(alpha).isEqualTo(0.25f)
-
+            // Before the halfway point, it will have reached zero
             repository.sendTransitionStep(step(.5f))
             assertThat(alpha).isEqualTo(0f)
         }
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/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/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index cc66f8b..f018cc1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -51,6 +51,8 @@
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.shared.model.fakeSceneDataSource
 import com.android.systemui.statusbar.NotificationShadeWindowController
+import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
+import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository
 import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
 import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
 import com.android.systemui.statusbar.phone.CentralSurfaces
@@ -175,10 +177,12 @@
             transitionStateFlow.value = ObservableTransitionState.Idle(Scenes.Gone)
             assertThat(isVisible).isFalse()
 
-            kosmos.headsUpNotificationRepository.hasPinnedHeadsUp.value = true
+            kosmos.headsUpNotificationRepository.activeHeadsUpRows.value =
+                buildNotificationRows(isPinned = true)
             assertThat(isVisible).isTrue()
 
-            kosmos.headsUpNotificationRepository.hasPinnedHeadsUp.value = false
+            kosmos.headsUpNotificationRepository.activeHeadsUpRows.value =
+                buildNotificationRows(isPinned = false)
             assertThat(isVisible).isFalse()
         }
 
@@ -1070,4 +1074,17 @@
 
         return transitionStateFlow
     }
+
+    private fun buildNotificationRows(isPinned: Boolean = false): Set<HeadsUpRowRepository> =
+        setOf(
+            fakeHeadsUpRowRepository(key = "0", isPinned = isPinned),
+            fakeHeadsUpRowRepository(key = "1", isPinned = isPinned),
+            fakeHeadsUpRowRepository(key = "2", isPinned = isPinned),
+            fakeHeadsUpRowRepository(key = "3", isPinned = isPinned),
+        )
+
+    private fun fakeHeadsUpRowRepository(key: String, isPinned: Boolean) =
+        FakeHeadsUpRowRepository(key = key, elementKey = Any()).apply {
+            this.isPinned.value = isPinned
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
new file mode 100644
index 0000000..bba9991
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
@@ -0,0 +1,269 @@
+/*
+ * 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.statusbar.notification.domain.interactor
+
+import android.platform.test.annotations.EnableFlags
+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.statusbar.notification.data.repository.FakeHeadsUpRowRepository
+import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
+import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
+import com.android.systemui.statusbar.notification.stack.data.repository.setNotifications
+import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
+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
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME)
+class HeadsUpNotificationInteractorTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val repository = kosmos.headsUpNotificationRepository
+
+    private val underTest = kosmos.headsUpNotificationInteractor
+
+    @Test
+    fun hasPinnedRows_emptyList_false() =
+        testScope.runTest {
+            val hasPinnedRows by collectLastValue(underTest.hasPinnedRows)
+
+            assertThat(hasPinnedRows).isFalse()
+        }
+
+    @Test
+    fun hasPinnedRows_noPinnedRows_false() =
+        testScope.runTest {
+            val hasPinnedRows by collectLastValue(underTest.hasPinnedRows)
+            // WHEN no pinned rows are set
+            repository.setNotifications(
+                fakeHeadsUpRowRepository("key 0"),
+                fakeHeadsUpRowRepository("key 1"),
+                fakeHeadsUpRowRepository("key 2"),
+            )
+            runCurrent()
+
+            // THEN hasPinnedRows is false
+            assertThat(hasPinnedRows).isFalse()
+        }
+
+    @Test
+    fun hasPinnedRows_hasPinnedRows_true() =
+        testScope.runTest {
+            val hasPinnedRows by collectLastValue(underTest.hasPinnedRows)
+            // WHEN a pinned rows is set
+            repository.setNotifications(
+                fakeHeadsUpRowRepository("key 0", isPinned = true),
+                fakeHeadsUpRowRepository("key 1"),
+                fakeHeadsUpRowRepository("key 2"),
+            )
+            runCurrent()
+
+            // THEN hasPinnedRows is true
+            assertThat(hasPinnedRows).isTrue()
+        }
+
+    @Test
+    fun hasPinnedRows_rowGetsPinned_true() =
+        testScope.runTest {
+            val hasPinnedRows by collectLastValue(underTest.hasPinnedRows)
+            // GIVEN no rows are pinned
+            val rows =
+                arrayListOf(
+                    fakeHeadsUpRowRepository("key 0"),
+                    fakeHeadsUpRowRepository("key 1"),
+                    fakeHeadsUpRowRepository("key 2"),
+                )
+            repository.setNotifications(rows)
+            runCurrent()
+
+            // WHEN a row gets pinned
+            rows[0].isPinned.value = true
+            runCurrent()
+
+            // THEN hasPinnedRows updates to true
+            assertThat(hasPinnedRows).isTrue()
+        }
+
+    @Test
+    fun hasPinnedRows_rowGetsUnPinned_false() =
+        testScope.runTest {
+            val hasPinnedRows by collectLastValue(underTest.hasPinnedRows)
+            // GIVEN one row is pinned
+            val rows =
+                arrayListOf(
+                    fakeHeadsUpRowRepository("key 0", isPinned = true),
+                    fakeHeadsUpRowRepository("key 1"),
+                    fakeHeadsUpRowRepository("key 2"),
+                )
+            repository.setNotifications(rows)
+            runCurrent()
+
+            // THEN that row gets unpinned
+            rows[0].isPinned.value = false
+            runCurrent()
+
+            // THEN hasPinnedRows updates to false
+            assertThat(hasPinnedRows).isFalse()
+        }
+
+    @Test
+    fun pinnedRows_noRows_isEmpty() =
+        testScope.runTest {
+            val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)
+
+            assertThat(pinnedHeadsUpRows).isEmpty()
+        }
+
+    @Test
+    fun pinnedRows_noPinnedRows_isEmpty() =
+        testScope.runTest {
+            val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)
+            // WHEN no rows are pinned
+            repository.setNotifications(
+                fakeHeadsUpRowRepository("key 0"),
+                fakeHeadsUpRowRepository("key 1"),
+                fakeHeadsUpRowRepository("key 2"),
+            )
+            runCurrent()
+
+            // THEN all rows are filtered
+            assertThat(pinnedHeadsUpRows).isEmpty()
+        }
+
+    @Test
+    fun pinnedRows_hasPinnedRows_containsPinnedRows() =
+        testScope.runTest {
+            val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)
+            // WHEN some rows are pinned
+            val rows =
+                arrayListOf(
+                    fakeHeadsUpRowRepository("key 0", isPinned = true),
+                    fakeHeadsUpRowRepository("key 1", isPinned = true),
+                    fakeHeadsUpRowRepository("key 2"),
+                )
+            repository.setNotifications(rows)
+            runCurrent()
+
+            // THEN the unpinned rows are filtered
+            assertThat(pinnedHeadsUpRows).containsExactly(rows[0], rows[1])
+        }
+
+    @Test
+    fun pinnedRows_rowGetsPinned_containsPinnedRows() =
+        testScope.runTest {
+            val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)
+            // GIVEN some rows are pinned
+            val rows =
+                arrayListOf(
+                    fakeHeadsUpRowRepository("key 0", isPinned = true),
+                    fakeHeadsUpRowRepository("key 1", isPinned = true),
+                    fakeHeadsUpRowRepository("key 2"),
+                )
+            repository.setNotifications(rows)
+            runCurrent()
+
+            // WHEN all rows gets pinned
+            rows[2].isPinned.value = true
+            runCurrent()
+
+            // THEN no rows are filtered
+            assertThat(pinnedHeadsUpRows).containsExactly(rows[0], rows[1], rows[2])
+        }
+
+    @Test
+    fun pinnedRows_allRowsPinned_containsAllRows() =
+        testScope.runTest {
+            val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)
+            // WHEN all rows are pinned
+            val rows =
+                arrayListOf(
+                    fakeHeadsUpRowRepository("key 0", isPinned = true),
+                    fakeHeadsUpRowRepository("key 1", isPinned = true),
+                    fakeHeadsUpRowRepository("key 2", isPinned = true),
+                )
+            repository.setNotifications(rows)
+            runCurrent()
+
+            // THEN no rows are filtered
+            assertThat(pinnedHeadsUpRows).containsExactly(rows[0], rows[1], rows[2])
+        }
+
+    @Test
+    fun pinnedRows_rowGetsUnPinned_containsPinnedRows() =
+        testScope.runTest {
+            val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)
+            // GIVEN all rows are pinned
+            val rows =
+                arrayListOf(
+                    fakeHeadsUpRowRepository("key 0", isPinned = true),
+                    fakeHeadsUpRowRepository("key 1", isPinned = true),
+                    fakeHeadsUpRowRepository("key 2", isPinned = true),
+                )
+            repository.setNotifications(rows)
+            runCurrent()
+
+            // WHEN a row gets unpinned
+            rows[0].isPinned.value = false
+            runCurrent()
+
+            // THEN the unpinned row is filtered
+            assertThat(pinnedHeadsUpRows).containsExactly(rows[1], rows[2])
+        }
+
+    @Test
+    fun pinnedRows_rowGetsPinnedAndUnPinned_containsTheSameInstance() =
+        testScope.runTest {
+            val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)
+
+            val rows =
+                arrayListOf(
+                    fakeHeadsUpRowRepository("key 0"),
+                    fakeHeadsUpRowRepository("key 1"),
+                    fakeHeadsUpRowRepository("key 2"),
+                )
+            repository.setNotifications(rows)
+            runCurrent()
+
+            rows[0].isPinned.value = true
+            runCurrent()
+            assertThat(pinnedHeadsUpRows).containsExactly(rows[0])
+
+            rows[0].isPinned.value = false
+            runCurrent()
+            assertThat(pinnedHeadsUpRows).isEmpty()
+
+            rows[0].isPinned.value = true
+            runCurrent()
+            assertThat(pinnedHeadsUpRows).containsExactly(rows[0])
+        }
+
+    private fun fakeHeadsUpRowRepository(key: String, isPinned: Boolean = false) =
+        FakeHeadsUpRowRepository(key = key, elementKey = Any()).apply {
+            this.isPinned.value = isPinned
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
index 53a8e5d..5256bb9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
@@ -720,6 +720,59 @@
         }
 
     @Test
+    fun alphaDoesNotUpdateWhileGoneTransitionIsRunning() =
+        testScope.runTest {
+            val viewState = ViewStateAccessor()
+            val alpha by collectLastValue(underTest.keyguardAlpha(viewState))
+
+            showLockscreen()
+            // GONE transition gets to 90% complete
+            keyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.GONE,
+                    transitionState = TransitionState.STARTED,
+                    value = 0f,
+                )
+            )
+            runCurrent()
+            keyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.GONE,
+                    transitionState = TransitionState.RUNNING,
+                    value = 0.9f,
+                )
+            )
+            runCurrent()
+
+            // At this point, alpha should be zero
+            assertThat(alpha).isEqualTo(0f)
+
+            // An attempt to override by the shade should be ignored
+            shadeRepository.setQsExpansion(0.5f)
+            assertThat(alpha).isEqualTo(0f)
+        }
+
+    @Test
+    fun alphaWhenGoneIsSetToOne() =
+        testScope.runTest {
+            val viewState = ViewStateAccessor()
+            val alpha by collectLastValue(underTest.keyguardAlpha(viewState))
+
+            showLockscreen()
+
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.GONE,
+                testScope
+            )
+            keyguardRepository.setStatusBarState(StatusBarState.SHADE)
+
+            assertThat(alpha).isEqualTo(1f)
+        }
+
+    @Test
     fun shadeCollapseFadeIn() =
         testScope.runTest {
             val fadeIn by collectValues(underTest.shadeCollapseFadeIn)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
index 8aa0e3fc..c8062fb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
@@ -16,12 +16,15 @@
 
 package com.android.systemui.statusbar.phone
 
+import android.app.ActivityOptions
 import android.app.PendingIntent
 import android.content.Intent
+import android.os.Bundle
 import android.os.RemoteException
 import android.os.UserHandle
 import android.view.View
 import android.widget.FrameLayout
+import android.window.SplashScreen.SPLASH_SCREEN_STYLE_SOLID_COLOR
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.keyguard.KeyguardUpdateMonitor
@@ -48,6 +51,7 @@
 import com.android.systemui.statusbar.window.StatusBarWindowController
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.nullable
 import com.android.systemui.util.mockito.whenever
@@ -173,6 +177,53 @@
             )
     }
 
+    fun startPendingIntentDismissingKeyguard_fillInIntentAndExtraOptions_sendAndReturnResult() {
+        val pendingIntent = mock(PendingIntent::class.java)
+        val fillInIntent = mock(Intent::class.java)
+        val parent = FrameLayout(context)
+        val view =
+            object : View(context), LaunchableView {
+                override fun setShouldBlockVisibilityChanges(block: Boolean) {}
+            }
+        parent.addView(view)
+        val controller = ActivityTransitionAnimator.Controller.fromView(view)
+        whenever(pendingIntent.isActivity).thenReturn(true)
+        whenever(keyguardStateController.isShowing).thenReturn(true)
+        whenever(deviceProvisionedController.isDeviceProvisioned).thenReturn(true)
+        whenever(activityIntentHelper.wouldPendingShowOverLockscreen(eq(pendingIntent), anyInt()))
+            .thenReturn(false)
+
+        // extra activity options to set on pending intent
+        val activityOptions = mock(ActivityOptions::class.java)
+        activityOptions.splashScreenStyle = SPLASH_SCREEN_STYLE_SOLID_COLOR
+        activityOptions.isPendingIntentBackgroundActivityLaunchAllowedByPermission = false
+        val bundleCaptor = argumentCaptor<Bundle>()
+
+        underTest.startPendingIntentMaybeDismissingKeyguard(
+            intent = pendingIntent,
+            animationController = controller,
+            intentSentUiThreadCallback = null,
+            fillInIntent = fillInIntent,
+            extraOptions = activityOptions.toBundle(),
+        )
+        mainExecutor.runAllReady()
+
+        // Fill-in intent is passed and options contain extra values specified
+        verify(pendingIntent)
+            .sendAndReturnResult(
+                eq(context),
+                eq(0),
+                eq(fillInIntent),
+                nullable(),
+                nullable(),
+                nullable(),
+                bundleCaptor.capture()
+            )
+        val options = ActivityOptions.fromBundle(bundleCaptor.value)
+        assertThat(options.isPendingIntentBackgroundActivityLaunchAllowedByPermission).isFalse()
+        assertThat(options.splashScreenStyle).isEqualTo(SPLASH_SCREEN_STYLE_SOLID_COLOR)
+    }
+
     @Test
     fun startPendingIntentDismissingKeyguard_associatedView_getAnimatorController() {
         val pendingIntent = mock(PendingIntent::class.java)
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/bottombar/ui/viewmodel/BottomBarViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModelTest.kt
index 471c8d8..8e92557 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModelTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.volume.panel.component.bottombar.ui.viewmodel
 
+import android.app.ActivityManager
 import android.content.Intent
 import android.provider.Settings
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -23,6 +24,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.activityStarter
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.capture
@@ -49,6 +51,8 @@
 
     @Captor private lateinit var intentCaptor: ArgumentCaptor<Intent>
 
+    @Captor private lateinit var activityStartedCaptor: ArgumentCaptor<ActivityStarter.Callback>
+
     private val kosmos = testKosmos()
 
     private lateinit var underTest: BottomBarViewModel
@@ -80,10 +84,13 @@
 
                 runCurrent()
 
+                verify(activityStarter).startActivity(capture(intentCaptor), eq(true),
+                        capture(activityStartedCaptor))
+                assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_SOUND_SETTINGS)
+
+                activityStartedCaptor.value.onActivityStarted(ActivityManager.START_SUCCESS)
                 val volumePanelState by collectLastValue(volumePanelViewModel.volumePanelState)
                 assertThat(volumePanelState!!.isVisible).isFalse()
-                verify(activityStarter).startActivity(capture(intentCaptor), eq(true))
-                assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_SOUND_SETTINGS)
             }
         }
     }
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java
index 1126ec3..072ec99 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java
@@ -17,6 +17,7 @@
 import android.annotation.Nullable;
 import android.app.PendingIntent;
 import android.content.Intent;
+import android.os.Bundle;
 import android.os.UserHandle;
 import android.view.View;
 
@@ -67,6 +68,17 @@
             @Nullable ActivityTransitionAnimator.Controller animationController);
 
     /**
+     * Similar to {@link #startPendingIntentMaybeDismissingKeyguard(PendingIntent, Runnable,
+     * ActivityTransitionAnimator.Controller)}, but also specifies a fill-in intent and extra
+     * options that could be used to populate the pending intent and launch the activity.
+     */
+    void startPendingIntentMaybeDismissingKeyguard(PendingIntent intent,
+            @Nullable Runnable intentSentUiThreadCallback,
+            @Nullable ActivityTransitionAnimator.Controller animationController,
+            @Nullable Intent fillInIntent,
+            @Nullable Bundle extraOptions);
+
+    /**
      * The intent flag can be specified in startActivity().
      */
     void startActivity(Intent intent, boolean onlyProvisioned, boolean dismissShade, int flags);
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 86f64f8..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,7 +630,7 @@
 
     @Nullable
     public ClockController getClock() {
-        if (migrateClocksToBlueprint()) {
+        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/communal/widgets/WidgetInteractionHandler.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt
index 4c1e77b..778d8cf 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt
@@ -16,9 +16,14 @@
 
 package com.android.systemui.communal.widgets
 
+import android.app.ActivityOptions
 import android.app.PendingIntent
+import android.content.Intent
+import android.util.Pair
 import android.view.View
 import android.widget.RemoteViews
+import androidx.core.util.component1
+import androidx.core.util.component2
 import com.android.systemui.animation.ActivityTransitionAnimator
 import com.android.systemui.common.ui.view.getNearestParent
 import com.android.systemui.plugins.ActivityStarter
@@ -33,21 +38,33 @@
         view: View,
         pendingIntent: PendingIntent,
         response: RemoteViews.RemoteResponse
-    ): Boolean =
-        when {
-            pendingIntent.isActivity -> startActivity(view, pendingIntent)
-            else ->
-                RemoteViews.startPendingIntent(view, pendingIntent, response.getLaunchOptions(view))
+    ): Boolean {
+        val launchOptions = response.getLaunchOptions(view)
+        return when {
+            pendingIntent.isActivity ->
+                // Forward the fill-in intent and activity options retrieved from the response
+                // to populate the pending intent, so that list items can launch respective
+                // activities.
+                startActivity(view, pendingIntent, launchOptions)
+            else -> RemoteViews.startPendingIntent(view, pendingIntent, launchOptions)
         }
+    }
 
-    private fun startActivity(view: View, pendingIntent: PendingIntent): Boolean {
+    private fun startActivity(
+        view: View,
+        pendingIntent: PendingIntent,
+        launchOptions: Pair<Intent, ActivityOptions>,
+    ): Boolean {
         val hostView = view.getNearestParent<CommunalAppWidgetHostView>()
         val animationController = hostView?.let(ActivityTransitionAnimator.Controller::fromView)
+        val (fillInIntent, activityOptions) = launchOptions
 
         activityStarter.startPendingIntentMaybeDismissingKeyguard(
             pendingIntent,
             /* intentSentUiThreadCallback = */ null,
-            animationController
+            animationController,
+            fillInIntent,
+            activityOptions.toBundle(),
         )
         return true
     }
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/keyboard/data/repository/CommandLineKeyboardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/CommandLineKeyboardRepository.kt
new file mode 100644
index 0000000..f49cfdd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/CommandLineKeyboardRepository.kt
@@ -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.systemui.keyboard.data.repository
+
+import android.util.Log
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyboard.data.model.Keyboard
+import com.android.systemui.keyboard.shared.model.BacklightModel
+import com.android.systemui.statusbar.commandline.Command
+import com.android.systemui.statusbar.commandline.CommandRegistry
+import java.io.PrintWriter
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.filterNotNull
+
+/**
+ * Helper class for development to mock various keyboard states with command line. Alternative for
+ * [KeyboardRepositoryImpl] which relies on real data from framework. [KeyboardRepositoryImpl] is
+ * the default implementation so to use this class you need to substitute it in [KeyboardModule].
+ *
+ * For usage information: see [KeyboardCommand.help] or run `adb shell cmd statusbar keyboard`.
+ */
+@SysUISingleton
+class CommandLineKeyboardRepository @Inject constructor(commandRegistry: CommandRegistry) :
+    KeyboardRepository {
+
+    private val _isAnyKeyboardConnected = MutableStateFlow(false)
+    override val isAnyKeyboardConnected: Flow<Boolean> = _isAnyKeyboardConnected
+
+    private val _backlightState: MutableStateFlow<BacklightModel?> = MutableStateFlow(null)
+    // filtering to make sure backlight doesn't have default initial value
+    override val backlight: Flow<BacklightModel> = _backlightState.filterNotNull()
+
+    private val _newlyConnectedKeyboard: MutableStateFlow<Keyboard?> = MutableStateFlow(null)
+    override val newlyConnectedKeyboard: Flow<Keyboard> = _newlyConnectedKeyboard.filterNotNull()
+
+    init {
+        Log.i(TAG, "initializing shell command $COMMAND")
+        commandRegistry.registerCommand(COMMAND) { KeyboardCommand() }
+    }
+
+    inner class KeyboardCommand : Command {
+        override fun execute(pw: PrintWriter, args: List<String>) {
+            Log.i(TAG, "$COMMAND command was called with args: $args")
+            if (args.isEmpty()) {
+                help(pw)
+                return
+            }
+            when (args[0]) {
+                "keyboard-connected" -> _isAnyKeyboardConnected.value = args[1].toBoolean()
+                "backlight" -> {
+                    @Suppress("Since15")
+                    val level = Math.clamp(args[1].toInt().toLong(), 0, MAX_BACKLIGHT_LEVEL)
+                    _backlightState.value = BacklightModel(level, MAX_BACKLIGHT_LEVEL)
+                }
+                "new-keyboard" -> {
+                    _newlyConnectedKeyboard.value =
+                        Keyboard(vendorId = args[1].toInt(), productId = args[2].toInt())
+                }
+                else -> help(pw)
+            }
+        }
+
+        override fun help(pw: PrintWriter) {
+            pw.println("Usage: adb shell cmd statusbar $COMMAND <command>")
+            pw.println(
+                "Note: this command only mocks setting these values on the framework level" +
+                    " but in reality doesn't change anything and is only used for testing UI"
+            )
+            pw.println("Available commands:")
+            pw.println("  keyboard-connected [true|false]")
+            pw.println("     Notify any physical keyboard connected/disconnected.")
+            pw.println("  backlight <level>")
+            pw.println("     Notify new keyboard backlight level: min 0, max $MAX_BACKLIGHT_LEVEL.")
+            pw.println("  new-keyboard <vendor-id> <product-id>")
+            pw.println("     Notify new physical keyboard with specified parameters got connected.")
+        }
+    }
+
+    companion object {
+        private const val TAG = "CommandLineKeyboardRepository"
+        private const val COMMAND = "keyboard"
+        private const val MAX_BACKLIGHT_LEVEL = 5
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt
index 2fac40a..91d5280 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt
@@ -46,6 +46,10 @@
 import kotlinx.coroutines.flow.mapNotNull
 import kotlinx.coroutines.flow.shareIn
 
+/**
+ * Provides information about physical keyboard states. [CommandLineKeyboardRepository] can be
+ * useful command line-driven implementation during development.
+ */
 interface KeyboardRepository {
     /** Emits true if any physical keyboard is connected to the device, false otherwise. */
     val isAnyKeyboardConnected: Flow<Boolean>
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 7a56554..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 {
@@ -123,30 +127,25 @@
                 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/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
index 12b27eb..2649d43 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
@@ -289,7 +289,10 @@
                 .collect { pair ->
                     val (isKeyguardGoingAway, lastStartedStep) = pair
                     if (isKeyguardGoingAway && lastStartedStep.to == KeyguardState.LOCKSCREEN) {
-                        startTransitionTo(KeyguardState.GONE)
+                        startTransitionTo(
+                            KeyguardState.GONE,
+                            modeOnCanceled = TransitionModeOnCanceled.RESET,
+                        )
                     }
                 }
         }
@@ -303,20 +306,6 @@
                     startTransitionTo(KeyguardState.GONE)
                 }
             }
-
-            return
-        }
-
-        scope.launch {
-            keyguardInteractor.isKeyguardGoingAway
-                .sample(startedKeyguardTransitionStep, ::Pair)
-                .collect { pair ->
-                    KeyguardWmStateRefactor.assertInLegacyMode()
-                    val (isKeyguardGoingAway, lastStartedStep) = pair
-                    if (isKeyguardGoingAway && lastStartedStep.to == KeyguardState.LOCKSCREEN) {
-                        startTransitionTo(KeyguardState.GONE)
-                    }
-                }
         }
     }
 
@@ -413,7 +402,7 @@
         val TO_OCCLUDED_DURATION = 450.milliseconds
         val TO_AOD_DURATION = 500.milliseconds
         val TO_PRIMARY_BOUNCER_DURATION = DEFAULT_DURATION
-        val TO_GONE_DURATION = DEFAULT_DURATION
+        val TO_GONE_DURATION = 633.milliseconds
         val TO_GLANCEABLE_HUB_DURATION = 1.seconds
     }
 }
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 89148b0..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
                             ) {
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 fa1fe5e..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
@@ -28,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
@@ -59,7 +59,7 @@
         keyguardRootView.repeatWhenAttached {
             repeatOnLifecycle(Lifecycle.State.CREATED) {
                 launch {
-                    if (!migrateClocksToBlueprint()) return@launch
+                    if (!MigrateClocksToBlueprint.isEnabled) return@launch
                     viewModel.currentClock.collect { currentClock ->
                         cleanupClockViews(currentClock, keyguardRootView, viewModel.burnInLayer)
                         addClockViews(currentClock, keyguardRootView)
@@ -68,14 +68,14 @@
                     }
                 }
                 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.currentClock.value?.let {
                             // Weather clock also has hasCustomPositionUpdatedAnimation as true
@@ -92,7 +92,7 @@
                     }
                 }
                 launch {
-                    if (!migrateClocksToBlueprint()) return@launch
+                    if (!MigrateClocksToBlueprint.isEnabled) return@launch
                     viewModel.isAodIconsVisible.collect { isAodIconsVisible ->
                         viewModel.currentClock.value?.let {
                             // Weather clock also has hasCustomPositionUpdatedAnimation as true
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 4d0a25f..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
@@ -43,7 +43,7 @@
         keyguardRootView.repeatWhenAttached {
             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 f60da0e..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
@@ -186,7 +186,7 @@
         coroutineScope = CoroutineScope(applicationScope.coroutineContext + Job())
         disposables += DisposableHandle { coroutineScope.cancel() }
 
-        if (keyguardBottomAreaRefactor()) {
+        if (KeyguardBottomAreaRefactor.isEnabled) {
             quickAffordancesCombinedViewModel.enablePreviewMode(
                 initiallySelectedSlotId =
                     bundle.getString(
@@ -204,7 +204,7 @@
                 shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance,
             )
         }
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled) {
             clockViewModel.shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance
         }
         runBlocking(mainDispatcher) {
@@ -231,7 +231,7 @@
 
             setupKeyguardRootView(previewContext, rootView)
 
-            if (!keyguardBottomAreaRefactor()) {
+            if (!KeyguardBottomAreaRefactor.isEnabled) {
                 setUpBottomArea(rootView)
             }
 
@@ -275,7 +275,7 @@
     }
 
     fun onSlotSelected(slotId: String) {
-        if (keyguardBottomAreaRefactor()) {
+        if (KeyguardBottomAreaRefactor.isEnabled) {
             quickAffordancesCombinedViewModel.onPreviewSlotSelected(slotId = slotId)
         } else {
             bottomAreaViewModel.onPreviewSlotSelected(slotId = slotId)
@@ -286,7 +286,7 @@
         isDestroyed = true
         lockscreenSmartspaceController.disconnect()
         disposables.dispose()
-        if (keyguardBottomAreaRefactor()) {
+        if (KeyguardBottomAreaRefactor.isEnabled) {
             shortcutsBindings.forEach { it.destroy() }
         }
     }
@@ -372,7 +372,7 @@
     @OptIn(ExperimentalCoroutinesApi::class)
     private fun setupKeyguardRootView(previewContext: Context, rootView: FrameLayout) {
         val keyguardRootView = KeyguardRootView(previewContext, null)
-        if (!keyguardBottomAreaRefactor()) {
+        if (!KeyguardBottomAreaRefactor.isEnabled) {
             disposables +=
                 KeyguardRootViewBinder.bind(
                     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() {
@@ -581,7 +582,7 @@
         )
         disposables += DisposableHandle { broadcastDispatcher.unregisterReceiver(receiver) }
 
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled) {
             val layoutChangeListener =
                 View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
                     if (clockController.clock !is DefaultClockController) {
@@ -629,7 +630,7 @@
         }
     }
     private fun onClockChanged() {
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled) {
             return
         }
         coroutineScope.launch {
@@ -676,7 +677,7 @@
     }
 
     private fun updateLargeClock(clock: ClockController) {
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled) {
             return
         }
         clock.largeClock.events.onTargetRegionChanged(
@@ -690,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/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 7847c1c..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,7 +83,7 @@
     }
 
     override fun applyConstraints(constraintSet: ConstraintSet) {
-        if (!Flags.migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled) {
             return
         }
         keyguardClockViewModel.currentClock.value?.let { clock ->
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 c1b0cc6..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,13 +25,11 @@
 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.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.ui.view.SharedNotificationContainer
-import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackViewBinder
 import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
 import dagger.Lazy
@@ -42,31 +40,27 @@
 @Inject
 constructor(
     context: Context,
-    sceneContainerFlags: SceneContainerFlags,
     notificationPanelView: NotificationPanelView,
     sharedNotificationContainer: SharedNotificationContainer,
     sharedNotificationContainerViewModel: SharedNotificationContainerViewModel,
     sharedNotificationContainerBinder: SharedNotificationContainerBinder,
-    notificationStackViewBinder: NotificationStackViewBinder,
     private val largeScreenHeaderHelperLazy: Lazy<LargeScreenHeaderHelper>,
 ) :
     NotificationStackScrollLayoutSection(
         context,
-        sceneContainerFlags,
         notificationPanelView,
         sharedNotificationContainer,
         sharedNotificationContainerViewModel,
         sharedNotificationContainerBinder,
-        notificationStackViewBinder,
     ) {
     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 8323502..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,30 +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.ui.view.SharedNotificationContainer
-import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackViewBinder
 import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
-import com.android.systemui.util.kotlin.DisposableHandles
+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 sharedNotificationContainerBinder: SharedNotificationContainerBinder,
-    private val notificationStackViewBinder: NotificationStackViewBinder,
 ) : KeyguardSection() {
     private val placeHolderId = R.id.nssl_placeholder
-    private val disposableHandles = DisposableHandles()
+    private var disposableHandle: DisposableHandle? = null
 
     /**
      * Align the notification placeholder bottom to the top of either the lock icon or the ambient
@@ -74,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
@@ -90,24 +86,21 @@
     }
 
     override fun bindData(constraintLayout: ConstraintLayout) {
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled) {
             return
         }
 
-        disposableHandles.dispose()
-        disposableHandles +=
+        disposableHandle?.dispose()
+        disposableHandle =
             sharedNotificationContainerBinder.bind(
                 sharedNotificationContainer,
                 sharedNotificationContainerViewModel,
             )
-
-        if (sceneContainerFlags.isEnabled()) {
-            disposableHandles += notificationStackViewBinder.bindWhileAttached()
-        }
     }
 
     override fun removeViews(constraintLayout: ConstraintLayout) {
-        disposableHandles.dispose()
+        disposableHandle?.dispose()
+        disposableHandle = null
         constraintLayout.removeView(placeHolderId)
     }
 }
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 4a705a7..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,12 +23,10 @@
 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.res.R
-import com.android.systemui.scene.shared.flag.SceneContainerFlags
 import com.android.systemui.shade.NotificationPanelView
 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
-import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackViewBinder
 import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
 import javax.inject.Inject
@@ -38,24 +36,20 @@
 @Inject
 constructor(
     context: Context,
-    sceneContainerFlags: SceneContainerFlags,
     notificationPanelView: NotificationPanelView,
     sharedNotificationContainer: SharedNotificationContainer,
     sharedNotificationContainerViewModel: SharedNotificationContainerViewModel,
     sharedNotificationContainerBinder: SharedNotificationContainerBinder,
-    notificationStackViewBinder: NotificationStackViewBinder,
 ) :
     NotificationStackScrollLayoutSection(
         context,
-        sceneContainerFlags,
         notificationPanelView,
         sharedNotificationContainer,
         sharedNotificationContainerViewModel,
         sharedNotificationContainerBinder,
-        notificationStackViewBinder,
     ) {
     override fun applyConstraints(constraintSet: ConstraintSet) {
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled) {
             return
         }
         constraintSet.apply {
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 9c1f077..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,7 +168,7 @@
     private fun clockController(
         provider: Provider<ClockController>?,
     ): Provider<ClockController>? {
-        return if (Flags.migrateClocksToBlueprint()) {
+        return if (MigrateClocksToBlueprint.isEnabled) {
             Provider { keyguardClockViewModel.currentClock.value }
         } else {
             provider
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt
index c409028..cbbb820 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt
@@ -30,8 +30,6 @@
 import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.map
 
 /**
  * Breaks down AOD->LOCKSCREEN transition into discrete steps for corresponding views to consume.
@@ -53,6 +51,8 @@
             to = KeyguardState.LOCKSCREEN,
         )
 
+    private var isShadeExpanded = false
+
     /**
      * Begin the transition from wherever the y-translation value is currently. This helps ensure a
      * smooth transition if a transition in canceled.
@@ -77,22 +77,21 @@
     }
 
     val notificationAlpha: Flow<Float> =
-        combine(
-            shadeInteractor.shadeExpansion.map { it > 0f },
-            shadeInteractor.qsExpansion.map { it > 0f },
-            transitionAnimation.sharedFlow(
-                duration = 500.milliseconds,
-                onStep = { it },
-                onCancel = { 1f },
-            ),
-        ) { isShadeExpanded, isQsExpanded, alpha ->
-            if (isShadeExpanded || isQsExpanded) {
-                // One example of this happening is dragging a notification while pulsing on AOD
-                1f
-            } else {
-                alpha
-            }
-        }
+        transitionAnimation.sharedFlow(
+            duration = 500.milliseconds,
+            onStart = {
+                isShadeExpanded =
+                    shadeInteractor.shadeExpansion.value > 0f ||
+                        shadeInteractor.qsExpansion.value > 0f
+            },
+            onStep = {
+                if (isShadeExpanded) {
+                    1f
+                } else {
+                    it
+                }
+            },
+        )
 
     val shortcutsAlpha: Flow<Float> =
         transitionAnimation.sharedFlow(
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/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 b662109..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
@@ -91,6 +91,7 @@
     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,
@@ -218,6 +219,7 @@
                         goneToAodTransitionViewModel.enterFromTopAnimationAlpha,
                         goneToDozingTransitionViewModel.lockscreenAlpha,
                         goneToDreamingTransitionViewModel.lockscreenAlpha,
+                        goneToLockscreenTransitionViewModel.lockscreenAlpha,
                         lockscreenToAodTransitionViewModel.lockscreenAlpha(viewState),
                         lockscreenToDozingTransitionViewModel.lockscreenAlpha,
                         lockscreenToDreamingTransitionViewModel.lockscreenAlpha,
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/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/tiles/BluetoothTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
index 18d2f30..b0707db 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
@@ -111,7 +111,7 @@
     @Override
     protected void handleClick(@Nullable View view) {
         if (mFeatureFlags.isEnabled(Flags.BLUETOOTH_QS_TILE_DIALOG)) {
-            mDialogViewModel.showDialog(mContext, view);
+            mDialogViewModel.showDialog(view);
         } else {
             // Secondary clicks are header clicks, just toggle.
             final boolean isEnabled = mState.value;
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/dialog/bluetooth/BluetoothAutoOnInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractor.kt
index 1247854..59fc81c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractor.kt
@@ -19,8 +19,6 @@
 import android.util.Log
 import com.android.systemui.dagger.SysUISingleton
 import javax.inject.Inject
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.map
 
 /** Interactor class responsible for interacting with the Bluetooth Auto-On feature. */
 @SysUISingleton
@@ -30,14 +28,10 @@
     private val bluetoothAutoOnRepository: BluetoothAutoOnRepository,
 ) {
 
-    val isEnabled = bluetoothAutoOnRepository.isAutoOn.map { it == ENABLED }.distinctUntilChanged()
+    val isEnabled = bluetoothAutoOnRepository.isAutoOn
 
-    /**
-     * Checks if the auto on value is present in the repository.
-     *
-     * @return `true` if a value is present (i.e, the feature is enabled by the Bluetooth server).
-     */
-    suspend fun isValuePresent(): Boolean = bluetoothAutoOnRepository.isValuePresent()
+    /** Checks if the auto on feature is supported. */
+    suspend fun isAutoOnSupported(): Boolean = bluetoothAutoOnRepository.isAutoOnSupported()
 
     /**
      * Sets enabled or disabled based on the provided value.
@@ -45,17 +39,14 @@
      * @param value `true` to enable the feature, `false` to disable it.
      */
     suspend fun setEnabled(value: Boolean) {
-        if (!isValuePresent()) {
+        if (!isAutoOnSupported()) {
             Log.e(TAG, "Trying to set toggle value while feature not available.")
         } else {
-            val newValue = if (value) ENABLED else DISABLED
-            bluetoothAutoOnRepository.setAutoOn(newValue)
+            bluetoothAutoOnRepository.setAutoOn(value)
         }
     }
 
     companion object {
         private const val TAG = "BluetoothAutoOnInteractor"
-        const val DISABLED = 0
-        const val ENABLED = 1
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepository.kt
index f97fc38..9ee582a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepository.kt
@@ -16,22 +16,23 @@
 
 package com.android.systemui.qs.tiles.dialog.bluetooth
 
+import android.bluetooth.BluetoothAdapter
+import android.util.Log
+import com.android.settingslib.bluetooth.BluetoothCallback
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.user.data.repository.UserRepository
-import com.android.systemui.util.settings.SecureSettings
-import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.withContext
@@ -44,61 +45,87 @@
 class BluetoothAutoOnRepository
 @Inject
 constructor(
-    private val secureSettings: SecureSettings,
-    private val userRepository: UserRepository,
+    localBluetoothManager: LocalBluetoothManager?,
+    private val bluetoothAdapter: BluetoothAdapter?,
     @Application private val coroutineScope: CoroutineScope,
     @Background private val backgroundDispatcher: CoroutineDispatcher,
 ) {
 
-    // Flow representing the auto on setting value for the current user
-    @OptIn(ExperimentalCoroutinesApi::class)
-    internal val isAutoOn: StateFlow<Int> =
-        userRepository.selectedUserInfo
-            .flatMapLatest { userInfo ->
-                secureSettings
-                    .observerFlow(userInfo.id, SETTING_NAME)
-                    .onStart { emit(Unit) }
-                    .map { secureSettings.getIntForUser(SETTING_NAME, UNSET, userInfo.id) }
-            }
-            .distinctUntilChanged()
-            .flowOn(backgroundDispatcher)
-            .stateIn(
-                coroutineScope,
-                SharingStarted.WhileSubscribed(replayExpirationMillis = 0),
-                UNSET
-            )
+    // Flow representing the auto on state for the current user
+    internal val isAutoOn: Flow<Boolean> =
+        localBluetoothManager?.eventManager?.let { eventManager ->
+            conflatedCallbackFlow {
+                    val listener =
+                        object : BluetoothCallback {
+                            override fun onAutoOnStateChanged(autoOnState: Int) {
+                                super.onAutoOnStateChanged(autoOnState)
+                                if (
+                                    autoOnState == BluetoothAdapter.AUTO_ON_STATE_ENABLED ||
+                                        autoOnState == BluetoothAdapter.AUTO_ON_STATE_DISABLED
+                                ) {
+                                    trySendWithFailureLogging(
+                                        autoOnState == BluetoothAdapter.AUTO_ON_STATE_ENABLED,
+                                        TAG,
+                                        "onAutoOnStateChanged"
+                                    )
+                                }
+                            }
+                        }
+                    eventManager.registerCallback(listener)
+                    awaitClose { eventManager.unregisterCallback(listener) }
+                }
+                .onStart { emit(isAutoOnEnabled()) }
+                .flowOn(backgroundDispatcher)
+                .stateIn(
+                    coroutineScope,
+                    SharingStarted.WhileSubscribed(replayExpirationMillis = 0),
+                    initialValue = false
+                )
+        }
+            ?: flowOf(false)
 
     /**
-     * Checks if the auto on setting value is ever set for the current user.
+     * Checks if the auto on feature is supported for the current user.
      *
-     * @return `true` if the setting value is not UNSET, `false` otherwise.
+     * @throws Exception if an error occurs while checking auto-on support.
      */
-    suspend fun isValuePresent(): Boolean =
+    suspend fun isAutoOnSupported(): Boolean =
         withContext(backgroundDispatcher) {
-            secureSettings.getIntForUser(
-                SETTING_NAME,
-                UNSET,
-                userRepository.getSelectedUserInfo().id
-            ) != UNSET
+            try {
+                bluetoothAdapter?.isAutoOnSupported ?: false
+            } catch (e: Exception) {
+                // Server could throw TimeoutException, InterruptedException or ExecutionException
+                Log.e(TAG, "Error calling isAutoOnSupported", e)
+                false
+            }
         }
 
-    /**
-     * Sets the Bluetooth Auto-On setting value for the current user.
-     *
-     * @param value The new setting value to be applied.
-     */
-    suspend fun setAutoOn(value: Int) {
+    /** Sets the Bluetooth Auto-On for the current user. */
+    suspend fun setAutoOn(value: Boolean) {
         withContext(backgroundDispatcher) {
-            secureSettings.putIntForUser(
-                SETTING_NAME,
-                value,
-                userRepository.getSelectedUserInfo().id
-            )
+            try {
+                bluetoothAdapter?.setAutoOnEnabled(value)
+            } catch (e: Exception) {
+                // Server could throw IllegalStateException, TimeoutException, InterruptedException
+                // or ExecutionException
+                Log.e(TAG, "Error calling setAutoOnEnabled", e)
+            }
         }
     }
 
-    companion object {
-        const val SETTING_NAME = "bluetooth_automatic_turn_on"
-        const val UNSET = -1
+    private suspend fun isAutoOnEnabled() =
+        withContext(backgroundDispatcher) {
+            try {
+                bluetoothAdapter?.isAutoOnEnabled ?: false
+            } catch (e: Exception) {
+                // Server could throw IllegalStateException, TimeoutException, InterruptedException
+                // or ExecutionException
+                Log.e(TAG, "Error calling isAutoOnEnabled", e)
+                false
+            }
+        }
+
+    private companion object {
+        const val TAG = "BluetoothAutoOnRepository"
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogDelegate.kt
index 9d53703..a8d9e78 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogDelegate.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.qs.tiles.dialog.bluetooth
 
-import android.content.Context
 import android.os.Bundle
 import android.view.LayoutInflater
 import android.view.View
@@ -58,7 +57,6 @@
 class BluetoothTileDialogDelegate
 @AssistedInject
 internal constructor(
-    @Assisted private val context: Context,
     @Assisted private val initialUiProperties: BluetoothTileDialogViewModel.UiProperties,
     @Assisted private val cachedContentHeight: Int,
     @Assisted private val bluetoothToggleInitialValue: Boolean,
@@ -69,11 +67,8 @@
     private val uiEventLogger: UiEventLogger,
     private val logger: BluetoothTileDialogLogger,
     private val systemuiDialogFactory: SystemUIDialog.Factory,
-    mainLayoutInflater: LayoutInflater,
 ) : SystemUIDialog.Delegate {
 
-    private val layoutInflater = mainLayoutInflater.cloneInContext(context)
-
     private val mutableBluetoothStateToggle: MutableStateFlow<Boolean> =
         MutableStateFlow(bluetoothToggleInitialValue)
     internal val bluetoothStateToggle
@@ -102,7 +97,6 @@
     @AssistedFactory
     internal interface Factory {
         fun create(
-            context: Context,
             initialUiProperties: BluetoothTileDialogViewModel.UiProperties,
             cachedContentHeight: Int,
             bluetoothEnabled: Boolean,
@@ -112,16 +106,15 @@
     }
 
     override fun createDialog(): SystemUIDialog {
-        val dialog = systemuiDialogFactory.create(this, context)
-
-        return dialog
+        return systemuiDialogFactory.create(this)
     }
 
     override fun onCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
         SystemUIDialog.registerDismissListener(dialog, dismissListener)
         uiEventLogger.log(BluetoothTileDialogUiEvent.BLUETOOTH_TILE_DIALOG_SHOWN)
+        val context = dialog.context
 
-        layoutInflater.inflate(R.layout.bluetooth_tile_dialog, null).apply {
+        LayoutInflater.from(context).inflate(R.layout.bluetooth_tile_dialog, null).apply {
             accessibilityPaneTitle = context.getText(R.string.accessibility_desc_quick_settings)
             dialog.setContentView(this)
         }
@@ -201,7 +194,7 @@
             setEnabled(true)
             alpha = ENABLED_ALPHA
         }
-        getSubtitleTextView(dialog).text = context.getString(uiProperties.subTitleResId)
+        getSubtitleTextView(dialog).text = dialog.context.getString(uiProperties.subTitleResId)
         getAutoOnToggleView(dialog).visibility = uiProperties.autoOnToggleVisibility
     }
 
@@ -215,7 +208,7 @@
             setEnabled(true)
             alpha = ENABLED_ALPHA
         }
-        getAutoOnToggleInfoTextView(dialog).text = context.getString(infoResId)
+        getAutoOnToggleInfoTextView(dialog).text = dialog.context.getString(infoResId)
     }
 
     private fun setupToggle(dialog: SystemUIDialog) {
@@ -288,7 +281,7 @@
 
     private fun setupRecyclerView(dialog: SystemUIDialog) {
         getDeviceListView(dialog).apply {
-            layoutManager = LinearLayoutManager(context)
+            layoutManager = LinearLayoutManager(dialog.context)
             adapter = deviceItemAdapter
         }
     }
@@ -343,7 +336,9 @@
         private val asyncListDiffer = AsyncListDiffer(this, diffUtilCallback)
 
         override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DeviceItemViewHolder {
-            val view = layoutInflater.inflate(R.layout.bluetooth_device_item, parent, false)
+            val view =
+                LayoutInflater.from(parent.context)
+                    .inflate(R.layout.bluetooth_device_item, parent, false)
             return DeviceItemViewHolder(view)
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt
index e4f3c19..fd624d2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.qs.tiles.dialog.bluetooth
 
-import android.content.Context
 import android.content.Intent
 import android.content.SharedPreferences
 import android.os.Bundle
@@ -29,7 +28,6 @@
 import androidx.annotation.VisibleForTesting
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.internal.logging.UiEventLogger
-import com.android.settingslib.flags.Flags.bluetoothQsTileDialogAutoOnToggle
 import com.android.systemui.Prefs
 import com.android.systemui.animation.DialogCuj
 import com.android.systemui.animation.DialogTransitionAnimator
@@ -78,19 +76,19 @@
     /**
      * Shows the dialog.
      *
-     * @param context The context in which the dialog is displayed.
      * @param view The view from which the dialog is shown.
      */
     @kotlinx.coroutines.ExperimentalCoroutinesApi
-    fun showDialog(context: Context, view: View?) {
+    fun showDialog(view: View?) {
         cancelJob()
 
         job =
             coroutineScope.launch(mainDispatcher) {
                 var updateDeviceItemJob: Job?
                 var updateDialogUiJob: Job? = null
-                val dialogDelegate = createBluetoothTileDialog(context)
+                val dialogDelegate = createBluetoothTileDialog()
                 val dialog = dialogDelegate.createDialog()
+                val context = dialog.context
 
                 view?.let {
                     dialogTransitionAnimator.showFromView(
@@ -213,7 +211,7 @@
             }
     }
 
-    private suspend fun createBluetoothTileDialog(context: Context): BluetoothTileDialogDelegate {
+    private suspend fun createBluetoothTileDialog(): BluetoothTileDialogDelegate {
         val cachedContentHeight =
             withContext(backgroundDispatcher) {
                 sharedPreferences.getInt(
@@ -223,7 +221,6 @@
             }
 
         return bluetoothDialogDelegateFactory.create(
-            context,
             UiProperties.build(
                 bluetoothStateInteractor.isBluetoothEnabled,
                 isAutoOnToggleFeatureAvailable()
@@ -277,7 +274,7 @@
 
     @VisibleForTesting
     internal suspend fun isAutoOnToggleFeatureAvailable() =
-        bluetoothQsTileDialogAutoOnToggle() && bluetoothAutoOnInteractor.isValuePresent()
+        bluetoothAutoOnInteractor.isAutoOnSupported()
 
     companion object {
         private const val INTERACTION_JANK_TAG = "bluetooth_tile_dialog"
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/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/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/appclips/AppClipsService.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java
index 06c0b8b..c89b476 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java
@@ -33,6 +33,7 @@
 import android.content.Intent.CaptureContentForNoteStatusCodes;
 import android.content.res.Resources;
 import android.os.IBinder;
+import android.util.Log;
 
 import androidx.annotation.Nullable;
 
@@ -58,6 +59,8 @@
  */
 public class AppClipsService extends Service {
 
+  private static final String TAG = AppClipsService.class.getSimpleName();
+
     @Application private final Context mContext;
     private final FeatureFlags mFeatureFlags;
     private final Optional<Bubbles> mOptionalBubbles;
@@ -77,14 +80,22 @@
 
     private boolean checkIndependentVariables() {
         if (!mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)) {
+            Log.d(TAG, "Feature flag disabled");
             return false;
         }
 
         if (mOptionalBubbles.isEmpty()) {
+            Log.d(TAG, "Bubbles not available");
             return false;
         }
 
-        return isComponentValid();
+        if (isComponentValid()) {
+            Log.d(TAG, "checkIndependentVariables returned true");
+            return true;
+        }
+
+        Log.d(TAG, "checkIndependentVariables returned false");
+        return false;
     }
 
     private boolean isComponentValid() {
@@ -93,12 +104,27 @@
             componentName = ComponentName.unflattenFromString(
                     mContext.getString(R.string.config_screenshotAppClipsActivityComponent));
         } catch (Resources.NotFoundException e) {
+            Log.d(TAG, "AppClips activity component resource not defined");
             return false;
         }
 
-        return componentName != null
-                && !componentName.getPackageName().isEmpty()
-                && !componentName.getClassName().isEmpty();
+        if (componentName == null) {
+            Log.d(TAG, "AppClips component name not defined");
+            return false;
+        }
+
+        if (componentName.getPackageName().isEmpty()) {
+            Log.d(TAG, "AppClips component package name is empty");
+            return false;
+        }
+
+        if (componentName.getClassName().isEmpty()) {
+            Log.d(TAG, "AppClips component class name is empty");
+            return false;
+        }
+
+        Log.d(TAG, "isComponentValid returned true");
+        return true;
     }
 
     @Nullable
@@ -107,24 +133,39 @@
         return new IAppClipsService.Stub() {
             @Override
             public boolean canLaunchCaptureContentActivityForNote(int taskId) {
-                return canLaunchCaptureContentActivityForNoteInternal(taskId)
-                        == CAPTURE_CONTENT_FOR_NOTE_SUCCESS;
+                if (canLaunchCaptureContentActivityForNoteInternal(taskId)
+                        == CAPTURE_CONTENT_FOR_NOTE_SUCCESS) {
+                    Log.d(TAG, String.format("Can launch AppClips returned true for %d", taskId));
+                    return true;
+                }
+
+                Log.d(TAG, String.format("Can launch AppClips returned false for %d", taskId));
+                return false;
             }
 
             @Override
             @CaptureContentForNoteStatusCodes
             public int canLaunchCaptureContentActivityForNoteInternal(int taskId) {
                 if (!mAreTaskAndTimeIndependentPrerequisitesMet) {
+                    Log.d(TAG,
+                        String.format("Task (%d) and time independent prereqs not met", taskId));
                     return CAPTURE_CONTENT_FOR_NOTE_FAILED;
                 }
 
                 if (!mOptionalBubbles.get().isAppBubbleTaskId(taskId)) {
+                    Log.d(TAG, String.format("Taskid %d is not app bubble task", taskId));
                     return CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED;
                 }
 
-                return mDevicePolicyManager.getScreenCaptureDisabled(null)
-                        ? CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN
-                        : CAPTURE_CONTENT_FOR_NOTE_SUCCESS;
+                if (mDevicePolicyManager.getScreenCaptureDisabled(null)) {
+                    Log.d(TAG,
+                        String.format("Screen capture disabled by admin, taskId %d", taskId));
+                    return CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN;
+                }
+
+                Log.d(TAG,
+                    String.format("Can launch AppClips (internal) successful for %d", taskId));
+                return CAPTURE_CONTENT_FOR_NOTE_SUCCESS;
             }
         };
     }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 9cb920a..f928ccc 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;
@@ -193,6 +193,7 @@
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableView;
 import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
+import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor;
 import com.android.systemui.statusbar.notification.stack.AmbientState;
 import com.android.systemui.statusbar.notification.stack.AnimationProperties;
 import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
@@ -1018,7 +1019,7 @@
                 instantCollapse();
             } else {
                 mView.animate().cancel();
-                if (!migrateClocksToBlueprint()) {
+                if (!MigrateClocksToBlueprint.isEnabled()) {
                     mView.animate()
                             .alpha(0f)
                             .setStartDelay(0)
@@ -1075,7 +1076,7 @@
         mQsController.init();
         mShadeHeadsUpTracker.addTrackingHeadsUpListener(
                 mNotificationStackScrollLayoutController::setTrackingHeadsUp);
-        if (!keyguardBottomAreaRefactor()) {
+        if (!KeyguardBottomAreaRefactor.isEnabled()) {
             setKeyguardBottomArea(mView.findViewById(R.id.keyguard_bottom_area));
         }
 
@@ -1154,7 +1155,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 +1166,7 @@
         // Lockscreen->Dreaming
         collectFlow(mView, mKeyguardTransitionInteractor.getLockscreenToDreamingTransition(),
                 mLockscreenToDreamingTransition, mMainDispatcher);
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled()) {
             collectFlow(mView, mLockscreenToDreamingTransitionViewModel.getLockscreenAlpha(),
                     setDreamLockscreenTransitionAlpha(mNotificationStackScrollLayoutController),
                     mMainDispatcher);
@@ -1177,7 +1178,7 @@
         // Gone->Dreaming
         collectFlow(mView, mKeyguardTransitionInteractor.getGoneToDreamingTransition(),
                 mGoneToDreamingTransition, mMainDispatcher);
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled()) {
             collectFlow(mView, mGoneToDreamingTransitionViewModel.getLockscreenAlpha(),
                     setTransitionAlpha(mNotificationStackScrollLayoutController), mMainDispatcher);
         }
@@ -1188,7 +1189,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 +1197,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 +1281,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 +1333,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 +1360,7 @@
         }
         updateClockAppearance();
         mQsController.updateQsState();
-        if (!migrateClocksToBlueprint() && !FooterViewRefactor.isEnabled()) {
+        if (!MigrateClocksToBlueprint.isEnabled() && !FooterViewRefactor.isEnabled()) {
             mNotificationStackScrollLayoutController.updateFooter();
         }
     }
@@ -1391,7 +1392,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 +1431,7 @@
 
         updateViewControllers(userAvatarView, keyguardUserSwitcherView);
 
-        if (!keyguardBottomAreaRefactor()) {
+        if (!KeyguardBottomAreaRefactor.isEnabled()) {
             // Update keyguard bottom area
             int index = mView.indexOfChild(mKeyguardBottomArea);
             mView.removeView(mKeyguardBottomArea);
@@ -1449,7 +1450,7 @@
         mStatusBarStateListener.onDozeAmountChanged(mStatusBarStateController.getDozeAmount(),
                 mStatusBarStateController.getInterpolatedDozeAmount());
 
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled()) {
             mKeyguardStatusViewController.setKeyguardStatusViewVisibility(
                     mBarState,
                     false,
@@ -1471,7 +1472,7 @@
                     mBarState);
         }
 
-        if (!keyguardBottomAreaRefactor()) {
+        if (!KeyguardBottomAreaRefactor.isEnabled()) {
             setKeyguardBottomAreaVisibility(mBarState, false);
         }
 
@@ -1480,14 +1481,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 +1514,7 @@
     }
 
     private void updateMaxDisplayedNotifications(boolean recompute) {
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled()) {
             return;
         }
 
@@ -1630,7 +1631,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 +1672,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 +1684,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 +1741,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 +1759,7 @@
 
     private void updateKeyguardStatusViewAlignment(boolean animate) {
         boolean shouldBeCentered = shouldKeyguardStatusViewBeCentered();
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled()) {
             mKeyguardInteractor.setClockShouldBeCentered(shouldBeCentered);
             return;
         }
@@ -1941,7 +1942,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 +2499,7 @@
         }
 
         if (!mKeyguardBypassController.getBypassEnabled()) {
-            if (migrateClocksToBlueprint() && !mSplitShadeEnabled) {
+            if (MigrateClocksToBlueprint.isEnabled() && !mSplitShadeEnabled) {
                 return (int) mKeyguardInteractor.getNotificationContainerBounds()
                         .getValue().getTop();
             }
@@ -2531,7 +2532,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 +2713,7 @@
             return;
         }
 
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled()) {
             float alpha = 1f;
             if (mClosingWithAlphaFadeOut && !mExpandingFromHeadsUp
                 && !mHeadsUpManager.hasPinnedHeadsUp()) {
@@ -2748,7 +2749,7 @@
     }
 
     private void updateKeyguardBottomAreaAlpha() {
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled()) {
             return;
         }
         if (mIsOcclusionTransitionRunning) {
@@ -2766,7 +2767,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 +2979,7 @@
     }
 
     private void updateDozingVisibilities(boolean animate) {
-        if (keyguardBottomAreaRefactor()) {
+        if (KeyguardBottomAreaRefactor.isEnabled()) {
             mKeyguardInteractor.setAnimateDozingTransitions(animate);
         } else {
             mKeyguardBottomAreaInteractor.setAnimateDozingTransitions(animate);
@@ -2990,7 +2991,7 @@
 
     @Override
     public void onScreenTurningOn() {
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled()) {
             mKeyguardStatusViewController.dozeTimeTick();
         }
     }
@@ -3189,7 +3190,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 +3246,7 @@
 
     public void dozeTimeTick() {
         mLockIconViewController.dozeTimeTick();
-        if (!migrateClocksToBlueprint()) {
+        if (!MigrateClocksToBlueprint.isEnabled()) {
             mKeyguardStatusViewController.dozeTimeTick();
         }
         if (mInterpolatedDarkAmount > 0) {
@@ -3324,7 +3325,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 +3349,7 @@
         @Override
         public void startFoldToAodAnimation(Runnable startAction, Runnable endAction,
                 Runnable cancelAction) {
-            if (migrateClocksToBlueprint()) {
+            if (MigrateClocksToBlueprint.isEnabled()) {
                 return;
             }
             final ViewPropertyAnimator viewAnimator = mView.animate();
@@ -3386,7 +3387,7 @@
         /** Cancels fold to AOD transition and resets view state. */
         @Override
         public void cancelFoldToAodAnimation() {
-            if (migrateClocksToBlueprint()) {
+            if (MigrateClocksToBlueprint.isEnabled()) {
                 return;
             }
             cancelAnimation();
@@ -4381,6 +4382,10 @@
 
         @Override
         public void onHeadsUpPinned(NotificationEntry entry) {
+            if (NotificationsHeadsUpRefactor.isEnabled()) {
+                return;
+            }
+
             if (!isKeyguardShowing()) {
                 mNotificationStackScrollLayoutController.generateHeadsUpAnimation(entry, true);
             }
@@ -4388,6 +4393,9 @@
 
         @Override
         public void onHeadsUpUnPinned(NotificationEntry entry) {
+            if (NotificationsHeadsUpRefactor.isEnabled()) {
+                return;
+            }
 
             // When we're unpinning the notification via active edge they remain heads-upped,
             // we need to make sure that an animation happens in this case, otherwise the
@@ -4460,7 +4468,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 +4477,7 @@
                 }
             }
 
-            if (!migrateClocksToBlueprint()) {
+            if (!MigrateClocksToBlueprint.isEnabled()) {
                 mKeyguardStatusViewController.setKeyguardStatusViewVisibility(
                         statusBarState,
                         keyguardFadingAway,
@@ -4477,7 +4485,7 @@
                         mBarState);
             }
 
-            if (!keyguardBottomAreaRefactor()) {
+            if (!KeyguardBottomAreaRefactor.isEnabled()) {
                 setKeyguardBottomAreaVisibility(statusBarState, goingToFullShade);
             }
 
@@ -4582,7 +4590,7 @@
         setDozing(true /* dozing */, false /* animate */);
         mStatusBarStateController.setUpcomingState(KEYGUARD);
 
-        if (migrateClocksToBlueprint()) {
+        if (MigrateClocksToBlueprint.isEnabled()) {
             mStatusBarStateController.setState(KEYGUARD);
         } else {
             mStatusBarStateListener.onStateChanged(KEYGUARD);
@@ -4645,7 +4653,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 +4754,7 @@
                 stackScroller.setMaxAlphaForKeyguard(alpha, "NPVC.setTransitionAlpha()");
             }
 
-            if (keyguardBottomAreaRefactor()) {
+            if (KeyguardBottomAreaRefactor.isEnabled()) {
                 mKeyguardInteractor.setAlpha(alpha);
             } else {
                 mKeyguardBottomAreaInteractor.setAlpha(alpha);
@@ -4765,7 +4773,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 +4815,7 @@
          */
         @Override
         public boolean onInterceptTouchEvent(MotionEvent event) {
-            if (migrateClocksToBlueprint() && !mUseExternalTouch) {
+            if (MigrateClocksToBlueprint.isEnabled() && !mUseExternalTouch) {
                 return false;
             }
 
@@ -4878,7 +4886,7 @@
 
             switch (event.getActionMasked()) {
                 case MotionEvent.ACTION_DOWN:
-                    if (!migrateClocksToBlueprint()) {
+                    if (!MigrateClocksToBlueprint.isEnabled()) {
                         mCentralSurfaces.userActivity();
                     }
                     mAnimatingOnDown = mHeightAnimator != null && !mIsSpringBackAnimation;
@@ -4979,7 +4987,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 8dbcead..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;
@@ -1778,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);
                     }
                 }
@@ -1823,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/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
index bc60c83..cde45f2 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
@@ -66,7 +66,7 @@
     val isAnyExpanded: StateFlow<Boolean>
 
     /** The amount [0-1] that the shade has been opened. */
-    val shadeExpansion: Flow<Float>
+    val shadeExpansion: StateFlow<Float>
 
     /**
      * The amount [0-1] QS has been opened. Normal shade with notifications (QQS) visible will
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
index e9bb4c6..5fbd2cf 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
@@ -29,7 +29,7 @@
     private val inactiveFlowBoolean = MutableStateFlow(false)
     private val inactiveFlowFloat = MutableStateFlow(0f)
     override val isShadeEnabled: StateFlow<Boolean> = inactiveFlowBoolean
-    override val shadeExpansion: Flow<Float> = inactiveFlowFloat
+    override val shadeExpansion: StateFlow<Float> = inactiveFlowFloat
     override val qsExpansion: StateFlow<Float> = inactiveFlowFloat
     override val isQsExpanded: StateFlow<Boolean> = inactiveFlowBoolean
     override val isQsBypassingShade: Flow<Boolean> = inactiveFlowBoolean
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt
index 421a761..ac881b5 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt
@@ -50,7 +50,7 @@
      * The amount [0-1] that the shade has been opened. Uses stateIn to avoid redundant calculations
      * in downstream flows.
      */
-    override val shadeExpansion: Flow<Float> =
+    override val shadeExpansion: StateFlow<Float> =
         combine(
                 repository.lockscreenShadeExpansion,
                 keyguardRepository.statusBarState,
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt
index 7785eda..7f35f17 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt
@@ -49,7 +49,9 @@
     sharedNotificationContainerInteractor: SharedNotificationContainerInteractor,
     shadeRepository: ShadeRepository,
 ) : BaseShadeInteractor {
-    override val shadeExpansion: Flow<Float> = sceneBasedExpansion(sceneInteractor, Scenes.Shade)
+    override val shadeExpansion: StateFlow<Float> =
+        sceneBasedExpansion(sceneInteractor, Scenes.Shade)
+            .stateIn(scope, SharingStarted.Eagerly, 0f)
 
     private val sceneBasedQsExpansion = sceneBasedExpansion(sceneInteractor, Scenes.QuickSettings)
 
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/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/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/KeyguardCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt
index 0c69a65..8531eaa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt
@@ -22,6 +22,7 @@
 import android.provider.Settings
 import androidx.annotation.VisibleForTesting
 import com.android.systemui.Dumpable
+import com.android.systemui.Flags.notificationMinimalismPrototype
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dump.DumpManager
@@ -59,6 +60,7 @@
 import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.conflate
 import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onEach
@@ -260,8 +262,11 @@
         }
     }
 
-    private suspend fun trackUnseenFilterSettingChanges() {
-        secureSettings
+    private fun unseenFeatureEnabled(): Flow<Boolean> {
+        if (notificationMinimalismPrototype()) {
+            return flowOf(true)
+        }
+        return secureSettings
             // emit whenever the setting has changed
             .observerFlow(
                 UserHandle.USER_ALL,
@@ -283,17 +288,20 @@
             // only track the most recent emission, if events are happening faster than they can be
             // consumed
             .conflate()
-            .collectLatest { setting ->
-                // update local field and invalidate if necessary
-                if (setting != unseenFilterEnabled) {
-                    unseenFilterEnabled = setting
-                    unseenNotifFilter.invalidateList("unseen setting changed")
-                }
-                // if the setting is enabled, then start tracking and filtering unseen notifications
-                if (setting) {
-                    trackSeenNotifications()
-                }
+    }
+
+    private suspend fun trackUnseenFilterSettingChanges() {
+        unseenFeatureEnabled().collectLatest { setting ->
+            // update local field and invalidate if necessary
+            if (setting != unseenFilterEnabled) {
+                unseenFilterEnabled = setting
+                unseenNotifFilter.invalidateList("unseen setting changed")
             }
+            // if the setting is enabled, then start tracking and filtering unseen notifications
+            if (setting) {
+                trackSeenNotifications()
+            }
+        }
     }
 
     private val collectionListener =
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/data/NotificationDataLayerModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationDataLayerModule.kt
index e5e5292..2b0d2aa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationDataLayerModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationDataLayerModule.kt
@@ -15,8 +15,8 @@
  */
 package com.android.systemui.statusbar.notification.data
 
-import com.android.systemui.statusbar.notification.data.repository.HeadsUpNotificationRepository
-import com.android.systemui.statusbar.notification.data.repository.HeadsUpNotificationRepositoryImpl
+import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository
+import com.android.systemui.statusbar.phone.HeadsUpManagerPhone
 import dagger.Binds
 import dagger.Module
 
@@ -27,8 +27,5 @@
         ]
 )
 interface NotificationDataLayerModule {
-    @Binds
-    fun bindHeadsUpNotificationRepository(
-        impl: HeadsUpNotificationRepositoryImpl
-    ): HeadsUpNotificationRepository
+    @Binds fun bindHeadsUpNotificationRepository(impl: HeadsUpManagerPhone): HeadsUpRepository
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpNotificationRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpNotificationRepository.kt
deleted file mode 100644
index d60ee98..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpNotificationRepository.kt
+++ /dev/null
@@ -1,59 +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.statusbar.notification.data.repository
-
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
-import com.android.systemui.statusbar.notification.collection.NotificationEntry
-import com.android.systemui.statusbar.policy.HeadsUpManager
-import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener
-import javax.inject.Inject
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.Flow
-
-class HeadsUpNotificationRepositoryImpl
-@Inject
-constructor(
-    headsUpManager: HeadsUpManager,
-) : HeadsUpNotificationRepository {
-    override val hasPinnedHeadsUp: Flow<Boolean> = conflatedCallbackFlow {
-        val listener =
-            object : OnHeadsUpChangedListener {
-                override fun onHeadsUpPinnedModeChanged(inPinnedMode: Boolean) {
-                    trySend(headsUpManager.hasPinnedHeadsUp())
-                }
-
-                override fun onHeadsUpPinned(entry: NotificationEntry?) {
-                    trySend(headsUpManager.hasPinnedHeadsUp())
-                }
-
-                override fun onHeadsUpUnPinned(entry: NotificationEntry?) {
-                    trySend(headsUpManager.hasPinnedHeadsUp())
-                }
-
-                override fun onHeadsUpStateChanged(entry: NotificationEntry, isHeadsUp: Boolean) {
-                    trySend(headsUpManager.hasPinnedHeadsUp())
-                }
-            }
-        trySend(headsUpManager.hasPinnedHeadsUp())
-        headsUpManager.addListener(listener)
-        awaitClose { headsUpManager.removeListener(listener) }
-    }
-}
-
-interface HeadsUpNotificationRepository {
-    val hasPinnedHeadsUp: Flow<Boolean>
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpRepository.kt
new file mode 100644
index 0000000..ed8c056
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpRepository.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.statusbar.notification.data.repository
+
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * A repository of currently displayed heads up notifications.
+ *
+ * This repository serves as a boundary between the
+ * [com.android.systemui.statusbar.policy.HeadsUpManager] and the modern notifications presentation
+ * codebase.
+ */
+interface HeadsUpRepository {
+
+    /**
+     * True if we are exiting the headsUp pinned mode, and some notifications might still be
+     * animating out. This is used to keep the touchable regions in a reasonable state.
+     */
+    val headsUpAnimatingAway: Flow<Boolean>
+
+    /** The heads up row that should be displayed on top. */
+    val topHeadsUpRow: Flow<HeadsUpRowRepository?>
+
+    /** Set of currently active top-level heads up rows to be displayed. */
+    val activeHeadsUpRows: Flow<Set<HeadsUpRowRepository>>
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpRowRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpRowRepository.kt
new file mode 100644
index 0000000..7b40812
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpRowRepository.kt
@@ -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.systemui.statusbar.notification.data.repository
+
+import com.android.systemui.statusbar.notification.shared.HeadsUpRowKey
+import kotlinx.coroutines.flow.StateFlow
+
+/** Representation of a top-level heads up row. */
+interface HeadsUpRowRepository : HeadsUpRowKey {
+    /**
+     * The key for this notification. Guaranteed to be immutable and unique.
+     *
+     * @see com.android.systemui.statusbar.notification.collection.NotificationEntry.getKey
+     */
+    val key: String
+
+    /** A key to identify this row in the view hierarchy. */
+    val elementKey: Any
+
+    /** Whether this notification is "pinned", meaning that it should stay on top of the screen. */
+    val isPinned: StateFlow<Boolean>
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt
index 5c8f354..d1dd7b5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt
@@ -14,14 +14,59 @@
  * limitations under the License.
  */
 
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
 package com.android.systemui.statusbar.notification.domain.interactor
 
-import com.android.systemui.statusbar.notification.data.repository.HeadsUpNotificationRepository
+import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository
+import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository
+import com.android.systemui.statusbar.notification.shared.HeadsUpRowKey
 import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
 
-class HeadsUpNotificationInteractor @Inject constructor(repository: HeadsUpNotificationRepository) {
+class HeadsUpNotificationInteractor @Inject constructor(repository: HeadsUpRepository) {
+
+    val topHeadsUpRow: Flow<HeadsUpRowKey?> = repository.topHeadsUpRow
+
+    /** Set of currently pinned top-level heads up rows to be displayed. */
+    val pinnedHeadsUpRows: Flow<Set<HeadsUpRowKey>> =
+        repository.activeHeadsUpRows.flatMapLatest { repositories ->
+            if (repositories.isNotEmpty()) {
+                val toCombine: List<Flow<Pair<HeadsUpRowRepository, Boolean>>> =
+                    repositories.map { repo -> repo.isPinned.map { isPinned -> repo to isPinned } }
+                combine(toCombine) { pairs ->
+                    pairs.filter { (_, isPinned) -> isPinned }.map { (repo, _) -> repo }.toSet()
+                }
+            } else {
+                // if the set is empty, there are no flows to combine
+                flowOf(emptySet())
+            }
+        }
+
+    /** Are there any pinned heads up rows to display? */
+    val hasPinnedRows: Flow<Boolean> =
+        repository.activeHeadsUpRows.flatMapLatest { rows ->
+            if (rows.isNotEmpty()) {
+                combine(rows.map { it.isPinned }) { pins -> pins.any { it } }
+            } else {
+                // if the set is empty, there are no flows to combine
+                flowOf(false)
+            }
+        }
+
     val isHeadsUpOrAnimatingAway: Flow<Boolean> =
-        // TODO(b/296118689): Needs to include the animating away state.
-        repository.hasPinnedHeadsUp
+        combine(hasPinnedRows, repository.headsUpAnimatingAway) { hasPinnedRows, animatingAway ->
+            hasPinnedRows || animatingAway
+        }
+
+    fun headsUpRow(key: HeadsUpRowKey): HeadsUpRowInteractor =
+        HeadsUpRowInteractor(key as HeadsUpRowRepository)
+    fun elementKeyFor(key: HeadsUpRowKey) = (key as HeadsUpRowRepository).elementKey
 }
+
+class HeadsUpRowInteractor(repository: HeadsUpRowRepository)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
index f792898..adcbbfb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
@@ -58,6 +58,7 @@
 
     private FooterViewButton mClearAllButton;
     private FooterViewButton mManageOrHistoryButton;
+    private boolean mShouldBeHidden;
     private boolean mShowHistory;
     // String cache, for performance reasons.
     // Reading them from a Resources object can be quite slow sometimes.
@@ -110,6 +111,20 @@
         setSecondaryVisible(visible, animate, onAnimationEnded);
     }
 
+    /** See {@link this#setShouldBeHidden} below. */
+    public boolean shouldBeHidden() {
+        return mShouldBeHidden;
+    }
+
+    /**
+     * Whether this view's visibility should be set to INVISIBLE. Note that this is different from
+     * the {@link StackScrollerDecorView#setVisible} method, which in turn handles visibility
+     * transitions between VISIBLE and GONE.
+     */
+    public void setShouldBeHidden(boolean hide) {
+        mShouldBeHidden = hide;
+    }
+
     @Override
     public void dump(PrintWriter pwOriginal, String[] args) {
         IndentingPrintWriter pw = DumpUtilsKt.asIndenting(pwOriginal);
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 b8b4a03..eb6c7b5 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
@@ -1767,9 +1767,7 @@
      */
     public ExpandableNotificationRow(Context context, AttributeSet attrs) {
         this(context, attrs, context);
-        if (com.android.systemui.Flags.notificationRowUserContext()) {
-            Log.wtf(TAG, "This constructor shouldn't be called");
-        }
+        Log.wtf(TAG, "This constructor shouldn't be called");
     }
 
     /**
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/NotificationInlineImageResolver.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInlineImageResolver.java
index 609b15e..3e932aa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInlineImageResolver.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInlineImageResolver.java
@@ -31,7 +31,6 @@
 import com.android.internal.widget.ImageResolver;
 import com.android.internal.widget.LocalImageResolver;
 import com.android.internal.widget.MessagingMessage;
-import com.android.systemui.Flags;
 
 import java.util.HashSet;
 import java.util.List;
@@ -67,11 +66,7 @@
      * @param imageCache The implementation of internal cache.
      */
     public NotificationInlineImageResolver(Context context, ImageCache imageCache) {
-        if (Flags.notificationRowUserContext()) {
-            mContext = context;
-        } else {
-            mContext = context.getApplicationContext();
-        }
+        mContext = context;
         mImageCache = imageCache;
 
         if (mImageCache != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java
index ea3036e..5fbcebd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java
@@ -66,9 +66,7 @@
             mInflateOrigin = new Throwable("inflate requested here");
         }
         mListener = listener;
-        AsyncLayoutInflater inflater = com.android.systemui.Flags.notificationRowUserContext()
-                ? new AsyncLayoutInflater(context, makeRowInflater(entry))
-                : new AsyncLayoutInflater(context);
+        AsyncLayoutInflater inflater = new AsyncLayoutInflater(context, makeRowInflater(entry));
         mEntry = entry;
         entry.setInflationTask(this);
 
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/HeadsUpRowKey.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/HeadsUpRowKey.kt
new file mode 100644
index 0000000..8dc395d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/HeadsUpRowKey.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.statusbar.notification.shared
+
+/**
+ * A unique key representing a top-level heads up notification.
+ *
+ * @see com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
+ */
+interface HeadsUpRowKey
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 f2c593d..fb52838 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
@@ -5405,7 +5405,7 @@
     /**
      * @param topHeadsUpRow the first headsUp row in z-order.
      */
-    public void setTopHeadsUpRow(ExpandableNotificationRow topHeadsUpRow) {
+    public void setTopHeadsUpRow(@Nullable ExpandableNotificationRow topHeadsUpRow) {
         mTopHeadsUpRow = topHeadsUpRow;
     }
 
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..ec111a1 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;
@@ -126,6 +126,7 @@
 import com.android.systemui.statusbar.notification.row.NotificationGuts;
 import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
 import com.android.systemui.statusbar.notification.row.NotificationSnooze;
+import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor;
 import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor;
 import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationListViewBinder;
 import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
@@ -685,11 +686,13 @@
             new OnHeadsUpChangedListener() {
                 @Override
                 public void onHeadsUpPinnedModeChanged(boolean inPinnedMode) {
+                    NotificationsHeadsUpRefactor.assertInLegacyMode();
                     mView.setInHeadsUpPinnedMode(inPinnedMode);
                 }
 
                 @Override
                 public void onHeadsUpStateChanged(NotificationEntry entry, boolean isHeadsUp) {
+                    NotificationsHeadsUpRefactor.assertInLegacyMode();
                     NotificationEntry topEntry = mHeadsUpManager.getTopEntry();
                     mView.setTopHeadsUpRow(topEntry != null ? topEntry.getRow() : null);
                     generateHeadsUpAnimation(entry, isHeadsUp);
@@ -870,7 +873,9 @@
             });
         }
 
-        mHeadsUpManager.addListener(mOnHeadsUpChangedListener);
+        if (!NotificationsHeadsUpRefactor.isEnabled()) {
+            mHeadsUpManager.addListener(mOnHeadsUpChangedListener);
+        }
         mHeadsUpManager.setAnimationStateHandler(mView::setHeadsUpGoingAwayAnimationsAllowed);
         mDynamicPrivacyController.addListener(mDynamicPrivacyControllerListener);
 
@@ -2090,7 +2095,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/NotificationStackSizeCalculator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
index 2d9c63e..1b53cbe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
@@ -20,6 +20,7 @@
 import android.util.Log
 import android.view.View.GONE
 import androidx.annotation.VisibleForTesting
+import com.android.systemui.Flags.notificationMinimalismPrototype
 import com.android.systemui.res.R
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
@@ -66,6 +67,11 @@
      */
     private var maxKeyguardNotifications by notNull<Int>()
 
+    /**
+     * Whether [maxKeyguardNotifications] will have 1 added to it when media is shown in the stack.
+     */
+    private var maxNotificationsExcludesMedia = false
+
     /** Minimum space between two notifications, see [calculateGapAndDividerHeight]. */
     private var dividerHeight by notNull<Float>()
 
@@ -168,7 +174,11 @@
         log { "\n" }
 
         val stackHeightSequence = computeHeightPerNotificationLimit(stack, shelfHeight)
+
+        // TODO: Avoid making this split shade assumption by simply checking the stack for media
         val isMediaShowing = mediaDataManager.hasActiveMediaOrRecommendation()
+        val isMediaShowingInStack = isMediaShowing && !splitShadeStateController
+                .shouldUseSplitNotificationShade(resources)
 
         log { "\tGet maxNotifWithoutSavingSpace ---" }
         val maxNotifWithoutSavingSpace =
@@ -181,12 +191,11 @@
             }
 
         // How many notifications we can show at heightWithoutLockscreenConstraints
-        var minCountAtHeightWithoutConstraints =
-            if (isMediaShowing && !splitShadeStateController
-                    .shouldUseSplitNotificationShade(resources)) 2 else 1
+        val minCountAtHeightWithoutConstraints = if (isMediaShowingInStack) 2 else 1
         log {
             "\t---maxNotifWithoutSavingSpace=$maxNotifWithoutSavingSpace " +
                 "isMediaShowing=$isMediaShowing" +
+                "isMediaShowingInStack=$isMediaShowingInStack" +
                 "minCountAtHeightWithoutConstraints=$minCountAtHeightWithoutConstraints"
         }
         log { "\n" }
@@ -223,7 +232,9 @@
         }
 
         if (onLockscreen()) {
-            maxNotifications = min(maxKeyguardNotifications, maxNotifications)
+            val increaseMaxForMedia = maxNotificationsExcludesMedia && isMediaShowingInStack
+            val lockscreenMax = maxKeyguardNotifications.safeIncrementIf(increaseMaxForMedia)
+            maxNotifications = min(lockscreenMax, maxNotifications)
         }
 
         // Could be < 0 if the space available is less than the shelf size. Returns 0 in this case.
@@ -276,7 +287,7 @@
             height = notifsHeight + shelfHeightWithSpaceBefore
             log {
                 "--- computeHeight(maxNotifs=$maxNotifs, shelfHeight=$shelfHeight)" +
-                    " -> ${height}=($notifsHeight+$shelfHeightWithSpaceBefore)" +
+                    " -> $height=($notifsHeight+$shelfHeightWithSpaceBefore)" +
                     " | saveSpaceOnLockscreen=$saveSpaceOnLockscreen"
             }
         }
@@ -367,8 +378,9 @@
     }
 
     fun updateResources() {
-        maxKeyguardNotifications =
-            infiniteIfNegative(resources.getInteger(R.integer.keyguard_max_notification_count))
+        maxKeyguardNotifications = if (notificationMinimalismPrototype()) 1
+            else infiniteIfNegative(resources.getInteger(R.integer.keyguard_max_notification_count))
+        maxNotificationsExcludesMedia = notificationMinimalismPrototype()
 
         dividerHeight =
             max(1f, resources.getDimensionPixelSize(R.dimen.notification_divider_height).toFloat())
@@ -486,6 +498,13 @@
             v
         }
 
+    private fun Int.safeIncrementIf(condition: Boolean): Int =
+        if (condition && this != Int.MAX_VALUE) {
+            this + 1
+        } else {
+            this
+        }
+
     /** Returns the last index where [predicate] returns true, or -1 if it was always false. */
     private fun <T> Sequence<T>.lastIndexWhile(predicate: (T) -> Boolean): Int =
         takeWhile(predicate).count() - 1
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 b42c07d..5eaccd9 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
@@ -594,15 +594,16 @@
         );
         if (view instanceof FooterView) {
             if (FooterViewRefactor.isEnabled()) {
-                final float footerEnd = algorithmState.mCurrentExpandedYPosition
-                        + view.getIntrinsicHeight();
-                final boolean noSpaceForFooter = footerEnd > ambientState.getStackEndHeight();
-                // TODO(b/293167744): May be able to keep only noSpaceForFooter here if we add an
-                //  emission when clearAllNotifications is called, and then use that in the footer
-                //  visibility flow.
-                ((FooterView.FooterViewState) viewState).hideContent =
-                        noSpaceForFooter || (ambientState.isClearAllInProgress()
-                                && !hasNonClearableNotifs(algorithmState));
+                if (((FooterView) view).shouldBeHidden()) {
+                    viewState.hidden = true;
+                } else {
+                    final float footerEnd = algorithmState.mCurrentExpandedYPosition
+                            + view.getIntrinsicHeight();
+                    final boolean noSpaceForFooter = footerEnd > ambientState.getStackEndHeight();
+                    ((FooterView.FooterViewState) viewState).hideContent =
+                            noSpaceForFooter || (ambientState.isClearAllInProgress()
+                                    && !hasNonClearableNotifs(algorithmState));
+                }
 
             } else {
                 final boolean shadeClosed = !ambientState.isShadeExpanded();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
index 6b30393..18bb5119 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
@@ -36,6 +36,7 @@
 import com.android.systemui.statusbar.notification.footer.ui.viewbinder.FooterViewBinder
 import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModel
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerShelfViewBinder
+import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
 import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
 import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinder
 import com.android.systemui.statusbar.notification.stack.DisplaySwitchNotificationsHiderTracker
@@ -44,6 +45,7 @@
 import com.android.systemui.statusbar.notification.stack.ui.view.NotificationStatsLogger
 import com.android.systemui.statusbar.notification.stack.ui.viewbinder.HideNotificationsBinder.bindHideList
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel
+import com.android.systemui.statusbar.notification.ui.viewbinder.HeadsUpNotificationViewBinder
 import com.android.systemui.statusbar.phone.NotificationIconAreaController
 import com.android.systemui.util.kotlin.awaitCancellationThenDispose
 import com.android.systemui.util.kotlin.getOrNull
@@ -71,6 +73,7 @@
     private val hiderTracker: DisplaySwitchNotificationsHiderTracker,
     private val configuration: ConfigurationState,
     private val falsingManager: FalsingManager,
+    private val hunBinder: HeadsUpNotificationViewBinder,
     private val iconAreaController: NotificationIconAreaController,
     private val loggerOptional: Optional<NotificationStatsLogger>,
     private val metricsLogger: MetricsLogger,
@@ -92,6 +95,9 @@
 
         view.repeatWhenAttached {
             lifecycleScope.launch {
+                if (NotificationsHeadsUpRefactor.isEnabled) {
+                    launch { hunBinder.bindHeadsUpNotifications(view) }
+                }
                 launch { bindShelf(shelf) }
                 bindHideList(viewController, viewModel, hiderTracker)
 
@@ -187,13 +193,14 @@
                 },
             )
         launch {
-            viewModel.shouldShowFooterView.collect { animatedVisibility ->
+            viewModel.shouldIncludeFooterView.collect { animatedVisibility ->
                 footerView.setVisible(
                     /* visible = */ animatedVisibility.value,
                     /* animate = */ animatedVisibility.isAnimating,
                 )
             }
         }
+        launch { viewModel.shouldHideFooterView.collect { footerView.setShouldBeHidden(it) } }
         disposableHandle.awaitCancellationThenDispose()
     }
 
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 6db6719..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
@@ -48,6 +48,7 @@
     private val sceneContainerFlags: SceneContainerFlags,
     private val controller: NotificationStackScrollLayoutController,
     private val notificationStackSizeCalculator: NotificationStackSizeCalculator,
+    private val notificationStackViewBinder: NotificationStackViewBinder,
     @Main private val mainImmediateDispatcher: CoroutineDispatcher,
 ) {
 
@@ -157,6 +158,10 @@
                 }
             }
 
+        if (sceneContainerFlags.isEnabled()) {
+            disposables += notificationStackViewBinder.bindWhileAttached()
+        }
+
         controller.setOnHeightChangedRunnable { viewModel.notificationStackChanged() }
         disposables += DisposableHandle { controller.setOnHeightChangedRunnable(null) }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/HeadsUpRowViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/HeadsUpRowViewModel.kt
new file mode 100644
index 0000000..ec5e5be
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/HeadsUpRowViewModel.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.statusbar.notification.stack.ui.viewmodel
+
+import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpRowInteractor
+
+class HeadsUpRowViewModel(headsUpRowInteractor: HeadsUpRowInteractor)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
index 4744fcb..5a7433d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
@@ -17,17 +17,20 @@
 package com.android.systemui.statusbar.notification.stack.ui.viewmodel
 
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.domain.interactor.RemoteInputInteractor
 import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
+import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
 import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
 import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
 import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModel
+import com.android.systemui.statusbar.notification.shared.HeadsUpRowKey
+import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
 import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.NotificationShelfViewModel
 import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackInteractor
 import com.android.systemui.statusbar.policy.domain.interactor.UserSetupInteractor
 import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
-import com.android.systemui.util.kotlin.combine
 import com.android.systemui.util.kotlin.sample
 import com.android.systemui.util.ui.AnimatableEvent
 import com.android.systemui.util.ui.AnimatedValue
@@ -53,6 +56,8 @@
     val logger: Optional<NotificationLoggerViewModel>,
     activeNotificationsInteractor: ActiveNotificationsInteractor,
     notificationStackInteractor: NotificationStackInteractor,
+    private val headsUpNotificationInteractor: HeadsUpNotificationInteractor,
+    keyguardInteractor: KeyguardInteractor,
     remoteInputInteractor: RemoteInputInteractor,
     seenNotificationsInteractor: SeenNotificationsInteractor,
     shadeInteractor: ShadeInteractor,
@@ -105,7 +110,32 @@
         }
     }
 
-    val shouldShowFooterView: Flow<AnimatedValue<Boolean>> by lazy {
+    /**
+     * Whether the footer should not be visible for the user, even if it's present in the list (as
+     * per [shouldIncludeFooterView] below).
+     *
+     * This essentially corresponds to having the view set to INVISIBLE.
+     */
+    val shouldHideFooterView: Flow<Boolean> by lazy {
+        if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
+            flowOf(false)
+        } else {
+            // When the shade is closed, the footer is still present in the list, but not visible.
+            // This prevents the footer from being shown when a HUN is present, while still allowing
+            // the footer to be counted as part of the shade for measurements.
+            shadeInteractor.shadeExpansion.map { it == 0f }.distinctUntilChanged()
+        }
+    }
+
+    /**
+     * Whether the footer should be part of the list or not, and whether the transition from one
+     * state to another should be animated. This essentially corresponds to transitioning the view
+     * visibility from VISIBLE to GONE and vice versa.
+     *
+     * Note that this value being true doesn't necessarily mean that the footer is visible. It could
+     * be hidden by another condition (see [shouldHideFooterView] above).
+     */
+    val shouldIncludeFooterView: Flow<AnimatedValue<Boolean>> by lazy {
         if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
             flowOf(AnimatedValue.NotAnimating(false))
         } else {
@@ -114,34 +144,30 @@
                     userSetupInteractor.isUserSetUp,
                     notificationStackInteractor.isShowingOnLockscreen,
                     shadeInteractor.isQsFullscreen,
-                    remoteInputInteractor.isRemoteInputActive,
-                    shadeInteractor.shadeExpansion.map { it == 0f }.distinctUntilChanged(),
+                    remoteInputInteractor.isRemoteInputActive
                 ) {
                     hasNotifications,
                     isUserSetUp,
                     isShowingOnLockscreen,
                     qsFullScreen,
-                    isRemoteInputActive,
-                    isShadeClosed ->
+                    isRemoteInputActive ->
                     when {
-                        !hasNotifications -> VisibilityChange.HIDE_WITH_ANIMATION
+                        !hasNotifications -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
                         // Hide the footer until the user setup is complete, to prevent access
                         // to settings (b/193149550).
-                        !isUserSetUp -> VisibilityChange.HIDE_WITH_ANIMATION
+                        !isUserSetUp -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
                         // Do not show the footer if the lockscreen is visible (incl. AOD),
                         // except if the shade is opened on top. See also b/219680200.
                         // Do not animate, as that makes the footer appear briefly when
                         // transitioning between the shade and keyguard.
-                        isShowingOnLockscreen -> VisibilityChange.HIDE_WITHOUT_ANIMATION
+                        isShowingOnLockscreen -> VisibilityChange.DISAPPEAR_WITHOUT_ANIMATION
                         // Do not show the footer if quick settings are fully expanded (except
                         // for the foldable split shade view). See b/201427195 && b/222699879.
-                        qsFullScreen -> VisibilityChange.HIDE_WITH_ANIMATION
+                        qsFullScreen -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
                         // Hide the footer if remote input is active (i.e. user is replying to a
                         // notification). See b/75984847.
-                        isRemoteInputActive -> VisibilityChange.HIDE_WITH_ANIMATION
-                        // Never show the footer if the shade is collapsed (e.g. when HUNing).
-                        isShadeClosed -> VisibilityChange.HIDE_WITHOUT_ANIMATION
-                        else -> VisibilityChange.SHOW_WITH_ANIMATION
+                        isRemoteInputActive -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
+                        else -> VisibilityChange.APPEAR_WITH_ANIMATION
                     }
                 }
                 .flowOn(bgDispatcher)
@@ -174,9 +200,9 @@
     }
 
     enum class VisibilityChange(val visible: Boolean, val canAnimate: Boolean) {
-        HIDE_WITHOUT_ANIMATION(visible = false, canAnimate = false),
-        HIDE_WITH_ANIMATION(visible = false, canAnimate = true),
-        SHOW_WITH_ANIMATION(visible = true, canAnimate = true)
+        DISAPPEAR_WITHOUT_ANIMATION(visible = false, canAnimate = false),
+        DISAPPEAR_WITH_ANIMATION(visible = false, canAnimate = true),
+        APPEAR_WITH_ANIMATION(visible = true, canAnimate = true)
     }
 
     // TODO(b/308591475): This should be tracked separately by the empty shade.
@@ -212,4 +238,41 @@
             activeNotificationsInteractor.hasNonClearableSilentNotifications
         }
     }
+
+    val topHeadsUpRow: Flow<HeadsUpRowKey?> by lazy {
+        if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) {
+            flowOf(null)
+        } else {
+            headsUpNotificationInteractor.topHeadsUpRow
+        }
+    }
+
+    val pinnedHeadsUpRows: Flow<Set<HeadsUpRowKey>> by lazy {
+        if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) {
+            flowOf(emptySet())
+        } else {
+            headsUpNotificationInteractor.pinnedHeadsUpRows
+        }
+    }
+
+    val headsUpAnimationsEnabled: Flow<Boolean> by lazy {
+        combine(keyguardInteractor.isKeyguardShowing, shadeInteractor.isShadeFullyExpanded) {
+            (isKeyguardShowing, isShadeFullyExpanded) ->
+            // TODO(b/325936094) use isShadeFullyCollapsed instead
+            !isKeyguardShowing && !isShadeFullyExpanded
+        }
+    }
+
+    val hasPinnedHeadsUpRow: Flow<Boolean> by lazy {
+        if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) {
+            flowOf(false)
+        } else {
+            headsUpNotificationInteractor.hasPinnedRows
+        }
+    }
+
+    // TODO(b/325936094) use it for the text displayed in the StatusBar
+    fun headsUpRow(key: HeadsUpRowKey): HeadsUpRowViewModel =
+        HeadsUpRowViewModel(headsUpNotificationInteractor.headsUpRow(key))
+    fun elementKeyFor(key: HeadsUpRowKey): Any = headsUpNotificationInteractor.elementKeyFor(key)
 }
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 ab6c148..d112edb 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
@@ -35,7 +35,7 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
 import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE
 import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED
-import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED
+import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
 import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToGoneTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel
 import com.android.systemui.keyguard.ui.viewmodel.AodToLockscreenTransitionViewModel
@@ -366,21 +366,34 @@
                     }
                 }
             }
-            .onStart { emit(0f) }
+            .onStart { emit(1f) }
             .dumpWhileCollecting("alphaForShadeAndQsExpansion")
 
-    private val alphaWhenGoneAndShadeState: Flow<Float> =
-        combineTransform(
-                keyguardTransitionInteractor.transitions
-                    .map { step -> step.to == GONE && step.transitionState == FINISHED }
-                    .distinctUntilChanged(),
-                keyguardInteractor.statusBarState,
-            ) { isGoneTransitionFinished, statusBarState ->
-                if (isGoneTransitionFinished && statusBarState == SHADE) {
-                    emit(1f)
+    private val isGoneTransitionRunning: Flow<Boolean> =
+        flow {
+                while (currentCoroutineContext().isActive) {
+                    emit(false)
+                    // Ensure start where GONE is inactive
+                    keyguardTransitionInteractor.transitionValue(GONE).first { it == 0f }
+                    // Wait for a GONE transition to begin
+                    keyguardTransitionInteractor.transitionStepsToState(GONE).first {
+                        it.value > 0f && it.transitionState == RUNNING
+                    }
+                    emit(true)
+                    // Now await the signal that SHADE state has been reached or the GONE transition
+                    // was reversed. Until SHADE state has been replaced and merged with GONE, it is
+                    // the only source of when it is considered safe to reset alpha to 1f for HUNs.
+                    combine(
+                            keyguardInteractor.statusBarState,
+                            // Emit -1f on start to make sure the flow runs
+                            keyguardTransitionInteractor.transitionValue(GONE).onStart { emit(-1f) }
+                        ) { statusBarState, goneValue ->
+                            statusBarState == SHADE || goneValue == 0f
+                        }
+                        .first { it }
                 }
             }
-            .dumpWhileCollecting("alphaWhenGoneAndShadeState")
+            .dumpWhileCollecting("goneTransitionInProgress")
 
     fun keyguardAlpha(viewState: ViewStateAccessor): Flow<Float> {
         // All transition view models are mututally exclusive, and safe to merge
@@ -407,12 +420,11 @@
 
         return merge(
                 alphaTransitions,
-                // Sends a final alpha value of 1f when truly gone, to make sure HUNs appear
-                alphaWhenGoneAndShadeState,
                 // These remaining cases handle alpha changes within an existing state, such as
                 // shade expansion or swipe to dismiss
                 combineTransform(
                     isOnLockscreenWithoutShade,
+                    isGoneTransitionRunning,
                     shadeCollapseFadeIn,
                     alphaForShadeAndQsExpansion,
                     keyguardInteractor.dismissAlpha.dumpWhileCollecting(
@@ -420,6 +432,7 @@
                     ),
                 ) {
                     isOnLockscreenWithoutShade,
+                    isGoneTransitionRunning,
                     shadeCollapseFadeIn,
                     alphaForShadeAndQsExpansion,
                     dismissAlpha ->
@@ -427,7 +440,7 @@
                         if (!shadeCollapseFadeIn && dismissAlpha != null) {
                             emit(dismissAlpha)
                         }
-                    } else {
+                    } else if (!isGoneTransitionRunning) {
                         emit(alphaForShadeAndQsExpansion)
                     }
                 },
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt
new file mode 100644
index 0000000..cb360fe
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt
@@ -0,0 +1,78 @@
+/*
+ * 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.ui.viewbinder
+
+import android.util.Log
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.android.systemui.statusbar.notification.shared.HeadsUpRowKey
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel
+import com.android.systemui.util.kotlin.sample
+import javax.inject.Inject
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+
+private const val TAG = "HunBinder"
+private val DEBUG = true // Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG)
+
+class HeadsUpNotificationViewBinder
+@Inject
+constructor(private val viewModel: NotificationListViewModel) {
+    suspend fun bindHeadsUpNotifications(parentView: NotificationStackScrollLayout): Unit =
+        coroutineScope {
+            launch {
+                var previousKeys = emptySet<HeadsUpRowKey>()
+                viewModel.pinnedHeadsUpRows
+                    .sample(viewModel.headsUpAnimationsEnabled, ::Pair)
+                    .collect { (newKeys, animationsEnabled) ->
+                        if (DEBUG) {
+                            Log.d(TAG, "update:$newKeys")
+                        }
+
+                        val added = newKeys - previousKeys
+                        val removed = previousKeys - newKeys
+                        previousKeys = newKeys
+
+                        if (animationsEnabled) {
+                            added.forEach { key ->
+                                parentView.generateHeadsUpAnimation(
+                                    obtainView(key),
+                                    /* isHeadsUp = */ true
+                                )
+                            }
+                            removed.forEach { key ->
+                                val row = obtainView(key)
+                                parentView.generateHeadsUpAnimation(row, /* isHeadsUp = */ false)
+                                row.setHeadsUpIsVisible()
+                            }
+                        }
+                    }
+            }
+            launch {
+                viewModel.topHeadsUpRow.collect { key ->
+                    parentView.setTopHeadsUpRow(key?.let(::obtainView))
+                }
+            }
+            launch {
+                viewModel.hasPinnedHeadsUpRow.collect { parentView.setInHeadsUpPinnedMode(it) }
+            }
+        }
+
+    private fun obtainView(key: HeadsUpRowKey): ExpandableNotificationRow {
+        return viewModel.elementKeyFor(key) as ExpandableNotificationRow
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
index a55de25..37646ae 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
@@ -21,6 +21,7 @@
 import android.app.TaskStackBuilder
 import android.content.Context
 import android.content.Intent
+import android.os.Bundle
 import android.os.RemoteException
 import android.os.UserHandle
 import android.provider.Settings
@@ -149,6 +150,23 @@
         )
     }
 
+    override fun startPendingIntentMaybeDismissingKeyguard(
+        intent: PendingIntent,
+        intentSentUiThreadCallback: Runnable?,
+        animationController: ActivityTransitionAnimator.Controller?,
+        fillInIntent: Intent?,
+        extraOptions: Bundle?,
+    ) {
+        activityStarterInternal.startPendingIntentDismissingKeyguard(
+            intent = intent,
+            intentSentUiThreadCallback = intentSentUiThreadCallback,
+            animationController = animationController,
+            showOverLockscreen = true,
+            fillInIntent = fillInIntent,
+            extraOptions = extraOptions,
+        )
+    }
+
     /**
      * TODO(b/279084380): Change callers to just call startActivityDismissingKeyguard and deprecate
      *   this.
@@ -554,6 +572,8 @@
             associatedView: View? = null,
             animationController: ActivityTransitionAnimator.Controller? = null,
             showOverLockscreen: Boolean = false,
+            fillInIntent: Intent? = null,
+            extraOptions: Bundle? = null,
         ) {
             val animationController =
                 if (associatedView is ExpandableNotificationRow) {
@@ -614,9 +634,10 @@
                                 val options =
                                     ActivityOptions(
                                         CentralSurfaces.getActivityOptions(
-                                            displayId,
-                                            animationAdapter
-                                        )
+                                                displayId,
+                                                animationAdapter
+                                            )
+                                            .apply { extraOptions?.let { putAll(it) } }
                                     )
                                 // TODO b/221255671: restrict this to only be set for
                                 // notifications
@@ -625,9 +646,9 @@
                                     ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
                                 )
                                 return intent.sendAndReturnResult(
-                                    null,
+                                    context,
                                     0,
-                                    null,
+                                    fillInIntent,
                                     null,
                                     null,
                                     null,
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 86bb844..3f200d5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
@@ -22,6 +22,7 @@
 import android.content.res.Resources;
 import android.graphics.Region;
 import android.os.Handler;
+import android.util.ArrayMap;
 import android.util.Pools;
 
 import androidx.collection.ArraySet;
@@ -40,6 +41,8 @@
 import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener;
 import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider;
 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
+import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository;
+import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor;
 import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
@@ -59,13 +62,21 @@
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Objects;
+import java.util.Set;
 import java.util.Stack;
 
 import javax.inject.Inject;
 
+import kotlinx.coroutines.flow.Flow;
+import kotlinx.coroutines.flow.MutableStateFlow;
+import kotlinx.coroutines.flow.StateFlow;
+import kotlinx.coroutines.flow.StateFlowKt;
+
 /** A implementation of HeadsUpManager for phone. */
 @SysUISingleton
-public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUpChangedListener {
+public class HeadsUpManagerPhone extends BaseHeadsUpManager implements
+        HeadsUpRepository, OnHeadsUpChangedListener {
     private static final String TAG = "HeadsUpManagerPhone";
 
     @VisibleForTesting
@@ -74,15 +85,20 @@
     private final GroupMembershipManager mGroupMembershipManager;
     private final List<OnHeadsUpPhoneListenerChange> mHeadsUpPhoneListeners = new ArrayList<>();
     private final VisualStabilityProvider mVisualStabilityProvider;
-    private boolean mReleaseOnExpandFinish;
 
+    // TODO(b/328393698) move the topHeadsUpRow logic to an interactor
+    private final MutableStateFlow<HeadsUpRowRepository> mTopHeadsUpRow =
+            StateFlowKt.MutableStateFlow(null);
+    private final MutableStateFlow<Set<HeadsUpRowRepository>> mHeadsUpNotificationRows =
+            StateFlowKt.MutableStateFlow(new HashSet<>());
+    private final MutableStateFlow<Boolean> mHeadsUpGoingAway = StateFlowKt.MutableStateFlow(false);
+    private boolean mReleaseOnExpandFinish;
     private boolean mTrackingHeadsUp;
     private final HashSet<String> mSwipedOutKeys = new HashSet<>();
     private final HashSet<NotificationEntry> mEntriesToRemoveAfterExpand = new HashSet<>();
     private final ArraySet<NotificationEntry> mEntriesToRemoveWhenReorderingAllowed
             = new ArraySet<>();
     private boolean mIsExpanded;
-    private boolean mHeadsUpGoingAway;
     private int mStatusBarState;
     private AnimationStateHandler mAnimationStateHandler;
     private int mHeadsUpInset;
@@ -248,7 +264,7 @@
         if (isExpanded != mIsExpanded) {
             mIsExpanded = isExpanded;
             if (isExpanded) {
-                mHeadsUpGoingAway = false;
+                mHeadsUpGoingAway.setValue(false);
             }
         }
     }
@@ -259,17 +275,17 @@
      */
     @Override
     public void setHeadsUpGoingAway(boolean headsUpGoingAway) {
-        if (headsUpGoingAway != mHeadsUpGoingAway) {
-            mHeadsUpGoingAway = headsUpGoingAway;
+        if (headsUpGoingAway != mHeadsUpGoingAway.getValue()) {
             for (OnHeadsUpPhoneListenerChange listener : mHeadsUpPhoneListeners) {
                 listener.onHeadsUpGoingAwayStateChanged(headsUpGoingAway);
             }
+            mHeadsUpGoingAway.setValue(headsUpGoingAway);
         }
     }
 
     @Override
     public boolean isHeadsUpGoingAway() {
-        return mHeadsUpGoingAway;
+        return mHeadsUpGoingAway.getValue();
     }
 
     /**
@@ -288,6 +304,7 @@
             } else {
                 headsUpEntry.updateEntry(false /* updatePostTime */, "setRemoteInputActive(false)");
             }
+            onEntryUpdated(headsUpEntry);
         }
     }
 
@@ -387,11 +404,35 @@
     }
 
     @Override
+    protected void onEntryAdded(HeadsUpEntry headsUpEntry) {
+        super.onEntryAdded(headsUpEntry);
+        updateTopHeadsUpFlow();
+        updateHeadsUpFlow();
+    }
+
+    @Override
+    protected void onEntryUpdated(HeadsUpEntry headsUpEntry) {
+        super.onEntryUpdated(headsUpEntry);
+        // no need to update the list here
+        updateTopHeadsUpFlow();
+    }
+
+    @Override
     protected void onEntryRemoved(HeadsUpEntry headsUpEntry) {
         super.onEntryRemoved(headsUpEntry);
         if (!NotificationsHeadsUpRefactor.isEnabled()) {
             mEntryPool.release((HeadsUpEntryPhone) headsUpEntry);
         }
+        updateTopHeadsUpFlow();
+        updateHeadsUpFlow();
+    }
+
+    private void updateTopHeadsUpFlow() {
+        mTopHeadsUpRow.setValue((HeadsUpRowRepository) getTopHeadsUpEntry());
+    }
+
+    private void updateHeadsUpFlow() {
+        mHeadsUpNotificationRows.setValue(new HashSet<>(getHeadsUpEntryPhoneMap().values()));
     }
 
     @Override
@@ -415,6 +456,12 @@
     ///////////////////////////////////////////////////////////////////////////////////////////////
     //  Private utility methods:
 
+    @NonNull
+    private ArrayMap<String, HeadsUpEntryPhone> getHeadsUpEntryPhoneMap() {
+        //noinspection unchecked
+        return (ArrayMap<String, HeadsUpEntryPhone>) ((ArrayMap) mHeadsUpEntryMap);
+    }
+
     @Nullable
     private HeadsUpEntryPhone getHeadsUpEntryPhone(@NonNull String key) {
         return (HeadsUpEntryPhone) mHeadsUpEntryMap.get(key);
@@ -422,7 +469,11 @@
 
     @Nullable
     private HeadsUpEntryPhone getTopHeadsUpEntryPhone() {
-        return (HeadsUpEntryPhone) getTopHeadsUpEntry();
+        if (NotificationsHeadsUpRefactor.isEnabled()) {
+            return (HeadsUpEntryPhone) mTopHeadsUpRow.getValue();
+        } else {
+            return (HeadsUpEntryPhone) getTopHeadsUpEntry();
+        }
     }
 
     @Override
@@ -439,12 +490,32 @@
         return headsUpEntry == null || headsUpEntry != topEntry || super.canRemoveImmediately(key);
     }
 
+    @Override
+    @NonNull
+    public Flow<HeadsUpRowRepository> getTopHeadsUpRow() {
+        return mTopHeadsUpRow;
+    }
+
+    @Override
+    @NonNull
+    public Flow<Set<HeadsUpRowRepository>> getActiveHeadsUpRows() {
+        return mHeadsUpNotificationRows;
+    }
+
+    @Override
+    @NonNull
+    public Flow<Boolean> getHeadsUpAnimatingAway() {
+        return mHeadsUpGoingAway;
+    }
+
     ///////////////////////////////////////////////////////////////////////////////////////////////
     //  HeadsUpEntryPhone:
 
-    protected class HeadsUpEntryPhone extends BaseHeadsUpManager.HeadsUpEntry {
+    protected class HeadsUpEntryPhone extends BaseHeadsUpManager.HeadsUpEntry implements
+            HeadsUpRowRepository {
 
         private boolean mGutsShownPinned;
+        private final MutableStateFlow<Boolean> mIsPinned = StateFlowKt.MutableStateFlow(false);
 
         /**
          * If the time this entry has been on was extended
@@ -465,6 +536,25 @@
         }
 
         @Override
+        @NonNull
+        public String getKey() {
+            return requireEntry().getKey();
+        }
+
+        @Override
+        @NonNull
+        public StateFlow<Boolean> isPinned() {
+            return mIsPinned;
+        }
+
+        @Override
+        protected void setRowPinned(boolean pinned) {
+            // TODO(b/327624082): replace this super call with a ViewBinder
+            super.setRowPinned(pinned);
+            mIsPinned.setValue(pinned);
+        }
+
+        @Override
         protected Runnable createRemoveRunnable(NotificationEntry entry) {
             return  () -> {
                 if (!mVisualStabilityProvider.isReorderingAllowed()
@@ -539,6 +629,17 @@
         protected long calculateFinishTime() {
             return super.calculateFinishTime() + (extended ? mExtensionTime : 0);
         }
+
+        @Override
+        @NonNull
+        public Object getElementKey() {
+            return requireEntry().getRow();
+        }
+
+        private NotificationEntry requireEntry() {
+            /* check if */ NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode();
+            return Objects.requireNonNull(mEntry);
+        }
     }
 
     private final StateListener mStatusBarStateListener = new StateListener() {
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/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt
index 60b8599..b085d80 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt
@@ -301,7 +301,7 @@
             .flatMapLatest { it.networkName }
             .logDiffsForTable(
                 tableLogBuffer,
-                columnPrefix = "",
+                columnPrefix = "intent",
                 initialValue = activeRepo.value.networkName.value,
             )
             .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.networkName.value)
@@ -311,7 +311,7 @@
             .flatMapLatest { it.carrierName }
             .logDiffsForTable(
                 tableLogBuffer,
-                columnPrefix = "",
+                columnPrefix = "sub",
                 initialValue = activeRepo.value.carrierName.value,
             )
             .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.carrierName.value)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
index f01ac0e..5ab2ae8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
@@ -358,7 +358,13 @@
             }
             .stateIn(scope, SharingStarted.WhileSubscribed(), telephonyManager.simCarrierId)
 
-    /** BroadcastDispatcher does not handle sticky broadcasts, so we can't use it here */
+    /**
+     * BroadcastDispatcher does not handle sticky broadcasts, so we can't use it here. Note that we
+     * now use the [SharingStarted.Eagerly] strategy, because there have been cases where the sticky
+     * broadcast does not represent the correct state.
+     *
+     * See b/322432056 for context.
+     */
     @SuppressLint("RegisterReceiverViaContext")
     override val networkName: StateFlow<NetworkNameModel> =
         conflatedCallbackFlow {
@@ -388,7 +394,7 @@
                 awaitClose { context.unregisterReceiver(receiver) }
             }
             .flowOn(bgDispatcher)
-            .stateIn(scope, SharingStarted.WhileSubscribed(), defaultNetworkName)
+            .stateIn(scope, SharingStarted.Eagerly, defaultNetworkName)
 
     override val dataEnabled = run {
         val initial = telephonyManager.isDataConnectionAllowed
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 6f7e046..20a82a4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
@@ -172,6 +172,7 @@
             // Add new entry and begin managing it
             mHeadsUpEntryMap.put(entry.getKey(), headsUpEntry);
             onEntryAdded(headsUpEntry);
+            // TODO(b/328390331) move accessibility events to the view layer
             entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
             entry.setIsHeadsUpEntry(true);
 
@@ -232,7 +233,7 @@
             // with the groupmanager
             return;
         }
-
+        // TODO(b/328390331) move accessibility events to the view layer
         headsUpEntry.mEntry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
 
         if (shouldHeadsUpAgain) {
@@ -332,15 +333,15 @@
         if (!isPinned) {
             headsUpEntry.mWasUnpinned = true;
         }
-        if (headsUpEntry.isPinned() != isPinned) {
-            headsUpEntry.setPinned(isPinned);
+        if (headsUpEntry.isRowPinned() != isPinned) {
+            headsUpEntry.setRowPinned(isPinned);
             updatePinnedMode();
             if (isPinned && entry.getSbn() != null) {
                mUiEventLogger.logWithInstanceId(
                         NotificationPeekEvent.NOTIFICATION_PEEK, entry.getSbn().getUid(),
                         entry.getSbn().getPackageName(), entry.getSbn().getInstanceId());
             }
-            // TODO(b/325936094) convert these listeners to collecting a flow
+        // TODO(b/325936094) use the isPinned Flow instead
             for (OnHeadsUpChangedListener listener : mListeners) {
                 if (isPinned) {
                     listener.onHeadsUpPinned(entry);
@@ -359,7 +360,7 @@
      * Manager-specific logic that should occur when an entry is added.
      * @param headsUpEntry entry added
      */
-    void onEntryAdded(HeadsUpEntry headsUpEntry) {
+    protected void onEntryAdded(HeadsUpEntry headsUpEntry) {
         NotificationEntry entry = headsUpEntry.mEntry;
         entry.setHeadsUp(true);
 
@@ -391,6 +392,7 @@
             entry.demoteStickyHun();
             mHeadsUpEntryMap.remove(key);
             onEntryRemoved(headsUpEntry);
+            // TODO(b/328390331) move accessibility events to the view layer
             entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
             if (NotificationsHeadsUpRefactor.isEnabled()) {
                 headsUpEntry.cancelAutoRemovalCallbacks("removeEntry");
@@ -416,7 +418,16 @@
         }
     }
 
-    private void updatePinnedMode() {
+    /**
+     * Manager-specific logic, that should occur, when the entry is updated, and its posted time has
+     * changed.
+     *
+     * @param headsUpEntry entry updated
+     */
+    protected void onEntryUpdated(HeadsUpEntry headsUpEntry) {
+    }
+
+    protected void updatePinnedMode() {
         boolean hasPinnedNotification = hasPinnedNotificationInternal();
         if (hasPinnedNotification == mHasPinnedNotification) {
             return;
@@ -471,7 +482,7 @@
     @Nullable
     protected HeadsUpEntry getHeadsUpEntry(@NonNull String key) {
         // TODO(b/315362456) See if callers need to check AvalancheController
-        return (HeadsUpEntry) mHeadsUpEntryMap.get(key);
+        return mHeadsUpEntryMap.get(key);
     }
 
     /**
@@ -491,7 +502,7 @@
         HeadsUpEntry topEntry = null;
         for (HeadsUpEntry entry: mHeadsUpEntryMap.values()) {
             if (topEntry == null || entry.compareTo(topEntry) < 0) {
-                topEntry = (HeadsUpEntry) entry;
+                topEntry = entry;
             }
         }
         return topEntry;
@@ -720,11 +731,11 @@
             updateEntry(true /* updatePostTime */, "setEntry");
         }
 
-        public boolean isPinned() {
+        protected boolean isRowPinned() {
             return mEntry != null && mEntry.isRowPinned();
         }
 
-        public void setPinned(boolean pinned) {
+        protected void setRowPinned(boolean pinned) {
             if (mEntry != null) mEntry.setRowPinned(pinned);
         }
 
@@ -764,6 +775,9 @@
                 return timeLeft;
             };
             scheduleAutoRemovalCallback(finishTimeCalculator, "updateEntry (not sticky)");
+
+            // Notify the manager, that the posted time has changed.
+            onEntryUpdated(this);
         }
 
         /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt
index 420701f..52a2e9c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt
@@ -196,6 +196,7 @@
      * Called when a heads up notification is 'going away' or no longer 'going away'. See
      * [HeadsUpManager.setHeadsUpGoingAway].
      */
+    // TODO(b/325936094) delete this callback, and listen to the flow instead
     fun onHeadsUpGoingAwayStateChanged(headsUpGoingAway: Boolean)
 }
 
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/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/panel/component/bottombar/ui/viewmodel/BottomBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModel.kt
index 46ea382..0207d6e 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModel.kt
@@ -36,11 +36,10 @@
     }
 
     fun onSettingsClicked() {
-        volumePanelViewModel.dismissPanel()
         activityStarter.startActivity(
             Intent(Settings.ACTION_SOUND_SETTINGS)
                 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT),
             true,
-        )
+        ) { volumePanelViewModel.dismissPanel() }
     }
 }
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 d49442c..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
@@ -116,6 +116,7 @@
             isEnabled = isEnabled,
             a11yStep = volumeRange.step,
             audioStreamModel = this,
+            isMutable = audioVolumeInteractor.isMutable(audioStream),
         )
     }
 
@@ -160,6 +161,7 @@
         override val disabledMessage: String?,
         override val isEnabled: Boolean,
         override val a11yStep: Int,
+        override val isMutable: Boolean,
         val audioStreamModel: AudioStreamModel,
     ) : SliderState
 
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 0f240b3..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
@@ -90,6 +90,8 @@
     ) : SliderState {
         override val disabledMessage: String?
             get() = null
+        override val isMutable: Boolean
+            get() = false
     }
 
     @AssistedFactory
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 3dca272..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,6 +36,7 @@
      */
     val a11yStep: Int
     val disabledMessage: String?
+    val isMutable: Boolean
 
     data object Empty : SliderState {
         override val value: Float = 0f
@@ -46,5 +47,6 @@
         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/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/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/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/dialog/bluetooth/BluetoothAutoOnInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractorTest.kt
index 3710713..036d3c8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractorTest.kt
@@ -16,22 +16,25 @@
 
 package com.android.systemui.qs.tiles.dialog.bluetooth
 
-import android.content.pm.UserInfo
+import android.bluetooth.BluetoothAdapter
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
+import com.android.settingslib.bluetooth.LocalBluetoothManager
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.user.data.repository.FakeUserRepository
-import com.android.systemui.util.settings.FakeSettings
-import com.google.common.truth.Truth
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
 import kotlin.test.Test
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
 import org.junit.Before
 import org.junit.Rule
 import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.Mock
 import org.mockito.junit.MockitoJUnit
 import org.mockito.junit.MockitoRule
 
@@ -41,8 +44,17 @@
     @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
     private val testDispatcher = StandardTestDispatcher()
     private val testScope = TestScope(testDispatcher)
-    private var secureSettings: FakeSettings = FakeSettings()
-    private val userRepository: FakeUserRepository = FakeUserRepository()
+    private val bluetoothAdapter =
+        mock<BluetoothAdapter> {
+            var autoOn = false
+            whenever(isAutoOnEnabled).thenAnswer { autoOn }
+
+            whenever(setAutoOnEnabled(anyBoolean())).thenAnswer { invocation ->
+                autoOn = invocation.getArgument(0) as Boolean
+                autoOn
+            }
+        }
+    @Mock private lateinit var localBluetoothManager: LocalBluetoothManager
     private lateinit var bluetoothAutoOnInteractor: BluetoothAutoOnInteractor
 
     @Before
@@ -50,49 +62,35 @@
         bluetoothAutoOnInteractor =
             BluetoothAutoOnInteractor(
                 BluetoothAutoOnRepository(
-                    secureSettings,
-                    userRepository,
+                    localBluetoothManager,
+                    bluetoothAdapter,
                     testScope.backgroundScope,
-                    testDispatcher
+                    testDispatcher,
                 )
             )
     }
 
     @Test
-    fun testSet_bluetoothAutoOnUnset_doNothing() {
+    fun testSetEnabled_bluetoothAutoOnUnsupported_doNothing() {
         testScope.runTest {
+            whenever(bluetoothAdapter.isAutoOnSupported).thenReturn(false)
+
             bluetoothAutoOnInteractor.setEnabled(true)
-
-            val actualValue by collectLastValue(bluetoothAutoOnInteractor.isEnabled)
-
             runCurrent()
 
-            Truth.assertThat(actualValue).isEqualTo(false)
+            assertFalse(bluetoothAdapter.isAutoOnEnabled)
         }
     }
 
     @Test
-    fun testSet_bluetoothAutoOnSet_setNewValue() {
+    fun testSetEnabled_bluetoothAutoOnSupported_setNewValue() {
         testScope.runTest {
-            userRepository.setUserInfos(listOf(SYSTEM_USER))
-            secureSettings.putIntForUser(
-                BluetoothAutoOnRepository.SETTING_NAME,
-                BluetoothAutoOnInteractor.DISABLED,
-                SYSTEM_USER_ID
-            )
+            whenever(bluetoothAdapter.isAutoOnSupported).thenReturn(true)
+
             bluetoothAutoOnInteractor.setEnabled(true)
-
-            val actualValue by collectLastValue(bluetoothAutoOnInteractor.isEnabled)
-
             runCurrent()
 
-            Truth.assertThat(actualValue).isEqualTo(true)
+            assertTrue(bluetoothAdapter.isAutoOnEnabled)
         }
     }
-
-    companion object {
-        private const val SYSTEM_USER_ID = 0
-        private val SYSTEM_USER =
-            UserInfo(/* id= */ SYSTEM_USER_ID, /* name= */ "system user", /* flags= */ 0)
-    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepositoryTest.kt
index cd1452a..3119284 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepositoryTest.kt
@@ -16,18 +16,14 @@
 
 package com.android.systemui.qs.tiles.dialog.bluetooth
 
-import android.content.pm.UserInfo
-import android.os.UserHandle
+import android.bluetooth.BluetoothAdapter
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
+import com.android.settingslib.bluetooth.BluetoothEventManager
+import com.android.settingslib.bluetooth.LocalBluetoothManager
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothAutoOnInteractor.Companion.DISABLED
-import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothAutoOnInteractor.Companion.ENABLED
-import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothAutoOnRepository.Companion.SETTING_NAME
-import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothAutoOnRepository.Companion.UNSET
-import com.android.systemui.user.data.repository.FakeUserRepository
-import com.android.systemui.util.settings.FakeSettings
+import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
@@ -37,6 +33,7 @@
 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
 
@@ -46,83 +43,57 @@
     @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
     private val testDispatcher = StandardTestDispatcher()
     private val testScope = TestScope(testDispatcher)
-    private var secureSettings: FakeSettings = FakeSettings()
-    private val userRepository: FakeUserRepository = FakeUserRepository()
+    @Mock private lateinit var bluetoothAdapter: BluetoothAdapter
+    @Mock private lateinit var localBluetoothManager: LocalBluetoothManager
+    @Mock private lateinit var eventManager: BluetoothEventManager
 
     private lateinit var bluetoothAutoOnRepository: BluetoothAutoOnRepository
 
     @Before
     fun setUp() {
+        whenever(localBluetoothManager.eventManager).thenReturn(eventManager)
         bluetoothAutoOnRepository =
             BluetoothAutoOnRepository(
-                secureSettings,
-                userRepository,
+                localBluetoothManager,
+                bluetoothAdapter,
                 testScope.backgroundScope,
-                testDispatcher
+                testDispatcher,
             )
-
-        userRepository.setUserInfos(listOf(SECONDARY_USER, SYSTEM_USER))
     }
 
     @Test
-    fun testGetValue_valueUnset() {
+    fun testIsAutoOn_returnFalse() {
         testScope.runTest {
-            userRepository.setSelectedUserInfo(SYSTEM_USER)
+            whenever(bluetoothAdapter.isAutoOnEnabled).thenReturn(false)
             val actualValue by collectLastValue(bluetoothAutoOnRepository.isAutoOn)
 
             runCurrent()
 
-            assertThat(actualValue).isEqualTo(UNSET)
-            assertThat(bluetoothAutoOnRepository.isValuePresent()).isFalse()
+            assertThat(actualValue).isEqualTo(false)
         }
     }
 
     @Test
-    fun testGetValue_valueFalse() {
+    fun testIsAutoOn_returnTrue() {
         testScope.runTest {
-            userRepository.setSelectedUserInfo(SYSTEM_USER)
+            whenever(bluetoothAdapter.isAutoOnEnabled).thenReturn(true)
             val actualValue by collectLastValue(bluetoothAutoOnRepository.isAutoOn)
 
-            secureSettings.putIntForUser(SETTING_NAME, DISABLED, UserHandle.USER_SYSTEM)
             runCurrent()
 
-            assertThat(actualValue).isEqualTo(DISABLED)
+            assertThat(actualValue).isEqualTo(true)
         }
     }
 
     @Test
-    fun testGetValue_valueTrue() {
+    fun testIsAutoOnSupported_returnTrue() {
         testScope.runTest {
-            userRepository.setSelectedUserInfo(SYSTEM_USER)
-            val actualValue by collectLastValue(bluetoothAutoOnRepository.isAutoOn)
+            whenever(bluetoothAdapter.isAutoOnSupported).thenReturn(true)
+            val actualValue = bluetoothAutoOnRepository.isAutoOnSupported()
 
-            secureSettings.putIntForUser(SETTING_NAME, ENABLED, UserHandle.USER_SYSTEM)
             runCurrent()
 
-            assertThat(actualValue).isEqualTo(ENABLED)
+            assertThat(actualValue).isEqualTo(true)
         }
     }
-
-    @Test
-    fun testGetValue_valueTrue_secondaryUser_returnTrue() {
-        testScope.runTest {
-            userRepository.setSelectedUserInfo(SECONDARY_USER)
-            val actualValue by collectLastValue(bluetoothAutoOnRepository.isAutoOn)
-
-            secureSettings.putIntForUser(SETTING_NAME, DISABLED, SYSTEM_USER_ID)
-            secureSettings.putIntForUser(SETTING_NAME, ENABLED, SECONDARY_USER_ID)
-            runCurrent()
-
-            assertThat(actualValue).isEqualTo(ENABLED)
-        }
-    }
-
-    companion object {
-        private const val SYSTEM_USER_ID = 0
-        private const val SECONDARY_USER_ID = 1
-        private val SYSTEM_USER =
-            UserInfo(/* id= */ SYSTEM_USER_ID, /* name= */ "system user", /* flags= */ 0)
-        private val SECONDARY_USER =
-            UserInfo(/* id= */ SECONDARY_USER_ID, /* name= */ "secondary user", /* flags= */ 0)
-    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogDelegateTest.kt
index 8ecb953..17b6127 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogDelegateTest.kt
@@ -109,7 +109,6 @@
 
         mBluetoothTileDialogDelegate =
             BluetoothTileDialogDelegate(
-                mContext,
                 uiProperties,
                 CONTENT_HEIGHT,
                 ENABLED,
@@ -119,14 +118,12 @@
                 fakeSystemClock,
                 uiEventLogger,
                 logger,
-                sysuiDialogFactory,
-                LayoutInflater.from(mContext)
+                sysuiDialogFactory
             )
 
         whenever(
                 sysuiDialogFactory.create(
-                    any(SystemUIDialog.Delegate::class.java),
-                    any(Context::class.java)
+                    any(SystemUIDialog.Delegate::class.java)
                 )
             )
             .thenAnswer {
@@ -216,7 +213,6 @@
             LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false)
         val viewHolder =
             BluetoothTileDialogDelegate(
-                    mContext,
                     uiProperties,
                     CONTENT_HEIGHT,
                     ENABLED,
@@ -227,7 +223,6 @@
                     uiEventLogger,
                     logger,
                     sysuiDialogFactory,
-                    LayoutInflater.from(mContext)
                 )
                 .Adapter(bluetoothTileDialogCallback)
                 .DeviceItemViewHolder(view)
@@ -273,7 +268,6 @@
             val cachedHeight = Int.MAX_VALUE
             val dialog =
                 BluetoothTileDialogDelegate(
-                        mContext,
                         BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED),
                         cachedHeight,
                         ENABLED,
@@ -284,7 +278,6 @@
                         uiEventLogger,
                         logger,
                         sysuiDialogFactory,
-                        LayoutInflater.from(mContext)
                     )
                     .createDialog()
             dialog.show()
@@ -298,7 +291,6 @@
         testScope.runTest {
             val dialog =
                 BluetoothTileDialogDelegate(
-                        mContext,
                         BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED),
                         MATCH_PARENT,
                         ENABLED,
@@ -309,7 +301,6 @@
                         uiEventLogger,
                         logger,
                         sysuiDialogFactory,
-                        LayoutInflater.from(mContext)
                     )
                     .createDialog()
             dialog.show()
@@ -323,7 +314,6 @@
         testScope.runTest {
             val dialog =
                 BluetoothTileDialogDelegate(
-                        mContext,
                         BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED),
                         MATCH_PARENT,
                         ENABLED,
@@ -334,7 +324,6 @@
                         uiEventLogger,
                         logger,
                         sysuiDialogFactory,
-                        LayoutInflater.from(mContext)
                     )
                     .createDialog()
             dialog.show()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt
index 39e2413..c8a2aa6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt
@@ -16,7 +16,7 @@
 
 package com.android.systemui.qs.tiles.dialog.bluetooth
 
-import android.content.pm.UserInfo
+import android.bluetooth.BluetoothAdapter
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.view.View
@@ -26,19 +26,18 @@
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.UiEventLogger
 import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.LocalBluetoothManager
 import com.android.settingslib.flags.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.DialogTransitionAnimator
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.statusbar.phone.SystemUIDialog
-import com.android.systemui.user.data.repository.FakeUserRepository
 import com.android.systemui.util.FakeSharedPreferences
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.kotlin.getMutableStateFlow
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.nullable
 import com.android.systemui.util.mockito.whenever
-import com.android.systemui.util.settings.FakeSettings
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineDispatcher
@@ -75,6 +74,8 @@
 
     @Mock private lateinit var bluetoothStateInteractor: BluetoothStateInteractor
 
+    @Mock private lateinit var bluetoothAutoOnInteractor: BluetoothAutoOnInteractor
+
     @Mock private lateinit var deviceItemInteractor: DeviceItemInteractor
 
     @Mock private lateinit var activityStarter: ActivityStarter
@@ -87,6 +88,10 @@
 
     @Mock private lateinit var uiEventLogger: UiEventLogger
 
+    @Mock private lateinit var bluetoothAdapter: BluetoothAdapter
+
+    @Mock private lateinit var localBluetoothManager: LocalBluetoothManager
+
     @Mock
     private lateinit var mBluetoothTileDialogDelegateDelegateFactory:
         BluetoothTileDialogDelegate.Factory
@@ -100,8 +105,6 @@
     private lateinit var scheduler: TestCoroutineScheduler
     private lateinit var dispatcher: CoroutineDispatcher
     private lateinit var testScope: TestScope
-    private lateinit var secureSettings: FakeSettings
-    private lateinit var userRepository: FakeUserRepository
 
     @Before
     fun setUp() {
@@ -109,14 +112,6 @@
         scheduler = TestCoroutineScheduler()
         dispatcher = UnconfinedTestDispatcher(scheduler)
         testScope = TestScope(dispatcher)
-        secureSettings = FakeSettings()
-        userRepository = FakeUserRepository()
-        userRepository.setUserInfos(listOf(SYSTEM_USER))
-        secureSettings.putIntForUser(
-            BluetoothAutoOnRepository.SETTING_NAME,
-            BluetoothAutoOnInteractor.ENABLED,
-            SYSTEM_USER_ID
-        )
         bluetoothTileDialogViewModel =
             BluetoothTileDialogViewModel(
                 deviceItemInteractor,
@@ -124,8 +119,8 @@
                 // TODO(b/316822488): Create FakeBluetoothAutoOnInteractor.
                 BluetoothAutoOnInteractor(
                     BluetoothAutoOnRepository(
-                        secureSettings,
-                        userRepository,
+                        localBluetoothManager,
+                        bluetoothAdapter,
                         testScope.backgroundScope,
                         dispatcher
                     )
@@ -148,7 +143,6 @@
         whenever(
                 mBluetoothTileDialogDelegateDelegateFactory.create(
                     any(),
-                    any(),
                     anyInt(),
                     ArgumentMatchers.anyBoolean(),
                     any(),
@@ -157,6 +151,7 @@
             )
             .thenReturn(bluetoothTileDialogDelegate)
         whenever(bluetoothTileDialogDelegate.createDialog()).thenReturn(sysuiDialog)
+        whenever(sysuiDialog.context).thenReturn(mContext)
         whenever(bluetoothTileDialogDelegate.bluetoothStateToggle)
             .thenReturn(getMutableStateFlow(false))
         whenever(bluetoothTileDialogDelegate.deviceItemClick)
@@ -169,7 +164,7 @@
     @Test
     fun testShowDialog_noAnimation() {
         testScope.runTest {
-            bluetoothTileDialogViewModel.showDialog(context, null)
+            bluetoothTileDialogViewModel.showDialog(null)
 
             verify(mDialogTransitionAnimator, never()).showFromView(any(), any(), any(), any())
         }
@@ -178,7 +173,7 @@
     @Test
     fun testShowDialog_animated() {
         testScope.runTest {
-            bluetoothTileDialogViewModel.showDialog(mContext, LinearLayout(mContext))
+            bluetoothTileDialogViewModel.showDialog(LinearLayout(mContext))
 
             verify(mDialogTransitionAnimator).showFromView(any(), any(), nullable(), anyBoolean())
         }
@@ -188,7 +183,7 @@
     fun testShowDialog_animated_callInBackgroundThread() {
         testScope.runTest {
             backgroundExecutor.execute {
-                bluetoothTileDialogViewModel.showDialog(mContext, LinearLayout(mContext))
+                bluetoothTileDialogViewModel.showDialog(LinearLayout(mContext))
 
                 verify(mDialogTransitionAnimator)
                     .showFromView(any(), any(), nullable(), anyBoolean())
@@ -199,7 +194,7 @@
     @Test
     fun testShowDialog_fetchDeviceItem() {
         testScope.runTest {
-            bluetoothTileDialogViewModel.showDialog(context, null)
+            bluetoothTileDialogViewModel.showDialog(null)
 
             verify(deviceItemInteractor).deviceItemUpdate
         }
@@ -208,7 +203,7 @@
     @Test
     fun testShowDialog_withBluetoothStateValue() {
         testScope.runTest {
-            bluetoothTileDialogViewModel.showDialog(context, null)
+            bluetoothTileDialogViewModel.showDialog(null)
 
             verify(bluetoothStateInteractor).bluetoothStateUpdate
         }
@@ -218,7 +213,7 @@
     fun testStartSettingsActivity_activityLaunched_dialogDismissed() {
         testScope.runTest {
             whenever(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice)
-            bluetoothTileDialogViewModel.showDialog(context, null)
+            bluetoothTileDialogViewModel.showDialog(null)
 
             val clickedView = View(context)
             bluetoothTileDialogViewModel.onPairNewDeviceClicked(clickedView)
@@ -265,26 +260,22 @@
     }
 
     @Test
-    fun testIsAutoOnToggleFeatureAvailable_flagOn_settingValueSet_returnTrue() {
+    fun testIsAutoOnToggleFeatureAvailable_returnTrue() {
         testScope.runTest {
+            whenever(bluetoothAdapter.isAutoOnSupported).thenReturn(true)
+
             val actual = bluetoothTileDialogViewModel.isAutoOnToggleFeatureAvailable()
             assertThat(actual).isTrue()
         }
     }
 
     @Test
-    fun testIsAutoOnToggleFeatureAvailable_flagOff_settingValueSet_returnFalse() {
+    fun testIsAutoOnToggleFeatureAvailable_returnFalse() {
         testScope.runTest {
-            mSetFlagsRule.disableFlags(Flags.FLAG_BLUETOOTH_QS_TILE_DIALOG_AUTO_ON_TOGGLE)
+            whenever(bluetoothAdapter.isAutoOnSupported).thenReturn(false)
 
             val actual = bluetoothTileDialogViewModel.isAutoOnToggleFeatureAvailable()
             assertThat(actual).isFalse()
         }
     }
-
-    companion object {
-        private const val SYSTEM_USER_ID = 0
-        private val SYSTEM_USER =
-            UserInfo(/* id= */ SYSTEM_USER_ID, /* name= */ "system user", /* flags= */ 0)
-    }
 }
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/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
index ee2eb80..0e89d80 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
@@ -845,7 +845,6 @@
     }
 
     @Test
-    @EnableFlags(com.android.systemui.Flags.FLAG_NOTIFICATION_ROW_USER_CONTEXT)
     public void imageResolver_differentNotificationUser_createsUserContext() throws Exception {
         UserHandle user = new UserHandle(33);
         Context userContext = new SysuiTestableContext(mContext);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java
index 6549193..fe0d9d0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java
@@ -23,6 +23,7 @@
 import static android.app.NotificationManager.IMPORTANCE_HIGH;
 import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE;
 
+import static com.android.systemui.concurrency.FakeExecutorKosmosKt.getFakeExecutor;
 import static com.android.systemui.statusbar.NotificationEntryHelper.modifyRanking;
 
 import static junit.framework.Assert.assertNotNull;
@@ -124,12 +125,11 @@
     private NotificationChannel mTestNotificationChannel = new NotificationChannel(
             TEST_CHANNEL_ID, TEST_CHANNEL_ID, IMPORTANCE_DEFAULT);
 
-    private KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this);
-    private TestScope mTestScope = mKosmos.getTestScope();
-    private JavaAdapter mJavaAdapter = new JavaAdapter(mTestScope.getBackgroundScope());
-    private FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock());
-    private TestableLooper mTestableLooper;
-    private Handler mHandler;
+    private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this);
+    private final TestScope mTestScope = mKosmos.getTestScope();
+    private final JavaAdapter mJavaAdapter = new JavaAdapter(mTestScope.getBackgroundScope());
+    private final FakeExecutor mExecutor = mKosmos.getFakeExecutor();
+    private final Handler mHandler = mKosmos.getFakeExecutorHandler();
     private NotificationTestHelper mHelper;
     private NotificationGutsManager mGutsManager;
 
@@ -171,10 +171,8 @@
 
     @Before
     public void setUp() {
-        mTestableLooper = TestableLooper.get(this);
         allowTestableLooperAsMainThread();
-        mHandler = Handler.createAsync(mTestableLooper.getLooper());
-        mHelper = new NotificationTestHelper(mContext, mDependency, TestableLooper.get(this));
+        mHelper = new NotificationTestHelper(mContext, mDependency);
         when(mAccessibilityManager.isTouchExplorationEnabled()).thenReturn(false);
 
         mWindowRootViewVisibilityInteractor = new WindowRootViewVisibilityInteractor(
@@ -248,7 +246,7 @@
 
         assertTrue(mGutsManager.openGutsInternal(row, 0, 0, menuItem));
         assertEquals(View.INVISIBLE, guts.getVisibility());
-        mTestableLooper.processAllMessages();
+        mExecutor.runAllReady();
         verify(guts).openControls(
                 anyInt(),
                 anyInt(),
@@ -261,7 +259,7 @@
 
         verify(guts).closeControls(anyBoolean(), anyBoolean(), anyInt(), anyInt(), anyBoolean());
         verify(row, times(1)).setGutsView(any());
-        mTestableLooper.processAllMessages();
+        mExecutor.runAllReady();
         verify(mHeadsUpManager).setGutsShown(realRow.getEntry(), false);
     }
 
@@ -352,7 +350,7 @@
         when(entry.getGuts()).thenReturn(guts);
 
         assertTrue(mGutsManager.openGutsInternal(row, 0, 0, menuItem));
-        mTestableLooper.processAllMessages();
+        mExecutor.runAllReady();
         verify(guts).openControls(
                 anyInt(),
                 anyInt(),
@@ -365,7 +363,7 @@
         row.onDensityOrFontScaleChanged();
         mGutsManager.onDensityOrFontScaleChanged(entry);
 
-        mTestableLooper.processAllMessages();
+        mExecutor.runAllReady();
 
         mGutsManager.closeAndSaveGuts(false, false, false, 0, 0, false);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt
index 012ff2e..65a960b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt
@@ -27,12 +27,11 @@
 import android.content.pm.launcherApps
 import android.graphics.Color
 import android.os.Binder
-import android.os.Handler
+import android.os.fakeExecutorHandler
 import android.os.userManager
 import android.provider.Settings
 import android.service.notification.NotificationListenerService.Ranking
 import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
 import android.testing.TestableLooper.RunWithLooper
 import android.util.ArraySet
 import android.view.View
@@ -45,6 +44,7 @@
 import com.android.internal.logging.testing.UiEventLoggerFake
 import com.android.internal.statusbar.statusBarService
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.concurrency.fakeExecutor
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.people.widget.PeopleSpaceWidgetManager
@@ -71,9 +71,7 @@
 import com.android.systemui.statusbar.policy.deviceProvisionedController
 import com.android.systemui.statusbar.policy.headsUpManager
 import com.android.systemui.testKosmos
-import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.kotlin.JavaAdapter
-import com.android.systemui.util.time.FakeSystemClock
 import com.android.systemui.wmshell.BubblesManager
 import java.util.Optional
 import junit.framework.Assert
@@ -106,9 +104,8 @@
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
     private val javaAdapter = JavaAdapter(testScope.backgroundScope)
-    private val executor = FakeExecutor(FakeSystemClock())
-    private lateinit var testableLooper: TestableLooper
-    private lateinit var handler: Handler
+    private val executor = kosmos.fakeExecutor
+    private val handler = kosmos.fakeExecutorHandler
     private lateinit var helper: NotificationTestHelper
     private lateinit var gutsManager: NotificationGutsManager
     private lateinit var windowRootViewVisibilityInteractor: WindowRootViewVisibilityInteractor
@@ -148,10 +145,8 @@
         MockitoAnnotations.initMocks(this)
         val sceneContainerFlags = kosmos.fakeSceneContainerFlags
         sceneContainerFlags.enabled = true
-        testableLooper = TestableLooper.get(this)
         allowTestableLooperAsMainThread()
-        handler = Handler.createAsync(testableLooper.getLooper())
-        helper = NotificationTestHelper(mContext, mDependency, TestableLooper.get(this))
+        helper = NotificationTestHelper(mContext, mDependency)
         Mockito.`when`(accessibilityManager.isTouchExplorationEnabled).thenReturn(false)
         windowRootViewVisibilityInteractor =
             WindowRootViewVisibilityInteractor(
@@ -227,7 +222,7 @@
         Mockito.`when`(row.guts).thenReturn(guts)
         Assert.assertTrue(gutsManager.openGutsInternal(row, 0, 0, menuItem))
         assertEquals(View.INVISIBLE.toLong(), guts.visibility.toLong())
-        testableLooper.processAllMessages()
+        executor.runAllReady()
         verify(guts)
             .openControls(
                 ArgumentMatchers.anyInt(),
@@ -247,7 +242,7 @@
                 ArgumentMatchers.anyBoolean()
             )
         verify(row, Mockito.times(1)).setGutsView(ArgumentMatchers.any())
-        testableLooper.processAllMessages()
+        executor.runAllReady()
         verify(headsUpManager).setGutsShown(realRow.entry, false)
     }
 
@@ -343,7 +338,7 @@
         Mockito.`when`(entry.row).thenReturn(row)
         Mockito.`when`(entry.getGuts()).thenReturn(guts)
         Assert.assertTrue(gutsManager.openGutsInternal(row, 0, 0, menuItem))
-        testableLooper.processAllMessages()
+        executor.runAllReady()
         verify(guts)
             .openControls(
                 ArgumentMatchers.anyInt(),
@@ -356,7 +351,7 @@
         verify(row).setGutsView(ArgumentMatchers.any())
         row.onDensityOrFontScaleChanged()
         gutsManager.onDensityOrFontScaleChanged(entry)
-        testableLooper.processAllMessages()
+        executor.runAllReady()
         gutsManager.closeAndSaveGuts(false, false, false, 0, 0, false)
         verify(guts)
             .closeControls(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
index 09a3eb48..954335e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
@@ -615,10 +615,8 @@
 
         LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
                 Context.LAYOUT_INFLATER_SERVICE);
-        if (com.android.systemui.Flags.notificationRowUserContext()) {
-            inflater.setFactory2(new RowInflaterTask.RowAsyncLayoutInflater(entry, mSystemClock,
-                    mRowInflaterTaskLogger));
-        }
+        inflater.setFactory2(new RowInflaterTask.RowAsyncLayoutInflater(entry, mSystemClock,
+                mRowInflaterTaskLogger));
         mRow = (ExpandableNotificationRow) inflater.inflate(
                 R.layout.status_bar_notification_row,
                 null /* root */,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java
index 8f88501..a15b4cd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java
@@ -25,8 +25,6 @@
 import android.graphics.drawable.Icon;
 import android.os.Bundle;
 import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-import android.testing.TestableLooper.RunWithLooper;
 import android.view.LayoutInflater;
 import android.view.View;
 
@@ -44,7 +42,6 @@
 
 @RunWith(AndroidTestingRunner.class)
 @SmallTest
-@RunWithLooper
 public class NotificationBigPictureTemplateViewWrapperTest extends SysuiTestCase {
 
     private View mView;
@@ -53,11 +50,7 @@
 
     @Before
     public void setup() throws Exception {
-        allowTestableLooperAsMainThread();
-        NotificationTestHelper helper = new NotificationTestHelper(
-                mContext,
-                mDependency,
-                TestableLooper.get(this));
+        NotificationTestHelper helper = new NotificationTestHelper(mContext, mDependency);
         mView = LayoutInflater.from(mContext).inflate(
                 com.android.internal.R.layout.notification_template_material_big_picture, null);
         mRow = helper.createRow();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt
index 3fa68bb..fe2971c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt
@@ -18,8 +18,6 @@
 
 import android.graphics.drawable.AnimatedImageDrawable
 import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
-import android.testing.TestableLooper.RunWithLooper
 import android.view.View
 import androidx.test.filters.SmallTest
 import com.android.internal.R
@@ -41,7 +39,6 @@
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
-@RunWithLooper
 class NotificationConversationTemplateViewWrapperTest : SysuiTestCase() {
 
     private lateinit var mRow: ExpandableNotificationRow
@@ -49,8 +46,7 @@
 
     @Before
     fun setUp() {
-        allowTestableLooperAsMainThread()
-        helper = NotificationTestHelper(mContext, mDependency, TestableLooper.get(this))
+        helper = NotificationTestHelper(mContext, mDependency)
         mRow = helper.createRow()
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCustomViewWrapperTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCustomViewWrapperTest.java
index 45f7c5a..2d72c7e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCustomViewWrapperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCustomViewWrapperTest.java
@@ -17,8 +17,6 @@
 package com.android.systemui.statusbar.notification.row.wrapper;
 
 import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-import android.testing.TestableLooper.RunWithLooper;
 import android.view.View;
 import android.widget.RemoteViews;
 
@@ -36,18 +34,13 @@
 
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
-@RunWithLooper
 public class NotificationCustomViewWrapperTest extends SysuiTestCase {
 
     private ExpandableNotificationRow mRow;
 
     @Before
     public void setUp() throws Exception {
-        allowTestableLooperAsMainThread();
-        NotificationTestHelper helper = new NotificationTestHelper(
-                mContext,
-                mDependency,
-                TestableLooper.get(this));
+        NotificationTestHelper helper = new NotificationTestHelper(mContext, mDependency);
         mRow = helper.createRow();
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapperTest.kt
index c0444b5..f26c18b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapperTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapperTest.kt
@@ -18,8 +18,6 @@
 
 import android.graphics.drawable.AnimatedImageDrawable
 import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
-import android.testing.TestableLooper.RunWithLooper
 import android.view.View
 import androidx.test.filters.SmallTest
 import com.android.internal.widget.MessagingGroup
@@ -39,7 +37,6 @@
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
-@RunWithLooper
 class NotificationMessagingTemplateViewWrapperTest : SysuiTestCase() {
 
     private lateinit var mRow: ExpandableNotificationRow
@@ -47,8 +44,7 @@
 
     @Before
     fun setUp() {
-        allowTestableLooperAsMainThread()
-        helper = NotificationTestHelper(mContext, mDependency, TestableLooper.get(this))
+        helper = NotificationTestHelper(mContext, mDependency)
         mRow = helper.createRow()
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapperTest.kt
index f7632aa..54eed26 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapperTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapperTest.kt
@@ -69,7 +69,7 @@
             TestUiOffloadThread(looper.looper)
         )
 
-        helper = NotificationTestHelper(mContext, mDependency, looper)
+        helper = NotificationTestHelper(mContext, mDependency)
         row = helper.createRow()
         // Some code in the view iterates through parents so we need some extra containers around
         // it.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapperTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapperTest.java
index 93a9e59..e3a77d3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapperTest.java
@@ -39,7 +39,6 @@
 
 @RunWith(AndroidTestingRunner.class)
 @SmallTest
-@RunWithLooper
 public class NotificationViewWrapperTest extends SysuiTestCase {
 
     private View mView;
@@ -48,13 +47,9 @@
 
     @Before
     public void setup() throws Exception {
-        allowTestableLooperAsMainThread();
         mView = mock(View.class);
         when(mView.getContext()).thenReturn(mContext);
-        NotificationTestHelper helper = new NotificationTestHelper(
-                mContext,
-                mDependency,
-                TestableLooper.get(this));
+        NotificationTestHelper helper = new NotificationTestHelper(mContext, mDependency);
         mRow = helper.createRow();
         mNotificationViewWrapper = new TestableNotificationViewWrapper(mContext, mView, mRow);
     }
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/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
index 0a18eb6..c308a98 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
@@ -35,9 +35,13 @@
 import com.android.systemui.res.R
 import com.android.systemui.shade.data.repository.fakeShadeRepository
 import com.android.systemui.statusbar.data.repository.fakeRemoteInputRepository
+import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
 import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
 import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs
 import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
+import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
+import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
+import com.android.systemui.statusbar.notification.stack.data.repository.setNotifications
 import com.android.systemui.statusbar.policy.data.repository.fakeUserSetupRepository
 import com.android.systemui.statusbar.policy.data.repository.zenModeRepository
 import com.android.systemui.statusbar.policy.fakeConfigurationController
@@ -70,6 +74,7 @@
     private val fakeRemoteInputRepository = kosmos.fakeRemoteInputRepository
     private val fakeShadeRepository = kosmos.fakeShadeRepository
     private val fakeUserSetupRepository = kosmos.fakeUserSetupRepository
+    private val headsUpRepository = kosmos.headsUpNotificationRepository
     private val zenModeRepository = kosmos.zenModeRepository
 
     val underTest = kosmos.notificationListViewModel
@@ -125,35 +130,35 @@
         }
 
     @Test
-    fun testShouldShowEmptyShadeView_trueWhenNoNotifs() =
+    fun testShouldIncludeEmptyShadeView_trueWhenNoNotifs() =
         testScope.runTest {
-            val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
+            val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
 
             // WHEN has no notifs
             activeNotificationListRepository.setActiveNotifs(count = 0)
             runCurrent()
 
             // THEN empty shade is visible
-            assertThat(shouldShow).isTrue()
+            assertThat(shouldInclude).isTrue()
         }
 
     @Test
-    fun testShouldShowEmptyShadeView_falseWhenNotifs() =
+    fun testShouldIncludeEmptyShadeView_falseWhenNotifs() =
         testScope.runTest {
-            val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
+            val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
 
             // WHEN has notifs
             activeNotificationListRepository.setActiveNotifs(count = 2)
             runCurrent()
 
             // THEN empty shade is not visible
-            assertThat(shouldShow).isFalse()
+            assertThat(shouldInclude).isFalse()
         }
 
     @Test
-    fun testShouldShowEmptyShadeView_falseWhenQsExpandedDefault() =
+    fun testShouldIncludeEmptyShadeView_falseWhenQsExpandedDefault() =
         testScope.runTest {
-            val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
+            val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
 
             // WHEN has no notifs
             activeNotificationListRepository.setActiveNotifs(count = 0)
@@ -162,13 +167,13 @@
             runCurrent()
 
             // THEN empty shade is not visible
-            assertThat(shouldShow).isFalse()
+            assertThat(shouldInclude).isFalse()
         }
 
     @Test
-    fun testShouldShowEmptyShadeView_trueWhenQsExpandedInSplitShade() =
+    fun testShouldIncludeEmptyShadeView_trueWhenQsExpandedInSplitShade() =
         testScope.runTest {
-            val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
+            val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
 
             // WHEN has no notifs
             activeNotificationListRepository.setActiveNotifs(count = 0)
@@ -180,13 +185,13 @@
             runCurrent()
 
             // THEN empty shade is visible
-            assertThat(shouldShow).isTrue()
+            assertThat(shouldInclude).isTrue()
         }
 
     @Test
-    fun testShouldShowEmptyShadeView_trueWhenLockedShade() =
+    fun testShouldIncludeEmptyShadeView_trueWhenLockedShade() =
         testScope.runTest {
-            val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
+            val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
 
             // WHEN has no notifs
             activeNotificationListRepository.setActiveNotifs(count = 0)
@@ -195,13 +200,13 @@
             runCurrent()
 
             // THEN empty shade is visible
-            assertThat(shouldShow).isTrue()
+            assertThat(shouldInclude).isTrue()
         }
 
     @Test
-    fun testShouldShowEmptyShadeView_falseWhenKeyguard() =
+    fun testShouldIncludeEmptyShadeView_falseWhenKeyguard() =
         testScope.runTest {
-            val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
+            val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
 
             // WHEN has no notifs
             activeNotificationListRepository.setActiveNotifs(count = 0)
@@ -210,13 +215,13 @@
             runCurrent()
 
             // THEN empty shade is not visible
-            assertThat(shouldShow).isFalse()
+            assertThat(shouldInclude).isFalse()
         }
 
     @Test
-    fun testShouldShowEmptyShadeView_falseWhenStartingToSleep() =
+    fun testShouldIncludeEmptyShadeView_falseWhenStartingToSleep() =
         testScope.runTest {
-            val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
+            val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
 
             // WHEN has no notifs
             activeNotificationListRepository.setActiveNotifs(count = 0)
@@ -227,7 +232,7 @@
             runCurrent()
 
             // THEN empty shade is not visible
-            assertThat(shouldShow).isFalse()
+            assertThat(shouldInclude).isFalse()
         }
 
     @Test
@@ -277,9 +282,9 @@
         }
 
     @Test
-    fun testShouldShowFooterView_trueWhenShade() =
+    fun testShouldIncludeFooterView_trueWhenShade() =
         testScope.runTest {
-            val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+            val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
 
             // WHEN has notifs
             activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -289,13 +294,13 @@
             runCurrent()
 
             // THEN footer is visible
-            assertThat(shouldShow?.value).isTrue()
+            assertThat(shouldInclude?.value).isTrue()
         }
 
     @Test
-    fun testShouldShowFooterView_trueWhenLockedShade() =
+    fun testShouldIncludeFooterView_trueWhenLockedShade() =
         testScope.runTest {
-            val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+            val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
 
             // WHEN has notifs
             activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -305,13 +310,13 @@
             runCurrent()
 
             // THEN footer is visible
-            assertThat(shouldShow?.value).isTrue()
+            assertThat(shouldInclude?.value).isTrue()
         }
 
     @Test
-    fun testShouldShowFooterView_falseWhenKeyguard() =
+    fun testShouldIncludeFooterView_falseWhenKeyguard() =
         testScope.runTest {
-            val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+            val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
 
             // WHEN has notifs
             activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -320,13 +325,13 @@
             runCurrent()
 
             // THEN footer is not visible
-            assertThat(shouldShow?.value).isFalse()
+            assertThat(shouldInclude?.value).isFalse()
         }
 
     @Test
-    fun testShouldShowFooterView_falseWhenUserNotSetUp() =
+    fun testShouldIncludeFooterView_falseWhenUserNotSetUp() =
         testScope.runTest {
-            val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+            val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
 
             // WHEN has notifs
             activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -338,13 +343,13 @@
             runCurrent()
 
             // THEN footer is not visible
-            assertThat(shouldShow?.value).isFalse()
+            assertThat(shouldInclude?.value).isFalse()
         }
 
     @Test
-    fun testShouldShowFooterView_falseWhenStartingToSleep() =
+    fun testShouldIncludeFooterView_falseWhenStartingToSleep() =
         testScope.runTest {
-            val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+            val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
 
             // WHEN has notifs
             activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -356,13 +361,13 @@
             runCurrent()
 
             // THEN footer is not visible
-            assertThat(shouldShow?.value).isFalse()
+            assertThat(shouldInclude?.value).isFalse()
         }
 
     @Test
-    fun testShouldShowFooterView_falseWhenQsExpandedDefault() =
+    fun testShouldIncludeFooterView_falseWhenQsExpandedDefault() =
         testScope.runTest {
-            val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+            val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
 
             // WHEN has notifs
             activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -375,13 +380,13 @@
             runCurrent()
 
             // THEN footer is not visible
-            assertThat(shouldShow?.value).isFalse()
+            assertThat(shouldInclude?.value).isFalse()
         }
 
     @Test
-    fun testShouldShowFooterView_trueWhenQsExpandedSplitShade() =
+    fun testShouldIncludeFooterView_trueWhenQsExpandedSplitShade() =
         testScope.runTest {
-            val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+            val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
 
             // WHEN has notifs
             activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -396,13 +401,13 @@
             runCurrent()
 
             // THEN footer is visible
-            assertThat(shouldShow?.value).isTrue()
+            assertThat(shouldInclude?.value).isTrue()
         }
 
     @Test
-    fun testShouldShowFooterView_falseWhenRemoteInputActive() =
+    fun testShouldIncludeFooterView_falseWhenRemoteInputActive() =
         testScope.runTest {
-            val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+            val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
 
             // WHEN has notifs
             activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -414,29 +419,13 @@
             runCurrent()
 
             // THEN footer is not visible
-            assertThat(shouldShow?.value).isFalse()
+            assertThat(shouldInclude?.value).isFalse()
         }
 
     @Test
-    fun testShouldShowFooterView_falseWhenShadeIsClosed() =
+    fun testShouldIncludeFooterView_animatesWhenShade() =
         testScope.runTest {
-            val shouldShow by collectLastValue(underTest.shouldShowFooterView)
-
-            // WHEN has notifs
-            activeNotificationListRepository.setActiveNotifs(count = 2)
-            // AND shade is closed
-            fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
-            fakeShadeRepository.setLegacyShadeExpansion(0f)
-            runCurrent()
-
-            // THEN footer is not visible
-            assertThat(shouldShow?.value).isFalse()
-        }
-
-    @Test
-    fun testShouldShowFooterView_animatesWhenShade() =
-        testScope.runTest {
-            val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+            val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
 
             // WHEN has notifs
             activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -446,13 +435,13 @@
             runCurrent()
 
             // THEN footer visibility animates
-            assertThat(shouldShow?.isAnimating).isTrue()
+            assertThat(shouldInclude?.isAnimating).isTrue()
         }
 
     @Test
-    fun testShouldShowFooterView_notAnimatingOnKeyguard() =
+    fun testShouldIncludeFooterView_notAnimatingOnKeyguard() =
         testScope.runTest {
-            val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+            val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
 
             // WHEN has notifs
             activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -462,6 +451,150 @@
             runCurrent()
 
             // THEN footer visibility does not animate
-            assertThat(shouldShow?.isAnimating).isFalse()
+            assertThat(shouldInclude?.isAnimating).isFalse()
+        }
+
+    @Test
+    fun testShouldHideFooterView_trueWhenShadeIsClosed() =
+        testScope.runTest {
+            val shouldHide by collectLastValue(underTest.shouldHideFooterView)
+
+            // WHEN shade is closed
+            fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
+            fakeShadeRepository.setLegacyShadeExpansion(0f)
+            runCurrent()
+
+            // THEN footer is hidden
+            assertThat(shouldHide).isTrue()
+        }
+
+    @Test
+    fun testShouldHideFooterView_falseWhenShadeIsOpen() =
+        testScope.runTest {
+            val shouldHide by collectLastValue(underTest.shouldHideFooterView)
+
+            // WHEN shade is open
+            fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
+            fakeShadeRepository.setLegacyShadeExpansion(1f)
+            runCurrent()
+
+            // THEN footer is hidden
+            assertThat(shouldHide).isFalse()
+        }
+
+    @Test
+    @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME)
+    fun testPinnedHeadsUpRows_filtersForPinnedItems() =
+        testScope.runTest {
+            val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)
+
+            // WHEN there are no pinned rows
+            val rows =
+                arrayListOf(
+                    fakeHeadsUpRowRepository(key = "0"),
+                    fakeHeadsUpRowRepository(key = "1"),
+                    fakeHeadsUpRowRepository(key = "2"),
+                )
+            headsUpRepository.setNotifications(
+                rows,
+            )
+            runCurrent()
+
+            // THEN the list is empty
+            assertThat(pinnedHeadsUpRows).isEmpty()
+
+            // WHEN a row gets pinned
+            rows[0].isPinned.value = true
+            runCurrent()
+
+            // THEN it's added to the list
+            assertThat(pinnedHeadsUpRows).containsExactly(rows[0])
+
+            // WHEN more rows are pinned
+            rows[1].isPinned.value = true
+            runCurrent()
+
+            // THEN they are all in the list
+            assertThat(pinnedHeadsUpRows).containsExactly(rows[0], rows[1])
+
+            // WHEN a row gets unpinned
+            rows[0].isPinned.value = false
+            runCurrent()
+
+            // THEN it's removed from the list
+            assertThat(pinnedHeadsUpRows).containsExactly(rows[1])
+        }
+
+    @Test
+    @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME)
+    fun testHasPinnedHeadsUpRows_true() =
+        testScope.runTest {
+            val hasPinnedHeadsUpRow by collectLastValue(underTest.hasPinnedHeadsUpRow)
+
+            headsUpRepository.setNotifications(
+                fakeHeadsUpRowRepository(key = "0", isPinned = true),
+                fakeHeadsUpRowRepository(key = "1")
+            )
+            runCurrent()
+
+            assertThat(hasPinnedHeadsUpRow).isTrue()
+        }
+
+    @Test
+    @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME)
+    fun testHasPinnedHeadsUpRows_false() =
+        testScope.runTest {
+            val hasPinnedHeadsUpRow by collectLastValue(underTest.hasPinnedHeadsUpRow)
+
+            headsUpRepository.setNotifications(
+                fakeHeadsUpRowRepository(key = "0"),
+                fakeHeadsUpRowRepository(key = "1"),
+            )
+            runCurrent()
+
+            assertThat(hasPinnedHeadsUpRow).isFalse()
+        }
+
+    @Test
+    @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME)
+    fun testTopHeadsUpRow_emptyList_null() =
+        testScope.runTest {
+            val topHeadsUpRow by collectLastValue(underTest.topHeadsUpRow)
+
+            headsUpRepository.setNotifications(emptyList())
+            runCurrent()
+
+            assertThat(topHeadsUpRow).isNull()
+        }
+
+    @Test
+    @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME)
+    fun testHeadsUpAnimationsEnabled_true() =
+        testScope.runTest {
+            val animationsEnabled by collectLastValue(underTest.headsUpAnimationsEnabled)
+
+            fakeShadeRepository.setQsExpansion(0.0f)
+            fakeKeyguardRepository.setKeyguardShowing(false)
+            runCurrent()
+
+            assertThat(animationsEnabled).isTrue()
+        }
+
+    @Test
+    @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME)
+    fun testHeadsUpAnimationsEnabled_keyguardShowing_false() =
+        testScope.runTest {
+            val animationsEnabled by collectLastValue(underTest.headsUpAnimationsEnabled)
+
+            fakeShadeRepository.setQsExpansion(0.0f)
+            fakeKeyguardRepository.setKeyguardShowing(true)
+            runCurrent()
+
+            assertThat(animationsEnabled).isFalse()
+        }
+
+    private fun fakeHeadsUpRowRepository(key: String, isPinned: Boolean = false) =
+        FakeHeadsUpRowRepository(key = key, elementKey = Any()).apply {
+            this.isPinned.value = isPinned
         }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
index 9855651..f761bcf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
@@ -868,6 +868,24 @@
         }
 
     @Test
+    fun networkName_usingEagerStrategy_retainsNameBetweenSubscribers() =
+        testScope.runTest {
+            // Use the [StateFlow.value] getter so we can prove that the collection happens
+            // even when there is no [Job]
+
+            // Starts out default
+            assertThat(underTest.networkName.value).isEqualTo(DEFAULT_NAME_MODEL)
+
+            val intent = spnIntent()
+            val captor = argumentCaptor<BroadcastReceiver>()
+            verify(context).registerReceiver(captor.capture(), any())
+            captor.value!!.onReceive(context, intent)
+
+            // The value is still there despite no active subscribers
+            assertThat(underTest.networkName.value).isEqualTo(intent.toNetworkNameModel(SEP))
+        }
+
+    @Test
     fun operatorAlphaShort_tracked() =
         testScope.runTest {
             var latest: String? = null
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/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/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 a84899e..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
@@ -52,6 +52,7 @@
         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/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
index e861892..c879588 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
@@ -19,6 +19,7 @@
 package com.android.systemui.kosmos
 
 import android.content.applicationContext
+import android.os.fakeExecutorHandler
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.bouncer.data.repository.bouncerRepository
 import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor
@@ -27,6 +28,7 @@
 import com.android.systemui.common.ui.domain.interactor.configurationInteractor
 import com.android.systemui.communal.data.repository.fakeCommunalRepository
 import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.concurrency.fakeExecutor
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
 import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
 import com.android.systemui.flags.fakeFeatureFlagsClassic
@@ -65,6 +67,8 @@
     val testScope by lazy { kosmos.testScope }
     val fakeFeatureFlags by lazy { kosmos.fakeFeatureFlagsClassic }
     val fakeSceneContainerFlags by lazy { kosmos.fakeSceneContainerFlags }
+    val fakeExecutor by lazy { kosmos.fakeExecutor }
+    val fakeExecutorHandler by lazy { kosmos.fakeExecutorHandler }
     val configurationRepository by lazy { kosmos.fakeConfigurationRepository }
     val configurationInteractor by lazy { kosmos.configurationInteractor }
     val bouncerRepository by lazy { kosmos.bouncerRepository }
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/data/repository/FakeHeadsUpRowRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/FakeHeadsUpRowRepository.kt
new file mode 100644
index 0000000..2e983a8
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/FakeHeadsUpRowRepository.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.statusbar.notification.data.repository
+
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeHeadsUpRowRepository(override val key: String, override val elementKey: Any) :
+    HeadsUpRowRepository {
+    override val isPinned: MutableStateFlow<Boolean> = MutableStateFlow(false)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt
index 25864ae..165c942 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt
@@ -18,11 +18,16 @@
 
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
-import com.android.systemui.statusbar.notification.data.repository.HeadsUpNotificationRepository
+import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository
+import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 
 val Kosmos.headsUpNotificationRepository by Fixture { FakeHeadsUpNotificationRepository() }
 
-class FakeHeadsUpNotificationRepository : HeadsUpNotificationRepository {
-    override val hasPinnedHeadsUp = MutableStateFlow(false)
+class FakeHeadsUpNotificationRepository : HeadsUpRepository {
+    override val headsUpAnimatingAway: MutableStateFlow<Boolean> = MutableStateFlow(false)
+    override val topHeadsUpRow: Flow<HeadsUpRowRepository?> = MutableStateFlow(null)
+    override val activeHeadsUpRows: MutableStateFlow<Set<HeadsUpRowRepository>> =
+        MutableStateFlow(emptySet())
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationsRepositoryExt.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationsRepositoryExt.kt
new file mode 100644
index 0000000..9be7dfe
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationsRepositoryExt.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack.data.repository
+
+import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository
+
+fun FakeHeadsUpNotificationRepository.setNotifications(notifications: List<HeadsUpRowRepository>) {
+    setNotifications(*notifications.toTypedArray())
+}
+
+fun FakeHeadsUpNotificationRepository.setNotifications(vararg notifications: HeadsUpRowRepository) {
+    this.activeHeadsUpRows.value = notifications.toSet()
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt
index 2de26f1..ee3216b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt
@@ -28,6 +28,7 @@
 import com.android.systemui.statusbar.notification.stack.displaySwitchNotificationsHiderTracker
 import com.android.systemui.statusbar.notification.stack.ui.view.notificationStatsLogger
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationListViewModel
+import com.android.systemui.statusbar.notification.ui.viewbinder.headsUpNotificationViewBinder
 import com.android.systemui.statusbar.phone.notificationIconAreaController
 import java.util.Optional
 
@@ -37,6 +38,7 @@
         backgroundDispatcher = testDispatcher,
         configuration = configurationState,
         falsingManager = falsingManager,
+        hunBinder = headsUpNotificationViewBinder,
         iconAreaController = notificationIconAreaController,
         loggerOptional = Optional.of(notificationStatsLogger),
         metricsLogger = metricsLogger,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt
index 930a4bb..c65d0a3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.notification.stack.ui.viewmodel
 
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.kosmos.testDispatcher
@@ -25,6 +26,7 @@
 import com.android.systemui.statusbar.notification.domain.interactor.seenNotificationsInteractor
 import com.android.systemui.statusbar.notification.footer.ui.viewmodel.footerViewModel
 import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.notificationShelfViewModel
+import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
 import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackInteractor
 import com.android.systemui.statusbar.policy.domain.interactor.userSetupInteractor
 import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor
@@ -38,6 +40,8 @@
         Optional.of(notificationListLoggerViewModel),
         activeNotificationsInteractor,
         notificationStackInteractor,
+        headsUpNotificationInteractor,
+        keyguardInteractor,
         remoteInputInteractor,
         seenNotificationsInteractor,
         shadeInteractor,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinderKosmos.kt
new file mode 100644
index 0000000..6a995c0
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinderKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.ui.viewbinder
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationListViewModel
+
+val Kosmos.headsUpNotificationViewBinder by
+    Kosmos.Fixture { HeadsUpNotificationViewBinder(viewModel = notificationListViewModel) }
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/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/features.aconfig b/services/autofill/features.aconfig
index 532db12..c130cee 100644
--- a/services/autofill/features.aconfig
+++ b/services/autofill/features.aconfig
@@ -16,6 +16,7 @@
 
 flag {
     name: "autofill_credman_dev_integration"
+    is_exported: true
     namespace: "autofill"
     description: "Guards against Autofill-Credman Phase1 developer integration via new APIs"
     bug: "320730001"
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/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/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/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index de000bf..0f951746 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -6854,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:
@@ -6871,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;
@@ -6895,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) {
@@ -12375,7 +12360,8 @@
     }
 
     private boolean callerHasPermission(String permission) {
-        return mContext.checkCallingPermission(permission) == PackageManager.PERMISSION_GRANTED;
+        return mContext.checkCallingOrSelfPermission(permission)
+                == PackageManager.PERMISSION_GRANTED;
     }
 
     /** @return true if projection is a valid MediaProjection that can project audio. */
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index 04e7f77..851d197 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -894,6 +894,7 @@
     @Nullable
     private HdrBrightnessData mHdrBrightnessData;
 
+    // Null if low brightness mode is disabled - in config or by flag.
     @Nullable
     public LowBrightnessData mLowBrightnessData;
 
@@ -1063,6 +1064,9 @@
      * @return The brightness mapping nits array.
      */
     public float[] getNits() {
+        if (mLowBrightnessData != null) {
+            return mLowBrightnessData.mNits;
+        }
         return mNits;
     }
 
@@ -1071,7 +1075,11 @@
      *
      * @return The backlight mapping value array.
      */
+    @VisibleForTesting
     public float[] getBacklight() {
+        if (mLowBrightnessData != null) {
+            return mLowBrightnessData.mBacklight;
+        }
         return mBacklight;
     }
 
@@ -1083,9 +1091,26 @@
      * @return backlight value on the HAL scale of 0-1
      */
     public float getBacklightFromBrightness(float brightness) {
+        if (mLowBrightnessData != null) {
+            return mLowBrightnessData.mBrightnessToBacklight.interpolate(brightness);
+        }
         return mBrightnessToBacklightSpline.interpolate(brightness);
     }
 
+    private float getBrightnessFromBacklight(float brightness) {
+        if (mLowBrightnessData != null) {
+            return mLowBrightnessData.mBacklightToBrightness.interpolate(brightness);
+        }
+        return mBacklightToBrightnessSpline.interpolate(brightness);
+    }
+
+    private Spline getBacklightToBrightnessSpline() {
+        if (mLowBrightnessData != null) {
+            return mLowBrightnessData.mBacklightToBrightness;
+        }
+        return mBacklightToBrightnessSpline;
+    }
+
     /**
      * Calculates the nits value for the specified backlight value if a mapping exists.
      *
@@ -1093,6 +1118,14 @@
      * exits.
      */
     public float getNitsFromBacklight(float backlight) {
+        if (mLowBrightnessData != null) {
+            if (mLowBrightnessData.mBacklightToNits == null) {
+                return INVALID_NITS;
+            }
+            backlight = Math.max(backlight, mBacklightMinimum);
+            return mLowBrightnessData.mBacklightToNits.interpolate(backlight);
+        }
+
         if (mBacklightToNitsSpline == null) {
             return INVALID_NITS;
         }
@@ -1100,6 +1133,20 @@
         return mBacklightToNitsSpline.interpolate(backlight);
     }
 
+    private float getBacklightFromNits(float nits) {
+        if (mLowBrightnessData != null) {
+            return mLowBrightnessData.mNitsToBacklight.interpolate(nits);
+        }
+        return mNitsToBacklightSpline.interpolate(nits);
+    }
+
+    private Spline getNitsToBacklightSpline() {
+        if (mLowBrightnessData != null) {
+            return mLowBrightnessData.mNitsToBacklight;
+        }
+        return mNitsToBacklightSpline;
+    }
+
     /**
      * @return true if there is sdrHdrRatioMap, false otherwise.
      */
@@ -1126,13 +1173,13 @@
 
         float ratio = Math.min(mSdrToHdrRatioSpline.interpolate(nits), maxDesiredHdrSdrRatio);
         float hdrNits = nits * ratio;
-        if (mNitsToBacklightSpline == null) {
+        if (getNitsToBacklightSpline() == null) {
             return PowerManager.BRIGHTNESS_INVALID;
         }
 
-        float hdrBacklight = mNitsToBacklightSpline.interpolate(hdrNits);
+        float hdrBacklight = getBacklightFromNits(hdrNits);
         hdrBacklight = Math.max(mBacklightMinimum, Math.min(mBacklightMaximum, hdrBacklight));
-        float hdrBrightness = mBacklightToBrightnessSpline.interpolate(hdrBacklight);
+        float hdrBrightness = getBrightnessFromBacklight(hdrBacklight);
 
         if (DEBUG) {
             Slog.d(TAG, "getHdrBrightnessFromSdr: sdr brightness " + brightness
@@ -1154,6 +1201,9 @@
      * @return brightness array
      */
     public float[] getBrightness() {
+        if (mLowBrightnessData != null) {
+            return mLowBrightnessData.mBrightness;
+        }
         return mBrightness;
     }
 
@@ -1987,7 +2037,8 @@
                 + "mHdrBrightnessData= " + mHdrBrightnessData + "\n"
                 + "mBrightnessCapForWearBedtimeMode= " + mBrightnessCapForWearBedtimeMode
                 + "\n"
-                + (mLowBrightnessData != null ? mLowBrightnessData.toString() : "")
+                + "mLowBrightnessData:" + (mLowBrightnessData != null
+                ? mLowBrightnessData.toString() : "null")
                 + "}";
     }
 
@@ -2588,9 +2639,9 @@
                     // A negative value means that there's no threshold
                     mLowDisplayBrightnessThresholds[i] = thresholdNits;
                 } else {
-                    float thresholdBacklight = mNitsToBacklightSpline.interpolate(thresholdNits);
+                    float thresholdBacklight = getBacklightFromNits(thresholdNits);
                     mLowDisplayBrightnessThresholds[i] =
-                            mBacklightToBrightnessSpline.interpolate(thresholdBacklight);
+                            getBrightnessFromBacklight(thresholdBacklight);
                 }
 
                 mLowAmbientBrightnessThresholds[i] = lowerThresholdDisplayBrightnessPoints
@@ -2639,9 +2690,9 @@
                     // A negative value means that there's no threshold
                     mHighDisplayBrightnessThresholds[i] = thresholdNits;
                 } else {
-                    float thresholdBacklight = mNitsToBacklightSpline.interpolate(thresholdNits);
+                    float thresholdBacklight = getBacklightFromNits(thresholdNits);
                     mHighDisplayBrightnessThresholds[i] =
-                            mBacklightToBrightnessSpline.interpolate(thresholdBacklight);
+                            getBrightnessFromBacklight(thresholdBacklight);
                 }
 
                 mHighAmbientBrightnessThresholds[i] = higherThresholdDisplayBrightnessPoints
@@ -2658,7 +2709,7 @@
         loadAutoBrightnessBrighteningLightDebounceIdle(autoBrightness);
         loadAutoBrightnessDarkeningLightDebounceIdle(autoBrightness);
         mDisplayBrightnessMapping = new DisplayBrightnessMappingConfig(mContext, mFlags,
-                autoBrightness, mBacklightToBrightnessSpline);
+                autoBrightness, getBacklightToBrightnessSpline());
         loadEnableAutoBrightness(autoBrightness);
     }
 
@@ -2832,17 +2883,10 @@
     // 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;
-        }
+
+        // Create original brightness splines - not using low brightness mode arrays - this is
+        // so that we can continue to log the original brightness splines.
 
         mBrightness = new float[mBacklight.length];
         for (int i = 0; i < mBrightness.length; i++) {
@@ -2884,7 +2928,7 @@
                         + mBacklightMaximum);
             }
             mHbmData.transitionPoint =
-                    mBacklightToBrightnessSpline.interpolate(transitionPointBacklightScale);
+                    getBrightnessFromBacklight(transitionPointBacklightScale);
             final HbmTiming hbmTiming = hbm.getTiming_all();
             mHbmData.timeWindowMillis = hbmTiming.getTimeWindowSecs_all().longValue() * 1000;
             mHbmData.timeMaxMillis = hbmTiming.getTimeMaxSecs_all().longValue() * 1000;
@@ -2953,7 +2997,7 @@
                         continue;
                     }
                     luxToTransitionPointMap.put(lux,
-                            mBacklightToBrightnessSpline.interpolate(maxBrightness));
+                            getBrightnessFromBacklight(maxBrightness));
                 }
                 if (!luxToTransitionPointMap.isEmpty()) {
                     mLuxThrottlingData.put(mappedType, luxToTransitionPointMap);
@@ -3048,7 +3092,7 @@
 
     private void loadAutoBrightnessConfigsFromConfigXml() {
         mDisplayBrightnessMapping = new DisplayBrightnessMappingConfig(mContext, mFlags,
-                /* autoBrightnessConfig= */ null, mBacklightToBrightnessSpline);
+                /* autoBrightnessConfig= */ null, getBacklightToBrightnessSpline());
     }
 
     private void loadBrightnessChangeThresholdsFromXml() {
diff --git a/services/core/java/com/android/server/feature/dropbox_flags.aconfig b/services/core/java/com/android/server/feature/dropbox_flags.aconfig
index fee4bf3..14e964b 100644
--- a/services/core/java/com/android/server/feature/dropbox_flags.aconfig
+++ b/services/core/java/com/android/server/feature/dropbox_flags.aconfig
@@ -2,6 +2,7 @@
 
 flag{
     name: "enable_read_dropbox_permission"
+    is_exported: true
     namespace: "preload_safety"
     description: "Feature flag for permission to Read dropbox data"
     bug: "287512663"
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/inputmethod/ZeroJankProxy.java b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
index 136ab42c..31ce630 100644
--- a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
+++ b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
@@ -441,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/pm/DeletePackageHelper.java b/services/core/java/com/android/server/pm/DeletePackageHelper.java
index 588c629..fd16221 100644
--- a/services/core/java/com/android/server/pm/DeletePackageHelper.java
+++ b/services/core/java/com/android/server/pm/DeletePackageHelper.java
@@ -542,7 +542,8 @@
         final Computer snapshot = mPm.snapshotComputer();
         for (final int affectedUserId : outInfo.mRemovedUsers) {
             if (hadSuspendAppsPermission.get(affectedUserId)) {
-                mPm.unsuspendForSuspendingPackage(snapshot, packageName, affectedUserId);
+                mPm.unsuspendForSuspendingPackage(snapshot, packageName,
+                        affectedUserId /*suspendingUserId*/, true /*inAllUsers*/);
                 mPm.removeAllDistractingPackageRestrictions(snapshot, affectedUserId);
             }
         }
diff --git a/services/core/java/com/android/server/pm/DexOptHelper.java b/services/core/java/com/android/server/pm/DexOptHelper.java
index 51793f6..c60f0af 100644
--- a/services/core/java/com/android/server/pm/DexOptHelper.java
+++ b/services/core/java/com/android/server/pm/DexOptHelper.java
@@ -46,7 +46,6 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.ApexStagedEvent;
-import android.content.pm.Flags;
 import android.content.pm.IPackageManagerNative;
 import android.content.pm.IStagedApexObserver;
 import android.content.pm.PackageManager;
@@ -663,9 +662,7 @@
             }
         }, new IntentFilter(Intent.ACTION_LOCKED_BOOT_COMPLETED));
 
-        if (Flags.useArtServiceV2()) {
-            StagedApexObserver.registerForStagedApexUpdates(artManager);
-        }
+        StagedApexObserver.registerForStagedApexUpdates(artManager);
     }
 
     /**
@@ -750,9 +747,7 @@
                             & PackageManager.INSTALL_IGNORE_DEXOPT_PROFILE)
                             != 0;
             /*@DexoptFlags*/ int extraFlags =
-                    ignoreDexoptProfile && Flags.useArtServiceV2()
-                            ? ArtFlags.FLAG_IGNORE_PROFILE
-                            : 0;
+                    ignoreDexoptProfile ? ArtFlags.FLAG_IGNORE_PROFILE : 0;
             DexoptParams params = dexoptOptions.convertToDexoptParams(extraFlags);
             DexoptResult dexOptResult = getArtManagerLocal().dexoptPackage(
                     snapshot, packageName, params);
diff --git a/services/core/java/com/android/server/pm/InstallRequest.java b/services/core/java/com/android/server/pm/InstallRequest.java
index 43075a2..c10196f 100644
--- a/services/core/java/com/android/server/pm/InstallRequest.java
+++ b/services/core/java/com/android/server/pm/InstallRequest.java
@@ -35,7 +35,6 @@
 import android.app.AppOpsManager;
 import android.content.pm.ArchivedPackageParcel;
 import android.content.pm.DataLoaderType;
-import android.content.pm.Flags;
 import android.content.pm.IPackageInstallObserver2;
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageManager;
@@ -951,7 +950,7 @@
         // Only report external profile warnings when installing from adb. The goal is to warn app
         // developers if they have provided bad external profiles, so it's not beneficial to report
         // those warnings in the normal app install workflow.
-        if (isInstallFromAdb() && Flags.useArtServiceV2()) {
+        if (isInstallFromAdb()) {
             var externalProfileErrors = new LinkedHashSet<String>();
             for (PackageDexoptResult packageResult : dexoptResult.getPackageDexoptResults()) {
                 for (DexContainerFileDexoptResult fileResult :
diff --git a/services/core/java/com/android/server/pm/PackageManagerInternalBase.java b/services/core/java/com/android/server/pm/PackageManagerInternalBase.java
index 8da1683..7a72e70 100644
--- a/services/core/java/com/android/server/pm/PackageManagerInternalBase.java
+++ b/services/core/java/com/android/server/pm/PackageManagerInternalBase.java
@@ -16,6 +16,7 @@
 
 package com.android.server.pm;
 
+import static android.app.admin.flags.Flags.crossUserSuspensionEnabled;
 import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT;
 import static android.content.pm.PackageManager.RESTRICTION_NONE;
 
@@ -45,6 +46,7 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Process;
+import android.os.UserHandle;
 import android.os.storage.StorageManager;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -687,14 +689,17 @@
     @Override
     @Deprecated
     public final void unsuspendAdminSuspendedPackages(int affectedUser) {
-        final int suspendingUserId = affectedUser;
-        mService.unsuspendForSuspendingPackage(snapshot(), PLATFORM_PACKAGE_NAME, suspendingUserId);
+        final int suspendingUserId =
+                crossUserSuspensionEnabled() ? UserHandle.USER_SYSTEM : affectedUser;
+        mService.unsuspendForSuspendingPackage(
+                snapshot(), PLATFORM_PACKAGE_NAME, suspendingUserId, /* inAllUsers= */ false);
     }
 
     @Override
     @Deprecated
     public final boolean isAdminSuspendingAnyPackages(int userId) {
-        final int suspendingUserId = userId;
+        final int suspendingUserId =
+                crossUserSuspensionEnabled() ? UserHandle.USER_SYSTEM : userId;
         return snapshot().isSuspendingAnyPackages(PLATFORM_PACKAGE_NAME, suspendingUserId, userId);
     }
 
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index d215822..9a2b98f 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -18,6 +18,7 @@
 import static android.Manifest.permission.MANAGE_DEVICE_ADMINS;
 import static android.Manifest.permission.SET_HARMFUL_APP_WARNINGS;
 import static android.app.AppOpsManager.MODE_IGNORED;
+import static android.app.admin.flags.Flags.crossUserSuspensionEnabled;
 import static android.content.pm.PackageManager.APP_METADATA_SOURCE_APK;
 import static android.content.pm.PackageManager.APP_METADATA_SOURCE_UNKNOWN;
 import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT;
@@ -3181,27 +3182,53 @@
                     callingMethod);
         }
 
-        final int packageUid = snapshot.getPackageUid(suspender.packageName, 0, targetUserId);
-        final boolean allowedPackageUid = packageUid == callingUid;
-        // TODO(b/139383163): remove special casing for shell and enforce INTERACT_ACROSS_USERS_FULL
-        final boolean allowedShell = callingUid == SHELL_UID
-                && UserHandle.isSameApp(packageUid, callingUid);
+        if (crossUserSuspensionEnabled()) {
+            final int suspendingPackageUid =
+                    snapshot.getPackageUid(suspender.packageName, 0, suspender.userId);
+            if (suspendingPackageUid != callingUid) {
+                throw new SecurityException("Suspender package %s doesn't match calling uid %d"
+                                .formatted(suspender.packageName, callingUid));
+            }
+            if (targetUserId != suspender.userId) {
+                mContext.enforceCallingOrSelfPermission(
+                        Manifest.permission.INTERACT_ACROSS_USERS_FULL, callingMethod);
+            }
+        } else {
+            // Here only SHELL can suspend across users
+            final int packageUid =
+                    snapshot.getPackageUid(suspender.packageName, 0, targetUserId);
+            final boolean allowedPackageUid = packageUid == callingUid;
+            final boolean allowedShell = callingUid == SHELL_UID
+                    && UserHandle.isSameApp(packageUid, callingUid);
 
-        if (!allowedShell && !allowedPackageUid) {
-            throw new SecurityException("Suspending package " + suspender.packageName
-                    + " in user " + targetUserId + " does not belong to calling uid " + callingUid);
+            if (!allowedShell && !allowedPackageUid) {
+                throw new SecurityException("Suspending package " + suspender.packageName
+                        + " in user " + targetUserId + " does not belong to calling uid "
+                        + callingUid);
+            }
         }
     }
 
+    /**
+     * @param inAllUsers Whether to unsuspend packages suspended by the given package in other
+     *                   users. This flag is only used when cross-user suspension is enabled.
+     */
     void unsuspendForSuspendingPackage(@NonNull Computer computer, String suspendingPackage,
-            @UserIdInt int suspendingUserId) {
+            @UserIdInt int suspendingUserId, boolean inAllUsers) {
         // TODO: This can be replaced by a special parameter to iterate all packages, rather than
         //  this weird pre-collect of all packages.
         final String[] allPackages = computer.getPackageStates().keySet().toArray(new String[0]);
         final Predicate<UserPackage> suspenderPredicate =
                 UserPackage.of(suspendingUserId, suspendingPackage)::equals;
-        mSuspendPackageHelper.removeSuspensionsBySuspendingPackage(computer,
-                allPackages, suspenderPredicate, suspendingUserId);
+        if (!crossUserSuspensionEnabled() || !inAllUsers) {
+            mSuspendPackageHelper.removeSuspensionsBySuspendingPackage(computer,
+                    allPackages, suspenderPredicate, suspendingUserId);
+        } else {
+            for (int targetUserId: mUserManager.getUserIds()) {
+                mSuspendPackageHelper.removeSuspensionsBySuspendingPackage(
+                        computer, allPackages, suspenderPredicate, targetUserId);
+            }
+        }
     }
 
     void removeAllDistractingPackageRestrictions(@NonNull Computer snapshot, int userId) {
@@ -4053,7 +4080,7 @@
                 // This app should not generally be allowed to get disabled by the UI, but
                 // if it ever does, we don't want to end up with some of the user's apps
                 // permanently suspended.
-                unsuspendForSuspendingPackage(computer, packageName, userId);
+                unsuspendForSuspendingPackage(computer, packageName, userId, true /* inAllUsers */);
                 removeAllDistractingPackageRestrictions(computer, userId);
             }
             success = true;
@@ -4339,6 +4366,19 @@
         }
         mInstantAppRegistry.onUserRemoved(userId);
         mPackageMonitorCallbackHelper.onUserRemoved(userId);
+        if (crossUserSuspensionEnabled()) {
+            cleanUpCrossUserSuspension(userId);
+        }
+    }
+
+    private void cleanUpCrossUserSuspension(int removedUser) {
+        final Computer computer = snapshotComputer();
+        var allPackages = computer.getAllAvailablePackageNames();
+        for (int targetUserId : mUserManager.getUserIds()) {
+            if (targetUserId == removedUser) continue;
+            mSuspendPackageHelper.removeSuspensionsBySuspendingPackage(computer, allPackages,
+                    userPackage -> userPackage.userId == removedUser, targetUserId);
+        }
     }
 
     /**
@@ -4745,7 +4785,8 @@
                         if (checkPermission(Manifest.permission.SUSPEND_APPS, packageName, userId)
                                 == PERMISSION_GRANTED) {
                             final Computer snapshot = snapshotComputer();
-                            unsuspendForSuspendingPackage(snapshot, packageName, userId);
+                            unsuspendForSuspendingPackage(
+                                    snapshot, packageName, userId, true /* inAllUsers */);
                             removeAllDistractingPackageRestrictions(snapshot, userId);
                             synchronized (mLock) {
                                 flushPackageRestrictionsAsUserInternalLocked(userId);
@@ -6239,7 +6280,9 @@
             final boolean quarantined = ((flags & PackageManager.FLAG_SUSPEND_QUARANTINED) != 0)
                     && Flags.quarantinedEnabled();
             final Computer snapshot = snapshotComputer();
-            final UserPackage suspender = UserPackage.of(targetUserId, suspendingPackage);
+            final UserPackage suspender = crossUserSuspensionEnabled()
+                    ? UserPackage.of(suspendingUserId, suspendingPackage)
+                    : UserPackage.of(targetUserId, suspendingPackage);
             enforceCanSetPackagesSuspendedAsUser(snapshot, quarantined, suspender, callingUid,
                     targetUserId, "setPackagesSuspendedAsUser");
             return mSuspendPackageHelper.setPackagesSuspended(snapshot, packageNames, suspended,
@@ -6707,7 +6750,10 @@
         @Override
         public String[] setPackagesSuspendedByAdmin(
                 @UserIdInt int userId, @NonNull String[] packageNames, boolean suspended) {
-            final int suspendingUserId = userId;
+            // Suspension by admin isn't attributed to admin package but to the platform,
+            // Using USER_SYSTEM for consistency with other internal suspenders, like shell or root.
+            final int suspendingUserId =
+                    crossUserSuspensionEnabled() ? UserHandle.USER_SYSTEM : userId;
             final UserPackage suspender = UserPackage.of(
                     suspendingUserId, PackageManagerService.PLATFORM_PACKAGE_NAME);
             return mSuspendPackageHelper.setPackagesSuspended(snapshotComputer(), packageNames,
diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java
index 12eb88e..b44042c 100644
--- a/services/core/java/com/android/server/pm/PackageSetting.java
+++ b/services/core/java/com/android/server/pm/PackageSetting.java
@@ -16,6 +16,7 @@
 
 package com.android.server.pm;
 
+import static android.app.admin.flags.Flags.crossUserSuspensionEnabled;
 import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_DEFAULT_TO_DEVICE_PROTECTED_STORAGE;
 import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT;
 import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
@@ -1240,6 +1241,10 @@
                 for (int j = 0; j < state.getSuspendParams().size(); j++) {
                     proto.write(PackageProto.UserInfoProto.SUSPENDING_PACKAGE,
                             state.getSuspendParams().keyAt(j).packageName);
+                    if (crossUserSuspensionEnabled()) {
+                        proto.write(PackageProto.UserInfoProto.SUSPENDING_USER,
+                                state.getSuspendParams().keyAt(j).userId);
+                    }
                 }
             }
             proto.write(PackageProto.UserInfoProto.IS_STOPPED, state.isStopped());
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index e35a169..f5ed8d4 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -16,6 +16,7 @@
 
 package com.android.server.pm;
 
+import static android.app.admin.flags.Flags.crossUserSuspensionEnabled;
 import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT;
 import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
 import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
@@ -342,6 +343,7 @@
     private static final String ATTR_DISTRACTION_FLAGS = "distraction_flags";
     private static final String ATTR_SUSPENDED = "suspended";
     private static final String ATTR_SUSPENDING_PACKAGE = "suspending-package";
+    private static final String ATTR_SUSPENDING_USER = "suspending-user";
 
     private static final String ATTR_OPTIONAL = "optional";
     /**
@@ -2051,7 +2053,20 @@
             Slog.wtf(TAG, "No suspendingPackage found inside tag " + TAG_SUSPEND_PARAMS);
             return null;
         }
-        final int suspendingUserId = userId;
+        int suspendingUserId;
+        if (crossUserSuspensionEnabled()) {
+            suspendingUserId = parser.getAttributeInt(
+                    null, ATTR_SUSPENDING_USER, UserHandle.USER_NULL);
+            if (suspendingUserId == UserHandle.USER_NULL) {
+                suspendingUserId = switch (suspendingPackage) {
+                    case "root", "com.android.shell", PLATFORM_PACKAGE_NAME
+                            -> UserHandle.USER_SYSTEM;
+                    default -> userId;
+                };
+            }
+        } else {
+            suspendingUserId = userId;
+        }
         return Map.entry(
                 UserPackage.of(suspendingUserId, suspendingPackage),
                 SuspendParams.restoreFromXml(parser));
@@ -2418,6 +2433,10 @@
                                 serializer.startTag(null, TAG_SUSPEND_PARAMS);
                                 serializer.attribute(null, ATTR_SUSPENDING_PACKAGE,
                                         suspendingPackage.packageName);
+                                if (crossUserSuspensionEnabled()) {
+                                    serializer.attributeInt(null, ATTR_SUSPENDING_USER,
+                                            suspendingPackage.userId);
+                                }
                                 final SuspendParams params =
                                         ustate.getSuspendParams().valueAt(i);
                                 if (params != null) {
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index f655455..76bf8fd 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -3508,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;
                     }
diff --git a/services/core/java/com/android/server/power/stats/flags.aconfig b/services/core/java/com/android/server/power/stats/flags.aconfig
index b2e01c5..c42ccea 100644
--- a/services/core/java/com/android/server/power/stats/flags.aconfig
+++ b/services/core/java/com/android/server/power/stats/flags.aconfig
@@ -2,6 +2,7 @@
 
 flag {
     name: "power_monitor_api"
+    is_exported: true
     namespace: "backstage_power"
     description: "Feature flag for ODPM API"
     bug: "295027807"
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/webkit/flags.aconfig b/services/core/java/com/android/server/webkit/flags.aconfig
index 1411acc..2afbcd6 100644
--- a/services/core/java/com/android/server/webkit/flags.aconfig
+++ b/services/core/java/com/android/server/webkit/flags.aconfig
@@ -2,6 +2,7 @@
 
 flag {
     name: "update_service_v2"
+    is_exported: true
     namespace: "webview"
     description: "Using a new version of the WebView update service"
     bug: "308907090"
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 60848a7..0effa6c 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -795,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(
@@ -823,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);
@@ -877,6 +881,11 @@
                 return;
             }
 
+            if (mDisableSecureWindowsUri.equals(uri)) {
+                updateDisableSecureWindows();
+                return;
+            }
+
             @UpdateAnimationScaleMode
             final int mode;
             if (mWindowAnimationScaleUri.equals(uri)) {
@@ -896,6 +905,7 @@
         void loadSettings() {
             updateSystemUiSettings(false /* handleChange */);
             updateMaximumObscuringOpacityForTouch();
+            updateDisableSecureWindows();
         }
 
         void updateMaximumObscuringOpacityForTouch() {
@@ -978,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;
@@ -1116,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) {
@@ -6905,6 +6939,7 @@
                     pw.print(mLastFinishedFreezeSource);
                 }
                 pw.println();
+        pw.print("  mDisableSecureWindows="); pw.println(mDisableSecureWindows);
 
         mInputManagerCallback.dump(pw, "  ");
         mSnapshotController.dump(pw, " ");
@@ -10076,4 +10111,8 @@
             mDragDropController.setGlobalDragListener(listener);
         }
     }
+
+    boolean getDisableSecureWindows() {
+        return mDisableSecureWindows;
+    }
 }
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/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/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/mockingservicestests/Android.bp b/services/tests/mockingservicestests/Android.bp
index 6d3b8ac..4149e44 100644
--- a/services/tests/mockingservicestests/Android.bp
+++ b/services/tests/mockingservicestests/Android.bp
@@ -75,6 +75,7 @@
         "compatibility-device-util-axt",
         "flag-junit",
         "am_flags_lib",
+        "device_policy_aconfig_flags_lib",
     ],
 
     libs: [
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/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/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/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/services/usage/java/com/android/server/usage/StorageStatsService.java b/services/usage/java/com/android/server/usage/StorageStatsService.java
index 883c702..e9da53a 100644
--- a/services/usage/java/com/android/server/usage/StorageStatsService.java
+++ b/services/usage/java/com/android/server/usage/StorageStatsService.java
@@ -968,22 +968,20 @@
         stats.libSize += getDirBytes(new File(sourceDirName + "/lib/"));
 
         // Get dexopt, current profle and reference profile sizes.
-        if (SystemProperties.getBoolean("dalvik.vm.features.art_managed_file_stats", false)) {
-            ArtManagedFileStats artManagedFileStats;
-            try (var snapshot = getPackageManagerLocal().withFilteredSnapshot()) {
-                artManagedFileStats =
-                    getArtManagerLocal().getArtManagedFileStats(snapshot, packageName);
-            }
-
-            stats.dexoptSize +=
-                artManagedFileStats
-                    .getTotalSizeBytesByType(ArtManagedFileStats.TYPE_DEXOPT_ARTIFACT);
-            stats.refProfSize +=
-                artManagedFileStats
-                    .getTotalSizeBytesByType(ArtManagedFileStats.TYPE_REF_PROFILE);
-            stats.curProfSize +=
-                artManagedFileStats
-                    .getTotalSizeBytesByType(ArtManagedFileStats.TYPE_CUR_PROFILE);
+        ArtManagedFileStats artManagedFileStats;
+        try (var snapshot = getPackageManagerLocal().withFilteredSnapshot()) {
+            artManagedFileStats =
+                getArtManagerLocal().getArtManagedFileStats(snapshot, packageName);
         }
+
+        stats.dexoptSize +=
+            artManagedFileStats
+                .getTotalSizeBytesByType(ArtManagedFileStats.TYPE_DEXOPT_ARTIFACT);
+        stats.refProfSize +=
+            artManagedFileStats
+                .getTotalSizeBytesByType(ArtManagedFileStats.TYPE_REF_PROFILE);
+        stats.curProfSize +=
+            artManagedFileStats
+                .getTotalSizeBytesByType(ArtManagedFileStats.TYPE_CUR_PROFILE);
     }
 }
diff --git a/tests/ChoreographerTests/src/main/java/android/view/choreographertests/AttachedChoreographerTest.java b/tests/ChoreographerTests/src/main/java/android/view/choreographertests/AttachedChoreographerTest.java
index 5460e4e87..64dbe71 100644
--- a/tests/ChoreographerTests/src/main/java/android/view/choreographertests/AttachedChoreographerTest.java
+++ b/tests/ChoreographerTests/src/main/java/android/view/choreographertests/AttachedChoreographerTest.java
@@ -43,6 +43,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -392,6 +393,7 @@
     }
 
     @Test
+    @Ignore("Can be enabled only after b/330536267 is ready")
     public void testChoreographerDivisorRefreshRate() {
         for (int divisor : new int[]{2, 3}) {
             CountDownLatch continueLatch = new CountDownLatch(1);
@@ -420,6 +422,7 @@
     }
 
     @Test
+    @Ignore("Can be enabled only after b/330536267 is ready")
     public void testChoreographerAttachedAfterSetFrameRate() {
         Log.i(TAG, "starting testChoreographerAttachedAfterSetFrameRate");
 
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/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
index df003b6..fb7a6ab 100644
--- 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
@@ -18,6 +18,7 @@
 
 import com.android.asllib.AndroidSafetyLabel;
 import com.android.asllib.AndroidSafetyLabel.Format;
+import com.android.asllib.util.MalformedXmlException;
 
 import org.xml.sax.SAXException;
 
@@ -32,7 +33,11 @@
 
     /** Takes the options to make file conversion. */
     public static void main(String[] args)
-            throws IOException, ParserConfigurationException, SAXException, TransformerException {
+            throws IOException,
+                    ParserConfigurationException,
+                    SAXException,
+                    TransformerException,
+                    MalformedXmlException {
 
         String inFile = null;
         String outFile = null;
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
index 07e0e73..bc8063e 100644
--- 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
@@ -16,6 +16,8 @@
 
 package com.android.asllib;
 
+import com.android.asllib.util.MalformedXmlException;
+
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 import org.xml.sax.SAXException;
@@ -23,6 +25,7 @@
 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;
@@ -33,7 +36,7 @@
 import javax.xml.transform.dom.DOMSource;
 import javax.xml.transform.stream.StreamResult;
 
-public class AndroidSafetyLabel {
+public class AndroidSafetyLabel implements AslMarshallable {
 
     public enum Format {
         NULL, HUMAN_READABLE, ON_DEVICE;
@@ -45,31 +48,55 @@
         return mSafetyLabels;
     }
 
-    private AndroidSafetyLabel(SafetyLabels safetyLabels) {
+    public AndroidSafetyLabel(SafetyLabels safetyLabels) {
         this.mSafetyLabels = safetyLabels;
     }
 
     /** Reads a {@link AndroidSafetyLabel} from an {@link InputStream}. */
-    // TODO(b/329902686): Support conversion in both directions, specified by format.
+    // TODO(b/329902686): Support parsing from on-device.
     public static AndroidSafetyLabel readFromStream(InputStream in, Format format)
-            throws IOException, ParserConfigurationException, SAXException {
+            throws IOException, ParserConfigurationException, SAXException, MalformedXmlException {
         DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
         factory.setNamespaceAware(true);
         Document document = factory.newDocumentBuilder().parse(in);
 
-        Element appMetadataBundles =
-                XmlUtils.getSingleElement(document, XmlUtils.HR_TAG_APP_METADATA_BUNDLES);
+        switch (format) {
+            case HUMAN_READABLE:
+                Element appMetadataBundles =
+                        XmlUtils.getSingleElement(document, XmlUtils.HR_TAG_APP_METADATA_BUNDLES);
 
-        return AndroidSafetyLabel.createFromHrElement(appMetadataBundles);
+                return new AndroidSafetyLabelFactory()
+                        .createFromHrElements(
+                                List.of(
+                                        XmlUtils.getSingleElement(
+                                                document, 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 conversion in both directions, specified by format.
+    // 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();
-        document.appendChild(this.toOdDomElement(document));
+
+        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();
@@ -81,19 +108,12 @@
         transformer.transform(domSource, streamResult);
     }
 
-    /** Creates an {@link AndroidSafetyLabel} from human-readable DOM element */
-    public static AndroidSafetyLabel createFromHrElement(Element appMetadataBundlesEle) {
-        Element safetyLabelsEle =
-                XmlUtils.getSingleElement(appMetadataBundlesEle, XmlUtils.HR_TAG_SAFETY_LABELS);
-        SafetyLabels safetyLabels = SafetyLabels.createFromHrElement(safetyLabelsEle);
-        return new AndroidSafetyLabel(safetyLabels);
-    }
-
     /** Creates an on-device DOM element from an {@link AndroidSafetyLabel} */
-    public Element toOdDomElement(Document doc) {
+    @Override
+    public List<Element> toOdDomElements(Document doc) {
         Element aslEle = doc.createElement(XmlUtils.OD_TAG_BUNDLE);
-        aslEle.appendChild(mSafetyLabels.toOdDomElement(doc));
-        return aslEle;
+        XmlUtils.appendChildren(aslEle, mSafetyLabels.toOdDomElements(doc));
+        return List.of(aslEle);
     }
 
     public static void test() {
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..7e7fcf9
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabelFactory.java
@@ -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.asllib;
+
+import com.android.asllib.util.MalformedXmlException;
+
+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)
+            throws MalformedXmlException {
+        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..b8f9f0e
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallableFactory.java
@@ -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.asllib;
+
+import com.android.asllib.util.MalformedXmlException;
+
+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) throws MalformedXmlException;
+}
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
index efdaa40..e5ed63b 100644
--- 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
@@ -16,6 +16,10 @@
 
 package com.android.asllib;
 
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -23,21 +27,32 @@
  * are defined in {@link DataCategoryConstants}, each category has a valid set of types {@link
  * DataType}, which are mapped in {@link DataTypeConstants}
  */
-public class DataCategory {
+public class DataCategory implements AslMarshallable {
+    private final String mCategoryName;
     private final Map<String, DataType> mDataTypes;
 
-    private DataCategory(Map<String, DataType> dataTypes) {
+    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 a {@link DataCategory} given map of {@param dataTypes}. */
-    public static DataCategory create(Map<String, DataType> dataTypes) {
-        return new DataCategory(dataTypes);
+    /** 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/DataCategoryFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryFactory.java
new file mode 100644
index 0000000..d946345
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryFactory.java
@@ -0,0 +1,44 @@
+/*
+ * 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 com.android.asllib.util.MalformedXmlException;
+
+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) throws MalformedXmlException {
+        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);
+            if (!DataTypeConstants.getValidDataTypes().contains(dataTypeName)) {
+                throw new MalformedXmlException(
+                        String.format("Unrecognized data type name: %s", dataTypeName));
+            }
+            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
index d2c3d75b..d2fffc0 100644
--- 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
@@ -18,16 +18,15 @@
 
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
-import org.w3c.dom.NodeList;
 
-import java.util.HashMap;
+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 {
+public class DataLabels implements AslMarshallable {
     private final Map<String, DataCategory> mDataAccessed;
     private final Map<String, DataCategory> mDataCollected;
     private final Map<String, DataCategory> mDataShared;
@@ -65,46 +64,9 @@
         return mDataShared;
     }
 
-    /** Creates a {@link DataLabels} from the human-readable DOM element. */
-    public static DataLabels createFromHrElement(Element ele) {
-        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 dataSharedNodeList = dataLabelsEle.getElementsByTagName(dataCategoryUsageTypeTag);
-
-        for (int i = 0; i < dataSharedNodeList.getLength(); i++) {
-            Element dataSharedEle = (Element) dataSharedNodeList.item(i);
-            String dataCategoryName = dataSharedEle.getAttribute(XmlUtils.HR_ATTR_DATA_CATEGORY);
-            String dataTypeName = dataSharedEle.getAttribute(XmlUtils.HR_ATTR_DATA_TYPE);
-
-            if (!dataTypeMap.containsKey((dataCategoryName))) {
-                dataTypeMap.put(dataCategoryName, new HashMap<String, DataType>());
-            }
-            dataTypeMap
-                    .get(dataCategoryName)
-                    .put(dataTypeName, DataType.createFromHrElement(dataSharedEle));
-        }
-
-        Map<String, DataCategory> dataCategoryMap = new HashMap<String, DataCategory>();
-        for (String dataCategoryName : dataTypeMap.keySet()) {
-            Map<String, DataType> dataTypes = dataTypeMap.get(dataCategoryName);
-            dataCategoryMap.put(dataCategoryName, DataCategory.create(dataTypes));
-        }
-        return dataCategoryMap;
-    }
-
     /** Gets the on-device DOM element for the {@link DataLabels}. */
-    public Element toOdDomElement(Document doc) {
+    @Override
+    public List<Element> toOdDomElements(Document doc) {
         Element dataLabelsEle =
                 XmlUtils.createPbundleEleWithName(doc, XmlUtils.OD_NAME_DATA_LABELS);
 
@@ -112,7 +74,7 @@
         maybeAppendDataUsages(doc, dataLabelsEle, mDataCollected, XmlUtils.OD_NAME_DATA_COLLECTED);
         maybeAppendDataUsages(doc, dataLabelsEle, mDataShared, XmlUtils.OD_NAME_DATA_SHARED);
 
-        return dataLabelsEle;
+        return List.of(dataLabelsEle);
     }
 
     private void maybeAppendDataUsages(
@@ -130,47 +92,10 @@
             DataCategory dataCategory = dataCategoriesMap.get(dataCategoryName);
             for (String dataTypeName : dataCategory.getDataTypes().keySet()) {
                 DataType dataType = dataCategory.getDataTypes().get(dataTypeName);
-                Element dataTypeEle = XmlUtils.createPbundleEleWithName(doc, dataTypeName);
-                if (!dataType.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(dataType.getPurposeSet().size()));
-                    for (DataType.Purpose purpose : dataType.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,
-                        dataType.getIsCollectionOptional(),
-                        XmlUtils.OD_NAME_IS_COLLECTION_OPTIONAL);
-                maybeAddBoolToOdElement(
-                        doc,
-                        dataTypeEle,
-                        dataType.getIsSharingOptional(),
-                        XmlUtils.OD_NAME_IS_SHARING_OPTIONAL);
-                maybeAddBoolToOdElement(
-                        doc, dataTypeEle, dataType.getEphemeral(), XmlUtils.OD_NAME_EPHEMERAL);
-
-                dataCategoryEle.appendChild(dataTypeEle);
+                XmlUtils.appendChildren(dataCategoryEle, dataType.toOdDomElements(doc));
             }
             dataUsageEle.appendChild(dataCategoryEle);
         }
         dataLabelsEle.appendChild(dataUsageEle);
     }
-
-    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/DataLabelsFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabelsFactory.java
new file mode 100644
index 0000000..1adb140
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabelsFactory.java
@@ -0,0 +1,117 @@
+/*
+ * 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 com.android.asllib.util.MalformedXmlException;
+
+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) throws MalformedXmlException {
+        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);
+
+        // Validate booleans such as isCollectionOptional, isSharingOptional.
+        for (DataCategory dataCategory : dataAccessed.values()) {
+            for (DataType dataType : dataCategory.getDataTypes().values()) {
+                if (dataType.getIsSharingOptional() != null) {
+                    throw new MalformedXmlException(
+                            String.format(
+                                    "isSharingOptional was unexpectedly defined on a DataType"
+                                            + " belonging to data accessed: %s",
+                                    dataType.getDataTypeName()));
+                }
+                if (dataType.getIsCollectionOptional() != null) {
+                    throw new MalformedXmlException(
+                            String.format(
+                                    "isCollectionOptional was unexpectedly defined on a DataType"
+                                            + " belonging to data accessed: %s",
+                                    dataType.getDataTypeName()));
+                }
+            }
+        }
+        for (DataCategory dataCategory : dataCollected.values()) {
+            for (DataType dataType : dataCategory.getDataTypes().values()) {
+                if (dataType.getIsSharingOptional() != null) {
+                    throw new MalformedXmlException(
+                            String.format(
+                                    "isSharingOptional was unexpectedly defined on a DataType"
+                                            + " belonging to data collected: %s",
+                                    dataType.getDataTypeName()));
+                }
+            }
+        }
+        for (DataCategory dataCategory : dataShared.values()) {
+            for (DataType dataType : dataCategory.getDataTypes().values()) {
+                if (dataType.getIsCollectionOptional() != null) {
+                    throw new MalformedXmlException(
+                            String.format(
+                                    "isCollectionOptional was unexpectedly defined on a DataType"
+                                            + " belonging to data shared: %s",
+                                    dataType.getDataTypeName()));
+                }
+            }
+        }
+
+        return new DataLabels(dataAccessed, dataCollected, dataShared);
+    }
+
+    private static Map<String, DataCategory> getDataCategoriesWithTag(
+            Element dataLabelsEle, String dataCategoryUsageTypeTag) throws MalformedXmlException {
+        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);
+            if (!DataCategoryConstants.getValidDataCategories().contains(dataCategoryName)) {
+                throw new MalformedXmlException(
+                        String.format("Unrecognized category name: %s", dataCategoryName));
+            }
+            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
index 7451c69..5ba2975 100644
--- 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
@@ -16,17 +16,18 @@
 
 package com.android.asllib;
 
+import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 
-import java.util.Arrays;
+import java.util.List;
 import java.util.Set;
-import java.util.stream.Collectors;
 
 /**
  * Data usage type representation. Types are specific to a {@link DataCategory} and contains
  * metadata related to the data usage purpose.
  */
-public class DataType {
+public class DataType implements AslMarshallable {
+
     public enum Purpose {
         PURPOSE_APP_FUNCTIONALITY(1),
         PURPOSE_ANALYTICS(2),
@@ -78,22 +79,30 @@
         }
     }
 
+    private final String mDataTypeName;
+
     private final Set<Purpose> mPurposeSet;
     private final Boolean mIsCollectionOptional;
     private final Boolean mIsSharingOptional;
     private final Boolean mEphemeral;
 
-    private DataType(
+    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
@@ -126,20 +135,42 @@
         return mEphemeral;
     }
 
-    /** Creates a {@link DataType} from the human-readable DOM element. */
-    public static DataType createFromHrElement(Element hrDataTypeEle) {
-        Set<Purpose> purposeSet =
-                Arrays.stream(hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_PURPOSES).split("\\|"))
-                        .map(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(purposeSet, isCollectionOptional, isSharingOptional, ephemeral);
+    @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/DataTypeFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeFactory.java
new file mode 100644
index 0000000..e3d1587
--- /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_COLLECTION_OPTIONAL));
+        Boolean isSharingOptional =
+                XmlUtils.fromString(
+                        hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_IS_SHARING_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
index 6ba15e1..f06522f 100644
--- 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
@@ -19,13 +19,15 @@
 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 {
+public class SafetyLabels implements AslMarshallable {
 
     private final Long mVersion;
     private final DataLabels mDataLabels;
 
-    private SafetyLabels(Long version, DataLabels dataLabels) {
+    public SafetyLabels(Long version, DataLabels dataLabels) {
         this.mVersion = version;
         this.mDataLabels = dataLabels;
     }
@@ -40,26 +42,12 @@
         return mVersion;
     }
 
-    /** Creates a {@link SafetyLabels} from the human-readable DOM element. */
-    public static SafetyLabels createFromHrElement(Element safetyLabelsEle) {
-        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.");
-        }
-        Element dataLabelsEle =
-                XmlUtils.getSingleElement(safetyLabelsEle, XmlUtils.HR_TAG_DATA_LABELS);
-        DataLabels dataLabels = DataLabels.createFromHrElement(dataLabelsEle);
-        return new SafetyLabels(version, dataLabels);
-    }
-
     /** Creates an on-device DOM element from the {@link SafetyLabels}. */
-    public Element toOdDomElement(Document doc) {
+    @Override
+    public List<Element> toOdDomElements(Document doc) {
         Element safetyLabelsEle =
                 XmlUtils.createPbundleEleWithName(doc, XmlUtils.OD_NAME_SAFETY_LABELS);
-        safetyLabelsEle.appendChild(mDataLabels.toOdDomElement(doc));
-        return safetyLabelsEle;
+        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..80b9f57
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabelsFactory.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 com.android.asllib.util.MalformedXmlException;
+
+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) throws MalformedXmlException {
+        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
index 4392c2c..3bc9ccc 100644
--- 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
@@ -16,10 +16,15 @@
 
 package com.android.asllib;
 
+import com.android.asllib.util.MalformedXmlException;
+
 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";
@@ -58,7 +63,8 @@
     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) {
+    public static Element getSingleElement(Document doc, String tagName)
+            throws MalformedXmlException {
         var elements = doc.getElementsByTagName(tagName);
         return getSingleElement(elements, tagName);
     }
@@ -66,24 +72,57 @@
     /**
      * Gets the single {@link Element} within {@param parentEle} and having the {@param tagName}.
      */
-    public static Element getSingleElement(Element parentEle, String tagName) {
+    public static Element getSingleChildElement(Element parentEle, String tagName)
+            throws MalformedXmlException {
         var elements = parentEle.getElementsByTagName(tagName);
         return getSingleElement(elements, tagName);
     }
 
-    /** Gets the single {@link Element} from {@param elements} and having the {@param tagName}. */
-    public static Element getSingleElement(NodeList elements, String tagName) {
+    /** Gets the single {@link Element} from {@param elements} */
+    public static Element getSingleElement(NodeList elements, String tagName)
+            throws MalformedXmlException {
         if (elements.getLength() != 1) {
-            throw new IllegalArgumentException(
-                    String.format("Expected 1 %s but got %s.", tagName, elements.getLength()));
+            throw new MalformedXmlException(
+                    String.format(
+                            "Expected 1 element \"%s\" in NodeList but got %s.",
+                            tagName, elements.getLength()));
         }
         var elementAsNode = elements.item(0);
         if (!(elementAsNode instanceof Element)) {
-            throw new IllegalStateException(String.format("%s was not an element.", tagName));
+            throw new MalformedXmlException(
+                    String.format("%s was not a valid XML element.", tagName));
         }
         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) {
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/MalformedXmlException.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/MalformedXmlException.java
new file mode 100644
index 0000000..216df56
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/MalformedXmlException.java
@@ -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.asllib.util;
+
+public class MalformedXmlException extends Exception {
+    /** Constructs an {@code MalformedXmlException} with no detail message. */
+    public MalformedXmlException() {
+        super();
+    }
+
+    /**
+     * Constructs an {@code MalformedXmlException} with the specified detail message.
+     *
+     * @param s the detail message.
+     */
+    public MalformedXmlException(String s) {
+        super(s);
+    }
+}
diff --git a/wifi/wifi.aconfig b/wifi/wifi.aconfig
index 6ac986e..6c4e4c3 100644
--- a/wifi/wifi.aconfig
+++ b/wifi/wifi.aconfig
@@ -2,6 +2,7 @@
 
 flag {
     name: "get_device_cross_akm_roaming_support"
+    is_exported: true
     namespace: "wifi"
     description: "Add new API to get the device support for CROSS-AKM roaming"
     bug: "313038031"