Merge "[RONs] Bind ProgressStyle values to NotificationProgressBar" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index d582cb7..c6ce799 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -65,6 +65,7 @@
         "android.server.app.flags-aconfig-java",
         "android.service.autofill.flags-aconfig-java",
         "android.service.chooser.flags-aconfig-java",
+        "android.service.compat.flags-aconfig-java",
         "android.service.controls.flags-aconfig-java",
         "android.service.dreams.flags-aconfig-java",
         "android.service.notification.flags-aconfig-java",
@@ -863,6 +864,21 @@
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
 }
 
+aconfig_declarations {
+    name: "android.service.compat.flags-aconfig",
+    package: "com.android.server.compat",
+    container: "system",
+    srcs: [
+        "services/core/java/com/android/server/compat/*.aconfig",
+    ],
+}
+
+java_aconfig_library {
+    name: "android.service.compat.flags-aconfig-java",
+    aconfig_declarations: "android.service.compat.flags-aconfig",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
 // Multi user
 aconfig_declarations {
     name: "android.multiuser.flags-aconfig",
diff --git a/Android.bp b/Android.bp
index 811755d..d4776f5 100644
--- a/Android.bp
+++ b/Android.bp
@@ -170,12 +170,6 @@
         //same purpose.
         "//external/robolectric:__subpackages__",
         "//frameworks/layoutlib:__subpackages__",
-
-        // This is for the same purpose as robolectric -- to build "framework.jar" for host-side
-        // testing.
-        // TODO: Once Ravenwood is stable, move the host side jar targets to this directory,
-        // and remove this line.
-        "//frameworks/base/tools/hoststubgen:__subpackages__",
     ],
 }
 
diff --git a/Ravenwood.bp b/Ravenwood.bp
index ec58210..2e038e0 100644
--- a/Ravenwood.bp
+++ b/Ravenwood.bp
@@ -12,256 +12,19 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// We need this "trampoline" rule to force soong to give a host-side jar to
-// framework-minus-apex.ravenwood-base. Otherwise, soong would mix up the arch (?) and we'd get
-// a dex jar.
-java_library {
-    name: "framework-minus-apex-for-hoststubgen",
-    installable: false, // host only jar.
-    static_libs: [
-        "framework-minus-apex",
-    ],
-    sdk_version: "core_platform",
-    visibility: ["//visibility:private"],
-}
-
-// Process framework-all with hoststubgen for Ravenwood.
-// This step takes several tens of seconds, so we manually shard it to multiple modules.
-// All the copies have to be kept in sync.
-// TODO: Do the sharding better, either by making hostsubgen support sharding natively, or
-// making a better build rule.
-
-genrule_defaults {
-    name: "framework-minus-apex.ravenwood-base_defaults",
-    defaults: ["ravenwood-internal-only-visibility-genrule"],
-    tools: ["hoststubgen"],
-    srcs: [
-        ":framework-minus-apex-for-hoststubgen",
-        ":ravenwood-framework-policies",
-        ":ravenwood-standard-options",
-        ":ravenwood-annotation-allowed-classes",
-    ],
-    out: [
-        "ravenwood.jar",
-        "hoststubgen_framework-minus-apex.log",
-    ],
-}
-
-framework_minus_apex_cmd = "$(location hoststubgen) " +
-    "@$(location :ravenwood-standard-options) " +
-    "--debug-log $(location hoststubgen_framework-minus-apex.log) " +
-    "--out-jar $(location ravenwood.jar) " +
-    "--in-jar $(location :framework-minus-apex-for-hoststubgen) " +
-    "--policy-override-file $(location :ravenwood-framework-policies) " +
-    "--annotation-allowed-classes-file $(location :ravenwood-annotation-allowed-classes) "
-
-java_genrule {
-    name: "framework-minus-apex.ravenwood-base_X0",
-    defaults: ["framework-minus-apex.ravenwood-base_defaults"],
-    cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 0",
-}
-
-java_genrule {
-    name: "framework-minus-apex.ravenwood-base_X1",
-    defaults: ["framework-minus-apex.ravenwood-base_defaults"],
-    cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 1",
-}
-
-java_genrule {
-    name: "framework-minus-apex.ravenwood-base_X2",
-    defaults: ["framework-minus-apex.ravenwood-base_defaults"],
-    cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 2",
-}
-
-java_genrule {
-    name: "framework-minus-apex.ravenwood-base_X3",
-    defaults: ["framework-minus-apex.ravenwood-base_defaults"],
-    cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 3",
-}
-
-java_genrule {
-    name: "framework-minus-apex.ravenwood-base_X4",
-    defaults: ["framework-minus-apex.ravenwood-base_defaults"],
-    cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 4",
-}
-
-java_genrule {
-    name: "framework-minus-apex.ravenwood-base_X5",
-    defaults: ["framework-minus-apex.ravenwood-base_defaults"],
-    cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 5",
-}
-
-java_genrule {
-    name: "framework-minus-apex.ravenwood-base_X6",
-    defaults: ["framework-minus-apex.ravenwood-base_defaults"],
-    cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 6",
-}
-
-java_genrule {
-    name: "framework-minus-apex.ravenwood-base_X7",
-    defaults: ["framework-minus-apex.ravenwood-base_defaults"],
-    cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 7",
-}
-
-java_genrule {
-    name: "framework-minus-apex.ravenwood-base_X8",
-    defaults: ["framework-minus-apex.ravenwood-base_defaults"],
-    cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 8",
-}
-
-java_genrule {
-    name: "framework-minus-apex.ravenwood-base_X9",
-    defaults: ["framework-minus-apex.ravenwood-base_defaults"],
-    cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 9",
-}
-
-// Build framework-minus-apex.ravenwood-base without sharding.
-// We extract the various dump files from this one, rather than the sharded ones, because
-// some dumps use the output from other classes (e.g. base classes) which may not be in the
-// same shard. Also some of the dump files ("apis") may be slow even when sharded, because
-// the output contains the information from all the input classes, rather than the output classes.
-// Not using sharding is fine for this module because it's only used for collecting the
-// dump / stats files, which don't have to happen regularly.
-java_genrule {
-    name: "framework-minus-apex.ravenwood-base_all",
-    defaults: ["framework-minus-apex.ravenwood-base_defaults"],
-    cmd: framework_minus_apex_cmd +
-        "--stats-file $(location hoststubgen_framework-minus-apex_stats.csv) " +
-        "--supported-api-list-file $(location hoststubgen_framework-minus-apex_apis.csv) " +
-
-        "--gen-keep-all-file $(location hoststubgen_framework-minus-apex_keep_all.txt) " +
-        "--gen-input-dump-file $(location hoststubgen_framework-minus-apex_dump.txt) ",
-
-    out: [
-        "hoststubgen_framework-minus-apex_keep_all.txt",
-        "hoststubgen_framework-minus-apex_dump.txt",
-        "hoststubgen_framework-minus-apex_stats.csv",
-        "hoststubgen_framework-minus-apex_apis.csv",
-    ],
-}
-
-// Marge all the sharded jars
-java_genrule {
-    name: "framework-minus-apex.ravenwood",
-    defaults: ["ravenwood-internal-only-visibility-java"],
-    cmd: "$(location merge_zips) $(out) $(in)",
-    tools: ["merge_zips"],
-    srcs: [
-        ":framework-minus-apex.ravenwood-base_X0{ravenwood.jar}",
-        ":framework-minus-apex.ravenwood-base_X1{ravenwood.jar}",
-        ":framework-minus-apex.ravenwood-base_X2{ravenwood.jar}",
-        ":framework-minus-apex.ravenwood-base_X3{ravenwood.jar}",
-        ":framework-minus-apex.ravenwood-base_X4{ravenwood.jar}",
-        ":framework-minus-apex.ravenwood-base_X5{ravenwood.jar}",
-        ":framework-minus-apex.ravenwood-base_X6{ravenwood.jar}",
-        ":framework-minus-apex.ravenwood-base_X7{ravenwood.jar}",
-        ":framework-minus-apex.ravenwood-base_X8{ravenwood.jar}",
-        ":framework-minus-apex.ravenwood-base_X9{ravenwood.jar}",
-    ],
-    out: [
-        "framework-minus-apex.ravenwood.jar",
-    ],
-}
+// "framework-minus-apex" and "all-updatable-modules-system-stubs" are not
+// visible publicly. We re-export them to Ravenwood in this file.
 
 java_library {
-    name: "services.core-for-hoststubgen",
-    installable: false, // host only jar.
-    static_libs: [
-        "services.core",
-    ],
-    sdk_version: "core_platform",
-    visibility: ["//visibility:private"],
-}
-
-java_genrule {
-    name: "services.core.ravenwood-base",
-    tools: ["hoststubgen"],
-    cmd: "$(location hoststubgen) " +
-        "@$(location :ravenwood-standard-options) " +
-
-        "--debug-log $(location hoststubgen_services.core.log) " +
-        "--stats-file $(location hoststubgen_services.core_stats.csv) " +
-        "--supported-api-list-file $(location hoststubgen_services.core_apis.csv) " +
-
-        "--out-jar $(location ravenwood.jar) " +
-
-        "--gen-keep-all-file $(location hoststubgen_services.core_keep_all.txt) " +
-        "--gen-input-dump-file $(location hoststubgen_services.core_dump.txt) " +
-
-        "--in-jar $(location :services.core-for-hoststubgen) " +
-        "--policy-override-file $(location :ravenwood-services-policies) " +
-        "--annotation-allowed-classes-file $(location :ravenwood-annotation-allowed-classes) ",
-    srcs: [
-        ":services.core-for-hoststubgen",
-        ":ravenwood-services-policies",
-        ":ravenwood-standard-options",
-        ":ravenwood-annotation-allowed-classes",
-    ],
-    out: [
-        "ravenwood.jar",
-
-        // Following files are created just as FYI.
-        "hoststubgen_services.core_keep_all.txt",
-        "hoststubgen_services.core_dump.txt",
-
-        "hoststubgen_services.core.log",
-        "hoststubgen_services.core_stats.csv",
-        "hoststubgen_services.core_apis.csv",
-    ],
-    defaults: ["ravenwood-internal-only-visibility-genrule"],
-}
-
-java_genrule {
-    name: "services.core.ravenwood",
-    defaults: ["ravenwood-internal-only-visibility-genrule"],
-    cmd: "cp $(in) $(out)",
-    srcs: [
-        ":services.core.ravenwood-base{ravenwood.jar}",
-    ],
-    out: [
-        "services.core.ravenwood.jar",
-    ],
-}
-
-// TODO(b/313930116) This jarjar is a bit slow. We should use hoststubgen for renaming,
-// but services.core.ravenwood has complex dependencies, so it'll take more than
-// just using hoststubgen "rename"s.
-java_library {
-    name: "services.core.ravenwood-jarjar",
-    defaults: ["ravenwood-internal-only-visibility-java"],
+    name: "framework-minus-apex-for-host",
     installable: false,
-    static_libs: [
-        "services.core.ravenwood",
-    ],
-    jarjar_rules: ":ravenwood-services-jarjar-rules",
+    static_libs: ["framework-minus-apex"],
+    visibility: ["//frameworks/base/ravenwood"],
 }
 
-// Jars in "ravenwood-runtime" are set to the classpath, sorted alphabetically.
-// Rename some of the dependencies to make sure they're included in the intended order.
 java_library {
-    name: "100-framework-minus-apex.ravenwood",
-    defaults: ["ravenwood-internal-only-visibility-java"],
-    static_libs: [
-        "framework-minus-apex.ravenwood",
-    ],
-    sdk_version: "core_platform",
-    // See b/313930116. Jarjar is too slow on this jar. We use HostStubGen to do the rename.
-    // jarjar_rules: ":ravenwood-framework-jarjar-rules",
-}
-
-java_genrule {
-    // Use 200 to make sure it comes before the mainline stub ("all-updatable...").
-    name: "200-kxml2-android",
-    defaults: ["ravenwood-internal-only-visibility-genrule"],
-    cmd: "cp $(in) $(out)",
-    srcs: [":kxml2-android"],
-    out: ["200-kxml2-android.jar"],
-}
-
-java_genrule {
-    name: "z00-all-updatable-modules-system-stubs",
-    defaults: ["ravenwood-internal-only-visibility-genrule"],
-    cmd: "cp $(in) $(out)",
-    srcs: [":all-updatable-modules-system-stubs"],
-    out: ["z00-all-updatable-modules-system-stubs.jar"],
+    name: "all-updatable-modules-system-stubs-for-host",
+    installable: false,
+    static_libs: ["all-updatable-modules-system-stubs"],
+    visibility: ["//frameworks/base/ravenwood"],
 }
diff --git a/apex/jobscheduler/service/aconfig/job.aconfig b/apex/jobscheduler/service/aconfig/job.aconfig
index e5389b4..11c5b51 100644
--- a/apex/jobscheduler/service/aconfig/job.aconfig
+++ b/apex/jobscheduler/service/aconfig/job.aconfig
@@ -75,3 +75,10 @@
        purpose: PURPOSE_BUGFIX
    }
 }
+
+flag {
+   name: "enforce_quota_policy_to_fgs_jobs"
+   namespace: "backstage_power"
+   description: "Applies the normal quota policy to FGS jobs"
+   bug: "341201311"
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
index a1c72fb..03a3a0d 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
@@ -99,10 +99,10 @@
  * the number of jobs or sessions that can run within the window. Regardless of bucket, apps will
  * not be allowed to run more than 20 jobs within the past 10 minutes.
  *
- * Jobs are throttled while an app is not in a foreground state. All jobs are allowed to run
- * freely when an app enters the foreground state and are restricted when the app leaves the
- * foreground state. However, jobs that are started while the app is in the TOP state do not count
- * towards any quota and are not restricted regardless of the app's state change.
+ * Jobs are throttled while an app is not in a TOP or BOUND_TOP state. All jobs are allowed to run
+ * freely when an app enters the TOP or BOUND_TOP state and are restricted when the app leaves those
+ * states. However, jobs that are started while the app is in the TOP state do not count towards any
+ * quota and are not restricted regardless of the app's state change.
  *
  * Jobs will not be throttled when the device is charging. The device is considered to be charging
  * once the {@link BatteryManager#ACTION_CHARGING} intent has been broadcast.
@@ -567,6 +567,11 @@
             ActivityManager.getService().registerUidObserver(new QcUidObserver(),
                     ActivityManager.UID_OBSERVER_PROCSTATE,
                     ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, null);
+            if (Flags.enforceQuotaPolicyToFgsJobs()) {
+                ActivityManager.getService().registerUidObserver(new QcUidObserver(),
+                        ActivityManager.UID_OBSERVER_PROCSTATE,
+                        ActivityManager.PROCESS_STATE_BOUND_TOP, null);
+            }
             ActivityManager.getService().registerUidObserver(new QcUidObserver(),
                     ActivityManager.UID_OBSERVER_PROCSTATE,
                     ActivityManager.PROCESS_STATE_TOP, null);
@@ -2706,6 +2711,12 @@
         }
     }
 
+    @VisibleForTesting
+    int getProcessStateQuotaFreeThreshold() {
+        return Flags.enforceQuotaPolicyToFgsJobs() ? ActivityManager.PROCESS_STATE_BOUND_TOP :
+                ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE;
+    }
+
     private class QcHandler extends Handler {
 
         QcHandler(Looper looper) {
@@ -2832,15 +2843,15 @@
                                 mTopAppCache.put(uid, true);
                                 mTopAppGraceCache.delete(uid);
                                 if (mForegroundUids.get(uid)) {
-                                    // Went from FGS to TOP. We don't need to reprocess timers or
-                                    // jobs.
+                                    // Went from a process state with quota free to TOP. We don't
+                                    // need to reprocess timers or jobs.
                                     break;
                                 }
                                 mForegroundUids.put(uid, true);
                                 isQuotaFree = true;
                             } else {
                                 final boolean reprocess;
-                                if (procState <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) {
+                                if (procState <= getProcessStateQuotaFreeThreshold()) {
                                     reprocess = !mForegroundUids.get(uid);
                                     mForegroundUids.put(uid, true);
                                     isQuotaFree = true;
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 7781f88..10206f2 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -3489,41 +3489,41 @@
 
   public static class VirtualDeviceManager.VirtualDevice implements java.lang.AutoCloseable {
     method public void addActivityListener(@NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.VirtualDeviceManager.ActivityListener);
-    method @FlaggedApi("android.companion.virtual.flags.dynamic_policy") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void addActivityPolicyExemption(@NonNull android.content.ComponentName);
-    method @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void addActivityPolicyExemption(@NonNull android.companion.virtual.ActivityPolicyExemption);
+    method @FlaggedApi("android.companion.virtual.flags.dynamic_policy") public void addActivityPolicyExemption(@NonNull android.content.ComponentName);
+    method @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") public void addActivityPolicyExemption(@NonNull android.companion.virtual.ActivityPolicyExemption);
     method public void addSoundEffectListener(@NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.VirtualDeviceManager.SoundEffectListener);
-    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close();
+    method public void close();
     method @NonNull public android.content.Context createContext();
-    method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.companion.virtual.audio.VirtualAudioDevice createVirtualAudioDevice(@NonNull android.hardware.display.VirtualDisplay, @Nullable java.util.concurrent.Executor, @Nullable android.companion.virtual.audio.VirtualAudioDevice.AudioConfigurationChangeCallback);
-    method @FlaggedApi("android.companion.virtual.flags.virtual_camera") @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.companion.virtual.camera.VirtualCamera createVirtualCamera(@NonNull android.companion.virtual.camera.VirtualCameraConfig);
-    method @Deprecated @Nullable @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.display.VirtualDisplay createVirtualDisplay(@IntRange(from=1) int, @IntRange(from=1) int, @IntRange(from=1) int, @Nullable android.view.Surface, int, @Nullable java.util.concurrent.Executor, @Nullable android.hardware.display.VirtualDisplay.Callback);
-    method @Nullable @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.display.VirtualDisplay createVirtualDisplay(@NonNull android.hardware.display.VirtualDisplayConfig, @Nullable java.util.concurrent.Executor, @Nullable android.hardware.display.VirtualDisplay.Callback);
-    method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualDpad createVirtualDpad(@NonNull android.hardware.input.VirtualDpadConfig);
-    method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualKeyboard createVirtualKeyboard(@NonNull android.hardware.input.VirtualKeyboardConfig);
-    method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualKeyboard createVirtualKeyboard(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int);
-    method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualMouse createVirtualMouse(@NonNull android.hardware.input.VirtualMouseConfig);
-    method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualMouse createVirtualMouse(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int);
-    method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualNavigationTouchpad createVirtualNavigationTouchpad(@NonNull android.hardware.input.VirtualNavigationTouchpadConfig);
-    method @FlaggedApi("android.companion.virtualdevice.flags.virtual_rotary") @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualRotaryEncoder createVirtualRotaryEncoder(@NonNull android.hardware.input.VirtualRotaryEncoderConfig);
-    method @FlaggedApi("android.companion.virtual.flags.virtual_stylus") @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualStylus createVirtualStylus(@NonNull android.hardware.input.VirtualStylusConfig);
-    method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualTouchscreen createVirtualTouchscreen(@NonNull android.hardware.input.VirtualTouchscreenConfig);
-    method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualTouchscreen createVirtualTouchscreen(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int);
+    method @NonNull public android.companion.virtual.audio.VirtualAudioDevice createVirtualAudioDevice(@NonNull android.hardware.display.VirtualDisplay, @Nullable java.util.concurrent.Executor, @Nullable android.companion.virtual.audio.VirtualAudioDevice.AudioConfigurationChangeCallback);
+    method @FlaggedApi("android.companion.virtual.flags.virtual_camera") @NonNull public android.companion.virtual.camera.VirtualCamera createVirtualCamera(@NonNull android.companion.virtual.camera.VirtualCameraConfig);
+    method @Deprecated @Nullable public android.hardware.display.VirtualDisplay createVirtualDisplay(@IntRange(from=1) int, @IntRange(from=1) int, @IntRange(from=1) int, @Nullable android.view.Surface, int, @Nullable java.util.concurrent.Executor, @Nullable android.hardware.display.VirtualDisplay.Callback);
+    method @Nullable public android.hardware.display.VirtualDisplay createVirtualDisplay(@NonNull android.hardware.display.VirtualDisplayConfig, @Nullable java.util.concurrent.Executor, @Nullable android.hardware.display.VirtualDisplay.Callback);
+    method @NonNull public android.hardware.input.VirtualDpad createVirtualDpad(@NonNull android.hardware.input.VirtualDpadConfig);
+    method @NonNull public android.hardware.input.VirtualKeyboard createVirtualKeyboard(@NonNull android.hardware.input.VirtualKeyboardConfig);
+    method @Deprecated @NonNull public android.hardware.input.VirtualKeyboard createVirtualKeyboard(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int);
+    method @NonNull public android.hardware.input.VirtualMouse createVirtualMouse(@NonNull android.hardware.input.VirtualMouseConfig);
+    method @Deprecated @NonNull public android.hardware.input.VirtualMouse createVirtualMouse(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int);
+    method @NonNull public android.hardware.input.VirtualNavigationTouchpad createVirtualNavigationTouchpad(@NonNull android.hardware.input.VirtualNavigationTouchpadConfig);
+    method @FlaggedApi("android.companion.virtualdevice.flags.virtual_rotary") @NonNull public android.hardware.input.VirtualRotaryEncoder createVirtualRotaryEncoder(@NonNull android.hardware.input.VirtualRotaryEncoderConfig);
+    method @FlaggedApi("android.companion.virtual.flags.virtual_stylus") @NonNull public android.hardware.input.VirtualStylus createVirtualStylus(@NonNull android.hardware.input.VirtualStylusConfig);
+    method @NonNull public android.hardware.input.VirtualTouchscreen createVirtualTouchscreen(@NonNull android.hardware.input.VirtualTouchscreenConfig);
+    method @Deprecated @NonNull public android.hardware.input.VirtualTouchscreen createVirtualTouchscreen(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int);
     method public int getDeviceId();
     method @FlaggedApi("android.companion.virtual.flags.vdm_public_apis") @Nullable public String getPersistentDeviceId();
-    method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public java.util.List<android.companion.virtual.sensor.VirtualSensor> getVirtualSensorList();
-    method @FlaggedApi("android.companion.virtualdevice.flags.device_aware_display_power") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void goToSleep();
-    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void launchPendingIntent(int, @NonNull android.app.PendingIntent, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.IntConsumer);
-    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void registerIntentInterceptor(@NonNull android.content.IntentFilter, @NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.VirtualDeviceManager.IntentInterceptorCallback);
+    method @NonNull public java.util.List<android.companion.virtual.sensor.VirtualSensor> getVirtualSensorList();
+    method @FlaggedApi("android.companion.virtualdevice.flags.device_aware_display_power") public void goToSleep();
+    method public void launchPendingIntent(int, @NonNull android.app.PendingIntent, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.IntConsumer);
+    method public void registerIntentInterceptor(@NonNull android.content.IntentFilter, @NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.VirtualDeviceManager.IntentInterceptorCallback);
     method public void removeActivityListener(@NonNull android.companion.virtual.VirtualDeviceManager.ActivityListener);
-    method @FlaggedApi("android.companion.virtual.flags.dynamic_policy") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void removeActivityPolicyExemption(@NonNull android.content.ComponentName);
-    method @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void removeActivityPolicyExemption(@NonNull android.companion.virtual.ActivityPolicyExemption);
+    method @FlaggedApi("android.companion.virtual.flags.dynamic_policy") public void removeActivityPolicyExemption(@NonNull android.content.ComponentName);
+    method @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") public void removeActivityPolicyExemption(@NonNull android.companion.virtual.ActivityPolicyExemption);
     method public void removeSoundEffectListener(@NonNull android.companion.virtual.VirtualDeviceManager.SoundEffectListener);
-    method @FlaggedApi("android.companion.virtual.flags.dynamic_policy") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void setDevicePolicy(int, int);
-    method @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void setDevicePolicy(int, int, int);
-    method @FlaggedApi("android.companion.virtual.flags.vdm_custom_ime") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void setDisplayImePolicy(int, int);
-    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void setShowPointerIcon(boolean);
-    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void unregisterIntentInterceptor(@NonNull android.companion.virtual.VirtualDeviceManager.IntentInterceptorCallback);
-    method @FlaggedApi("android.companion.virtualdevice.flags.device_aware_display_power") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void wakeUp();
+    method @FlaggedApi("android.companion.virtual.flags.dynamic_policy") public void setDevicePolicy(int, int);
+    method @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") public void setDevicePolicy(int, int, int);
+    method @FlaggedApi("android.companion.virtual.flags.vdm_custom_ime") public void setDisplayImePolicy(int, int);
+    method public void setShowPointerIcon(boolean);
+    method public void unregisterIntentInterceptor(@NonNull android.companion.virtual.VirtualDeviceManager.IntentInterceptorCallback);
+    method @FlaggedApi("android.companion.virtualdevice.flags.device_aware_display_power") public void wakeUp();
   }
 
   public final class VirtualDeviceParams implements android.os.Parcelable {
@@ -3632,7 +3632,7 @@
 package android.companion.virtual.camera {
 
   @FlaggedApi("android.companion.virtual.flags.virtual_camera") public final class VirtualCamera implements java.io.Closeable {
-    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close();
+    method public void close();
     method @NonNull public android.companion.virtual.camera.VirtualCameraConfig getConfig();
   }
 
@@ -3684,7 +3684,7 @@
     method public int getDeviceId();
     method @NonNull public String getName();
     method public int getType();
-    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendEvent(@NonNull android.companion.virtual.sensor.VirtualSensorEvent);
+    method public void sendEvent(@NonNull android.companion.virtual.sensor.VirtualSensorEvent);
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.companion.virtual.sensor.VirtualSensor> CREATOR;
   }
@@ -5695,8 +5695,8 @@
 package android.hardware.input {
 
   public class VirtualDpad implements java.io.Closeable {
-    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close();
-    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendKeyEvent(@NonNull android.hardware.input.VirtualKeyEvent);
+    method public void close();
+    method public void sendKeyEvent(@NonNull android.hardware.input.VirtualKeyEvent);
   }
 
   public final class VirtualDpadConfig extends android.hardware.input.VirtualInputDeviceConfig implements android.os.Parcelable {
@@ -5747,8 +5747,8 @@
   }
 
   public class VirtualKeyboard implements java.io.Closeable {
-    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close();
-    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendKeyEvent(@NonNull android.hardware.input.VirtualKeyEvent);
+    method public void close();
+    method public void sendKeyEvent(@NonNull android.hardware.input.VirtualKeyEvent);
   }
 
   public final class VirtualKeyboardConfig extends android.hardware.input.VirtualInputDeviceConfig implements android.os.Parcelable {
@@ -5769,11 +5769,11 @@
   }
 
   public class VirtualMouse implements java.io.Closeable {
-    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close();
-    method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.graphics.PointF getCursorPosition();
-    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendButtonEvent(@NonNull android.hardware.input.VirtualMouseButtonEvent);
-    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendRelativeEvent(@NonNull android.hardware.input.VirtualMouseRelativeEvent);
-    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendScrollEvent(@NonNull android.hardware.input.VirtualMouseScrollEvent);
+    method public void close();
+    method @NonNull public android.graphics.PointF getCursorPosition();
+    method public void sendButtonEvent(@NonNull android.hardware.input.VirtualMouseButtonEvent);
+    method public void sendRelativeEvent(@NonNull android.hardware.input.VirtualMouseRelativeEvent);
+    method public void sendScrollEvent(@NonNull android.hardware.input.VirtualMouseScrollEvent);
   }
 
   public final class VirtualMouseButtonEvent implements android.os.Parcelable {
@@ -5846,8 +5846,8 @@
   }
 
   public class VirtualNavigationTouchpad implements java.io.Closeable {
-    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close();
-    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendTouchEvent(@NonNull android.hardware.input.VirtualTouchEvent);
+    method public void close();
+    method public void sendTouchEvent(@NonNull android.hardware.input.VirtualTouchEvent);
   }
 
   public final class VirtualNavigationTouchpadConfig extends android.hardware.input.VirtualInputDeviceConfig implements android.os.Parcelable {
@@ -5864,8 +5864,8 @@
   }
 
   @FlaggedApi("android.companion.virtualdevice.flags.virtual_rotary") public class VirtualRotaryEncoder implements java.io.Closeable {
-    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close();
-    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendScrollEvent(@NonNull android.hardware.input.VirtualRotaryEncoderScrollEvent);
+    method public void close();
+    method public void sendScrollEvent(@NonNull android.hardware.input.VirtualRotaryEncoderScrollEvent);
   }
 
   @FlaggedApi("android.companion.virtualdevice.flags.virtual_rotary") public final class VirtualRotaryEncoderConfig extends android.hardware.input.VirtualInputDeviceConfig implements android.os.Parcelable {
@@ -5895,9 +5895,9 @@
   }
 
   @FlaggedApi("android.companion.virtual.flags.virtual_stylus") public class VirtualStylus implements java.io.Closeable {
-    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close();
-    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendButtonEvent(@NonNull android.hardware.input.VirtualStylusButtonEvent);
-    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendMotionEvent(@NonNull android.hardware.input.VirtualStylusMotionEvent);
+    method public void close();
+    method public void sendButtonEvent(@NonNull android.hardware.input.VirtualStylusButtonEvent);
+    method public void sendMotionEvent(@NonNull android.hardware.input.VirtualStylusMotionEvent);
   }
 
   @FlaggedApi("android.companion.virtual.flags.virtual_stylus") public final class VirtualStylusButtonEvent implements android.os.Parcelable {
@@ -6000,8 +6000,8 @@
   }
 
   public class VirtualTouchscreen implements java.io.Closeable {
-    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close();
-    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendTouchEvent(@NonNull android.hardware.input.VirtualTouchEvent);
+    method public void close();
+    method public void sendTouchEvent(@NonNull android.hardware.input.VirtualTouchEvent);
   }
 
   public final class VirtualTouchscreenConfig extends android.hardware.input.VirtualInputDeviceConfig implements android.os.Parcelable {
diff --git a/core/java/android/app/AppOps.md b/core/java/android/app/AppOps.md
index 7b11a03..535d62c 100644
--- a/core/java/android/app/AppOps.md
+++ b/core/java/android/app/AppOps.md
@@ -119,20 +119,20 @@
 In addition to proc state, the `AppOpsService` also receives process capability update from the
 `ActivityManagerService`. Proc capability specifies what while-in-use(`MODE_FOREGROUND`) operations
  the proc is allowed to perform in its current proc state. There are three proc capabilities
- defined so far: 
+ defined so far:
 `PROCESS_CAPABILITY_FOREGROUND_LOCATION`, `PROCESS_CAPABILITY_FOREGROUND_CAMERA` and
 `PROCESS_CAPABILITY_FOREGROUND_MICROPHONE`, they correspond to the while-in-use operation of
 location, camera and microphone (microphone is `RECORD_AUDIO`).
 
 In `ActivityManagerService`, `PROCESS_STATE_TOP` and `PROCESS_STATE_PERSISTENT` have all
 three capabilities, `PROCESS_STATE_FOREGROUND_SERVICE` has capabilities defined by
- `foregroundServiceType` that is specified in foreground service's manifest file. A client process 
+ `foregroundServiceType` that is specified in foreground service's manifest file. A client process
  can pass its capabilities to service using `BIND_INCLUDE_CAPABILITIES` flag.
 
 The proc state and capability are used for two use cases: Firstly, Tracking remembers the proc state
  for each tracked event. Secondly, `noteOp`/`checkOp` calls for app-op that are set to
  `MODE_FOREGROUND` are translated using the `AppOpsService.UidState.evalMode` method into
- `MODE_ALLOWED` when the app has the capability and `MODE_IGNORED` when the app does not have the 
+ `MODE_ALLOWED` when the app has the capability and `MODE_IGNORED` when the app does not have the
  capability. `checkOpRaw` calls are not affected.
 
 The current proc state and capability for an app can be read from `dumpsys appops`.
@@ -284,7 +284,7 @@
 ##### Self data accesses
 
 This is similar to the [synchronous data access](#synchronous-data-accesses) case only that the data
-provider and client are in the same process. In this case Android's RPC code is no involved and
+provider and client are in the same process. In this case Android's RPC code is not involved and
 `AppOpsManager.noteOp` directly triggers `OnOpNotedCallback.onSelfNoted`. This should be a uncommon
 case as it is uncommon for an app to provide data, esp. to itself.
 
diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl
index 40debe8..d3a1c25 100644
--- a/core/java/android/companion/virtual/IVirtualDevice.aidl
+++ b/core/java/android/companion/virtual/IVirtualDevice.aidl
@@ -98,191 +98,160 @@
     /*
      * Turns off all trusted non-mirror displays of the virtual device.
      */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     void goToSleep();
 
     /**
      * Turns on all trusted non-mirror displays of the virtual device.
      */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     void wakeUp();
 
     /**
      * Closes the virtual device and frees all associated resources.
      */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     void close();
 
     /**
      * Specifies a policy for this virtual device.
      */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     void setDevicePolicy(int policyType, int devicePolicy);
 
     /**
      * Adds an exemption to the default activity launch policy.
      */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     void addActivityPolicyExemption(in ActivityPolicyExemption exemption);
 
     /**
      * Removes an exemption to the default activity launch policy.
      */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     void removeActivityPolicyExemption(in ActivityPolicyExemption exemption);
 
     /**
      * Specifies a policy for this virtual device on the given display.
      */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     void setDevicePolicyForDisplay(int displayId, int policyType, int devicePolicy);
 
     /**
      * Notifies that an audio session being started.
      */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     void onAudioSessionStarting(int displayId, IAudioRoutingCallback routingCallback,
             IAudioConfigChangedCallback configChangedCallback);
 
     /**
      * Notifies that an audio session has ended.
      */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     void onAudioSessionEnded();
 
     /**
      * Creates a virtual display and registers it with the display framework.
      */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     int createVirtualDisplay(in VirtualDisplayConfig virtualDisplayConfig,
             in IVirtualDisplayCallback callback);
 
     /**
      * Creates a new dpad and registers it with the input framework with the given token.
      */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     void createVirtualDpad(in VirtualDpadConfig config, IBinder token);
 
     /**
      * Creates a new keyboard and registers it with the input framework with the given token.
      */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     void createVirtualKeyboard(in VirtualKeyboardConfig config, IBinder token);
 
     /**
      * Creates a new mouse and registers it with the input framework with the given token.
      */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     void createVirtualMouse(in VirtualMouseConfig config, IBinder token);
 
     /**
      * Creates a new touchscreen and registers it with the input framework with the given token.
      */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     void createVirtualTouchscreen(in VirtualTouchscreenConfig config, IBinder token);
 
     /**
      * Creates a new navigation touchpad and registers it with the input framework with the given
      * token.
      */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     void createVirtualNavigationTouchpad(in VirtualNavigationTouchpadConfig config, IBinder token);
 
     /**
      * Creates a new stylus and registers it with the input framework with the given token.
      */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     void createVirtualStylus(in VirtualStylusConfig config, IBinder token);
 
     /**
      * Creates a new rotary encoder and registers it with the input framework with the given token.
      */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     void createVirtualRotaryEncoder(in VirtualRotaryEncoderConfig config, IBinder token);
 
     /**
      * Removes the input device corresponding to the given token from the framework.
      */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     void unregisterInputDevice(IBinder token);
 
     /**
      * Returns the ID of the device corresponding to the given token, as registered with the input
      * framework.
      */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     int getInputDeviceId(IBinder token);
 
     /**
      * Injects a key event to the virtual dpad corresponding to the given token.
      */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     boolean sendDpadKeyEvent(IBinder token, in VirtualKeyEvent event);
 
     /**
      * Injects a key event to the virtual keyboard corresponding to the given token.
      */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     boolean sendKeyEvent(IBinder token, in VirtualKeyEvent event);
 
     /**
      * Injects a button event to the virtual mouse corresponding to the given token.
      */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     boolean sendButtonEvent(IBinder token, in VirtualMouseButtonEvent event);
 
     /**
      * Injects a relative event to the virtual mouse corresponding to the given token.
      */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     boolean sendRelativeEvent(IBinder token, in VirtualMouseRelativeEvent event);
 
     /**
      * Injects a scroll event to the virtual mouse corresponding to the given token.
      */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     boolean sendScrollEvent(IBinder token, in VirtualMouseScrollEvent event);
 
     /**
     * Injects a touch event to the virtual touch input device corresponding to the given token.
     */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     boolean sendTouchEvent(IBinder token, in VirtualTouchEvent event);
 
     /**
      * Injects a motion event from the virtual stylus input device corresponding to the given token.
      */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     boolean sendStylusMotionEvent(IBinder token, in VirtualStylusMotionEvent event);
 
     /**
      * Injects a button event from the virtual stylus input device corresponding to the given token.
      */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     boolean sendStylusButtonEvent(IBinder token, in VirtualStylusButtonEvent event);
 
     /**
      * Injects a scroll event from the virtual rotary encoder corresponding to the given token.
      */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     boolean sendRotaryEncoderScrollEvent(IBinder token, in VirtualRotaryEncoderScrollEvent event);
 
     /**
      * Returns all virtual sensors created for this device.
      */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     List<VirtualSensor> getVirtualSensorList();
 
     /**
      * Sends an event to the virtual sensor corresponding to the given token.
      */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     boolean sendSensorEvent(IBinder token, in VirtualSensorEvent event);
 
     /**
      * Launches a pending intent on the given display that is owned by this virtual device.
      */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     void launchPendingIntent(int displayId, in PendingIntent pendingIntent,
             in ResultReceiver resultReceiver);
 
@@ -290,15 +259,12 @@
      * Returns the current cursor position of the mouse corresponding to the given token, in x and y
      * coordinates.
      */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     PointF getCursorPosition(IBinder token);
 
     /** Sets whether to show or hide the cursor while this virtual device is active. */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     void setShowPointerIcon(boolean showPointerIcon);
 
     /** Sets an IME policy for the given display. */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     void setDisplayImePolicy(int displayId, int policy);
 
     /**
@@ -306,33 +272,28 @@
      * when matching the provided IntentFilter and calls the callback with the intercepted
      * intent.
      */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     void registerIntentInterceptor(in IVirtualDeviceIntentInterceptor intentInterceptor,
             in IntentFilter filter);
 
     /**
      * Unregisters a previously registered intent interceptor.
      */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     void unregisterIntentInterceptor(in IVirtualDeviceIntentInterceptor intentInterceptor);
 
     /**
      * Creates a new virtual camera and registers it with the virtual camera service.
      */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     void registerVirtualCamera(in VirtualCameraConfig camera);
 
     /**
      * Destroys the virtual camera with given config and unregisters it from the virtual camera
      * service.
      */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     void unregisterVirtualCamera(in VirtualCameraConfig camera);
 
     /**
      * Returns the id of the virtual camera with given config.
      */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     String getVirtualCameraId(in VirtualCameraConfig camera);
 
     /**
@@ -342,7 +303,6 @@
      * This is needed for virtual devices that are created by the system, as the VirtualDeviceImpl
      * object is created before the returned VirtualDeviceInternal one.
      */
-    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     void setListeners(in IVirtualDeviceActivityListener activityListener,
             in IVirtualDeviceSoundEffectListener soundEffectListener);
 }
diff --git a/core/java/android/companion/virtual/VirtualDeviceInternal.java b/core/java/android/companion/virtual/VirtualDeviceInternal.java
index 6708cce..d63a443 100644
--- a/core/java/android/companion/virtual/VirtualDeviceInternal.java
+++ b/core/java/android/companion/virtual/VirtualDeviceInternal.java
@@ -26,7 +26,6 @@
 import android.annotation.CallbackExecutor;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.annotation.RequiresPermission;
 import android.annotation.UserIdInt;
 import android.app.PendingIntent;
 import android.companion.virtual.audio.VirtualAudioDevice;
@@ -242,7 +241,6 @@
         }
     }
 
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     @NonNull
     List<VirtualSensor> getVirtualSensorList() {
         try {
@@ -252,7 +250,6 @@
         }
     }
 
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     void goToSleep() {
         try {
             mVirtualDevice.goToSleep();
@@ -261,7 +258,6 @@
         }
     }
 
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     void wakeUp() {
         try {
             mVirtualDevice.wakeUp();
@@ -270,7 +266,6 @@
         }
     }
 
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     void launchPendingIntent(
             int displayId,
             @NonNull PendingIntent pendingIntent,
@@ -292,7 +287,6 @@
         }
     }
 
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     @Nullable
     VirtualDisplay createVirtualDisplay(
             @NonNull VirtualDisplayConfig config,
@@ -310,7 +304,6 @@
         return displayManager.createVirtualDisplayWrapper(config, callbackWrapper, displayId);
     }
 
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     void close() {
         try {
             // This also takes care of unregistering all virtual sensors.
@@ -324,7 +317,6 @@
         }
     }
 
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     void setDevicePolicy(@VirtualDeviceParams.DynamicPolicyType int policyType,
             @VirtualDeviceParams.DevicePolicy int devicePolicy) {
         switch (policyType) {
@@ -344,7 +336,6 @@
         }
     }
 
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     void addActivityPolicyExemption(@NonNull ActivityPolicyExemption exemption) {
         try {
             mVirtualDevice.addActivityPolicyExemption(exemption);
@@ -353,7 +344,6 @@
         }
     }
 
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     void removeActivityPolicyExemption(@NonNull ActivityPolicyExemption exemption) {
         try {
             mVirtualDevice.removeActivityPolicyExemption(exemption);
@@ -362,7 +352,6 @@
         }
     }
 
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     void setDevicePolicyForDisplay(int displayId,
             @VirtualDeviceParams.DynamicDisplayPolicyType int policyType,
             @VirtualDeviceParams.DevicePolicy int devicePolicy) {
@@ -382,7 +371,6 @@
         }
     }
 
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     @NonNull
     VirtualDpad createVirtualDpad(@NonNull VirtualDpadConfig config) {
         try {
@@ -395,7 +383,6 @@
         }
     }
 
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     @NonNull
     VirtualKeyboard createVirtualKeyboard(@NonNull VirtualKeyboardConfig config) {
         try {
@@ -408,7 +395,6 @@
         }
     }
 
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     @NonNull
     VirtualMouse createVirtualMouse(@NonNull VirtualMouseConfig config) {
         try {
@@ -421,7 +407,6 @@
         }
     }
 
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     @NonNull
     VirtualTouchscreen createVirtualTouchscreen(
             @NonNull VirtualTouchscreenConfig config) {
@@ -435,7 +420,6 @@
         }
     }
 
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     @NonNull
     VirtualStylus createVirtualStylus(@NonNull VirtualStylusConfig config) {
         try {
@@ -448,7 +432,6 @@
         }
     }
 
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     @NonNull
     VirtualRotaryEncoder createVirtualRotaryEncoder(@NonNull VirtualRotaryEncoderConfig config) {
         try {
@@ -461,7 +444,6 @@
         }
     }
 
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     @NonNull
     VirtualNavigationTouchpad createVirtualNavigationTouchpad(
             @NonNull VirtualNavigationTouchpadConfig config) {
@@ -476,7 +458,6 @@
         }
     }
 
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     @NonNull
     VirtualAudioDevice createVirtualAudioDevice(
             @NonNull VirtualDisplay display,
@@ -501,7 +482,6 @@
         return mVirtualAudioDevice;
     }
 
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     @NonNull
     VirtualCamera createVirtualCamera(@NonNull VirtualCameraConfig config) {
         try {
@@ -513,7 +493,6 @@
         }
     }
 
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     void setShowPointerIcon(boolean showPointerIcon) {
         try {
             mVirtualDevice.setShowPointerIcon(showPointerIcon);
@@ -522,7 +501,6 @@
         }
     }
 
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     void setDisplayImePolicy(int displayId, @WindowManager.DisplayImePolicy int policy) {
         try {
             mVirtualDevice.setDisplayImePolicy(displayId, policy);
@@ -564,7 +542,6 @@
         }
     }
 
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     void registerIntentInterceptor(
             @NonNull IntentFilter interceptorFilter,
             @CallbackExecutor @NonNull Executor executor,
@@ -584,7 +561,6 @@
         }
     }
 
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     void unregisterIntentInterceptor(
             @NonNull VirtualDeviceManager.IntentInterceptorCallback interceptorCallback) {
         Objects.requireNonNull(interceptorCallback);
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index 96700a9..6ea7834 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -614,7 +614,6 @@
          *
          * @return A list of all sensors for this device, or an empty list if no sensors exist.
          */
-        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         @NonNull
         public List<VirtualSensor> getVirtualSensorList() {
             return mVirtualDeviceInternal.getVirtualSensorList();
@@ -635,7 +634,6 @@
          * @see DisplayManager#VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
          */
         @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER)
-        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         public void goToSleep() {
             mVirtualDeviceInternal.goToSleep();
         }
@@ -654,7 +652,6 @@
          * @see DisplayManager#VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
          */
         @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER)
-        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         public void wakeUp() {
             mVirtualDeviceInternal.wakeUp();
         }
@@ -677,7 +674,6 @@
          *   on the virtual display, or one of the {@code LAUNCH_FAILED} status explaining why it
          *   failed.
          */
-        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         public void launchPendingIntent(
                 int displayId,
                 @NonNull PendingIntent pendingIntent,
@@ -718,7 +714,6 @@
          * VirtualDisplay.Callback)}
          */
         @Deprecated
-        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         @Nullable
         public VirtualDisplay createVirtualDisplay(
                 @IntRange(from = 1) int width,
@@ -756,7 +751,6 @@
          *
          * @see DisplayManager#createVirtualDisplay
          */
-        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         @Nullable
         public VirtualDisplay createVirtualDisplay(
                 @NonNull VirtualDisplayConfig config,
@@ -770,7 +764,6 @@
          * Closes the virtual device, stopping and tearing down any virtual displays, associated
          * virtual audio device, and event injection that's currently in progress.
          */
-        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         public void close() {
             mVirtualDeviceInternal.close();
         }
@@ -789,7 +782,6 @@
          * @see VirtualDeviceParams#POLICY_TYPE_ACTIVITY
          */
         @FlaggedApi(Flags.FLAG_DYNAMIC_POLICY)
-        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         public void setDevicePolicy(@VirtualDeviceParams.DynamicPolicyType int policyType,
                 @VirtualDeviceParams.DevicePolicy int devicePolicy) {
             mVirtualDeviceInternal.setDevicePolicy(policyType, devicePolicy);
@@ -812,7 +804,6 @@
          * @see #setDevicePolicy
          */
         @FlaggedApi(Flags.FLAG_DYNAMIC_POLICY)
-        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         public void addActivityPolicyExemption(@NonNull ComponentName componentName) {
             addActivityPolicyExemption(new ActivityPolicyExemption.Builder()
                     .setComponentName(componentName)
@@ -836,7 +827,6 @@
          * @see #setDevicePolicy
          */
         @FlaggedApi(Flags.FLAG_DYNAMIC_POLICY)
-        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         public void removeActivityPolicyExemption(@NonNull ComponentName componentName) {
             removeActivityPolicyExemption(new ActivityPolicyExemption.Builder()
                     .setComponentName(componentName)
@@ -861,7 +851,6 @@
          * @see #setDevicePolicy
          */
         @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API)
-        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         public void addActivityPolicyExemption(@NonNull ActivityPolicyExemption exemption) {
             mVirtualDeviceInternal.addActivityPolicyExemption(Objects.requireNonNull(exemption));
         }
@@ -877,7 +866,6 @@
          * @see #setDevicePolicy
          */
         @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API)
-        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         public void removeActivityPolicyExemption(@NonNull ActivityPolicyExemption exemption) {
             mVirtualDeviceInternal.removeActivityPolicyExemption(Objects.requireNonNull(exemption));
         }
@@ -900,7 +888,6 @@
          * @see VirtualDeviceParams#POLICY_TYPE_ACTIVITY
          */
         @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API)
-        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         public void setDevicePolicy(
                 @VirtualDeviceParams.DynamicDisplayPolicyType int policyType,
                 @VirtualDeviceParams.DevicePolicy int devicePolicy,
@@ -913,7 +900,6 @@
          *
          * @param config the configurations of the virtual dpad.
          */
-        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         @NonNull
         public VirtualDpad createVirtualDpad(@NonNull VirtualDpadConfig config) {
             Objects.requireNonNull(config, "config must not be null");
@@ -925,7 +911,6 @@
          *
          * @param config the configurations of the virtual keyboard.
          */
-        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         @NonNull
         public VirtualKeyboard createVirtualKeyboard(@NonNull VirtualKeyboardConfig config) {
             Objects.requireNonNull(config, "config must not be null");
@@ -943,7 +928,6 @@
          * @deprecated Use {@link #createVirtualKeyboard(VirtualKeyboardConfig config)} instead
          */
         @Deprecated
-        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         @NonNull
         public VirtualKeyboard createVirtualKeyboard(@NonNull VirtualDisplay display,
                 @NonNull String inputDeviceName, int vendorId, int productId) {
@@ -962,7 +946,6 @@
          *
          * @param config the configurations of the virtual mouse.
          */
-        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         @NonNull
         public VirtualMouse createVirtualMouse(@NonNull VirtualMouseConfig config) {
             Objects.requireNonNull(config, "config must not be null");
@@ -980,7 +963,6 @@
          * @deprecated Use {@link #createVirtualMouse(VirtualMouseConfig config)} instead
          */
         @Deprecated
-        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         @NonNull
         public VirtualMouse createVirtualMouse(@NonNull VirtualDisplay display,
                 @NonNull String inputDeviceName, int vendorId, int productId) {
@@ -999,7 +981,6 @@
          *
          * @param config the configurations of the virtual touchscreen.
          */
-        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         @NonNull
         public VirtualTouchscreen createVirtualTouchscreen(
                 @NonNull VirtualTouchscreenConfig config) {
@@ -1019,7 +1000,6 @@
          * instead
          */
         @Deprecated
-        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         @NonNull
         public VirtualTouchscreen createVirtualTouchscreen(@NonNull VirtualDisplay display,
                 @NonNull String inputDeviceName, int vendorId, int productId) {
@@ -1046,7 +1026,6 @@
          * @param config the configurations of the virtual navigation touchpad.
          * @see android.view.InputDevice#SOURCE_TOUCH_NAVIGATION
          */
-        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         @NonNull
         public VirtualNavigationTouchpad createVirtualNavigationTouchpad(
                 @NonNull VirtualNavigationTouchpadConfig config) {
@@ -1058,7 +1037,6 @@
          *
          * @param config the touchscreen configurations for the virtual stylus.
          */
-        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         @NonNull
         @FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS)
         public VirtualStylus createVirtualStylus(
@@ -1072,7 +1050,6 @@
          * @param config the configuration for the virtual rotary encoder.
          * @see android.view.InputDevice#SOURCE_ROTARY_ENCODER
          */
-        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         @NonNull
         @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_VIRTUAL_ROTARY)
         public VirtualRotaryEncoder createVirtualRotaryEncoder(
@@ -1100,7 +1077,6 @@
          *   applications running on virtual display is changed.
          * @return A {@link VirtualAudioDevice} instance.
          */
-        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         @NonNull
         public VirtualAudioDevice createVirtualAudioDevice(
                 @NonNull VirtualDisplay display,
@@ -1121,7 +1097,6 @@
          * @throws UnsupportedOperationException if virtual camera isn't supported on this device.
          * @see VirtualDeviceParams#POLICY_TYPE_CAMERA
          */
-        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         @NonNull
         @FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
         public VirtualCamera createVirtualCamera(@NonNull VirtualCameraConfig config) {
@@ -1135,10 +1110,12 @@
         /**
          * Sets the visibility of the pointer icon for this VirtualDevice's associated displays.
          *
+         * <p>Only applicable to trusted displays.</p>
+         *
          * @param showPointerIcon True if the pointer should be shown; false otherwise. The default
          *   visibility is true.
+         * @see DisplayManager#VIRTUAL_DISPLAY_FLAG_TRUSTED
          */
-        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         public void setShowPointerIcon(boolean showPointerIcon) {
             mVirtualDeviceInternal.setShowPointerIcon(showPointerIcon);
         }
@@ -1153,7 +1130,6 @@
          * @throws SecurityException if the display is not owned by this device or is not
          *                           {@link DisplayManager#VIRTUAL_DISPLAY_FLAG_TRUSTED trusted}
          */
-        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         @FlaggedApi(Flags.FLAG_VDM_CUSTOM_IME)
         public void setDisplayImePolicy(int displayId, @WindowManager.DisplayImePolicy int policy) {
             if (Flags.vdmCustomIme()) {
@@ -1217,7 +1193,6 @@
          * is intercepted.
          * @see #unregisterIntentInterceptor(IntentInterceptorCallback)
          */
-        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         public void registerIntentInterceptor(
                 @NonNull IntentFilter interceptorFilter,
                 @CallbackExecutor @NonNull Executor executor,
@@ -1230,7 +1205,6 @@
          * Unregisters the intent interceptor previously registered with
          * {@link #registerIntentInterceptor}.
          */
-        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         public void unregisterIntentInterceptor(
                     @NonNull IntentInterceptorCallback interceptorCallback) {
             mVirtualDeviceInternal.unregisterIntentInterceptor(interceptorCallback);
diff --git a/core/java/android/companion/virtual/VirtualDeviceParams.java b/core/java/android/companion/virtual/VirtualDeviceParams.java
index 03b72bd..65f9cbe 100644
--- a/core/java/android/companion/virtual/VirtualDeviceParams.java
+++ b/core/java/android/companion/virtual/VirtualDeviceParams.java
@@ -159,7 +159,7 @@
      * @hide
      */
     @IntDef(prefix = "POLICY_TYPE_", value = {POLICY_TYPE_SENSORS, POLICY_TYPE_AUDIO,
-            POLICY_TYPE_RECENTS, POLICY_TYPE_ACTIVITY, POLICY_TYPE_CAMERA,
+            POLICY_TYPE_RECENTS, POLICY_TYPE_ACTIVITY, POLICY_TYPE_CLIPBOARD, POLICY_TYPE_CAMERA,
             POLICY_TYPE_BLOCKED_ACTIVITY})
     @Retention(RetentionPolicy.SOURCE)
     @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
@@ -220,11 +220,16 @@
      * Tells the activity manager how to handle recents entries for activities run on this device.
      *
      * <ul>
-     *     <li>{@link #DEVICE_POLICY_DEFAULT}: Activities launched on VirtualDisplays owned by this
+     *     <li>{@link #DEVICE_POLICY_DEFAULT}: Activities launched on trusted displays owned by this
      *     device will appear in the host device recents.
-     *     <li>{@link #DEVICE_POLICY_CUSTOM}: Activities launched on VirtualDisplays owned by this
+     *     <li>{@link #DEVICE_POLICY_CUSTOM}: Activities launched on trusted displays owned by this
      *     device will not appear in recents.
      * </ul>
+     *
+     * <p>Activities launched on untrusted displays will always show in the host device recents,
+     * regardless of the policy.</p>
+     *
+     * @see android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED
      */
     public static final int POLICY_TYPE_RECENTS = 2;
 
@@ -254,8 +259,10 @@
      *     not shared with other devices' clipboards, including the clipboard of the default device.
      *     <li>{@link #DEVICE_POLICY_CUSTOM}: The device's clipboard is shared with the default
      *     device's clipboard. Any clipboard operation on the virtual device is as if it was done on
-     *     the default device.
+     *     the default device. Requires all displays of the virtual device to be trusted.
      * </ul>
+     *
+     * @see android.hardware.display.DisplayManager#VIRTUAL_DISPLAY_FLAG_TRUSTED
      */
     @FlaggedApi(Flags.FLAG_CROSS_DEVICE_CLIPBOARD)
     public static final int POLICY_TYPE_CLIPBOARD = 4;
@@ -821,8 +828,8 @@
         }
 
         /**
-         * Specifies a component to be used as input method on all displays owned by this virtual
-         * device.
+         * Specifies a component to be used as input method on all trusted displays owned by this
+         * virtual device.
          *
          * @param inputMethodComponent The component name to be used as input method. Must comply to
          *   all general input method requirements described in the guide to
@@ -831,6 +838,7 @@
          *   may interact with the virtual device, then there will effectively be no IME on this
          *   device's displays for that user.
          *
+         * @see android.hardware.display.DisplayManager#VIRTUAL_DISPLAY_FLAG_TRUSTED
          * @see android.inputmethodservice.InputMethodService
          * @attr ref android.R.styleable#InputMethod_isVirtualDeviceOnly
          * @attr ref android.R.styleable#InputMethod_showInInputMethodPicker
diff --git a/core/java/android/companion/virtual/camera/VirtualCamera.java b/core/java/android/companion/virtual/camera/VirtualCamera.java
index f727589..ece048d 100644
--- a/core/java/android/companion/virtual/camera/VirtualCamera.java
+++ b/core/java/android/companion/virtual/camera/VirtualCamera.java
@@ -17,7 +17,6 @@
 package android.companion.virtual.camera;
 
 import android.annotation.FlaggedApi;
-import android.annotation.RequiresPermission;
 import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.annotation.TestApi;
@@ -94,7 +93,6 @@
     }
 
     @Override
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void close() {
         try {
             mVirtualDevice.unregisterVirtualCamera(mConfig);
diff --git a/core/java/android/companion/virtual/sensor/VirtualSensor.java b/core/java/android/companion/virtual/sensor/VirtualSensor.java
index 37e494b..934a1a8 100644
--- a/core/java/android/companion/virtual/sensor/VirtualSensor.java
+++ b/core/java/android/companion/virtual/sensor/VirtualSensor.java
@@ -17,7 +17,6 @@
 package android.companion.virtual.sensor;
 
 import android.annotation.NonNull;
-import android.annotation.RequiresPermission;
 import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.annotation.TestApi;
@@ -136,7 +135,6 @@
     /**
      * Send a sensor event to the system.
      */
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void sendEvent(@NonNull VirtualSensorEvent event) {
         try {
             mVirtualDevice.sendSensorEvent(mToken, event);
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index dcf82bf..ff0a3dd 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -47,13 +47,6 @@
 }
 
 flag {
-    name: "start_user_before_scheduled_alarms"
-    namespace: "multiuser"
-    description: "Persist list of users with alarms scheduled and wakeup stopped users before alarms are due"
-    bug: "314907186"
-}
-
-flag {
     name: "add_ui_for_sounds_from_background_users"
     namespace: "multiuser"
     description: "Allow foreground user to dismiss sounds that are coming from background users"
diff --git a/core/java/android/hardware/devicestate/DeviceStateManagerGlobal.java b/core/java/android/hardware/devicestate/DeviceStateManagerGlobal.java
index 1845827..739dbe1 100644
--- a/core/java/android/hardware/devicestate/DeviceStateManagerGlobal.java
+++ b/core/java/android/hardware/devicestate/DeviceStateManagerGlobal.java
@@ -32,6 +32,7 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.annotations.VisibleForTesting.Visibility;
+import com.android.window.flags.Flags;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -47,6 +48,7 @@
 @VisibleForTesting(visibility = Visibility.PACKAGE)
 public final class DeviceStateManagerGlobal {
     @Nullable
+    @GuardedBy("DeviceStateManagerGlobal.class")
     private static DeviceStateManagerGlobal sInstance;
     private static final String TAG = "DeviceStateManagerGlobal";
     private static final boolean DEBUG = Build.IS_DEBUGGABLE;
@@ -84,10 +86,12 @@
     @GuardedBy("mLock")
     private DeviceStateInfo mLastReceivedInfo;
 
+    // Constructor should be called while holding the lock.
+    // @GuardedBy("DeviceStateManagerGlobal.class") can't be used on constructors.
     @VisibleForTesting
     public DeviceStateManagerGlobal(@NonNull IDeviceStateManager deviceStateManager) {
         mDeviceStateManager = deviceStateManager;
-        registerCallbackIfNeededLocked();
+        registerCallbackLocked();
     }
 
     /**
@@ -279,14 +283,17 @@
         }
     }
 
-    private void registerCallbackIfNeededLocked() {
-        if (mCallback != null) {
-            return;
-        }
-
+    @GuardedBy("DeviceStateManagerGlobal.class")
+    private void registerCallbackLocked() {
         mCallback = new DeviceStateManagerCallback();
         try {
-            mDeviceStateManager.registerCallback(mCallback);
+            if (Flags.wlinfoOncreate()) {
+                synchronized (mLock) {
+                    mLastReceivedInfo = mDeviceStateManager.registerCallback(mCallback);
+                }
+            } else {
+                mDeviceStateManager.registerCallback(mCallback);
+            }
         } catch (RemoteException ex) {
             mCallback = null;
             throw ex.rethrowFromSystemServer();
diff --git a/core/java/android/hardware/devicestate/IDeviceStateManager.aidl b/core/java/android/hardware/devicestate/IDeviceStateManager.aidl
index ea4fe26..50d0623 100644
--- a/core/java/android/hardware/devicestate/IDeviceStateManager.aidl
+++ b/core/java/android/hardware/devicestate/IDeviceStateManager.aidl
@@ -32,16 +32,24 @@
     DeviceStateInfo getDeviceStateInfo();
 
     /**
-     * Registers a callback to receive notifications from the device state manager. Only one
-     * callback can be registered per-process.
+     * Registers a callback to receive notifications from the device state manager and returns the
+     * current {@link DeviceStateInfo}. Only one callback can be registered per-process.
      * <p>
      * As the callback mechanism is used to alert the caller of changes to request status a callback
      * <b>MUST</b> be registered before calling {@link #requestState(IBinder, int, int)} or
      * {@link #cancelRequest(IBinder)}, otherwise an exception will be thrown.
+     * <p>
+     * Upon successful registration, this method returns the committed {@link DeviceStateInfo} if
+     * available, ensuring the availability of the device state after the callback is registered.
+     * This guarantees that the client will have access to the latest device state immediately upon
+     * completion of the two-way IPC call.
      *
+     * @param callback the callback to register for device state notifications.
+     * @return DeviceStateInfo the current device state information including the committed state
+     *         or null if no state has been committed by the {@link DeviceStateProvider} yet.
      * @throws SecurityException if a callback is already registered for the calling process.
      */
-    void registerCallback(in IDeviceStateManagerCallback callback);
+    @nullable DeviceStateInfo registerCallback(in IDeviceStateManagerCallback callback);
 
     /**
      * Requests that the device enter the supplied {@code state}. A callback <b>MUST</b> have been
diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java
index 6c1aa90..75ffcc3 100644
--- a/core/java/android/hardware/display/DisplayManagerInternal.java
+++ b/core/java/android/hardware/display/DisplayManagerInternal.java
@@ -461,6 +461,16 @@
     public abstract void stylusGestureStarted(long eventTime);
 
     /**
+     * Called by {@link com.android.server.wm.ContentRecorder} to verify whether
+     * the display is allowed to mirror primary display's content.
+     * @param displayId the id of the display where we mirror to.
+     * @return true if the mirroring dialog is confirmed (display is enabled), or
+     * {@link com.android.server.display.ExternalDisplayPolicy#ENABLE_ON_CONNECT}
+     * system property is enabled.
+     */
+    public abstract boolean isDisplayReadyForMirroring(int displayId);
+
+    /**
      * Describes the requested power state of the display.
      *
      * This object is intended to describe the general characteristics of the
diff --git a/core/java/android/hardware/input/InputSettings.java b/core/java/android/hardware/input/InputSettings.java
index 177ee6f..897ce4a 100644
--- a/core/java/android/hardware/input/InputSettings.java
+++ b/core/java/android/hardware/input/InputSettings.java
@@ -24,6 +24,8 @@
 import static com.android.hardware.input.Flags.keyboardA11ySlowKeysFlag;
 import static com.android.hardware.input.Flags.keyboardA11yStickyKeysFlag;
 import static com.android.hardware.input.Flags.keyboardA11yMouseKeys;
+import static com.android.hardware.input.Flags.mouseReverseVerticalScrolling;
+import static com.android.hardware.input.Flags.mouseSwapPrimaryButton;
 import static com.android.hardware.input.Flags.touchpadTapDragging;
 import static com.android.hardware.input.Flags.touchpadVisualizer;
 import static com.android.input.flags.Flags.enableInputFilterRustImpl;
@@ -363,6 +365,22 @@
     }
 
     /**
+     * Returns true if the feature flag for mouse reverse vertical scrolling is enabled.
+     * @hide
+     */
+    public static boolean isMouseReverseVerticalScrollingFeatureFlagEnabled() {
+        return mouseReverseVerticalScrolling();
+    }
+
+    /**
+     * Returns true if the feature flag for mouse swap primary button is enabled.
+     * @hide
+     */
+    public static boolean isMouseSwapPrimaryButtonFeatureFlagEnabled() {
+        return mouseSwapPrimaryButton();
+    }
+
+    /**
      * Returns true if the touchpad visualizer is allowed to appear.
      *
      * @param context The application context.
@@ -501,6 +519,86 @@
     }
 
     /**
+     * Whether mouse vertical scrolling is enabled, this applies only to connected mice.
+     *
+     * @param context The application context.
+     * @return Whether the mouse will have its vertical scrolling reversed
+     * (scroll down to move up).
+     *
+     * @hide
+     */
+    public static boolean isMouseReverseVerticalScrollingEnabled(@NonNull Context context) {
+        if (!isMouseReverseVerticalScrollingFeatureFlagEnabled()) {
+            return false;
+        }
+
+        return Settings.System.getIntForUser(context.getContentResolver(),
+                Settings.System.MOUSE_REVERSE_VERTICAL_SCROLLING, 0, UserHandle.USER_CURRENT)
+                != 0;
+    }
+
+    /**
+     * Sets whether the connected mouse will have its vertical scrolling reversed.
+     *
+     * @param context The application context.
+     * @param reverseScrolling Whether reverse scrolling is enabled.
+     *
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.WRITE_SETTINGS)
+    public static void setMouseReverseVerticalScrolling(@NonNull Context context,
+            boolean reverseScrolling) {
+        if (!isMouseReverseVerticalScrollingFeatureFlagEnabled()) {
+            return;
+        }
+
+        Settings.System.putIntForUser(context.getContentResolver(),
+                Settings.System.MOUSE_REVERSE_VERTICAL_SCROLLING, reverseScrolling ? 1 : 0,
+                UserHandle.USER_CURRENT);
+    }
+
+    /**
+     * Whether the primary mouse button is swapped on connected mice.
+     *
+     * @param context The application context.
+     * @return Whether mice will have their primary buttons swapped, so that left clicking will
+     * perform the secondary action (e.g. show menu) and right clicking will perform the primary
+     * action.
+     *
+     * @hide
+     */
+    public static boolean isMouseSwapPrimaryButtonEnabled(@NonNull Context context) {
+        if (!isMouseSwapPrimaryButtonFeatureFlagEnabled()) {
+            return false;
+        }
+
+        return Settings.System.getIntForUser(context.getContentResolver(),
+                Settings.System.MOUSE_SWAP_PRIMARY_BUTTON, 0, UserHandle.USER_CURRENT)
+                != 0;
+    }
+
+    /**
+     * Sets whether mice will have their primary buttons swapped between left and right
+     * clicks.
+     *
+     * @param context The application context.
+     * @param swapPrimaryButton Whether swapping the primary button is enabled.
+     *
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.WRITE_SETTINGS)
+    public static void setMouseSwapPrimaryButton(@NonNull Context context,
+            boolean swapPrimaryButton) {
+        if (!isMouseSwapPrimaryButtonFeatureFlagEnabled()) {
+            return;
+        }
+
+        Settings.System.putIntForUser(context.getContentResolver(),
+                Settings.System.MOUSE_SWAP_PRIMARY_BUTTON, swapPrimaryButton ? 1 : 0,
+                UserHandle.USER_CURRENT);
+    }
+
+    /**
      * Whether Accessibility bounce keys feature is enabled.
      *
      * <p>
diff --git a/core/java/android/hardware/input/VirtualDpad.java b/core/java/android/hardware/input/VirtualDpad.java
index 5985c39..5b08a0e 100644
--- a/core/java/android/hardware/input/VirtualDpad.java
+++ b/core/java/android/hardware/input/VirtualDpad.java
@@ -17,7 +17,6 @@
 package android.hardware.input;
 
 import android.annotation.NonNull;
-import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
 import android.companion.virtual.IVirtualDevice;
 import android.os.IBinder;
@@ -72,7 +71,6 @@
      *
      * @param event the event to send
      */
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void sendKeyEvent(@NonNull VirtualKeyEvent event) {
         try {
             if (!mSupportedKeyCodes.contains(event.getKeyCode())) {
diff --git a/core/java/android/hardware/input/VirtualInputDevice.java b/core/java/android/hardware/input/VirtualInputDevice.java
index affa4ed..8e4e097 100644
--- a/core/java/android/hardware/input/VirtualInputDevice.java
+++ b/core/java/android/hardware/input/VirtualInputDevice.java
@@ -16,7 +16,6 @@
 
 package android.hardware.input;
 
-import android.annotation.RequiresPermission;
 import android.companion.virtual.IVirtualDevice;
 import android.os.IBinder;
 import android.os.RemoteException;
@@ -68,7 +67,6 @@
     }
 
     @Override
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void close() {
         Log.d(TAG, "Closing virtual input device " + mConfig.getInputDeviceName());
         try {
diff --git a/core/java/android/hardware/input/VirtualInputDeviceConfig.java b/core/java/android/hardware/input/VirtualInputDeviceConfig.java
index e8ef8cd..3b74d7f 100644
--- a/core/java/android/hardware/input/VirtualInputDeviceConfig.java
+++ b/core/java/android/hardware/input/VirtualInputDeviceConfig.java
@@ -163,7 +163,6 @@
             return self();
         }
 
-
         /**
          * Sets the product id of the device, uniquely identifying the device within the address
          * space of a given vendor, identified by the device's vendor id.
@@ -179,6 +178,10 @@
          *
          * <p>The input device is restricted to the display with the given ID and may not send
          * events to any other display.</p>
+         * <p>The corresponding display must be trusted or mirror display.</p>
+         *
+         * @see android.hardware.display.DisplayManager#VIRTUAL_DISPLAY_FLAG_TRUSTED
+         * @see android.hardware.display.DisplayManager#VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR
          */
         @NonNull
         public T setAssociatedDisplayId(int displayId) {
diff --git a/core/java/android/hardware/input/VirtualKeyboard.java b/core/java/android/hardware/input/VirtualKeyboard.java
index 6a7d195..9664004 100644
--- a/core/java/android/hardware/input/VirtualKeyboard.java
+++ b/core/java/android/hardware/input/VirtualKeyboard.java
@@ -17,7 +17,6 @@
 package android.hardware.input;
 
 import android.annotation.NonNull;
-import android.annotation.RequiresPermission;
 import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.annotation.TestApi;
@@ -31,8 +30,8 @@
  * A virtual keyboard representing a key input mechanism on a remote device, such as a built-in
  * keyboard on a laptop, a software keyboard on a tablet, or a keypad on a TV remote control.
  *
- * This registers an InputDevice that is interpreted like a physically-connected device and
- * dispatches received events to it.
+ * <p>This registers an InputDevice that is interpreted like a physically-connected device and
+ * dispatches received events to it.</p>
  *
  * @hide
  */
@@ -52,7 +51,6 @@
      *
      * @param event the event to send
      */
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void sendKeyEvent(@NonNull VirtualKeyEvent event) {
         try {
             if (mUnsupportedKeyCode == event.getKeyCode()) {
diff --git a/core/java/android/hardware/input/VirtualMouse.java b/core/java/android/hardware/input/VirtualMouse.java
index fb0f700..f2d113c 100644
--- a/core/java/android/hardware/input/VirtualMouse.java
+++ b/core/java/android/hardware/input/VirtualMouse.java
@@ -17,7 +17,6 @@
 package android.hardware.input;
 
 import android.annotation.NonNull;
-import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
 import android.companion.virtual.IVirtualDevice;
 import android.graphics.PointF;
@@ -30,8 +29,8 @@
  * A virtual mouse representing a relative input mechanism on a remote device, such as a mouse or
  * trackpad.
  *
- * This registers an InputDevice that is interpreted like a physically-connected device and
- * dispatches received events to it.
+ * <p>This registers an InputDevice that is interpreted like a physically-connected device and
+ * dispatches received events to it.</p>
  *
  * @hide
  */
@@ -50,7 +49,6 @@
      * @throws IllegalStateException if the display this mouse is associated with is not currently
      * targeted
      */
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void sendButtonEvent(@NonNull VirtualMouseButtonEvent event) {
         try {
             if (!mVirtualDevice.sendButtonEvent(mToken, event)) {
@@ -70,7 +68,6 @@
      * @throws IllegalStateException if the display this mouse is associated with is not currently
      * targeted
      */
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void sendScrollEvent(@NonNull VirtualMouseScrollEvent event) {
         try {
             if (!mVirtualDevice.sendScrollEvent(mToken, event)) {
@@ -89,7 +86,6 @@
      * @throws IllegalStateException if the display this mouse is associated with is not currently
      * targeted
      */
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void sendRelativeEvent(@NonNull VirtualMouseRelativeEvent event) {
         try {
             if (!mVirtualDevice.sendRelativeEvent(mToken, event)) {
@@ -108,7 +104,6 @@
      * @throws IllegalStateException if the display this mouse is associated with is not currently
      * targeted
      */
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public @NonNull PointF getCursorPosition() {
         try {
             return mVirtualDevice.getCursorPosition(mToken);
diff --git a/core/java/android/hardware/input/VirtualNavigationTouchpad.java b/core/java/android/hardware/input/VirtualNavigationTouchpad.java
index 3dbb385..94e2151 100644
--- a/core/java/android/hardware/input/VirtualNavigationTouchpad.java
+++ b/core/java/android/hardware/input/VirtualNavigationTouchpad.java
@@ -17,7 +17,6 @@
 package android.hardware.input;
 
 import android.annotation.NonNull;
-import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
 import android.companion.virtual.IVirtualDevice;
 import android.os.IBinder;
@@ -51,7 +50,6 @@
      *
      * @param event the event to send
      */
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void sendTouchEvent(@NonNull VirtualTouchEvent event) {
         try {
             if (!mVirtualDevice.sendTouchEvent(mToken, event)) {
diff --git a/core/java/android/hardware/input/VirtualRotaryEncoder.java b/core/java/android/hardware/input/VirtualRotaryEncoder.java
index bc131df..47c92c8 100644
--- a/core/java/android/hardware/input/VirtualRotaryEncoder.java
+++ b/core/java/android/hardware/input/VirtualRotaryEncoder.java
@@ -18,7 +18,6 @@
 
 import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
-import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
 import android.companion.virtual.IVirtualDevice;
 import android.companion.virtualdevice.flags.Flags;
@@ -48,7 +47,6 @@
      *
      * @param event the event to send
      */
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void sendScrollEvent(@NonNull VirtualRotaryEncoderScrollEvent event) {
         try {
             if (!mVirtualDevice.sendRotaryEncoderScrollEvent(mToken, event)) {
diff --git a/core/java/android/hardware/input/VirtualStylus.java b/core/java/android/hardware/input/VirtualStylus.java
index c763f740..4b79bc4 100644
--- a/core/java/android/hardware/input/VirtualStylus.java
+++ b/core/java/android/hardware/input/VirtualStylus.java
@@ -18,7 +18,6 @@
 
 import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
-import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
 import android.companion.virtual.IVirtualDevice;
 import android.companion.virtual.flags.Flags;
@@ -30,8 +29,8 @@
  * A virtual stylus which can be used to inject input into the framework that represents a stylus
  * on a remote device.
  *
- * This registers an {@link android.view.InputDevice} that is interpreted like a
- * physically-connected device and dispatches received events to it.
+ * <p>This registers an {@link android.view.InputDevice} that is interpreted like a
+ * physically-connected device and dispatches received events to it.</p>
  *
  * @hide
  */
@@ -49,7 +48,6 @@
      *
      * @param event the event to send
      */
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void sendMotionEvent(@NonNull VirtualStylusMotionEvent event) {
         try {
             if (!mVirtualDevice.sendStylusMotionEvent(mToken, event)) {
@@ -66,7 +64,6 @@
      *
      * @param event the event to send
      */
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void sendButtonEvent(@NonNull VirtualStylusButtonEvent event) {
         try {
             if (!mVirtualDevice.sendStylusButtonEvent(mToken, event)) {
diff --git a/core/java/android/hardware/input/VirtualTouchscreen.java b/core/java/android/hardware/input/VirtualTouchscreen.java
index 2c800aa..d0537f0 100644
--- a/core/java/android/hardware/input/VirtualTouchscreen.java
+++ b/core/java/android/hardware/input/VirtualTouchscreen.java
@@ -17,7 +17,6 @@
 package android.hardware.input;
 
 import android.annotation.NonNull;
-import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
 import android.companion.virtual.IVirtualDevice;
 import android.os.IBinder;
@@ -27,8 +26,8 @@
 /**
  * A virtual touchscreen representing a touch-based display input mechanism on a remote device.
  *
- * This registers an InputDevice that is interpreted like a physically-connected device and
- * dispatches received events to it.
+ * <p>This registers an InputDevice that is interpreted like a physically-connected device and
+ * dispatches received events to it.</p>
  *
  * @hide
  */
@@ -45,7 +44,6 @@
      *
      * @param event the event to send
      */
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void sendTouchEvent(@NonNull VirtualTouchEvent event) {
         try {
             if (!mVirtualDevice.sendTouchEvent(mToken, event)) {
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 3ae9511..f1964e7 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -6368,8 +6368,12 @@
                 Settings.Global.DEVICE_DEMO_MODE, 0) > 0;
     }
 
-    /** @hide */
-    public static final void invalidateUserSerialNumberCache() {
+
+    /**
+     * This method is used to invalidate caches, when user was added or removed.
+     * @hide
+     */
+    public static final void invalidateCacheOnUserListChange() {
         UserManagerCache.invalidateUserSerialNumber();
     }
 
@@ -6382,7 +6386,7 @@
      * @hide
      */
     @UnsupportedAppUsage
-    @CachedProperty(modsFlagOnOrNone = {})
+    @CachedProperty(modsFlagOnOrNone = {}, api = "user_manager_users")
     public int getUserSerialNumber(@UserIdInt int userId) {
         // Read only flag should is to fix early access to this API
         // cacheUserSerialNumber to be removed after the
diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig
index a1bfe39..81987907 100644
--- a/core/java/android/os/flags.aconfig
+++ b/core/java/android/os/flags.aconfig
@@ -232,3 +232,19 @@
     bug: "361329788"
     is_exported: true
 }
+
+flag {
+    name: "enable_angle_allow_list"
+    namespace: "gpu"
+    description: "Whether to read from angle allowlist to determine if app should use ANGLE"
+    is_fixed_read_only: true
+    bug: "370845648"
+}
+
+flag {
+    name: "api_for_backported_fixes"
+    namespace: "media_reliability"
+    description: "Public API app developers use to check if a known issue is fixed on a device."
+    bug: "308461809"
+    is_exported: true
+}
diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig
index 271970b..1d8fcec 100644
--- a/core/java/android/permission/flags.aconfig
+++ b/core/java/android/permission/flags.aconfig
@@ -261,7 +261,7 @@
     is_fixed_read_only: true
     namespace: "permissions"
     description: "If proc state is decreasing over the restriction threshold and capability is changed, delay if no new capabilities are added"
-    bug: "308573169"
+    bug: "347891382"
     metadata {
         purpose: PURPOSE_BUGFIX
     }
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 1a15d09..594005c 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -2351,6 +2351,11 @@
 
     /**
      * Activity Action: Show the permission screen for allowing apps to post promoted notifications.
+     * Properly formatted priority notifications are elevated in appearance. For example they may be
+     * able to use colors, have richer progress bars, show as chips in the status bar, and/or
+     * permanently appear on always-on-displays. This functionality is intended to be reserved for
+     * user initiated ongoing activities like navigation, phone calls, and ride sharing.
+     *
      * <p>
      *     Input: {@link #EXTRA_APP_PACKAGE}, the package to display.
      * <p>
@@ -6205,6 +6210,25 @@
         public static final String TOUCHPAD_RIGHT_CLICK_ZONE = "touchpad_right_click_zone";
 
         /**
+         * Whether to enable reversed vertical scrolling for connected mice.
+         *
+         * When enabled, scrolling down on the mouse wheel will move the screen up and vice versa.
+         * @hide
+         */
+        public static final String MOUSE_REVERSE_VERTICAL_SCROLLING =
+                "mouse_reverse_vertical_scrolling";
+
+        /**
+         * Whether to enable swapping the primary button for connected mice.
+         *
+         * When enabled, right clicking will be the primary button and left clicking will be the
+         * secondary button (e.g. show menu).
+         * @hide
+         */
+        public static final String MOUSE_SWAP_PRIMARY_BUTTON =
+                "mouse_swap_primary_button";
+
+        /**
          * Pointer fill style, specified by
          * {@link android.view.PointerIcon.PointerIconVectorStyleFill} constants.
          *
@@ -6442,6 +6466,8 @@
             PRIVATE_SETTINGS.add(SCREEN_FLASH_NOTIFICATION);
             PRIVATE_SETTINGS.add(SCREEN_FLASH_NOTIFICATION_COLOR);
             PRIVATE_SETTINGS.add(DEFAULT_DEVICE_FONT_SCALE);
+            PRIVATE_SETTINGS.add(MOUSE_REVERSE_VERTICAL_SCROLLING);
+            PRIVATE_SETTINGS.add(MOUSE_SWAP_PRIMARY_BUTTON);
         }
 
         /**
diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java
index 6fb82af..8e35843e 100644
--- a/core/java/android/window/DesktopModeFlags.java
+++ b/core/java/android/window/DesktopModeFlags.java
@@ -65,7 +65,9 @@
     ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS(
             Flags::enableDesktopWindowingTaskbarRunningApps, true),
     ENABLE_DESKTOP_WINDOWING_TRANSITIONS(Flags::enableDesktopWindowingTransitions, false),
-    ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS(Flags::enableDesktopWindowingExitTransitions, false);
+    ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS(Flags::enableDesktopWindowingExitTransitions, false),
+    ENABLE_WINDOWING_TRANSITION_HANDLERS_OBSERVERS(
+            Flags::enableWindowingTransitionHandlersObservers, false);
 
     private static final String TAG = "DesktopModeFlagsUtil";
     // Function called to obtain aconfig flag value.
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index 31bb3a6..155494f 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -228,7 +228,7 @@
     name: "enable_desktop_windowing_app_handle_education"
     namespace: "lse_desktop_experience"
     description: "Enables desktop windowing app handle education"
-    bug: "348208342"
+    bug: "316006079"
 }
 
 flag {
diff --git a/core/java/com/android/internal/util/MemInfoReader.java b/core/java/com/android/internal/util/MemInfoReader.java
index 0c5c853..d34bca6 100644
--- a/core/java/com/android/internal/util/MemInfoReader.java
+++ b/core/java/com/android/internal/util/MemInfoReader.java
@@ -88,6 +88,13 @@
     }
 
     /**
+     * Amount of RAM that used by shared memory (shmem) and tmpfs
+     */
+    public long getShmemSizeKb() {
+        return mInfos[Debug.MEMINFO_SHMEM];
+    }
+
+    /**
      * Amount of RAM that the kernel is being used for caches, not counting caches
      * that are mapped in to processes.
      */
diff --git a/core/proto/android/providers/settings/system.proto b/core/proto/android/providers/settings/system.proto
index e795e809..9779dc0e 100644
--- a/core/proto/android/providers/settings/system.proto
+++ b/core/proto/android/providers/settings/system.proto
@@ -220,6 +220,15 @@
     }
     optional Touchpad touchpad = 36;
 
+    message Mouse {
+        option (android.msg_privacy).dest = DEST_EXPLICIT;
+
+        optional SettingProto reverse_vertical_scrolling = 1 [ (android.privacy).dest = DEST_AUTOMATIC ];
+        optional SettingProto swap_primary_button = 2 [ (android.privacy).dest = DEST_AUTOMATIC ];
+    }
+
+    optional Mouse mouse = 38;
+
     optional SettingProto tty_mode = 31 [ (android.privacy).dest = DEST_AUTOMATIC ];
 
     message Vibrate {
@@ -277,5 +286,5 @@
 
     // Please insert fields in alphabetical order and group them into messages
     // if possible (to avoid reaching the method limit).
-    // Next tag = 38;
+    // Next tag = 39;
 }
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 549f8df..5693d66 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -155,6 +155,7 @@
 
     <protected-broadcast android:name="android.bluetooth.intent.DISCOVERABLE_TIMEOUT" />
     <protected-broadcast android:name="android.bluetooth.action.AUTO_ON_STATE_CHANGED" />
+    <protected-broadcast android:name="android.bluetooth.action.CONNECTION_STATE_CHANGED" />
     <protected-broadcast android:name="android.bluetooth.adapter.action.STATE_CHANGED" />
     <protected-broadcast android:name="android.bluetooth.adapter.action.SCAN_MODE_CHANGED" />
     <protected-broadcast android:name="android.bluetooth.adapter.action.DISCOVERY_STARTED" />
@@ -240,6 +241,8 @@
     <protected-broadcast
         android:name="android.bluetooth.avrcp-controller.profile.action.FOLDER_LIST" />
     <protected-broadcast
+        android:name="android.bluetooth.avrcp-controller.profile.action.PLAYER_SETTING" />
+    <protected-broadcast
         android:name="android.bluetooth.avrcp-controller.profile.action.TRACK_EVENT" />
     <protected-broadcast
         android:name="android.bluetooth.input.profile.action.CONNECTION_STATE_CHANGED" />
@@ -266,6 +269,7 @@
     <protected-broadcast
         android:name="android.bluetooth.pan.profile.action.CONNECTION_STATE_CHANGED" />
     <protected-broadcast android:name="android.bluetooth.action.HAP_CONNECTION_STATE_CHANGED" />
+    <protected-broadcast android:name="android.bluetooth.action.HAP_DEVICE_AVAILABLE" />
     <protected-broadcast android:name="android.bluetooth.action.LE_AUDIO_CONNECTION_STATE_CHANGED" />
     <protected-broadcast android:name="android.bluetooth.action.LE_AUDIO_ACTIVE_DEVICE_CHANGED" />
     <protected-broadcast android:name="android.bluetooth.action.LE_AUDIO_CONF_CHANGED" />
diff --git a/core/tests/coretests/src/android/app/ActivityManagerTest.java b/core/tests/coretests/src/android/app/ActivityManagerTest.java
index d850f86..85ff846 100644
--- a/core/tests/coretests/src/android/app/ActivityManagerTest.java
+++ b/core/tests/coretests/src/android/app/ActivityManagerTest.java
@@ -60,7 +60,6 @@
     public void testProcState() throws Exception {
         // For the moment mostly want to confirm we don't crash
         assertNotNull(ActivityManager.procStateToString(PROCESS_STATE_SERVICE));
-        assertNotNull(ActivityManager.processStateAmToProto(PROCESS_STATE_SERVICE));
         assertTrue(ActivityManager.isProcStateBackground(PROCESS_STATE_SERVICE));
         assertFalse(ActivityManager.isProcStateCached(PROCESS_STATE_SERVICE));
         assertFalse(ActivityManager.isForegroundService(PROCESS_STATE_SERVICE));
diff --git a/core/tests/coretests/src/android/app/activity/ActivityManagerTest.java b/core/tests/coretests/src/android/app/activity/ActivityManagerTest.java
index b972882..cd52421 100644
--- a/core/tests/coretests/src/android/app/activity/ActivityManagerTest.java
+++ b/core/tests/coretests/src/android/app/activity/ActivityManagerTest.java
@@ -111,12 +111,6 @@
         assertEquals(config.reqKeyboardType, vconfig.keyboard);
         assertEquals(config.reqTouchScreen, vconfig.touchscreen);
         assertEquals(config.reqNavigation, vconfig.navigation);
-        if (vconfig.navigation == Configuration.NAVIGATION_NONAV) {
-            assertNotNull(config.reqInputFeatures & ConfigurationInfo.INPUT_FEATURE_FIVE_WAY_NAV);
-        }
-        if (vconfig.keyboard != Configuration.KEYBOARD_UNDEFINED) {
-            assertNotNull(config.reqInputFeatures & ConfigurationInfo.INPUT_FEATURE_HARD_KEYBOARD);
-        }    
     }
 
     @SmallTest
diff --git a/core/tests/devicestatetests/Android.bp b/core/tests/devicestatetests/Android.bp
index a2aa62d..e573a51 100644
--- a/core/tests/devicestatetests/Android.bp
+++ b/core/tests/devicestatetests/Android.bp
@@ -28,8 +28,10 @@
     static_libs: [
         "androidx.test.ext.junit",
         "androidx.test.rules",
+        "flag-junit",
         "frameworks-base-testutils",
         "mockito-target-minus-junit4",
+        "platform-parametric-runner-lib",
         "platform-test-annotations",
         "truth",
     ],
diff --git a/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateManagerGlobalTest.java b/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateManagerGlobalTest.java
index 7c01ecc..e640ce5 100644
--- a/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateManagerGlobalTest.java
+++ b/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateManagerGlobalTest.java
@@ -18,8 +18,10 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyZeroInteractions;
@@ -29,15 +31,20 @@
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.test.FakePermissionEnforcer;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.FlagsParameterization;
+import android.platform.test.flag.junit.SetFlagsRule;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.util.ConcurrentUtils;
+import com.android.window.flags.Flags;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -46,6 +53,9 @@
 import java.util.List;
 import java.util.Set;
 
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
 /**
  * Unit tests for {@link DeviceStateManagerGlobal}.
  *
@@ -53,18 +63,30 @@
  * atest FrameworksCoreDeviceStateManagerTests:DeviceStateManagerGlobalTest
  */
 @SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(ParameterizedAndroidJunit4.class)
 public final class DeviceStateManagerGlobalTest {
     private static final DeviceState DEFAULT_DEVICE_STATE = new DeviceState(
             new DeviceState.Configuration.Builder(0 /* identifier */, "" /* name */).build());
     private static final DeviceState OTHER_DEVICE_STATE = new DeviceState(
             new DeviceState.Configuration.Builder(1 /* identifier */, "" /* name */).build());
 
+    @Rule
+    public final SetFlagsRule mSetFlagsRule;
+
+    @Parameters(name = "{0}")
+    public static List<FlagsParameterization> getParams() {
+        return FlagsParameterization.allCombinationsOf(Flags.FLAG_WLINFO_ONCREATE);
+    }
+
     @NonNull
     private TestDeviceStateManagerService mService;
     @NonNull
     private DeviceStateManagerGlobal mDeviceStateManagerGlobal;
 
+    public DeviceStateManagerGlobalTest(FlagsParameterization flags) {
+        mSetFlagsRule = new SetFlagsRule(flags);
+    }
+
     @Before
     public void setUp() {
         final FakePermissionEnforcer permissionEnforcer = new FakePermissionEnforcer();
@@ -74,6 +96,36 @@
     }
 
     @Test
+    @DisableFlags(Flags.FLAG_WLINFO_ONCREATE)
+    public void create_whenWlinfoOncreateIsDisabled_receivesDeviceStateInfoFromCallback() {
+        final FakePermissionEnforcer permissionEnforcer = new FakePermissionEnforcer();
+        final TestDeviceStateManagerService service = new TestDeviceStateManagerService(
+                permissionEnforcer, true /* simulatePostCallback */);
+        final DeviceStateManagerGlobal dsmGlobal = new DeviceStateManagerGlobal(service);
+        final DeviceStateCallback callback = mock(DeviceStateCallback.class);
+        dsmGlobal.registerDeviceStateCallback(callback, ConcurrentUtils.DIRECT_EXECUTOR);
+
+        verify(callback, never()).onDeviceStateChanged(any());
+
+        // Simulate DeviceStateManagerService#registerProcess by notifying clients of current device
+        // state via callback.
+        service.notifyDeviceStateInfoChanged();
+        verify(callback).onDeviceStateChanged(eq(DEFAULT_DEVICE_STATE));
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_WLINFO_ONCREATE)
+    public void create_whenWlinfoOncreateIsEnabled_returnsDeviceStateInfoFromRegistration() {
+        final FakePermissionEnforcer permissionEnforcer = new FakePermissionEnforcer();
+        final IDeviceStateManager service = new TestDeviceStateManagerService(permissionEnforcer);
+        final DeviceStateManagerGlobal dsmGlobal = new DeviceStateManagerGlobal(service);
+        final DeviceStateCallback callback = mock(DeviceStateCallback.class);
+        dsmGlobal.registerDeviceStateCallback(callback, ConcurrentUtils.DIRECT_EXECUTOR);
+
+        verify(callback).onDeviceStateChanged(eq(DEFAULT_DEVICE_STATE));
+    }
+
+    @Test
     public void registerCallback() {
         final DeviceStateCallback callback1 = mock(DeviceStateCallback.class);
         final DeviceStateCallback callback2 = mock(DeviceStateCallback.class);
@@ -267,10 +319,17 @@
         @Nullable
         private Request mBaseStateRequest;
 
+        private final boolean mSimulatePostCallback;
         private final Set<IDeviceStateManagerCallback> mCallbacks = new HashSet<>();
 
         TestDeviceStateManagerService(@NonNull FakePermissionEnforcer enforcer) {
+            this(enforcer, false /* simulatePostCallback */);
+        }
+
+        TestDeviceStateManagerService(@NonNull FakePermissionEnforcer enforcer,
+                boolean simulatePostCallback) {
             super(enforcer);
+            mSimulatePostCallback = simulatePostCallback;
         }
 
         @NonNull
@@ -304,18 +363,26 @@
             return getInfo();
         }
 
+        @Nullable
         @Override
-        public void registerCallback(IDeviceStateManagerCallback callback) {
+        public DeviceStateInfo registerCallback(IDeviceStateManagerCallback callback) {
             if (mCallbacks.contains(callback)) {
                 throw new SecurityException("Callback is already registered.");
             }
 
             mCallbacks.add(callback);
-            try {
-                callback.onDeviceStateInfoChanged(getInfo());
-            } catch (RemoteException e) {
-                e.rethrowFromSystemServer();
+            if (Flags.wlinfoOncreate()) {
+                return getInfo();
             }
+
+            if (!mSimulatePostCallback) {
+                try {
+                    callback.onDeviceStateInfoChanged(getInfo());
+                } catch (RemoteException e) {
+                    e.rethrowFromSystemServer();
+                }
+            }
+            return null;
         }
 
         @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
index 5a277316f..379e052 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
@@ -156,6 +156,21 @@
         )
     }
 
+    fun logTaskInfoStateInit() {
+        logTaskUpdate(
+            FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INIT_STATSD,
+            /* session_id */ 0,
+            TaskUpdate(
+                visibleTaskCount = 0,
+                instanceId = 0,
+                uid = 0,
+                taskHeight = 0,
+                taskWidth = 0,
+                taskX = 0,
+                taskY = 0)
+        )
+    }
+
     private fun logTaskUpdate(taskEvent: Int, sessionId: Int, taskUpdate: TaskUpdate) {
         FrameworkStatsLog.write(
             DESKTOP_MODE_TASK_UPDATE_ATOM_ID,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
index b8507e3..f847aa89 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
@@ -102,6 +102,7 @@
         SystemProperties.set(
             VISIBLE_TASKS_COUNTER_SYSTEM_PROPERTY,
             VISIBLE_TASKS_COUNTER_SYSTEM_PROPERTY_DEFAULT_VALUE)
+        desktopModeEventLogger.logTaskInfoStateInit()
     }
 
     override fun onTransitionReady(
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt
index d7a132d..dde9fda 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt
@@ -16,7 +16,6 @@
 
 package com.android.wm.shell.desktopmode
 
-import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.SetFlagsRule
 import com.android.dx.mockito.inline.extended.ExtendedMockito.verify
@@ -397,6 +396,37 @@
         }
     }
 
+    @Test
+    fun logTaskInfoStateInit_logsTaskInfoChangedStateInit() {
+        desktopModeEventLogger.logTaskInfoStateInit()
+        verify {
+            FrameworkStatsLog.write(eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE),
+                /* task_event */
+                eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INIT_STATSD),
+                /* instance_id */
+                eq(0),
+                /* uid */
+                eq(0),
+                /* task_height */
+                eq(0),
+                /* task_width */
+                eq(0),
+                /* task_x */
+                eq(0),
+                /* task_y */
+                eq(0),
+                /* session_id */
+                eq(0),
+                /* minimize_reason */
+                eq(UNSET_MINIMIZE_REASON),
+                /* unminimize_reason */
+                eq(UNSET_UNMINIMIZE_REASON),
+                /* visible_task_count */
+                eq(0)
+            )
+        }
+    }
+
     private companion object {
         private const val SESSION_ID = 1
         private const val TASK_ID = 1
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
index daf7e7d..e7593b5 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
@@ -115,6 +115,9 @@
     val initRunnableCaptor = ArgumentCaptor.forClass(Runnable::class.java)
     verify(mockShellInit).addInitCallback(initRunnableCaptor.capture(), same(transitionObserver))
     initRunnableCaptor.value.run()
+    // verify this initialisation interaction to leave the desktopmodeEventLogger mock in a
+    // consistent state with no outstanding interactions when test cases start executing.
+    verify(desktopModeEventLogger).logTaskInfoStateInit()
   }
 
   @Test
diff --git a/nfc/api/system-current.txt b/nfc/api/system-current.txt
index 4428ade..24e14e6 100644
--- a/nfc/api/system-current.txt
+++ b/nfc/api/system-current.txt
@@ -63,7 +63,7 @@
     method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean isAutoChangeEnabled();
     method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean isTagPresent();
     method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void maybeTriggerFirmwareUpdate();
-    method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void overwriteRoutingTable(int, int, int);
+    method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void overwriteRoutingTable(int, int, int, int);
     method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void pausePolling(int);
     method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void registerCallback(@NonNull java.util.concurrent.Executor, @NonNull android.nfc.NfcOemExtension.Callback);
     method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void resumePolling();
diff --git a/nfc/java/android/nfc/INfcCardEmulation.aidl b/nfc/java/android/nfc/INfcCardEmulation.aidl
index 1eae3c6..8535e4a 100644
--- a/nfc/java/android/nfc/INfcCardEmulation.aidl
+++ b/nfc/java/android/nfc/INfcCardEmulation.aidl
@@ -54,5 +54,5 @@
     void setAutoChangeStatus(boolean state);
     boolean isAutoChangeEnabled();
     List<String> getRoutingStatus();
-    void overwriteRoutingTable(int userHandle, String emptyAid, String protocol, String tech);
+    void overwriteRoutingTable(int userHandle, String emptyAid, String protocol, String tech, String sc);
 }
diff --git a/nfc/java/android/nfc/NfcOemExtension.java b/nfc/java/android/nfc/NfcOemExtension.java
index fb63b5c..bc410c7 100644
--- a/nfc/java/android/nfc/NfcOemExtension.java
+++ b/nfc/java/android/nfc/NfcOemExtension.java
@@ -647,24 +647,29 @@
      *                   {@link ProtocolAndTechnologyRoute}
      * @param emptyAid Zero-length AID route destination, where the possible inputs are defined in
      *                 {@link ProtocolAndTechnologyRoute}
+     * @param systemCode System Code route destination, where the possible inputs are defined in
+     *                   {@link ProtocolAndTechnologyRoute}
      */
     @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
     @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
     public void overwriteRoutingTable(
             @CardEmulation.ProtocolAndTechnologyRoute int protocol,
             @CardEmulation.ProtocolAndTechnologyRoute int technology,
-            @CardEmulation.ProtocolAndTechnologyRoute int emptyAid) {
+            @CardEmulation.ProtocolAndTechnologyRoute int emptyAid,
+            @CardEmulation.ProtocolAndTechnologyRoute int systemCode) {
 
         String protocolRoute = routeIntToString(protocol);
         String technologyRoute = routeIntToString(technology);
         String emptyAidRoute = routeIntToString(emptyAid);
+        String systemCodeRoute = routeIntToString(systemCode);
 
         NfcAdapter.callService(() ->
                 NfcAdapter.sCardEmulationService.overwriteRoutingTable(
                         mContext.getUser().getIdentifier(),
                         emptyAidRoute,
                         protocolRoute,
-                        technologyRoute
+                        technologyRoute,
+                        systemCodeRoute
                 ));
     }
 
diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyValueStore.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyValueStore.kt
index 3d41337..5f1f8df 100644
--- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyValueStore.kt
+++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyValueStore.kt
@@ -25,6 +25,7 @@
     fun contains(key: String): Boolean
 
     /** Gets default value of given key. */
+    @Suppress("UNCHECKED_CAST")
     fun <T : Any> getDefaultValue(key: String, valueType: Class<T>): T? =
         when (valueType) {
             Boolean::class.javaObjectType -> false
@@ -56,6 +57,7 @@
 
     override fun contains(key: String): Boolean = sharedPreferences.contains(key)
 
+    @Suppress("IMPLICIT_CAST_TO_ANY", "UNCHECKED_CAST")
     override fun <T : Any> getValue(key: String, valueType: Class<T>): T? =
         when (valueType) {
             Boolean::class.javaObjectType -> sharedPreferences.getBoolean(key, false)
@@ -68,6 +70,7 @@
         }
             as T?
 
+    @Suppress("UNCHECKED_CAST")
     override fun <T : Any> setValue(key: String, valueType: Class<T>, value: T?) {
         if (value == null) {
             sharedPreferences.edit().remove(key).apply()
diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsGlobalStore.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsGlobalStore.kt
index 4aef0fc..fb93559 100644
--- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsGlobalStore.kt
+++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsGlobalStore.kt
@@ -34,6 +34,7 @@
 
     override fun contains(key: String): Boolean = Global.getString(contentResolver, key) != null
 
+    @Suppress("UNCHECKED_CAST")
     override fun <T : Any> getValue(key: String, valueType: Class<T>): T? =
         try {
             when (valueType) {
diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsSecureStore.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsSecureStore.kt
index 9f41ecb..bc37571 100644
--- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsSecureStore.kt
+++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsSecureStore.kt
@@ -34,6 +34,7 @@
 
     override fun contains(key: String): Boolean = Secure.getString(contentResolver, key) != null
 
+    @Suppress("UNCHECKED_CAST")
     override fun <T : Any> getValue(key: String, valueType: Class<T>): T? =
         try {
             when (valueType) {
diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsStore.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsStore.kt
index 5981688..fdefa39 100644
--- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsStore.kt
+++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsStore.kt
@@ -27,7 +27,7 @@
 import java.util.concurrent.atomic.AtomicInteger
 
 /** Base class of the Settings provider data stores. */
-open abstract class SettingsStore(protected val contentResolver: ContentResolver) :
+abstract class SettingsStore(protected val contentResolver: ContentResolver) :
     KeyedDataObservable<String>(), KeyValueStore {
 
     /**
diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsSystemStore.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsSystemStore.kt
index 6cca7ed..1c75c7c 100644
--- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsSystemStore.kt
+++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsSystemStore.kt
@@ -34,6 +34,7 @@
 
     override fun contains(key: String): Boolean = System.getString(contentResolver, key) != null
 
+    @Suppress("UNCHECKED_CAST")
     override fun <T : Any> getValue(key: String, valueType: Class<T>): T? =
         try {
             when (valueType) {
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceDataStoreAdapter.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceDataStoreAdapter.kt
index 02acfca..c2728b4 100644
--- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceDataStoreAdapter.kt
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceDataStoreAdapter.kt
@@ -37,6 +37,7 @@
     override fun getString(key: String, defValue: String?): String? =
         keyValueStore.getValue(key, String::class.javaObjectType) ?: defValue
 
+    @Suppress("UNCHECKED_CAST")
     override fun getStringSet(key: String, defValues: Set<String>?): Set<String>? =
         (keyValueStore.getValue(key, Set::class.javaObjectType) as Set<String>?) ?: defValues
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistant.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistant.java
index c9f9d1b..a4c5a00d 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistant.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistant.java
@@ -42,6 +42,7 @@
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
 
 /**
  * LocalBluetoothLeBroadcastAssistant provides an interface between the Settings app and the
@@ -63,6 +64,7 @@
     private BluetoothLeBroadcastMetadata mBluetoothLeBroadcastMetadata;
     private BluetoothLeBroadcastMetadata.Builder mBuilder;
     private boolean mIsProfileReady;
+    private Executor mExecutor;
     // Cached assistant callbacks being register before service is connected.
     private final Map<BluetoothLeBroadcastAssistant.Callback, Executor> mCachedCallbackExecutorMap =
             new ConcurrentHashMap<>();
@@ -98,15 +100,19 @@
                     }
 
                     mProfileManager.callServiceConnectedListeners();
-                    mIsProfileReady = true;
-                    if (DEBUG) {
-                        Log.d(
-                                TAG,
-                                "onServiceConnected, register mCachedCallbackExecutorMap = "
-                                        + mCachedCallbackExecutorMap);
+                    if (!mIsProfileReady) {
+                        mIsProfileReady = true;
+                        registerServiceCallBack(mExecutor, mAssistantCallback);
+                        if (DEBUG) {
+                            Log.d(
+                                    TAG,
+                                    "onServiceConnected, register mCachedCallbackExecutorMap = "
+                                            + mCachedCallbackExecutorMap);
+                        }
+                        mCachedCallbackExecutorMap.forEach(
+                                (callback, executor) -> registerServiceCallBack(executor,
+                                        callback));
                     }
-                    mCachedCallbackExecutorMap.forEach(
-                            (callback, executor) -> registerServiceCallBack(executor, callback));
                 }
 
                 @Override
@@ -119,17 +125,71 @@
                         Log.d(TAG, "Bluetooth service disconnected");
                     }
                     mProfileManager.callServiceDisconnectedListeners();
-                    mIsProfileReady = false;
-                    mCachedCallbackExecutorMap.clear();
+                    if (mIsProfileReady) {
+                        mIsProfileReady = false;
+                        unregisterServiceCallBack(mAssistantCallback);
+                        mCachedCallbackExecutorMap.clear();
+                    }
                 }
             };
 
+    private final BluetoothLeBroadcastAssistant.Callback mAssistantCallback =
+            new BluetoothLeBroadcastAssistant.Callback() {
+                @Override
+                public void onSourceAdded(@NonNull BluetoothDevice sink, int sourceId, int reason) {
+                }
+
+                @Override
+                public void onSearchStarted(int reason) {}
+
+                @Override
+                public void onSearchStartFailed(int reason) {}
+
+                @Override
+                public void onSearchStopped(int reason) {}
+
+                @Override
+                public void onSearchStopFailed(int reason) {}
+
+                @Override
+                public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {}
+
+                @Override
+                public void onSourceAddFailed(
+                        @NonNull BluetoothDevice sink,
+                        @NonNull BluetoothLeBroadcastMetadata source,
+                        int reason) {}
+
+                @Override
+                public void onSourceModified(
+                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
+
+                @Override
+                public void onSourceModifyFailed(
+                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
+
+                @Override
+                public void onSourceRemoved(
+                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
+
+                @Override
+                public void onSourceRemoveFailed(
+                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
+
+                @Override
+                public void onReceiveStateChanged(
+                        @NonNull BluetoothDevice sink,
+                        int sourceId,
+                        @NonNull BluetoothLeBroadcastReceiveState state) {}
+            };
+
     public LocalBluetoothLeBroadcastAssistant(
             Context context,
             CachedBluetoothDeviceManager deviceManager,
             LocalBluetoothProfileManager profileManager) {
         mProfileManager = profileManager;
         mDeviceManager = deviceManager;
+        mExecutor = Executors.newSingleThreadExecutor();
         BluetoothAdapter.getDefaultAdapter()
                 .getProfileProxy(
                         context, mServiceListener, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastCallbackExt.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastCallbackExt.kt
index 0bcf7fe..07abb6b 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastCallbackExt.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastCallbackExt.kt
@@ -68,3 +68,44 @@
                 awaitClose { unregisterServiceCallBack(listener) }
             }
             .buffer(capacity = Channel.CONFLATED)
+
+/** [Flow] for [BluetoothLeBroadcast.Callback] onPlaybackStarted event */
+val LocalBluetoothLeBroadcast.onPlaybackStarted: Flow<Unit>
+    get() =
+        callbackFlow {
+            val listener =
+                object : BluetoothLeBroadcast.Callback {
+                    override fun onBroadcastStarted(reason: Int, broadcastId: Int) {}
+
+                    override fun onBroadcastStartFailed(reason: Int) {
+                    }
+
+                    override fun onBroadcastStopped(reason: Int, broadcastId: Int) {
+                    }
+
+                    override fun onBroadcastStopFailed(reason: Int) {
+                    }
+
+                    override fun onPlaybackStarted(reason: Int, broadcastId: Int) {
+                        launch { trySend(Unit) }
+                    }
+
+                    override fun onPlaybackStopped(reason: Int, broadcastId: Int) {
+                    }
+
+                    override fun onBroadcastUpdated(reason: Int, broadcastId: Int) {}
+
+                    override fun onBroadcastUpdateFailed(reason: Int, broadcastId: Int) {}
+
+                    override fun onBroadcastMetadataChanged(
+                        broadcastId: Int,
+                        metadata: BluetoothLeBroadcastMetadata
+                    ) {}
+                }
+            registerServiceCallBack(
+                ConcurrentUtils.DIRECT_EXECUTOR,
+                listener,
+            )
+            awaitClose { unregisterServiceCallBack(listener) }
+        }
+            .buffer(capacity = Channel.CONFLATED)
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java
index 727662b..4f315a2 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java
@@ -22,6 +22,7 @@
 import android.media.AudioDeviceAttributes;
 import android.media.AudioDeviceCallback;
 import android.media.AudioDeviceInfo;
+import android.media.AudioDeviceInfo.AudioDeviceType;
 import android.media.AudioManager;
 import android.media.MediaRecorder;
 import android.os.Handler;
@@ -63,7 +64,7 @@
 
     @VisibleForTesting final List<MediaDevice> mInputMediaDevices = new CopyOnWriteArrayList<>();
 
-    private MediaDevice mSelectedInputDevice;
+    private @AudioDeviceType int mSelectedInputDeviceType;
 
     private final Collection<InputDeviceCallback> mCallbacks = new CopyOnWriteArrayList<>();
     private final Object mCallbackLock = new Object();
@@ -73,12 +74,12 @@
             new AudioDeviceCallback() {
                 @Override
                 public void onAudioDevicesAdded(@NonNull AudioDeviceInfo[] addedDevices) {
-                    dispatchInputDeviceListUpdate();
+                    applyDefaultSelectedTypeToAllPresets();
                 }
 
                 @Override
                 public void onAudioDevicesRemoved(@NonNull AudioDeviceInfo[] removedDevices) {
-                    dispatchInputDeviceListUpdate();
+                    applyDefaultSelectedTypeToAllPresets();
                 }
             };
 
@@ -92,9 +93,12 @@
         mAudioManager.addOnPreferredDevicesForCapturePresetChangedListener(
                 new HandlerExecutor(handler),
                 this::onPreferredDevicesForCapturePresetChangedListener);
+
+        applyDefaultSelectedTypeToAllPresets();
     }
 
-    private void onPreferredDevicesForCapturePresetChangedListener(
+    @VisibleForTesting
+    void onPreferredDevicesForCapturePresetChangedListener(
             @MediaRecorder.SystemSource int capturePreset,
             @NonNull List<AudioDeviceAttributes> devices) {
         if (capturePreset == MediaRecorder.AudioSource.MIC) {
@@ -117,12 +121,30 @@
         }
     }
 
+    // TODO(b/355684672): handle edge case where there are two devices with the same type. Only
+    // using a single mSelectedInputDeviceType might not be enough to recognize the correct device.
     public @Nullable MediaDevice getSelectedInputDevice() {
-        return mSelectedInputDevice;
+        for (MediaDevice device : mInputMediaDevices) {
+            if (((InputMediaDevice) device).getAudioDeviceInfoType() == mSelectedInputDeviceType) {
+                return device;
+            }
+        }
+        return null;
     }
 
-    private void dispatchInputDeviceListUpdate() {
-        // Get selected input device.
+    private void applyDefaultSelectedTypeToAllPresets() {
+        mSelectedInputDeviceType = retrieveDefaultSelectedDeviceType();
+        AudioDeviceAttributes deviceAttributes =
+                createInputDeviceAttributes(mSelectedInputDeviceType);
+        setPreferredDeviceForAllPresets(deviceAttributes);
+    }
+
+    private AudioDeviceAttributes createInputDeviceAttributes(@AudioDeviceType int type) {
+        // Address is not used.
+        return new AudioDeviceAttributes(AudioDeviceAttributes.ROLE_INPUT, type, /* address= */ "");
+    }
+
+    private @AudioDeviceType int retrieveDefaultSelectedDeviceType() {
         List<AudioDeviceAttributes> attributesOfSelectedInputDevices =
                 mAudioManager.getDevicesForAttributes(INPUT_ATTRIBUTES);
         int selectedInputDeviceAttributesType;
@@ -138,7 +160,10 @@
             }
             selectedInputDeviceAttributesType = attributesOfSelectedInputDevices.get(0).getType();
         }
+        return selectedInputDeviceAttributesType;
+    }
 
+    private void dispatchInputDeviceListUpdate() {
         // Get all input devices.
         AudioDeviceInfo[] audioDeviceInfos =
                 mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS);
@@ -154,9 +179,8 @@
                             isInputGainFixed(),
                             getProductNameFromAudioDeviceInfo(info));
             if (mediaDevice != null) {
-                if (info.getType() == selectedInputDeviceAttributesType) {
+                if (info.getType() == mSelectedInputDeviceType) {
                     mediaDevice.setState(STATE_SELECTED);
-                    mSelectedInputDevice = mediaDevice;
                 }
                 mInputMediaDevices.add(mediaDevice);
             }
@@ -190,12 +214,12 @@
     }
 
     public void selectDevice(@NonNull MediaDevice device) {
-        if (!(device instanceof InputMediaDevice)) {
+        if (!(device instanceof InputMediaDevice inputMediaDevice)) {
             Slog.w(TAG, "This device is not an InputMediaDevice: " + device.getName());
             return;
         }
 
-        if (device.equals(mSelectedInputDevice)) {
+        if (inputMediaDevice.getAudioDeviceInfoType() == mSelectedInputDeviceType) {
             Slog.w(TAG, "This device is already selected: " + device.getName());
             return;
         }
@@ -206,12 +230,11 @@
             return;
         }
 
-        // TODO(b/355684672): apply address for BT devices.
+        // Update mSelectedInputDeviceType directly based on user action.
+        mSelectedInputDeviceType = inputMediaDevice.getAudioDeviceInfoType();
+
         AudioDeviceAttributes deviceAttributes =
-                new AudioDeviceAttributes(
-                        AudioDeviceAttributes.ROLE_INPUT,
-                        ((InputMediaDevice) device).getAudioDeviceInfoType(),
-                        /* address= */ "");
+                createInputDeviceAttributes(inputMediaDevice.getAudioDeviceInfoType());
         try {
             setPreferredDeviceForAllPresets(deviceAttributes);
         } catch (IllegalArgumentException e) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt
index 2f8105a..b41e970 100644
--- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt
@@ -74,6 +74,8 @@
     /** The headset groupId to volume map during audio sharing. */
     val volumeMap: StateFlow<GroupIdToVolumes>
 
+    suspend fun audioSharingAvailable(): Boolean
+
     /** Set the volume of secondary headset during audio sharing. */
     suspend fun setSecondaryVolume(
         @IntRange(from = AUDIO_SHARING_VOLUME_MIN.toLong(), to = AUDIO_SHARING_VOLUME_MAX.toLong())
@@ -216,6 +218,12 @@
         }
             .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyMap())
 
+    override suspend fun audioSharingAvailable(): Boolean {
+        return withContext(backgroundCoroutineContext) {
+            BluetoothUtils.isAudioSharingEnabled()
+        }
+    }
+
     override suspend fun setSecondaryVolume(
         @IntRange(from = AUDIO_SHARING_VOLUME_MIN.toLong(), to = AUDIO_SHARING_VOLUME_MAX.toLong())
         volume: Int
@@ -262,6 +270,8 @@
         MutableStateFlow(BluetoothCsipSetCoordinator.GROUP_ID_INVALID)
     override val volumeMap: StateFlow<GroupIdToVolumes> = MutableStateFlow(emptyMap())
 
+    override suspend fun audioSharingAvailable(): Boolean = false
+
     override suspend fun setSecondaryVolume(
         @IntRange(from = AUDIO_SHARING_VOLUME_MIN.toLong(), to = AUDIO_SHARING_VOLUME_MAX.toLong())
         volume: Int
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java
index f63bfc7..782cee2 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java
@@ -21,6 +21,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
@@ -55,6 +56,7 @@
     private static final int INPUT_USB_DEVICE_ID = 3;
     private static final int INPUT_USB_HEADSET_ID = 4;
     private static final int INPUT_USB_ACCESSORY_ID = 5;
+    private static final int HDMI_ID = 6;
     private static final int MAX_VOLUME = 1;
     private static final int CURRENT_VOLUME = 0;
     private static final boolean VOLUME_FIXED_TRUE = true;
@@ -63,10 +65,86 @@
     private static final String PRODUCT_NAME_USB_HEADSET = "My USB Headset";
     private static final String PRODUCT_NAME_USB_DEVICE = "My USB Device";
     private static final String PRODUCT_NAME_USB_ACCESSORY = "My USB Accessory";
+    private static final String PRODUCT_NAME_HDMI_DEVICE = "HDMI device";
 
     private final Context mContext = spy(RuntimeEnvironment.application);
     private InputRouteManager mInputRouteManager;
 
+    private AudioDeviceInfo mockBuiltinMicInfo() {
+        final AudioDeviceInfo info = mock(AudioDeviceInfo.class);
+        when(info.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_MIC);
+        when(info.getId()).thenReturn(BUILTIN_MIC_ID);
+        when(info.getAddress()).thenReturn("");
+        when(info.getProductName()).thenReturn(PRODUCT_NAME_BUILTIN_MIC);
+        return info;
+    }
+
+    private AudioDeviceInfo mockWiredHeadsetInfo() {
+        final AudioDeviceInfo info = mock(AudioDeviceInfo.class);
+        when(info.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
+        when(info.getId()).thenReturn(INPUT_WIRED_HEADSET_ID);
+        when(info.getAddress()).thenReturn("");
+        when(info.getProductName()).thenReturn(PRODUCT_NAME_WIRED_HEADSET);
+        return info;
+    }
+
+    private AudioDeviceInfo mockUsbDeviceInfo() {
+        final AudioDeviceInfo info = mock(AudioDeviceInfo.class);
+        when(info.getType()).thenReturn(AudioDeviceInfo.TYPE_USB_DEVICE);
+        when(info.getId()).thenReturn(INPUT_USB_DEVICE_ID);
+        when(info.getAddress()).thenReturn("");
+        when(info.getProductName()).thenReturn(PRODUCT_NAME_USB_DEVICE);
+        return info;
+    }
+
+    private AudioDeviceInfo mockUsbHeadsetInfo() {
+        final AudioDeviceInfo info = mock(AudioDeviceInfo.class);
+        when(info.getType()).thenReturn(AudioDeviceInfo.TYPE_USB_HEADSET);
+        when(info.getId()).thenReturn(INPUT_USB_HEADSET_ID);
+        when(info.getAddress()).thenReturn("");
+        when(info.getProductName()).thenReturn(PRODUCT_NAME_USB_HEADSET);
+        return info;
+    }
+
+    private AudioDeviceInfo mockUsbAccessoryInfo() {
+        final AudioDeviceInfo info = mock(AudioDeviceInfo.class);
+        when(info.getType()).thenReturn(AudioDeviceInfo.TYPE_USB_ACCESSORY);
+        when(info.getId()).thenReturn(INPUT_USB_ACCESSORY_ID);
+        when(info.getAddress()).thenReturn("");
+        when(info.getProductName()).thenReturn(PRODUCT_NAME_USB_ACCESSORY);
+        return info;
+    }
+
+    private AudioDeviceInfo mockHdmiInfo() {
+        final AudioDeviceInfo info = mock(AudioDeviceInfo.class);
+        when(info.getType()).thenReturn(AudioDeviceInfo.TYPE_HDMI);
+        when(info.getId()).thenReturn(HDMI_ID);
+        when(info.getAddress()).thenReturn("");
+        when(info.getProductName()).thenReturn(PRODUCT_NAME_HDMI_DEVICE);
+        return info;
+    }
+
+    private AudioDeviceAttributes getBuiltinMicDeviceAttributes() {
+        return new AudioDeviceAttributes(
+                AudioDeviceAttributes.ROLE_INPUT,
+                AudioDeviceInfo.TYPE_BUILTIN_MIC,
+                /* address= */ "");
+    }
+
+    private AudioDeviceAttributes getWiredHeadsetDeviceAttributes() {
+        return new AudioDeviceAttributes(
+                AudioDeviceAttributes.ROLE_INPUT,
+                AudioDeviceInfo.TYPE_WIRED_HEADSET,
+                /* address= */ "");
+    }
+
+    private void onPreferredDevicesForCapturePresetChanged(InputRouteManager inputRouteManager) {
+        final List<AudioDeviceAttributes> audioDeviceAttributesList =
+                new ArrayList<AudioDeviceAttributes>();
+        inputRouteManager.onPreferredDevicesForCapturePresetChangedListener(
+                MediaRecorder.AudioSource.MIC, audioDeviceAttributesList);
+    }
+
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
@@ -77,37 +155,15 @@
 
     @Test
     public void onAudioDevicesAdded_shouldUpdateInputMediaDevice() {
-        final AudioDeviceInfo info1 = mock(AudioDeviceInfo.class);
-        when(info1.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_MIC);
-        when(info1.getId()).thenReturn(BUILTIN_MIC_ID);
-        when(info1.getProductName()).thenReturn(PRODUCT_NAME_BUILTIN_MIC);
-
-        final AudioDeviceInfo info2 = mock(AudioDeviceInfo.class);
-        when(info2.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
-        when(info2.getId()).thenReturn(INPUT_WIRED_HEADSET_ID);
-        when(info2.getProductName()).thenReturn(PRODUCT_NAME_WIRED_HEADSET);
-
-        final AudioDeviceInfo info3 = mock(AudioDeviceInfo.class);
-        when(info3.getType()).thenReturn(AudioDeviceInfo.TYPE_USB_DEVICE);
-        when(info3.getId()).thenReturn(INPUT_USB_DEVICE_ID);
-        when(info3.getProductName()).thenReturn(PRODUCT_NAME_USB_DEVICE);
-
-        final AudioDeviceInfo info4 = mock(AudioDeviceInfo.class);
-        when(info4.getType()).thenReturn(AudioDeviceInfo.TYPE_USB_HEADSET);
-        when(info4.getId()).thenReturn(INPUT_USB_HEADSET_ID);
-        when(info4.getProductName()).thenReturn(PRODUCT_NAME_USB_HEADSET);
-
-        final AudioDeviceInfo info5 = mock(AudioDeviceInfo.class);
-        when(info5.getType()).thenReturn(AudioDeviceInfo.TYPE_USB_ACCESSORY);
-        when(info5.getId()).thenReturn(INPUT_USB_ACCESSORY_ID);
-        when(info5.getProductName()).thenReturn(PRODUCT_NAME_USB_ACCESSORY);
-
-        final AudioDeviceInfo unsupportedInfo = mock(AudioDeviceInfo.class);
-        when(unsupportedInfo.getType()).thenReturn(AudioDeviceInfo.TYPE_HDMI);
-        when(unsupportedInfo.getProductName()).thenReturn("HDMI device");
-
         final AudioManager audioManager = mock(AudioManager.class);
-        AudioDeviceInfo[] devices = {info1, info2, info3, info4, info5, unsupportedInfo};
+        AudioDeviceInfo[] devices = {
+            mockBuiltinMicInfo(),
+            mockWiredHeadsetInfo(),
+            mockUsbDeviceInfo(),
+            mockUsbHeadsetInfo(),
+            mockUsbAccessoryInfo(),
+            mockHdmiInfo()
+        };
         when(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn(devices);
 
         InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
@@ -115,8 +171,9 @@
         assertThat(inputRouteManager.mInputMediaDevices).isEmpty();
 
         inputRouteManager.mAudioDeviceCallback.onAudioDevicesAdded(devices);
+        onPreferredDevicesForCapturePresetChanged(inputRouteManager);
 
-        // The unsupported info should be filtered out.
+        // The unsupported (hdmi) info should be filtered out.
         assertThat(inputRouteManager.mInputMediaDevices).hasSize(devices.length - 1);
         assertThat(inputRouteManager.mInputMediaDevices.get(0).getId())
                 .isEqualTo(String.valueOf(BUILTIN_MIC_ID));
@@ -141,36 +198,25 @@
         final MediaDevice device = mock(MediaDevice.class);
         inputRouteManager.mInputMediaDevices.add(device);
 
-        final AudioDeviceInfo info = mock(AudioDeviceInfo.class);
-        when(info.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
-        inputRouteManager.mAudioDeviceCallback.onAudioDevicesRemoved(new AudioDeviceInfo[] {info});
+        inputRouteManager.mAudioDeviceCallback.onAudioDevicesRemoved(
+                new AudioDeviceInfo[] {mockWiredHeadsetInfo()});
+        onPreferredDevicesForCapturePresetChanged(inputRouteManager);
 
         assertThat(inputRouteManager.mInputMediaDevices).isEmpty();
     }
 
     @Test
     public void getSelectedInputDevice_returnOneFromAudioManager() {
-        final AudioDeviceInfo info1 = mock(AudioDeviceInfo.class);
-        when(info1.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
-        when(info1.getId()).thenReturn(INPUT_WIRED_HEADSET_ID);
-        when(info1.getProductName()).thenReturn(PRODUCT_NAME_WIRED_HEADSET);
-
-        final AudioDeviceInfo info2 = mock(AudioDeviceInfo.class);
-        when(info2.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_MIC);
-        when(info2.getId()).thenReturn(BUILTIN_MIC_ID);
-        when(info2.getProductName()).thenReturn(PRODUCT_NAME_BUILTIN_MIC);
-
         final AudioManager audioManager = mock(AudioManager.class);
-        AudioDeviceInfo[] devices = {info1, info2};
+        AudioDeviceInfo[] devices = {mockWiredHeadsetInfo(), mockBuiltinMicInfo()};
         when(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn(devices);
 
         // Mock audioManager.getDevicesForAttributes returns exactly one audioDeviceAttributes.
-        AudioDeviceAttributes audioDeviceAttributes = new AudioDeviceAttributes(info1);
         when(audioManager.getDevicesForAttributes(INPUT_ATTRIBUTES))
-                .thenReturn(Collections.singletonList(audioDeviceAttributes));
+                .thenReturn(Collections.singletonList(getWiredHeadsetDeviceAttributes()));
 
         InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
-        inputRouteManager.mAudioDeviceCallback.onAudioDevicesAdded(devices);
+        onPreferredDevicesForCapturePresetChanged(inputRouteManager);
 
         // The selected input device has the same type as the one returned from AudioManager.
         InputMediaDevice selectedInputDevice =
@@ -181,31 +227,19 @@
 
     @Test
     public void getSelectedInputDevice_returnMoreThanOneFromAudioManager() {
-        final AudioDeviceInfo info1 = mock(AudioDeviceInfo.class);
-        when(info1.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
-        when(info1.getId()).thenReturn(INPUT_WIRED_HEADSET_ID);
-        when(info1.getProductName()).thenReturn(PRODUCT_NAME_WIRED_HEADSET);
-
-        final AudioDeviceInfo info2 = mock(AudioDeviceInfo.class);
-        when(info2.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_MIC);
-        when(info2.getId()).thenReturn(BUILTIN_MIC_ID);
-        when(info2.getProductName()).thenReturn(PRODUCT_NAME_BUILTIN_MIC);
-
         final AudioManager audioManager = mock(AudioManager.class);
-        AudioDeviceInfo[] devices = {info1, info2};
+        AudioDeviceInfo[] devices = {mockWiredHeadsetInfo(), mockBuiltinMicInfo()};
         when(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn(devices);
 
         // Mock audioManager.getDevicesForAttributes returns more than one audioDeviceAttributes.
-        AudioDeviceAttributes audioDeviceAttributes1 = new AudioDeviceAttributes(info1);
-        AudioDeviceAttributes audioDeviceAttributes2 = new AudioDeviceAttributes(info2);
         List<AudioDeviceAttributes> attributesOfSelectedInputDevices = new ArrayList<>();
-        attributesOfSelectedInputDevices.add(audioDeviceAttributes1);
-        attributesOfSelectedInputDevices.add(audioDeviceAttributes2);
+        attributesOfSelectedInputDevices.add(getWiredHeadsetDeviceAttributes());
+        attributesOfSelectedInputDevices.add(getBuiltinMicDeviceAttributes());
         when(audioManager.getDevicesForAttributes(INPUT_ATTRIBUTES))
                 .thenReturn(attributesOfSelectedInputDevices);
 
         InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
-        inputRouteManager.mAudioDeviceCallback.onAudioDevicesAdded(devices);
+        onPreferredDevicesForCapturePresetChanged(inputRouteManager);
 
         // The selected input device has the same type as the first one returned from AudioManager.
         InputMediaDevice selectedInputDevice =
@@ -216,27 +250,17 @@
 
     @Test
     public void getSelectedInputDevice_returnEmptyFromAudioManager() {
-        final AudioDeviceInfo info1 = mock(AudioDeviceInfo.class);
-        when(info1.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
-        when(info1.getId()).thenReturn(INPUT_WIRED_HEADSET_ID);
-        when(info1.getProductName()).thenReturn(PRODUCT_NAME_WIRED_HEADSET);
-
-        final AudioDeviceInfo info2 = mock(AudioDeviceInfo.class);
-        when(info2.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_MIC);
-        when(info2.getId()).thenReturn(BUILTIN_MIC_ID);
-        when(info2.getProductName()).thenReturn(PRODUCT_NAME_BUILTIN_MIC);
-
         final AudioManager audioManager = mock(AudioManager.class);
-        AudioDeviceInfo[] devices = {info1, info2};
+        AudioDeviceInfo[] devices = {mockWiredHeadsetInfo(), mockBuiltinMicInfo()};
         when(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn(devices);
 
         // Mock audioManager.getDevicesForAttributes returns empty list of audioDeviceAttributes.
-        List<AudioDeviceAttributes> attributesOfSelectedInputDevices = new ArrayList<>();
+        List<AudioDeviceAttributes> emptyAttributesOfSelectedInputDevices = new ArrayList<>();
         when(audioManager.getDevicesForAttributes(INPUT_ATTRIBUTES))
-                .thenReturn(attributesOfSelectedInputDevices);
+                .thenReturn(emptyAttributesOfSelectedInputDevices);
 
         InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
-        inputRouteManager.mAudioDeviceCallback.onAudioDevicesAdded(devices);
+        onPreferredDevicesForCapturePresetChanged(inputRouteManager);
 
         // The selected input device has default type AudioDeviceInfo.TYPE_BUILTIN_MIC.
         InputMediaDevice selectedInputDevice =
@@ -249,7 +273,7 @@
     public void selectDevice() {
         final AudioManager audioManager = mock(AudioManager.class);
         InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
-        final MediaDevice inputMediaDevice =
+        final MediaDevice builtinMicDevice =
                 InputMediaDevice.create(
                         mContext,
                         String.valueOf(BUILTIN_MIC_ID),
@@ -258,16 +282,57 @@
                         CURRENT_VOLUME,
                         VOLUME_FIXED_TRUE,
                         PRODUCT_NAME_BUILTIN_MIC);
-        inputRouteManager.selectDevice(inputMediaDevice);
+        inputRouteManager.selectDevice(builtinMicDevice);
 
-        AudioDeviceAttributes deviceAttributes =
-                new AudioDeviceAttributes(
-                        AudioDeviceAttributes.ROLE_INPUT,
-                        AudioDeviceInfo.TYPE_BUILTIN_MIC,
-                        /* address= */ "");
         for (@MediaRecorder.Source int preset : PRESETS) {
             verify(audioManager, atLeastOnce())
-                    .setPreferredDeviceForCapturePreset(preset, deviceAttributes);
+                    .setPreferredDeviceForCapturePreset(preset, getBuiltinMicDeviceAttributes());
+        }
+    }
+
+    @Test
+    public void onInitiation_shouldApplyDefaultSelectedDeviceToAllPresets() {
+        final AudioManager audioManager = mock(AudioManager.class);
+        new InputRouteManager(mContext, audioManager);
+
+        verify(audioManager, atLeastOnce()).getDevicesForAttributes(INPUT_ATTRIBUTES);
+        for (@MediaRecorder.Source int preset : PRESETS) {
+            verify(audioManager, atLeastOnce())
+                    .setPreferredDeviceForCapturePreset(preset, getBuiltinMicDeviceAttributes());
+        }
+    }
+
+    @Test
+    public void onAudioDevicesAdded_shouldApplyDefaultSelectedDeviceToAllPresets() {
+        final AudioManager audioManager = mock(AudioManager.class);
+        AudioDeviceAttributes wiredHeadsetDeviceAttributes = getWiredHeadsetDeviceAttributes();
+        when(audioManager.getDevicesForAttributes(INPUT_ATTRIBUTES))
+                .thenReturn(Collections.singletonList(wiredHeadsetDeviceAttributes));
+
+        InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
+        AudioDeviceInfo[] devices = {mockWiredHeadsetInfo()};
+        inputRouteManager.mAudioDeviceCallback.onAudioDevicesAdded(devices);
+
+        // Called twice, one after initiation, the other after onAudioDevicesAdded call.
+        verify(audioManager, atLeast(2)).getDevicesForAttributes(INPUT_ATTRIBUTES);
+        for (@MediaRecorder.Source int preset : PRESETS) {
+            verify(audioManager, atLeast(2))
+                    .setPreferredDeviceForCapturePreset(preset, wiredHeadsetDeviceAttributes);
+        }
+    }
+
+    @Test
+    public void onAudioDevicesRemoved_shouldApplyDefaultSelectedDeviceToAllPresets() {
+        final AudioManager audioManager = mock(AudioManager.class);
+        InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
+        AudioDeviceInfo[] devices = {mockWiredHeadsetInfo()};
+        inputRouteManager.mAudioDeviceCallback.onAudioDevicesRemoved(devices);
+
+        // Called twice, one after initiation, the other after onAudioDevicesRemoved call.
+        verify(audioManager, atLeast(2)).getDevicesForAttributes(INPUT_ATTRIBUTES);
+        for (@MediaRecorder.Source int preset : PRESETS) {
+            verify(audioManager, atLeast(2))
+                    .setPreferredDeviceForCapturePreset(preset, getBuiltinMicDeviceAttributes());
         }
     }
 
@@ -288,27 +353,25 @@
 
     @Test
     public void onAudioDevicesAdded_shouldSetProductNameCorrectly() {
-        final AudioDeviceInfo info1 = mock(AudioDeviceInfo.class);
-        when(info1.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
-        when(info1.getId()).thenReturn(INPUT_WIRED_HEADSET_ID);
+        final AudioDeviceInfo info1 = mockWiredHeadsetInfo();
         String firstProductName = "My first headset";
         when(info1.getProductName()).thenReturn(firstProductName);
+        InputMediaDevice inputMediaDevice1 = createInputMediaDeviceFromDeviceInfo(info1);
 
-        final AudioDeviceInfo info2 = mock(AudioDeviceInfo.class);
-        when(info2.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
-        when(info2.getId()).thenReturn(INPUT_WIRED_HEADSET_ID);
+        final AudioDeviceInfo info2 = mockWiredHeadsetInfo();
         String secondProductName = "My second headset";
         when(info2.getProductName()).thenReturn(secondProductName);
+        InputMediaDevice inputMediaDevice2 = createInputMediaDeviceFromDeviceInfo(info2);
 
-        final AudioDeviceInfo infoWithNullProductName = mock(AudioDeviceInfo.class);
-        when(infoWithNullProductName.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
-        when(infoWithNullProductName.getId()).thenReturn(INPUT_WIRED_HEADSET_ID);
+        final AudioDeviceInfo infoWithNullProductName = mockWiredHeadsetInfo();
         when(infoWithNullProductName.getProductName()).thenReturn(null);
+        InputMediaDevice inputMediaDevice3 =
+                createInputMediaDeviceFromDeviceInfo(infoWithNullProductName);
 
-        final AudioDeviceInfo infoWithBlankProductName = mock(AudioDeviceInfo.class);
-        when(infoWithBlankProductName.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
-        when(infoWithBlankProductName.getId()).thenReturn(INPUT_WIRED_HEADSET_ID);
+        final AudioDeviceInfo infoWithBlankProductName = mockWiredHeadsetInfo();
         when(infoWithBlankProductName.getProductName()).thenReturn("");
+        InputMediaDevice inputMediaDevice4 =
+                createInputMediaDeviceFromDeviceInfo(infoWithBlankProductName);
 
         final AudioManager audioManager = mock(AudioManager.class);
         AudioDeviceInfo[] devices = {
@@ -321,15 +384,22 @@
         assertThat(inputRouteManager.mInputMediaDevices).isEmpty();
 
         inputRouteManager.mAudioDeviceCallback.onAudioDevicesAdded(devices);
+        onPreferredDevicesForCapturePresetChanged(inputRouteManager);
 
-        assertThat(getProductNameAtIndex(inputRouteManager, 1)).isEqualTo(firstProductName);
-        assertThat(getProductNameAtIndex(inputRouteManager, 2)).isEqualTo(secondProductName);
-        assertThat(getProductNameAtIndex(inputRouteManager, 3)).isNull();
-        assertThat(getProductNameAtIndex(inputRouteManager, 4)).isNull();
+        assertThat(inputRouteManager.mInputMediaDevices)
+                .containsExactly(
+                        inputMediaDevice1, inputMediaDevice2, inputMediaDevice3, inputMediaDevice4)
+                .inOrder();
     }
 
-    private String getProductNameAtIndex(InputRouteManager inputRouteManager, int index) {
-        return ((InputMediaDevice) inputRouteManager.mInputMediaDevices.get(index))
-                .getProductName();
+    private InputMediaDevice createInputMediaDeviceFromDeviceInfo(AudioDeviceInfo info) {
+        return InputMediaDevice.create(
+                mContext,
+                String.valueOf(info.getId()),
+                info.getType(),
+                MAX_VOLUME,
+                CURRENT_VOLUME,
+                VOLUME_FIXED_TRUE,
+                info.getProductName() == null ? null : info.getProductName().toString());
     }
 }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiUtilsTest.java
index 5293011..d8b6707 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiUtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiUtilsTest.java
@@ -207,20 +207,6 @@
     }
 
     @Test
-    public void getHotspotIconResource_deviceTypeExists_shouldNotNull() {
-        assertThat(WifiUtils.getHotspotIconResource(NetworkProviderInfo.DEVICE_TYPE_PHONE))
-                .isNotNull();
-        assertThat(WifiUtils.getHotspotIconResource(NetworkProviderInfo.DEVICE_TYPE_TABLET))
-                .isNotNull();
-        assertThat(WifiUtils.getHotspotIconResource(NetworkProviderInfo.DEVICE_TYPE_LAPTOP))
-                .isNotNull();
-        assertThat(WifiUtils.getHotspotIconResource(NetworkProviderInfo.DEVICE_TYPE_WATCH))
-                .isNotNull();
-        assertThat(WifiUtils.getHotspotIconResource(NetworkProviderInfo.DEVICE_TYPE_AUTO))
-                .isNotNull();
-    }
-
-    @Test
     public void testInternetIconInjector_getIcon_returnsCorrectValues() {
         WifiUtils.InternetIconInjector iconInjector = new WifiUtils.InternetIconInjector(mContext);
 
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
index 2cdd0ae..3530e0f 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
@@ -106,6 +106,8 @@
                 Settings.System.UNREAD_NOTIFICATION_DOT_INDICATOR,
                 Settings.System.AUTO_LAUNCH_MEDIA_CONTROLS,
                 Settings.System.LOCALE_PREFERENCES,
+                Settings.System.MOUSE_REVERSE_VERTICAL_SCROLLING,
+                Settings.System.MOUSE_SWAP_PRIMARY_BUTTON,
                 Settings.System.TOUCHPAD_POINTER_SPEED,
                 Settings.System.TOUCHPAD_NATURAL_SCROLLING,
                 Settings.System.TOUCHPAD_TAP_TO_CLICK,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
index 2823277..509b88b 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
@@ -221,6 +221,8 @@
                         POINTER_ICON_VECTOR_STYLE_STROKE_END));
         VALIDATORS.put(System.POINTER_SCALE,
                 new InclusiveFloatRangeValidator(DEFAULT_POINTER_SCALE, LARGE_POINTER_SCALE));
+        VALIDATORS.put(System.MOUSE_REVERSE_VERTICAL_SCROLLING, BOOLEAN_VALIDATOR);
+        VALIDATORS.put(System.MOUSE_SWAP_PRIMARY_BUTTON, BOOLEAN_VALIDATOR);
         VALIDATORS.put(System.TOUCHPAD_POINTER_SPEED, new InclusiveIntegerRangeValidator(-7, 7));
         VALIDATORS.put(System.TOUCHPAD_NATURAL_SCROLLING, BOOLEAN_VALIDATOR);
         VALIDATORS.put(System.TOUCHPAD_TAP_TO_CLICK, BOOLEAN_VALIDATOR);
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 1f10ead..c4a45d0 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -425,8 +425,8 @@
         "tests/src/**/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt",
         "tests/src/**/systemui/shared/system/RemoteTransitionTest.java",
         "tests/src/**/systemui/navigationbar/NavigationBarControllerImplTest.java",
-        "tests/src/**/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt",
-        "tests/src/**/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt",
+        "tests/src/**/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelTest.kt",
+        "tests/src/**/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt",
         "tests/src/**/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt",
         "tests/src/**/systemui/notetask/LaunchNotesRoleSettingsTrampolineActivityTest.kt",
         "tests/src/**/systemui/notetask/shortcut/LaunchNoteTaskActivityTest.kt",
diff --git a/packages/SystemUI/TEST_MAPPING b/packages/SystemUI/TEST_MAPPING
index 07a1e63..380344a 100644
--- a/packages/SystemUI/TEST_MAPPING
+++ b/packages/SystemUI/TEST_MAPPING
@@ -148,5 +148,10 @@
     {
       "name": "SystemUIGoogleRobo2RNGTests"
     }
+  ],
+  "imports": [
+    {
+      "path": "cts/tests/tests/multiuser"
+    }
   ]
 }
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt
new file mode 100644
index 0000000..9b94c91
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt
@@ -0,0 +1,203 @@
+/*
+ * 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.shared.clocks
+
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.Rect
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.log.core.Logger
+import com.android.systemui.log.core.MessageBuffer
+import com.android.systemui.plugins.clocks.AlarmData
+import com.android.systemui.plugins.clocks.ClockAnimations
+import com.android.systemui.plugins.clocks.ClockEvents
+import com.android.systemui.plugins.clocks.ClockFaceConfig
+import com.android.systemui.plugins.clocks.ClockFaceEvents
+import com.android.systemui.plugins.clocks.ClockReactiveSetting
+import com.android.systemui.plugins.clocks.WeatherData
+import com.android.systemui.plugins.clocks.ZenData
+import com.android.systemui.shared.clocks.view.DigitalClockFaceView
+import com.android.systemui.shared.clocks.view.FlexClockView
+import java.util.Locale
+import java.util.TimeZone
+
+class ComposedDigitalLayerController(
+    private val ctx: Context,
+    private val assets: AssetLoader,
+    private val layer: ComposedDigitalHandLayer,
+    private val isLargeClock: Boolean,
+    messageBuffer: MessageBuffer,
+) : SimpleClockLayerController {
+    private val logger = Logger(messageBuffer, ComposedDigitalLayerController::class.simpleName!!)
+
+    val layerControllers = mutableListOf<SimpleClockLayerController>()
+    val dozeState = DefaultClockController.AnimationState(1F)
+    var isRegionDark = true
+
+    override var view: DigitalClockFaceView =
+        when (layer.customizedView) {
+            "FlexClockView" -> FlexClockView(ctx, assets, messageBuffer)
+            else -> {
+                throw IllegalStateException("CustomizedView string is not valid")
+            }
+        }
+
+    // Matches LayerControllerConstructor
+    internal constructor(
+        ctx: Context,
+        assets: AssetLoader,
+        layer: ClockLayer,
+        isLargeClock: Boolean,
+        messageBuffer: MessageBuffer,
+    ) : this(ctx, assets, layer as ComposedDigitalHandLayer, isLargeClock, messageBuffer)
+
+    init {
+        layer.digitalLayers.forEach {
+            val controller =
+                SimpleClockLayerController.Factory.create(
+                    ctx,
+                    assets,
+                    it,
+                    isLargeClock,
+                    messageBuffer,
+                )
+            view.addView(controller.view)
+            layerControllers.add(controller)
+        }
+    }
+
+    private fun refreshTime() {
+        layerControllers.forEach { it.faceEvents.onTimeTick() }
+        view.refreshTime()
+    }
+
+    override val events =
+        object : ClockEvents {
+            override fun onTimeZoneChanged(timeZone: TimeZone) {
+                layerControllers.forEach { it.events.onTimeZoneChanged(timeZone) }
+                refreshTime()
+            }
+
+            override fun onTimeFormatChanged(is24Hr: Boolean) {
+                layerControllers.forEach { it.events.onTimeFormatChanged(is24Hr) }
+                refreshTime()
+            }
+
+            override fun onLocaleChanged(locale: Locale) {
+                layerControllers.forEach { it.events.onLocaleChanged(locale) }
+                view.onLocaleChanged(locale)
+                refreshTime()
+            }
+
+            override fun onWeatherDataChanged(data: WeatherData) {
+                view.onWeatherDataChanged(data)
+            }
+
+            override fun onAlarmDataChanged(data: AlarmData) {
+                view.onAlarmDataChanged(data)
+            }
+
+            override fun onZenDataChanged(data: ZenData) {
+                view.onZenDataChanged(data)
+            }
+
+            override fun onColorPaletteChanged(resources: Resources) {}
+
+            override fun onSeedColorChanged(seedColor: Int?) {}
+
+            override fun onReactiveAxesChanged(axes: List<ClockReactiveSetting>) {}
+
+            override var isReactiveTouchInteractionEnabled
+                get() = view.isReactiveTouchInteractionEnabled
+                set(value) {
+                    view.isReactiveTouchInteractionEnabled = value
+                }
+        }
+
+    override fun updateColors() {
+        view.updateColors(assets, isRegionDark)
+    }
+
+    override val animations =
+        object : ClockAnimations {
+            override fun enter() {
+                refreshTime()
+            }
+
+            override fun doze(fraction: Float) {
+                val (hasChanged, hasJumped) = dozeState.update(fraction)
+                if (hasChanged) view.animateDoze(dozeState.isActive, !hasJumped)
+                view.dozeFraction = fraction
+                view.invalidate()
+            }
+
+            override fun fold(fraction: Float) {
+                refreshTime()
+            }
+
+            override fun charge() {
+                view.animateCharge()
+            }
+
+            override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {
+                view.onPositionUpdated(fromLeft, direction, fraction)
+            }
+
+            override fun onPositionUpdated(distance: Float, fraction: Float) {}
+
+            override fun onPickerCarouselSwiping(swipingFraction: Float) {
+                view.onPickerCarouselSwiping(swipingFraction)
+            }
+        }
+
+    override val faceEvents =
+        object : ClockFaceEvents {
+            override fun onTimeTick() {
+                refreshTime()
+            }
+
+            override fun onRegionDarknessChanged(isRegionDark: Boolean) {
+                this@ComposedDigitalLayerController.isRegionDark = isRegionDark
+                updateColors()
+            }
+
+            override fun onFontSettingChanged(fontSizePx: Float) {
+                view.onFontSettingChanged(fontSizePx)
+            }
+
+            override fun onTargetRegionChanged(targetRegion: Rect?) {}
+
+            override fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean) {}
+        }
+
+    override val config =
+        ClockFaceConfig(
+            hasCustomWeatherDataDisplay = view.hasCustomWeatherDataDisplay,
+            hasCustomPositionUpdatedAnimation = view.hasCustomPositionUpdatedAnimation,
+            useCustomClockScene = view.useCustomClockScene,
+        )
+
+    @VisibleForTesting
+    override var fakeTimeMills: Long? = null
+        get() = field
+        set(timeInMills) {
+            field = timeInMills
+            for (layerController in layerControllers) {
+                layerController.fakeTimeMills = timeInMills
+            }
+        }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
index 07191c6..ac26842 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
@@ -24,6 +24,8 @@
 import com.android.systemui.plugins.clocks.ClockPickerConfig
 import com.android.systemui.plugins.clocks.ClockProvider
 import com.android.systemui.plugins.clocks.ClockSettings
+import com.android.systemui.shared.clocks.view.HorizontalAlignment
+import com.android.systemui.shared.clocks.view.VerticalAlignment
 
 private val TAG = DefaultClockProvider::class.simpleName
 const val DEFAULT_CLOCK_ID = "DEFAULT"
@@ -33,8 +35,9 @@
     val ctx: Context,
     val layoutInflater: LayoutInflater,
     val resources: Resources,
-    val hasStepClockAnimation: Boolean = false,
-    val migratedClocks: Boolean = false,
+    private val hasStepClockAnimation: Boolean = false,
+    private val migratedClocks: Boolean = false,
+    private val clockReactiveVariants: Boolean = false,
 ) : ClockProvider {
     private var messageBuffers: ClockMessageBuffers? = null
 
@@ -49,15 +52,23 @@
             throw IllegalArgumentException("${settings.clockId} is unsupported by $TAG")
         }
 
-        return DefaultClockController(
-            ctx,
-            layoutInflater,
-            resources,
-            settings,
-            hasStepClockAnimation,
-            migratedClocks,
-            messageBuffers,
-        )
+        return if (clockReactiveVariants) {
+            // TODO handle the case here where only the smallClock message buffer is added
+            val assetLoader =
+                AssetLoader(ctx, ctx, "clocks/", messageBuffers?.smallClockMessageBuffer!!)
+
+            SimpleClockController(ctx, assetLoader, FLEX_DESIGN, messageBuffers)
+        } else {
+            DefaultClockController(
+                ctx,
+                layoutInflater,
+                resources,
+                settings,
+                hasStepClockAnimation,
+                migratedClocks,
+                messageBuffers,
+            )
+        }
     }
 
     override fun getClockPickerConfig(id: ClockId): ClockPickerConfig {
@@ -73,4 +84,163 @@
             resources.getDrawable(R.drawable.clock_default_thumbnail, null),
         )
     }
+
+    companion object {
+        val FLEX_DESIGN = run {
+            val largeLayer =
+                listOf(
+                    ComposedDigitalHandLayer(
+                        layerBounds = LayerBounds.FIT,
+                        customizedView = "FlexClockView",
+                        digitalLayers =
+                            listOf(
+                                DigitalHandLayer(
+                                    layerBounds = LayerBounds.FIT,
+                                    timespec = DigitalTimespec.FIRST_DIGIT,
+                                    style =
+                                        FontTextStyle(
+                                            fontFamily = "google_sans_flex.ttf",
+                                            lineHeight = 147.25f,
+                                            fontVariation =
+                                                "'wght' 603, 'wdth' 100, 'opsz' 144, 'ROND' 100",
+                                        ),
+                                    aodStyle =
+                                        FontTextStyle(
+                                            fontVariation =
+                                                "'wght' 74, 'wdth' 43, 'opsz' 144, 'ROND' 100",
+                                            fontFamily = "google_sans_flex.ttf",
+                                            fillColorLight = "#FFFFFFFF",
+                                            outlineColor = "#00000000",
+                                            renderType = RenderType.CHANGE_WEIGHT,
+                                            transitionInterpolator = InterpolatorEnum.EMPHASIZED,
+                                            transitionDuration = 750,
+                                        ),
+                                    alignment =
+                                        DigitalAlignment(
+                                            HorizontalAlignment.CENTER,
+                                            VerticalAlignment.CENTER
+                                        ),
+                                    dateTimeFormat = "hh"
+                                ),
+                                DigitalHandLayer(
+                                    layerBounds = LayerBounds.FIT,
+                                    timespec = DigitalTimespec.SECOND_DIGIT,
+                                    style =
+                                        FontTextStyle(
+                                            fontFamily = "google_sans_flex.ttf",
+                                            lineHeight = 147.25f,
+                                            fontVariation =
+                                                "'wght' 603, 'wdth' 100, 'opsz' 144, 'ROND' 100",
+                                        ),
+                                    aodStyle =
+                                        FontTextStyle(
+                                            fontVariation =
+                                                "'wght' 74, 'wdth' 43, 'opsz' 144, 'ROND' 100",
+                                            fontFamily = "google_sans_flex.ttf",
+                                            fillColorLight = "#FFFFFFFF",
+                                            outlineColor = "#00000000",
+                                            renderType = RenderType.CHANGE_WEIGHT,
+                                            transitionInterpolator = InterpolatorEnum.EMPHASIZED,
+                                            transitionDuration = 750,
+                                        ),
+                                    alignment =
+                                        DigitalAlignment(
+                                            HorizontalAlignment.CENTER,
+                                            VerticalAlignment.CENTER
+                                        ),
+                                    dateTimeFormat = "hh"
+                                ),
+                                DigitalHandLayer(
+                                    layerBounds = LayerBounds.FIT,
+                                    timespec = DigitalTimespec.FIRST_DIGIT,
+                                    style =
+                                        FontTextStyle(
+                                            fontFamily = "google_sans_flex.ttf",
+                                            lineHeight = 147.25f,
+                                            fontVariation =
+                                                "'wght' 603, 'wdth' 100, 'opsz' 144, 'ROND' 100",
+                                        ),
+                                    aodStyle =
+                                        FontTextStyle(
+                                            fontVariation =
+                                                "'wght' 74, 'wdth' 43, 'opsz' 144, 'ROND' 100",
+                                            fontFamily = "google_sans_flex.ttf",
+                                            fillColorLight = "#FFFFFFFF",
+                                            outlineColor = "#00000000",
+                                            renderType = RenderType.CHANGE_WEIGHT,
+                                            transitionInterpolator = InterpolatorEnum.EMPHASIZED,
+                                            transitionDuration = 750,
+                                        ),
+                                    alignment =
+                                        DigitalAlignment(
+                                            HorizontalAlignment.CENTER,
+                                            VerticalAlignment.CENTER
+                                        ),
+                                    dateTimeFormat = "mm"
+                                ),
+                                DigitalHandLayer(
+                                    layerBounds = LayerBounds.FIT,
+                                    timespec = DigitalTimespec.SECOND_DIGIT,
+                                    style =
+                                        FontTextStyle(
+                                            fontFamily = "google_sans_flex.ttf",
+                                            lineHeight = 147.25f,
+                                            fontVariation =
+                                                "'wght' 603, 'wdth' 100, 'opsz' 144, 'ROND' 100",
+                                        ),
+                                    aodStyle =
+                                        FontTextStyle(
+                                            fontVariation =
+                                                "'wght' 74, 'wdth' 43, 'opsz' 144, 'ROND' 100",
+                                            fontFamily = "google_sans_flex.ttf",
+                                            fillColorLight = "#FFFFFFFF",
+                                            outlineColor = "#00000000",
+                                            renderType = RenderType.CHANGE_WEIGHT,
+                                            transitionInterpolator = InterpolatorEnum.EMPHASIZED,
+                                            transitionDuration = 750,
+                                        ),
+                                    alignment =
+                                        DigitalAlignment(
+                                            HorizontalAlignment.CENTER,
+                                            VerticalAlignment.CENTER
+                                        ),
+                                    dateTimeFormat = "mm"
+                                )
+                            )
+                    )
+                )
+
+            val smallLayer =
+                listOf(
+                    DigitalHandLayer(
+                        layerBounds = LayerBounds.FIT,
+                        timespec = DigitalTimespec.TIME_FULL_FORMAT,
+                        style =
+                            FontTextStyle(
+                                fontFamily = "google_sans_flex.ttf",
+                                fontVariation = "'wght' 600, 'wdth' 100, 'opsz' 144, 'ROND' 100",
+                                fontSizeScale = 0.98f,
+                            ),
+                        aodStyle =
+                            FontTextStyle(
+                                fontFamily = "google_sans_flex.ttf",
+                                fontVariation = "'wght' 133, 'wdth' 43, 'opsz' 144, 'ROND' 100",
+                                fillColorLight = "#FFFFFFFF",
+                                outlineColor = "#00000000",
+                                renderType = RenderType.CHANGE_WEIGHT,
+                            ),
+                        alignment = DigitalAlignment(HorizontalAlignment.LEFT, null),
+                        dateTimeFormat = "h:mm"
+                    )
+                )
+
+            ClockDesign(
+                id = DEFAULT_CLOCK_ID,
+                name = "@string/clock_default_name",
+                description = "@string/clock_default_description",
+                large = ClockFace(layers = largeLayer),
+                small = ClockFace(layers = smallLayer)
+            )
+        }
+    }
 }
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/LayoutUtils.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/LayoutUtils.kt
new file mode 100644
index 0000000..ef8bee0
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/LayoutUtils.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.shared.clocks
+
+import android.graphics.Rect
+import android.view.View
+
+fun computeLayoutDiff(
+    view: View,
+    targetRegion: Rect,
+    isLargeClock: Boolean,
+): Pair<Float, Float> {
+    val parent = view.parent
+    if (parent is View && parent.isLaidOut() && isLargeClock) {
+        return Pair(
+            targetRegion.centerX() - parent.width / 2f,
+            targetRegion.centerY() - parent.height / 2f
+        )
+    }
+    return Pair(0f, 0f)
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockController.kt
new file mode 100644
index 0000000..ec77798
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockController.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.shared.clocks
+
+import android.content.Context
+import android.content.res.Resources
+import com.android.systemui.monet.Style as MonetStyle
+import com.android.systemui.plugins.clocks.AlarmData
+import com.android.systemui.plugins.clocks.ClockConfig
+import com.android.systemui.plugins.clocks.ClockController
+import com.android.systemui.plugins.clocks.ClockEvents
+import com.android.systemui.plugins.clocks.ClockMessageBuffers
+import com.android.systemui.plugins.clocks.ClockReactiveSetting
+import com.android.systemui.plugins.clocks.WeatherData
+import com.android.systemui.plugins.clocks.ZenData
+import java.io.PrintWriter
+import java.util.Locale
+import java.util.TimeZone
+
+/** Controller for a simple json specified clock */
+class SimpleClockController(
+    private val ctx: Context,
+    private val assets: AssetLoader,
+    val design: ClockDesign,
+    val messageBuffers: ClockMessageBuffers?,
+) : ClockController {
+    override val smallClock = run {
+        val buffer = messageBuffers?.smallClockMessageBuffer ?: LogUtil.DEFAULT_MESSAGE_BUFFER
+        SimpleClockFaceController(
+            ctx,
+            assets.copy(messageBuffer = buffer),
+            design.small ?: design.large!!,
+            false,
+            buffer,
+        )
+    }
+
+    override val largeClock = run {
+        val buffer = messageBuffers?.largeClockMessageBuffer ?: LogUtil.DEFAULT_MESSAGE_BUFFER
+        SimpleClockFaceController(
+            ctx,
+            assets.copy(messageBuffer = buffer),
+            design.large ?: design.small!!,
+            true,
+            buffer,
+        )
+    }
+
+    override val config: ClockConfig by lazy {
+        ClockConfig(
+            design.id,
+            design.name?.let { assets.tryReadString(it) ?: it } ?: "",
+            design.description?.let { assets.tryReadString(it) ?: it } ?: "",
+            isReactiveToTone =
+                design.colorPalette == null || design.colorPalette == MonetStyle.CLOCK,
+            useAlternateSmartspaceAODTransition =
+                smallClock.config.hasCustomWeatherDataDisplay ||
+                    largeClock.config.hasCustomWeatherDataDisplay,
+            useCustomClockScene =
+                smallClock.config.useCustomClockScene || largeClock.config.useCustomClockScene,
+        )
+    }
+
+    override val events =
+        object : ClockEvents {
+            override var isReactiveTouchInteractionEnabled = false
+                set(value) {
+                    field = value
+                    smallClock.events.isReactiveTouchInteractionEnabled = value
+                    largeClock.events.isReactiveTouchInteractionEnabled = value
+                }
+
+            override fun onTimeZoneChanged(timeZone: TimeZone) {
+                smallClock.events.onTimeZoneChanged(timeZone)
+                largeClock.events.onTimeZoneChanged(timeZone)
+            }
+
+            override fun onTimeFormatChanged(is24Hr: Boolean) {
+                smallClock.events.onTimeFormatChanged(is24Hr)
+                largeClock.events.onTimeFormatChanged(is24Hr)
+            }
+
+            override fun onLocaleChanged(locale: Locale) {
+                smallClock.events.onLocaleChanged(locale)
+                largeClock.events.onLocaleChanged(locale)
+            }
+
+            override fun onColorPaletteChanged(resources: Resources) {
+                assets.refreshColorPalette(design.colorPalette)
+                smallClock.assets.refreshColorPalette(design.colorPalette)
+                largeClock.assets.refreshColorPalette(design.colorPalette)
+
+                smallClock.events.onColorPaletteChanged(resources)
+                largeClock.events.onColorPaletteChanged(resources)
+            }
+
+            override fun onSeedColorChanged(seedColor: Int?) {
+                assets.setSeedColor(seedColor, design.colorPalette)
+                smallClock.assets.setSeedColor(seedColor, design.colorPalette)
+                largeClock.assets.setSeedColor(seedColor, design.colorPalette)
+
+                smallClock.events.onSeedColorChanged(seedColor)
+                largeClock.events.onSeedColorChanged(seedColor)
+            }
+
+            override fun onWeatherDataChanged(data: WeatherData) {
+                smallClock.events.onWeatherDataChanged(data)
+                largeClock.events.onWeatherDataChanged(data)
+            }
+
+            override fun onAlarmDataChanged(data: AlarmData) {
+                smallClock.events.onAlarmDataChanged(data)
+                largeClock.events.onAlarmDataChanged(data)
+            }
+
+            override fun onZenDataChanged(data: ZenData) {
+                smallClock.events.onZenDataChanged(data)
+                largeClock.events.onZenDataChanged(data)
+            }
+
+            override fun onReactiveAxesChanged(axes: List<ClockReactiveSetting>) {
+                smallClock.events.onReactiveAxesChanged(axes)
+                largeClock.events.onReactiveAxesChanged(axes)
+            }
+        }
+
+    override fun initialize(resources: Resources, dozeFraction: Float, foldFraction: Float) {
+        events.onColorPaletteChanged(resources)
+        smallClock.animations.doze(dozeFraction)
+        largeClock.animations.doze(dozeFraction)
+        smallClock.animations.fold(foldFraction)
+        largeClock.animations.fold(foldFraction)
+        smallClock.events.onTimeTick()
+        largeClock.events.onTimeTick()
+    }
+
+    override fun dump(pw: PrintWriter) {}
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockFaceController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockFaceController.kt
new file mode 100644
index 0000000..ef398d1
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockFaceController.kt
@@ -0,0 +1,314 @@
+/*
+ * 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.shared.clocks
+
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.Rect
+import android.view.Gravity
+import android.view.View
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.widget.FrameLayout
+import com.android.systemui.log.core.MessageBuffer
+import com.android.systemui.plugins.clocks.AlarmData
+import com.android.systemui.plugins.clocks.ClockAnimations
+import com.android.systemui.plugins.clocks.ClockEvents
+import com.android.systemui.plugins.clocks.ClockFaceConfig
+import com.android.systemui.plugins.clocks.ClockFaceController
+import com.android.systemui.plugins.clocks.ClockFaceEvents
+import com.android.systemui.plugins.clocks.ClockFaceLayout
+import com.android.systemui.plugins.clocks.ClockReactiveSetting
+import com.android.systemui.plugins.clocks.ClockTickRate
+import com.android.systemui.plugins.clocks.DefaultClockFaceLayout
+import com.android.systemui.plugins.clocks.WeatherData
+import com.android.systemui.plugins.clocks.ZenData
+import com.android.systemui.shared.clocks.view.DigitalClockFaceView
+import java.util.Locale
+import java.util.TimeZone
+import kotlin.math.max
+
+interface ClockEventUnion : ClockEvents, ClockFaceEvents
+
+class SimpleClockFaceController(
+    ctx: Context,
+    val assets: AssetLoader,
+    face: ClockFace,
+    isLargeClock: Boolean,
+    messageBuffer: MessageBuffer,
+) : ClockFaceController {
+    override val view: View
+    override val config: ClockFaceConfig by lazy {
+        ClockFaceConfig(
+            hasCustomWeatherDataDisplay = layers.any { it.config.hasCustomWeatherDataDisplay },
+            hasCustomPositionUpdatedAnimation =
+                layers.any { it.config.hasCustomPositionUpdatedAnimation },
+            tickRate = getTickRate(),
+            useCustomClockScene = layers.any { it.config.useCustomClockScene },
+        )
+    }
+
+    val layers = mutableListOf<SimpleClockLayerController>()
+
+    val timespecHandler = DigitalTimespecHandler(DigitalTimespec.TIME_FULL_FORMAT, "hh:mm")
+
+    init {
+        val lp = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
+        lp.gravity = Gravity.CENTER
+        view =
+            if (face.layers.size == 1) {
+                // Optimize a clocks with a single layer by excluding the face level view group. We
+                // expect the view container from the host process to always be a FrameLayout.
+                val layer = face.layers[0]
+                val controller =
+                    SimpleClockLayerController.Factory.create(
+                        ctx,
+                        assets,
+                        layer,
+                        isLargeClock,
+                        messageBuffer,
+                    )
+                layers.add(controller)
+                controller.view.layoutParams = lp
+                controller.view
+            } else {
+                // For multiple views, we use an intermediate RelativeLayout so that we can do some
+                // intelligent laying out between the children views.
+                val group = SimpleClockRelativeLayout(ctx, face.faceLayout)
+                group.layoutParams = lp
+                group.gravity = Gravity.CENTER
+                group.clipChildren = false
+                for (layer in face.layers) {
+                    face.faceLayout?.let {
+                        if (layer is DigitalHandLayer) {
+                            layer.faceLayout = it
+                        }
+                    }
+                    val controller =
+                        SimpleClockLayerController.Factory.create(
+                            ctx,
+                            assets,
+                            layer,
+                            isLargeClock,
+                            messageBuffer,
+                        )
+                    group.addView(controller.view)
+                    layers.add(controller)
+                }
+                group
+            }
+    }
+
+    override val layout: ClockFaceLayout =
+        DefaultClockFaceLayout(view).apply {
+            views[0].id =
+                if (isLargeClock) {
+                    assets.getResourcesId("lockscreen_clock_view_large")
+                } else {
+                    assets.getResourcesId("lockscreen_clock_view")
+                }
+        }
+
+    override val events =
+        object : ClockEventUnion {
+            override var isReactiveTouchInteractionEnabled = false
+                get() = field
+                set(value) {
+                    field = value
+                    layers.forEach { it.events.isReactiveTouchInteractionEnabled = value }
+                }
+
+            override fun onTimeTick() {
+                timespecHandler.updateTime()
+                if (
+                    config.tickRate == ClockTickRate.PER_MINUTE ||
+                        view.contentDescription != timespecHandler.getContentDescription()
+                ) {
+                    view.contentDescription = timespecHandler.getContentDescription()
+                }
+                layers.forEach { it.faceEvents.onTimeTick() }
+            }
+
+            override fun onTimeZoneChanged(timeZone: TimeZone) {
+                timespecHandler.timeZone = timeZone
+                layers.forEach { it.events.onTimeZoneChanged(timeZone) }
+            }
+
+            override fun onTimeFormatChanged(is24Hr: Boolean) {
+                timespecHandler.is24Hr = is24Hr
+                layers.forEach { it.events.onTimeFormatChanged(is24Hr) }
+            }
+
+            override fun onLocaleChanged(locale: Locale) {
+                timespecHandler.updateLocale(locale)
+                layers.forEach { it.events.onLocaleChanged(locale) }
+            }
+
+            override fun onFontSettingChanged(fontSizePx: Float) {
+                layers.forEach { it.faceEvents.onFontSettingChanged(fontSizePx) }
+            }
+
+            override fun onColorPaletteChanged(resources: Resources) {
+                layers.forEach {
+                    it.events.onColorPaletteChanged(resources)
+                    it.updateColors()
+                }
+            }
+
+            override fun onSeedColorChanged(seedColor: Int?) {
+                layers.forEach {
+                    it.events.onSeedColorChanged(seedColor)
+                    it.updateColors()
+                }
+            }
+
+            override fun onRegionDarknessChanged(isRegionDark: Boolean) {
+                layers.forEach { it.faceEvents.onRegionDarknessChanged(isRegionDark) }
+            }
+
+            override fun onReactiveAxesChanged(axes: List<ClockReactiveSetting>) {}
+
+            /**
+             * targetRegion passed to all customized clock applies counter translationY of
+             * KeyguardStatusView and keyguard_large_clock_top_margin from default clock
+             */
+            override fun onTargetRegionChanged(targetRegion: Rect?) {
+                // When a clock needs to be aligned with screen, like weather clock
+                // it needs to offset back the translation of keyguard_large_clock_top_margin
+                if (view is DigitalClockFaceView && view.isAlignedWithScreen()) {
+                    val topMargin = getKeyguardLargeClockTopMargin(assets)
+                    targetRegion?.let {
+                        val (_, yDiff) = computeLayoutDiff(view, it, isLargeClock)
+                        // In LS, we use yDiff to counter translate
+                        // the translation of KeyguardLargeClockTopMargin
+                        // With the targetRegion passed from picker,
+                        // we will have yDiff = 0, no translation is needed for weather clock
+                        if (yDiff.toInt() != 0) view.translationY = yDiff - topMargin / 2
+                    }
+                    return
+                }
+
+                var maxWidth = 0f
+                var maxHeight = 0f
+
+                for (layer in layers) {
+                    layer.faceEvents.onTargetRegionChanged(targetRegion)
+                    maxWidth = max(maxWidth, layer.view.layoutParams.width.toFloat())
+                    maxHeight = max(maxHeight, layer.view.layoutParams.height.toFloat())
+                }
+
+                val lp =
+                    if (maxHeight <= 0 || maxWidth <= 0 || targetRegion == null) {
+                        // No specified width/height. Just match parent size.
+                        FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
+                    } else {
+                        // Scale to fit in targetRegion based on largest child elements.
+                        val ratio = maxWidth / maxHeight
+                        val targetRatio = targetRegion.width() / targetRegion.height().toFloat()
+                        val scale =
+                            if (ratio > targetRatio) targetRegion.width() / maxWidth
+                            else targetRegion.height() / maxHeight
+
+                        FrameLayout.LayoutParams(
+                            (maxWidth * scale).toInt(),
+                            (maxHeight * scale).toInt(),
+                        )
+                    }
+
+                lp.gravity = Gravity.CENTER
+                view.layoutParams = lp
+                targetRegion?.let {
+                    val (xDiff, yDiff) = computeLayoutDiff(view, it, isLargeClock)
+                    view.translationX = xDiff
+                    view.translationY = yDiff
+                }
+            }
+
+            override fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean) {}
+
+            override fun onWeatherDataChanged(data: WeatherData) {
+                layers.forEach { it.events.onWeatherDataChanged(data) }
+            }
+
+            override fun onAlarmDataChanged(data: AlarmData) {
+                layers.forEach { it.events.onAlarmDataChanged(data) }
+            }
+
+            override fun onZenDataChanged(data: ZenData) {
+                layers.forEach { it.events.onZenDataChanged(data) }
+            }
+        }
+
+    override val animations =
+        object : ClockAnimations {
+            override fun enter() {
+                layers.forEach { it.animations.enter() }
+            }
+
+            override fun doze(fraction: Float) {
+                layers.forEach { it.animations.doze(fraction) }
+            }
+
+            override fun fold(fraction: Float) {
+                layers.forEach { it.animations.fold(fraction) }
+            }
+
+            override fun charge() {
+                layers.forEach { it.animations.charge() }
+            }
+
+            override fun onPickerCarouselSwiping(swipingFraction: Float) {
+                face.pickerScale?.let {
+                    view.scaleX = swipingFraction * (1 - it.scaleX) + it.scaleX
+                    view.scaleY = swipingFraction * (1 - it.scaleY) + it.scaleY
+                }
+                if (!(view is DigitalClockFaceView && view.isAlignedWithScreen())) {
+                    val topMargin = getKeyguardLargeClockTopMargin(assets)
+                    view.translationY = topMargin / 2F * swipingFraction
+                }
+                layers.forEach { it.animations.onPickerCarouselSwiping(swipingFraction) }
+                view.invalidate()
+            }
+
+            override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {
+                layers.forEach { it.animations.onPositionUpdated(fromLeft, direction, fraction) }
+            }
+
+            override fun onPositionUpdated(distance: Float, fraction: Float) {
+                layers.forEach { it.animations.onPositionUpdated(distance, fraction) }
+            }
+        }
+
+    private fun getTickRate(): ClockTickRate {
+        var tickRate = ClockTickRate.PER_MINUTE
+        for (layer in layers) {
+            if (layer.config.tickRate.value < tickRate.value) {
+                tickRate = layer.config.tickRate
+            }
+        }
+        return tickRate
+    }
+
+    private fun getKeyguardLargeClockTopMargin(assets: AssetLoader): Int {
+        val topMarginRes =
+            assets.resolveResourceId(null, "dimen", "keyguard_large_clock_top_margin")
+        if (topMarginRes != null) {
+            val (res, id) = topMarginRes
+            return res.getDimensionPixelSize(id)
+        }
+        return 0
+    }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt
new file mode 100644
index 0000000..f71543e
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt
@@ -0,0 +1,102 @@
+/*
+ * 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.shared.clocks
+
+import android.content.Context
+import android.view.View
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.log.core.MessageBuffer
+import com.android.systemui.plugins.clocks.ClockAnimations
+import com.android.systemui.plugins.clocks.ClockEvents
+import com.android.systemui.plugins.clocks.ClockFaceConfig
+import com.android.systemui.plugins.clocks.ClockFaceEvents
+import com.android.systemui.shared.clocks.view.SimpleDigitalClockTextView
+import kotlin.reflect.KClass
+
+typealias LayerControllerConstructor =
+    (
+        ctx: Context,
+        assets: AssetLoader,
+        layer: ClockLayer,
+        isLargeClock: Boolean,
+        messageBuffer: MessageBuffer,
+    ) -> SimpleClockLayerController
+
+interface SimpleClockLayerController {
+    val view: View
+    val events: ClockEvents
+    val animations: ClockAnimations
+    val faceEvents: ClockFaceEvents
+    val config: ClockFaceConfig
+
+    @VisibleForTesting var fakeTimeMills: Long?
+
+    // Called immediately after either onColorPaletteChanged or onSeedColorChanged is called.
+    // Provided for convience to not duplicate color update logic after state updated.
+    fun updateColors() {}
+
+    companion object Factory {
+        val constructorMap = mutableMapOf<Pair<KClass<*>, KClass<*>?>, LayerControllerConstructor>()
+
+        internal inline fun <reified TLayer> registerConstructor(
+            noinline constructor: LayerControllerConstructor,
+        ) where TLayer : ClockLayer {
+            constructorMap[Pair(TLayer::class, null)] = constructor
+        }
+
+        inline fun <reified TLayer, reified TStyle> registerTextConstructor(
+            noinline constructor: LayerControllerConstructor,
+        ) where TLayer : ClockLayer, TStyle : TextStyle {
+            constructorMap[Pair(TLayer::class, TStyle::class)] = constructor
+        }
+
+        init {
+            registerConstructor<ComposedDigitalHandLayer>(::ComposedDigitalLayerController)
+            registerTextConstructor<DigitalHandLayer, FontTextStyle>(::createSimpleDigitalLayer)
+        }
+
+        private fun createSimpleDigitalLayer(
+            ctx: Context,
+            assets: AssetLoader,
+            layer: ClockLayer,
+            isLargeClock: Boolean,
+            messageBuffer: MessageBuffer
+        ): SimpleClockLayerController {
+            val view = SimpleDigitalClockTextView(ctx, messageBuffer)
+            return SimpleDigitalHandLayerController(
+                ctx,
+                assets,
+                layer as DigitalHandLayer,
+                view,
+                messageBuffer
+            )
+        }
+
+        fun create(
+            ctx: Context,
+            assets: AssetLoader,
+            layer: ClockLayer,
+            isLargeClock: Boolean,
+            messageBuffer: MessageBuffer
+        ): SimpleClockLayerController {
+            val styleClass = if (layer is DigitalHandLayer) layer.style::class else null
+            val key = Pair(layer::class, styleClass)
+            return constructorMap[key]?.invoke(ctx, assets, layer, isLargeClock, messageBuffer)
+                ?: throw IllegalArgumentException("Unrecognized ClockLayer type: $key")
+        }
+    }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockRelativeLayout.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockRelativeLayout.kt
new file mode 100644
index 0000000..6e1b9aa
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockRelativeLayout.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.shared.clocks
+
+import android.content.Context
+import android.view.View.MeasureSpec.EXACTLY
+import android.widget.RelativeLayout
+import androidx.core.view.children
+import com.android.systemui.shared.clocks.view.SimpleDigitalClockView
+
+class SimpleClockRelativeLayout(context: Context, val faceLayout: DigitalFaceLayout?) :
+    RelativeLayout(context) {
+    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+        // For migrate_clocks_to_blueprint, mode is EXACTLY
+        // when the flag is turned off, we won't execute this codes
+        if (MeasureSpec.getMode(heightMeasureSpec) == EXACTLY) {
+            if (
+                faceLayout == DigitalFaceLayout.TWO_PAIRS_VERTICAL ||
+                    faceLayout == DigitalFaceLayout.FOUR_DIGITS_ALIGN_CENTER
+            ) {
+                val constrainedHeight = MeasureSpec.getSize(heightMeasureSpec) / 2F
+                children.forEach {
+                    // The assumption here is the height of text view is linear to font size
+                    (it as SimpleDigitalClockView).applyTextSize(
+                        constrainedHeight,
+                        constrainedByHeight = true,
+                    )
+                }
+            }
+        }
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+    }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt
new file mode 100644
index 0000000..a3240f8
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt
@@ -0,0 +1,328 @@
+/*
+ * 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.shared.clocks
+
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.Rect
+import android.view.View
+import android.view.ViewGroup
+import android.widget.RelativeLayout
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.customization.R
+import com.android.systemui.log.core.Logger
+import com.android.systemui.log.core.MessageBuffer
+import com.android.systemui.plugins.clocks.AlarmData
+import com.android.systemui.plugins.clocks.ClockAnimations
+import com.android.systemui.plugins.clocks.ClockEvents
+import com.android.systemui.plugins.clocks.ClockFaceConfig
+import com.android.systemui.plugins.clocks.ClockFaceEvents
+import com.android.systemui.plugins.clocks.ClockReactiveSetting
+import com.android.systemui.plugins.clocks.WeatherData
+import com.android.systemui.plugins.clocks.ZenData
+import com.android.systemui.shared.clocks.view.SimpleDigitalClockView
+import java.util.Locale
+import java.util.TimeZone
+
+private val TAG = SimpleDigitalHandLayerController::class.simpleName!!
+
+open class SimpleDigitalHandLayerController<T>(
+    private val ctx: Context,
+    private val assets: AssetLoader,
+    private val layer: DigitalHandLayer,
+    override val view: T,
+    messageBuffer: MessageBuffer,
+) : SimpleClockLayerController where T : View, T : SimpleDigitalClockView {
+    private val logger = Logger(messageBuffer, TAG)
+    val timespec = DigitalTimespecHandler(layer.timespec, layer.dateTimeFormat)
+
+    @VisibleForTesting
+    fun hasLeadingZero() = layer.dateTimeFormat.startsWith("hh") || timespec.is24Hr
+
+    @VisibleForTesting
+    override var fakeTimeMills: Long?
+        get() = timespec.fakeTimeMills
+        set(value) {
+            timespec.fakeTimeMills = value
+        }
+
+    override val config = ClockFaceConfig()
+    var dozeState: DefaultClockController.AnimationState? = null
+    var isRegionDark: Boolean = true
+
+    init {
+        view.layoutParams =
+            RelativeLayout.LayoutParams(
+                ViewGroup.LayoutParams.WRAP_CONTENT,
+                ViewGroup.LayoutParams.WRAP_CONTENT
+            )
+        if (layer.alignment != null) {
+            layer.alignment.verticalAlignment?.let { view.verticalAlignment = it }
+            layer.alignment.horizontalAlignment?.let { view.horizontalAlignment = it }
+        }
+        view.applyStyles(assets, layer.style, layer.aodStyle)
+        view.id =
+            ctx.resources.getIdentifier(
+                generateDigitalLayerIdString(layer),
+                "id",
+                ctx.getPackageName(),
+            )
+    }
+
+    fun applyLayout(layout: DigitalFaceLayout?) {
+        when (layout) {
+            DigitalFaceLayout.FOUR_DIGITS_ALIGN_CENTER,
+            DigitalFaceLayout.FOUR_DIGITS_HORIZONTAL -> applyFourDigitsLayout(layout)
+            DigitalFaceLayout.TWO_PAIRS_HORIZONTAL,
+            DigitalFaceLayout.TWO_PAIRS_VERTICAL -> applyTwoPairsLayout(layout)
+            else -> {
+                // one view always use FrameLayout
+                // no need to change here
+            }
+        }
+        applyMargin()
+    }
+
+    private fun applyMargin() {
+        if (view.layoutParams is RelativeLayout.LayoutParams) {
+            val lp = view.layoutParams as RelativeLayout.LayoutParams
+            layer.marginRatio?.let {
+                lp.setMargins(
+                    /* left = */ (it.left * view.measuredWidth).toInt(),
+                    /* top = */ (it.top * view.measuredHeight).toInt(),
+                    /* right = */ (it.right * view.measuredWidth).toInt(),
+                    /* bottom = */ (it.bottom * view.measuredHeight).toInt(),
+                )
+            }
+            view.layoutParams = lp
+        }
+    }
+
+    private fun applyTwoPairsLayout(twoPairsLayout: DigitalFaceLayout) {
+        val lp = view.layoutParams as RelativeLayout.LayoutParams
+        lp.addRule(RelativeLayout.TEXT_ALIGNMENT_CENTER)
+        if (twoPairsLayout == DigitalFaceLayout.TWO_PAIRS_HORIZONTAL) {
+            when (view.id) {
+                R.id.HOUR_DIGIT_PAIR -> {
+                    lp.addRule(RelativeLayout.CENTER_VERTICAL)
+                    lp.addRule(RelativeLayout.ALIGN_PARENT_START)
+                }
+                R.id.MINUTE_DIGIT_PAIR -> {
+                    lp.addRule(RelativeLayout.CENTER_VERTICAL)
+                    lp.addRule(RelativeLayout.END_OF, R.id.HOUR_DIGIT_PAIR)
+                }
+                else -> {
+                    throw Exception("cannot apply two pairs layout to view ${view.id}")
+                }
+            }
+        } else {
+            when (view.id) {
+                R.id.HOUR_DIGIT_PAIR -> {
+                    lp.addRule(RelativeLayout.CENTER_HORIZONTAL)
+                    lp.addRule(RelativeLayout.ALIGN_PARENT_TOP)
+                }
+                R.id.MINUTE_DIGIT_PAIR -> {
+                    lp.addRule(RelativeLayout.CENTER_HORIZONTAL)
+                    lp.addRule(RelativeLayout.BELOW, R.id.HOUR_DIGIT_PAIR)
+                }
+                else -> {
+                    throw Exception("cannot apply two pairs layout to view ${view.id}")
+                }
+            }
+        }
+        view.layoutParams = lp
+    }
+
+    private fun applyFourDigitsLayout(fourDigitsfaceLayout: DigitalFaceLayout) {
+        val lp = view.layoutParams as RelativeLayout.LayoutParams
+        when (fourDigitsfaceLayout) {
+            DigitalFaceLayout.FOUR_DIGITS_ALIGN_CENTER -> {
+                when (view.id) {
+                    R.id.HOUR_FIRST_DIGIT -> {
+                        lp.addRule(RelativeLayout.ALIGN_PARENT_START)
+                        lp.addRule(RelativeLayout.ALIGN_PARENT_TOP)
+                    }
+                    R.id.HOUR_SECOND_DIGIT -> {
+                        lp.addRule(RelativeLayout.END_OF, R.id.HOUR_FIRST_DIGIT)
+                        lp.addRule(RelativeLayout.ALIGN_TOP, R.id.HOUR_FIRST_DIGIT)
+                    }
+                    R.id.MINUTE_FIRST_DIGIT -> {
+                        lp.addRule(RelativeLayout.ALIGN_START, R.id.HOUR_FIRST_DIGIT)
+                        lp.addRule(RelativeLayout.BELOW, R.id.HOUR_FIRST_DIGIT)
+                    }
+                    R.id.MINUTE_SECOND_DIGIT -> {
+                        lp.addRule(RelativeLayout.ALIGN_START, R.id.HOUR_SECOND_DIGIT)
+                        lp.addRule(RelativeLayout.BELOW, R.id.HOUR_SECOND_DIGIT)
+                    }
+                    else -> {
+                        throw Exception("cannot apply four digits layout to view ${view.id}")
+                    }
+                }
+            }
+            DigitalFaceLayout.FOUR_DIGITS_HORIZONTAL -> {
+                when (view.id) {
+                    R.id.HOUR_FIRST_DIGIT -> {
+                        lp.addRule(RelativeLayout.CENTER_VERTICAL)
+                        lp.addRule(RelativeLayout.ALIGN_PARENT_START)
+                    }
+                    R.id.HOUR_SECOND_DIGIT -> {
+                        lp.addRule(RelativeLayout.CENTER_VERTICAL)
+                        lp.addRule(RelativeLayout.END_OF, R.id.HOUR_FIRST_DIGIT)
+                    }
+                    R.id.MINUTE_FIRST_DIGIT -> {
+                        lp.addRule(RelativeLayout.CENTER_VERTICAL)
+                        lp.addRule(RelativeLayout.END_OF, R.id.HOUR_SECOND_DIGIT)
+                    }
+                    R.id.MINUTE_SECOND_DIGIT -> {
+                        lp.addRule(RelativeLayout.CENTER_VERTICAL)
+                        lp.addRule(RelativeLayout.END_OF, R.id.MINUTE_FIRST_DIGIT)
+                    }
+                    else -> {
+                        throw Exception("cannot apply FOUR_DIGITS_HORIZONTAL to view ${view.id}")
+                    }
+                }
+            }
+            else -> {
+                throw IllegalArgumentException(
+                    "applyFourDigitsLayout function should not " +
+                        "have parameters as ${layer.faceLayout}"
+                )
+            }
+        }
+        if (lp == view.layoutParams) {
+            return
+        }
+        view.layoutParams = lp
+    }
+
+    fun refreshTime() {
+        timespec.updateTime()
+        val text = timespec.getDigitString()
+        if (view.text != text) {
+            view.text = text
+            view.refreshTime()
+            logger.d({ "refreshTime: new text=$str1" }) { str1 = text }
+        }
+    }
+
+    override val events =
+        object : ClockEvents {
+            override var isReactiveTouchInteractionEnabled = false
+
+            override fun onLocaleChanged(locale: Locale) {
+                timespec.updateLocale(locale)
+                refreshTime()
+            }
+
+            /** Call whenever the text time format changes (12hr vs 24hr) */
+            override fun onTimeFormatChanged(is24Hr: Boolean) {
+                timespec.is24Hr = is24Hr
+                refreshTime()
+            }
+
+            override fun onTimeZoneChanged(timeZone: TimeZone) {
+                timespec.timeZone = timeZone
+                refreshTime()
+            }
+
+            override fun onColorPaletteChanged(resources: Resources) {}
+
+            override fun onSeedColorChanged(seedColor: Int?) {}
+
+            override fun onWeatherDataChanged(data: WeatherData) {}
+
+            override fun onAlarmDataChanged(data: AlarmData) {}
+
+            override fun onZenDataChanged(data: ZenData) {}
+
+            override fun onReactiveAxesChanged(axes: List<ClockReactiveSetting>) {}
+        }
+
+    override fun updateColors() {
+        view.updateColors(assets, isRegionDark)
+        refreshTime()
+    }
+
+    override val animations =
+        object : ClockAnimations {
+            override fun enter() {
+                applyLayout(layer.faceLayout)
+                refreshTime()
+            }
+
+            override fun doze(fraction: Float) {
+                if (dozeState == null) {
+                    dozeState = DefaultClockController.AnimationState(fraction)
+                    view.animateDoze(dozeState!!.isActive, false)
+                } else {
+                    val (hasChanged, hasJumped) = dozeState!!.update(fraction)
+                    if (hasChanged) view.animateDoze(dozeState!!.isActive, !hasJumped)
+                }
+                view.dozeFraction = fraction
+            }
+
+            override fun fold(fraction: Float) {
+                applyLayout(layer.faceLayout)
+                refreshTime()
+            }
+
+            override fun charge() {
+                view.animateCharge()
+            }
+
+            override fun onPickerCarouselSwiping(swipingFraction: Float) {}
+
+            override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {}
+
+            override fun onPositionUpdated(distance: Float, fraction: Float) {}
+        }
+
+    override val faceEvents =
+        object : ClockFaceEvents {
+            override fun onTimeTick() {
+                refreshTime()
+                if (
+                    layer.timespec == DigitalTimespec.TIME_FULL_FORMAT ||
+                        layer.timespec == DigitalTimespec.DATE_FORMAT
+                ) {
+                    view.contentDescription = timespec.getContentDescription()
+                }
+            }
+
+            override fun onFontSettingChanged(fontSizePx: Float) {
+                view.applyTextSize(fontSizePx)
+                applyMargin()
+            }
+
+            override fun onRegionDarknessChanged(isRegionDark: Boolean) {
+                this@SimpleDigitalHandLayerController.isRegionDark = isRegionDark
+                updateColors()
+            }
+
+            override fun onTargetRegionChanged(targetRegion: Rect?) {}
+
+            override fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean) {}
+        }
+
+    companion object {
+        private val DEFAULT_LIGHT_COLOR = "@android:color/system_accent1_100+0"
+        private val DEFAULT_DARK_COLOR = "@android:color/system_accent2_600+0"
+
+        fun getDefaultColor(assets: AssetLoader, isRegionDark: Boolean) =
+            assets.readColor(if (isRegionDark) DEFAULT_LIGHT_COLOR else DEFAULT_DARK_COLOR)
+    }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TimespecHandler.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TimespecHandler.kt
new file mode 100644
index 0000000..ed6a403
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TimespecHandler.kt
@@ -0,0 +1,161 @@
+/*
+ * 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.shared.clocks
+
+import android.icu.text.DateFormat
+import android.icu.text.SimpleDateFormat
+import android.icu.util.TimeZone as IcuTimeZone
+import android.icu.util.ULocale
+import androidx.annotation.VisibleForTesting
+import java.util.Calendar
+import java.util.Locale
+import java.util.TimeZone
+
+open class TimespecHandler(
+    val cal: Calendar,
+) {
+    var timeZone: TimeZone
+        get() = cal.timeZone
+        set(value) {
+            cal.timeZone = value
+            onTimeZoneChanged()
+        }
+
+    @VisibleForTesting var fakeTimeMills: Long? = null
+
+    fun updateTime() {
+        var timeMs = fakeTimeMills ?: System.currentTimeMillis()
+        cal.timeInMillis = (timeMs * TIME_TRAVEL_SCALE).toLong()
+    }
+
+    protected open fun onTimeZoneChanged() {}
+
+    companion object {
+        // Modifying this will cause the clock to run faster or slower. This is a useful way of
+        // manually checking that clocks are correctly animating through time.
+        private const val TIME_TRAVEL_SCALE = 1.0
+    }
+}
+
+class DigitalTimespecHandler(
+    val timespec: DigitalTimespec,
+    private val timeFormat: String,
+    cal: Calendar = Calendar.getInstance(),
+) : TimespecHandler(cal) {
+    var is24Hr = false
+        set(value) {
+            field = value
+            applyPattern()
+        }
+
+    private var dateFormat = updateSimpleDateFormat(Locale.getDefault())
+    private var contentDescriptionFormat = getContentDescriptionFormat(Locale.getDefault())
+
+    init {
+        applyPattern()
+    }
+
+    override fun onTimeZoneChanged() {
+        dateFormat.timeZone = IcuTimeZone.getTimeZone(cal.timeZone.id)
+        contentDescriptionFormat?.timeZone = IcuTimeZone.getTimeZone(cal.timeZone.id)
+        applyPattern()
+    }
+
+    fun updateLocale(locale: Locale) {
+        dateFormat = updateSimpleDateFormat(locale)
+        contentDescriptionFormat = getContentDescriptionFormat(locale)
+        onTimeZoneChanged()
+    }
+
+    private fun updateSimpleDateFormat(locale: Locale): DateFormat {
+        if (
+            locale.language.equals(Locale.ENGLISH.language) ||
+                timespec != DigitalTimespec.DATE_FORMAT
+        ) {
+            // force date format in English, and time format to use format defined in json
+            return SimpleDateFormat(timeFormat, timeFormat, ULocale.forLocale(locale))
+        } else {
+            return SimpleDateFormat.getInstanceForSkeleton(timeFormat, locale)
+        }
+    }
+
+    private fun getContentDescriptionFormat(locale: Locale): DateFormat? {
+        return when (timespec) {
+            DigitalTimespec.TIME_FULL_FORMAT ->
+                SimpleDateFormat.getInstanceForSkeleton("hh:mm", locale)
+            DigitalTimespec.DATE_FORMAT ->
+                SimpleDateFormat.getInstanceForSkeleton("EEEE MMMM d", locale)
+            else -> {
+                null
+            }
+        }
+    }
+
+    private fun applyPattern() {
+        val timeFormat24Hour = timeFormat.replace("hh", "h").replace("h", "HH")
+        val format = if (is24Hr) timeFormat24Hour else timeFormat
+        if (timespec != DigitalTimespec.DATE_FORMAT) {
+            (dateFormat as SimpleDateFormat).applyPattern(format)
+            (contentDescriptionFormat as? SimpleDateFormat)?.applyPattern(
+                if (is24Hr) CONTENT_DESCRIPTION_TIME_FORMAT_24_HOUR
+                else CONTENT_DESCRIPTION_TIME_FORMAT_12_HOUR
+            )
+        }
+    }
+
+    private fun getSingleDigit(): String {
+        val isFirstDigit = timespec == DigitalTimespec.FIRST_DIGIT
+        val text = dateFormat.format(cal.time).toString()
+        return text.substring(
+            if (isFirstDigit) 0 else text.length - 1,
+            if (isFirstDigit) text.length - 1 else text.length
+        )
+    }
+
+    fun getDigitString(): String {
+        return when (timespec) {
+            DigitalTimespec.FIRST_DIGIT,
+            DigitalTimespec.SECOND_DIGIT -> getSingleDigit()
+            DigitalTimespec.DIGIT_PAIR -> {
+                dateFormat.format(cal.time).toString()
+            }
+            DigitalTimespec.TIME_FULL_FORMAT -> {
+                dateFormat.format(cal.time).toString()
+            }
+            DigitalTimespec.DATE_FORMAT -> {
+                dateFormat.format(cal.time).toString().uppercase()
+            }
+        }
+    }
+
+    fun getContentDescription(): String? {
+        return when (timespec) {
+            DigitalTimespec.TIME_FULL_FORMAT,
+            DigitalTimespec.DATE_FORMAT -> {
+                contentDescriptionFormat?.format(cal.time).toString()
+            }
+            else -> {
+                return null
+            }
+        }
+    }
+
+    companion object {
+        const val CONTENT_DESCRIPTION_TIME_FORMAT_12_HOUR = "hh:mm"
+        const val CONTENT_DESCRIPTION_TIME_FORMAT_24_HOUR = "HH:mm"
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSliceViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSliceViewControllerTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/keyguard/KeyguardSliceViewControllerTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSliceViewControllerTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlaySuppressionControllerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/ClipboardOverlaySuppressionControllerImplTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlaySuppressionControllerImplTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/ClipboardOverlaySuppressionControllerImplTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
index 179ba22..cecc11e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
@@ -19,7 +19,6 @@
 import android.appwidget.AppWidgetProviderInfo
 import android.content.ActivityNotFoundException
 import android.content.ComponentName
-import android.content.Intent
 import android.content.pm.PackageManager
 import android.content.pm.UserInfo
 import android.provider.Settings
@@ -27,7 +26,6 @@
 import android.view.accessibility.AccessibilityManager
 import android.view.accessibility.accessibilityManager
 import android.widget.RemoteViews
-import androidx.activity.result.ActivityResultLauncher
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.UiEventLogger
@@ -88,7 +86,6 @@
     @Mock private lateinit var mediaHost: MediaHost
     @Mock private lateinit var uiEventLogger: UiEventLogger
     @Mock private lateinit var packageManager: PackageManager
-    @Mock private lateinit var activityResultLauncher: ActivityResultLauncher<Intent>
     @Mock private lateinit var metricsLogger: CommunalMetricsLogger
 
     private val kosmos = testKosmos()
@@ -117,10 +114,7 @@
         communalSceneInteractor = kosmos.communalSceneInteractor
         communalInteractor = spy(kosmos.communalInteractor)
         kosmos.fakeUserRepository.setUserInfos(listOf(MAIN_USER_INFO))
-        kosmos.fakeUserTracker.set(
-            userInfos = listOf(MAIN_USER_INFO),
-            selectedUserIndex = 0,
-        )
+        kosmos.fakeUserTracker.set(userInfos = listOf(MAIN_USER_INFO), selectedUserIndex = 0)
         kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true)
         accessibilityManager = kosmos.accessibilityManager
 
@@ -257,10 +251,13 @@
     @Test
     fun onOpenWidgetPicker_launchesWidgetPickerActivity() {
         testScope.runTest {
+            var activityStarted = false
             val success =
-                underTest.onOpenWidgetPicker(testableResources.resources, activityResultLauncher)
+                underTest.onOpenWidgetPicker(testableResources.resources) { _ ->
+                    run { activityStarted = true }
+                }
 
-            verify(activityResultLauncher).launch(any())
+            assertTrue(activityStarted)
             assertTrue(success)
         }
     }
@@ -268,14 +265,10 @@
     @Test
     fun onOpenWidgetPicker_activityLaunchThrowsException_failure() {
         testScope.runTest {
-            whenever(activityResultLauncher.launch(any()))
-                .thenThrow(ActivityNotFoundException::class.java)
-
             val success =
-                underTest.onOpenWidgetPicker(
-                    testableResources.resources,
-                    activityResultLauncher,
-                )
+                underTest.onOpenWidgetPicker(testableResources.resources) { _ ->
+                    run { throw ActivityNotFoundException() }
+                }
 
             assertFalse(success)
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/lifecycle/HydratorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/HydratorTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/lifecycle/HydratorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/HydratorTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarButtonTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarButtonTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarButtonTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarButtonTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/data/model/ScreenRecordModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/data/model/ScreenRecordModelTest.kt
index 9331c8d..0bbf47c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/data/model/ScreenRecordModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/data/model/ScreenRecordModelTest.kt
@@ -16,13 +16,16 @@
 
 package com.android.systemui.screenrecord.data.model
 
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.screenrecord.data.model.ScreenRecordModel.Starting.Companion.toCountdownSeconds
 import com.google.common.truth.Truth.assertThat
+import org.junit.runner.RunWith
 import kotlin.test.Test
 
 @SmallTest
+@RunWith(AndroidJUnit4::class)
 class ScreenRecordModelTest : SysuiTestCase() {
     @Test
     fun countdownSeconds_millis0_is0() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDataTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/ScreenshotDataTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDataTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/ScreenshotDataTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index 0e9ef06..0454317 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -22,6 +22,10 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static kotlinx.coroutines.flow.FlowKt.emptyFlow;
+import static kotlinx.coroutines.flow.SharedFlowKt.MutableSharedFlow;
+import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow;
+
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyFloat;
@@ -36,10 +40,6 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import static kotlinx.coroutines.flow.FlowKt.emptyFlow;
-import static kotlinx.coroutines.flow.SharedFlowKt.MutableSharedFlow;
-import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow;
-
 import android.animation.Animator;
 import android.annotation.IdRes;
 import android.content.ContentResolver;
@@ -201,6 +201,12 @@
 import com.android.systemui.util.time.SystemClock;
 import com.android.wm.shell.animation.FlingAnimationUtils;
 
+import dagger.Lazy;
+
+import kotlinx.coroutines.CoroutineDispatcher;
+import kotlinx.coroutines.channels.BufferOverflow;
+import kotlinx.coroutines.test.TestScope;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
@@ -215,11 +221,6 @@
 import java.util.List;
 import java.util.Optional;
 
-import dagger.Lazy;
-import kotlinx.coroutines.CoroutineDispatcher;
-import kotlinx.coroutines.channels.BufferOverflow;
-import kotlinx.coroutines.test.TestScope;
-
 public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {
 
     protected static final int SPLIT_SHADE_FULL_TRANSITION_DISTANCE = 400;
@@ -461,7 +462,8 @@
                 () -> mKosmos.getSceneInteractor(),
                 () -> mKosmos.getSceneContainerOcclusionInteractor(),
                 () -> mKosmos.getKeyguardClockInteractor(),
-                () -> mKosmos.getSceneBackInteractor());
+                () -> mKosmos.getSceneBackInteractor(),
+                () -> mKosmos.getAlternateBouncerInteractor());
 
         KeyguardStatusView keyguardStatusView = new KeyguardStatusView(mContext);
         keyguardStatusView.setId(R.id.keyguard_status_view);
@@ -622,7 +624,8 @@
                                 () -> mKosmos.getSceneInteractor(),
                                 () -> mKosmos.getSceneContainerOcclusionInteractor(),
                                 () -> mKosmos.getKeyguardClockInteractor(),
-                                () -> mKosmos.getSceneBackInteractor()),
+                                () -> mKosmos.getSceneBackInteractor(),
+                                () -> mKosmos.getAlternateBouncerInteractor()),
                         mKeyguardBypassController,
                         mDozeParameters,
                         mScreenOffAnimationController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/OperatorNameViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/OperatorNameViewControllerTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/OperatorNameViewControllerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/OperatorNameViewControllerTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
index db274cc..f8720b4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
@@ -28,6 +28,8 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.givenCanShowAlternateBouncer
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
 import com.android.systemui.flags.DisableSceneContainer
@@ -83,8 +85,9 @@
 
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
-    private val sceneInteractor = kosmos.sceneInteractor
-    private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+    private val sceneInteractor by lazy { kosmos.sceneInteractor }
+    private val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository }
+    private val alternateBouncerInteractor by lazy { kosmos.alternateBouncerInteractor }
     private val mockDarkAnimator = mock<ObjectAnimator>()
 
     private lateinit var underTest: StatusBarStateControllerImpl
@@ -121,6 +124,7 @@
                     { kosmos.sceneContainerOcclusionInteractor },
                     { kosmos.keyguardClockInteractor },
                     { kosmos.sceneBackInteractor },
+                    { kosmos.alternateBouncerInteractor },
                 ) {
                 override fun createDarkAnimator(): ObjectAnimator {
                     return mockDarkAnimator
@@ -299,6 +303,52 @@
 
     @Test
     @EnableSceneContainer
+    @DisableFlags(DualShade.FLAG_NAME)
+    fun start_hydratesStatusBarState_withAlternateBouncer() =
+        testScope.runTest {
+            var statusBarState = underTest.state
+            val listener =
+                object : StatusBarStateController.StateListener {
+                    override fun onStateChanged(newState: Int) {
+                        statusBarState = newState
+                    }
+                }
+            underTest.addCallback(listener)
+
+            val currentScene by collectLastValue(sceneInteractor.currentScene)
+            val deviceUnlockStatus by
+                collectLastValue(kosmos.deviceUnlockedInteractor.deviceUnlockStatus)
+            val alternateBouncerIsVisible by collectLastValue(alternateBouncerInteractor.isVisible)
+
+            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Password
+            )
+            kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+                SuccessFingerprintAuthenticationStatus(0, true)
+            )
+            runCurrent()
+            assertThat(deviceUnlockStatus!!.isUnlocked).isTrue()
+
+            sceneInteractor.changeScene(toScene = Scenes.Lockscreen, loggingReason = "reason")
+            runCurrent()
+            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+
+            kosmos.givenCanShowAlternateBouncer()
+            alternateBouncerInteractor.forceShow()
+            runCurrent()
+            assertThat(alternateBouncerIsVisible).isTrue()
+
+            // Call start to begin hydrating based on the scene framework:
+            underTest.start()
+
+            sceneInteractor.changeScene(toScene = Scenes.Gone, loggingReason = "reason")
+            runCurrent()
+            assertThat(currentScene).isEqualTo(Scenes.Gone)
+            assertThat(statusBarState).isEqualTo(StatusBarState.KEYGUARD)
+        }
+
+    @Test
+    @EnableSceneContainer
     @EnableFlags(DualShade.FLAG_NAME)
     fun start_hydratesStatusBarState_dualShade_whileLocked() =
         testScope.runTest {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractorTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractorTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/domian/interactor/MediaRouterChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/domian/interactor/MediaRouterChipInteractorTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/domian/interactor/MediaRouterChipInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/domian/interactor/MediaRouterChipInteractorTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithRonsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithRonsViewModelTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithRonsViewModelTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithRonsViewModelTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/AboveShelfObserverTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/AboveShelfObserverTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/AboveShelfObserverTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/AboveShelfObserverTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorControllerTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorControllerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationSnoozeTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationSnoozeTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationSnoozeTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationSnoozeTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCustomViewWrapperTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCustomViewWrapperTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCustomViewWrapperTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCustomViewWrapperTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapperTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapperTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapperTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapperTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapperTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapperTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapperTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarTransitionsControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LightBarTransitionsControllerTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarTransitionsControllerTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LightBarTransitionsControllerTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialogTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialogTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialogTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialogTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastDeviceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/CastDeviceTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastDeviceTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/CastDeviceTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt
index ba9fa92..cd18925 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NotStarted
 import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.SWIPE_DISTANCE
 import com.google.common.truth.Truth.assertThat
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -34,10 +35,12 @@
 
     private var gestureState: GestureState = NotStarted
     private val gestureMonitor =
-        BackGestureMonitor(
-            gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt(),
-            gestureStateChangedCallback = { gestureState = it },
-        )
+        BackGestureMonitor(gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt())
+
+    @Before
+    fun before() {
+        gestureMonitor.addGestureStateCallback { gestureState = it }
+    }
 
     @Test
     fun triggersGestureFinishedForThreeFingerGestureRight() {
@@ -82,7 +85,7 @@
     }
 
     private fun assertStateAfterEvents(events: List<MotionEvent>, expectedState: GestureState) {
-        events.forEach { gestureMonitor.processTouchpadEvent(it) }
+        events.forEach { gestureMonitor.accept(it) }
         assertThat(gestureState).isEqualTo(expectedState)
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt
index a83ed56..3f1633a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt
@@ -36,10 +36,7 @@
     private var triggered = false
     private val handler =
         TouchpadGestureHandler(
-            BackGestureMonitor(
-                gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt(),
-                gestureStateChangedCallback = {},
-            ),
+            BackGestureMonitor(gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt()),
             EasterEggGestureMonitor(callback = { triggered = true }),
         )
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitorTest.kt
index 59cc026..edf0e56 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitorTest.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NotStarted
 import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.SWIPE_DISTANCE
 import com.google.common.truth.Truth.assertThat
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -34,10 +35,12 @@
 
     private var gestureState: GestureState = GestureState.NotStarted
     private val gestureMonitor =
-        HomeGestureMonitor(
-            gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt(),
-            gestureStateChangedCallback = { gestureState = it },
-        )
+        HomeGestureMonitor(gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt())
+
+    @Before
+    fun before() {
+        gestureMonitor.addGestureStateCallback { gestureState = it }
+    }
 
     @Test
     fun triggersGestureFinishedForThreeFingerGestureUp() {
@@ -78,7 +81,7 @@
     }
 
     private fun assertStateAfterEvents(events: List<MotionEvent>, expectedState: GestureState) {
-        events.forEach { gestureMonitor.processTouchpadEvent(it) }
+        events.forEach { gestureMonitor.accept(it) }
         assertThat(gestureState).isEqualTo(expectedState)
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitorTest.kt
index 7eac6bb..f68e773 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitorTest.kt
@@ -26,6 +26,7 @@
 import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NotStarted
 import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.SWIPE_DISTANCE
 import com.google.common.truth.Truth.assertThat
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.kotlin.doReturn
@@ -44,7 +45,7 @@
     }
 
     private var gestureState: GestureState = GestureState.NotStarted
-    private val velocityTracker =
+    private val velocityTracker1D =
         mock<VelocityTracker1D> {
             // by default return correct speed for the gesture - as if pointer is slowing down
             on { calculateVelocity() } doReturn SLOW
@@ -52,11 +53,15 @@
     private val gestureMonitor =
         RecentAppsGestureMonitor(
             gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt(),
-            gestureStateChangedCallback = { gestureState = it },
             velocityThresholdPxPerMs = THRESHOLD_VELOCITY_PX_PER_MS,
-            velocityTracker = velocityTracker,
+            velocityTracker = VerticalVelocityTracker(velocityTracker1D),
         )
 
+    @Before
+    fun before() {
+        gestureMonitor.addGestureStateCallback { gestureState = it }
+    }
+
     @Test
     fun triggersGestureFinishedForThreeFingerGestureUp() {
         assertStateAfterEvents(events = ThreeFingerGesture.swipeUp(), expectedState = Finished)
@@ -64,7 +69,7 @@
 
     @Test
     fun doesntTriggerGestureFinished_onGestureSpeedTooHigh() {
-        whenever(velocityTracker.calculateVelocity()).thenReturn(FAST)
+        whenever(velocityTracker1D.calculateVelocity()).thenReturn(FAST)
         assertStateAfterEvents(events = ThreeFingerGesture.swipeUp(), expectedState = NotStarted)
     }
 
@@ -102,7 +107,7 @@
     }
 
     private fun assertStateAfterEvents(events: List<MotionEvent>, expectedState: GestureState) {
-        events.forEach { gestureMonitor.processTouchpadEvent(it) }
+        events.forEach { gestureMonitor.accept(it) }
         assertThat(gestureState).isEqualTo(expectedState)
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt
index 4d26366..9f7ea679 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.SWIPE_DISTANCE
 import com.google.common.truth.Truth.assertThat
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -35,14 +36,14 @@
 class TouchpadGestureHandlerTest : SysuiTestCase() {
 
     private var gestureState: GestureState = GestureState.NotStarted
-    private val handler =
-        TouchpadGestureHandler(
-            BackGestureMonitor(
-                gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt(),
-                gestureStateChangedCallback = { gestureState = it },
-            ),
-            EasterEggGestureMonitor {},
-        )
+    private val gestureMonitor =
+        BackGestureMonitor(gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt())
+    private val handler = TouchpadGestureHandler(gestureMonitor, EasterEggGestureMonitor {})
+
+    @Before
+    fun before() {
+        gestureMonitor.addGestureStateCallback { gestureState = it }
+    }
 
     @Test
     fun handlesEventsFromTouchpad() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java
index b1736b1..c09509d 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java
@@ -14,7 +14,6 @@
 
 package com.android.systemui.plugins;
 
-import android.annotation.IntegerRes;
 import android.content.ComponentName;
 import android.media.AudioManager;
 import android.media.AudioSystem;
@@ -22,6 +21,8 @@
 import android.os.VibrationEffect;
 import android.util.SparseArray;
 
+import androidx.annotation.StringRes;
+
 import com.android.systemui.plugins.VolumeDialogController.Callbacks;
 import com.android.systemui.plugins.VolumeDialogController.State;
 import com.android.systemui.plugins.VolumeDialogController.StreamState;
@@ -90,7 +91,7 @@
         public int levelMax;
         public boolean muted;
         public boolean muteSupported;
-        public @IntegerRes int name;
+        public @StringRes int name;
         public String remoteLabel;
         public boolean routedToBluetooth;
 
diff --git a/packages/SystemUI/res/drawable/volume_dialog_background.xml b/packages/SystemUI/res/drawable/volume_dialog_background.xml
new file mode 100644
index 0000000..7d7498f
--- /dev/null
+++ b/packages/SystemUI/res/drawable/volume_dialog_background.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+    Copyright (C) 2024 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+
+<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/volume_dialog_background_corner_radius" />
+    <solid android:color="?androidprv:attr/materialColorSurface" />
+</shape>
diff --git a/packages/SystemUI/res/drawable/volume_dialog_floating_slider_background.xml b/packages/SystemUI/res/drawable/volume_dialog_floating_slider_background.xml
new file mode 100644
index 0000000..2694435
--- /dev/null
+++ b/packages/SystemUI/res/drawable/volume_dialog_floating_slider_background.xml
@@ -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.
+  -->
+<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="20dp" />
+    <solid android:color="?androidprv:attr/colorSurface" />
+</shape>
diff --git a/packages/SystemUI/res/drawable/volume_dialog_floating_sliders_spacer.xml b/packages/SystemUI/res/drawable/volume_dialog_floating_sliders_spacer.xml
new file mode 100644
index 0000000..66a205a
--- /dev/null
+++ b/packages/SystemUI/res/drawable/volume_dialog_floating_sliders_spacer.xml
@@ -0,0 +1,23 @@
+<?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.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <size
+        android:width="@dimen/volume_dialog_floating_sliders_spacing"
+        android:height="@dimen/volume_dialog_floating_sliders_spacing" />
+    <solid android:color="@color/transparent" />
+</shape>
diff --git a/packages/SystemUI/res/drawable/volume_dialog_spacer.xml b/packages/SystemUI/res/drawable/volume_dialog_spacer.xml
new file mode 100644
index 0000000..3c60784
--- /dev/null
+++ b/packages/SystemUI/res/drawable/volume_dialog_spacer.xml
@@ -0,0 +1,23 @@
+<?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.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <size
+        android:width="@dimen/volume_dialog_spacing"
+        android:height="@dimen/volume_dialog_spacing" />
+    <solid android:color="@color/transparent" />
+</shape>
diff --git a/packages/SystemUI/res/drawable/volume_row_seekbar_progress.xml b/packages/SystemUI/res/drawable/volume_row_seekbar_progress.xml
index 21b177b..fa06bd6 100644
--- a/packages/SystemUI/res/drawable/volume_row_seekbar_progress.xml
+++ b/packages/SystemUI/res/drawable/volume_row_seekbar_progress.xml
@@ -22,7 +22,7 @@
     android:autoMirrored="true">
     <item android:id="@+id/volume_seekbar_progress_solid">
         <shape>
-            <size android:height="@dimen/volume_dialog_slider_width" />
+            <size android:height="@dimen/volume_dialog_slider_width_legacy" />
             <solid android:color="?android:attr/colorAccent" />
             <corners android:radius="@dimen/volume_dialog_slider_corner_radius"/>
         </shape>
diff --git a/packages/SystemUI/res/layout-land-television/volume_dialog.xml b/packages/SystemUI/res/layout-land-television/volume_dialog.xml
index 0fbc519..f77db95 100644
--- a/packages/SystemUI/res/layout-land-television/volume_dialog.xml
+++ b/packages/SystemUI/res/layout-land-television/volume_dialog.xml
@@ -1,92 +1,67 @@
 <!--
-  ~ 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
-  -->
-<FrameLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:sysui="http://schemas.android.com/apk/res-auto"
+     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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     android:id="@+id/volume_dialog_container"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
-    android:background="@android:color/transparent"
+    android:layout_gravity="right"
+    android:divider="@drawable/volume_dialog_floating_sliders_spacer"
+    android:orientation="horizontal"
+    android:showDividers="middle|end|beginning"
     android:theme="@style/volume_dialog_theme">
 
-    <FrameLayout
-        android:id="@+id/volume_dialog"
+    <LinearLayout
+        android:id="@+id/volume_dialog_floating_sliders_container"
         android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:background="@drawable/volume_dialog_background"
+        android:divider="@drawable/volume_dialog_floating_sliders_spacer"
+        android:gravity="bottom"
+        android:orientation="horizontal"
+        android:paddingBottom="@dimen/volume_dialog_floating_sliders_bottom_padding"
+        android:showDividers="middle" />
+
+    <LinearLayout
+        android:layout_width="@dimen/volume_dialog_width"
         android:layout_height="wrap_content"
-        android:layout_gravity="right"
-        android:background="@android:color/transparent"
-        android:padding="@dimen/volume_dialog_panel_transparent_padding"
-        android:clipToPadding="false">
-
-        <LinearLayout
-            android:id="@+id/volume_dialog_rows_container"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_gravity="right"
-            android:orientation="vertical"
-            android:translationZ="@dimen/volume_dialog_elevation"
-            android:clipChildren="false"
-            android:clipToPadding="false"
-            android:background="@android:color/transparent">
-
-            <LinearLayout
-                android:id="@+id/volume_dialog_rows"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:gravity="center"
-                android:orientation="horizontal"
-                android:background="@drawable/tv_volume_dialog_background">
-                <!-- volume rows added and removed here! :-) -->
-            </LinearLayout>
-
-        </LinearLayout>
+        android:background="@drawable/volume_dialog_background"
+        android:divider="@drawable/volume_dialog_spacer"
+        android:gravity="center_horizontal"
+        android:orientation="vertical"
+        android:paddingVertical="@dimen/volume_dialog_vertical_padding"
+        android:showDividers="middle">
 
         <FrameLayout
-            android:id="@+id/odi_captions"
-            android:layout_width="@dimen/volume_dialog_caption_size"
-            android:layout_height="@dimen/volume_dialog_caption_size"
-            android:layout_marginRight="68dp"
-            android:layout_gravity="right"
-            android:clipToPadding="false"
-            android:translationZ="@dimen/volume_dialog_elevation"
-            android:background="@drawable/rounded_bg_full">
+            android:id="@+id/volume_dialog_ringer_button"
+            android:layout_width="@dimen/volume_dialog_button_size"
+            android:layout_height="@dimen/volume_dialog_button_size" />
 
-            <com.android.systemui.volume.CaptionsToggleImageButton
-                android:id="@+id/odi_captions_icon"
-                android:src="@drawable/ic_volume_odi_captions_disabled"
-                style="@style/VolumeButtons"
-                android:background="@drawable/rounded_ripple"
-                android:layout_width="match_parent"
-                android:layout_height="match_parent"
-                android:tint="@color/caption_tint_color_selector"
-                android:layout_gravity="center"
-                android:soundEffectsEnabled="false"/>
+        <include
+            android:id="@+id/volume_dialog_slider"
+            layout="@layout/volume_dialog_slider" />
 
-        </FrameLayout>
-
-        <ViewStub
-            android:id="@+id/odi_captions_tooltip_stub"
-            android:inflatedId="@+id/odi_captions_tooltip_view"
-            android:layout="@layout/volume_tool_tip_view"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_marginRight="@dimen/volume_tool_tip_right_margin"
-            android:layout_marginTop="@dimen/volume_tool_tip_top_margin"
-            android:layout_gravity="right"/>
-
-    </FrameLayout>
-
-</FrameLayout>
+        <Button
+            android:id="@+id/volume_dialog_settings"
+            android:layout_width="@dimen/volume_dialog_button_size"
+            android:layout_height="@dimen/volume_dialog_button_size"
+            android:background="@drawable/ripple_drawable_20dp"
+            android:contentDescription="@string/accessibility_volume_settings"
+            android:soundEffectsEnabled="false"
+            android:src="@drawable/horizontal_ellipsis"
+            android:tint="?androidprv:attr/materialColorPrimary" />
+    </LinearLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout-land-television/volume_dialog_legacy.xml b/packages/SystemUI/res/layout-land-television/volume_dialog_legacy.xml
new file mode 100644
index 0000000..0fbc519
--- /dev/null
+++ b/packages/SystemUI/res/layout-land-television/volume_dialog_legacy.xml
@@ -0,0 +1,92 @@
+<!--
+  ~ 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
+  -->
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:sysui="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/volume_dialog_container"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:background="@android:color/transparent"
+    android:theme="@style/volume_dialog_theme">
+
+    <FrameLayout
+        android:id="@+id/volume_dialog"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="right"
+        android:background="@android:color/transparent"
+        android:padding="@dimen/volume_dialog_panel_transparent_padding"
+        android:clipToPadding="false">
+
+        <LinearLayout
+            android:id="@+id/volume_dialog_rows_container"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="right"
+            android:orientation="vertical"
+            android:translationZ="@dimen/volume_dialog_elevation"
+            android:clipChildren="false"
+            android:clipToPadding="false"
+            android:background="@android:color/transparent">
+
+            <LinearLayout
+                android:id="@+id/volume_dialog_rows"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:gravity="center"
+                android:orientation="horizontal"
+                android:background="@drawable/tv_volume_dialog_background">
+                <!-- volume rows added and removed here! :-) -->
+            </LinearLayout>
+
+        </LinearLayout>
+
+        <FrameLayout
+            android:id="@+id/odi_captions"
+            android:layout_width="@dimen/volume_dialog_caption_size"
+            android:layout_height="@dimen/volume_dialog_caption_size"
+            android:layout_marginRight="68dp"
+            android:layout_gravity="right"
+            android:clipToPadding="false"
+            android:translationZ="@dimen/volume_dialog_elevation"
+            android:background="@drawable/rounded_bg_full">
+
+            <com.android.systemui.volume.CaptionsToggleImageButton
+                android:id="@+id/odi_captions_icon"
+                android:src="@drawable/ic_volume_odi_captions_disabled"
+                style="@style/VolumeButtons"
+                android:background="@drawable/rounded_ripple"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:tint="@color/caption_tint_color_selector"
+                android:layout_gravity="center"
+                android:soundEffectsEnabled="false"/>
+
+        </FrameLayout>
+
+        <ViewStub
+            android:id="@+id/odi_captions_tooltip_stub"
+            android:inflatedId="@+id/odi_captions_tooltip_view"
+            android:layout="@layout/volume_tool_tip_view"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginRight="@dimen/volume_tool_tip_right_margin"
+            android:layout_marginTop="@dimen/volume_tool_tip_top_margin"
+            android:layout_gravity="right"/>
+
+    </FrameLayout>
+
+</FrameLayout>
diff --git a/packages/SystemUI/res/layout-land-television/volume_dialog_row.xml b/packages/SystemUI/res/layout-land-television/volume_dialog_row.xml
index cf301c9..eb89489 100644
--- a/packages/SystemUI/res/layout-land-television/volume_dialog_row.xml
+++ b/packages/SystemUI/res/layout-land-television/volume_dialog_row.xml
@@ -63,8 +63,8 @@
                 android:layout_height="match_parent"
                 android:layout_gravity="center"
                 android:layoutDirection="ltr"
-                android:maxHeight="@dimen/volume_dialog_slider_width"
-                android:minHeight="@dimen/volume_dialog_slider_width"
+                android:maxHeight="@dimen/volume_dialog_slider_width_legacy"
+                android:minHeight="@dimen/volume_dialog_slider_width_legacy"
                 android:progressDrawable="@drawable/volume_row_seekbar"
                 android:thumb="@drawable/tv_volume_row_seek_thumb"
                 android:splitTrack="false"
diff --git a/packages/SystemUI/res/layout-land/volume_dialog.xml b/packages/SystemUI/res/layout-land/volume_dialog.xml
index 08edf59..f77db95 100644
--- a/packages/SystemUI/res/layout-land/volume_dialog.xml
+++ b/packages/SystemUI/res/layout-land/volume_dialog.xml
@@ -1,146 +1,67 @@
 <!--
-  ~ 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
-  -->
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+     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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     android:id="@+id/volume_dialog_container"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
-    android:gravity="right"
     android:layout_gravity="right"
-    android:background="@android:color/transparent"
+    android:divider="@drawable/volume_dialog_floating_sliders_spacer"
+    android:orientation="horizontal"
+    android:showDividers="middle|end|beginning"
     android:theme="@style/volume_dialog_theme">
 
-    <!-- right-aligned to be physically near volume button -->
     <LinearLayout
-        android:id="@+id/volume_dialog"
+        android:id="@+id/volume_dialog_floating_sliders_container"
         android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:background="@drawable/volume_dialog_background"
+        android:divider="@drawable/volume_dialog_floating_sliders_spacer"
+        android:gravity="bottom"
+        android:orientation="horizontal"
+        android:paddingBottom="@dimen/volume_dialog_floating_sliders_bottom_padding"
+        android:showDividers="middle" />
+
+    <LinearLayout
+        android:layout_width="@dimen/volume_dialog_width"
         android:layout_height="wrap_content"
-        android:gravity="right"
-        android:layout_gravity="right"
-        android:layout_marginRight="@dimen/volume_dialog_panel_transparent_padding_right"
+        android:background="@drawable/volume_dialog_background"
+        android:divider="@drawable/volume_dialog_spacer"
+        android:gravity="center_horizontal"
         android:orientation="vertical"
-        android:clipToPadding="false"
-        android:clipChildren="false">
-
-
-        <LinearLayout
-            android:id="@+id/volume_dialog_top_container"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:orientation="vertical"
-            android:clipChildren="false"
-            android:gravity="right">
-
-            <include layout="@layout/volume_ringer_drawer" />
-
-            <FrameLayout
-                android:visibility="gone"
-                android:id="@+id/ringer"
-                android:layout_width="@dimen/volume_dialog_ringer_size"
-                android:layout_height="@dimen/volume_dialog_ringer_size"
-                android:layout_marginBottom="@dimen/volume_dialog_spacer"
-                android:gravity="right"
-                android:layout_gravity="right"
-                android:translationZ="@dimen/volume_dialog_elevation"
-                android:clipToPadding="false"
-                android:background="@drawable/rounded_bg_full">
-                <com.android.keyguard.AlphaOptimizedImageButton
-                    android:id="@+id/ringer_icon"
-                    style="@style/VolumeButtons"
-                    android:background="@drawable/rounded_ripple"
-                    android:layout_width="match_parent"
-                    android:layout_height="match_parent"
-                    android:scaleType="fitCenter"
-                    android:padding="@dimen/volume_dialog_ringer_icon_padding"
-                    android:tint="?android:attr/textColorPrimary"
-                    android:layout_gravity="center"
-                    android:soundEffectsEnabled="false" />
-            </FrameLayout>
-
-            <LinearLayout
-                android:id="@+id/volume_dialog_rows_container"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:gravity="right"
-                android:layout_gravity="right"
-                android:orientation="vertical"
-                android:clipChildren="false"
-                android:clipToPadding="false" >
-                <LinearLayout
-                    android:id="@+id/volume_dialog_rows"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:gravity="center"
-                    android:orientation="horizontal">
-                    <!-- volume rows added and removed here! :-) -->
-                </LinearLayout>
-                <FrameLayout
-                    android:id="@+id/settings_container"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:background="@drawable/volume_background_bottom"
-                    android:paddingLeft="@dimen/volume_dialog_ringer_rows_padding"
-                    android:paddingBottom="@dimen/volume_dialog_ringer_rows_padding"
-                    android:paddingRight="@dimen/volume_dialog_ringer_rows_padding">
-
-                    <com.android.keyguard.AlphaOptimizedImageButton
-                        android:id="@+id/settings"
-                        android:layout_width="@dimen/volume_dialog_tap_target_size"
-                        android:layout_height="@dimen/volume_dialog_tap_target_size"
-                        android:layout_gravity="center"
-                        android:background="@drawable/ripple_drawable_20dp"
-                        android:contentDescription="@string/accessibility_volume_settings"
-                        android:scaleType="centerInside"
-                        android:soundEffectsEnabled="false"
-                        android:src="@drawable/horizontal_ellipsis"
-                        android:tint="?androidprv:attr/colorAccent" />
-                </FrameLayout>
-            </LinearLayout>
-
-        </LinearLayout>
+        android:paddingVertical="@dimen/volume_dialog_vertical_padding"
+        android:showDividers="middle">
 
         <FrameLayout
-            android:id="@+id/odi_captions"
-            android:layout_width="@dimen/volume_dialog_caption_size"
-            android:layout_height="@dimen/volume_dialog_caption_size"
-            android:layout_marginTop="@dimen/volume_dialog_row_margin_bottom"
-            android:gravity="right"
-            android:layout_gravity="right"
-            android:clipToPadding="false"
-            android:clipToOutline="true"
-            android:background="@drawable/volume_row_rounded_background">
-            <com.android.systemui.volume.CaptionsToggleImageButton
-                android:id="@+id/odi_captions_icon"
-                android:src="@drawable/ic_volume_odi_captions_disabled"
-                style="@style/VolumeButtons"
-                android:layout_width="match_parent"
-                android:layout_height="match_parent"
-                android:tint="?android:attr/colorAccent"
-                android:layout_gravity="center"
-                android:soundEffectsEnabled="false" />
-        </FrameLayout>
+            android:id="@+id/volume_dialog_ringer_button"
+            android:layout_width="@dimen/volume_dialog_button_size"
+            android:layout_height="@dimen/volume_dialog_button_size" />
+
+        <include
+            android:id="@+id/volume_dialog_slider"
+            layout="@layout/volume_dialog_slider" />
+
+        <Button
+            android:id="@+id/volume_dialog_settings"
+            android:layout_width="@dimen/volume_dialog_button_size"
+            android:layout_height="@dimen/volume_dialog_button_size"
+            android:background="@drawable/ripple_drawable_20dp"
+            android:contentDescription="@string/accessibility_volume_settings"
+            android:soundEffectsEnabled="false"
+            android:src="@drawable/horizontal_ellipsis"
+            android:tint="?androidprv:attr/materialColorPrimary" />
     </LinearLayout>
-
-    <ViewStub
-        android:id="@+id/odi_captions_tooltip_stub"
-        android:inflatedId="@+id/odi_captions_tooltip_view"
-        android:layout="@layout/volume_tool_tip_view"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_gravity="bottom | right"
-        android:layout_marginRight="@dimen/volume_tool_tip_right_margin"/>
-
-</FrameLayout>
\ No newline at end of file
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout-land/volume_dialog_legacy.xml b/packages/SystemUI/res/layout-land/volume_dialog_legacy.xml
new file mode 100644
index 0000000..08edf59
--- /dev/null
+++ b/packages/SystemUI/res/layout-land/volume_dialog_legacy.xml
@@ -0,0 +1,146 @@
+<!--
+  ~ 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
+  -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    android:id="@+id/volume_dialog_container"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:gravity="right"
+    android:layout_gravity="right"
+    android:background="@android:color/transparent"
+    android:theme="@style/volume_dialog_theme">
+
+    <!-- right-aligned to be physically near volume button -->
+    <LinearLayout
+        android:id="@+id/volume_dialog"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:gravity="right"
+        android:layout_gravity="right"
+        android:layout_marginRight="@dimen/volume_dialog_panel_transparent_padding_right"
+        android:orientation="vertical"
+        android:clipToPadding="false"
+        android:clipChildren="false">
+
+
+        <LinearLayout
+            android:id="@+id/volume_dialog_top_container"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:orientation="vertical"
+            android:clipChildren="false"
+            android:gravity="right">
+
+            <include layout="@layout/volume_ringer_drawer" />
+
+            <FrameLayout
+                android:visibility="gone"
+                android:id="@+id/ringer"
+                android:layout_width="@dimen/volume_dialog_ringer_size"
+                android:layout_height="@dimen/volume_dialog_ringer_size"
+                android:layout_marginBottom="@dimen/volume_dialog_spacer"
+                android:gravity="right"
+                android:layout_gravity="right"
+                android:translationZ="@dimen/volume_dialog_elevation"
+                android:clipToPadding="false"
+                android:background="@drawable/rounded_bg_full">
+                <com.android.keyguard.AlphaOptimizedImageButton
+                    android:id="@+id/ringer_icon"
+                    style="@style/VolumeButtons"
+                    android:background="@drawable/rounded_ripple"
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent"
+                    android:scaleType="fitCenter"
+                    android:padding="@dimen/volume_dialog_ringer_icon_padding"
+                    android:tint="?android:attr/textColorPrimary"
+                    android:layout_gravity="center"
+                    android:soundEffectsEnabled="false" />
+            </FrameLayout>
+
+            <LinearLayout
+                android:id="@+id/volume_dialog_rows_container"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:gravity="right"
+                android:layout_gravity="right"
+                android:orientation="vertical"
+                android:clipChildren="false"
+                android:clipToPadding="false" >
+                <LinearLayout
+                    android:id="@+id/volume_dialog_rows"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:gravity="center"
+                    android:orientation="horizontal">
+                    <!-- volume rows added and removed here! :-) -->
+                </LinearLayout>
+                <FrameLayout
+                    android:id="@+id/settings_container"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:background="@drawable/volume_background_bottom"
+                    android:paddingLeft="@dimen/volume_dialog_ringer_rows_padding"
+                    android:paddingBottom="@dimen/volume_dialog_ringer_rows_padding"
+                    android:paddingRight="@dimen/volume_dialog_ringer_rows_padding">
+
+                    <com.android.keyguard.AlphaOptimizedImageButton
+                        android:id="@+id/settings"
+                        android:layout_width="@dimen/volume_dialog_tap_target_size"
+                        android:layout_height="@dimen/volume_dialog_tap_target_size"
+                        android:layout_gravity="center"
+                        android:background="@drawable/ripple_drawable_20dp"
+                        android:contentDescription="@string/accessibility_volume_settings"
+                        android:scaleType="centerInside"
+                        android:soundEffectsEnabled="false"
+                        android:src="@drawable/horizontal_ellipsis"
+                        android:tint="?androidprv:attr/colorAccent" />
+                </FrameLayout>
+            </LinearLayout>
+
+        </LinearLayout>
+
+        <FrameLayout
+            android:id="@+id/odi_captions"
+            android:layout_width="@dimen/volume_dialog_caption_size"
+            android:layout_height="@dimen/volume_dialog_caption_size"
+            android:layout_marginTop="@dimen/volume_dialog_row_margin_bottom"
+            android:gravity="right"
+            android:layout_gravity="right"
+            android:clipToPadding="false"
+            android:clipToOutline="true"
+            android:background="@drawable/volume_row_rounded_background">
+            <com.android.systemui.volume.CaptionsToggleImageButton
+                android:id="@+id/odi_captions_icon"
+                android:src="@drawable/ic_volume_odi_captions_disabled"
+                style="@style/VolumeButtons"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:tint="?android:attr/colorAccent"
+                android:layout_gravity="center"
+                android:soundEffectsEnabled="false" />
+        </FrameLayout>
+    </LinearLayout>
+
+    <ViewStub
+        android:id="@+id/odi_captions_tooltip_stub"
+        android:inflatedId="@+id/odi_captions_tooltip_view"
+        android:layout="@layout/volume_tool_tip_view"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="bottom | right"
+        android:layout_marginRight="@dimen/volume_tool_tip_right_margin"/>
+
+</FrameLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/audio_sharing_dialog.xml b/packages/SystemUI/res/layout/audio_sharing_dialog.xml
new file mode 100644
index 0000000..7534e15
--- /dev/null
+++ b/packages/SystemUI/res/layout/audio_sharing_dialog.xml
@@ -0,0 +1,115 @@
+<?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.
+  -->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/root"
+    style="@style/Widget.SliceView.Panel"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content">
+
+    <ImageView android:id="@+id/icon"
+        android:layout_width="28dp"
+        android:layout_height="28dp"
+        android:src="@drawable/ic_bt_le_audio_sharing"
+        android:layout_marginTop="5dp"
+        android:layout_marginBottom="20dp"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintBottom_toTopOf="@id/title"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <TextView
+        android:id="@+id/title"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="20dp"
+        android:gravity="center_vertical|center_horizontal"
+        android:maxLines="1"
+        android:ellipsize="end"
+        android:text="@string/quick_settings_bluetooth_audio_sharing_dialog_title"
+        android:textAppearance="@style/TextAppearance.Dialog.Title"
+        android:textSize="24sp"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintBottom_toTopOf="@id/subtitle"
+        app:layout_constraintTop_toBottomOf="@id/icon" />
+
+    <TextView
+        android:id="@+id/subtitle"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="20dp"
+        android:gravity="center_vertical|center_horizontal"
+        android:ellipsize="end"
+        android:maxLines="2"
+        android:textAppearance="@style/TextAppearance.Dialog.Body.Message"
+        android:textFontWeight="500"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintBottom_toTopOf="@id/message"
+        app:layout_constraintTop_toBottomOf="@id/title" />
+
+    <TextView
+        android:id="@+id/message"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="20dp"
+        android:gravity="center_vertical|center_horizontal"
+        android:ellipsize="end"
+        android:maxLines="2"
+        android:text="@string/quick_settings_bluetooth_audio_sharing_dialog_message"
+        android:textAppearance="@style/TextAppearance.Dialog.Body.Message"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintBottom_toTopOf="@id/share_audio_button"
+        app:layout_constraintTop_toBottomOf="@id/subtitle" />
+
+    <Button
+        android:id="@+id/share_audio_button"
+        style="@style/SettingsLibActionButton"
+        android:textColor="?androidprv:attr/textColorOnAccent"
+        android:background="@drawable/audio_sharing_rounded_bg_ripple"
+        android:layout_marginBottom="4dp"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:minHeight="64dp"
+        android:contentDescription="@string/accessibility_bluetooth_device_settings_see_all"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/message"
+        app:layout_constraintBottom_toTopOf="@+id/switch_active_button"
+        android:text="@string/quick_settings_bluetooth_audio_sharing_button"
+        android:maxLines="2" />
+
+    <Button
+        android:id="@+id/switch_active_button"
+        style="@style/SettingsLibActionButton"
+        android:textColor="?androidprv:attr/textColorOnAccent"
+        android:background="@drawable/audio_sharing_rounded_bg_ripple"
+        android:layout_marginBottom="20dp"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:minHeight="64dp"
+        android:contentDescription="@string/accessibility_bluetooth_device_settings_pair_new_device"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/share_audio_button"
+        app:layout_constraintBottom_toBottomOf="parent"
+        android:maxLines="2" />
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/volume_dialog.xml b/packages/SystemUI/res/layout/volume_dialog.xml
index 39a1f1f..f77db95 100644
--- a/packages/SystemUI/res/layout/volume_dialog.xml
+++ b/packages/SystemUI/res/layout/volume_dialog.xml
@@ -1,5 +1,5 @@
 <!--
-     Copyright (C) 2015 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.
@@ -13,133 +13,55 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<FrameLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:sysui="http://schemas.android.com/apk/res-auto"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     android:id="@+id/volume_dialog_container"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
-    android:gravity="right"
     android:layout_gravity="right"
-    android:clipToPadding="false"
+    android:divider="@drawable/volume_dialog_floating_sliders_spacer"
+    android:orientation="horizontal"
+    android:showDividers="middle|end|beginning"
     android:theme="@style/volume_dialog_theme">
 
-    <!-- right-aligned to be physically near volume button -->
     <LinearLayout
-        android:id="@+id/volume_dialog"
+        android:id="@+id/volume_dialog_floating_sliders_container"
         android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:background="@drawable/volume_dialog_background"
+        android:divider="@drawable/volume_dialog_floating_sliders_spacer"
+        android:gravity="bottom"
+        android:orientation="horizontal"
+        android:paddingBottom="@dimen/volume_dialog_floating_sliders_bottom_padding"
+        android:showDividers="middle" />
+
+    <LinearLayout
+        android:layout_width="@dimen/volume_dialog_width"
         android:layout_height="wrap_content"
-        android:gravity="right"
-        android:layout_gravity="right"
-        android:layout_marginRight="@dimen/volume_dialog_panel_transparent_padding_right"
+        android:background="@drawable/volume_dialog_background"
+        android:divider="@drawable/volume_dialog_spacer"
+        android:gravity="center_horizontal"
         android:orientation="vertical"
-        android:clipToPadding="false"
-        android:clipChildren="false">
-
-        <LinearLayout
-            android:id="@+id/volume_dialog_top_container"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:clipChildren="false"
-            android:orientation="vertical"
-            android:gravity="right">
-
-            <include layout="@layout/volume_ringer_drawer" />
-
-            <FrameLayout
-                android:visibility="gone"
-                android:id="@+id/ringer"
-                android:layout_width="@dimen/volume_dialog_ringer_size"
-                android:layout_height="@dimen/volume_dialog_ringer_size"
-                android:layout_marginBottom="@dimen/volume_dialog_spacer"
-                android:gravity="right"
-                android:layout_gravity="right"
-                android:translationZ="@dimen/volume_dialog_elevation"
-                android:clipToPadding="false"
-                android:background="@drawable/rounded_bg_full">
-                <com.android.keyguard.AlphaOptimizedImageButton
-                    android:id="@+id/ringer_icon"
-                    style="@style/VolumeButtons"
-                    android:background="@drawable/rounded_ripple"
-                    android:layout_width="match_parent"
-                    android:layout_height="match_parent"
-                    android:scaleType="fitCenter"
-                    android:padding="@dimen/volume_dialog_ringer_icon_padding"
-                    android:tint="?android:attr/textColorPrimary"
-                    android:layout_gravity="center"
-                    android:soundEffectsEnabled="false" />
-            </FrameLayout>
-
-            <LinearLayout
-                android:id="@+id/volume_dialog_rows_container"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:gravity="right"
-                android:layout_gravity="right"
-                android:orientation="vertical"
-                android:clipChildren="false"
-                android:clipToPadding="false" >
-                <LinearLayout
-                    android:id="@+id/volume_dialog_rows"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:gravity="center"
-                    android:orientation="horizontal">
-                        <!-- volume rows added and removed here! :-) -->
-                </LinearLayout>
-                <FrameLayout
-                    android:id="@+id/settings_container"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:background="@drawable/volume_background_bottom"
-                    android:paddingLeft="@dimen/volume_dialog_ringer_rows_padding"
-                    android:paddingBottom="@dimen/volume_dialog_ringer_rows_padding"
-                    android:paddingRight="@dimen/volume_dialog_ringer_rows_padding">
-                    <com.android.keyguard.AlphaOptimizedImageButton
-                        android:id="@+id/settings"
-                        android:src="@drawable/horizontal_ellipsis"
-                        android:layout_width="@dimen/volume_dialog_tap_target_size"
-                        android:layout_height="@dimen/volume_dialog_tap_target_size"
-                        android:layout_gravity="center"
-                        android:contentDescription="@string/accessibility_volume_settings"
-                        android:background="@drawable/ripple_drawable_20dp"
-                        android:tint="?androidprv:attr/colorAccent"
-                        android:soundEffectsEnabled="false" />
-                </FrameLayout>
-            </LinearLayout>
-
-        </LinearLayout>
+        android:paddingVertical="@dimen/volume_dialog_vertical_padding"
+        android:showDividers="middle">
 
         <FrameLayout
-            android:id="@+id/odi_captions"
-            android:layout_width="@dimen/volume_dialog_caption_size"
-            android:layout_height="@dimen/volume_dialog_caption_size"
-            android:layout_marginTop="@dimen/volume_dialog_row_margin_bottom"
-            android:gravity="right"
-            android:layout_gravity="right"
-            android:clipToPadding="false"
-            android:clipToOutline="true"
-            android:background="@drawable/volume_row_rounded_background">
-            <com.android.systemui.volume.CaptionsToggleImageButton
-                android:id="@+id/odi_captions_icon"
-                android:src="@drawable/ic_volume_odi_captions_disabled"
-                style="@style/VolumeButtons"
-                android:layout_width="match_parent"
-                android:layout_height="match_parent"
-                android:tint="?android:attr/colorAccent"
-                android:layout_gravity="center"
-                android:soundEffectsEnabled="false"/>
-        </FrameLayout>
+            android:id="@+id/volume_dialog_ringer_button"
+            android:layout_width="@dimen/volume_dialog_button_size"
+            android:layout_height="@dimen/volume_dialog_button_size" />
+
+        <include
+            android:id="@+id/volume_dialog_slider"
+            layout="@layout/volume_dialog_slider" />
+
+        <Button
+            android:id="@+id/volume_dialog_settings"
+            android:layout_width="@dimen/volume_dialog_button_size"
+            android:layout_height="@dimen/volume_dialog_button_size"
+            android:background="@drawable/ripple_drawable_20dp"
+            android:contentDescription="@string/accessibility_volume_settings"
+            android:soundEffectsEnabled="false"
+            android:src="@drawable/horizontal_ellipsis"
+            android:tint="?androidprv:attr/materialColorPrimary" />
     </LinearLayout>
-
-    <ViewStub
-        android:id="@+id/odi_captions_tooltip_stub"
-        android:inflatedId="@+id/odi_captions_tooltip_view"
-        android:layout="@layout/volume_tool_tip_view"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_gravity="bottom | right"
-        android:layout_marginRight="@dimen/volume_tool_tip_right_margin"/>
-
-</FrameLayout>
\ No newline at end of file
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/volume_dialog_legacy.xml b/packages/SystemUI/res/layout/volume_dialog_legacy.xml
new file mode 100644
index 0000000..39a1f1f
--- /dev/null
+++ b/packages/SystemUI/res/layout/volume_dialog_legacy.xml
@@ -0,0 +1,145 @@
+<!--
+     Copyright (C) 2015 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.
+-->
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:sysui="http://schemas.android.com/apk/res-auto"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    android:id="@+id/volume_dialog_container"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:gravity="right"
+    android:layout_gravity="right"
+    android:clipToPadding="false"
+    android:theme="@style/volume_dialog_theme">
+
+    <!-- right-aligned to be physically near volume button -->
+    <LinearLayout
+        android:id="@+id/volume_dialog"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:gravity="right"
+        android:layout_gravity="right"
+        android:layout_marginRight="@dimen/volume_dialog_panel_transparent_padding_right"
+        android:orientation="vertical"
+        android:clipToPadding="false"
+        android:clipChildren="false">
+
+        <LinearLayout
+            android:id="@+id/volume_dialog_top_container"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:clipChildren="false"
+            android:orientation="vertical"
+            android:gravity="right">
+
+            <include layout="@layout/volume_ringer_drawer" />
+
+            <FrameLayout
+                android:visibility="gone"
+                android:id="@+id/ringer"
+                android:layout_width="@dimen/volume_dialog_ringer_size"
+                android:layout_height="@dimen/volume_dialog_ringer_size"
+                android:layout_marginBottom="@dimen/volume_dialog_spacer"
+                android:gravity="right"
+                android:layout_gravity="right"
+                android:translationZ="@dimen/volume_dialog_elevation"
+                android:clipToPadding="false"
+                android:background="@drawable/rounded_bg_full">
+                <com.android.keyguard.AlphaOptimizedImageButton
+                    android:id="@+id/ringer_icon"
+                    style="@style/VolumeButtons"
+                    android:background="@drawable/rounded_ripple"
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent"
+                    android:scaleType="fitCenter"
+                    android:padding="@dimen/volume_dialog_ringer_icon_padding"
+                    android:tint="?android:attr/textColorPrimary"
+                    android:layout_gravity="center"
+                    android:soundEffectsEnabled="false" />
+            </FrameLayout>
+
+            <LinearLayout
+                android:id="@+id/volume_dialog_rows_container"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:gravity="right"
+                android:layout_gravity="right"
+                android:orientation="vertical"
+                android:clipChildren="false"
+                android:clipToPadding="false" >
+                <LinearLayout
+                    android:id="@+id/volume_dialog_rows"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:gravity="center"
+                    android:orientation="horizontal">
+                        <!-- volume rows added and removed here! :-) -->
+                </LinearLayout>
+                <FrameLayout
+                    android:id="@+id/settings_container"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:background="@drawable/volume_background_bottom"
+                    android:paddingLeft="@dimen/volume_dialog_ringer_rows_padding"
+                    android:paddingBottom="@dimen/volume_dialog_ringer_rows_padding"
+                    android:paddingRight="@dimen/volume_dialog_ringer_rows_padding">
+                    <com.android.keyguard.AlphaOptimizedImageButton
+                        android:id="@+id/settings"
+                        android:src="@drawable/horizontal_ellipsis"
+                        android:layout_width="@dimen/volume_dialog_tap_target_size"
+                        android:layout_height="@dimen/volume_dialog_tap_target_size"
+                        android:layout_gravity="center"
+                        android:contentDescription="@string/accessibility_volume_settings"
+                        android:background="@drawable/ripple_drawable_20dp"
+                        android:tint="?androidprv:attr/colorAccent"
+                        android:soundEffectsEnabled="false" />
+                </FrameLayout>
+            </LinearLayout>
+
+        </LinearLayout>
+
+        <FrameLayout
+            android:id="@+id/odi_captions"
+            android:layout_width="@dimen/volume_dialog_caption_size"
+            android:layout_height="@dimen/volume_dialog_caption_size"
+            android:layout_marginTop="@dimen/volume_dialog_row_margin_bottom"
+            android:gravity="right"
+            android:layout_gravity="right"
+            android:clipToPadding="false"
+            android:clipToOutline="true"
+            android:background="@drawable/volume_row_rounded_background">
+            <com.android.systemui.volume.CaptionsToggleImageButton
+                android:id="@+id/odi_captions_icon"
+                android:src="@drawable/ic_volume_odi_captions_disabled"
+                style="@style/VolumeButtons"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:tint="?android:attr/colorAccent"
+                android:layout_gravity="center"
+                android:soundEffectsEnabled="false"/>
+        </FrameLayout>
+    </LinearLayout>
+
+    <ViewStub
+        android:id="@+id/odi_captions_tooltip_stub"
+        android:inflatedId="@+id/odi_captions_tooltip_view"
+        android:layout="@layout/volume_tool_tip_view"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="bottom | right"
+        android:layout_marginRight="@dimen/volume_tool_tip_right_margin"/>
+
+</FrameLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/volume_dialog_slider.xml b/packages/SystemUI/res/layout/volume_dialog_slider.xml
new file mode 100644
index 0000000..8acdd39
--- /dev/null
+++ b/packages/SystemUI/res/layout/volume_dialog_slider.xml
@@ -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.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="@dimen/volume_dialog_slider_width"
+    android:layout_height="@dimen/volume_dialog_slider_height">
+
+    <com.google.android.material.slider.Slider
+        android:id="@+id/volume_dialog_slider"
+        android:layout_width="@dimen/volume_dialog_slider_height"
+        android:layout_height="match_parent"
+        android:layout_gravity="center"
+        android:rotation="270"
+        android:theme="@style/Theme.MaterialComponents.DayNight" />
+</FrameLayout>
diff --git a/packages/SystemUI/res/layout/volume_dialog_slider_floating.xml b/packages/SystemUI/res/layout/volume_dialog_slider_floating.xml
new file mode 100644
index 0000000..db800aa
--- /dev/null
+++ b/packages/SystemUI/res/layout/volume_dialog_slider_floating.xml
@@ -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.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:background="@drawable/volume_dialog_floating_slider_background"
+    android:paddingHorizontal="@dimen/volume_dialog_floating_sliders_horizontal_padding"
+    android:paddingVertical="@dimen/volume_dialog_floating_sliders_vertical_padding">
+
+    <include layout="@layout/volume_dialog_slider" />
+</FrameLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 1727a5f..6c8a740 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -591,7 +591,7 @@
 
     <dimen name="volume_dialog_panel_width_half">28dp</dimen>
 
-    <dimen name="volume_dialog_slider_width">42dp</dimen>
+    <dimen name="volume_dialog_slider_width_legacy">42dp</dimen>
 
     <dimen name="volume_dialog_slider_corner_radius">21dp</dimen>
 
@@ -622,10 +622,6 @@
 
     <dimen name="volume_tool_tip_arrow_corner_radius">2dp</dimen>
 
-    <!-- Volume panel slices dimensions -->
-    <dimen name="volume_panel_slice_vertical_padding">8dp</dimen>
-    <dimen name="volume_panel_slice_horizontal_padding">24dp</dimen>
-
     <dimen name="bottom_sheet_corner_radius">28dp</dimen>
 
     <!-- Size of each item in the ringer selector drawer. -->
@@ -2050,4 +2046,22 @@
 
     <dimen name="contextual_edu_dialog_bottom_margin">80dp</dimen>
     <dimen name="contextual_edu_dialog_elevation">2dp</dimen>
+
+    <!-- Volume start -->
+    <dimen name="volume_dialog_background_corner_radius">30dp</dimen>
+    <dimen name="volume_dialog_width">60dp</dimen>
+    <dimen name="volume_dialog_vertical_padding">6dp</dimen>
+    <dimen name="volume_dialog_components_spacing">8dp</dimen>
+    <dimen name="volume_dialog_floating_sliders_spacing">8dp</dimen>
+    <dimen name="volume_dialog_floating_sliders_vertical_padding">10dp</dimen>
+    <dimen name="volume_dialog_floating_sliders_horizontal_padding">4dp</dimen>
+    <dimen name="volume_dialog_spacing">4dp</dimen>
+    <dimen name="volume_dialog_button_size">48dp</dimen>
+    <dimen name="volume_dialog_floating_sliders_bottom_padding">48dp</dimen>
+    <dimen name="volume_dialog_slider_width">52dp</dimen>
+    <dimen name="volume_dialog_slider_height">254dp</dimen>
+
+    <dimen name="volume_panel_slice_vertical_padding">8dp</dimen>
+    <dimen name="volume_panel_slice_horizontal_padding">24dp</dimen>
+    <!-- Volume end -->
 </resources>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 7225061..2c5fb56 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -765,6 +765,14 @@
     <string name="quick_settings_bluetooth_audio_sharing_button_sharing">Sharing audio</string>
     <!-- QuickSettings: Bluetooth dialog audio sharing button text accessibility label. Used as part of the string "Double tap to enter audio sharing settings". [CHAR LIMIT=50]-->
     <string name="quick_settings_bluetooth_audio_sharing_button_accessibility">enter audio sharing settings</string>
+    <!-- QuickSettings: Bluetooth audio sharing dialog message. [CHAR LIMIT=NONE]-->
+    <string name="quick_settings_bluetooth_audio_sharing_dialog_message">This device\'s music and videos will play on both pairs of headphones</string>
+    <!-- QuickSettings: Bluetooth audio sharing dialog title. [CHAR LIMIT=NONE]-->
+    <string name="quick_settings_bluetooth_audio_sharing_dialog_title">Share your audio</string>
+    <!-- QuickSettings: Bluetooth audio sharing dialog subtitle. [CHAR LIMIT=NONE]-->
+    <string name="quick_settings_bluetooth_audio_sharing_dialog_subtitle"><xliff:g id="available_device_name" example="device 1">%1$s</xliff:g> and <xliff:g id="active_device_name" example="device 2">%2$s</xliff:g></string>
+    <!-- QuickSettings: Bluetooth audio sharing dialog button text. [CHAR LIMIT=NONE]-->
+    <string name="quick_settings_bluetooth_audio_sharing_dialog_switch_to_button">Switch to <xliff:g id="available_device_name" example="device 1">%1$s</xliff:g></string>
 
     <!-- QuickSettings: Bluetooth secondary label for the battery level of a connected device [CHAR LIMIT=20]-->
     <string name="quick_settings_bluetooth_secondary_label_battery_level"><xliff:g id="battery_level_as_percentage">%s</xliff:g> battery</string>
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 431f048..83ab524 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -1875,7 +1875,9 @@
                     if (posture == DEVICE_POSTURE_OPENED) {
                         mLogger.d("Posture changed to open - attempting to request active"
                                 + " unlock and run face auth");
-                        getFaceAuthInteractor().onDeviceUnfolded();
+                        if (getFaceAuthInteractor() != null) {
+                            getFaceAuthInteractor().onDeviceUnfolded();
+                        }
                         requestActiveUnlockFromWakeReason(PowerManager.WAKE_REASON_UNFOLD_DEVICE,
                                 false);
                     }
diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java
index 831543d..ef172a1 100644
--- a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java
+++ b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java
@@ -69,7 +69,9 @@
                         layoutInflater,
                         resources,
                         featureFlags.isEnabled(Flags.STEP_CLOCK_ANIMATION),
-                        MigrateClocksToBlueprint.isEnabled()),
+                        MigrateClocksToBlueprint.isEnabled(),
+                        com.android.systemui.Flags.clockReactiveVariants()
+                ),
                 context.getString(R.string.lockscreen_clock_id_fallback),
                 clockBuffers,
                 /* keepAllLoaded = */ false,
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModel.kt
new file mode 100644
index 0000000..a6fb150
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModel.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.bluetooth.qsdialog
+
+import androidx.annotation.StringRes
+import com.android.settingslib.bluetooth.BluetoothUtils
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.res.R
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+
+sealed class AudioSharingButtonState {
+    object Gone : AudioSharingButtonState()
+
+    data class Visible(@StringRes val resId: Int, val isActive: Boolean) :
+        AudioSharingButtonState()
+}
+
+class AudioSharingButtonViewModel
+@AssistedInject
+constructor(
+    private val localBluetoothManager: LocalBluetoothManager?,
+    private val audioSharingInteractor: AudioSharingInteractor,
+    private val bluetoothStateInteractor: BluetoothStateInteractor,
+    private val deviceItemInteractor: DeviceItemInteractor,
+) : ExclusiveActivatable() {
+
+    private val mutableButtonState =
+        MutableStateFlow<AudioSharingButtonState>(AudioSharingButtonState.Gone)
+    /** Flow representing the update of AudioSharingButtonState. */
+    val audioSharingButtonStateUpdate: StateFlow<AudioSharingButtonState> =
+        mutableButtonState.asStateFlow()
+
+    override suspend fun onActivated(): Nothing {
+        combine(
+                bluetoothStateInteractor.bluetoothStateUpdate,
+                deviceItemInteractor.deviceItemUpdate,
+                audioSharingInteractor.isAudioSharingOn
+            ) { bluetoothState, deviceItem, audioSharingOn ->
+                getButtonState(bluetoothState, deviceItem, audioSharingOn)
+            }
+            .collect { mutableButtonState.value = it }
+        awaitCancellation()
+    }
+
+    private fun getButtonState(
+        bluetoothState: Boolean,
+        deviceItem: List<DeviceItem>,
+        audioSharingOn: Boolean
+    ): AudioSharingButtonState {
+        return when {
+            // Don't show button when bluetooth is off
+            !bluetoothState -> AudioSharingButtonState.Gone
+            // Show sharing audio when broadcasting
+            audioSharingOn ->
+                AudioSharingButtonState.Visible(
+                    R.string.quick_settings_bluetooth_audio_sharing_button_sharing,
+                    isActive = true
+                )
+            // When not broadcasting, don't show button if there's connected source in any device
+            deviceItem.any {
+                BluetoothUtils.hasConnectedBroadcastSource(
+                    it.cachedBluetoothDevice,
+                    localBluetoothManager
+                )
+            } -> AudioSharingButtonState.Gone
+            // Show audio sharing when there's a connected LE audio device
+            deviceItem.any { BluetoothUtils.isActiveLeAudioDevice(it.cachedBluetoothDevice) } ->
+                AudioSharingButtonState.Visible(
+                    R.string.quick_settings_bluetooth_audio_sharing_button,
+                    isActive = false
+                )
+            else -> AudioSharingButtonState.Gone
+        }
+    }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(): AudioSharingButtonViewModel
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt
new file mode 100644
index 0000000..692a78b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.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.bluetooth.qsdialog
+
+import android.bluetooth.BluetoothDevice
+import android.content.Intent
+import android.os.Bundle
+import android.provider.Settings
+import com.android.internal.logging.UiEventLogger
+import com.android.settingslib.bluetooth.A2dpProfile
+import com.android.settingslib.bluetooth.BluetoothUtils
+import com.android.settingslib.bluetooth.HeadsetProfile
+import com.android.settingslib.bluetooth.HearingAidProfile
+import com.android.settingslib.bluetooth.LeAudioProfile
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.settingslib.flags.Flags.audioSharingQsDialogImprovement
+import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+@SysUISingleton
+class AudioSharingDeviceItemActionInteractorImpl
+@Inject
+constructor(
+    private val activityStarter: ActivityStarter,
+    private val audioSharingInteractor: AudioSharingInteractor,
+    private val dialogTransitionAnimator: DialogTransitionAnimator,
+    private val localBluetoothManager: LocalBluetoothManager?,
+    @Main private val mainDispatcher: CoroutineDispatcher,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+    private val logger: BluetoothTileDialogLogger,
+    private val uiEventLogger: UiEventLogger,
+    private val delegateFactory: AudioSharingDialogDelegate.Factory,
+    private val deviceItemActionInteractorImpl: DeviceItemActionInteractorImpl,
+) : DeviceItemActionInteractor {
+
+    override suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) {
+        withContext(backgroundDispatcher) {
+            if (!audioSharingInteractor.audioSharingAvailable()) {
+                return@withContext deviceItemActionInteractorImpl.onClick(deviceItem, dialog)
+            }
+            val inAudioSharing = BluetoothUtils.isBroadcasting(localBluetoothManager)
+            logger.logDeviceClickInAudioSharingWhenEnabled(inAudioSharing)
+
+            when {
+                deviceItem.type ==
+                    DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> {
+                    if (audioSharingQsDialogImprovement()) {
+                        withContext(mainDispatcher) {
+                            delegateFactory
+                                .create(deviceItem.cachedBluetoothDevice)
+                                .createDialog()
+                                .let { dialogTransitionAnimator.showFromDialog(it, dialog) }
+                        }
+                    } else {
+                        launchSettings(deviceItem.cachedBluetoothDevice.device, dialog)
+                        logger.logLaunchSettingsCriteriaMatched(
+                            "AvailableAudioSharingDeviceClicked",
+                            deviceItem,
+                        )
+                    }
+                    uiEventLogger.log(
+                        BluetoothTileDialogUiEvent.AVAILABLE_AUDIO_SHARING_DEVICE_CLICKED
+                    )
+                }
+                inSharingAndDeviceNoSource(inAudioSharing, deviceItem) -> {
+                    launchSettings(deviceItem.cachedBluetoothDevice.device, dialog)
+                    logger.logLaunchSettingsCriteriaMatched("InSharingClickedNoSource", deviceItem)
+                    uiEventLogger.log(
+                        if (deviceItem.isLeAudioSupported)
+                            BluetoothTileDialogUiEvent.LAUNCH_SETTINGS_IN_SHARING_LE_DEVICE_CLICKED
+                        else
+                            BluetoothTileDialogUiEvent
+                                .LAUNCH_SETTINGS_IN_SHARING_NON_LE_DEVICE_CLICKED
+                    )
+                }
+                else -> {
+                    deviceItemActionInteractorImpl.onClick(deviceItem, dialog)
+                }
+            }
+        }
+    }
+
+    private fun inSharingAndDeviceNoSource(
+        inAudioSharing: Boolean,
+        deviceItem: DeviceItem,
+    ): Boolean {
+        return inAudioSharing &&
+            deviceItem.isMediaDevice &&
+            !BluetoothUtils.hasConnectedBroadcastSource(
+                deviceItem.cachedBluetoothDevice,
+                localBluetoothManager,
+            )
+    }
+
+    private fun launchSettings(device: BluetoothDevice, dialog: SystemUIDialog) {
+        val intent =
+            Intent(Settings.ACTION_BLUETOOTH_SETTINGS).apply {
+                putExtra(
+                    EXTRA_SHOW_FRAGMENT_ARGUMENTS,
+                    Bundle().apply {
+                        putParcelable(LocalBluetoothLeBroadcast.EXTRA_BLUETOOTH_DEVICE, device)
+                    },
+                )
+            }
+        intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK
+        activityStarter.postStartActivityDismissingKeyguard(
+            intent,
+            0,
+            dialogTransitionAnimator.createActivityTransitionController(dialog),
+        )
+    }
+
+    private companion object {
+        const val EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args"
+
+        val DeviceItem.isLeAudioSupported: Boolean
+            get() =
+                cachedBluetoothDevice.profiles.any { profile ->
+                    profile is LeAudioProfile && profile.isEnabled(cachedBluetoothDevice.device)
+                }
+
+        val DeviceItem.isMediaDevice: Boolean
+            get() =
+                cachedBluetoothDevice.uiAccessibleProfiles.any {
+                    it is A2dpProfile ||
+                        it is HearingAidProfile ||
+                        it is LeAudioProfile ||
+                        it is HeadsetProfile
+                }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegate.kt
new file mode 100644
index 0000000..3ac942b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegate.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bluetooth.qsdialog
+
+import android.os.Bundle
+import android.widget.Button
+import android.widget.TextView
+import com.android.internal.logging.UiEventLogger
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+class AudioSharingDialogDelegate
+@AssistedInject
+constructor(
+    @Assisted private val cachedBluetoothDevice: CachedBluetoothDevice,
+    @Application private val coroutineScope: CoroutineScope,
+    private val viewModelFactory: AudioSharingDialogViewModel.Factory,
+    private val sysuiDialogFactory: SystemUIDialog.Factory,
+    private val uiEventLogger: UiEventLogger,
+) : SystemUIDialog.Delegate {
+
+    override fun createDialog(): SystemUIDialog = sysuiDialogFactory.create(this)
+
+    override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
+        with(dialog.layoutInflater.inflate(R.layout.audio_sharing_dialog, null)) {
+            dialog.setView(this)
+            val subtitleTextView = requireViewById<TextView>(R.id.subtitle)
+            val shareAudioButton = requireViewById<TextView>(R.id.share_audio_button)
+            val switchActiveButton = requireViewById<Button>(R.id.switch_active_button)
+            val job =
+                coroutineScope.launch {
+                    val viewModel = viewModelFactory.create(cachedBluetoothDevice, this)
+                    viewModel.dialogState.collect {
+                        when (it) {
+                            is AudioSharingDialogState.Hide -> dialog.dismiss()
+                            is AudioSharingDialogState.Show -> {
+                                subtitleTextView.text = it.subtitle
+                                switchActiveButton.text = it.switchButtonText
+                                switchActiveButton.setOnClickListener {
+                                    viewModel.switchActiveClicked()
+                                    uiEventLogger.log(
+                                        BluetoothTileDialogUiEvent
+                                            .AUDIO_SHARING_DIALOG_SWITCH_ACTIVE_CLICKED
+                                    )
+                                    dialog.dismiss()
+                                }
+                                shareAudioButton.setOnClickListener {
+                                    viewModel.shareAudioClicked()
+                                    uiEventLogger.log(
+                                        BluetoothTileDialogUiEvent
+                                            .AUDIO_SHARING_DIALOG_SHARE_AUDIO_CLICKED
+                                    )
+                                    dialog.dismiss()
+                                }
+                            }
+                        }
+                    }
+                }
+            SystemUIDialog.registerDismissListener(dialog) { job.cancel() }
+        }
+    }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(cachedBluetoothDevice: CachedBluetoothDevice): AudioSharingDialogDelegate
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModel.kt
new file mode 100644
index 0000000..dc970aea
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModel.kt
@@ -0,0 +1,109 @@
+/*
+ * 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.bluetooth.qsdialog
+
+import android.content.Context
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.res.R
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.launch
+
+sealed class AudioSharingDialogState {
+    data object Hide : AudioSharingDialogState()
+
+    data class Show(val subtitle: String, val switchButtonText: String) : AudioSharingDialogState()
+}
+
+class AudioSharingDialogViewModel
+@AssistedInject
+constructor(
+    deviceItemInteractor: DeviceItemInteractor,
+    private val audioSharingInteractor: AudioSharingInteractor,
+    private val context: Context,
+    private val localBluetoothManager: LocalBluetoothManager?,
+    @Assisted private val cachedBluetoothDevice: CachedBluetoothDevice,
+    @Assisted private val coroutineScope: CoroutineScope,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+) {
+    val dialogState: Flow<AudioSharingDialogState> =
+        deviceItemInteractor.deviceItemUpdateRequest
+            .map {
+                if (
+                    audioSharingInteractor.isAvailableAudioSharingMediaBluetoothDevice(
+                        cachedBluetoothDevice
+                    )
+                ) {
+                    createShowState(cachedBluetoothDevice)
+                } else {
+                    AudioSharingDialogState.Hide
+                }
+            }
+            .onStart { emit(createShowState(cachedBluetoothDevice)) }
+            .flowOn(backgroundDispatcher)
+            .distinctUntilChanged()
+
+    fun switchActiveClicked() {
+        coroutineScope.launch { audioSharingInteractor.switchActive(cachedBluetoothDevice) }
+    }
+
+    fun shareAudioClicked() {
+        coroutineScope.launch { audioSharingInteractor.startAudioSharing() }
+    }
+
+    private fun createShowState(
+        cachedBluetoothDevice: CachedBluetoothDevice
+    ): AudioSharingDialogState {
+        val activeDeviceName =
+            localBluetoothManager
+                ?.profileManager
+                ?.leAudioProfile
+                ?.activeDevices
+                ?.firstOrNull()
+                ?.let { localBluetoothManager.cachedDeviceManager?.findDevice(it)?.name } ?: ""
+        val availableDeviceName = cachedBluetoothDevice.name
+        return AudioSharingDialogState.Show(
+            context.getString(
+                R.string.quick_settings_bluetooth_audio_sharing_dialog_subtitle,
+                availableDeviceName,
+                activeDeviceName
+            ),
+            context.getString(
+                R.string.quick_settings_bluetooth_audio_sharing_dialog_switch_to_button,
+                availableDeviceName
+            )
+        )
+    }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(
+            cachedBluetoothDevice: CachedBluetoothDevice,
+            coroutineScope: CoroutineScope
+        ): AudioSharingDialogViewModel
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt
index 817f2d7..65f1105 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt
@@ -16,82 +16,148 @@
 
 package com.android.systemui.bluetooth.qsdialog
 
-import androidx.annotation.StringRes
 import com.android.settingslib.bluetooth.BluetoothUtils
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
 import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.settingslib.bluetooth.onPlaybackStarted
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.res.R
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.stateIn
-
-internal sealed class AudioSharingButtonState {
-    object Gone : AudioSharingButtonState()
-
-    data class Visible(@StringRes val resId: Int, val isActive: Boolean) :
-        AudioSharingButtonState()
-}
+import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.withContext
 
 /** Holds business logic for the audio sharing state. */
+interface AudioSharingInteractor {
+    val isAudioSharingOn: Flow<Boolean>
+
+    val audioSourceStateUpdate: Flow<Unit>
+
+    suspend fun handleAudioSourceWhenReady()
+
+    suspend fun isAvailableAudioSharingMediaBluetoothDevice(
+        cachedBluetoothDevice: CachedBluetoothDevice
+    ): Boolean
+
+    suspend fun switchActive(cachedBluetoothDevice: CachedBluetoothDevice)
+
+    suspend fun startAudioSharing()
+
+    suspend fun audioSharingAvailable(): Boolean
+}
+
 @SysUISingleton
-internal class AudioSharingInteractor
+@OptIn(ExperimentalCoroutinesApi::class)
+class AudioSharingInteractorImpl
 @Inject
 constructor(
     private val localBluetoothManager: LocalBluetoothManager?,
-    bluetoothStateInteractor: BluetoothStateInteractor,
-    deviceItemInteractor: DeviceItemInteractor,
-    @Application private val coroutineScope: CoroutineScope,
+    private val audioSharingRepository: AudioSharingRepository,
     @Background private val backgroundDispatcher: CoroutineDispatcher,
-) {
-    /** Flow representing the update of AudioSharingButtonState. */
-    internal val audioSharingButtonStateUpdate: Flow<AudioSharingButtonState> =
-        combine(
-                bluetoothStateInteractor.bluetoothStateUpdate,
-                deviceItemInteractor.deviceItemUpdate
-            ) { bluetoothState, deviceItem ->
-                getButtonState(bluetoothState, deviceItem)
+) : AudioSharingInteractor {
+
+    override val isAudioSharingOn: Flow<Boolean> =
+        flow { emit(audioSharingAvailable()) }
+            .flatMapLatest { isEnabled ->
+                if (isEnabled) {
+                    audioSharingRepository.inAudioSharing
+                } else {
+                    flowOf(false)
+                }
             }
             .flowOn(backgroundDispatcher)
-            .stateIn(
-                coroutineScope,
-                SharingStarted.WhileSubscribed(replayExpirationMillis = 0),
-                initialValue = AudioSharingButtonState.Gone
-            )
 
-    private fun getButtonState(
-        bluetoothState: Boolean,
-        deviceItem: List<DeviceItem>
-    ): AudioSharingButtonState {
-        return when {
-            // Don't show button when bluetooth is off
-            !bluetoothState -> AudioSharingButtonState.Gone
-            // Show sharing audio when broadcasting
-            BluetoothUtils.isBroadcasting(localBluetoothManager) ->
-                AudioSharingButtonState.Visible(
-                    R.string.quick_settings_bluetooth_audio_sharing_button_sharing,
-                    isActive = true
-                )
-            // When not broadcasting, don't show button if there's connected source in any device
-            deviceItem.any {
-                BluetoothUtils.hasConnectedBroadcastSource(
-                    it.cachedBluetoothDevice,
-                    localBluetoothManager
-                )
-            } -> AudioSharingButtonState.Gone
-            // Show audio sharing when there's a connected LE audio device
-            deviceItem.any { BluetoothUtils.isActiveLeAudioDevice(it.cachedBluetoothDevice) } ->
-                AudioSharingButtonState.Visible(
-                    R.string.quick_settings_bluetooth_audio_sharing_button,
-                    isActive = false
-                )
-            else -> AudioSharingButtonState.Gone
+    override val audioSourceStateUpdate =
+        isAudioSharingOn
+            .flatMapLatest {
+                if (it) {
+                    audioSharingRepository.audioSourceStateUpdate
+                } else {
+                    emptyFlow()
+                }
+            }
+            .flowOn(backgroundDispatcher)
+
+    override suspend fun handleAudioSourceWhenReady() {
+        withContext(backgroundDispatcher) {
+            if (audioSharingAvailable()) {
+                audioSharingRepository.leAudioBroadcastProfile?.let { profile ->
+                    isAudioSharingOn
+                        .mapNotNull { audioSharingOn ->
+                            if (audioSharingOn) {
+                                // onPlaybackStarted could emit multiple times during one
+                                // audio sharing session, we only perform add source on the
+                                // first time
+                                profile.onPlaybackStarted.firstOrNull()
+                            } else {
+                                null
+                            }
+                        }
+                        .flowOn(backgroundDispatcher)
+                        .collect { audioSharingRepository.addSource() }
+                }
+            }
         }
     }
+
+    override suspend fun isAvailableAudioSharingMediaBluetoothDevice(
+        cachedBluetoothDevice: CachedBluetoothDevice
+    ): Boolean {
+        return withContext(backgroundDispatcher) {
+            if (audioSharingAvailable()) {
+                BluetoothUtils.isAvailableAudioSharingMediaBluetoothDevice(
+                    cachedBluetoothDevice,
+                    localBluetoothManager,
+                )
+            } else {
+                false
+            }
+        }
+    }
+
+    override suspend fun switchActive(cachedBluetoothDevice: CachedBluetoothDevice) {
+        if (!audioSharingAvailable()) {
+            return
+        }
+        audioSharingRepository.setActive(cachedBluetoothDevice)
+    }
+
+    override suspend fun startAudioSharing() {
+        if (!audioSharingAvailable()) {
+            return
+        }
+        audioSharingRepository.startAudioSharing()
+    }
+
+    // TODO(b/367965193): Move this after flags rollout
+    override suspend fun audioSharingAvailable(): Boolean {
+        return audioSharingRepository.audioSharingAvailable()
+    }
+}
+
+@SysUISingleton
+class AudioSharingInteractorEmptyImpl @Inject constructor() : AudioSharingInteractor {
+    override val isAudioSharingOn: Flow<Boolean> = flowOf(false)
+
+    override val audioSourceStateUpdate: Flow<Unit> = emptyFlow()
+
+    override suspend fun handleAudioSourceWhenReady() {}
+
+    override suspend fun isAvailableAudioSharingMediaBluetoothDevice(
+        cachedBluetoothDevice: CachedBluetoothDevice
+    ) = false
+
+    override suspend fun switchActive(cachedBluetoothDevice: CachedBluetoothDevice) {}
+
+    override suspend fun startAudioSharing() {}
+
+    override suspend fun audioSharingAvailable(): Boolean = false
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepository.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepository.kt
new file mode 100644
index 0000000..b9b8d36
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepository.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bluetooth.qsdialog
+
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.settingslib.bluetooth.onSourceConnectedOrRemoved
+import com.android.settingslib.volume.data.repository.AudioSharingRepository as SettingsLibAudioSharingRepository
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.withContext
+
+interface AudioSharingRepository {
+    val leAudioBroadcastProfile: LocalBluetoothLeBroadcast?
+
+    val audioSourceStateUpdate: Flow<Unit>
+
+    val inAudioSharing: StateFlow<Boolean>
+
+    suspend fun audioSharingAvailable(): Boolean
+
+    suspend fun addSource()
+
+    suspend fun setActive(cachedBluetoothDevice: CachedBluetoothDevice)
+
+    suspend fun startAudioSharing()
+}
+
+@SysUISingleton
+class AudioSharingRepositoryImpl(
+    private val localBluetoothManager: LocalBluetoothManager,
+    private val settingsLibAudioSharingRepository: SettingsLibAudioSharingRepository,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+) : AudioSharingRepository {
+
+    override val leAudioBroadcastProfile: LocalBluetoothLeBroadcast?
+        get() = localBluetoothManager.profileManager?.leAudioBroadcastProfile
+
+    private val leAudioBroadcastAssistantProfile: LocalBluetoothLeBroadcastAssistant?
+        get() = localBluetoothManager.profileManager?.leAudioBroadcastAssistantProfile
+
+    override val audioSourceStateUpdate: Flow<Unit> =
+        leAudioBroadcastAssistantProfile?.onSourceConnectedOrRemoved ?: emptyFlow()
+
+    override val inAudioSharing: StateFlow<Boolean> =
+        settingsLibAudioSharingRepository.inAudioSharing
+
+    override suspend fun audioSharingAvailable(): Boolean {
+        return settingsLibAudioSharingRepository.audioSharingAvailable()
+    }
+
+    override suspend fun addSource() {
+        withContext(backgroundDispatcher) {
+            if (!settingsLibAudioSharingRepository.audioSharingAvailable()) {
+                return@withContext
+            }
+            leAudioBroadcastProfile?.latestBluetoothLeBroadcastMetadata?.let { metadata ->
+                leAudioBroadcastAssistantProfile?.let {
+                    it.allConnectedDevices.forEach { sink -> it.addSource(sink, metadata, false) }
+                }
+            }
+        }
+    }
+
+    override suspend fun setActive(cachedBluetoothDevice: CachedBluetoothDevice) {
+        withContext(backgroundDispatcher) {
+            if (!settingsLibAudioSharingRepository.audioSharingAvailable()) {
+                return@withContext
+            }
+            cachedBluetoothDevice.setActive()
+        }
+    }
+
+    override suspend fun startAudioSharing() {
+        withContext(backgroundDispatcher) {
+            if (!settingsLibAudioSharingRepository.audioSharingAvailable()) {
+                return@withContext
+            }
+            leAudioBroadcastProfile?.startPrivateBroadcast()
+        }
+    }
+}
+
+@SysUISingleton
+class AudioSharingRepositoryEmptyImpl : AudioSharingRepository {
+    override val leAudioBroadcastProfile: LocalBluetoothLeBroadcast? = null
+
+    override val audioSourceStateUpdate: Flow<Unit> = emptyFlow()
+
+    override val inAudioSharing: StateFlow<Boolean> = MutableStateFlow(false)
+
+    override suspend fun audioSharingAvailable(): Boolean = false
+
+    override suspend fun addSource() {}
+
+    override suspend fun setActive(cachedBluetoothDevice: CachedBluetoothDevice) {}
+
+    override suspend fun startAudioSharing() {}
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractor.kt
index 17f9e63..55d4d3e 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractor.kt
@@ -39,7 +39,7 @@
 
 /** Holds business logic for the Bluetooth Dialog's bluetooth and device connection state */
 @SysUISingleton
-internal class BluetoothStateInteractor
+class BluetoothStateInteractor
 @Inject
 constructor(
     private val localBluetoothManager: LocalBluetoothManager?,
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt
index 7deea73..a9c5c69 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt
@@ -300,7 +300,7 @@
     }
 
     private fun getProgressBarBackground(dialog: SystemUIDialog): View {
-        return dialog.requireViewById(R.id.bluetooth_tile_dialog_progress_animation)
+        return dialog.requireViewById(R.id.bluetooth_tile_dialog_progress_background)
     }
 
     private fun getScrollViewContent(dialog: SystemUIDialog): View {
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt
index bdd4c16..aad233f 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt
@@ -42,6 +42,7 @@
     LAUNCH_SETTINGS_IN_SHARING_LE_DEVICE_CLICKED(1717),
     @UiEvent(doc = "Currently broadcasting and a non-LE audio supported device is clicked")
     LAUNCH_SETTINGS_IN_SHARING_NON_LE_DEVICE_CLICKED(1718),
+    @Deprecated("Use case no longer needed")
     @UiEvent(
         doc = "Not broadcasting, having one connected, another saved LE audio device is clicked"
     )
@@ -52,8 +53,13 @@
     )
     @UiEvent(doc = "Not broadcasting, one of the two connected LE audio devices is clicked")
     LAUNCH_SETTINGS_NOT_SHARING_CONNECTED_LE_DEVICE_CLICKED(1720),
+    @Deprecated("Use case no longer needed")
     @UiEvent(doc = "Not broadcasting, having two connected, the active LE audio devices is clicked")
-    LAUNCH_SETTINGS_NOT_SHARING_ACTIVE_LE_DEVICE_CLICKED(1881);
+    LAUNCH_SETTINGS_NOT_SHARING_ACTIVE_LE_DEVICE_CLICKED(1881),
+    @UiEvent(doc = "Clicked on switch active button on audio sharing dialog")
+    AUDIO_SHARING_DIALOG_SWITCH_ACTIVE_CLICKED(1890),
+    @UiEvent(doc = "Clicked on share audio button on audio sharing dialog")
+    AUDIO_SHARING_DIALOG_SHARE_AUDIO_CLICKED(1891);
 
     override fun getId() = metricId
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
index a8f7fc3..5c35c52 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
@@ -28,8 +28,8 @@
 import androidx.annotation.VisibleForTesting
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.internal.logging.UiEventLogger
-import com.android.settingslib.bluetooth.BluetoothUtils
 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
+import com.android.settingslib.flags.Flags.audioSharingQsDialogImprovement
 import com.android.systemui.Prefs
 import com.android.systemui.animation.DialogCuj
 import com.android.systemui.animation.DialogTransitionAnimator
@@ -51,6 +51,7 @@
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.channels.produce
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.merge
@@ -68,10 +69,12 @@
     private val bluetoothStateInteractor: BluetoothStateInteractor,
     private val bluetoothAutoOnInteractor: BluetoothAutoOnInteractor,
     private val audioSharingInteractor: AudioSharingInteractor,
+    private val audioSharingButtonViewModelFactory: AudioSharingButtonViewModel.Factory,
     private val bluetoothDeviceMetadataInteractor: BluetoothDeviceMetadataInteractor,
     private val dialogTransitionAnimator: DialogTransitionAnimator,
     private val activityStarter: ActivityStarter,
     private val uiEventLogger: UiEventLogger,
+    private val logger: BluetoothTileDialogLogger,
     @Application private val coroutineScope: CoroutineScope,
     @Main private val mainDispatcher: CoroutineDispatcher,
     @Background private val backgroundDispatcher: CoroutineDispatcher,
@@ -102,7 +105,7 @@
                     expandable?.dialogTransitionController(
                         DialogCuj(
                             InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
-                            INTERACTION_JANK_TAG
+                            INTERACTION_JANK_TAG,
                         )
                     )
                 controller?.let {
@@ -117,7 +120,7 @@
                 // stop the progress bar.
                 combine(
                         deviceItemInteractor.deviceItemUpdate,
-                        deviceItemInteractor.showSeeAllUpdate
+                        deviceItemInteractor.showSeeAllUpdate,
                     ) { deviceItem, showSeeAll ->
                         updateDialogUiJob?.cancel()
                         updateDialogUiJob = launch {
@@ -127,7 +130,7 @@
                                     deviceItem,
                                     showSeeAll,
                                     showPairNewDevice =
-                                        bluetoothStateInteractor.isBluetoothEnabled()
+                                        bluetoothStateInteractor.isBluetoothEnabled(),
                                 )
                                 animateProgressBar(dialog, false)
                             }
@@ -139,7 +142,15 @@
                 // the device item list and animate the progress bar.
                 merge(
                         deviceItemInteractor.deviceItemUpdateRequest,
-                        bluetoothDeviceMetadataInteractor.metadataUpdate
+                        bluetoothDeviceMetadataInteractor.metadataUpdate,
+                        if (
+                            audioSharingInteractor.audioSharingAvailable() &&
+                                audioSharingQsDialogImprovement()
+                        ) {
+                            audioSharingInteractor.audioSourceStateUpdate
+                        } else {
+                            emptyFlow()
+                        },
                     )
                     .onEach {
                         dialogDelegate.animateProgressBar(dialog, true)
@@ -147,35 +158,42 @@
                         updateDeviceItemJob = launch {
                             deviceItemInteractor.updateDeviceItems(
                                 context,
-                                DeviceFetchTrigger.BLUETOOTH_CALLBACK_RECEIVED
+                                DeviceFetchTrigger.BLUETOOTH_CALLBACK_RECEIVED,
                             )
                         }
                     }
                     .launchIn(this)
 
-                if (BluetoothUtils.isAudioSharingEnabled()) {
-                    audioSharingInteractor.audioSharingButtonStateUpdate
-                        .onEach {
-                            when (it) {
-                                is AudioSharingButtonState.Visible -> {
-                                    dialogDelegate.onAudioSharingButtonUpdated(
-                                        dialog,
-                                        VISIBLE,
-                                        context.getString(it.resId),
-                                        it.isActive
-                                    )
-                                }
-                                is AudioSharingButtonState.Gone -> {
-                                    dialogDelegate.onAudioSharingButtonUpdated(
-                                        dialog,
-                                        GONE,
-                                        label = null,
-                                        isActive = false
-                                    )
+                if (audioSharingInteractor.audioSharingAvailable()) {
+                    if (audioSharingQsDialogImprovement()) {
+                        launch { audioSharingInteractor.handleAudioSourceWhenReady() }
+                    }
+
+                    audioSharingButtonViewModelFactory.create().run {
+                        audioSharingButtonStateUpdate
+                            .onEach {
+                                when (it) {
+                                    is AudioSharingButtonState.Visible -> {
+                                        dialogDelegate.onAudioSharingButtonUpdated(
+                                            dialog,
+                                            VISIBLE,
+                                            context.getString(it.resId),
+                                            it.isActive,
+                                        )
+                                    }
+                                    is AudioSharingButtonState.Gone -> {
+                                        dialogDelegate.onAudioSharingButtonUpdated(
+                                            dialog,
+                                            GONE,
+                                            label = null,
+                                            isActive = false,
+                                        )
+                                    }
                                 }
                             }
-                        }
-                        .launchIn(this)
+                            .launchIn(this@launch)
+                        launch { activate() }
+                    }
                 }
 
                 // bluetoothStateUpdate is emitted when bluetooth on/off state is changed, re-fetch
@@ -185,13 +203,13 @@
                         dialogDelegate.onBluetoothStateUpdated(
                             dialog,
                             it,
-                            UiProperties.build(it, isAutoOnToggleFeatureAvailable())
+                            UiProperties.build(it, isAutoOnToggleFeatureAvailable()),
                         )
                         updateDeviceItemJob?.cancel()
                         updateDeviceItemJob = launch {
                             deviceItemInteractor.updateDeviceItems(
                                 context,
-                                DeviceFetchTrigger.BLUETOOTH_STATE_CHANGE_RECEIVED
+                                DeviceFetchTrigger.BLUETOOTH_STATE_CHANGE_RECEIVED,
                             )
                         }
                     }
@@ -209,7 +227,10 @@
 
                 // deviceItemClick is emitted when user clicked on a device item.
                 dialogDelegate.deviceItemClick
-                    .onEach { deviceItemActionInteractor.onClick(it, dialog) }
+                    .onEach {
+                        deviceItemActionInteractor.onClick(it, dialog)
+                        logger.logDeviceClick(it.cachedBluetoothDevice.address, it.type)
+                    }
                     .launchIn(this)
 
                 // contentHeight is emitted when the dialog is dismissed.
@@ -230,7 +251,7 @@
                                 dialog,
                                 it,
                                 if (it) R.string.turn_on_bluetooth_auto_info_enabled
-                                else R.string.turn_on_bluetooth_auto_info_disabled
+                                else R.string.turn_on_bluetooth_auto_info_disabled,
                             )
                         }
                         .launchIn(this)
@@ -252,18 +273,18 @@
             withContext(backgroundDispatcher) {
                 sharedPreferences.getInt(
                     CONTENT_HEIGHT_PREF_KEY,
-                    ViewGroup.LayoutParams.WRAP_CONTENT
+                    ViewGroup.LayoutParams.WRAP_CONTENT,
                 )
             }
 
         return bluetoothDialogDelegateFactory.create(
             UiProperties.build(
                 bluetoothStateInteractor.isBluetoothEnabled(),
-                isAutoOnToggleFeatureAvailable()
+                isAutoOnToggleFeatureAvailable(),
             ),
             cachedContentHeight,
             this@BluetoothTileDialogViewModel,
-            { cancelJob() }
+            { cancelJob() },
         )
     }
 
@@ -275,7 +296,7 @@
                     EXTRA_SHOW_FRAGMENT_ARGUMENTS,
                     Bundle().apply {
                         putString("device_address", deviceItem.cachedBluetoothDevice.address)
-                    }
+                    },
                 )
             }
         startSettingsActivity(intent, view)
@@ -299,7 +320,7 @@
                     EXTRA_SHOW_FRAGMENT_ARGUMENTS,
                     Bundle().apply {
                         putBoolean(LocalBluetoothLeBroadcast.EXTRA_START_LE_AUDIO_SHARING, true)
-                    }
+                    },
                 )
             }
         startSettingsActivity(intent, view)
@@ -345,7 +366,7 @@
         companion object {
             internal fun build(
                 isBluetoothEnabled: Boolean,
-                isAutoOnToggleFeatureAvailable: Boolean
+                isAutoOnToggleFeatureAvailable: Boolean,
             ) =
                 UiProperties(
                     subTitleResId = getSubtitleResId(isBluetoothEnabled),
@@ -355,7 +376,7 @@
                     scrollViewMinHeightResId =
                         if (isAutoOnToggleFeatureAvailable)
                             R.dimen.bluetooth_dialog_scroll_view_min_height_with_auto_on
-                        else R.dimen.bluetooth_dialog_scroll_view_min_height
+                        else R.dimen.bluetooth_dialog_scroll_view_min_height,
                 )
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt
index f1894d3..cf0f19f 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt
@@ -16,87 +16,28 @@
 
 package com.android.systemui.bluetooth.qsdialog
 
-import android.bluetooth.BluetoothDevice
-import android.bluetooth.BluetoothProfile
-import android.content.Intent
-import android.os.Bundle
-import android.provider.Settings
 import com.android.internal.logging.UiEventLogger
-import com.android.settingslib.bluetooth.A2dpProfile
-import com.android.settingslib.bluetooth.BluetoothUtils
-import com.android.settingslib.bluetooth.HeadsetProfile
-import com.android.settingslib.bluetooth.HearingAidProfile
-import com.android.settingslib.bluetooth.LeAudioProfile
-import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
-import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant
-import com.android.settingslib.bluetooth.LocalBluetoothManager
-import com.android.systemui.animation.DialogTransitionAnimator
-import com.android.systemui.bluetooth.qsdialog.DeviceItemActionInteractor.LaunchSettingsCriteria.Companion.getCurrentConnectedLeByGroupId
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.withContext
 
+interface DeviceItemActionInteractor {
+    suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) {}
+}
+
 @SysUISingleton
-class DeviceItemActionInteractor
+class DeviceItemActionInteractorImpl
 @Inject
 constructor(
-    private val activityStarter: ActivityStarter,
-    private val dialogTransitionAnimator: DialogTransitionAnimator,
-    private val localBluetoothManager: LocalBluetoothManager?,
     @Background private val backgroundDispatcher: CoroutineDispatcher,
-    private val logger: BluetoothTileDialogLogger,
     private val uiEventLogger: UiEventLogger,
-) {
-    private val leAudioProfile: LeAudioProfile?
-        get() = localBluetoothManager?.profileManager?.leAudioProfile
+) : DeviceItemActionInteractor {
 
-    private val assistantProfile: LocalBluetoothLeBroadcastAssistant?
-        get() = localBluetoothManager?.profileManager?.leAudioBroadcastAssistantProfile
-
-    private val launchSettingsCriteriaList: List<LaunchSettingsCriteria>
-        get() =
-            listOf(
-                InSharingClickedNoSource(localBluetoothManager, backgroundDispatcher, logger),
-                NotSharingClickedNonConnect(
-                    leAudioProfile,
-                    assistantProfile,
-                    backgroundDispatcher,
-                    logger
-                ),
-                NotSharingClickedActive(
-                    leAudioProfile,
-                    assistantProfile,
-                    backgroundDispatcher,
-                    logger
-                )
-            )
-
-    suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) {
+    override suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) {
         withContext(backgroundDispatcher) {
-            logger.logDeviceClick(deviceItem.cachedBluetoothDevice.address, deviceItem.type)
-            if (
-                BluetoothUtils.isAudioSharingEnabled() &&
-                    localBluetoothManager != null &&
-                    leAudioProfile != null &&
-                    assistantProfile != null
-            ) {
-                val inAudioSharing = BluetoothUtils.isBroadcasting(localBluetoothManager)
-                logger.logDeviceClickInAudioSharingWhenEnabled(inAudioSharing)
-
-                val criteriaMatched =
-                    launchSettingsCriteriaList.firstOrNull {
-                        it.matched(inAudioSharing, deviceItem)
-                    }
-                if (criteriaMatched != null) {
-                    uiEventLogger.log(criteriaMatched.getClickUiEvent(deviceItem))
-                    launchSettings(deviceItem.cachedBluetoothDevice.device, dialog)
-                    return@withContext
-                }
-            }
             deviceItem.cachedBluetoothDevice.apply {
                 when (deviceItem.type) {
                     DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE -> {
@@ -106,12 +47,6 @@
                     DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> {
                         uiEventLogger.log(BluetoothTileDialogUiEvent.AUDIO_SHARING_DEVICE_CLICKED)
                     }
-                    DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> {
-                        // TODO(b/360759048): pop up dialog
-                        uiEventLogger.log(
-                            BluetoothTileDialogUiEvent.AVAILABLE_AUDIO_SHARING_DEVICE_CLICKED
-                        )
-                    }
                     DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE -> {
                         setActive()
                         uiEventLogger.log(BluetoothTileDialogUiEvent.CONNECTED_DEVICE_SET_ACTIVE)
@@ -126,186 +61,12 @@
                         connect()
                         uiEventLogger.log(BluetoothTileDialogUiEvent.SAVED_DEVICE_CONNECT)
                     }
-                }
-            }
-        }
-    }
-
-    private fun launchSettings(device: BluetoothDevice, dialog: SystemUIDialog) {
-        val intent =
-            Intent(Settings.ACTION_BLUETOOTH_SETTINGS).apply {
-                putExtra(
-                    EXTRA_SHOW_FRAGMENT_ARGUMENTS,
-                    Bundle().apply {
-                        putParcelable(LocalBluetoothLeBroadcast.EXTRA_BLUETOOTH_DEVICE, device)
+                    DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> {
+                        // Do nothing. Should already be handled in
+                        // AudioSharingDeviceItemActionInteractor.
                     }
-                )
-            }
-        intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK
-        activityStarter.postStartActivityDismissingKeyguard(
-            intent,
-            0,
-            dialogTransitionAnimator.createActivityTransitionController(dialog)
-        )
-    }
-
-    private interface LaunchSettingsCriteria {
-        suspend fun matched(inAudioSharing: Boolean, deviceItem: DeviceItem): Boolean
-
-        suspend fun getClickUiEvent(deviceItem: DeviceItem): BluetoothTileDialogUiEvent
-
-        companion object {
-            suspend fun getCurrentConnectedLeByGroupId(
-                leAudioProfile: LeAudioProfile,
-                assistantProfile: LocalBluetoothLeBroadcastAssistant,
-                @Background backgroundDispatcher: CoroutineDispatcher,
-                logger: BluetoothTileDialogLogger,
-            ): Map<Int, List<BluetoothDevice>> {
-                return withContext(backgroundDispatcher) {
-                    assistantProfile
-                        .getDevicesMatchingConnectionStates(
-                            intArrayOf(BluetoothProfile.STATE_CONNECTED)
-                        )
-                        ?.filterNotNull()
-                        ?.groupBy { leAudioProfile.getGroupId(it) }
-                        ?.also { logger.logConnectedLeByGroupId(it) } ?: emptyMap()
                 }
             }
         }
     }
-
-    private class InSharingClickedNoSource(
-        private val localBluetoothManager: LocalBluetoothManager?,
-        @Background private val backgroundDispatcher: CoroutineDispatcher,
-        private val logger: BluetoothTileDialogLogger,
-    ) : LaunchSettingsCriteria {
-        // If currently broadcasting and the clicked device is not connected to the source
-        override suspend fun matched(inAudioSharing: Boolean, deviceItem: DeviceItem): Boolean {
-            return withContext(backgroundDispatcher) {
-                val matched =
-                    inAudioSharing &&
-                        deviceItem.isMediaDevice &&
-                        !BluetoothUtils.hasConnectedBroadcastSource(
-                            deviceItem.cachedBluetoothDevice,
-                            localBluetoothManager
-                        )
-
-                if (matched) {
-                    logger.logLaunchSettingsCriteriaMatched("InSharingClickedNoSource", deviceItem)
-                }
-
-                matched
-            }
-        }
-
-        override suspend fun getClickUiEvent(deviceItem: DeviceItem) =
-            if (deviceItem.isLeAudioSupported)
-                BluetoothTileDialogUiEvent.LAUNCH_SETTINGS_IN_SHARING_LE_DEVICE_CLICKED
-            else BluetoothTileDialogUiEvent.LAUNCH_SETTINGS_IN_SHARING_NON_LE_DEVICE_CLICKED
-    }
-
-    private class NotSharingClickedNonConnect(
-        private val leAudioProfile: LeAudioProfile?,
-        private val assistantProfile: LocalBluetoothLeBroadcastAssistant?,
-        @Background private val backgroundDispatcher: CoroutineDispatcher,
-        private val logger: BluetoothTileDialogLogger,
-    ) : LaunchSettingsCriteria {
-        // If not broadcasting, having one device connected, and clicked on a not yet connected LE
-        // audio device
-        override suspend fun matched(inAudioSharing: Boolean, deviceItem: DeviceItem): Boolean {
-            return withContext(backgroundDispatcher) {
-                val matched =
-                    leAudioProfile?.let { leAudio ->
-                        assistantProfile?.let { assistant ->
-                            !inAudioSharing &&
-                                getCurrentConnectedLeByGroupId(
-                                        leAudio,
-                                        assistant,
-                                        backgroundDispatcher,
-                                        logger
-                                    )
-                                    .size == 1 &&
-                                deviceItem.isNotConnectedLeAudioSupported
-                        }
-                    } ?: false
-
-                if (matched) {
-                    logger.logLaunchSettingsCriteriaMatched(
-                        "NotSharingClickedNonConnect",
-                        deviceItem
-                    )
-                }
-
-                matched
-            }
-        }
-
-        override suspend fun getClickUiEvent(deviceItem: DeviceItem) =
-            BluetoothTileDialogUiEvent.LAUNCH_SETTINGS_NOT_SHARING_SAVED_LE_DEVICE_CLICKED
-    }
-
-    private class NotSharingClickedActive(
-        private val leAudioProfile: LeAudioProfile?,
-        private val assistantProfile: LocalBluetoothLeBroadcastAssistant?,
-        @Background private val backgroundDispatcher: CoroutineDispatcher,
-        private val logger: BluetoothTileDialogLogger,
-    ) : LaunchSettingsCriteria {
-        // If not broadcasting, having two device connected, clicked on the active LE audio
-        // device
-        override suspend fun matched(inAudioSharing: Boolean, deviceItem: DeviceItem): Boolean {
-            return withContext(backgroundDispatcher) {
-                val matched =
-                    leAudioProfile?.let { leAudio ->
-                        assistantProfile?.let { assistant ->
-                            !inAudioSharing &&
-                                getCurrentConnectedLeByGroupId(
-                                        leAudio,
-                                        assistant,
-                                        backgroundDispatcher,
-                                        logger
-                                    )
-                                    .size == 2 &&
-                                deviceItem.isActiveLeAudioSupported
-                        }
-                    } ?: false
-
-                if (matched) {
-                    logger.logLaunchSettingsCriteriaMatched(
-                        "NotSharingClickedConnected",
-                        deviceItem
-                    )
-                }
-
-                matched
-            }
-        }
-
-        override suspend fun getClickUiEvent(deviceItem: DeviceItem) =
-            BluetoothTileDialogUiEvent.LAUNCH_SETTINGS_NOT_SHARING_ACTIVE_LE_DEVICE_CLICKED
-    }
-
-    private companion object {
-        const val EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args"
-
-        val DeviceItem.isLeAudioSupported: Boolean
-            get() =
-                cachedBluetoothDevice.profiles.any { profile ->
-                    profile is LeAudioProfile && profile.isEnabled(cachedBluetoothDevice.device)
-                }
-
-        val DeviceItem.isNotConnectedLeAudioSupported: Boolean
-            get() = type == DeviceItemType.SAVED_BLUETOOTH_DEVICE && isLeAudioSupported
-
-        val DeviceItem.isActiveLeAudioSupported: Boolean
-            get() = type == DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE && isLeAudioSupported
-
-        val DeviceItem.isMediaDevice: Boolean
-            get() =
-                cachedBluetoothDevice.uiAccessibleProfiles.any {
-                    it is A2dpProfile ||
-                        it is HearingAidProfile ||
-                        it is LeAudioProfile ||
-                        it is HeadsetProfile
-                }
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt
index 7280489..7ed5629 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt
@@ -23,7 +23,6 @@
 import com.android.settingslib.bluetooth.CachedBluetoothDevice
 import com.android.settingslib.bluetooth.LocalBluetoothManager
 import com.android.settingslib.flags.Flags
-import com.android.settingslib.flags.Flags.enableLeAudioSharing
 import com.android.systemui.res.R
 
 private val backgroundOn = R.drawable.settingslib_switch_bar_bg_on
@@ -56,7 +55,7 @@
             connectionSummary: String,
             background: Int,
             actionAccessibilityLabel: String,
-            isActive: Boolean
+            isActive: Boolean,
         ): DeviceItem {
             return DeviceItem(
                 type = type,
@@ -70,7 +69,7 @@
                 background = background,
                 isEnabled = !cachedDevice.isBusy,
                 actionAccessibilityLabel = actionAccessibilityLabel,
-                isActive = isActive
+                isActive = isActive,
             )
         }
     }
@@ -80,7 +79,7 @@
     override fun isFilterMatched(
         context: Context,
         cachedDevice: CachedBluetoothDevice,
-        audioManager: AudioManager
+        audioManager: AudioManager,
     ): Boolean {
         return BluetoothUtils.isActiveMediaDevice(cachedDevice) &&
             BluetoothUtils.isAvailableMediaBluetoothDevice(cachedDevice, audioManager)
@@ -94,20 +93,20 @@
             cachedDevice.connectionSummary ?: "",
             backgroundOn,
             context.getString(actionAccessibilityLabelDisconnect),
-            isActive = true
+            isActive = true,
         )
     }
 }
 
 internal class AudioSharingMediaDeviceItemFactory(
-    private val localBluetoothManager: LocalBluetoothManager?
+    private val localBluetoothManager: LocalBluetoothManager
 ) : DeviceItemFactory() {
     override fun isFilterMatched(
         context: Context,
         cachedDevice: CachedBluetoothDevice,
-        audioManager: AudioManager
+        audioManager: AudioManager,
     ): Boolean {
-        return enableLeAudioSharing() &&
+        return BluetoothUtils.isAudioSharingEnabled() &&
             BluetoothUtils.hasConnectedBroadcastSource(cachedDevice, localBluetoothManager)
     }
 
@@ -120,24 +119,24 @@
                 ?: context.getString(audioSharing),
             if (cachedDevice.isBusy) backgroundOffBusy else backgroundOn,
             "",
-            isActive = !cachedDevice.isBusy
+            isActive = !cachedDevice.isBusy,
         )
     }
 }
 
 internal class AvailableAudioSharingMediaDeviceItemFactory(
-    private val localBluetoothManager: LocalBluetoothManager?
+    private val localBluetoothManager: LocalBluetoothManager
 ) : AvailableMediaDeviceItemFactory() {
     override fun isFilterMatched(
         context: Context,
         cachedDevice: CachedBluetoothDevice,
-        audioManager: AudioManager
+        audioManager: AudioManager,
     ): Boolean {
         return BluetoothUtils.isAudioSharingEnabled() &&
             super.isFilterMatched(context, cachedDevice, audioManager) &&
             BluetoothUtils.isAvailableAudioSharingMediaBluetoothDevice(
                 cachedDevice,
-                localBluetoothManager
+                localBluetoothManager,
             )
     }
 
@@ -151,7 +150,7 @@
             ),
             if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff,
             "",
-            isActive = false
+            isActive = false,
         )
     }
 }
@@ -160,7 +159,7 @@
     override fun isFilterMatched(
         context: Context,
         cachedDevice: CachedBluetoothDevice,
-        audioManager: AudioManager
+        audioManager: AudioManager,
     ): Boolean {
         return BluetoothUtils.isActiveMediaDevice(cachedDevice) &&
             BluetoothUtils.isAvailableHearingDevice(cachedDevice)
@@ -171,7 +170,7 @@
     override fun isFilterMatched(
         context: Context,
         cachedDevice: CachedBluetoothDevice,
-        audioManager: AudioManager
+        audioManager: AudioManager,
     ): Boolean {
         return !BluetoothUtils.isActiveMediaDevice(cachedDevice) &&
             BluetoothUtils.isAvailableMediaBluetoothDevice(cachedDevice, audioManager)
@@ -186,7 +185,7 @@
                 ?: context.getString(connected),
             if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff,
             context.getString(actionAccessibilityLabelActivate),
-            isActive = false
+            isActive = false,
         )
     }
 }
@@ -195,7 +194,7 @@
     override fun isFilterMatched(
         context: Context,
         cachedDevice: CachedBluetoothDevice,
-        audioManager: AudioManager
+        audioManager: AudioManager,
     ): Boolean {
         return !BluetoothUtils.isActiveMediaDevice(cachedDevice) &&
             BluetoothUtils.isAvailableHearingDevice(cachedDevice)
@@ -206,7 +205,7 @@
     override fun isFilterMatched(
         context: Context,
         cachedDevice: CachedBluetoothDevice,
-        audioManager: AudioManager
+        audioManager: AudioManager,
     ): Boolean {
         return if (Flags.enableHideExclusivelyManagedBluetoothDevice()) {
             !BluetoothUtils.isExclusivelyManagedBluetoothDevice(context, cachedDevice.device) &&
@@ -225,7 +224,7 @@
                 ?: context.getString(connected),
             if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff,
             context.getString(actionAccessibilityLabelDisconnect),
-            isActive = false
+            isActive = false,
         )
     }
 }
@@ -234,7 +233,7 @@
     override fun isFilterMatched(
         context: Context,
         cachedDevice: CachedBluetoothDevice,
-        audioManager: AudioManager
+        audioManager: AudioManager,
     ): Boolean {
         return if (Flags.enableHideExclusivelyManagedBluetoothDevice()) {
             !BluetoothUtils.isExclusivelyManagedBluetoothDevice(context, cachedDevice.device) &&
@@ -254,7 +253,7 @@
                 ?: context.getString(saved),
             if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff,
             context.getString(actionAccessibilityLabelActivate),
-            isActive = false
+            isActive = false,
         )
     }
 }
@@ -263,12 +262,12 @@
     override fun isFilterMatched(
         context: Context,
         cachedDevice: CachedBluetoothDevice,
-        audioManager: AudioManager
+        audioManager: AudioManager,
     ): Boolean {
         return if (Flags.enableHideExclusivelyManagedBluetoothDevice()) {
             !BluetoothUtils.isExclusivelyManagedBluetoothDevice(
                 context,
-                cachedDevice.getDevice()
+                cachedDevice.getDevice(),
             ) &&
                 cachedDevice.isHearingAidDevice &&
                 cachedDevice.bondState == BluetoothDevice.BOND_BONDED &&
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt
index 9114eca..01b84da 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt
@@ -39,6 +39,7 @@
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.asSharedFlow
 import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.shareIn
 import kotlinx.coroutines.isActive
 import kotlinx.coroutines.withContext
@@ -54,6 +55,8 @@
     private val localBluetoothManager: LocalBluetoothManager?,
     private val systemClock: SystemClock,
     private val logger: BluetoothTileDialogLogger,
+    private val deviceItemFactoryList: List<@JvmSuppressWildcards DeviceItemFactory>,
+    private val deviceItemDisplayPriority: List<@JvmSuppressWildcards DeviceItemType>,
     @Application private val coroutineScope: CoroutineScope,
     @Background private val backgroundDispatcher: CoroutineDispatcher,
 ) {
@@ -67,7 +70,7 @@
     internal val showSeeAllUpdate
         get() = mutableShowSeeAllUpdate.asStateFlow()
 
-    internal val deviceItemUpdateRequest: SharedFlow<Unit> =
+    val deviceItemUpdateRequest: SharedFlow<Unit> =
         conflatedCallbackFlow {
                 val listener =
                     object : BluetoothCallback {
@@ -112,28 +115,9 @@
                 localBluetoothManager?.eventManager?.registerCallback(listener)
                 awaitClose { localBluetoothManager?.eventManager?.unregisterCallback(listener) }
             }
+            .flowOn(backgroundDispatcher)
             .shareIn(coroutineScope, SharingStarted.WhileSubscribed(replayExpirationMillis = 0))
 
-    private var deviceItemFactoryList: List<DeviceItemFactory> =
-        listOf(
-            ActiveMediaDeviceItemFactory(),
-            AudioSharingMediaDeviceItemFactory(localBluetoothManager),
-            AvailableAudioSharingMediaDeviceItemFactory(localBluetoothManager),
-            AvailableMediaDeviceItemFactory(),
-            ConnectedDeviceItemFactory(),
-            SavedDeviceItemFactory()
-        )
-
-    private var displayPriority: List<DeviceItemType> =
-        listOf(
-            DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE,
-            DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE,
-            DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE,
-            DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE,
-            DeviceItemType.CONNECTED_BLUETOOTH_DEVICE,
-            DeviceItemType.SAVED_BLUETOOTH_DEVICE,
-        )
-
     internal suspend fun updateDeviceItems(context: Context, trigger: DeviceFetchTrigger) {
         withContext(backgroundDispatcher) {
             val start = systemClock.elapsedRealtime()
@@ -144,7 +128,7 @@
                             .firstOrNull { it.isFilterMatched(context, cachedDevice, audioManager) }
                             ?.create(context, cachedDevice)
                     }
-                    .sort(displayPriority, bluetoothAdapter?.mostRecentlyConnectedDevices)
+                    .sort(deviceItemDisplayPriority, bluetoothAdapter?.mostRecentlyConnectedDevices)
             // Only emit when the job is not cancelled
             if (isActive) {
                 mutableDeviceItemUpdate.tryEmit(deviceItems.take(MAX_DEVICE_ITEM_ENTRY))
@@ -176,14 +160,6 @@
         )
     }
 
-    internal fun setDeviceItemFactoryListForTesting(list: List<DeviceItemFactory>) {
-        deviceItemFactoryList = list
-    }
-
-    internal fun setDisplayPriorityForTesting(list: List<DeviceItemType>) {
-        displayPriority = list
-    }
-
     companion object {
         private const val TAG = "DeviceItemInteractor"
         private const val MAX_DEVICE_ITEM_ENTRY = 3
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/dagger/AudioSharingModule.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/dagger/AudioSharingModule.kt
new file mode 100644
index 0000000..50970a5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/dagger/AudioSharingModule.kt
@@ -0,0 +1,128 @@
+/*
+ * 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.bluetooth.qsdialog.dagger
+
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.settingslib.flags.Flags
+import com.android.settingslib.volume.data.repository.AudioSharingRepository as SettingsLibAudioSharingRepository
+import com.android.systemui.bluetooth.qsdialog.ActiveMediaDeviceItemFactory
+import com.android.systemui.bluetooth.qsdialog.AudioSharingDeviceItemActionInteractorImpl
+import com.android.systemui.bluetooth.qsdialog.AudioSharingInteractor
+import com.android.systemui.bluetooth.qsdialog.AudioSharingInteractorEmptyImpl
+import com.android.systemui.bluetooth.qsdialog.AudioSharingInteractorImpl
+import com.android.systemui.bluetooth.qsdialog.AudioSharingMediaDeviceItemFactory
+import com.android.systemui.bluetooth.qsdialog.AudioSharingRepository
+import com.android.systemui.bluetooth.qsdialog.AudioSharingRepositoryEmptyImpl
+import com.android.systemui.bluetooth.qsdialog.AudioSharingRepositoryImpl
+import com.android.systemui.bluetooth.qsdialog.AvailableAudioSharingMediaDeviceItemFactory
+import com.android.systemui.bluetooth.qsdialog.AvailableMediaDeviceItemFactory
+import com.android.systemui.bluetooth.qsdialog.ConnectedDeviceItemFactory
+import com.android.systemui.bluetooth.qsdialog.DeviceItemActionInteractor
+import com.android.systemui.bluetooth.qsdialog.DeviceItemActionInteractorImpl
+import com.android.systemui.bluetooth.qsdialog.DeviceItemFactory
+import com.android.systemui.bluetooth.qsdialog.DeviceItemType
+import com.android.systemui.bluetooth.qsdialog.SavedDeviceItemFactory
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import dagger.Lazy
+import dagger.Module
+import dagger.Provides
+import kotlinx.coroutines.CoroutineDispatcher
+
+/** Dagger module for audio sharing code for BT QS dialog */
+@Module
+interface AudioSharingModule {
+
+    companion object {
+        @Provides
+        @SysUISingleton
+        fun provideAudioSharingRepository(
+            localBluetoothManager: LocalBluetoothManager?,
+            settingsLibAudioSharingRepository: SettingsLibAudioSharingRepository,
+            @Background backgroundDispatcher: CoroutineDispatcher,
+        ): AudioSharingRepository =
+            if (
+                Flags.enableLeAudioSharing() &&
+                    Flags.audioSharingQsDialogImprovement() &&
+                    localBluetoothManager != null
+            ) {
+                AudioSharingRepositoryImpl(
+                    localBluetoothManager,
+                    settingsLibAudioSharingRepository,
+                    backgroundDispatcher,
+                )
+            } else {
+                AudioSharingRepositoryEmptyImpl()
+            }
+
+        @Provides
+        @SysUISingleton
+        fun provideAudioSharingInteractor(
+            localBluetoothManager: LocalBluetoothManager?,
+            impl: Lazy<AudioSharingInteractorImpl>,
+            emptyImpl: Lazy<AudioSharingInteractorEmptyImpl>,
+        ): AudioSharingInteractor =
+            if (Flags.enableLeAudioSharing() && localBluetoothManager != null) {
+                impl.get()
+            } else {
+                emptyImpl.get()
+            }
+
+        @Provides
+        @SysUISingleton
+        fun provideDeviceItemActionInteractor(
+            localBluetoothManager: LocalBluetoothManager?,
+            audioSharingImpl: Lazy<AudioSharingDeviceItemActionInteractorImpl>,
+            impl: Lazy<DeviceItemActionInteractorImpl>,
+        ): DeviceItemActionInteractor =
+            if (Flags.enableLeAudioSharing() && localBluetoothManager != null) {
+                audioSharingImpl.get()
+            } else {
+                impl.get()
+            }
+
+        @Provides
+        @SysUISingleton
+        fun provideDeviceItemFactoryList(
+            localBluetoothManager: LocalBluetoothManager?
+        ): List<DeviceItemFactory> = buildList {
+            add(ActiveMediaDeviceItemFactory())
+            if (Flags.enableLeAudioSharing() && localBluetoothManager != null) {
+                add(AudioSharingMediaDeviceItemFactory(localBluetoothManager))
+                add(AvailableAudioSharingMediaDeviceItemFactory(localBluetoothManager))
+            }
+            add(AvailableMediaDeviceItemFactory())
+            add(ConnectedDeviceItemFactory())
+            add(SavedDeviceItemFactory())
+        }
+
+        @Provides
+        @SysUISingleton
+        fun provideDeviceItemDisplayPriority(
+            localBluetoothManager: LocalBluetoothManager?
+        ): List<DeviceItemType> = buildList {
+            add(DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE)
+            if (Flags.enableLeAudioSharing() && localBluetoothManager != null) {
+                add(DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE)
+                add(DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE)
+            }
+            add(DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE)
+            add(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
+            add(DeviceItemType.SAVED_BLUETOOTH_DEVICE)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
index 3ae9250..6508e4b5 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
@@ -27,7 +27,6 @@
 import android.util.Log
 import android.view.accessibility.AccessibilityEvent
 import android.view.accessibility.AccessibilityManager
-import androidx.activity.result.ActivityResultLauncher
 import com.android.internal.logging.UiEventLogger
 import com.android.systemui.communal.dagger.CommunalModule.Companion.LAUNCHER_PACKAGE
 import com.android.systemui.communal.data.model.CommunalWidgetCategories
@@ -184,10 +183,10 @@
 
     val isIdleOnCommunal: StateFlow<Boolean> = communalInteractor.isIdleOnCommunal
 
-    /** Launch the widget picker activity using the given {@link ActivityResultLauncher}. */
+    /** Launch the widget picker activity using the given startActivity method. */
     suspend fun onOpenWidgetPicker(
         resources: Resources,
-        activityLauncher: ActivityResultLauncher<Intent>,
+        startActivity: (intent: Intent) -> Unit,
     ): Boolean =
         withContext(backgroundDispatcher) {
             val widgets = communalInteractor.widgetContent.first()
@@ -199,7 +198,7 @@
                 }
             getWidgetPickerActivityIntent(resources, excludeList)?.let {
                 try {
-                    activityLauncher.launch(it)
+                    startActivity(it)
                     return@withContext true
                 } catch (e: Exception) {
                     Log.e(TAG, "Failed to launch widget picker activity", e)
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
index 6228ac5..8c14d63 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
@@ -27,8 +27,6 @@
 import android.view.WindowInsets
 import androidx.activity.ComponentActivity
 import androidx.activity.compose.setContent
-import androidx.activity.result.ActivityResultLauncher
-import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
@@ -51,6 +49,7 @@
 import com.android.systemui.log.core.Logger
 import com.android.systemui.log.dagger.CommunalLog
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.settings.UserTracker
 import javax.inject.Inject
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.launch
@@ -64,12 +63,15 @@
     private val uiEventLogger: UiEventLogger,
     private val widgetConfiguratorFactory: WidgetConfigurationController.Factory,
     private val widgetSection: CommunalAppWidgetSection,
+    private val userTracker: UserTracker,
     @CommunalLog logBuffer: LogBuffer,
 ) : ComponentActivity() {
     companion object {
         private const val TAG = "EditWidgetsActivity"
         private const val EXTRA_IS_PENDING_WIDGET_DRAG = "is_pending_widget_drag"
         const val EXTRA_OPEN_WIDGET_PICKER_ON_START = "open_widget_picker_on_start"
+
+        private const val REQUEST_CODE_WIDGET_PICKER = 200
     }
 
     /**
@@ -110,7 +112,7 @@
                 object : ActivityLifecycleCallbacks {
                     override fun onActivityCreated(
                         activity: Activity,
-                        savedInstanceState: Bundle?
+                        savedInstanceState: Bundle?,
                     ) {
                         waitingForResult =
                             savedInstanceState?.getBoolean(STATE_EXTRA_IS_WAITING_FOR_RESULT)
@@ -172,41 +174,6 @@
         if (communalEditWidgetsActivityFinishFix()) ActivityControllerImpl(this)
         else NopActivityController()
 
-    private val addWidgetActivityLauncher: ActivityResultLauncher<Intent> =
-        registerForActivityResult(StartActivityForResult()) { result ->
-            when (result.resultCode) {
-                RESULT_OK -> {
-                    uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_WIDGET_PICKER_SHOWN)
-
-                    result.data?.let { intent ->
-                        val isPendingWidgetDrag =
-                            intent.getBooleanExtra(EXTRA_IS_PENDING_WIDGET_DRAG, false)
-                        // Nothing to do when a widget is being dragged & dropped. The drop
-                        // target in the communal grid will receive the widget to be added (if
-                        // the user drops it over).
-                        if (!isPendingWidgetDrag) {
-                            val (componentName, user) = getWidgetExtraFromIntent(intent)
-                            if (componentName != null && user != null) {
-                                // Add widget at the end.
-                                communalViewModel.onAddWidget(
-                                    componentName,
-                                    user,
-                                    configurator = widgetConfigurator,
-                                )
-                            } else {
-                                run { Log.w(TAG, "No AppWidgetProviderInfo found in result.") }
-                            }
-                        }
-                    } ?: run { Log.w(TAG, "No data in result.") }
-                }
-                else ->
-                    Log.w(
-                        TAG,
-                        "Failed to receive result from widget picker, code=${result.resultCode}"
-                    )
-            }
-        }
-
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
 
@@ -226,8 +193,7 @@
             PlatformTheme {
                 Box(
                     modifier =
-                        Modifier.fillMaxSize()
-                            .background(MaterialTheme.colorScheme.surfaceDim),
+                        Modifier.fillMaxSize().background(MaterialTheme.colorScheme.surfaceDim)
                 ) {
                     CommunalHub(
                         viewModel = communalViewModel,
@@ -274,7 +240,13 @@
 
     private fun onOpenWidgetPicker() {
         lifecycleScope.launch {
-            communalViewModel.onOpenWidgetPicker(resources, addWidgetActivityLauncher)
+            communalViewModel.onOpenWidgetPicker(resources) { intent: Intent ->
+                startActivityForResultAsUser(
+                    intent,
+                    REQUEST_CODE_WIDGET_PICKER,
+                    userTracker.userHandle,
+                )
+            }
         }
     }
 
@@ -285,7 +257,7 @@
             communalViewModel.changeScene(
                 scene = CommunalScenes.Communal,
                 loggingReason = "edit mode closing",
-                transitionKey = CommunalTransitionKeys.FromEditMode
+                transitionKey = CommunalTransitionKeys.FromEditMode,
             )
 
             // Wait for the current scene to be idle on communal.
@@ -309,7 +281,7 @@
         flagsMask: Int,
         flagsValues: Int,
         extraFlags: Int,
-        options: Bundle?
+        options: Bundle?,
     ) {
         activityController.onWaitingForResult(true)
         super.startIntentSenderForResult(
@@ -319,15 +291,46 @@
             flagsMask,
             flagsValues,
             extraFlags,
-            options
+            options,
         )
     }
 
     override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
         activityController.onWaitingForResult(false)
         super.onActivityResult(requestCode, resultCode, data)
-        if (requestCode == WidgetConfigurationController.REQUEST_CODE) {
-            widgetConfigurator.setConfigurationResult(resultCode)
+
+        when (requestCode) {
+            WidgetConfigurationController.REQUEST_CODE ->
+                widgetConfigurator.setConfigurationResult(resultCode)
+            REQUEST_CODE_WIDGET_PICKER -> {
+                if (resultCode != RESULT_OK) {
+                    Log.w(TAG, "Failed to receive result from widget picker, code=$resultCode")
+                    return
+                }
+
+                uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_WIDGET_PICKER_SHOWN)
+
+                data?.let { intent ->
+                    val isPendingWidgetDrag =
+                        intent.getBooleanExtra(EXTRA_IS_PENDING_WIDGET_DRAG, false)
+                    // Nothing to do when a widget is being dragged & dropped. The drop
+                    // target in the communal grid will receive the widget to be added (if
+                    // the user drops it over).
+                    if (!isPendingWidgetDrag) {
+                        val (componentName, user) = getWidgetExtraFromIntent(intent)
+                        if (componentName != null && user != null) {
+                            // Add widget at the end.
+                            communalViewModel.onAddWidget(
+                                componentName,
+                                user,
+                                configurator = widgetConfigurator,
+                            )
+                        } else {
+                            run { Log.w(TAG, "No AppWidgetProviderInfo found in result.") }
+                        }
+                    }
+                } ?: run { Log.w(TAG, "No data in result.") }
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt b/packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt
index b56ed8c..589dbf9 100644
--- a/packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt
@@ -24,6 +24,8 @@
 import com.android.systemui.display.data.repository.DisplayRepositoryImpl
 import com.android.systemui.display.data.repository.DisplayScopeRepository
 import com.android.systemui.display.data.repository.DisplayScopeRepositoryImpl
+import com.android.systemui.display.data.repository.DisplayWindowPropertiesRepository
+import com.android.systemui.display.data.repository.DisplayWindowPropertiesRepositoryImpl
 import com.android.systemui.display.data.repository.FocusedDisplayRepository
 import com.android.systemui.display.data.repository.FocusedDisplayRepositoryImpl
 import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor
@@ -58,6 +60,11 @@
 
     @Binds fun displayScopeRepository(impl: DisplayScopeRepositoryImpl): DisplayScopeRepository
 
+    @Binds
+    fun displayWindowPropertiesRepository(
+        impl: DisplayWindowPropertiesRepositoryImpl
+    ): DisplayWindowPropertiesRepository
+
     companion object {
         @Provides
         @SysUISingleton
@@ -72,5 +79,19 @@
                 CoreStartable.NOP
             }
         }
+
+        @Provides
+        @SysUISingleton
+        @IntoMap
+        @ClassKey(DisplayWindowPropertiesRepository::class)
+        fun displayWindowPropertiesRepoAsCoreStartable(
+            repoLazy: Lazy<DisplayWindowPropertiesRepositoryImpl>
+        ): CoreStartable {
+            return if (StatusBarConnectedDisplays.isEnabled) {
+                return repoLazy.get()
+            } else {
+                CoreStartable.NOP
+            }
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepository.kt
new file mode 100644
index 0000000..88d3a28
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepository.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.display.data.repository
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.view.Display
+import android.view.WindowManager
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.display.shared.model.DisplayWindowProperties
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
+import com.google.common.collect.HashBasedTable
+import com.google.common.collect.Table
+import java.io.PrintWriter
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/** Provides per display instances of [DisplayWindowProperties]. */
+interface DisplayWindowPropertiesRepository {
+
+    /**
+     * Returns a [DisplayWindowProperties] instance for a given display id and window type.
+     *
+     * @throws IllegalArgumentException if no display with the given display id exists.
+     */
+    fun get(
+        displayId: Int,
+        @WindowManager.LayoutParams.WindowType windowType: Int,
+    ): DisplayWindowProperties
+}
+
+@SysUISingleton
+class DisplayWindowPropertiesRepositoryImpl
+@Inject
+constructor(
+    @Background private val backgroundApplicationScope: CoroutineScope,
+    private val globalContext: Context,
+    private val globalWindowManager: WindowManager,
+    private val displayRepository: DisplayRepository,
+) : DisplayWindowPropertiesRepository, CoreStartable {
+
+    init {
+        StatusBarConnectedDisplays.assertInNewMode()
+    }
+
+    private val properties: Table<Int, Int, DisplayWindowProperties> = HashBasedTable.create()
+
+    override fun get(
+        displayId: Int,
+        @WindowManager.LayoutParams.WindowType windowType: Int,
+    ): DisplayWindowProperties {
+        val display =
+            displayRepository.getDisplay(displayId)
+                ?: throw IllegalArgumentException("Display with id $displayId doesn't exist")
+        return properties.get(displayId, windowType)
+            ?: create(display, windowType).also { properties.put(displayId, windowType, it) }
+    }
+
+    override fun start() {
+        backgroundApplicationScope.launch(
+            CoroutineName("DisplayWindowPropertiesRepositoryImpl#start")
+        ) {
+            displayRepository.displayRemovalEvent.collect { removedDisplayId ->
+                properties.row(removedDisplayId).clear()
+            }
+        }
+    }
+
+    private fun create(display: Display, windowType: Int): DisplayWindowProperties {
+        val displayId = display.displayId
+        return if (displayId == Display.DEFAULT_DISPLAY) {
+            // For the default display, we can just reuse the global/application properties.
+            // Creating a window context is expensive, therefore we avoid it.
+            DisplayWindowProperties(
+                displayId = displayId,
+                windowType = windowType,
+                context = globalContext,
+                windowManager = globalWindowManager,
+            )
+        } else {
+            val context = createWindowContext(display, windowType)
+            @SuppressLint("NonInjectedService") // Need to manually get the service
+            val windowManager = context.getSystemService(WindowManager::class.java) as WindowManager
+            DisplayWindowProperties(displayId, windowType, context, windowManager)
+        }
+    }
+
+    private fun createWindowContext(display: Display, windowType: Int): Context =
+        globalContext.createWindowContext(display, windowType, /* options= */ null).also {
+            it.setTheme(R.style.Theme_SystemUI)
+        }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        pw.write("perDisplayContexts: $properties")
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/display/shared/model/DisplayWindowProperties.kt b/packages/SystemUI/src/com/android/systemui/display/shared/model/DisplayWindowProperties.kt
new file mode 100644
index 0000000..6acc296
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/display/shared/model/DisplayWindowProperties.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.display.shared.model
+
+import android.content.Context
+import android.view.WindowManager
+
+/** Represents a display specific group of window related properties. */
+data class DisplayWindowProperties(
+    /** The id of the display associated with this instance. */
+    val displayId: Int,
+    /**
+     * The window type that was used to create the [Context] in this instance, using
+     * [Context.createWindowContext]. This is the window type that can be used when adding views to
+     * the [WindowManager] associated with this instance.
+     */
+    @WindowManager.LayoutParams.WindowType val windowType: Int,
+    /**
+     * The display specific [Context] created using [Context.createWindowContext] with window type
+     * associated with this instance.
+     */
+    val context: Context,
+
+    /**
+     * The display specific [WindowManager] instance to be used when adding windows of the type
+     * associated with this instance.
+     */
+    val windowManager: WindowManager,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
index 65c29b8..9c5231d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
@@ -92,6 +92,7 @@
 import com.android.systemui.qs.composefragment.SceneKeys.QuickQuickSettings
 import com.android.systemui.qs.composefragment.SceneKeys.QuickSettings
 import com.android.systemui.qs.composefragment.SceneKeys.toIdleSceneKey
+import com.android.systemui.qs.composefragment.ui.NotificationScrimClipParams
 import com.android.systemui.qs.composefragment.ui.notificationScrimClip
 import com.android.systemui.qs.composefragment.ui.quickQuickSettingsToQuickSettings
 import com.android.systemui.qs.composefragment.viewmodel.QSFragmentComposeViewModel
@@ -149,20 +150,12 @@
     private val notificationScrimClippingParams =
         object {
             var isEnabled by mutableStateOf(false)
-            var leftInset by mutableStateOf(0)
-            var rightInset by mutableStateOf(0)
-            var top by mutableStateOf(0)
-            var bottom by mutableStateOf(0)
-            var radius by mutableStateOf(0)
+            var params by mutableStateOf(NotificationScrimClipParams())
 
             fun dump(pw: IndentingPrintWriter) {
                 pw.printSection("NotificationScrimClippingParams") {
                     pw.println("isEnabled", isEnabled)
-                    pw.println("leftInset", "${leftInset}px")
-                    pw.println("rightInset", "${rightInset}px")
-                    pw.println("top", "${top}px")
-                    pw.println("bottom", "${bottom}px")
-                    pw.println("radius", "${radius}px")
+                    pw.println("params", params)
                 }
             }
         }
@@ -216,7 +209,7 @@
             FrameLayoutTouchPassthrough(
                 context,
                 { notificationScrimClippingParams.isEnabled },
-                { notificationScrimClippingParams.top },
+                { notificationScrimClippingParams.params.top },
             )
         frame.addView(
             composeView,
@@ -237,13 +230,7 @@
                     Modifier.windowInsetsPadding(WindowInsets.navigationBars).thenIf(
                         notificationScrimClippingParams.isEnabled
                     ) {
-                        Modifier.notificationScrimClip(
-                            notificationScrimClippingParams.leftInset,
-                            notificationScrimClippingParams.top,
-                            notificationScrimClippingParams.rightInset,
-                            notificationScrimClippingParams.bottom,
-                            notificationScrimClippingParams.radius,
-                        )
+                        Modifier.notificationScrimClip { notificationScrimClippingParams.params }
                     },
             ) {
                 val isEditing by
@@ -445,13 +432,14 @@
         fullWidth: Boolean,
     ) {
         notificationScrimClippingParams.isEnabled = visible
-        notificationScrimClippingParams.top = top
-        notificationScrimClippingParams.bottom = bottom
-        // Full width means that QS will show in the entire width allocated to it (for example
-        // phone) vs. showing in a narrower column (for example, tablet portrait).
-        notificationScrimClippingParams.leftInset = if (fullWidth) 0 else leftInset
-        notificationScrimClippingParams.rightInset = if (fullWidth) 0 else rightInset
-        notificationScrimClippingParams.radius = cornerRadius
+        notificationScrimClippingParams.params =
+            NotificationScrimClipParams(
+                top,
+                bottom,
+                if (fullWidth) 0 else leftInset,
+                if (fullWidth) 0 else rightInset,
+                cornerRadius,
+            )
     }
 
     override fun isFullyCollapsed(): Boolean {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt
index 93c6445..c912bd5 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt
@@ -31,87 +31,73 @@
  * ([ClipOp.Difference]) a `RoundRect(-leftInset, top, width + rightInset, bottom, radius, radius)`
  * from the QS container.
  */
-fun Modifier.notificationScrimClip(
-    leftInset: Int,
-    top: Int,
-    rightInset: Int,
-    bottom: Int,
-    radius: Int
-): Modifier {
-    return this then NotificationScrimClipElement(leftInset, top, rightInset, bottom, radius)
+fun Modifier.notificationScrimClip(clipParams: () -> NotificationScrimClipParams): Modifier {
+    return this then NotificationScrimClipElement(clipParams)
 }
 
-private class NotificationScrimClipNode(
-    var leftInset: Float,
-    var top: Float,
-    var rightInset: Float,
-    var bottom: Float,
-    var radius: Float,
-) : DrawModifierNode, Modifier.Node() {
+private class NotificationScrimClipNode(var clipParams: () -> NotificationScrimClipParams) :
+    DrawModifierNode, Modifier.Node() {
     private val path = Path()
 
-    var invalidated = true
+    private var lastClipParams = NotificationScrimClipParams()
 
     override fun ContentDrawScope.draw() {
-        if (invalidated) {
+        val newClipParams = clipParams()
+        if (newClipParams != lastClipParams) {
+            lastClipParams = newClipParams
+            applyClipParams(path, lastClipParams)
+        }
+        clipPath(path, ClipOp.Difference) { this@draw.drawContent() }
+    }
+
+    private fun ContentDrawScope.applyClipParams(
+        path: Path,
+        clipParams: NotificationScrimClipParams,
+    ) {
+        with(clipParams) {
             path.rewind()
             path
                 .asAndroidPath()
                 .addRoundRect(
-                    -leftInset,
-                    top,
+                    -leftInset.toFloat(),
+                    top.toFloat(),
                     size.width + rightInset,
-                    bottom,
-                    radius,
-                    radius,
-                    android.graphics.Path.Direction.CW
+                    bottom.toFloat(),
+                    radius.toFloat(),
+                    radius.toFloat(),
+                    android.graphics.Path.Direction.CW,
                 )
-            invalidated = false
         }
-        clipPath(path, ClipOp.Difference) { this@draw.drawContent() }
     }
 }
 
-private data class NotificationScrimClipElement(
-    val leftInset: Int,
-    val top: Int,
-    val rightInset: Int,
-    val bottom: Int,
-    val radius: Int,
-) : ModifierNodeElement<NotificationScrimClipNode>() {
+private data class NotificationScrimClipElement(val clipParams: () -> NotificationScrimClipParams) :
+    ModifierNodeElement<NotificationScrimClipNode>() {
     override fun create(): NotificationScrimClipNode {
-        return NotificationScrimClipNode(
-            leftInset.toFloat(),
-            top.toFloat(),
-            rightInset.toFloat(),
-            bottom.toFloat(),
-            radius.toFloat(),
-        )
+        return NotificationScrimClipNode(clipParams)
     }
 
     override fun update(node: NotificationScrimClipNode) {
-        val changed =
-            node.leftInset != leftInset.toFloat() ||
-                node.top != top.toFloat() ||
-                node.rightInset != rightInset.toFloat() ||
-                node.bottom != bottom.toFloat() ||
-                node.radius != radius.toFloat()
-        if (changed) {
-            node.leftInset = leftInset.toFloat()
-            node.top = top.toFloat()
-            node.rightInset = rightInset.toFloat()
-            node.bottom = bottom.toFloat()
-            node.radius = radius.toFloat()
-            node.invalidated = true
-        }
+        node.clipParams = clipParams
     }
 
     override fun InspectorInfo.inspectableProperties() {
         name = "notificationScrimClip"
-        properties["leftInset"] = leftInset
-        properties["top"] = top
-        properties["rightInset"] = rightInset
-        properties["bottom"] = bottom
-        properties["radius"] = radius
+        with(clipParams()) {
+            properties["leftInset"] = leftInset
+            properties["top"] = top
+            properties["rightInset"] = rightInset
+            properties["bottom"] = bottom
+            properties["radius"] = radius
+        }
     }
 }
+
+/** Params for [notificationScrimClip]. */
+data class NotificationScrimClipParams(
+    val top: Int = 0,
+    val bottom: Int = 0,
+    val leftInset: Int = 0,
+    val rightInset: Int = 0,
+    val radius: Int = 0,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
index 73ad0e5..da04f6e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
@@ -46,6 +46,7 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.keyguard.KeyguardClockSwitch;
 import com.android.systemui.DejankUtils;
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor;
 import com.android.systemui.deviceentry.shared.model.DeviceUnlockStatus;
@@ -123,6 +124,7 @@
     private final Lazy<SceneContainerOcclusionInteractor> mSceneContainerOcclusionInteractorLazy;
     private final Lazy<KeyguardClockInteractor> mKeyguardClockInteractorLazy;
     private final Lazy<SceneBackInteractor> mSceneBackInteractorLazy;
+    private final Lazy<AlternateBouncerInteractor> mAlternateBouncerInteractorLazy;
     private int mState;
     private int mLastState;
     private int mUpcomingState;
@@ -193,7 +195,8 @@
             Lazy<SceneInteractor> sceneInteractorLazy,
             Lazy<SceneContainerOcclusionInteractor> sceneContainerOcclusionInteractor,
             Lazy<KeyguardClockInteractor> keyguardClockInteractorLazy,
-            Lazy<SceneBackInteractor> sceneBackInteractorLazy) {
+            Lazy<SceneBackInteractor> sceneBackInteractorLazy,
+            Lazy<AlternateBouncerInteractor> alternateBouncerInteractorLazy) {
         mUiEventLogger = uiEventLogger;
         mInteractionJankMonitorLazy = interactionJankMonitorLazy;
         mJavaAdapter = javaAdapter;
@@ -205,6 +208,7 @@
         mSceneContainerOcclusionInteractorLazy = sceneContainerOcclusionInteractor;
         mKeyguardClockInteractorLazy = keyguardClockInteractorLazy;
         mSceneBackInteractorLazy = sceneBackInteractorLazy;
+        mAlternateBouncerInteractorLazy = alternateBouncerInteractorLazy;
         for (int i = 0; i < HISTORY_SIZE; i++) {
             mHistoricalRecords[i] = new HistoricalState();
         }
@@ -233,6 +237,7 @@
                         mSceneInteractorLazy.get().getCurrentOverlays(),
                         mSceneBackInteractorLazy.get().getBackStack(),
                         mSceneContainerOcclusionInteractorLazy.get().getInvisibleDueToOcclusion(),
+                        mAlternateBouncerInteractorLazy.get().isVisible(),
                         this::calculateStateFromSceneFramework),
                     this::onStatusBarStateChanged);
 
@@ -693,7 +698,8 @@
             SceneKey currentScene,
             Set<OverlayKey> currentOverlays,
             SceneStack backStack,
-            boolean isOccluded) {
+            boolean isOccluded,
+            boolean alternateBouncerIsVisible) {
         SceneContainerFlag.isUnexpectedlyInLegacyMode();
 
         final boolean onBouncer = currentScene.equals(Scenes.Bouncer);
@@ -714,7 +720,8 @@
 
         final String inputLogString = "currentScene=" + currentScene.getTestTag()
                 + " currentOverlays=" + currentOverlays + " backStack=" + backStack
-                + " isUnlocked=" + isUnlocked + " isOccluded=" + isOccluded;
+                + " isUnlocked=" + isUnlocked + " isOccluded=" + isOccluded
+                + " alternateBouncerIsVisible=" + alternateBouncerIsVisible;
 
         int newState;
 
@@ -722,6 +729,7 @@
         // 1. deviceUnlockStatus.isUnlocked changes from false to true.
         // 2. Lockscreen changes to Gone, either in currentScene or in backStack.
         // 3. Bouncer is removed from currentScene or backStack, if it was present.
+        // 4. the alternate bouncer is hidden, if it was visible.
         //
         // From this function's perspective, though, deviceUnlockStatus, currentScene, and backStack
         // each update separately, and the relative order of those updates is not well-defined. This
@@ -733,6 +741,7 @@
         // 1. deviceUnlockStatus.isUnlocked is false.
         // 2. currentScene is a keyguardish scene (Lockscreen, Bouncer, or Communal).
         // 3. backStack contains a keyguardish scene (Lockscreen or Communal).
+        // 4. the alternate bouncer is visible.
 
         final boolean onKeyguardish = onLockscreen || onBouncer || onCommunal;
         final boolean overKeyguardish = overLockscreen || overCommunal;
@@ -741,7 +750,7 @@
             // Occlusion is special; even though the device is still technically on the lockscreen,
             // the UI behaves as if it is unlocked.
             newState = StatusBarState.SHADE;
-        } else if (onKeyguardish || overKeyguardish) {
+        } else if (onKeyguardish || overKeyguardish || alternateBouncerIsVisible) {
             // We get here if we are on or over a keyguardish scene, even if isUnlocked is true; we
             // want to return SHADE_LOCKED or KEYGUARD until we are also neither on nor over a
             // keyguardish scene.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt
index dac0102..10090283 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.connectivity
 
 import android.os.UserManager
+import com.android.systemui.bluetooth.qsdialog.dagger.AudioSharingModule
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags.SIGNAL_CALLBACK_DEPRECATION
 import com.android.systemui.qs.QsEventLogger
@@ -56,7 +57,7 @@
 import dagger.multibindings.IntoMap
 import dagger.multibindings.StringKey
 
-@Module
+@Module(includes = [AudioSharingModule::class])
 interface ConnectivityModule {
 
     /** Inject BluetoothTile into tileMap in QSModule */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
index cf238d5..cd1642e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
@@ -22,15 +22,20 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.LogBufferFactory
+import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
 import com.android.systemui.statusbar.data.StatusBarDataLayerModule
 import com.android.systemui.statusbar.phone.LightBarController
 import com.android.systemui.statusbar.phone.StatusBarSignalPolicy
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallLog
 import com.android.systemui.statusbar.ui.SystemBarUtilsProxyImpl
+import com.android.systemui.statusbar.window.MultiDisplayStatusBarWindowControllerStore
+import com.android.systemui.statusbar.window.SingleDisplayStatusBarWindowControllerStore
 import com.android.systemui.statusbar.window.StatusBarWindowController
 import com.android.systemui.statusbar.window.StatusBarWindowControllerImpl
+import com.android.systemui.statusbar.window.StatusBarWindowControllerStore
 import dagger.Binds
+import dagger.Lazy
 import dagger.Module
 import dagger.Provides
 import dagger.multibindings.ClassKey
@@ -62,13 +67,19 @@
     @ClassKey(StatusBarSignalPolicy::class)
     abstract fun bindStatusBarSignalPolicy(impl: StatusBarSignalPolicy): CoreStartable
 
+    @Binds
+    @SysUISingleton
+    abstract fun statusBarWindowControllerFactory(
+        implFactory: StatusBarWindowControllerImpl.Factory
+    ): StatusBarWindowController.Factory
+
     companion object {
 
         @Provides
         @SysUISingleton
-        fun statusBarWindowController(
-            context: Context?,
-            viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager?,
+        fun defaultStatusBarWindowController(
+            context: Context,
+            viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager,
             factory: StatusBarWindowControllerImpl.Factory,
         ): StatusBarWindowController {
             return factory.create(context, viewCaptureAwareWindowManager)
@@ -76,6 +87,33 @@
 
         @Provides
         @SysUISingleton
+        fun windowControllerStore(
+            multiDisplayImplLazy: Lazy<MultiDisplayStatusBarWindowControllerStore>,
+            singleDisplayImplLazy: Lazy<SingleDisplayStatusBarWindowControllerStore>,
+        ): StatusBarWindowControllerStore {
+            return if (StatusBarConnectedDisplays.isEnabled) {
+                multiDisplayImplLazy.get()
+            } else {
+                singleDisplayImplLazy.get()
+            }
+        }
+
+        @Provides
+        @SysUISingleton
+        @IntoMap
+        @ClassKey(MultiDisplayStatusBarWindowControllerStore::class)
+        fun multiDisplayControllerStoreAsCoreStartable(
+            storeLazy: Lazy<MultiDisplayStatusBarWindowControllerStore>
+        ): CoreStartable {
+            return if (StatusBarConnectedDisplays.isEnabled) {
+                storeLazy.get()
+            } else {
+                CoreStartable.NOP
+            }
+        }
+
+        @Provides
+        @SysUISingleton
         @OngoingCallLog
         fun provideOngoingCallLogBuffer(factory: LogBufferFactory): LogBuffer {
             return factory.create("OngoingCall", 75)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt
index d67947d..4e26ae8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt
@@ -90,7 +90,7 @@
                 notification = notification,
                 isGroupConversation = isGroupConversation,
                 builder = builder,
-                systemUiContext = systemUiContext
+                systemUiContext = systemUiContext,
             )
 
         val conversationData =
@@ -98,7 +98,7 @@
                 // We don't show the sender's name for one-to-one conversation
                 conversationSenderName =
                     if (isGroupConversation) conversationTextData?.senderName else null,
-                avatar = conversationAvatar
+                avatar = conversationAvatar,
             )
 
         return SingleLineViewModel(
@@ -111,7 +111,7 @@
     @JvmStatic
     fun inflateRedactedSingleLineViewModel(
         context: Context,
-        isConversation: Boolean = false
+        isConversation: Boolean = false,
     ): SingleLineViewModel {
         val conversationData =
             if (isConversation) {
@@ -122,7 +122,7 @@
                             com.android.systemui.res.R.drawable
                                 .ic_redacted_notification_single_line_icon
                         )
-                    )
+                    ),
                 )
             } else {
                 null
@@ -134,7 +134,7 @@
             context.getString(
                 com.android.systemui.res.R.string.redacted_notification_single_line_text
             ),
-            conversationData
+            conversationData,
         )
     }
 
@@ -159,11 +159,13 @@
         }
 
         // load the sender's name to display
-        val name = lastMessage.senderPerson?.name
+        // null senderPerson means the current user.
+        val name = lastMessage.senderPerson?.name ?: user.name
+
         val senderName =
             systemUiContext.resources.getString(
                 R.string.conversation_single_line_name_display,
-                if (Flags.cleanUpSpansAndNewLines()) name?.toString() else name
+                if (Flags.cleanUpSpansAndNewLines()) name?.toString() else name,
             )
 
         // We need to find back-up values for those texts if they are needed and empty
@@ -333,7 +335,7 @@
                         sender.icon
                             ?: builder.getDefaultAvatar(
                                 name = sender.name,
-                                uniqueNames = uniqueNames
+                                uniqueNames = uniqueNames,
                             )
                     lastKey = senderKey
                 } else {
@@ -341,7 +343,7 @@
                         sender.icon
                             ?: builder.getDefaultAvatar(
                                 name = sender.name,
-                                uniqueNames = uniqueNames
+                                uniqueNames = uniqueNames,
                             )
                     break
                 }
@@ -424,7 +426,7 @@
 
     private fun Notification.Builder.getDefaultAvatar(
         name: CharSequence?,
-        uniqueNames: PeopleHelper.NameToPrefixMap? = null
+        uniqueNames: PeopleHelper.NameToPrefixMap? = null,
     ): Icon {
         val layoutColor = getSmallIconColor(/* isHeader= */ false)
         if (!name.isNullOrEmpty()) {
@@ -432,7 +434,7 @@
             return peopleHelper.createAvatarSymbol(
                 /* name = */ name,
                 /* symbol = */ symbol,
-                /* layoutColor = */ layoutColor
+                /* layoutColor = */ layoutColor,
             )
         }
         // If name is null, create default avatar with background color
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt
index f5cfc8c..e0bf00f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt
@@ -26,6 +26,7 @@
 import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
 import android.net.NetworkCapabilities.TRANSPORT_WIFI
 import android.net.vcn.VcnTransportInfo
+import android.net.vcn.VcnUtils
 import android.net.wifi.WifiInfo
 import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
 import androidx.annotation.ArrayRes
@@ -161,7 +162,9 @@
         defaultNetworkCapabilities
             .map { networkCapabilities ->
                 networkCapabilities?.run {
-                    val subId = (transportInfo as? VcnTransportInfo)?.subId
+                    val subId =
+                        VcnUtils.getSubIdFromVcnCaps(connectivityManager, networkCapabilities)
+
                     // Never return an INVALID_SUBSCRIPTION_ID (-1)
                     if (subId != INVALID_SUBSCRIPTION_ID) {
                         subId
@@ -245,9 +248,9 @@
          * info.
          */
         fun NetworkCapabilities.getMainOrUnderlyingWifiInfo(
-            connectivityManager: ConnectivityManager,
+            connectivityManager: ConnectivityManager
         ): WifiInfo? {
-            val mainWifiInfo = this.getMainWifiInfo()
+            val mainWifiInfo = this.getMainWifiInfo(connectivityManager)
             if (mainWifiInfo != null) {
                 return mainWifiInfo
             }
@@ -264,7 +267,9 @@
             // eventually traced to a wifi or carrier merged connection. So, check those underlying
             // networks for possible wifi information as well. See b/225902574.
             return this.underlyingNetworks?.firstNotNullOfOrNull { underlyingNetwork ->
-                connectivityManager.getNetworkCapabilities(underlyingNetwork)?.getMainWifiInfo()
+                connectivityManager
+                    .getNetworkCapabilities(underlyingNetwork)
+                    ?.getMainWifiInfo(connectivityManager)
             }
         }
 
@@ -272,7 +277,9 @@
          * Checks the network capabilities for wifi info, but does *not* check the underlying
          * networks. See [getMainOrUnderlyingWifiInfo].
          */
-        private fun NetworkCapabilities.getMainWifiInfo(): WifiInfo? {
+        private fun NetworkCapabilities.getMainWifiInfo(
+            connectivityManager: ConnectivityManager
+        ): WifiInfo? {
             // Wifi info can either come from a WIFI Transport, or from a CELLULAR transport for
             // virtual networks like VCN.
             val canHaveWifiInfo =
@@ -286,7 +293,7 @@
                 // [com.android.settingslib.Utils.tryGetWifiInfoForVcn]. It's copied instead of
                 // re-used because it makes the logic here clearer, and because the method will be
                 // removed once this pipeline is fully launched.
-                is VcnTransportInfo -> currentTransportInfo.wifiInfo
+                is VcnTransportInfo -> VcnUtils.getWifiInfoFromVcnCaps(connectivityManager, this)
                 is WifiInfo -> currentTransportInfo
                 else -> null
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt
index 421e5c4..e8dc934 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt
@@ -16,8 +16,10 @@
 
 package com.android.systemui.statusbar.window
 
+import android.content.Context
 import android.view.View
 import android.view.ViewGroup
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager
 import com.android.systemui.animation.ActivityTransitionAnimator
 import com.android.systemui.fragments.FragmentHostManager
 import java.util.Optional
@@ -73,4 +75,11 @@
      *   this#setForceStatusBarVisible} together and use some sort of ranking system instead.
      */
     fun setOngoingProcessRequiresStatusBarVisible(visible: Boolean)
+
+    interface Factory {
+        fun create(
+            context: Context,
+            viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager,
+        ): StatusBarWindowController
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java
index 1ee7cf3..d709e5a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java
@@ -354,11 +354,13 @@
     }
 
     @AssistedFactory
-    public interface Factory {
+    public interface Factory extends StatusBarWindowController.Factory {
         /** Creates a new instance. */
+        @NonNull
+        @Override
         StatusBarWindowControllerImpl create(
-                Context context,
-                ViewCaptureAwareWindowManager viewCaptureAwareWindowManager);
+                @NonNull Context context,
+                @NonNull ViewCaptureAwareWindowManager viewCaptureAwareWindowManager);
     }
 
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerStore.kt b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerStore.kt
new file mode 100644
index 0000000..5f30b37
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerStore.kt
@@ -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.systemui.statusbar.window
+
+import android.view.Display
+import android.view.WindowManager
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.display.data.repository.DisplayRepository
+import com.android.systemui.display.data.repository.DisplayWindowPropertiesRepository
+import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
+import java.util.concurrent.ConcurrentHashMap
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/** Store that allows to retrieve per display instances of [StatusBarWindowController]. */
+interface StatusBarWindowControllerStore {
+    /**
+     * The instance for the default/main display of the device. For example, on a phone or a tablet,
+     * the default display is the internal/built-in display of the device.
+     *
+     * Note that the id of the default display is [Display.DEFAULT_DISPLAY].
+     */
+    val defaultDisplay: StatusBarWindowController
+
+    /**
+     * Returns an instance for a specific display id.
+     *
+     * @throws IllegalArgumentException if [displayId] doesn't match the id of any existing
+     *   displays.
+     */
+    fun forDisplay(displayId: Int): StatusBarWindowController
+}
+
+@SysUISingleton
+class MultiDisplayStatusBarWindowControllerStore
+@Inject
+constructor(
+    @Background private val backgroundApplicationScope: CoroutineScope,
+    private val controllerFactory: StatusBarWindowController.Factory,
+    private val displayWindowPropertiesRepository: DisplayWindowPropertiesRepository,
+    private val viewCaptureAwareWindowManagerFactory: ViewCaptureAwareWindowManager.Factory,
+    private val displayRepository: DisplayRepository,
+) : StatusBarWindowControllerStore, CoreStartable {
+
+    init {
+        StatusBarConnectedDisplays.assertInNewMode()
+    }
+
+    private val perDisplayControllers = ConcurrentHashMap<Int, StatusBarWindowController>()
+
+    override fun start() {
+        backgroundApplicationScope.launch(CoroutineName("StatusBarWindowController#start")) {
+            displayRepository.displayRemovalEvent.collect { displayId ->
+                perDisplayControllers.remove(displayId)
+            }
+        }
+    }
+
+    override val defaultDisplay: StatusBarWindowController
+        get() = forDisplay(Display.DEFAULT_DISPLAY)
+
+    override fun forDisplay(displayId: Int): StatusBarWindowController {
+        if (displayRepository.getDisplay(displayId) == null) {
+            throw IllegalArgumentException("Display with id $displayId doesn't exist.")
+        }
+        return perDisplayControllers.computeIfAbsent(displayId) {
+            createControllerForDisplay(displayId)
+        }
+    }
+
+    private fun createControllerForDisplay(displayId: Int): StatusBarWindowController {
+        val statusBarDisplayContext =
+            displayWindowPropertiesRepository.get(
+                displayId = displayId,
+                windowType = WindowManager.LayoutParams.TYPE_STATUS_BAR,
+            )
+        val viewCaptureAwareWindowManager =
+            viewCaptureAwareWindowManagerFactory.create(statusBarDisplayContext.windowManager)
+        return controllerFactory.create(
+            statusBarDisplayContext.context,
+            viewCaptureAwareWindowManager,
+        )
+    }
+}
+
+@SysUISingleton
+class SingleDisplayStatusBarWindowControllerStore
+@Inject
+constructor(private val controller: StatusBarWindowController) : StatusBarWindowControllerStore {
+
+    init {
+        StatusBarConnectedDisplays.assertInLegacyMode()
+    }
+
+    override val defaultDisplay = controller
+
+    override fun forDisplay(displayId: Int) = controller
+}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
index bfc5429..6879a34 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
@@ -27,10 +27,7 @@
 import com.android.systemui.touchpad.tutorial.ui.gesture.BackGestureMonitor
 
 @Composable
-fun BackGestureTutorialScreen(
-    onDoneButtonClicked: () -> Unit,
-    onBack: () -> Unit,
-) {
+fun BackGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) {
     val screenConfig =
         TutorialScreenConfig(
             colors = rememberScreenColors(),
@@ -39,18 +36,20 @@
                     titleResId = R.string.touchpad_back_gesture_action_title,
                     bodyResId = R.string.touchpad_back_gesture_guidance,
                     titleSuccessResId = R.string.touchpad_back_gesture_success_title,
-                    bodySuccessResId = R.string.touchpad_back_gesture_success_body
+                    bodySuccessResId = R.string.touchpad_back_gesture_success_body,
                 ),
             animations =
                 TutorialScreenConfig.Animations(
                     educationResId = R.raw.trackpad_back_edu,
-                    successResId = R.raw.trackpad_back_success
-                )
+                    successResId = R.raw.trackpad_back_success,
+                ),
         )
     val gestureMonitorProvider =
         DistanceBasedGestureMonitorProvider(
             monitorFactory = { distanceThresholdPx, gestureStateCallback ->
-                BackGestureMonitor(distanceThresholdPx, gestureStateCallback)
+                BackGestureMonitor(distanceThresholdPx).also {
+                    it.addGestureStateCallback(gestureStateCallback)
+                }
             }
         )
     GestureTutorialScreen(screenConfig, gestureMonitorProvider, onDoneButtonClicked, onBack)
@@ -67,7 +66,7 @@
             rememberColorFilterProperty(".tertiaryFixedDim", tertiaryFixedDim),
             rememberColorFilterProperty(".onTertiaryFixed", onTertiaryFixed),
             rememberColorFilterProperty(".onTertiary", onTertiary),
-            rememberColorFilterProperty(".onTertiaryFixedVariant", onTertiaryFixedVariant)
+            rememberColorFilterProperty(".onTertiaryFixedVariant", onTertiaryFixedVariant),
         )
     val screenColors =
         remember(dynamicProperties) {
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt
index f2fec5f..a55fa44 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt
@@ -26,10 +26,7 @@
 import com.android.systemui.touchpad.tutorial.ui.gesture.HomeGestureMonitor
 
 @Composable
-fun HomeGestureTutorialScreen(
-    onDoneButtonClicked: () -> Unit,
-    onBack: () -> Unit,
-) {
+fun HomeGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) {
     val screenConfig =
         TutorialScreenConfig(
             colors = rememberScreenColors(),
@@ -38,18 +35,20 @@
                     titleResId = R.string.touchpad_home_gesture_action_title,
                     bodyResId = R.string.touchpad_home_gesture_guidance,
                     titleSuccessResId = R.string.touchpad_home_gesture_success_title,
-                    bodySuccessResId = R.string.touchpad_home_gesture_success_body
+                    bodySuccessResId = R.string.touchpad_home_gesture_success_body,
                 ),
             animations =
                 TutorialScreenConfig.Animations(
                     educationResId = R.raw.trackpad_home_edu,
-                    successResId = R.raw.trackpad_home_success
-                )
+                    successResId = R.raw.trackpad_home_success,
+                ),
         )
     val gestureMonitorProvider =
         DistanceBasedGestureMonitorProvider(
             monitorFactory = { distanceThresholdPx, gestureStateCallback ->
-                HomeGestureMonitor(distanceThresholdPx, gestureStateCallback)
+                HomeGestureMonitor(distanceThresholdPx).also {
+                    it.addGestureStateCallback(gestureStateCallback)
+                }
             }
         )
     GestureTutorialScreen(screenConfig, gestureMonitorProvider, onDoneButtonClicked, onBack)
@@ -64,7 +63,7 @@
         rememberLottieDynamicProperties(
             rememberColorFilterProperty(".primaryFixedDim", primaryFixedDim),
             rememberColorFilterProperty(".onPrimaryFixed", onPrimaryFixed),
-            rememberColorFilterProperty(".onPrimaryFixedVariant", onPrimaryFixedVariant)
+            rememberColorFilterProperty(".onPrimaryFixedVariant", onPrimaryFixedVariant),
         )
     val screenColors =
         remember(dynamicProperties) {
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt
index b2fb6cd..6ee15aa 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt
@@ -29,10 +29,7 @@
 import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGestureMonitor
 
 @Composable
-fun RecentAppsGestureTutorialScreen(
-    onDoneButtonClicked: () -> Unit,
-    onBack: () -> Unit,
-) {
+fun RecentAppsGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) {
     val screenConfig =
         TutorialScreenConfig(
             colors = rememberScreenColors(),
@@ -41,20 +38,20 @@
                     titleResId = R.string.touchpad_recent_apps_gesture_action_title,
                     bodyResId = R.string.touchpad_recent_apps_gesture_guidance,
                     titleSuccessResId = R.string.touchpad_recent_apps_gesture_success_title,
-                    bodySuccessResId = R.string.touchpad_recent_apps_gesture_success_body
+                    bodySuccessResId = R.string.touchpad_recent_apps_gesture_success_body,
                 ),
             animations =
                 TutorialScreenConfig.Animations(
                     educationResId = R.raw.trackpad_recent_apps_edu,
-                    successResId = R.raw.trackpad_recent_apps_success
-                )
+                    successResId = R.raw.trackpad_recent_apps_success,
+                ),
         )
     val gestureMonitorProvider =
         object : GestureMonitorProvider {
             @Composable
             override fun rememberGestureMonitor(
                 resources: Resources,
-                gestureStateChangedCallback: (GestureState) -> Unit
+                gestureStateChangedCallback: (GestureState) -> Unit,
             ): TouchpadGestureMonitor {
                 val distanceThresholdPx =
                     resources.getDimensionPixelSize(
@@ -63,11 +60,9 @@
                 val velocityThresholdPxPerMs =
                     resources.getDimension(R.dimen.touchpad_recent_apps_gesture_velocity_threshold)
                 return remember(distanceThresholdPx, velocityThresholdPxPerMs) {
-                    RecentAppsGestureMonitor(
-                        distanceThresholdPx,
-                        gestureStateChangedCallback,
-                        velocityThresholdPxPerMs
-                    )
+                    RecentAppsGestureMonitor(distanceThresholdPx, velocityThresholdPxPerMs).also {
+                        it.addGestureStateCallback(gestureStateChangedCallback)
+                    }
                 }
             }
         }
@@ -83,7 +78,7 @@
         rememberLottieDynamicProperties(
             rememberColorFilterProperty(".secondaryFixedDim", secondaryFixedDim),
             rememberColorFilterProperty(".onSecondaryFixed", onSecondaryFixed),
-            rememberColorFilterProperty(".onSecondaryFixedVariant", onSecondaryFixedVariant)
+            rememberColorFilterProperty(".onSecondaryFixedVariant", onSecondaryFixedVariant),
         )
     val screenColors =
         remember(dynamicProperties) {
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt
index ecb5574..490f04d 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt
@@ -20,18 +20,21 @@
 import kotlin.math.abs
 
 /** Monitors for touchpad back gesture, that is three fingers swiping left or right */
-class BackGestureMonitor(
-    private val gestureDistanceThresholdPx: Int,
-    override val gestureStateChangedCallback: (GestureState) -> Unit,
-) : TouchpadGestureMonitor {
-    private val distanceTracker = DistanceTracker()
+class BackGestureMonitor(private val gestureDistanceThresholdPx: Int) : TouchpadGestureMonitor {
 
-    override fun processTouchpadEvent(event: MotionEvent) {
+    private val distanceTracker = DistanceTracker()
+    private var gestureStateChangedCallback: (GestureState) -> Unit = {}
+
+    override fun addGestureStateCallback(callback: (GestureState) -> Unit) {
+        gestureStateChangedCallback = callback
+    }
+
+    override fun accept(event: MotionEvent) {
         if (!isThreeFingerTouchpadSwipe(event)) return
-        val distanceState = distanceTracker.processEvent(event)
-        updateGestureStateBasedOnDistance(
+        val gestureState = distanceTracker.processEvent(event)
+        updateGestureState(
             gestureStateChangedCallback,
-            distanceState,
+            gestureState,
             isFinished = { abs(it.deltaX) >= gestureDistanceThresholdPx },
             progress = { 0f },
         )
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/DistanceTracker.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/DistanceTracker.kt
index 70d9366..d482358 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/DistanceTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/DistanceTracker.kt
@@ -38,10 +38,10 @@
     }
 }
 
-sealed interface DistanceGestureState
+sealed class DistanceGestureState(val deltaX: Float, val deltaY: Float)
 
-class Started(val deltaX: Float, val deltaY: Float) : DistanceGestureState
+class Started(deltaX: Float, deltaY: Float) : DistanceGestureState(deltaX, deltaY)
 
-class Moving(val deltaX: Float, val deltaY: Float) : DistanceGestureState
+class Moving(deltaX: Float, deltaY: Float) : DistanceGestureState(deltaX, deltaY)
 
-class Finished(val deltaX: Float, val deltaY: Float) : DistanceGestureState
+class Finished(deltaX: Float, deltaY: Float) : DistanceGestureState(deltaX, deltaY)
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureStateUpdates.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureStateUpdates.kt
index c1caeb3..f194677 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureStateUpdates.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureStateUpdates.kt
@@ -16,11 +16,8 @@
 
 package com.android.systemui.touchpad.tutorial.ui.gesture
 
-/**
- * Helper function for gesture recognizers to have common state triggering logic based on distance
- * only.
- */
-inline fun updateGestureStateBasedOnDistance(
+/** Helper function for gesture recognizers to have common state triggering logic */
+inline fun updateGestureState(
     gestureStateChangedCallback: (GestureState) -> Unit,
     gestureState: DistanceGestureState?,
     isFinished: (Finished) -> Boolean,
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitor.kt
index fdcf9de..83d4f56 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitor.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitor.kt
@@ -19,18 +19,21 @@
 import android.view.MotionEvent
 
 /** Monitors for touchpad home gesture, that is three fingers swiping up */
-class HomeGestureMonitor(
-    private val gestureDistanceThresholdPx: Int,
-    override val gestureStateChangedCallback: (GestureState) -> Unit,
-) : TouchpadGestureMonitor {
-    private val distanceTracker = DistanceTracker()
+class HomeGestureMonitor(private val gestureDistanceThresholdPx: Int) : TouchpadGestureMonitor {
 
-    override fun processTouchpadEvent(event: MotionEvent) {
+    private val distanceTracker = DistanceTracker()
+    private var gestureStateChangedCallback: (GestureState) -> Unit = {}
+
+    override fun addGestureStateCallback(callback: (GestureState) -> Unit) {
+        gestureStateChangedCallback = callback
+    }
+
+    override fun accept(event: MotionEvent) {
         if (!isThreeFingerTouchpadSwipe(event)) return
-        val distanceState = distanceTracker.processEvent(event)
-        updateGestureStateBasedOnDistance(
+        val gestureState = distanceTracker.processEvent(event)
+        updateGestureState(
             gestureStateChangedCallback,
-            distanceState,
+            gestureState,
             isFinished = { -it.deltaY >= gestureDistanceThresholdPx },
             progress = { 0f },
         )
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitor.kt
index dd31ce3..1731bb8 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitor.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitor.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.touchpad.tutorial.ui.gesture
 
 import android.view.MotionEvent
-import androidx.compose.ui.input.pointer.util.VelocityTracker1D
 import kotlin.math.abs
 
 /**
@@ -27,45 +26,30 @@
  */
 class RecentAppsGestureMonitor(
     private val gestureDistanceThresholdPx: Int,
-    override val gestureStateChangedCallback: (GestureState) -> Unit,
     private val velocityThresholdPxPerMs: Float,
-    private val velocityTracker: VelocityTracker1D = VelocityTracker1D(isDataDifferential = false),
+    private val distanceTracker: DistanceTracker = DistanceTracker(),
+    private val velocityTracker: VerticalVelocityTracker = VerticalVelocityTracker(),
 ) : TouchpadGestureMonitor {
 
-    private var xStart = 0f
-    private var yStart = 0f
+    private var gestureStateChangedCallback: (GestureState) -> Unit = {}
 
-    override fun processTouchpadEvent(event: MotionEvent) {
-        val action = event.actionMasked
-        velocityTracker.addDataPoint(event.eventTime, event.y)
-        when (action) {
-            MotionEvent.ACTION_DOWN -> {
-                if (isThreeFingerTouchpadSwipe(event)) {
-                    xStart = event.x
-                    yStart = event.y
-                    gestureStateChangedCallback(GestureState.InProgress())
-                }
-            }
-            MotionEvent.ACTION_UP -> {
-                if (isThreeFingerTouchpadSwipe(event) && isRecentAppsGesture(event)) {
-                    gestureStateChangedCallback(GestureState.Finished)
-                } else {
-                    gestureStateChangedCallback(GestureState.NotStarted)
-                }
-                velocityTracker.resetTracking()
-            }
-            MotionEvent.ACTION_CANCEL -> {
-                velocityTracker.resetTracking()
-            }
-        }
+    override fun addGestureStateCallback(callback: (GestureState) -> Unit) {
+        gestureStateChangedCallback = callback
     }
 
-    private fun isRecentAppsGesture(event: MotionEvent): Boolean {
-        // below is trying to mirror behavior of TriggerSwipeUpTouchTracker#onGestureEnd.
-        // We're diving velocity by 1000, to have the same unit of measure: pixels/ms.
-        val swipeDistance = yStart - event.y
-        val velocity = velocityTracker.calculateVelocity() / 1000
-        return swipeDistance >= gestureDistanceThresholdPx &&
-            abs(velocity) <= velocityThresholdPxPerMs
+    override fun accept(event: MotionEvent) {
+        if (!isThreeFingerTouchpadSwipe(event)) return
+        val gestureState = distanceTracker.processEvent(event)
+        velocityTracker.accept(event)
+
+        updateGestureState(
+            gestureStateChangedCallback,
+            gestureState,
+            isFinished = { state ->
+                -state.deltaY >= gestureDistanceThresholdPx &&
+                    abs(velocityTracker.calculateVelocity().value) <= velocityThresholdPxPerMs
+            },
+            progress = { 0f },
+        )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt
index 88671d4..4b82ba1 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt
@@ -18,13 +18,14 @@
 
 import android.view.InputDevice
 import android.view.MotionEvent
+import java.util.function.Consumer
 
 /**
  * Allows listening to touchpadGesture and calling onDone when gesture was triggered. Can have all
  * motion events passed to [onMotionEvent] and will filter touchpad events accordingly
  */
 class TouchpadGestureHandler(
-    private val gestureMonitor: TouchpadGestureMonitor,
+    private val gestureMonitor: Consumer<MotionEvent>,
     private val easterEggGestureMonitor: EasterEggGestureMonitor,
 ) {
 
@@ -40,7 +41,7 @@
             if (isTwoFingerSwipe(event)) {
                 easterEggGestureMonitor.processTouchpadEvent(event)
             } else {
-                gestureMonitor.processTouchpadEvent(event)
+                gestureMonitor.accept(event)
             }
             true
         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureMonitor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureMonitor.kt
index 4655c98..9216821 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureMonitor.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureMonitor.kt
@@ -17,15 +17,11 @@
 package com.android.systemui.touchpad.tutorial.ui.gesture
 
 import android.view.MotionEvent
+import java.util.function.Consumer
 
-/**
- * Monitor for touchpad gestures that calls [gestureStateChangedCallback] when [GestureState]
- * changes. All tracked motion events should be passed to [processTouchpadEvent]
- */
-interface TouchpadGestureMonitor {
-    val gestureStateChangedCallback: (GestureState) -> Unit
-
-    fun processTouchpadEvent(event: MotionEvent)
+/** Monitor for touchpad gestures that can notify callback when [GestureState] changes. */
+interface TouchpadGestureMonitor : Consumer<MotionEvent> {
+    fun addGestureStateCallback(callback: (GestureState) -> Unit)
 }
 
 fun isThreeFingerTouchpadSwipe(event: MotionEvent) = isNFingerTouchpadSwipe(event, fingerCount = 3)
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/VelocityTracker.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/VelocityTracker.kt
new file mode 100644
index 0000000..9b38eca
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/VelocityTracker.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.touchpad.tutorial.ui.gesture
+
+import android.view.MotionEvent
+import androidx.compose.ui.input.pointer.util.VelocityTracker1D
+import java.util.function.Consumer
+
+/** Velocity in pixels/ms. */
+@JvmInline value class Velocity(val value: Float)
+
+/**
+ * Tracks velocity for processed MotionEvents. Useful for recognizing gestures based on velocity.
+ */
+interface VelocityTracker : Consumer<MotionEvent> {
+
+    fun calculateVelocity(): Velocity
+}
+
+class VerticalVelocityTracker(
+    private val velocityTracker: VelocityTracker1D = VelocityTracker1D(isDataDifferential = false)
+) : VelocityTracker {
+
+    override fun accept(event: MotionEvent) {
+        val action = event.actionMasked
+        if (action == MotionEvent.ACTION_DOWN) {
+            velocityTracker.resetTracking()
+        }
+        velocityTracker.addDataPoint(event.eventTime, event.y)
+    }
+
+    /**
+     * Calculates velocity on demand - this calculation can be expensive so shouldn't be called
+     * after every event.
+     */
+    override fun calculateVelocity() = Velocity(velocityTracker.calculateVelocity() / 1000)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
index 1f92bc1..bbd8f3dc 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
@@ -59,7 +59,6 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
 import androidx.lifecycle.Observer;
 
 import com.android.internal.annotations.GuardedBy;
@@ -110,8 +109,8 @@
     // It is safe to use 99 as the broadcast stream now. There are only 10+ default audio
     // streams defined in AudioSystem for now and audio team is in the middle of restructure,
     // no new default stream is preferred.
-    @VisibleForTesting static final int DYNAMIC_STREAM_BROADCAST = 99;
-    private static final int DYNAMIC_STREAM_REMOTE_START_INDEX = 100;
+    public static final int DYNAMIC_STREAM_BROADCAST = 99;
+    public static final int DYNAMIC_STREAM_REMOTE_START_INDEX = 100;
     private static final AudioAttributes SONIFICIATION_VIBRATION_ATTRIBUTES =
             new AudioAttributes.Builder()
                     .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index 7166428..7c5116d 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -537,7 +537,7 @@
 
         mWindow.setAttributes(lp);
         mWindow.setLayout(WRAP_CONTENT, WRAP_CONTENT);
-        mDialog.setContentView(R.layout.volume_dialog);
+        mDialog.setContentView(R.layout.volume_dialog_legacy);
         mDialogView = mDialog.findViewById(R.id.volume_dialog);
         mDialogView.setAlpha(0);
         mDialogTimeoutMillis = mSecureSettings.get().getInt(
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStateModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStateModel.kt
index f1443e3..500cc0b 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStateModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStateModel.kt
@@ -23,7 +23,7 @@
 
 /** Models a state of the Volume Dialog. */
 data class VolumeDialogStateModel(
-    val states: Map<Int, VolumeDialogStreamStateModel>,
+    val states: Map<Int, VolumeDialogStreamModel>,
     val ringerModeInternal: Int = 0,
     val ringerModeExternal: Int = 0,
     val zenMode: Int = 0,
@@ -39,7 +39,7 @@
     constructor(
         legacyState: VolumeDialogController.State
     ) : this(
-        states = legacyState.states.mapToMap { VolumeDialogStreamStateModel(it) },
+        states = legacyState.states.mapToMap { VolumeDialogStreamModel(it) },
         ringerModeInternal = legacyState.ringerModeInternal,
         ringerModeExternal = legacyState.ringerModeExternal,
         zenMode = legacyState.zenMode,
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStreamStateModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStreamModel.kt
similarity index 92%
rename from packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStreamStateModel.kt
rename to packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStreamModel.kt
index a9d367d..26c96ea 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStreamStateModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStreamModel.kt
@@ -16,18 +16,18 @@
 
 package com.android.systemui.volume.dialog.domain.model
 
-import android.annotation.IntegerRes
+import androidx.annotation.StringRes
 import com.android.systemui.plugins.VolumeDialogController
 
 /** Models a state of an audio stream of the Volume Dialog. */
-data class VolumeDialogStreamStateModel(
+data class VolumeDialogStreamModel(
     val isDynamic: Boolean = false,
     val level: Int = 0,
     val levelMin: Int = 0,
     val levelMax: Int = 0,
     val muted: Boolean = false,
     val muteSupported: Boolean = false,
-    @IntegerRes val name: Int = 0,
+    @StringRes val name: Int = 0,
     val remoteLabel: String? = null,
     val routedToBluetooth: Boolean = false,
 ) {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinder.kt
index ba08876..b2f6cb3 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinder.kt
@@ -34,7 +34,7 @@
 
     fun bind(view: View) {
         with(view) {
-            val button = requireViewById<View>(R.id.settings)
+            val button = requireViewById<View>(R.id.volume_dialog_settings)
             repeatWhenAttached {
                 viewModel(
                     traceName = "VolumeDialogViewBinder",
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractor.kt
new file mode 100644
index 0000000..81507ba
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractor.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.volume.dialog.sliders.domain.interactor
+
+import com.android.systemui.plugins.VolumeDialogController
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope
+import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogStateInteractor
+import com.android.systemui.volume.dialog.domain.model.VolumeDialogStreamModel
+import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSliderType
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.mapNotNull
+
+/** Operates a state of particular slider of the Volume Dialog. */
+class VolumeDialogSliderInteractor
+@AssistedInject
+constructor(
+    @Assisted private val sliderType: VolumeDialogSliderType,
+    volumeDialogStateInteractor: VolumeDialogStateInteractor,
+    private val volumeDialogController: VolumeDialogController,
+) {
+
+    val slider: Flow<VolumeDialogStreamModel> =
+        volumeDialogStateInteractor.volumeDialogState.mapNotNull {
+            it.states[sliderType.audioStream]
+        }
+
+    fun setStreamVolume(userLevel: Int) {
+        volumeDialogController.setStreamVolume(sliderType.audioStream, userLevel)
+    }
+
+    @VolumeDialogScope
+    @AssistedFactory
+    interface Factory {
+
+        fun create(sliderType: VolumeDialogSliderType): VolumeDialogSliderInteractor
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSlidersInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSlidersInteractor.kt
new file mode 100644
index 0000000..325e4c95
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSlidersInteractor.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.domain.interactor
+
+import com.android.systemui.volume.VolumeDialogControllerImpl
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope
+import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogStateInteractor
+import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSliderType
+import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSlidersModel
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+/** Provides a state for the Sliders section of the Volume Dialog. */
+@VolumeDialogScope
+class VolumeDialogSlidersInteractor
+@Inject
+constructor(volumeDialogStateInteractor: VolumeDialogStateInteractor) {
+
+    val sliders: Flow<VolumeDialogSlidersModel> =
+        volumeDialogStateInteractor.volumeDialogState.map {
+            val sliderTypes: List<VolumeDialogSliderType> =
+                it.states.keys.sortedWith(StreamsSorter).map { audioStream ->
+                    when {
+                        audioStream == VolumeDialogControllerImpl.DYNAMIC_STREAM_BROADCAST ->
+                            VolumeDialogSliderType.AudioSharingStream(audioStream)
+                        audioStream >=
+                            VolumeDialogControllerImpl.DYNAMIC_STREAM_REMOTE_START_INDEX ->
+                            VolumeDialogSliderType.RemoteMediaStream(audioStream)
+                        else -> VolumeDialogSliderType.Stream(audioStream)
+                    }
+                }
+            VolumeDialogSlidersModel(
+                slider = sliderTypes.first(),
+                floatingSliders = sliderTypes.drop(1),
+            )
+        }
+
+    private object StreamsSorter : Comparator<Int> {
+
+        // TODO(b/369992924) order the streams
+        override fun compare(lhs: Int, rhs: Int): Int {
+            return lhs - rhs
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/model/VolumeDialogSliderType.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/model/VolumeDialogSliderType.kt
new file mode 100644
index 0000000..18a2689
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/model/VolumeDialogSliderType.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.volume.dialog.sliders.domain.model
+
+/** Models different possible audio sliders shown in the Volume Dialog. */
+sealed interface VolumeDialogSliderType {
+
+    // VolumeDialogController uses the same model for every slider type. We need to follow the same
+    // logic until we refactor and decouple data and domain layers from the VolumeDialogController
+    // into separated interactors.
+    val audioStream: Int
+
+    class Stream(override val audioStream: Int) : VolumeDialogSliderType
+
+    class RemoteMediaStream(override val audioStream: Int) : VolumeDialogSliderType
+
+    class AudioSharingStream(override val audioStream: Int) : VolumeDialogSliderType
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/model/VolumeDialogSlidersModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/model/VolumeDialogSlidersModel.kt
new file mode 100644
index 0000000..91a3328
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/model/VolumeDialogSlidersModel.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.volume.dialog.sliders.domain.model
+
+/** Models a state of the sliders section of the Volume Dialog. */
+data class VolumeDialogSlidersModel(
+    val slider: VolumeDialogSliderType,
+    val floatingSliders: List<VolumeDialogSliderType>,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
new file mode 100644
index 0000000..25a5f28
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.ui
+
+import android.view.View
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.android.systemui.lifecycle.WindowLifecycleState
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.lifecycle.setSnapshotBinding
+import com.android.systemui.lifecycle.viewModel
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope
+import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderViewModel
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.awaitCancellation
+
+class VolumeDialogSliderViewBinder
+@AssistedInject
+constructor(@Assisted private val viewModelProvider: () -> VolumeDialogSliderViewModel) {
+
+    fun bind(view: View) {
+        with(view) {
+            repeatWhenAttached {
+                viewModel(
+                    traceName = "VolumeDialogSliderViewBinder",
+                    minWindowLifecycleState = WindowLifecycleState.ATTACHED,
+                    factory = { viewModelProvider() },
+                ) { viewModel ->
+                    setSnapshotBinding {}
+
+                    awaitCancellation()
+                }
+            }
+        }
+    }
+
+    @AssistedFactory
+    @VolumeDialogScope
+    interface Factory {
+
+        fun create(
+            viewModelProvider: () -> VolumeDialogSliderViewModel
+        ): VolumeDialogSliderViewBinder
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt
new file mode 100644
index 0000000..0a00f70
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.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.volume.dialog.sliders.ui
+
+import android.view.View
+import com.android.systemui.lifecycle.WindowLifecycleState
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.lifecycle.setSnapshotBinding
+import com.android.systemui.lifecycle.viewModel
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope
+import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSlidersViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.awaitCancellation
+
+@VolumeDialogScope
+class VolumeDialogSlidersViewBinder
+@Inject
+constructor(private val viewModelFactory: VolumeDialogSlidersViewModel.Factory) {
+
+    fun bind(view: View) {
+        with(view) {
+            repeatWhenAttached {
+                viewModel(
+                    traceName = "VolumeDialogSlidersViewBinder",
+                    minWindowLifecycleState = WindowLifecycleState.ATTACHED,
+                    factory = { viewModelFactory.create() },
+                ) { viewModel ->
+                    setSnapshotBinding {}
+
+                    awaitCancellation()
+                }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt
new file mode 100644
index 0000000..27b8f2f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.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.volume.dialog.sliders.ui.viewmodel
+
+import com.android.systemui.volume.dialog.domain.model.VolumeDialogStreamModel
+import com.android.systemui.volume.dialog.sliders.domain.interactor.VolumeDialogSliderInteractor
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.flow.Flow
+
+class VolumeDialogSliderViewModel
+@AssistedInject
+constructor(@Assisted private val interactor: VolumeDialogSliderInteractor) {
+
+    val model: Flow<VolumeDialogStreamModel> = interactor.slider
+
+    fun setStreamVolume(volume: Int) {
+        interactor.setStreamVolume(volume)
+    }
+
+    @AssistedFactory
+    interface Factory {
+
+        fun create(interactor: VolumeDialogSliderInteractor): VolumeDialogSliderViewModel
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSlidersViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSlidersViewModel.kt
new file mode 100644
index 0000000..b5b292f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSlidersViewModel.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.dialog.sliders.ui.viewmodel
+
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
+import com.android.systemui.volume.dialog.sliders.domain.interactor.VolumeDialogSliderInteractor
+import com.android.systemui.volume.dialog.sliders.domain.interactor.VolumeDialogSlidersInteractor
+import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSliderType
+import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSliderViewBinder
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+class VolumeDialogSlidersViewModel
+@AssistedInject
+constructor(
+    @VolumeDialog coroutineScope: CoroutineScope,
+    private val slidersInteractor: VolumeDialogSlidersInteractor,
+    private val sliderInteractorFactory: VolumeDialogSliderInteractor.Factory,
+    private val sliderViewModelFactory: VolumeDialogSliderViewModel.Factory,
+    private val sliderViewBinderFactory: VolumeDialogSliderViewBinder.Factory,
+) {
+
+    val sliders: Flow<VolumeDialogSliderUiModel> =
+        slidersInteractor.sliders
+            .distinctUntilChanged()
+            .map { slidersModel ->
+                VolumeDialogSliderUiModel(
+                    sliderViewBinder = createSliderViewBinder(slidersModel.slider),
+                    floatingSliderViewBinders =
+                        slidersModel.floatingSliders.map(::createSliderViewBinder),
+                )
+            }
+            .stateIn(coroutineScope, SharingStarted.Eagerly, null)
+            .filterNotNull()
+
+    private fun createSliderViewBinder(type: VolumeDialogSliderType): VolumeDialogSliderViewBinder =
+        sliderViewBinderFactory.create {
+            sliderViewModelFactory.create(sliderInteractorFactory.create(type))
+        }
+
+    @AssistedFactory
+    interface Factory {
+
+        fun create(): VolumeDialogSlidersViewModel
+    }
+}
+
+/** Models slider ui */
+data class VolumeDialogSliderUiModel(
+    val sliderViewBinder: VolumeDialogSliderViewBinder,
+    val floatingSliderViewBinders: List<VolumeDialogSliderViewBinder>,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogBinder.kt
index 9452d8c..77733fe 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogBinder.kt
@@ -50,7 +50,7 @@
             dialog.setContentView(R.layout.volume_dialog)
             dialog.setCanceledOnTouchOutside(true)
 
-            settingsButtonViewBinder.bind(dialog.requireViewById(R.id.settings_container))
+            settingsButtonViewBinder.bind(dialog.requireViewById(R.id.volume_dialog_settings))
             volumeDialogViewBinder.bind(
                 dialog,
                 dialog.requireViewById(R.id.volume_dialog_container),
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsViewTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelTest.kt
new file mode 100644
index 0000000..655b2cc
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelTest.kt
@@ -0,0 +1,185 @@
+/*
+ * 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.bluetooth.qsdialog
+
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
+import com.android.dx.mockito.inline.extended.StaticMockitoSession
+import com.android.settingslib.bluetooth.BluetoothUtils
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
+import com.android.systemui.res.R
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class AudioSharingButtonViewModelTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val bluetoothState = MutableStateFlow(false)
+    private val deviceItemUpdate: MutableSharedFlow<List<DeviceItem>> = MutableSharedFlow()
+    @Mock private lateinit var cachedBluetoothDevice: CachedBluetoothDevice
+    @Mock private lateinit var localBluetoothManager: LocalBluetoothManager
+    @Mock private lateinit var bluetoothStateInteractor: BluetoothStateInteractor
+    @Mock private lateinit var deviceItemInteractor: DeviceItemInteractor
+    @Mock private lateinit var deviceItem: DeviceItem
+    private lateinit var mockitoSession: StaticMockitoSession
+    private lateinit var audioSharingButtonViewModel: AudioSharingButtonViewModel
+
+    @Before
+    fun setUp() {
+        mockitoSession =
+            mockitoSession().initMocks(this).mockStatic(BluetoothUtils::class.java).startMocking()
+        whenever(bluetoothStateInteractor.bluetoothStateUpdate).thenReturn(bluetoothState)
+        whenever(deviceItemInteractor.deviceItemUpdate).thenReturn(deviceItemUpdate)
+        audioSharingButtonViewModel =
+            AudioSharingButtonViewModel(
+                localBluetoothManager,
+                kosmos.audioSharingInteractor,
+                bluetoothStateInteractor,
+                deviceItemInteractor,
+            )
+        audioSharingButtonViewModel.activateIn(testScope)
+    }
+
+    @After
+    fun tearDown() {
+        mockitoSession.finishMocking()
+    }
+
+    @Test
+    fun testButtonStateUpdate_bluetoothOff_returnGone() {
+        testScope.runTest {
+            val actual by
+                collectLastValue(audioSharingButtonViewModel.audioSharingButtonStateUpdate)
+            kosmos.bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+
+            runCurrent()
+
+            assertThat(actual).isEqualTo(AudioSharingButtonState.Gone)
+        }
+    }
+
+    @Test
+    fun testButtonStateUpdate_noDevice_returnGone() {
+        testScope.runTest {
+            val actual by
+                collectLastValue(audioSharingButtonViewModel.audioSharingButtonStateUpdate)
+            kosmos.bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+            bluetoothState.value = true
+            runCurrent()
+
+            assertThat(actual).isEqualTo(AudioSharingButtonState.Gone)
+        }
+    }
+
+    @Test
+    fun testButtonStateUpdate_isBroadcasting_returnSharingAudio() {
+        testScope.runTest {
+            val actual by
+                collectLastValue(audioSharingButtonViewModel.audioSharingButtonStateUpdate)
+            kosmos.bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+            bluetoothState.value = true
+            runCurrent()
+            deviceItemUpdate.emit(listOf())
+            runCurrent()
+            kosmos.bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
+            runCurrent()
+
+            assertThat(actual)
+                .isEqualTo(
+                    AudioSharingButtonState.Visible(
+                        R.string.quick_settings_bluetooth_audio_sharing_button_sharing,
+                        isActive = true,
+                    )
+                )
+        }
+    }
+
+    @Test
+    fun testButtonStateUpdate_hasSource_returnGone() {
+        testScope.runTest {
+            val actual by
+                collectLastValue(audioSharingButtonViewModel.audioSharingButtonStateUpdate)
+            kosmos.bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+            whenever(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice)
+            whenever(
+                    BluetoothUtils.hasConnectedBroadcastSource(
+                        cachedBluetoothDevice,
+                        localBluetoothManager,
+                    )
+                )
+                .thenReturn(true)
+            bluetoothState.value = true
+            runCurrent()
+            deviceItemUpdate.emit(listOf(deviceItem))
+            runCurrent()
+
+            assertThat(actual).isEqualTo(AudioSharingButtonState.Gone)
+        }
+    }
+
+    @Test
+    fun testButtonStateUpdate_hasActiveDevice_returnAudioSharing() {
+        testScope.runTest {
+            val actual by
+                collectLastValue(audioSharingButtonViewModel.audioSharingButtonStateUpdate)
+            kosmos.bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+            whenever(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice)
+            whenever(
+                    BluetoothUtils.hasConnectedBroadcastSource(
+                        cachedBluetoothDevice,
+                        localBluetoothManager,
+                    )
+                )
+                .thenReturn(false)
+            whenever(BluetoothUtils.isActiveLeAudioDevice(cachedBluetoothDevice)).thenReturn(true)
+            bluetoothState.value = true
+            runCurrent()
+            deviceItemUpdate.emit(listOf(deviceItem))
+            runCurrent()
+
+            assertThat(actual)
+                .isEqualTo(
+                    AudioSharingButtonState.Visible(
+                        R.string.quick_settings_bluetooth_audio_sharing_button,
+                        isActive = false,
+                    )
+                )
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt
new file mode 100644
index 0000000..ce37eee
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt
@@ -0,0 +1,193 @@
+/*
+ * 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.bluetooth.qsdialog
+
+import android.bluetooth.BluetoothDevice
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
+import com.android.dx.mockito.inline.extended.StaticMockitoSession
+import com.android.settingslib.bluetooth.BluetoothUtils
+import com.android.settingslib.bluetooth.LeAudioProfile
+import com.android.settingslib.flags.Flags
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.activityStarter
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.testKosmos
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.verify
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.never
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@OptIn(ExperimentalCoroutinesApi::class)
+class AudioSharingDeviceItemActionInteractorTest : SysuiTestCase() {
+    @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+    private val kosmos = testKosmos().apply { testDispatcher = UnconfinedTestDispatcher() }
+    private lateinit var actionInteractorImpl: DeviceItemActionInteractor
+    private lateinit var mockitoSession: StaticMockitoSession
+    private lateinit var connectedAudioSharingMediaDeviceItem: DeviceItem
+    private lateinit var connectedMediaDeviceItem: DeviceItem
+    @Mock private lateinit var dialog: SystemUIDialog
+    @Mock private lateinit var leAudioProfile: LeAudioProfile
+    @Mock private lateinit var bluetoothDevice: BluetoothDevice
+
+    @Before
+    fun setUp() {
+        mockitoSession =
+            mockitoSession().initMocks(this).mockStatic(BluetoothUtils::class.java).startMocking()
+        connectedMediaDeviceItem =
+            DeviceItem(
+                type = DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE,
+                cachedBluetoothDevice = kosmos.cachedBluetoothDevice,
+                deviceName = DEVICE_NAME,
+                connectionSummary = DEVICE_CONNECTION_SUMMARY,
+                iconWithDescription = null,
+                background = null,
+            )
+        connectedAudioSharingMediaDeviceItem =
+            DeviceItem(
+                type = DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE,
+                cachedBluetoothDevice = kosmos.cachedBluetoothDevice,
+                deviceName = DEVICE_NAME,
+                connectionSummary = DEVICE_CONNECTION_SUMMARY,
+                iconWithDescription = null,
+                background = null,
+            )
+        actionInteractorImpl = kosmos.audioSharingDeviceItemActionInteractorImpl
+    }
+
+    @After
+    fun tearDown() {
+        mockitoSession.finishMocking()
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_AUDIO_SHARING_QS_DIALOG_IMPROVEMENT)
+    fun testOnClick_connectedAudioSharingMediaDevice_flagOn_createDialog() {
+        with(kosmos) {
+            testScope.runTest {
+                bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+                actionInteractorImpl.onClick(connectedAudioSharingMediaDeviceItem, dialog)
+                verify(dialogTransitionAnimator)
+                    .showFromDialog(any(), any(), eq(null), anyBoolean())
+            }
+        }
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_AUDIO_SHARING_QS_DIALOG_IMPROVEMENT)
+    fun testOnClick_connectedAudioSharingMediaDevice_flagOff_shouldLaunchSettings() {
+        with(kosmos) {
+            testScope.runTest {
+                bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+                whenever(cachedBluetoothDevice.device).thenReturn(bluetoothDevice)
+                actionInteractorImpl.onClick(connectedAudioSharingMediaDeviceItem, dialog)
+                verify(activityStarter)
+                    .postStartActivityDismissingKeyguard(
+                        ArgumentMatchers.any(),
+                        ArgumentMatchers.anyInt(),
+                        ArgumentMatchers.any(),
+                    )
+                verify(dialogTransitionAnimator, never())
+                    .showFromDialog(any(), any(), eq(null), anyBoolean())
+            }
+        }
+    }
+
+    @Test
+    fun testOnClick_inAudioSharing_clickedDeviceHasSource_shouldNotLaunchSettings() {
+        with(kosmos) {
+            testScope.runTest {
+                bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+                whenever(cachedBluetoothDevice.uiAccessibleProfiles)
+                    .thenReturn(listOf(leAudioProfile))
+                whenever(BluetoothUtils.isBroadcasting(ArgumentMatchers.any())).thenReturn(true)
+                whenever(
+                        BluetoothUtils.hasConnectedBroadcastSource(
+                            ArgumentMatchers.any(),
+                            ArgumentMatchers.any(),
+                        )
+                    )
+                    .thenReturn(true)
+
+                actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog)
+                verify(activityStarter, Mockito.never())
+                    .postStartActivityDismissingKeyguard(
+                        ArgumentMatchers.any(),
+                        ArgumentMatchers.anyInt(),
+                        ArgumentMatchers.any(),
+                    )
+            }
+        }
+    }
+
+    @Test
+    fun testOnClick_inAudioSharing_clickedDeviceNoSource_shouldLaunchSettings() {
+        with(kosmos) {
+            testScope.runTest {
+                bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+                whenever(cachedBluetoothDevice.device).thenReturn(bluetoothDevice)
+                whenever(cachedBluetoothDevice.uiAccessibleProfiles)
+                    .thenReturn(listOf(leAudioProfile))
+
+                whenever(BluetoothUtils.isBroadcasting(ArgumentMatchers.any())).thenReturn(true)
+                whenever(
+                        BluetoothUtils.hasConnectedBroadcastSource(
+                            ArgumentMatchers.any(),
+                            ArgumentMatchers.any(),
+                        )
+                    )
+                    .thenReturn(false)
+
+                actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog)
+                verify(activityStarter)
+                    .postStartActivityDismissingKeyguard(
+                        ArgumentMatchers.any(),
+                        ArgumentMatchers.anyInt(),
+                        ArgumentMatchers.any(),
+                    )
+            }
+        }
+    }
+
+    private companion object {
+        const val DEVICE_NAME = "device"
+        const val DEVICE_CONNECTION_SUMMARY = "active"
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegateTest.kt
new file mode 100644
index 0000000..25b85b5
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegateTest.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bluetooth.qsdialog
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.widget.Button
+import android.widget.TextView
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@OptIn(ExperimentalCoroutinesApi::class)
+class AudioSharingDialogDelegateTest : SysuiTestCase() {
+    @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+    private val kosmos = testKosmos()
+    private val updateFlow = MutableSharedFlow<Unit>()
+    private lateinit var underTest: AudioSharingDialogDelegate
+
+    @Before
+    fun setUp() {
+        with(kosmos) {
+            // TODO(b/364515243): use real object instead of mock
+            whenever(deviceItemInteractor.deviceItemUpdateRequest).thenReturn(updateFlow)
+            whenever(deviceItemInteractor.deviceItemUpdate)
+                .thenReturn(MutableStateFlow(emptyList()))
+            underTest = audioSharingDialogDelegate
+        }
+    }
+
+    @Test
+    fun testCreateDialog() =
+        kosmos.testScope.runTest {
+            val dialog = underTest.createDialog()
+            assertThat(dialog).isInstanceOf(SystemUIDialog::class.java)
+        }
+
+    @Test
+    fun testCreateDialog_showState() =
+        with(kosmos) {
+            testScope.runTest {
+                val availableDeviceName = "name"
+                whenever(cachedBluetoothDevice.name).thenReturn(availableDeviceName)
+                val dialog = spy(underTest.createDialog())
+                dialog.show()
+                runCurrent()
+                val subtitleTextView = dialog.findViewById<TextView>(R.id.subtitle)
+                val switchActiveButton = dialog.findViewById<Button>(R.id.switch_active_button)
+                val shareAudioButton = dialog.findViewById<Button>(R.id.share_audio_button)
+                val subtitle =
+                    context.getString(
+                        R.string.quick_settings_bluetooth_audio_sharing_dialog_subtitle,
+                        availableDeviceName,
+                        ""
+                    )
+                val switchButtonText =
+                    context.getString(
+                        R.string.quick_settings_bluetooth_audio_sharing_dialog_switch_to_button,
+                        availableDeviceName
+                    )
+                assertThat(subtitleTextView.text).isEqualTo(subtitle)
+                assertThat(switchActiveButton.text).isEqualTo(switchButtonText)
+                assertThat(switchActiveButton.hasOnClickListeners()).isTrue()
+                assertThat(shareAudioButton.hasOnClickListeners()).isTrue()
+
+                switchActiveButton.performClick()
+                verify(dialog).dismiss()
+            }
+        }
+
+    @Test
+    fun testCreateDialog_hideState() =
+        with(kosmos) {
+            testScope.runTest {
+                val dialog = spy(underTest.createDialog())
+                dialog.show()
+                runCurrent()
+                updateFlow.emit(Unit)
+                runCurrent()
+                verify(dialog).dismiss()
+            }
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModelTest.kt
new file mode 100644
index 0000000..beb816c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModelTest.kt
@@ -0,0 +1,142 @@
+/*
+ * 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.bluetooth.qsdialog
+
+import android.bluetooth.BluetoothDevice
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.settingslib.bluetooth.LeAudioProfile
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.bluetooth.cachedBluetoothDeviceManager
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.res.R
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@OptIn(ExperimentalCoroutinesApi::class)
+class AudioSharingDialogViewModelTest : SysuiTestCase() {
+    @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+    private val kosmos = testKosmos().apply { testDispatcher = UnconfinedTestDispatcher() }
+    @Mock private lateinit var profileManager: LocalBluetoothProfileManager
+    @Mock private lateinit var leAudioProfile: LeAudioProfile
+    private val updateFlow = MutableSharedFlow<Unit>()
+    private lateinit var underTest: AudioSharingDialogViewModel
+
+    @Before
+    fun setUp() {
+        with(kosmos) {
+            // TODO(b/364515243): use real object instead of mock
+            whenever(deviceItemInteractor.deviceItemUpdateRequest).thenReturn(updateFlow)
+            whenever(deviceItemInteractor.deviceItemUpdate)
+                .thenReturn(MutableStateFlow(emptyList()))
+            underTest = audioSharingDialogViewModel
+        }
+    }
+
+    @Test
+    fun testDialogState_show() =
+        with(kosmos) {
+            testScope.runTest {
+                val deviceName = "name"
+                whenever(cachedBluetoothDevice.name).thenReturn(deviceName)
+                val actual by collectLastValue(underTest.dialogState)
+                runCurrent()
+                assertThat(actual)
+                    .isEqualTo(
+                        AudioSharingDialogState.Show(
+                            context.getString(
+                                R.string.quick_settings_bluetooth_audio_sharing_dialog_subtitle,
+                                deviceName,
+                                ""
+                            ),
+                            context.getString(
+                                R.string
+                                    .quick_settings_bluetooth_audio_sharing_dialog_switch_to_button,
+                                deviceName
+                            )
+                        )
+                    )
+            }
+        }
+
+    @Test
+    fun testDialogState_showWithActiveDeviceName() =
+        with(kosmos) {
+            testScope.runTest {
+                val deviceName = "name"
+                whenever(cachedBluetoothDevice.name).thenReturn(deviceName)
+                whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
+                whenever(localBluetoothManager.cachedDeviceManager)
+                    .thenReturn(cachedBluetoothDeviceManager)
+                whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile)
+                whenever(leAudioProfile.activeDevices).thenReturn(listOf(mock<BluetoothDevice>()))
+                whenever(cachedBluetoothDeviceManager.findDevice(any()))
+                    .thenReturn(cachedBluetoothDevice)
+                val actual by collectLastValue(underTest.dialogState)
+                runCurrent()
+                assertThat(actual)
+                    .isEqualTo(
+                        AudioSharingDialogState.Show(
+                            context.getString(
+                                R.string.quick_settings_bluetooth_audio_sharing_dialog_subtitle,
+                                deviceName,
+                                deviceName
+                            ),
+                            context.getString(
+                                R.string
+                                    .quick_settings_bluetooth_audio_sharing_dialog_switch_to_button,
+                                deviceName
+                            )
+                        )
+                    )
+            }
+        }
+
+    @Test
+    fun testDialogState_hide() =
+        with(kosmos) {
+            testScope.runTest {
+                val actual by collectLastValue(underTest.dialogState)
+                runCurrent()
+                updateFlow.emit(Unit)
+                assertThat(actual).isEqualTo(AudioSharingDialogState.Hide)
+            }
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt
index 2c53fd6..25f9565 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt
@@ -16,158 +16,197 @@
 
 package com.android.systemui.bluetooth.qsdialog
 
+import android.bluetooth.BluetoothLeBroadcast
 import android.testing.TestableLooper
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
-import com.android.dx.mockito.inline.extended.StaticMockitoSession
-import com.android.settingslib.bluetooth.BluetoothUtils
-import com.android.settingslib.bluetooth.CachedBluetoothDevice
-import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.res.R
-import com.android.systemui.util.mockito.whenever
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.launch
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
-import org.junit.After
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
 import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
 
-@ExperimentalCoroutinesApi
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
+@OptIn(ExperimentalCoroutinesApi::class)
 class AudioSharingInteractorTest : SysuiTestCase() {
-    private val testDispatcher = UnconfinedTestDispatcher()
-    private val testScope = TestScope(testDispatcher)
-    private val bluetoothState = MutableStateFlow(false)
-    private val deviceItemUpdate: MutableSharedFlow<List<DeviceItem>> = MutableSharedFlow()
-    @Mock private lateinit var cachedBluetoothDevice: CachedBluetoothDevice
-    @Mock private lateinit var localBluetoothManager: LocalBluetoothManager
-    @Mock private lateinit var bluetoothStateInteractor: BluetoothStateInteractor
-    @Mock private lateinit var deviceItemInteractor: DeviceItemInteractor
-    @Mock private lateinit var deviceItem: DeviceItem
-    private lateinit var mockitoSession: StaticMockitoSession
-    private lateinit var audioSharingInteractor: AudioSharingInteractor
+    @get:Rule val mockito: MockitoRule = MockitoJUnit.rule()
+    private val kosmos = testKosmos()
+    @Mock private lateinit var localBluetoothLeBroadcast: LocalBluetoothLeBroadcast
+    @Captor private lateinit var callbackCaptor: ArgumentCaptor<BluetoothLeBroadcast.Callback>
+    private lateinit var underTest: AudioSharingInteractor
 
     @Before
     fun setUp() {
-        mockitoSession =
-            mockitoSession().initMocks(this).mockStatic(BluetoothUtils::class.java).startMocking()
-        whenever(bluetoothStateInteractor.bluetoothStateUpdate).thenReturn(bluetoothState)
-        whenever(deviceItemInteractor.deviceItemUpdate).thenReturn(deviceItemUpdate)
-        audioSharingInteractor =
-            AudioSharingInteractor(
-                localBluetoothManager,
-                bluetoothStateInteractor,
-                deviceItemInteractor,
-                testScope.backgroundScope,
-                testDispatcher,
-            )
-    }
-
-    @After
-    fun tearDown() {
-        mockitoSession.finishMocking()
+        with(kosmos) { underTest = audioSharingInteractor }
     }
 
     @Test
-    fun testButtonStateUpdate_bluetoothOff_returnGone() {
-        testScope.runTest {
-            val actual by collectLastValue(audioSharingInteractor.audioSharingButtonStateUpdate)
+    fun testIsAudioSharingOn_flagOff_false() =
+        with(kosmos) {
+            testScope.runTest {
+                bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(false)
+                val value by collectLastValue(underTest.isAudioSharingOn)
+                runCurrent()
 
-            assertThat(actual).isEqualTo(AudioSharingButtonState.Gone)
+                assertThat(value).isFalse()
+            }
         }
-    }
 
     @Test
-    fun testButtonStateUpdate_noDevice_returnGone() {
-        testScope.runTest {
-            val actual by collectLastValue(audioSharingInteractor.audioSharingButtonStateUpdate)
-            bluetoothState.value = true
-            runCurrent()
+    fun testIsAudioSharingOn_flagOn_notInAudioSharing_false() =
+        with(kosmos) {
+            testScope.runTest {
+                bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false)
+                val value by collectLastValue(underTest.isAudioSharingOn)
+                runCurrent()
 
-            assertThat(actual).isEqualTo(AudioSharingButtonState.Gone)
+                assertThat(value).isFalse()
+            }
         }
-    }
 
     @Test
-    fun testButtonStateUpdate_isBroadcasting_returnSharingAudio() {
-        testScope.runTest {
-            whenever(BluetoothUtils.isBroadcasting(localBluetoothManager)).thenReturn(true)
+    fun testIsAudioSharingOn_flagOn_inAudioSharing_true() =
+        with(kosmos) {
+            testScope.runTest {
+                bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
+                val value by collectLastValue(underTest.isAudioSharingOn)
+                runCurrent()
 
-            val actual by collectLastValue(audioSharingInteractor.audioSharingButtonStateUpdate)
-            bluetoothState.value = true
-            deviceItemUpdate.emit(listOf())
-            runCurrent()
+                assertThat(value).isTrue()
+            }
+        }
 
-            assertThat(actual)
-                .isEqualTo(
-                    AudioSharingButtonState.Visible(
-                        R.string.quick_settings_bluetooth_audio_sharing_button_sharing,
-                        isActive = true
-                    )
+    @Test
+    fun testAudioSourceStateUpdate_notInAudioSharing_returnEmpty() =
+        with(kosmos) {
+            testScope.runTest {
+                bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false)
+                val value by collectLastValue(underTest.audioSourceStateUpdate)
+                runCurrent()
+
+                assertThat(value).isNull()
+            }
+        }
+
+    @Test
+    fun testAudioSourceStateUpdate_inAudioSharing_returnUnit() =
+        with(kosmos) {
+            testScope.runTest {
+                bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
+                val value by collectLastValue(underTest.audioSourceStateUpdate)
+                runCurrent()
+                bluetoothTileDialogAudioSharingRepository.emitAudioSourceStateUpdate()
+                runCurrent()
+
+                assertThat(value).isNull()
+            }
+        }
+
+    @Test
+    fun testHandleAudioSourceWhenReady_flagOff_sourceNotAdded() =
+        with(kosmos) {
+            testScope.runTest {
+                bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(false)
+                val job = launch { underTest.handleAudioSourceWhenReady() }
+                runCurrent()
+
+                assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse()
+                job.cancel()
+            }
+        }
+
+    @Test
+    fun testHandleAudioSourceWhenReady_noProfile_sourceNotAdded() =
+        with(kosmos) {
+            testScope.runTest {
+                bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+                bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile(null)
+                val job = launch { underTest.handleAudioSourceWhenReady() }
+                runCurrent()
+
+                assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse()
+                job.cancel()
+            }
+        }
+
+    @Test
+    fun testHandleAudioSourceWhenReady_hasProfileButAudioSharingOff_sourceNotAdded() =
+        with(kosmos) {
+            testScope.runTest {
+                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false)
+                bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+                bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile(
+                    localBluetoothLeBroadcast
                 )
+                val job = launch { underTest.handleAudioSourceWhenReady() }
+                runCurrent()
+
+                assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse()
+                job.cancel()
+            }
         }
-    }
 
     @Test
-    fun testButtonStateUpdate_hasSource_returnGone() {
-        testScope.runTest {
-            whenever(BluetoothUtils.isBroadcasting(localBluetoothManager)).thenReturn(false)
-            whenever(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice)
-            whenever(
-                    BluetoothUtils.hasConnectedBroadcastSource(
-                        cachedBluetoothDevice,
-                        localBluetoothManager
-                    )
+    fun testHandleAudioSourceWhenReady_audioSharingOnButNoPlayback_sourceNotAdded() =
+        with(kosmos) {
+            testScope.runTest {
+                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
+                bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+                bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile(
+                    localBluetoothLeBroadcast
                 )
-                .thenReturn(true)
+                val job = launch { underTest.handleAudioSourceWhenReady() }
+                runCurrent()
+                verify(localBluetoothLeBroadcast)
+                    .registerServiceCallBack(any(), callbackCaptor.capture())
+                runCurrent()
 
-            val actual by collectLastValue(audioSharingInteractor.audioSharingButtonStateUpdate)
-            bluetoothState.value = true
-            deviceItemUpdate.emit(listOf(deviceItem))
-            runCurrent()
-
-            assertThat(actual).isEqualTo(AudioSharingButtonState.Gone)
+                assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse()
+                job.cancel()
+            }
         }
-    }
 
     @Test
-    fun testButtonStateUpdate_hasActiveDevice_returnAudioSharing() {
-        testScope.runTest {
-            whenever(BluetoothUtils.isBroadcasting(localBluetoothManager)).thenReturn(false)
-            whenever(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice)
-            whenever(
-                    BluetoothUtils.hasConnectedBroadcastSource(
-                        cachedBluetoothDevice,
-                        localBluetoothManager
-                    )
+    fun testHandleAudioSourceWhenReady_audioSharingOnAndPlaybackStarts_sourceAdded() =
+        with(kosmos) {
+            testScope.runTest {
+                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
+                bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+                bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile(
+                    localBluetoothLeBroadcast
                 )
-                .thenReturn(false)
-            whenever(BluetoothUtils.isActiveLeAudioDevice(cachedBluetoothDevice)).thenReturn(true)
+                val job = launch { underTest.handleAudioSourceWhenReady() }
+                runCurrent()
+                verify(localBluetoothLeBroadcast)
+                    .registerServiceCallBack(any(), callbackCaptor.capture())
+                runCurrent()
+                callbackCaptor.value.onPlaybackStarted(0, 0)
+                runCurrent()
 
-            val actual by collectLastValue(audioSharingInteractor.audioSharingButtonStateUpdate)
-            bluetoothState.value = true
-            deviceItemUpdate.emit(listOf(deviceItem))
-            runCurrent()
-
-            assertThat(actual)
-                .isEqualTo(
-                    AudioSharingButtonState.Visible(
-                        R.string.quick_settings_bluetooth_audio_sharing_button,
-                        isActive = false
-                    )
-                )
+                assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isTrue()
+                job.cancel()
+            }
         }
-    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryTest.kt
new file mode 100644
index 0000000..c9e8813
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryTest.kt
@@ -0,0 +1,183 @@
+/*
+ * 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.bluetooth.qsdialog
+
+import android.bluetooth.BluetoothDevice
+import android.bluetooth.BluetoothLeBroadcastMetadata
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.volume.data.repository.audioSharingRepository
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class AudioSharingRepositoryTest : SysuiTestCase() {
+    @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+    @Mock private lateinit var profileManager: LocalBluetoothProfileManager
+    @Mock private lateinit var leAudioBroadcastProfile: LocalBluetoothLeBroadcast
+    @Mock private lateinit var leAudioBroadcastAssistant: LocalBluetoothLeBroadcastAssistant
+    @Mock private lateinit var metadata: BluetoothLeBroadcastMetadata
+    @Mock private lateinit var bluetoothDevice: BluetoothDevice
+    private val kosmos = testKosmos()
+    private lateinit var underTest: AudioSharingRepository
+
+    @Before
+    fun setUp() {
+        underTest =
+            AudioSharingRepositoryImpl(
+                kosmos.localBluetoothManager,
+                kosmos.audioSharingRepository,
+                kosmos.testDispatcher,
+            )
+    }
+
+    @Test
+    fun testSwitchActive() =
+        with(kosmos) {
+            testScope.runTest {
+                audioSharingRepository.setAudioSharingAvailable(true)
+                underTest.setActive(cachedBluetoothDevice)
+                verify(cachedBluetoothDevice).setActive()
+            }
+        }
+
+    @Test
+    fun testSwitchActive_flagOff_doNothing() =
+        with(kosmos) {
+            testScope.runTest {
+                audioSharingRepository.setAudioSharingAvailable(false)
+                underTest.setActive(cachedBluetoothDevice)
+                verify(cachedBluetoothDevice, never()).setActive()
+            }
+        }
+
+    @Test
+    fun testStartAudioSharing() =
+        with(kosmos) {
+            testScope.runTest {
+                whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
+                whenever(profileManager.leAudioBroadcastProfile).thenReturn(leAudioBroadcastProfile)
+                audioSharingRepository.setAudioSharingAvailable(true)
+                underTest.startAudioSharing()
+                verify(leAudioBroadcastProfile).startPrivateBroadcast()
+            }
+        }
+
+    @Test
+    fun testStartAudioSharing_flagOff_doNothing() =
+        with(kosmos) {
+            testScope.runTest {
+                audioSharingRepository.setAudioSharingAvailable(false)
+                underTest.startAudioSharing()
+                verify(leAudioBroadcastProfile, never()).startPrivateBroadcast()
+            }
+        }
+
+    @Test
+    fun testAddSource_flagOff_doesNothing() =
+        with(kosmos) {
+            testScope.runTest {
+                audioSharingRepository.setAudioSharingAvailable(false)
+
+                underTest.addSource()
+                runCurrent()
+
+                verify(leAudioBroadcastAssistant, never()).allConnectedDevices
+            }
+        }
+
+    @Test
+    fun testAddSource_noMetadata_doesNothing() =
+        with(kosmos) {
+            testScope.runTest {
+                whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
+                whenever(profileManager.leAudioBroadcastProfile).thenReturn(leAudioBroadcastProfile)
+                audioSharingRepository.setAudioSharingAvailable(true)
+                whenever(leAudioBroadcastProfile.latestBluetoothLeBroadcastMetadata)
+                    .thenReturn(null)
+
+                underTest.addSource()
+                runCurrent()
+
+                verify(leAudioBroadcastAssistant, never()).allConnectedDevices
+            }
+        }
+
+    @Test
+    fun testAddSource_noConnectedDevice_doesNothing() =
+        with(kosmos) {
+            testScope.runTest {
+                whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
+                whenever(profileManager.leAudioBroadcastProfile).thenReturn(leAudioBroadcastProfile)
+                whenever(profileManager.leAudioBroadcastAssistantProfile)
+                    .thenReturn(leAudioBroadcastAssistant)
+                audioSharingRepository.setAudioSharingAvailable(true)
+                whenever(leAudioBroadcastProfile.latestBluetoothLeBroadcastMetadata)
+                    .thenReturn(metadata)
+                whenever(leAudioBroadcastAssistant.allConnectedDevices).thenReturn(emptyList())
+
+                underTest.addSource()
+                runCurrent()
+
+                verify(leAudioBroadcastAssistant, never()).addSource(any(), any(), anyBoolean())
+            }
+        }
+
+    @Test
+    fun testAddSource_hasConnectedDeviceAndMetadata_addSource() =
+        with(kosmos) {
+            testScope.runTest {
+                whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
+                whenever(profileManager.leAudioBroadcastProfile).thenReturn(leAudioBroadcastProfile)
+                whenever(profileManager.leAudioBroadcastAssistantProfile)
+                    .thenReturn(leAudioBroadcastAssistant)
+                audioSharingRepository.setAudioSharingAvailable(true)
+                whenever(leAudioBroadcastProfile.latestBluetoothLeBroadcastMetadata)
+                    .thenReturn(metadata)
+                whenever(leAudioBroadcastAssistant.allConnectedDevices)
+                    .thenReturn(listOf(bluetoothDevice))
+
+                underTest.addSource()
+                runCurrent()
+
+                verify(leAudioBroadcastAssistant).addSource(bluetoothDevice, metadata, false)
+            }
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt
index d7bea66..a56c2cb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt
@@ -31,8 +31,11 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.DialogTransitionAnimator
 import com.android.systemui.animation.Expandable
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.testKosmos
 import com.android.systemui.util.FakeSharedPreferences
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.kotlin.getMutableStateFlow
@@ -42,12 +45,12 @@
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.test.TestCoroutineScheduler
 import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Rule
@@ -64,10 +67,12 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
+@OptIn(ExperimentalCoroutinesApi::class)
 @EnableFlags(Flags.FLAG_BLUETOOTH_QS_TILE_DIALOG_AUTO_ON_TOGGLE)
 class BluetoothTileDialogViewModelTest : SysuiTestCase() {
 
     @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+    private val kosmos = testKosmos()
     private val fakeSystemClock = FakeSystemClock()
     private val backgroundExecutor = FakeExecutor(fakeSystemClock)
 
@@ -75,8 +80,6 @@
 
     @Mock private lateinit var bluetoothStateInteractor: BluetoothStateInteractor
 
-    @Mock private lateinit var audioSharingInteractor: AudioSharingInteractor
-
     @Mock private lateinit var bluetoothDeviceMetadataInteractor: BluetoothDeviceMetadataInteractor
 
     @Mock private lateinit var deviceItemInteractor: DeviceItemInteractor
@@ -111,15 +114,15 @@
 
     private val sharedPreferences = FakeSharedPreferences()
 
-    private lateinit var scheduler: TestCoroutineScheduler
     private lateinit var dispatcher: CoroutineDispatcher
     private lateinit var testScope: TestScope
 
     @Before
     fun setUp() {
-        scheduler = TestCoroutineScheduler()
-        dispatcher = UnconfinedTestDispatcher(scheduler)
-        testScope = TestScope(dispatcher)
+        dispatcher = kosmos.testDispatcher
+        testScope = kosmos.testScope
+        // TODO(b/364515243): use real object instead of mock
+        whenever(kosmos.deviceItemInteractor.deviceItemUpdate).thenReturn(MutableSharedFlow())
         bluetoothTileDialogViewModel =
             BluetoothTileDialogViewModel(
                 deviceItemInteractor,
@@ -139,11 +142,13 @@
                         dispatcher
                     )
                 ),
-                audioSharingInteractor,
+                kosmos.audioSharingInteractor,
+                kosmos.audioSharingButtonViewModelFactory,
                 bluetoothDeviceMetadataInteractor,
                 mDialogTransitionAnimator,
                 activityStarter,
                 uiEventLogger,
+                bluetoothTileDialogLogger,
                 testScope.backgroundScope,
                 dispatcher,
                 dispatcher,
@@ -161,13 +166,10 @@
         whenever(sysuiDialog.context).thenReturn(mContext)
         whenever(bluetoothTileDialogDelegate.bluetoothStateToggle)
             .thenReturn(getMutableStateFlow(false))
-        whenever(bluetoothTileDialogDelegate.deviceItemClick)
-            .thenReturn(getMutableStateFlow(deviceItem))
+        whenever(bluetoothTileDialogDelegate.deviceItemClick).thenReturn(MutableSharedFlow())
         whenever(bluetoothTileDialogDelegate.contentHeight).thenReturn(getMutableStateFlow(0))
         whenever(bluetoothTileDialogDelegate.bluetoothAutoOnToggle)
             .thenReturn(getMutableStateFlow(false))
-        whenever(audioSharingInteractor.audioSharingButtonStateUpdate)
-            .thenReturn(getMutableStateFlow(AudioSharingButtonState.Gone))
         whenever(expandable.dialogTransitionController(any())).thenReturn(controller)
     }
 
@@ -175,6 +177,7 @@
     fun testShowDialog_noAnimation() {
         testScope.runTest {
             bluetoothTileDialogViewModel.showDialog(null)
+            runCurrent()
 
             verify(mDialogTransitionAnimator, never()).show(any(), any(), any())
         }
@@ -184,6 +187,7 @@
     fun testShowDialog_animated() {
         testScope.runTest {
             bluetoothTileDialogViewModel.showDialog(expandable)
+            runCurrent()
 
             verify(mDialogTransitionAnimator).show(any(), any(), anyBoolean())
         }
@@ -194,6 +198,7 @@
         testScope.runTest {
             backgroundExecutor.execute {
                 bluetoothTileDialogViewModel.showDialog(expandable)
+                runCurrent()
 
                 verify(mDialogTransitionAnimator).show(any(), any(), anyBoolean())
             }
@@ -204,6 +209,7 @@
     fun testShowDialog_fetchDeviceItem() {
         testScope.runTest {
             bluetoothTileDialogViewModel.showDialog(null)
+            runCurrent()
 
             verify(deviceItemInteractor).deviceItemUpdate
         }
@@ -214,6 +220,7 @@
         testScope.runTest {
             whenever(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice)
             bluetoothTileDialogViewModel.showDialog(null)
+            runCurrent()
 
             val clickedView = View(context)
             bluetoothTileDialogViewModel.onPairNewDeviceClicked(clickedView)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt
index 681ea75..9c427c6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt
@@ -15,34 +15,22 @@
  */
 package com.android.systemui.bluetooth.qsdialog
 
-import android.bluetooth.BluetoothDevice
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import androidx.test.filters.SmallTest
-import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
-import com.android.dx.mockito.inline.extended.StaticMockitoSession
-import com.android.settingslib.bluetooth.BluetoothUtils
-import com.android.settingslib.bluetooth.CachedBluetoothDevice
-import com.android.settingslib.bluetooth.LeAudioProfile
-import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant
-import com.android.settingslib.bluetooth.LocalBluetoothProfileManager
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
-import com.android.systemui.plugins.activityStarter
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.testKosmos
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runTest
-import org.junit.After
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers
 import org.mockito.Mock
-import org.mockito.Mockito
 import org.mockito.Mockito.verify
 import org.mockito.junit.MockitoJUnit
 import org.mockito.junit.MockitoRule
@@ -56,28 +44,18 @@
     @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
     private val kosmos = testKosmos().apply { testDispatcher = UnconfinedTestDispatcher() }
     private lateinit var actionInteractorImpl: DeviceItemActionInteractor
-    private lateinit var mockitoSession: StaticMockitoSession
     private lateinit var activeMediaDeviceItem: DeviceItem
     private lateinit var notConnectedDeviceItem: DeviceItem
-    private lateinit var connectedAudioSharingMediaDeviceItem: DeviceItem
     private lateinit var connectedMediaDeviceItem: DeviceItem
     private lateinit var connectedOtherDeviceItem: DeviceItem
     @Mock private lateinit var dialog: SystemUIDialog
-    @Mock private lateinit var profileManager: LocalBluetoothProfileManager
-    @Mock private lateinit var leAudioProfile: LeAudioProfile
-    @Mock private lateinit var assistantProfile: LocalBluetoothLeBroadcastAssistant
-    @Mock private lateinit var bluetoothDevice: BluetoothDevice
-    @Mock private lateinit var bluetoothDeviceGroupId2: BluetoothDevice
-    @Mock private lateinit var cachedBluetoothDevice: CachedBluetoothDevice
 
     @Before
     fun setUp() {
-        mockitoSession =
-            mockitoSession().initMocks(this).mockStatic(BluetoothUtils::class.java).startMocking()
         activeMediaDeviceItem =
             DeviceItem(
                 type = DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE,
-                cachedBluetoothDevice = cachedBluetoothDevice,
+                cachedBluetoothDevice = kosmos.cachedBluetoothDevice,
                 deviceName = DEVICE_NAME,
                 connectionSummary = DEVICE_CONNECTION_SUMMARY,
                 iconWithDescription = null,
@@ -86,7 +64,7 @@
         notConnectedDeviceItem =
             DeviceItem(
                 type = DeviceItemType.SAVED_BLUETOOTH_DEVICE,
-                cachedBluetoothDevice = cachedBluetoothDevice,
+                cachedBluetoothDevice = kosmos.cachedBluetoothDevice,
                 deviceName = DEVICE_NAME,
                 connectionSummary = DEVICE_CONNECTION_SUMMARY,
                 iconWithDescription = null,
@@ -95,16 +73,7 @@
         connectedMediaDeviceItem =
             DeviceItem(
                 type = DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE,
-                cachedBluetoothDevice = cachedBluetoothDevice,
-                deviceName = DEVICE_NAME,
-                connectionSummary = DEVICE_CONNECTION_SUMMARY,
-                iconWithDescription = null,
-                background = null
-            )
-        connectedAudioSharingMediaDeviceItem =
-            DeviceItem(
-                type = DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE,
-                cachedBluetoothDevice = cachedBluetoothDevice,
+                cachedBluetoothDevice = kosmos.cachedBluetoothDevice,
                 deviceName = DEVICE_NAME,
                 connectionSummary = DEVICE_CONNECTION_SUMMARY,
                 iconWithDescription = null,
@@ -113,18 +82,13 @@
         connectedOtherDeviceItem =
             DeviceItem(
                 type = DeviceItemType.CONNECTED_BLUETOOTH_DEVICE,
-                cachedBluetoothDevice = cachedBluetoothDevice,
+                cachedBluetoothDevice = kosmos.cachedBluetoothDevice,
                 deviceName = DEVICE_NAME,
                 connectionSummary = DEVICE_CONNECTION_SUMMARY,
                 iconWithDescription = null,
                 background = null
             )
-        actionInteractorImpl = kosmos.deviceItemActionInteractor
-    }
-
-    @After
-    fun tearDown() {
-        mockitoSession.finishMocking()
+        actionInteractorImpl = kosmos.deviceItemActionInteractorImpl
     }
 
     @Test
@@ -132,14 +96,8 @@
         with(kosmos) {
             testScope.runTest {
                 whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
-                whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(false)
                 actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog)
                 verify(cachedBluetoothDevice).setActive()
-                verify(bluetoothTileDialogLogger)
-                    .logDeviceClick(
-                        cachedBluetoothDevice.address,
-                        DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE
-                    )
             }
         }
     }
@@ -149,14 +107,8 @@
         with(kosmos) {
             testScope.runTest {
                 whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
-                whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(false)
                 actionInteractorImpl.onClick(activeMediaDeviceItem, dialog)
                 verify(cachedBluetoothDevice).disconnect()
-                verify(bluetoothTileDialogLogger)
-                    .logDeviceClick(
-                        cachedBluetoothDevice.address,
-                        DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE
-                    )
             }
         }
     }
@@ -166,14 +118,8 @@
         with(kosmos) {
             testScope.runTest {
                 whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
-                whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(false)
                 actionInteractorImpl.onClick(connectedOtherDeviceItem, dialog)
                 verify(cachedBluetoothDevice).disconnect()
-                verify(bluetoothTileDialogLogger)
-                    .logDeviceClick(
-                        cachedBluetoothDevice.address,
-                        DeviceItemType.CONNECTED_BLUETOOTH_DEVICE
-                    )
             }
         }
     }
@@ -183,293 +129,8 @@
         with(kosmos) {
             testScope.runTest {
                 whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
-                whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(false)
                 actionInteractorImpl.onClick(notConnectedDeviceItem, dialog)
                 verify(cachedBluetoothDevice).connect()
-                verify(bluetoothTileDialogLogger)
-                    .logDeviceClick(
-                        cachedBluetoothDevice.address,
-                        DeviceItemType.SAVED_BLUETOOTH_DEVICE
-                    )
-            }
-        }
-    }
-
-    @Test
-    fun testOnClick_connectedAudioSharingMediaDevice_logClick() {
-        with(kosmos) {
-            testScope.runTest {
-                whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
-                actionInteractorImpl.onClick(connectedAudioSharingMediaDeviceItem, dialog)
-                verify(bluetoothTileDialogLogger)
-                    .logDeviceClick(
-                        cachedBluetoothDevice.address,
-                        DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE
-                    )
-            }
-        }
-    }
-
-    @Test
-    fun testOnClick_audioSharingDisabled_shouldNotLaunchSettings() {
-        with(kosmos) {
-            testScope.runTest {
-                whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
-                whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(false)
-
-                actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog)
-                verify(activityStarter, Mockito.never())
-                    .postStartActivityDismissingKeyguard(
-                        ArgumentMatchers.any(),
-                        ArgumentMatchers.anyInt(),
-                        ArgumentMatchers.any()
-                    )
-            }
-        }
-    }
-
-    @Test
-    fun testOnClick_inAudioSharing_clickedDeviceHasSource_shouldNotLaunchSettings() {
-        with(kosmos) {
-            testScope.runTest {
-                whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
-                whenever(cachedBluetoothDevice.uiAccessibleProfiles)
-                    .thenReturn(listOf(leAudioProfile))
-
-                whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true)
-                whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
-                whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile)
-                whenever(profileManager.leAudioBroadcastAssistantProfile)
-                    .thenReturn(assistantProfile)
-
-                whenever(BluetoothUtils.isBroadcasting(ArgumentMatchers.any())).thenReturn(true)
-                whenever(
-                        BluetoothUtils.hasConnectedBroadcastSource(
-                            ArgumentMatchers.any(),
-                            ArgumentMatchers.any()
-                        )
-                    )
-                    .thenReturn(true)
-
-                actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog)
-                verify(activityStarter, Mockito.never())
-                    .postStartActivityDismissingKeyguard(
-                        ArgumentMatchers.any(),
-                        ArgumentMatchers.anyInt(),
-                        ArgumentMatchers.any()
-                    )
-            }
-        }
-    }
-
-    @Test
-    fun testOnClick_inAudioSharing_clickedDeviceNoSource_shouldLaunchSettings() {
-        with(kosmos) {
-            testScope.runTest {
-                whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
-                whenever(cachedBluetoothDevice.device).thenReturn(bluetoothDevice)
-                whenever(cachedBluetoothDevice.uiAccessibleProfiles)
-                    .thenReturn(listOf(leAudioProfile))
-
-                whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true)
-                whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
-                whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile)
-                whenever(profileManager.leAudioBroadcastAssistantProfile)
-                    .thenReturn(assistantProfile)
-
-                whenever(BluetoothUtils.isBroadcasting(ArgumentMatchers.any())).thenReturn(true)
-                whenever(
-                        BluetoothUtils.hasConnectedBroadcastSource(
-                            ArgumentMatchers.any(),
-                            ArgumentMatchers.any()
-                        )
-                    )
-                    .thenReturn(false)
-
-                actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog)
-                verify(activityStarter)
-                    .postStartActivityDismissingKeyguard(
-                        ArgumentMatchers.any(),
-                        ArgumentMatchers.anyInt(),
-                        ArgumentMatchers.any()
-                    )
-            }
-        }
-    }
-
-    @Test
-    fun testOnClick_noConnectedLeDevice_shouldNotLaunchSettings() {
-        with(kosmos) {
-            testScope.runTest {
-                whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
-
-                whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true)
-                whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
-                whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile)
-                whenever(profileManager.leAudioBroadcastAssistantProfile)
-                    .thenReturn(assistantProfile)
-
-                actionInteractorImpl.onClick(notConnectedDeviceItem, dialog)
-                verify(activityStarter, Mockito.never())
-                    .postStartActivityDismissingKeyguard(
-                        ArgumentMatchers.any(),
-                        ArgumentMatchers.anyInt(),
-                        ArgumentMatchers.any()
-                    )
-            }
-        }
-    }
-
-    @Test
-    fun testOnClick_hasOneConnectedLeDevice_clickedNonLe_shouldNotLaunchSettings() {
-        with(kosmos) {
-            testScope.runTest {
-                whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
-
-                whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true)
-                whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
-                whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile)
-                whenever(profileManager.leAudioBroadcastAssistantProfile)
-                    .thenReturn(assistantProfile)
-
-                whenever(
-                        assistantProfile.getDevicesMatchingConnectionStates(ArgumentMatchers.any())
-                    )
-                    .thenReturn(listOf(bluetoothDevice))
-
-                actionInteractorImpl.onClick(notConnectedDeviceItem, dialog)
-                verify(activityStarter, Mockito.never())
-                    .postStartActivityDismissingKeyguard(
-                        ArgumentMatchers.any(),
-                        ArgumentMatchers.anyInt(),
-                        ArgumentMatchers.any()
-                    )
-            }
-        }
-    }
-
-    @Test
-    fun testOnClick_hasOneConnectedLeDevice_clickedLe_shouldLaunchSettings() {
-        with(kosmos) {
-            testScope.runTest {
-                whenever(cachedBluetoothDevice.device).thenReturn(bluetoothDevice)
-                whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
-                whenever(cachedBluetoothDevice.profiles).thenReturn(listOf(leAudioProfile))
-                whenever(leAudioProfile.isEnabled(ArgumentMatchers.any())).thenReturn(true)
-
-                whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true)
-                whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
-                whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile)
-                whenever(profileManager.leAudioBroadcastAssistantProfile)
-                    .thenReturn(assistantProfile)
-
-                whenever(
-                        assistantProfile.getDevicesMatchingConnectionStates(ArgumentMatchers.any())
-                    )
-                    .thenReturn(listOf(bluetoothDevice))
-
-                actionInteractorImpl.onClick(notConnectedDeviceItem, dialog)
-                verify(activityStarter)
-                    .postStartActivityDismissingKeyguard(
-                        ArgumentMatchers.any(),
-                        ArgumentMatchers.anyInt(),
-                        ArgumentMatchers.any()
-                    )
-            }
-        }
-    }
-
-    @Test
-    fun testOnClick_hasOneConnectedLeDevice_clickedConnectedLe_shouldNotLaunchSettings() {
-        with(kosmos) {
-            testScope.runTest {
-                whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
-
-                whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true)
-                whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
-                whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile)
-                whenever(profileManager.leAudioBroadcastAssistantProfile)
-                    .thenReturn(assistantProfile)
-
-                whenever(
-                        assistantProfile.getDevicesMatchingConnectionStates(ArgumentMatchers.any())
-                    )
-                    .thenReturn(listOf(bluetoothDevice))
-
-                actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog)
-                verify(activityStarter, Mockito.never())
-                    .postStartActivityDismissingKeyguard(
-                        ArgumentMatchers.any(),
-                        ArgumentMatchers.anyInt(),
-                        ArgumentMatchers.any()
-                    )
-            }
-        }
-    }
-
-    @Test
-    fun testOnClick_hasTwoConnectedLeDevice_clickedNotConnectedLe_shouldNotLaunchSettings() {
-        with(kosmos) {
-            testScope.runTest {
-                whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
-
-                whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true)
-                whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
-                whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile)
-                whenever(profileManager.leAudioBroadcastAssistantProfile)
-                    .thenReturn(assistantProfile)
-
-                whenever(
-                        assistantProfile.getDevicesMatchingConnectionStates(ArgumentMatchers.any())
-                    )
-                    .thenReturn(listOf(bluetoothDevice, bluetoothDeviceGroupId2))
-                whenever(leAudioProfile.getGroupId(ArgumentMatchers.any())).thenAnswer {
-                    val device = it.arguments.first() as BluetoothDevice
-                    if (device == bluetoothDevice) GROUP_ID_1 else GROUP_ID_2
-                }
-
-                actionInteractorImpl.onClick(notConnectedDeviceItem, dialog)
-                verify(activityStarter, Mockito.never())
-                    .postStartActivityDismissingKeyguard(
-                        ArgumentMatchers.any(),
-                        ArgumentMatchers.anyInt(),
-                        ArgumentMatchers.any()
-                    )
-            }
-        }
-    }
-
-    @Test
-    fun testOnClick_hasTwoConnectedLeDevice_clickedActiveLe_shouldLaunchSettings() {
-        with(kosmos) {
-            testScope.runTest {
-                whenever(cachedBluetoothDevice.device).thenReturn(bluetoothDevice)
-                whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
-                whenever(cachedBluetoothDevice.profiles).thenReturn(listOf(leAudioProfile))
-                whenever(leAudioProfile.isEnabled(ArgumentMatchers.any())).thenReturn(true)
-
-                whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true)
-                whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
-                whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile)
-                whenever(profileManager.leAudioBroadcastAssistantProfile)
-                    .thenReturn(assistantProfile)
-
-                whenever(
-                        assistantProfile.getDevicesMatchingConnectionStates(ArgumentMatchers.any())
-                    )
-                    .thenReturn(listOf(bluetoothDevice, bluetoothDeviceGroupId2))
-                whenever(leAudioProfile.getGroupId(ArgumentMatchers.any())).thenAnswer {
-                    val device = it.arguments.first() as BluetoothDevice
-                    if (device == bluetoothDevice) GROUP_ID_1 else GROUP_ID_2
-                }
-
-                actionInteractorImpl.onClick(activeMediaDeviceItem, dialog)
-                verify(activityStarter)
-                    .postStartActivityDismissingKeyguard(
-                        ArgumentMatchers.any(),
-                        ArgumentMatchers.anyInt(),
-                        ArgumentMatchers.any()
-                    )
             }
         }
     }
@@ -478,7 +139,5 @@
         const val DEVICE_NAME = "device"
         const val DEVICE_CONNECTION_SUMMARY = "active"
         const val DEVICE_ADDRESS = "address"
-        const val GROUP_ID_1 = 1
-        const val GROUP_ID_2 = 2
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt
index ef441c1..10c3457 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt
@@ -133,8 +133,8 @@
 
     @Test
     fun testAvailableAudioSharingMediaDeviceItemFactory_isFilterMatched_flagOff_returnsFalse() {
-         // Flags.FLAG_ENABLE_LE_AUDIO_SHARING off or the device doesn't support broadcast
-         // source or assistant.
+        // Flags.FLAG_ENABLE_LE_AUDIO_SHARING off or the device doesn't support broadcast
+        // source or assistant.
         `when`(BluetoothUtils.isAudioSharingEnabled()).thenReturn(false)
 
         assertThat(
@@ -145,9 +145,9 @@
     }
 
     @Test
-    fun testAvailableAudioSharingMediaDeviceItemFactory_isFilterMatched_isActiveDevice_returnsFalse() {
-         // Flags.FLAG_ENABLE_LE_AUDIO_SHARING on and the device support broadcast source and
-         // assistant.
+    fun testAvailableAudioSharingMediaDeviceItemFactory_isFilterMatched_isActiveDevice_false() {
+        // Flags.FLAG_ENABLE_LE_AUDIO_SHARING on and the device support broadcast source and
+        // assistant.
         `when`(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true)
         `when`(BluetoothUtils.isActiveMediaDevice(any())).thenReturn(true)
 
@@ -159,9 +159,9 @@
     }
 
     @Test
-    fun testAvailableAudioSharingMediaDeviceItemFactory_isFilterMatched_isNotAvailable_returnsFalse() {
-         // Flags.FLAG_ENABLE_LE_AUDIO_SHARING on and the device support broadcast source and
-         // assistant.
+    fun testAvailableAudioSharingMediaDeviceItemFactory_isFilterMatched_isNotAvailable_false() {
+        // Flags.FLAG_ENABLE_LE_AUDIO_SHARING on and the device support broadcast source and
+        // assistant.
         `when`(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true)
         `when`(BluetoothUtils.isActiveMediaDevice(any())).thenReturn(false)
         `when`(BluetoothUtils.isAvailableMediaBluetoothDevice(any(), any())).thenReturn(true)
@@ -177,8 +177,8 @@
 
     @Test
     fun testAvailableAudioSharingMediaDeviceItemFactory_isFilterMatched_returnsTrue() {
-         // Flags.FLAG_ENABLE_LE_AUDIO_SHARING on and the device support broadcast source and
-         // assistant.
+        // Flags.FLAG_ENABLE_LE_AUDIO_SHARING on and the device support broadcast source and
+        // assistant.
         `when`(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true)
         `when`(BluetoothUtils.isActiveMediaDevice(any())).thenReturn(false)
         `when`(BluetoothUtils.isAvailableMediaBluetoothDevice(any(), any())).thenReturn(true)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt
index 194590c..c39b9a6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt
@@ -83,18 +83,6 @@
     fun setUp() {
         dispatcher = UnconfinedTestDispatcher()
         testScope = TestScope(dispatcher)
-        interactor =
-            DeviceItemInteractor(
-                bluetoothTileDialogRepository,
-                audioManager,
-                adapter,
-                localBluetoothManager,
-                fakeSystemClock,
-                logger,
-                testScope.backgroundScope,
-                dispatcher
-            )
-
         `when`(deviceItem1.cachedBluetoothDevice).thenReturn(cachedDevice1)
         `when`(deviceItem2.cachedBluetoothDevice).thenReturn(cachedDevice2)
         `when`(cachedDevice1.address).thenReturn("ADDRESS")
@@ -108,9 +96,19 @@
     fun testUpdateDeviceItems_noCachedDevice_returnEmpty() {
         testScope.runTest {
             `when`(bluetoothTileDialogRepository.cachedDevices).thenReturn(emptyList())
-            interactor.setDeviceItemFactoryListForTesting(
-                listOf(createFactory({ true }, deviceItem1))
-            )
+            interactor =
+                DeviceItemInteractor(
+                    bluetoothTileDialogRepository,
+                    audioManager,
+                    adapter,
+                    localBluetoothManager,
+                    fakeSystemClock,
+                    logger,
+                    listOf(createFactory({ true }, deviceItem1)),
+                    emptyList(),
+                    testScope.backgroundScope,
+                    dispatcher
+                )
 
             val latest by collectLastValue(interactor.deviceItemUpdate)
             val latestShowSeeAll by collectLastValue(interactor.showSeeAllUpdate)
@@ -125,9 +123,19 @@
     fun testUpdateDeviceItems_hasCachedDevice_filterNotMatch_returnEmpty() {
         testScope.runTest {
             `when`(bluetoothTileDialogRepository.cachedDevices).thenReturn(listOf(cachedDevice1))
-            interactor.setDeviceItemFactoryListForTesting(
-                listOf(createFactory({ false }, deviceItem1))
-            )
+            interactor =
+                DeviceItemInteractor(
+                    bluetoothTileDialogRepository,
+                    audioManager,
+                    adapter,
+                    localBluetoothManager,
+                    fakeSystemClock,
+                    logger,
+                    listOf(createFactory({ false }, deviceItem1)),
+                    emptyList(),
+                    testScope.backgroundScope,
+                    dispatcher
+                )
 
             val latest by collectLastValue(interactor.deviceItemUpdate)
             val latestShowSeeAll by collectLastValue(interactor.showSeeAllUpdate)
@@ -142,9 +150,19 @@
     fun testUpdateDeviceItems_hasCachedDevice_filterMatch_returnDeviceItem() {
         testScope.runTest {
             `when`(bluetoothTileDialogRepository.cachedDevices).thenReturn(listOf(cachedDevice1))
-            interactor.setDeviceItemFactoryListForTesting(
-                listOf(createFactory({ true }, deviceItem1))
-            )
+            interactor =
+                DeviceItemInteractor(
+                    bluetoothTileDialogRepository,
+                    audioManager,
+                    adapter,
+                    localBluetoothManager,
+                    fakeSystemClock,
+                    logger,
+                    listOf(createFactory({ true }, deviceItem1)),
+                    emptyList(),
+                    testScope.backgroundScope,
+                    dispatcher
+                )
 
             val latest by collectLastValue(interactor.deviceItemUpdate)
             val latestShowSeeAll by collectLastValue(interactor.showSeeAllUpdate)
@@ -159,9 +177,22 @@
     fun testUpdateDeviceItems_hasCachedDevice_filterMatch_returnMultipleDeviceItem() {
         testScope.runTest {
             `when`(adapter.mostRecentlyConnectedDevices).thenReturn(null)
-            interactor.setDeviceItemFactoryListForTesting(
-                listOf(createFactory({ false }, deviceItem1), createFactory({ true }, deviceItem2))
-            )
+            interactor =
+                DeviceItemInteractor(
+                    bluetoothTileDialogRepository,
+                    audioManager,
+                    adapter,
+                    localBluetoothManager,
+                    fakeSystemClock,
+                    logger,
+                    listOf(
+                        createFactory({ false }, deviceItem1),
+                        createFactory({ true }, deviceItem2)
+                    ),
+                    emptyList(),
+                    testScope.backgroundScope,
+                    dispatcher
+                )
 
             val latest by collectLastValue(interactor.deviceItemUpdate)
             val latestShowSeeAll by collectLastValue(interactor.showSeeAllUpdate)
@@ -176,18 +207,31 @@
     fun testUpdateDeviceItems_sortByDisplayPriority() {
         testScope.runTest {
             `when`(adapter.mostRecentlyConnectedDevices).thenReturn(null)
-            interactor.setDeviceItemFactoryListForTesting(
-                listOf(
-                    createFactory({ cachedDevice -> cachedDevice.device == device1 }, deviceItem1),
-                    createFactory({ cachedDevice -> cachedDevice.device == device2 }, deviceItem2)
+            interactor =
+                DeviceItemInteractor(
+                    bluetoothTileDialogRepository,
+                    audioManager,
+                    adapter,
+                    localBluetoothManager,
+                    fakeSystemClock,
+                    logger,
+                    listOf(
+                        createFactory(
+                            { cachedDevice -> cachedDevice.device == device1 },
+                            deviceItem1
+                        ),
+                        createFactory(
+                            { cachedDevice -> cachedDevice.device == device2 },
+                            deviceItem2
+                        )
+                    ),
+                    listOf(
+                        DeviceItemType.SAVED_BLUETOOTH_DEVICE,
+                        DeviceItemType.CONNECTED_BLUETOOTH_DEVICE
+                    ),
+                    testScope.backgroundScope,
+                    dispatcher
                 )
-            )
-            interactor.setDisplayPriorityForTesting(
-                listOf(
-                    DeviceItemType.SAVED_BLUETOOTH_DEVICE,
-                    DeviceItemType.CONNECTED_BLUETOOTH_DEVICE
-                )
-            )
             `when`(deviceItem1.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
             `when`(deviceItem2.type).thenReturn(DeviceItemType.SAVED_BLUETOOTH_DEVICE)
 
@@ -204,15 +248,28 @@
     fun testUpdateDeviceItems_sameType_sortByRecentlyConnected() {
         testScope.runTest {
             `when`(adapter.mostRecentlyConnectedDevices).thenReturn(listOf(device2, device1))
-            interactor.setDeviceItemFactoryListForTesting(
-                listOf(
-                    createFactory({ cachedDevice -> cachedDevice.device == device1 }, deviceItem1),
-                    createFactory({ cachedDevice -> cachedDevice.device == device2 }, deviceItem2)
+            interactor =
+                DeviceItemInteractor(
+                    bluetoothTileDialogRepository,
+                    audioManager,
+                    adapter,
+                    localBluetoothManager,
+                    fakeSystemClock,
+                    logger,
+                    listOf(
+                        createFactory(
+                            { cachedDevice -> cachedDevice.device == device1 },
+                            deviceItem1
+                        ),
+                        createFactory(
+                            { cachedDevice -> cachedDevice.device == device2 },
+                            deviceItem2
+                        )
+                    ),
+                    listOf(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE),
+                    testScope.backgroundScope,
+                    dispatcher
                 )
-            )
-            interactor.setDisplayPriorityForTesting(
-                listOf(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
-            )
             `when`(deviceItem1.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
             `when`(deviceItem2.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
 
@@ -231,10 +288,19 @@
             `when`(bluetoothTileDialogRepository.cachedDevices)
                 .thenReturn(listOf(cachedDevice2, cachedDevice2, cachedDevice2, cachedDevice2))
             `when`(adapter.mostRecentlyConnectedDevices).thenReturn(null)
-            interactor.setDeviceItemFactoryListForTesting(
-                listOf(createFactory({ true }, deviceItem2))
-            )
-
+            interactor =
+                DeviceItemInteractor(
+                    bluetoothTileDialogRepository,
+                    audioManager,
+                    adapter,
+                    localBluetoothManager,
+                    fakeSystemClock,
+                    logger,
+                    listOf(createFactory({ true }, deviceItem2)),
+                    emptyList(),
+                    testScope.backgroundScope,
+                    dispatcher
+                )
             val latest by collectLastValue(interactor.deviceItemUpdate)
             val latestShowSeeAll by collectLastValue(interactor.showSeeAllUpdate)
             interactor.updateDeviceItems(mContext, DeviceFetchTrigger.FIRST_LOAD)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardModelTest.kt
index 85e8ab4..5741d64 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardModelTest.kt
@@ -122,6 +122,7 @@
 
     @Test
     @Throws(IOException::class)
+    @DisableFlags(FLAG_CLIPBOARD_USE_DESCRIPTION_MIMETYPE)
     fun test_imageClipData_loadFailure() {
         whenever(mMockContext.contentResolver).thenReturn(mMockContentResolver)
         whenever(mMockContext.resources).thenReturn(mContext.resources)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepositoryImplTest.kt
new file mode 100644
index 0000000..ff3186a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepositoryImplTest.kt
@@ -0,0 +1,158 @@
+/*
+ * 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.display.data.repository
+
+import android.content.testableContext
+import android.platform.test.annotations.EnableFlags
+import android.view.Display
+import android.view.mockWindowManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.display.shared.model.DisplayWindowProperties
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.unconfinedTestDispatcher
+import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+
+@EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class DisplayWindowPropertiesRepositoryImplTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos().also { it.testDispatcher = it.unconfinedTestDispatcher }
+    private val fakeDisplayRepository = kosmos.displayRepository
+    private val testScope = kosmos.testScope
+
+    private val applicationContext = kosmos.testableContext
+    private val applicationWindowManager = kosmos.mockWindowManager
+
+    private val repo =
+        DisplayWindowPropertiesRepositoryImpl(
+            kosmos.applicationCoroutineScope,
+            applicationContext,
+            applicationWindowManager,
+            fakeDisplayRepository,
+        )
+
+    @Before
+    fun start() {
+        repo.start()
+    }
+
+    @Before
+    fun addDisplays() = runBlocking {
+        fakeDisplayRepository.addDisplay(createDisplay(DEFAULT_DISPLAY_ID))
+        fakeDisplayRepository.addDisplay(createDisplay(NON_DEFAULT_DISPLAY_ID))
+    }
+
+    @Test
+    fun get_defaultDisplayId_returnsDefaultProperties() =
+        testScope.runTest {
+            val displayContext = repo.get(DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO)
+
+            assertThat(displayContext)
+                .isEqualTo(
+                    DisplayWindowProperties(
+                        displayId = DEFAULT_DISPLAY_ID,
+                        windowType = WINDOW_TYPE_FOO,
+                        context = applicationContext,
+                        windowManager = applicationWindowManager,
+                    )
+                )
+        }
+
+    @Test
+    fun get_nonDefaultDisplayId_returnsNewStatusBarContext() =
+        testScope.runTest {
+            val displayContext = repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO)
+
+            assertThat(displayContext.context).isNotSameInstanceAs(applicationContext)
+        }
+
+    @Test
+    fun get_nonDefaultDisplayId_returnsNewWindowManager() =
+        testScope.runTest {
+            val displayContext = repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO)
+
+            assertThat(displayContext.windowManager).isNotSameInstanceAs(applicationWindowManager)
+        }
+
+    @Test
+    fun get_multipleCallsForDefaultDisplay_returnsSameInstance() =
+        testScope.runTest {
+            val displayContext = repo.get(DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO)
+
+            assertThat(repo.get(DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO))
+                .isSameInstanceAs(displayContext)
+        }
+
+    @Test
+    fun get_multipleCallsForNonDefaultDisplay_returnsSameInstance() =
+        testScope.runTest {
+            val displayContext = repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO)
+
+            assertThat(repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO))
+                .isSameInstanceAs(displayContext)
+        }
+
+    @Test
+    fun get_multipleCalls_differentType_returnsNewInstance() =
+        testScope.runTest {
+            val displayContext = repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO)
+
+            assertThat(repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_BAR))
+                .isNotSameInstanceAs(displayContext)
+        }
+
+    @Test
+    fun get_afterDisplayRemoved_returnsNewInstance() =
+        testScope.runTest {
+            val displayContext = repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO)
+
+            fakeDisplayRepository.removeDisplay(NON_DEFAULT_DISPLAY_ID)
+            fakeDisplayRepository.addDisplay(createDisplay(NON_DEFAULT_DISPLAY_ID))
+
+            assertThat(repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO))
+                .isNotSameInstanceAs(displayContext)
+        }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun get_nonExistingDisplayId_throws() =
+        testScope.runTest { repo.get(NON_EXISTING_DISPLAY_ID, WINDOW_TYPE_FOO) }
+
+    private fun createDisplay(displayId: Int) =
+        mock<Display> { on { getDisplayId() } doReturn displayId }
+
+    companion object {
+        private const val DEFAULT_DISPLAY_ID = Display.DEFAULT_DISPLAY
+        private const val NON_DEFAULT_DISPLAY_ID = DEFAULT_DISPLAY_ID + 1
+        private const val NON_EXISTING_DISPLAY_ID = DEFAULT_DISPLAY_ID + 2
+        private const val WINDOW_TYPE_FOO = 123
+        private const val WINDOW_TYPE_BAR = 321
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/ActivatableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/ActivatableTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/ActivatableTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/lifecycle/ActivatableTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSImplTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/ScreenshotDetectionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDetectionControllerTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/ScreenshotDetectionControllerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDetectionControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerWifiTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerWifiTest.java
index 6febb91..7a579ba 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerWifiTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerWifiTest.java
@@ -58,7 +58,7 @@
     private static final int MIN_RSSI = -100;
     private static final int MAX_RSSI = -55;
     private WifiInfo mWifiInfo = mock(WifiInfo.class);
-    private VcnTransportInfo mVcnTransportInfo = mock(VcnTransportInfo.class);
+    private VcnTransportInfo mVcnTransportInfo = new VcnTransportInfo.Builder().build();
 
     @Before
     public void setUp() throws Exception {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index 59fc0d1..87cda64 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -591,8 +591,8 @@
         ArgumentCaptor<FooterView> captor = ArgumentCaptor.forClass(FooterView.class);
         verify(mStackScroller).setFooterView(captor.capture());
 
-        assertNotNull(captor.getValue().findViewById(R.id.manage_text).hasOnClickListeners());
-        assertNotNull(captor.getValue().findViewById(R.id.dismiss_text).hasOnClickListeners());
+        assertNotNull(captor.getValue().findViewById(R.id.manage_text));
+        assertNotNull(captor.getValue().findViewById(R.id.dismiss_text));
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
index 328d310..c48898a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
@@ -136,6 +136,7 @@
     private val wifiLogBuffer = LogBuffer("wifi", maxSize = 100, logcatEchoTracker = mock())
     private val wifiPickerTrackerCallback =
         argumentCaptor<WifiPickerTracker.WifiPickerTrackerCallback>()
+    private val vcnTransportInfo = VcnTransportInfo.Builder().build()
 
     private val testDispatcher = StandardTestDispatcher()
     private val testScope = TestScope(testDispatcher)
@@ -1003,6 +1004,18 @@
             assertThat(latest).isTrue()
         }
 
+    private fun newWifiNetwork(wifiInfo: WifiInfo): Network {
+        val network = mock<Network>()
+        val capabilities =
+            mock<NetworkCapabilities>().also {
+                whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
+                whenever(it.transportInfo).thenReturn(wifiInfo)
+            }
+        whenever(connectivityManager.getNetworkCapabilities(network)).thenReturn(capabilities)
+
+        return network
+    }
+
     /** Regression test for b/272586234. */
     @Test
     fun hasCarrierMergedConnection_carrierMergedViaWifiWithVcnTransport_isTrue() =
@@ -1012,10 +1025,12 @@
                     whenever(this.isCarrierMerged).thenReturn(true)
                     whenever(this.isPrimary).thenReturn(true)
                 }
+            val underlyingWifi = newWifiNetwork(carrierMergedInfo)
             val caps =
                 mock<NetworkCapabilities>().also {
                     whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
-                    whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo))
+                    whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+                    whenever(it.underlyingNetworks).thenReturn(listOf(underlyingWifi))
                 }
 
             val latest by collectLastValue(underTest.hasCarrierMergedConnection)
@@ -1034,10 +1049,12 @@
                     whenever(this.isCarrierMerged).thenReturn(true)
                     whenever(this.isPrimary).thenReturn(true)
                 }
+            val underlyingWifi = newWifiNetwork(carrierMergedInfo)
             val caps =
                 mock<NetworkCapabilities>().also {
                     whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
-                    whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo))
+                    whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+                    whenever(it.underlyingNetworks).thenReturn(listOf(underlyingWifi))
                 }
 
             val latest by collectLastValue(underTest.hasCarrierMergedConnection)
@@ -1094,10 +1111,15 @@
                     whenever(this.isCarrierMerged).thenReturn(true)
                     whenever(this.isPrimary).thenReturn(true)
                 }
+
+            // The Wifi network that is under the VCN network
+            val physicalWifiNetwork = newWifiNetwork(carrierMergedInfo)
+
             val underlyingCapabilities =
                 mock<NetworkCapabilities>().also {
                     whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
-                    whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo))
+                    whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+                    whenever(it.underlyingNetworks).thenReturn(listOf(physicalWifiNetwork))
                 }
             whenever(connectivityManager.getNetworkCapabilities(underlyingCarrierMergedNetwork))
                 .thenReturn(underlyingCapabilities)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryImplTest.kt
index 0945742..88f262b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryImplTest.kt
@@ -23,6 +23,7 @@
 import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
 import android.net.NetworkCapabilities.TRANSPORT_VPN
 import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.TelephonyNetworkSpecifier
 import android.net.VpnTransportInfo
 import android.net.vcn.VcnTransportInfo
 import android.net.wifi.WifiInfo
@@ -74,6 +75,8 @@
     private val testScope = kosmos.testScope
     private val tunerService = mock<TunerService>()
 
+    private val vcnTransportInfo = VcnTransportInfo.Builder().build()
+
     @Before
     fun setUp() {
         createAndSetRepo()
@@ -343,6 +346,30 @@
             assertThat(latest!!.wifi.isDefault).isTrue()
         }
 
+    private fun newWifiNetwork(wifiInfo: WifiInfo): Network {
+        val network = mock<Network>()
+        val capabilities =
+            mock<NetworkCapabilities>().also {
+                whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
+                whenever(it.transportInfo).thenReturn(wifiInfo)
+            }
+        whenever(connectivityManager.getNetworkCapabilities(network)).thenReturn(capabilities)
+
+        return network
+    }
+
+    private fun newCellNetwork(subId: Int): Network {
+        val network = mock<Network>()
+        val capabilities =
+            mock<NetworkCapabilities>().also {
+                whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
+                whenever(it.networkSpecifier).thenReturn(TelephonyNetworkSpecifier(subId))
+            }
+        whenever(connectivityManager.getNetworkCapabilities(network)).thenReturn(capabilities)
+
+        return network
+    }
+
     @Test
     fun defaultConnections_carrierMergedViaWifiWithVcnTransport_wifiAndCarrierMergedDefault() =
         testScope.runTest {
@@ -350,10 +377,12 @@
 
             val carrierMergedInfo =
                 mock<WifiInfo>().apply { whenever(this.isCarrierMerged).thenReturn(true) }
+            val underlyingWifi = newWifiNetwork(carrierMergedInfo)
             val capabilities =
                 mock<NetworkCapabilities>().also {
                     whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
-                    whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo))
+                    whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+                    whenever(it.underlyingNetworks).thenReturn(listOf(underlyingWifi))
                     whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(false)
                     whenever(it.hasTransport(TRANSPORT_ETHERNET)).thenReturn(false)
                 }
@@ -373,10 +402,12 @@
 
             val carrierMergedInfo =
                 mock<WifiInfo>().apply { whenever(this.isCarrierMerged).thenReturn(true) }
+            val underlyingWifi = newWifiNetwork(carrierMergedInfo)
             val capabilities =
                 mock<NetworkCapabilities>().also {
                     whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
-                    whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo))
+                    whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+                    whenever(it.underlyingNetworks).thenReturn(listOf(underlyingWifi))
                     whenever(it.hasTransport(TRANSPORT_ETHERNET)).thenReturn(false)
                     whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(false)
                 }
@@ -561,10 +592,12 @@
             val underlyingCarrierMergedNetwork = mock<Network>()
             val carrierMergedInfo =
                 mock<WifiInfo>().apply { whenever(this.isCarrierMerged).thenReturn(true) }
+            val underlyingWifi = newWifiNetwork(carrierMergedInfo)
             val underlyingCapabilities =
                 mock<NetworkCapabilities>().also {
                     whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
-                    whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo))
+                    whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+                    whenever(it.underlyingNetworks).thenReturn(listOf(underlyingWifi))
                 }
             whenever(connectivityManager.getNetworkCapabilities(underlyingCarrierMergedNetwork))
                 .thenReturn(underlyingCapabilities)
@@ -645,14 +678,15 @@
     @Test
     fun vcnSubId_tracksVcnTransportInfo() =
         testScope.runTest {
-            val vcnInfo = VcnTransportInfo(SUB_1_ID)
+            val underlyingCell = newCellNetwork(SUB_1_ID)
 
             val latest by collectLastValue(underTest.vcnSubId)
 
             val capabilities =
                 mock<NetworkCapabilities>().also {
                     whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
-                    whenever(it.transportInfo).thenReturn(vcnInfo)
+                    whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+                    whenever(it.underlyingNetworks).thenReturn(listOf(underlyingCell))
                 }
 
             getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
@@ -663,14 +697,15 @@
     @Test
     fun vcnSubId_filersOutInvalid() =
         testScope.runTest {
-            val vcnInfo = VcnTransportInfo(INVALID_SUBSCRIPTION_ID)
+            val underlyingCell = newCellNetwork(INVALID_SUBSCRIPTION_ID)
 
             val latest by collectLastValue(underTest.vcnSubId)
 
             val capabilities =
                 mock<NetworkCapabilities>().also {
                     whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
-                    whenever(it.transportInfo).thenReturn(vcnInfo)
+                    whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+                    whenever(it.underlyingNetworks).thenReturn(listOf(underlyingCell))
                 }
 
             getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
@@ -703,11 +738,12 @@
             val latest by collectLastValue(underTest.vcnSubId)
 
             val wifiInfo = mock<WifiInfo>()
-            val vcnInfo = VcnTransportInfo(wifiInfo)
+            val underlyingWifi = newWifiNetwork(wifiInfo)
             val capabilities =
                 mock<NetworkCapabilities>().also {
                     whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
-                    whenever(it.transportInfo).thenReturn(vcnInfo)
+                    whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+                    whenever(it.underlyingNetworks).thenReturn(listOf(underlyingWifi))
                 }
 
             getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
@@ -721,14 +757,15 @@
             val latest by collectLastValue(underTest.vcnSubId)
 
             val wifiInfo = mock<WifiInfo>()
-            val wifiVcnInfo = VcnTransportInfo(wifiInfo)
-            val sub1VcnInfo = VcnTransportInfo(SUB_1_ID)
-            val sub2VcnInfo = VcnTransportInfo(SUB_2_ID)
+            val underlyingWifi = newWifiNetwork(wifiInfo)
+            val underlyingCell1 = newCellNetwork(SUB_1_ID)
+            val underlyingCell2 = newCellNetwork(SUB_2_ID)
 
             val capabilities =
                 mock<NetworkCapabilities>().also {
                     whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
-                    whenever(it.transportInfo).thenReturn(wifiVcnInfo)
+                    whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+                    whenever(it.underlyingNetworks).thenReturn(listOf(underlyingWifi))
                 }
 
             // WIFI VCN info
@@ -738,14 +775,16 @@
 
             // Cellular VCN info with subId 1
             whenever(capabilities.hasTransport(eq(TRANSPORT_CELLULAR))).thenReturn(true)
-            whenever(capabilities.transportInfo).thenReturn(sub1VcnInfo)
+            whenever(capabilities.transportInfo).thenReturn(vcnTransportInfo)
+            whenever(capabilities.underlyingNetworks).thenReturn(listOf(underlyingCell1))
 
             getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
 
             assertThat(latest).isEqualTo(SUB_1_ID)
 
             // Cellular VCN info with subId 2
-            whenever(capabilities.transportInfo).thenReturn(sub2VcnInfo)
+            whenever(capabilities.transportInfo).thenReturn(vcnTransportInfo)
+            whenever(capabilities.underlyingNetworks).thenReturn(listOf(underlyingCell2))
 
             getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
 
@@ -776,11 +815,12 @@
     @Test
     fun getMainOrUnderlyingWifiInfo_vcnWithWifi_hasInfo() {
         val wifiInfo = mock<WifiInfo>()
-        val vcnInfo = VcnTransportInfo(wifiInfo)
+        val underlyingWifi = newWifiNetwork(wifiInfo)
         val capabilities =
             mock<NetworkCapabilities>().also {
                 whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
-                whenever(it.transportInfo).thenReturn(vcnInfo)
+                whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+                whenever(it.underlyingNetworks).thenReturn(listOf(underlyingWifi))
             }
 
         val result = capabilities.getMainOrUnderlyingWifiInfo(connectivityManager)
@@ -860,11 +900,15 @@
     fun getMainOrUnderlyingWifiInfo_cellular_underlyingVcnWithWifi_hasInfo() {
         val wifiInfo = mock<WifiInfo>()
         val underlyingNetwork = mock<Network>()
-        val underlyingVcnInfo = VcnTransportInfo(wifiInfo)
+
+        // The Wifi network that is under the VCN network
+        val physicalWifiNetwork = newWifiNetwork(wifiInfo)
+
         val underlyingWifiCapabilities =
             mock<NetworkCapabilities>().also {
                 whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
-                whenever(it.transportInfo).thenReturn(underlyingVcnInfo)
+                whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+                whenever(it.underlyingNetworks).thenReturn(listOf(physicalWifiNetwork))
             }
         whenever(connectivityManager.getNetworkCapabilities(underlyingNetwork))
             .thenReturn(underlyingWifiCapabilities)
@@ -887,11 +931,15 @@
     @DisableFlags(FLAG_STATUS_BAR_ALWAYS_CHECK_UNDERLYING_NETWORKS)
     fun getMainOrUnderlyingWifiInfo_notCellular_underlyingVcnWithWifi_noInfo() {
         val underlyingNetwork = mock<Network>()
-        val underlyingVcnInfo = VcnTransportInfo(mock<WifiInfo>())
+
+        // The Wifi network that is under the VCN network
+        val physicalWifiNetwork = newWifiNetwork(mock<WifiInfo>())
+
         val underlyingWifiCapabilities =
             mock<NetworkCapabilities>().also {
                 whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
-                whenever(it.transportInfo).thenReturn(underlyingVcnInfo)
+                whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+                whenever(it.underlyingNetworks).thenReturn(listOf(physicalWifiNetwork))
             }
         whenever(connectivityManager.getNetworkCapabilities(underlyingNetwork))
             .thenReturn(underlyingWifiCapabilities)
@@ -917,10 +965,15 @@
         val underlyingCarrierMergedNetwork = mock<Network>()
         val carrierMergedInfo =
             mock<WifiInfo>().apply { whenever(this.isCarrierMerged).thenReturn(true) }
+
+        // The Wifi network that is under the VCN network
+        val physicalWifiNetwork = newWifiNetwork(carrierMergedInfo)
+
         val underlyingCapabilities =
             mock<NetworkCapabilities>().also {
                 whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
-                whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo))
+                whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+                whenever(it.underlyingNetworks).thenReturn(listOf(physicalWifiNetwork))
             }
         whenever(connectivityManager.getNetworkCapabilities(underlyingCarrierMergedNetwork))
             .thenReturn(underlyingCapabilities)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/window/MultiDisplayStatusBarWindowControllerStoreTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/window/MultiDisplayStatusBarWindowControllerStoreTest.kt
new file mode 100644
index 0000000..faaa4c4
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/window/MultiDisplayStatusBarWindowControllerStoreTest.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.window
+
+import android.platform.test.annotations.EnableFlags
+import android.view.Display
+import android.view.WindowManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager
+import com.android.app.viewcapture.mockViewCaptureAwareWindowManager
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.display.data.repository.displayRepository
+import com.android.systemui.display.data.repository.fakeDisplayWindowPropertiesRepository
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.unconfinedTestDispatcher
+import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+@EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+class MultiDisplayStatusBarWindowControllerStoreTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos().also { it.testDispatcher = it.unconfinedTestDispatcher }
+    private val testScope = kosmos.testScope
+    private val fakeDisplayRepository = kosmos.displayRepository
+
+    private val store =
+        MultiDisplayStatusBarWindowControllerStore(
+            backgroundApplicationScope = kosmos.applicationCoroutineScope,
+            controllerFactory = kosmos.fakeStatusBarWindowControllerFactory,
+            displayWindowPropertiesRepository = kosmos.fakeDisplayWindowPropertiesRepository,
+            viewCaptureAwareWindowManagerFactory =
+                object : ViewCaptureAwareWindowManager.Factory {
+                    override fun create(
+                        windowManager: WindowManager
+                    ): ViewCaptureAwareWindowManager {
+                        return kosmos.mockViewCaptureAwareWindowManager
+                    }
+                },
+            displayRepository = fakeDisplayRepository,
+        )
+
+    @Before
+    fun start() {
+        store.start()
+    }
+
+    @Before
+    fun addDisplays() = runBlocking {
+        fakeDisplayRepository.addDisplay(createDisplay(DEFAULT_DISPLAY_ID))
+        fakeDisplayRepository.addDisplay(createDisplay(NON_DEFAULT_DISPLAY_ID))
+    }
+
+    @Test
+    fun forDisplay_defaultDisplay_multipleCalls_returnsSameInstance() =
+        testScope.runTest {
+            val controller = store.defaultDisplay
+
+            assertThat(store.defaultDisplay).isSameInstanceAs(controller)
+        }
+
+    @Test
+    fun forDisplay_nonDefaultDisplay_multipleCalls_returnsSameInstance() =
+        testScope.runTest {
+            val controller = store.forDisplay(NON_DEFAULT_DISPLAY_ID)
+
+            assertThat(store.forDisplay(NON_DEFAULT_DISPLAY_ID)).isSameInstanceAs(controller)
+        }
+
+    @Test
+    fun forDisplay_nonDefaultDisplay_afterDisplayRemoved_returnsNewInstance() =
+        testScope.runTest {
+            val controller = store.forDisplay(NON_DEFAULT_DISPLAY_ID)
+
+            fakeDisplayRepository.removeDisplay(NON_DEFAULT_DISPLAY_ID)
+            fakeDisplayRepository.addDisplay(createDisplay(NON_DEFAULT_DISPLAY_ID))
+
+            assertThat(store.forDisplay(NON_DEFAULT_DISPLAY_ID)).isNotSameInstanceAs(controller)
+        }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun forDisplay_nonExistingDisplayId_throws() =
+        testScope.runTest { store.forDisplay(NON_EXISTING_DISPLAY_ID) }
+
+    private fun createDisplay(displayId: Int): Display = mock {
+        on { getDisplayId() } doReturn displayId
+    }
+
+    companion object {
+        private const val DEFAULT_DISPLAY_ID = Display.DEFAULT_DISPLAY
+        private const val NON_DEFAULT_DISPLAY_ID = DEFAULT_DISPLAY_ID + 1
+        private const val NON_EXISTING_DISPLAY_ID = DEFAULT_DISPLAY_ID + 2
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/app/viewcapture/ViewCaptureAwareWindowManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/app/viewcapture/ViewCaptureAwareWindowManagerKosmos.kt
new file mode 100644
index 0000000..e1c6699
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/app/viewcapture/ViewCaptureAwareWindowManagerKosmos.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.app.viewcapture
+
+import com.android.systemui.kosmos.Kosmos
+import org.mockito.kotlin.mock
+
+val Kosmos.mockViewCaptureAwareWindowManager by
+    Kosmos.Fixture { mock<ViewCaptureAwareWindowManager>() }
+
+var Kosmos.viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager by
+    Kosmos.Fixture { mockViewCaptureAwareWindowManager }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelKosmos.kt
new file mode 100644
index 0000000..cac4ff3
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelKosmos.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bluetooth.qsdialog
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.audioSharingButtonViewModel: AudioSharingButtonViewModel by
+    Kosmos.Fixture {
+        AudioSharingButtonViewModel(
+            localBluetoothManager,
+            audioSharingInteractor,
+            bluetoothStateInteractor,
+            deviceItemInteractor,
+        )
+    }
+
+val Kosmos.audioSharingButtonViewModelFactory: AudioSharingButtonViewModel.Factory by
+    Kosmos.Fixture {
+        object : AudioSharingButtonViewModel.Factory {
+            override fun create(): AudioSharingButtonViewModel {
+                return audioSharingButtonViewModel
+            }
+        }
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorKosmos.kt
new file mode 100644
index 0000000..8019efc
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorKosmos.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.bluetooth.qsdialog
+
+import com.android.internal.logging.uiEventLogger
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.plugins.activityStarter
+
+val Kosmos.audioSharingDeviceItemActionInteractorImpl: AudioSharingDeviceItemActionInteractorImpl by
+    Kosmos.Fixture {
+        AudioSharingDeviceItemActionInteractorImpl(
+            activityStarter,
+            audioSharingInteractor,
+            dialogTransitionAnimator,
+            localBluetoothManager,
+            testDispatcher,
+            testDispatcher,
+            bluetoothTileDialogLogger,
+            uiEventLogger,
+            audioSharingDialogDelegateFactory,
+            deviceItemActionInteractorImpl,
+        )
+    }
+
+val Kosmos.audioSharingDialogDelegateFactory: AudioSharingDialogDelegate.Factory by
+    Kosmos.Fixture {
+        object : AudioSharingDialogDelegate.Factory {
+            override fun create(
+                cachedBluetoothDevice: CachedBluetoothDevice
+            ): AudioSharingDialogDelegate {
+                return audioSharingDialogDelegate
+            }
+        }
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModelKosmos.kt
new file mode 100644
index 0000000..b8899de8
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModelKosmos.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.bluetooth.qsdialog
+
+import android.content.applicationContext
+import com.android.internal.logging.uiEventLogger
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.phone.systemUIDialogDotFactory
+import kotlinx.coroutines.CoroutineScope
+import org.mockito.kotlin.mock
+
+val Kosmos.cachedBluetoothDevice: CachedBluetoothDevice by Kosmos.Fixture { mock {} }
+
+val Kosmos.audioSharingDialogViewModel: AudioSharingDialogViewModel by
+    Kosmos.Fixture {
+        AudioSharingDialogViewModel(
+            deviceItemInteractor,
+            audioSharingInteractor,
+            applicationContext,
+            localBluetoothManager,
+            cachedBluetoothDevice,
+            testScope.backgroundScope,
+            testDispatcher
+        )
+    }
+
+val Kosmos.audioSharingDialogViewModelFactory: AudioSharingDialogViewModel.Factory by
+    Kosmos.Fixture {
+        object : AudioSharingDialogViewModel.Factory {
+            override fun create(
+                cachedBluetoothDevice: CachedBluetoothDevice,
+                coroutineScope: CoroutineScope
+            ): AudioSharingDialogViewModel {
+                return audioSharingDialogViewModel
+            }
+        }
+    }
+
+val Kosmos.audioSharingDialogDelegate: AudioSharingDialogDelegate by
+    Kosmos.Fixture {
+        AudioSharingDialogDelegate(
+            cachedBluetoothDevice,
+            testScope.backgroundScope,
+            audioSharingDialogViewModelFactory,
+            systemUIDialogDotFactory,
+            uiEventLogger
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorKosmos.kt
new file mode 100644
index 0000000..4f4d1da
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorKosmos.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bluetooth.qsdialog
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+
+val Kosmos.audioSharingInteractor: AudioSharingInteractor by
+    Kosmos.Fixture {
+        AudioSharingInteractorImpl(
+            localBluetoothManager,
+            bluetoothTileDialogAudioSharingRepository,
+            testDispatcher,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryKosmos.kt
new file mode 100644
index 0000000..d15d0e5
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryKosmos.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.bluetooth.qsdialog
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.bluetoothTileDialogAudioSharingRepository by
+    Kosmos.Fixture { FakeAudioSharingRepository() }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDeviceMetadataInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/BluetoothDeviceMetadataInteractorKosmos.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDeviceMetadataInteractorKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/BluetoothDeviceMetadataInteractorKosmos.kt
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractorKosmos.kt
new file mode 100644
index 0000000..aaa918c
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractorKosmos.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.bluetooth.qsdialog
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+
+val Kosmos.bluetoothStateInteractor: BluetoothStateInteractor by
+    Kosmos.Fixture {
+        BluetoothStateInteractor(
+            localBluetoothManager,
+            bluetoothTileDialogLogger,
+            testScope.backgroundScope,
+            testDispatcher
+        )
+    }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt
similarity index 79%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt
index 5ff4634..b5b2f5e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt
@@ -20,8 +20,7 @@
 import com.android.systemui.animation.DialogTransitionAnimator
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testDispatcher
-import com.android.systemui.plugins.activityStarter
-import com.android.systemui.util.mockito.mock
+import org.mockito.kotlin.mock
 
 val Kosmos.bluetoothTileDialogLogger: BluetoothTileDialogLogger by Kosmos.Fixture { mock {} }
 
@@ -29,14 +28,10 @@
 
 val Kosmos.dialogTransitionAnimator: DialogTransitionAnimator by Kosmos.Fixture { mock {} }
 
-val Kosmos.deviceItemActionInteractor: DeviceItemActionInteractor by
+val Kosmos.deviceItemActionInteractorImpl: DeviceItemActionInteractorImpl by
     Kosmos.Fixture {
-        DeviceItemActionInteractor(
-            activityStarter,
-            dialogTransitionAnimator,
-            localBluetoothManager,
+        DeviceItemActionInteractorImpl(
             testDispatcher,
-            bluetoothTileDialogLogger,
             uiEventLogger,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/FakeAudioSharingRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/FakeAudioSharingRepository.kt
new file mode 100644
index 0000000..a839f17
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/FakeAudioSharingRepository.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bluetooth.qsdialog
+
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+class FakeAudioSharingRepository : AudioSharingRepository {
+    private var mutableAvailable: Boolean = false
+
+    private val mutableInAudioSharing: MutableStateFlow<Boolean> = MutableStateFlow(false)
+
+    private val mutableAudioSourceStateUpdate = MutableSharedFlow<Unit>()
+
+    var sourceAdded: Boolean = false
+        private set
+
+    private var profile: LocalBluetoothLeBroadcast? = null
+
+    override val leAudioBroadcastProfile: LocalBluetoothLeBroadcast?
+        get() = profile
+
+    override val audioSourceStateUpdate: Flow<Unit> = mutableAudioSourceStateUpdate
+
+    override val inAudioSharing: StateFlow<Boolean> = mutableInAudioSharing
+
+    override suspend fun audioSharingAvailable(): Boolean = mutableAvailable
+
+    override suspend fun addSource() {
+        sourceAdded = true
+    }
+
+    override suspend fun setActive(cachedBluetoothDevice: CachedBluetoothDevice) {}
+
+    override suspend fun startAudioSharing() {}
+
+    fun setAudioSharingAvailable(available: Boolean) {
+        mutableAvailable = available
+    }
+
+    fun setInAudioSharing(state: Boolean) {
+        mutableInAudioSharing.value = state
+    }
+
+    fun setLeAudioBroadcastProfile(leAudioBroadcastProfile: LocalBluetoothLeBroadcast?) {
+        profile = leAudioBroadcastProfile
+    }
+
+    fun emitAudioSourceStateUpdate() {
+        mutableAudioSourceStateUpdate.tryEmit(Unit)
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DisplayScopeRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DisplayScopeRepositoryKosmos.kt
new file mode 100644
index 0000000..ff4ba61
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DisplayScopeRepositoryKosmos.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.display.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+
+val Kosmos.fakeDisplayScopeRepository by
+    Kosmos.Fixture { FakeDisplayScopeRepository(testDispatcher) }
+
+var Kosmos.displayScopeRepository: DisplayScopeRepository by
+    Kosmos.Fixture { fakeDisplayScopeRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepositoryKosmos.kt
new file mode 100644
index 0000000..65b18c1
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepositoryKosmos.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.display.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.fakeDisplayWindowPropertiesRepository by
+    Kosmos.Fixture { FakeDisplayWindowPropertiesRepository() }
+
+var Kosmos.displayWindowPropertiesRepository: DisplayWindowPropertiesRepository by
+    Kosmos.Fixture { fakeDisplayWindowPropertiesRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayScopeRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayScopeRepository.kt
new file mode 100644
index 0000000..3c25924
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayScopeRepository.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.display.data.repository
+
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+
+class FakeDisplayScopeRepository(private val dispatcher: CoroutineDispatcher) :
+    DisplayScopeRepository {
+
+    private val perDisplayScopes = mutableMapOf<Int, CoroutineScope>()
+
+    override fun scopeForDisplay(displayId: Int): CoroutineScope {
+        return perDisplayScopes.computeIfAbsent(displayId) { CoroutineScope(dispatcher) }
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayWindowPropertiesRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayWindowPropertiesRepository.kt
new file mode 100644
index 0000000..9282f27
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayWindowPropertiesRepository.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.display.data.repository
+
+import com.android.systemui.display.shared.model.DisplayWindowProperties
+import com.google.common.collect.HashBasedTable
+import org.mockito.kotlin.mock
+
+class FakeDisplayWindowPropertiesRepository : DisplayWindowPropertiesRepository {
+
+    private val properties = HashBasedTable.create<Int, Int, DisplayWindowProperties>()
+
+    override fun get(displayId: Int, windowType: Int): DisplayWindowProperties {
+        return properties.get(displayId, windowType)
+            ?: DisplayWindowProperties(
+                    displayId = displayId,
+                    windowType = windowType,
+                    context = mock(),
+                    windowManager = mock(),
+                )
+                .also { properties.put(displayId, windowType, it) }
+    }
+}
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 f97f303..522c387 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
@@ -23,6 +23,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.bouncer.data.repository.bouncerRepository
 import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
+import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
 import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor
 import com.android.systemui.classifier.falsingCollector
 import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
@@ -152,6 +153,7 @@
     val wifiInteractor by lazy { kosmos.wifiInteractor }
     val fakeWifiRepository by lazy { kosmos.fakeWifiRepository }
     val volumeDialogInteractor by lazy { kosmos.volumeDialogInteractor }
+    val alternateBouncerInteractor by lazy { kosmos.alternateBouncerInteractor }
 
     val ongoingActivityChipsViewModel by lazy { kosmos.ongoingActivityChipsViewModel }
     val scrimController by lazy { kosmos.scrimController }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt
index cfc31c7..10b073e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.plugins.statusbar
 
 import com.android.internal.logging.uiEventLogger
+import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
 import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
 import com.android.systemui.jank.interactionJankMonitor
 import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor
@@ -45,5 +46,6 @@
             { sceneContainerOcclusionInteractor },
             { keyguardClockInteractor },
             { sceneBackInteractor },
+            { alternateBouncerInteractor },
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerFactory.kt
new file mode 100644
index 0000000..10f328b
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerFactory.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.window
+
+import android.content.Context
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager
+
+class FakeStatusBarWindowControllerFactory : StatusBarWindowController.Factory {
+    override fun create(
+        context: Context,
+        viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager,
+    ) = FakeStatusBarWindowController()
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerStore.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerStore.kt
new file mode 100644
index 0000000..d19e322
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerStore.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.statusbar.window
+
+import android.view.Display
+
+class FakeStatusBarWindowControllerStore : StatusBarWindowControllerStore {
+
+    private val perDisplayControllers = mutableMapOf<Int, FakeStatusBarWindowController>()
+
+    override val defaultDisplay
+        get() = forDisplay(Display.DEFAULT_DISPLAY)
+
+    override fun forDisplay(displayId: Int): StatusBarWindowController {
+        return perDisplayControllers.computeIfAbsent(displayId) { FakeStatusBarWindowController() }
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerKosmos.kt
index c198b35..6c6f243 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerKosmos.kt
@@ -21,3 +21,15 @@
 val Kosmos.fakeStatusBarWindowController by Kosmos.Fixture { FakeStatusBarWindowController() }
 
 var Kosmos.statusBarWindowController by Kosmos.Fixture { fakeStatusBarWindowController }
+
+val Kosmos.fakeStatusBarWindowControllerStore by
+    Kosmos.Fixture { FakeStatusBarWindowControllerStore() }
+
+var Kosmos.statusBarWindowControllerStore: StatusBarWindowControllerStore by
+    Kosmos.Fixture { fakeStatusBarWindowControllerStore }
+
+val Kosmos.fakeStatusBarWindowControllerFactory by
+    Kosmos.Fixture { FakeStatusBarWindowControllerFactory() }
+
+var Kosmos.statusBarWindowControllerFactory: StatusBarWindowController.Factory by
+    Kosmos.Fixture { fakeStatusBarWindowControllerFactory }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioSharingRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioSharingRepository.kt
index a4719e5..5da6ee9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioSharingRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioSharingRepository.kt
@@ -22,6 +22,7 @@
 import kotlinx.coroutines.flow.StateFlow
 
 class FakeAudioSharingRepository : AudioSharingRepository {
+    private var mutableAvailable: Boolean = false
     private val mutableInAudioSharing: MutableStateFlow<Boolean> = MutableStateFlow(false)
     private val mutablePrimaryGroupId: MutableStateFlow<Int> =
         MutableStateFlow(TEST_GROUP_ID_INVALID)
@@ -34,8 +35,14 @@
     override val secondaryGroupId: StateFlow<Int> = mutableSecondaryGroupId
     override val volumeMap: StateFlow<GroupIdToVolumes> = mutableVolumeMap
 
+    override suspend fun audioSharingAvailable(): Boolean = mutableAvailable
+
     override suspend fun setSecondaryVolume(volume: Int) {}
 
+    fun setAudioSharingAvailable(available: Boolean) {
+        mutableAvailable = available
+    }
+
     fun setInAudioSharing(state: Boolean) {
         mutableInAudioSharing.value = state
     }
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index 9629a87..8896d77 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -12,11 +12,19 @@
 }
 
 filegroup {
+    name: "ravenwood-common-policies",
+    srcs: [
+        "texts/ravenwood-common-policies.txt",
+    ],
+    visibility: ["//visibility:private"],
+}
+
+filegroup {
     name: "ravenwood-services-policies",
     srcs: [
         "texts/ravenwood-services-policies.txt",
     ],
-    visibility: ["//visibility:public"],
+    visibility: ["//visibility:private"],
 }
 
 filegroup {
@@ -24,7 +32,7 @@
     srcs: [
         "texts/ravenwood-framework-policies.txt",
     ],
-    visibility: ["//visibility:public"],
+    visibility: ["//visibility:private"],
 }
 
 filegroup {
@@ -32,7 +40,7 @@
     srcs: [
         "texts/ravenwood-standard-options.txt",
     ],
-    visibility: ["//visibility:public"],
+    visibility: ["//visibility:private"],
 }
 
 filegroup {
@@ -40,7 +48,7 @@
     srcs: [
         "texts/ravenwood-annotation-allowed-classes.txt",
     ],
-    visibility: ["//visibility:public"],
+    visibility: ["//visibility:private"],
 }
 
 // This and the next module contain the same classes with different implementations.
@@ -337,6 +345,30 @@
     ],
 }
 
+// JARs in "ravenwood-runtime" are set to the classpath, sorted alphabetically.
+// Rename some of the dependencies to make sure they're included in the intended order.
+
+java_library {
+    name: "100-framework-minus-apex.ravenwood",
+    installable: false,
+    static_libs: ["framework-minus-apex.ravenwood"],
+    visibility: ["//visibility:private"],
+}
+
+java_library {
+    name: "200-kxml2-android",
+    installable: false,
+    static_libs: ["kxml2-android"],
+    visibility: ["//visibility:private"],
+}
+
+java_library {
+    name: "z00-all-updatable-modules-system-stubs",
+    installable: false,
+    static_libs: ["all-updatable-modules-system-stubs-for-host"],
+    visibility: ["//visibility:private"],
+}
+
 android_ravenwood_libgroup {
     name: "ravenwood-runtime",
     data: [
@@ -395,3 +427,7 @@
         "inline-mockito-ravenwood-prebuilt",
     ],
 }
+
+build = [
+    "Framework.bp",
+]
diff --git a/ravenwood/Framework.bp b/ravenwood/Framework.bp
new file mode 100644
index 0000000..5cb1479
--- /dev/null
+++ b/ravenwood/Framework.bp
@@ -0,0 +1,292 @@
+// 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.
+
+// This file hosts all the genrule and module definitions for all Android specific
+// code that needs further post-processing by hoststubgen to support Ravenwood.
+
+/////////////////////////
+// framework-minus-apex
+/////////////////////////
+
+// Process framework-minus-apex with hoststubgen for Ravenwood.
+// This step takes several tens of seconds, so we manually shard it to multiple modules.
+// All the copies have to be kept in sync.
+// TODO: Do the sharding better, either by making hostsubgen support sharding natively, or
+// making a better build rule.
+
+genrule_defaults {
+    name: "framework-minus-apex.ravenwood-base_defaults",
+    tools: ["hoststubgen"],
+    srcs: [
+        ":framework-minus-apex-for-host",
+        ":ravenwood-common-policies",
+        ":ravenwood-framework-policies",
+        ":ravenwood-standard-options",
+        ":ravenwood-annotation-allowed-classes",
+    ],
+    out: [
+        "ravenwood.jar",
+        "hoststubgen_framework-minus-apex.log",
+    ],
+    visibility: ["//visibility:private"],
+}
+
+framework_minus_apex_cmd = "$(location hoststubgen) " +
+    "@$(location :ravenwood-standard-options) " +
+    "--debug-log $(location hoststubgen_framework-minus-apex.log) " +
+    "--out-jar $(location ravenwood.jar) " +
+    "--in-jar $(location :framework-minus-apex-for-host) " +
+    "--policy-override-file $(location :ravenwood-common-policies) " +
+    "--policy-override-file $(location :ravenwood-framework-policies) " +
+    "--annotation-allowed-classes-file $(location :ravenwood-annotation-allowed-classes) "
+
+java_genrule {
+    name: "framework-minus-apex.ravenwood-base_X0",
+    defaults: ["framework-minus-apex.ravenwood-base_defaults"],
+    cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 0",
+}
+
+java_genrule {
+    name: "framework-minus-apex.ravenwood-base_X1",
+    defaults: ["framework-minus-apex.ravenwood-base_defaults"],
+    cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 1",
+}
+
+java_genrule {
+    name: "framework-minus-apex.ravenwood-base_X2",
+    defaults: ["framework-minus-apex.ravenwood-base_defaults"],
+    cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 2",
+}
+
+java_genrule {
+    name: "framework-minus-apex.ravenwood-base_X3",
+    defaults: ["framework-minus-apex.ravenwood-base_defaults"],
+    cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 3",
+}
+
+java_genrule {
+    name: "framework-minus-apex.ravenwood-base_X4",
+    defaults: ["framework-minus-apex.ravenwood-base_defaults"],
+    cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 4",
+}
+
+java_genrule {
+    name: "framework-minus-apex.ravenwood-base_X5",
+    defaults: ["framework-minus-apex.ravenwood-base_defaults"],
+    cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 5",
+}
+
+java_genrule {
+    name: "framework-minus-apex.ravenwood-base_X6",
+    defaults: ["framework-minus-apex.ravenwood-base_defaults"],
+    cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 6",
+}
+
+java_genrule {
+    name: "framework-minus-apex.ravenwood-base_X7",
+    defaults: ["framework-minus-apex.ravenwood-base_defaults"],
+    cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 7",
+}
+
+java_genrule {
+    name: "framework-minus-apex.ravenwood-base_X8",
+    defaults: ["framework-minus-apex.ravenwood-base_defaults"],
+    cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 8",
+}
+
+java_genrule {
+    name: "framework-minus-apex.ravenwood-base_X9",
+    defaults: ["framework-minus-apex.ravenwood-base_defaults"],
+    cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 9",
+}
+
+// Build framework-minus-apex.ravenwood-base without sharding.
+// We extract the various dump files from this one, rather than the sharded ones, because
+// some dumps use the output from other classes (e.g. base classes) which may not be in the
+// same shard. Also some of the dump files ("apis") may be slow even when sharded, because
+// the output contains the information from all the input classes, rather than the output classes.
+// Not using sharding is fine for this module because it's only used for collecting the
+// dump / stats files, which don't have to happen regularly.
+java_genrule {
+    name: "framework-minus-apex.ravenwood-base_all",
+    defaults: ["framework-minus-apex.ravenwood-base_defaults"],
+    cmd: framework_minus_apex_cmd +
+        "--stats-file $(location hoststubgen_framework-minus-apex_stats.csv) " +
+        "--supported-api-list-file $(location hoststubgen_framework-minus-apex_apis.csv) " +
+
+        "--gen-keep-all-file $(location hoststubgen_framework-minus-apex_keep_all.txt) " +
+        "--gen-input-dump-file $(location hoststubgen_framework-minus-apex_dump.txt) ",
+
+    out: [
+        "hoststubgen_framework-minus-apex_keep_all.txt",
+        "hoststubgen_framework-minus-apex_dump.txt",
+        "hoststubgen_framework-minus-apex_stats.csv",
+        "hoststubgen_framework-minus-apex_apis.csv",
+    ],
+}
+
+// Marge all the sharded jars
+java_genrule {
+    name: "framework-minus-apex.ravenwood",
+    defaults: ["ravenwood-internal-only-visibility-java"],
+    cmd: "$(location merge_zips) $(out) $(in)",
+    tools: ["merge_zips"],
+    srcs: [
+        ":framework-minus-apex.ravenwood-base_X0{ravenwood.jar}",
+        ":framework-minus-apex.ravenwood-base_X1{ravenwood.jar}",
+        ":framework-minus-apex.ravenwood-base_X2{ravenwood.jar}",
+        ":framework-minus-apex.ravenwood-base_X3{ravenwood.jar}",
+        ":framework-minus-apex.ravenwood-base_X4{ravenwood.jar}",
+        ":framework-minus-apex.ravenwood-base_X5{ravenwood.jar}",
+        ":framework-minus-apex.ravenwood-base_X6{ravenwood.jar}",
+        ":framework-minus-apex.ravenwood-base_X7{ravenwood.jar}",
+        ":framework-minus-apex.ravenwood-base_X8{ravenwood.jar}",
+        ":framework-minus-apex.ravenwood-base_X9{ravenwood.jar}",
+    ],
+    out: [
+        "framework-minus-apex.ravenwood.jar",
+    ],
+}
+
+//////////////////
+// services.core
+//////////////////
+
+java_library {
+    name: "services.core-for-host",
+    installable: false, // host only jar.
+    static_libs: [
+        "services.core",
+    ],
+    sdk_version: "core_platform",
+    visibility: ["//visibility:private"],
+}
+
+java_genrule {
+    name: "services.core.ravenwood-base",
+    tools: ["hoststubgen"],
+    cmd: "$(location hoststubgen) " +
+        "@$(location :ravenwood-standard-options) " +
+
+        "--debug-log $(location hoststubgen_services.core.log) " +
+        "--stats-file $(location hoststubgen_services.core_stats.csv) " +
+        "--supported-api-list-file $(location hoststubgen_services.core_apis.csv) " +
+        "--gen-keep-all-file $(location hoststubgen_services.core_keep_all.txt) " +
+        "--gen-input-dump-file $(location hoststubgen_services.core_dump.txt) " +
+
+        "--out-jar $(location ravenwood.jar) " +
+        "--in-jar $(location :services.core-for-host) " +
+
+        "--policy-override-file $(location :ravenwood-common-policies) " +
+        "--policy-override-file $(location :ravenwood-services-policies) " +
+        "--annotation-allowed-classes-file $(location :ravenwood-annotation-allowed-classes) ",
+    srcs: [
+        ":services.core-for-host",
+        ":ravenwood-common-policies",
+        ":ravenwood-services-policies",
+        ":ravenwood-standard-options",
+        ":ravenwood-annotation-allowed-classes",
+    ],
+    out: [
+        "ravenwood.jar",
+
+        // Following files are created just as FYI.
+        "hoststubgen_services.core_keep_all.txt",
+        "hoststubgen_services.core_dump.txt",
+
+        "hoststubgen_services.core.log",
+        "hoststubgen_services.core_stats.csv",
+        "hoststubgen_services.core_apis.csv",
+    ],
+    visibility: ["//visibility:private"],
+}
+
+java_genrule {
+    name: "services.core.ravenwood",
+    defaults: ["ravenwood-internal-only-visibility-genrule"],
+    cmd: "cp $(in) $(out)",
+    srcs: [
+        ":services.core.ravenwood-base{ravenwood.jar}",
+    ],
+    out: [
+        "services.core.ravenwood.jar",
+    ],
+}
+
+// TODO(b/313930116) This jarjar is a bit slow. We should use hoststubgen for renaming,
+// but services.core.ravenwood has complex dependencies, so it'll take more than
+// just using hoststubgen "rename"s.
+java_library {
+    name: "services.core.ravenwood-jarjar",
+    defaults: ["ravenwood-internal-only-visibility-java"],
+    installable: false,
+    static_libs: [
+        "services.core.ravenwood",
+    ],
+    jarjar_rules: ":ravenwood-services-jarjar-rules",
+}
+
+///////////////
+// core-icu4j
+///////////////
+
+java_genrule {
+    name: "core-icu4j-for-host.ravenwood-base",
+    tools: ["hoststubgen"],
+    cmd: "$(location hoststubgen) " +
+        "@$(location :ravenwood-standard-options) " +
+
+        "--debug-log $(location hoststubgen_core-icu4j-for-host.log) " +
+        "--stats-file $(location hoststubgen_core-icu4j-for-host_stats.csv) " +
+        "--supported-api-list-file $(location hoststubgen_core-icu4j-for-host_apis.csv) " +
+        "--gen-keep-all-file $(location hoststubgen_core-icu4j-for-host_keep_all.txt) " +
+        "--gen-input-dump-file $(location hoststubgen_core-icu4j-for-host_dump.txt) " +
+
+        "--out-jar $(location ravenwood.jar) " +
+        "--in-jar $(location :core-icu4j-for-host) " +
+
+        "--policy-override-file $(location :ravenwood-common-policies) " +
+        "--policy-override-file $(location :icu-ravenwood-policies) ",
+    srcs: [
+        ":core-icu4j-for-host",
+
+        ":ravenwood-common-policies",
+        ":icu-ravenwood-policies",
+        ":ravenwood-standard-options",
+    ],
+    out: [
+        "ravenwood.jar",
+
+        // Following files are created just as FYI.
+        "hoststubgen_core-icu4j-for-host_keep_all.txt",
+        "hoststubgen_core-icu4j-for-host_dump.txt",
+
+        "hoststubgen_core-icu4j-for-host.log",
+        "hoststubgen_core-icu4j-for-host_stats.csv",
+        "hoststubgen_core-icu4j-for-host_apis.csv",
+    ],
+    visibility: ["//visibility:private"],
+}
+
+java_genrule {
+    name: "core-icu4j-for-host.ravenwood",
+    defaults: ["ravenwood-internal-only-visibility-genrule"],
+    cmd: "cp $(in) $(out)",
+    srcs: [
+        ":core-icu4j-for-host.ravenwood-base{ravenwood.jar}",
+    ],
+    out: [
+        "core-icu4j-for-host.ravenwood.jar",
+    ],
+}
diff --git a/ravenwood/texts/ravenwood-common-policies.txt b/ravenwood/texts/ravenwood-common-policies.txt
new file mode 100644
index 0000000..08f53977
--- /dev/null
+++ b/ravenwood/texts/ravenwood-common-policies.txt
@@ -0,0 +1,20 @@
+# Ravenwood "policy" that should apply to all code.
+
+# Keep all AIDL interfaces
+class :aidl keepclass
+
+# Keep all feature flag implementations
+class :feature_flags keepclass
+
+# Keep all sysprops generated code implementations
+class :sysprops keepclass
+
+# Keep all resource R classes
+class :r keepclass
+
+# Support APIs not available in standard JRE
+class java.io.FileDescriptor keep
+    method getInt$ ()I @com.android.ravenwood.RavenwoodJdkPatch.getInt$
+    method setInt$ (I)V @com.android.ravenwood.RavenwoodJdkPatch.setInt$
+class java.util.LinkedHashMap keep
+    method eldest ()Ljava/util/Map$Entry; @com.android.ravenwood.RavenwoodJdkPatch.eldest
diff --git a/ravenwood/texts/ravenwood-framework-policies.txt b/ravenwood/texts/ravenwood-framework-policies.txt
index d962c82..3649f0e 100644
--- a/ravenwood/texts/ravenwood-framework-policies.txt
+++ b/ravenwood/texts/ravenwood-framework-policies.txt
@@ -1,29 +1,10 @@
 # Ravenwood "policy" file for framework-minus-apex.
 
-# Keep all AIDL interfaces
-class :aidl keepclass
-
-# Keep all feature flag implementations
-class :feature_flags keepclass
-
-# Keep all sysprops generated code implementations
-class :sysprops keepclass
-
-# Keep all resource R classes
-class :r keepclass
-
 # To avoid VerifyError on nano proto files (b/324063814), we rename nano proto classes.
 # Note: The "rename" directive must use slashes (/) as a package name separator.
 rename com/.*/nano/   devicenano/
 rename android/.*/nano/   devicenano/
 
-# Support APIs not available in standard JRE
-class java.io.FileDescriptor keep
-    method getInt$ ()I @com.android.ravenwood.RavenwoodJdkPatch.getInt$
-    method setInt$ (I)V @com.android.ravenwood.RavenwoodJdkPatch.setInt$
-class java.util.LinkedHashMap keep
-    method eldest ()Ljava/util/Map$Entry; @com.android.ravenwood.RavenwoodJdkPatch.eldest
-
 # Exported to Mainline modules; cannot use annotations
 class com.android.internal.util.FastXmlSerializer keepclass
 class com.android.internal.util.FileRotator keepclass
diff --git a/ravenwood/texts/ravenwood-services-policies.txt b/ravenwood/texts/ravenwood-services-policies.txt
index 5cdb4f7..cc2fa60 100644
--- a/ravenwood/texts/ravenwood-services-policies.txt
+++ b/ravenwood/texts/ravenwood-services-policies.txt
@@ -1,7 +1 @@
 # Ravenwood "policy" file for services.core.
-
-# Keep all AIDL interfaces
-class :aidl keepclass
-
-# Keep all feature flag implementations
-class :feature_flags keepclass
diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
index c5fef19..5d57408 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
@@ -65,8 +65,6 @@
 import com.android.internal.infra.AndroidFuture;
 import com.android.internal.util.DumpUtils;
 import com.android.server.SystemService.TargetUser;
-import com.android.server.appfunctions.RemoteServiceCaller.RunServiceCallCallback;
-import com.android.server.appfunctions.RemoteServiceCaller.ServiceUsageCompleteListener;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -233,6 +231,9 @@
                                                 "Caller does not have permission to execute the"
                                                         + " appfunction",
                                                 /* extras= */ null));
+                                throw new SecurityException(
+                                        "Caller does not have permission to execute the"
+                                                + " appfunction");
                             }
                         })
                 .thenCompose(
@@ -380,7 +381,8 @@
                                     runtimeMetadataSearchSession));
             AppFunctionRuntimeMetadata newMetadata =
                     new AppFunctionRuntimeMetadata.Builder(existingMetadata)
-                            .setEnabled(enabledState).build();
+                            .setEnabled(enabledState)
+                            .build();
             AppSearchBatchResult<String, Void> putDocumentBatchResult =
                     runtimeMetadataSearchSession
                             .put(
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 3bcca1c..3d67ed4 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -16,10 +16,13 @@
 
 package com.android.server.companion.virtual;
 
+import static android.Manifest.permission.ADD_ALWAYS_UNLOCKED_DISPLAY;
+import static android.Manifest.permission.ADD_TRUSTED_DISPLAY;
 import static android.app.admin.DevicePolicyManager.NEARBY_STREAMING_ENABLED;
 import static android.app.admin.DevicePolicyManager.NEARBY_STREAMING_NOT_CONTROLLED_BY_POLICY;
 import static android.app.admin.DevicePolicyManager.NEARBY_STREAMING_SAME_MANAGED_ACCOUNT_ONLY;
 import static android.companion.virtual.VirtualDeviceParams.ACTIVITY_POLICY_DEFAULT_ALLOWED;
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM;
 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;
@@ -30,7 +33,6 @@
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_RECENTS;
 import static android.companion.virtualdevice.flags.Flags.virtualCameraServiceDiscovery;
 
-import android.annotation.EnforcePermission;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
@@ -95,7 +97,6 @@
 import android.os.IBinder;
 import android.os.LocaleList;
 import android.os.Looper;
-import android.os.PermissionEnforcer;
 import android.os.PowerManager;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
@@ -402,7 +403,6 @@
             VirtualDeviceParams params,
             DisplayManagerGlobal displayManager,
             VirtualCameraController virtualCameraController) {
-        super(PermissionEnforcer.fromContext(context));
         mVirtualDeviceLog = virtualDeviceLog;
         mOwnerPackageName = attributionSource.getPackageName();
         mAttributionSource = attributionSource;
@@ -425,6 +425,27 @@
         mDisplayManager = displayManager;
         mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class);
         mPowerManager = context.getSystemService(PowerManager.class);
+
+        if (mDevicePolicies.get(POLICY_TYPE_CLIPBOARD, DEVICE_POLICY_DEFAULT)
+                != DEVICE_POLICY_DEFAULT) {
+            if (mContext.checkCallingOrSelfPermission(ADD_TRUSTED_DISPLAY)
+                    != PackageManager.PERMISSION_GRANTED) {
+                throw new SecurityException("Requires ADD_TRUSTED_DISPLAY permission to "
+                        + "set a custom clipboard policy.");
+            }
+        }
+
+        int flags = DEFAULT_VIRTUAL_DISPLAY_FLAGS;
+        if (mParams.getLockState() == VirtualDeviceParams.LOCK_STATE_ALWAYS_UNLOCKED) {
+            if (mContext.checkCallingOrSelfPermission(ADD_ALWAYS_UNLOCKED_DISPLAY)
+                    != PackageManager.PERMISSION_GRANTED) {
+                throw new SecurityException("Requires ADD_ALWAYS_UNLOCKED_DISPLAY permission to "
+                        + "create an always unlocked virtual device.");
+            }
+            flags |= DisplayManager.VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED;
+        }
+        mBaseVirtualDisplayFlags = flags;
+
         if (inputController == null) {
             mInputController = new InputController(
                     context.getMainThreadHandler(),
@@ -467,12 +488,6 @@
                             : mParams.getAllowedActivities();
         }
 
-        int flags = DEFAULT_VIRTUAL_DISPLAY_FLAGS;
-        if (mParams.getLockState() == VirtualDeviceParams.LOCK_STATE_ALWAYS_UNLOCKED) {
-            flags |= DisplayManager.VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED;
-        }
-        mBaseVirtualDisplayFlags = flags;
-
         if (Flags.vdmCustomIme() && mParams.getInputMethodComponent() != null) {
             final String imeId = mParams.getInputMethodComponent().flattenToShortString();
             Slog.d(TAG, "Setting custom input method " + imeId + " as default for virtual device "
@@ -547,10 +562,8 @@
      * object is created before the returned VirtualDeviceInternal one.
      */
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void setListeners(@NonNull IVirtualDeviceActivityListener activityListener,
             @NonNull IVirtualDeviceSoundEffectListener soundEffectListener) {
-        super.setListeners_enforcePermission();
         mActivityListener = Objects.requireNonNull(activityListener);
         mSoundEffectListener = Objects.requireNonNull(soundEffectListener);
     }
@@ -597,9 +610,8 @@
     }
 
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void goToSleep() {
-        super.goToSleep_enforcePermission();
+        checkCallerIsDeviceOwner();
         synchronized (mVirtualDeviceLock) {
             mRequestedToBeAwake = false;
         }
@@ -612,9 +624,8 @@
     }
 
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void wakeUp() {
-        super.wakeUp_enforcePermission();
+        checkCallerIsDeviceOwner();
         synchronized (mVirtualDeviceLock) {
             mRequestedToBeAwake = true;
             if (mLockdownActive) {
@@ -632,16 +643,12 @@
     }
 
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void launchPendingIntent(int displayId, PendingIntent pendingIntent,
             ResultReceiver resultReceiver) {
-        super.launchPendingIntent_enforcePermission();
+        checkCallerIsDeviceOwner();
         Objects.requireNonNull(pendingIntent);
         synchronized (mVirtualDeviceLock) {
-            if (!mVirtualDisplays.contains(displayId)) {
-                throw new SecurityException("Display ID " + displayId
-                        + " not found for this virtual device");
-            }
+            checkDisplayOwnedByVirtualDeviceLocked(displayId);
         }
         if (pendingIntent.isActivity()) {
             try {
@@ -673,9 +680,8 @@
     }
 
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void addActivityPolicyExemption(@NonNull ActivityPolicyExemption exemption) {
-        super.addActivityPolicyExemption_enforcePermission();
+        checkCallerIsDeviceOwner();
         final int displayId = exemption.getDisplayId();
         if (exemption.getComponentName() == null || displayId != Display.INVALID_DISPLAY) {
             if (!android.companion.virtualdevice.flags.Flags.activityControlApi()) {
@@ -711,9 +717,8 @@
     }
 
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void removeActivityPolicyExemption(@NonNull ActivityPolicyExemption exemption) {
-        super.removeActivityPolicyExemption_enforcePermission();
+        checkCallerIsDeviceOwner();
         final int displayId = exemption.getDisplayId();
         if (exemption.getComponentName() == null || displayId != Display.INVALID_DISPLAY) {
             if (!android.companion.virtualdevice.flags.Flags.activityControlApi()) {
@@ -764,9 +769,7 @@
     }
 
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void close() {
-        super.close_enforcePermission();
         // Remove about-to-be-closed virtual device from the service before butchering it.
         if (!mService.removeVirtualDevice(mDeviceId)) {
             // Device is already closed.
@@ -841,11 +844,10 @@
     }
 
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void onAudioSessionStarting(int displayId,
             @NonNull IAudioRoutingCallback routingCallback,
             @Nullable IAudioConfigChangedCallback configChangedCallback) {
-        super.onAudioSessionStarting_enforcePermission();
+        checkCallerIsDeviceOwner();
         synchronized (mVirtualDeviceLock) {
             checkDisplayOwnedByVirtualDeviceLocked(displayId);
             if (mVirtualAudioController == null) {
@@ -859,9 +861,8 @@
     }
 
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void onAudioSessionEnded() {
-        super.onAudioSessionEnded_enforcePermission();
+        checkCallerIsDeviceOwner();
         synchronized (mVirtualDeviceLock) {
             if (mVirtualAudioController != null) {
                 mVirtualAudioController.stopListening();
@@ -871,10 +872,9 @@
     }
 
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void setDevicePolicy(@VirtualDeviceParams.DynamicPolicyType int policyType,
             @VirtualDeviceParams.DevicePolicy int devicePolicy) {
-        super.setDevicePolicy_enforcePermission();
+        checkCallerIsDeviceOwner();
         if (!Flags.dynamicPolicy()) {
             return;
         }
@@ -884,8 +884,12 @@
                 synchronized (mVirtualDeviceLock) {
                     mDevicePolicies.put(policyType, devicePolicy);
                     for (int i = 0; i < mVirtualDisplays.size(); i++) {
-                        mVirtualDisplays.valueAt(i).getWindowPolicyController()
-                                .setShowInHostDeviceRecents(devicePolicy == DEVICE_POLICY_DEFAULT);
+                        VirtualDisplayWrapper wrapper = mVirtualDisplays.valueAt(i);
+                        if (wrapper.isTrusted()) {
+                            wrapper.getWindowPolicyController()
+                                    .setShowInHostDeviceRecents(
+                                            devicePolicy == DEVICE_POLICY_DEFAULT);
+                        }
                     }
                 }
                 break;
@@ -905,7 +909,20 @@
                 break;
             case POLICY_TYPE_CLIPBOARD:
                 if (Flags.crossDeviceClipboard()) {
+                    if (policyType == DEVICE_POLICY_CUSTOM
+                            && mContext.checkCallingOrSelfPermission(ADD_TRUSTED_DISPLAY)
+                            != PackageManager.PERMISSION_GRANTED) {
+                        throw new SecurityException("Requires ADD_TRUSTED_DISPLAY permission to "
+                                + "set a custom clipboard policy.");
+                    }
                     synchronized (mVirtualDeviceLock) {
+                        for (int i = 0; i < mVirtualDisplays.size(); i++) {
+                            VirtualDisplayWrapper wrapper = mVirtualDisplays.valueAt(i);
+                            if (!wrapper.isTrusted() && !wrapper.isMirror()) {
+                                throw new SecurityException("All displays must be trusted for "
+                                        + "devices with custom clipboard policy.");
+                            }
+                        }
                         mDevicePolicies.put(policyType, devicePolicy);
                     }
                 }
@@ -924,11 +941,10 @@
     }
 
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void setDevicePolicyForDisplay(int displayId,
             @VirtualDeviceParams.DynamicDisplayPolicyType int policyType,
             @VirtualDeviceParams.DevicePolicy int devicePolicy) {
-        super.setDevicePolicyForDisplay_enforcePermission();
+        checkCallerIsDeviceOwner();
         if (!android.companion.virtualdevice.flags.Flags.activityControlApi()) {
             return;
         }
@@ -936,8 +952,11 @@
             checkDisplayOwnedByVirtualDeviceLocked(displayId);
             switch (policyType) {
                 case POLICY_TYPE_RECENTS:
-                    mVirtualDisplays.get(displayId).getWindowPolicyController()
-                            .setShowInHostDeviceRecents(devicePolicy == DEVICE_POLICY_DEFAULT);
+                    VirtualDisplayWrapper wrapper = mVirtualDisplays.get(displayId);
+                    if (wrapper.isTrusted()) {
+                        wrapper.getWindowPolicyController()
+                                .setShowInHostDeviceRecents(devicePolicy == DEVICE_POLICY_DEFAULT);
+                    }
                     break;
                 case POLICY_TYPE_ACTIVITY:
                     mVirtualDisplays.get(displayId).getWindowPolicyController()
@@ -951,9 +970,8 @@
     }
 
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void createVirtualDpad(VirtualDpadConfig config, @NonNull IBinder deviceToken) {
-        super.createVirtualDpad_enforcePermission();
+        checkCallerIsDeviceOwner();
         Objects.requireNonNull(config);
         checkVirtualInputDeviceDisplayIdAssociation(config.getAssociatedDisplayId());
         final long ident = Binder.clearCallingIdentity();
@@ -969,9 +987,8 @@
     }
 
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void createVirtualKeyboard(VirtualKeyboardConfig config, @NonNull IBinder deviceToken) {
-        super.createVirtualKeyboard_enforcePermission();
+        checkCallerIsDeviceOwner();
         Objects.requireNonNull(config);
         checkVirtualInputDeviceDisplayIdAssociation(config.getAssociatedDisplayId());
         final long ident = Binder.clearCallingIdentity();
@@ -991,9 +1008,8 @@
     }
 
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void createVirtualMouse(VirtualMouseConfig config, @NonNull IBinder deviceToken) {
-        super.createVirtualMouse_enforcePermission();
+        checkCallerIsDeviceOwner();
         Objects.requireNonNull(config);
         checkVirtualInputDeviceDisplayIdAssociation(config.getAssociatedDisplayId());
         final long ident = Binder.clearCallingIdentity();
@@ -1008,10 +1024,9 @@
     }
 
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void createVirtualTouchscreen(VirtualTouchscreenConfig config,
             @NonNull IBinder deviceToken) {
-        super.createVirtualTouchscreen_enforcePermission();
+        checkCallerIsDeviceOwner();
         Objects.requireNonNull(config);
         checkVirtualInputDeviceDisplayIdAssociation(config.getAssociatedDisplayId());
         final long ident = Binder.clearCallingIdentity();
@@ -1027,10 +1042,9 @@
     }
 
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void createVirtualNavigationTouchpad(VirtualNavigationTouchpadConfig config,
             @NonNull IBinder deviceToken) {
-        super.createVirtualNavigationTouchpad_enforcePermission();
+        checkCallerIsDeviceOwner();
         Objects.requireNonNull(config);
         checkVirtualInputDeviceDisplayIdAssociation(config.getAssociatedDisplayId());
         final long ident = Binder.clearCallingIdentity();
@@ -1048,10 +1062,9 @@
     }
 
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void createVirtualStylus(@NonNull VirtualStylusConfig config,
             @NonNull IBinder deviceToken) {
-        super.createVirtualStylus_enforcePermission();
+        checkCallerIsDeviceOwner();
         Objects.requireNonNull(config);
         Objects.requireNonNull(deviceToken);
         checkVirtualInputDeviceDisplayIdAssociation(config.getAssociatedDisplayId());
@@ -1068,10 +1081,9 @@
     }
 
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void createVirtualRotaryEncoder(@NonNull VirtualRotaryEncoderConfig config,
             @NonNull IBinder deviceToken) {
-        super.createVirtualRotaryEncoder_enforcePermission();
+        checkCallerIsDeviceOwner();
         Objects.requireNonNull(config);
         Objects.requireNonNull(deviceToken);
         checkVirtualInputDeviceDisplayIdAssociation(config.getAssociatedDisplayId());
@@ -1088,9 +1100,8 @@
     }
 
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void unregisterInputDevice(IBinder token) {
-        super.unregisterInputDevice_enforcePermission();
+        checkCallerIsDeviceOwner();
         final long ident = Binder.clearCallingIdentity();
         try {
             mInputController.unregisterInputDevice(token);
@@ -1100,9 +1111,7 @@
     }
 
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public int getInputDeviceId(IBinder token) {
-        super.getInputDeviceId_enforcePermission();
         final long ident = Binder.clearCallingIdentity();
         try {
             return mInputController.getInputDeviceId(token);
@@ -1113,9 +1122,8 @@
 
 
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public boolean sendDpadKeyEvent(IBinder token, VirtualKeyEvent event) {
-        super.sendDpadKeyEvent_enforcePermission();
+        checkCallerIsDeviceOwner();
         final long ident = Binder.clearCallingIdentity();
         try {
             return mInputController.sendDpadKeyEvent(token, event);
@@ -1125,9 +1133,8 @@
     }
 
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public boolean sendKeyEvent(IBinder token, VirtualKeyEvent event) {
-        super.sendKeyEvent_enforcePermission();
+        checkCallerIsDeviceOwner();
         final long ident = Binder.clearCallingIdentity();
         try {
             return mInputController.sendKeyEvent(token, event);
@@ -1137,9 +1144,8 @@
     }
 
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public boolean sendButtonEvent(IBinder token, VirtualMouseButtonEvent event) {
-        super.sendButtonEvent_enforcePermission();
+        checkCallerIsDeviceOwner();
         final long ident = Binder.clearCallingIdentity();
         try {
             return mInputController.sendButtonEvent(token, event);
@@ -1149,9 +1155,8 @@
     }
 
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public boolean sendTouchEvent(IBinder token, VirtualTouchEvent event) {
-        super.sendTouchEvent_enforcePermission();
+        checkCallerIsDeviceOwner();
         final long ident = Binder.clearCallingIdentity();
         try {
             return mInputController.sendTouchEvent(token, event);
@@ -1161,9 +1166,8 @@
     }
 
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public boolean sendRelativeEvent(IBinder token, VirtualMouseRelativeEvent event) {
-        super.sendRelativeEvent_enforcePermission();
+        checkCallerIsDeviceOwner();
         final long ident = Binder.clearCallingIdentity();
         try {
             return mInputController.sendRelativeEvent(token, event);
@@ -1173,9 +1177,8 @@
     }
 
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public boolean sendScrollEvent(IBinder token, VirtualMouseScrollEvent event) {
-        super.sendScrollEvent_enforcePermission();
+        checkCallerIsDeviceOwner();
         final long ident = Binder.clearCallingIdentity();
         try {
             return mInputController.sendScrollEvent(token, event);
@@ -1185,9 +1188,7 @@
     }
 
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public PointF getCursorPosition(IBinder token) {
-        super.getCursorPosition_enforcePermission();
         final long ident = Binder.clearCallingIdentity();
         try {
             return mInputController.getCursorPosition(token);
@@ -1197,10 +1198,9 @@
     }
 
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public boolean sendStylusMotionEvent(@NonNull IBinder token,
             @NonNull VirtualStylusMotionEvent event) {
-        super.sendStylusMotionEvent_enforcePermission();
+        checkCallerIsDeviceOwner();
         Objects.requireNonNull(token);
         Objects.requireNonNull(event);
         final long ident = Binder.clearCallingIdentity();
@@ -1212,10 +1212,9 @@
     }
 
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public boolean sendStylusButtonEvent(@NonNull IBinder token,
             @NonNull VirtualStylusButtonEvent event) {
-        super.sendStylusButtonEvent_enforcePermission();
+        checkCallerIsDeviceOwner();
         Objects.requireNonNull(token);
         Objects.requireNonNull(event);
         final long ident = Binder.clearCallingIdentity();
@@ -1227,10 +1226,9 @@
     }
 
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public boolean sendRotaryEncoderScrollEvent(@NonNull IBinder token,
             @NonNull VirtualRotaryEncoderScrollEvent event) {
-        super.sendRotaryEncoderScrollEvent_enforcePermission();
+        checkCallerIsDeviceOwner();
         final long ident = Binder.clearCallingIdentity();
         try {
             return mInputController.sendRotaryEncoderScrollEvent(token, event);
@@ -1240,17 +1238,19 @@
     }
 
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void setShowPointerIcon(boolean showPointerIcon) {
-        super.setShowPointerIcon_enforcePermission();
+        checkCallerIsDeviceOwner();
         final long ident = Binder.clearCallingIdentity();
         try {
             synchronized (mVirtualDeviceLock) {
                 mDefaultShowPointerIcon = showPointerIcon;
-            }
-            final int[] displayIds = getDisplayIds();
-            for (int i = 0; i < displayIds.length; ++i) {
-                mInputController.setShowPointerIcon(showPointerIcon, displayIds[i]);
+                for (int i = 0; i < mVirtualDisplays.size(); i++) {
+                    VirtualDisplayWrapper wrapper = mVirtualDisplays.valueAt(i);
+                    if (wrapper.isTrusted() || wrapper.isMirror()) {
+                        mInputController.setShowPointerIcon(
+                                mDefaultShowPointerIcon, mVirtualDisplays.keyAt(i));
+                    }
+                }
             }
         } finally {
             Binder.restoreCallingIdentity(ident);
@@ -1258,14 +1258,10 @@
     }
 
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void setDisplayImePolicy(int displayId, @WindowManager.DisplayImePolicy int policy) {
-        super.setDisplayImePolicy_enforcePermission();
+        checkCallerIsDeviceOwner();
         synchronized (mVirtualDeviceLock) {
-            if (!mVirtualDisplays.contains(displayId)) {
-                throw new SecurityException("Display ID " + displayId
-                        + " not found for this virtual device");
-            }
+            checkDisplayOwnedByVirtualDeviceLocked(displayId);
         }
         final long ident = Binder.clearCallingIdentity();
         try {
@@ -1276,10 +1272,9 @@
     }
 
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     @Nullable
     public List<VirtualSensor> getVirtualSensorList() {
-        super.getVirtualSensorList_enforcePermission();
+        checkCallerIsDeviceOwner();
         return mSensorController.getSensorList();
     }
 
@@ -1289,9 +1284,8 @@
     }
 
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public boolean sendSensorEvent(@NonNull IBinder token, @NonNull VirtualSensorEvent event) {
-        super.sendSensorEvent_enforcePermission();
+        checkCallerIsDeviceOwner();
         final long ident = Binder.clearCallingIdentity();
         try {
             return mSensorController.sendSensorEvent(token, event);
@@ -1301,10 +1295,9 @@
     }
 
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void registerIntentInterceptor(IVirtualDeviceIntentInterceptor intentInterceptor,
             IntentFilter filter) {
-        super.registerIntentInterceptor_enforcePermission();
+        checkCallerIsDeviceOwner();
         Objects.requireNonNull(intentInterceptor);
         Objects.requireNonNull(filter);
         synchronized (mVirtualDeviceLock) {
@@ -1313,10 +1306,9 @@
     }
 
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void unregisterIntentInterceptor(
             @NonNull IVirtualDeviceIntentInterceptor intentInterceptor) {
-        super.unregisterIntentInterceptor_enforcePermission();
+        checkCallerIsDeviceOwner();
         Objects.requireNonNull(intentInterceptor);
         synchronized (mVirtualDeviceLock) {
             mIntentInterceptors.remove(intentInterceptor.asBinder());
@@ -1324,10 +1316,9 @@
     }
 
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void registerVirtualCamera(@NonNull VirtualCameraConfig cameraConfig)
             throws RemoteException {
-        super.registerVirtualCamera_enforcePermission();
+        checkCallerIsDeviceOwner();
         Objects.requireNonNull(cameraConfig);
         if (mVirtualCameraController == null) {
             throw new UnsupportedOperationException("Virtual camera controller is not available");
@@ -1336,10 +1327,9 @@
     }
 
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void unregisterVirtualCamera(@NonNull VirtualCameraConfig cameraConfig)
             throws RemoteException {
-        super.unregisterVirtualCamera_enforcePermission();
+        checkCallerIsDeviceOwner();
         Objects.requireNonNull(cameraConfig);
         if (mVirtualCameraController == null) {
             throw new UnsupportedOperationException("Virtual camera controller is not available");
@@ -1348,10 +1338,8 @@
     }
 
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public String getVirtualCameraId(@NonNull VirtualCameraConfig cameraConfig)
             throws RemoteException {
-        super.getVirtualCameraId_enforcePermission();
         Objects.requireNonNull(cameraConfig);
         if (mVirtualCameraController == null) {
             throw new UnsupportedOperationException("Virtual camera controller is not available");
@@ -1474,10 +1462,9 @@
     }
 
     @Override // Binder call
-    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public int createVirtualDisplay(@NonNull VirtualDisplayConfig virtualDisplayConfig,
             @NonNull IVirtualDisplayCallback callback) {
-        super.createVirtualDisplay_enforcePermission();
+        checkCallerIsDeviceOwner();
         GenericWindowPolicyController gwpc;
         synchronized (mVirtualDeviceLock) {
             gwpc = createWindowPolicyControllerLocked(virtualDisplayConfig.getDisplayCategories());
@@ -1491,6 +1478,12 @@
         boolean isTrustedDisplay =
                 (mDisplayManagerInternal.getDisplayInfo(displayId).flags & Display.FLAG_TRUSTED)
                         == Display.FLAG_TRUSTED;
+        if (!isTrustedDisplay) {
+            if (getDevicePolicy(POLICY_TYPE_CLIPBOARD) != DEVICE_POLICY_DEFAULT) {
+                throw new SecurityException("All displays must be trusted for devices with custom"
+                        + "clipboard policy.");
+            }
+        }
 
         boolean showPointer;
         synchronized (mVirtualDeviceLock) {
@@ -1500,7 +1493,8 @@
                         "Virtual device already has a virtual display with ID " + displayId);
             }
 
-            PowerManager.WakeLock wakeLock = createAndAcquireWakeLockForDisplay(displayId);
+            PowerManager.WakeLock wakeLock =
+                    isTrustedDisplay ? createAndAcquireWakeLockForDisplay(displayId) : null;
             mVirtualDisplays.put(displayId, new VirtualDisplayWrapper(callback, gwpc, wakeLock,
                     isTrustedDisplay, isMirrorDisplay));
             showPointer = mDefaultShowPointerIcon;
@@ -1508,14 +1502,15 @@
 
         final long token = Binder.clearCallingIdentity();
         try {
-            mInputController.setShowPointerIcon(showPointer, displayId);
             mInputController.setMousePointerAccelerationEnabled(false, displayId);
             mInputController.setDisplayEligibilityForPointerCapture(/* isEligible= */ false,
                     displayId);
-            // WM throws a SecurityException if the display is untrusted.
             if (isTrustedDisplay) {
+                mInputController.setShowPointerIcon(showPointer, displayId);
                 mInputController.setDisplayImePolicy(displayId,
                         WindowManager.DISPLAY_IME_POLICY_LOCAL);
+            } else {
+                gwpc.setShowInHostDeviceRecents(true);
             }
         } finally {
             Binder.restoreCallingIdentity(token);
@@ -1616,6 +1611,11 @@
                     != PackageManager.PERMISSION_GRANTED) {
             synchronized (mVirtualDeviceLock) {
                 checkDisplayOwnedByVirtualDeviceLocked(displayId);
+                VirtualDisplayWrapper wrapper = mVirtualDisplays.get(displayId);
+                if (!wrapper.isTrusted() && !wrapper.isMirror()) {
+                    throw new SecurityException(
+                            "Cannot create input device associated with an untrusted display");
+                }
             }
         }
     }
@@ -1629,6 +1629,13 @@
         }
     }
 
+    private void checkCallerIsDeviceOwner() {
+        if (Binder.getCallingUid() != mOwnerUid) {
+            throw new SecurityException(
+                "Caller is not the owner of this virtual device");
+        }
+    }
+
     void goToSleepInternal(@PowerManager.GoToSleepReason int reason) {
         final long now = SystemClock.uptimeMillis();
         synchronized (mVirtualDeviceLock) {
@@ -1665,7 +1672,7 @@
      * @param virtualDisplayWrapper - VirtualDisplayWrapper to release resources for.
      */
     private void releaseOwnedVirtualDisplayResources(VirtualDisplayWrapper virtualDisplayWrapper) {
-        virtualDisplayWrapper.getWakeLock().release();
+        virtualDisplayWrapper.releaseWakeLock();
         virtualDisplayWrapper.getWindowPolicyController().unregisterRunningAppsChangedListener(
                 this);
     }
@@ -1833,10 +1840,10 @@
 
         VirtualDisplayWrapper(@NonNull IVirtualDisplayCallback token,
                 @NonNull GenericWindowPolicyController windowPolicyController,
-                @NonNull PowerManager.WakeLock wakeLock, boolean isTrusted, boolean isMirror) {
+                @Nullable PowerManager.WakeLock wakeLock, boolean isTrusted, boolean isMirror) {
             mToken = Objects.requireNonNull(token);
             mWindowPolicyController = Objects.requireNonNull(windowPolicyController);
-            mWakeLock = Objects.requireNonNull(wakeLock);
+            mWakeLock = wakeLock;
             mIsTrusted = isTrusted;
             mIsMirror = isMirror;
         }
@@ -1845,8 +1852,10 @@
             return mWindowPolicyController;
         }
 
-        PowerManager.WakeLock getWakeLock() {
-            return mWakeLock;
+        void releaseWakeLock() {
+            if (mWakeLock != null && mWakeLock.isHeld()) {
+                mWakeLock.release();
+            }
         }
 
         boolean isTrusted() {
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
index 41b6a85..f87e3c3 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -21,6 +21,7 @@
 
 import static com.android.server.wm.ActivityInterceptorCallback.VIRTUAL_DEVICE_SERVICE_ORDERED_ID;
 
+import android.annotation.EnforcePermission;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
@@ -122,7 +123,6 @@
     private final CompanionDeviceManager.OnAssociationsChangedListener mCdmAssociationListener =
             new CompanionDeviceManager.OnAssociationsChangedListener() {
                 @Override
-                @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
                 public void onAssociationsChanged(@NonNull List<AssociationInfo> associations) {
                     syncVirtualDevicesToCdmAssociations(associations);
                 }
@@ -339,7 +339,6 @@
         return true;
     }
 
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     private void syncVirtualDevicesToCdmAssociations(List<AssociationInfo> associations) {
         Set<VirtualDeviceImpl> virtualDevicesToRemove = new HashSet<>();
         synchronized (mVirtualDeviceManagerLock) {
@@ -382,7 +381,6 @@
         cdm.removeOnAssociationsChangedListener(mCdmAssociationListener);
     }
 
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     void onCdmAssociationsChanged(List<AssociationInfo> associations) {
         ArrayMap<String, AssociationInfo> vdmAssociations = new ArrayMap<>();
         for (int i = 0; i < associations.size(); ++i) {
@@ -452,7 +450,7 @@
                     }
                 };
 
-        @android.annotation.EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+        @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         @Override // Binder call
         public IVirtualDevice createVirtualDevice(
                 IBinder token,
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index bcca20b..f42f91e 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -12824,6 +12824,8 @@
             final long lostRAM = memInfo.getTotalSizeKb()
                     - (ss[INDEX_TOTAL_PSS] - ss[INDEX_TOTAL_SWAP_PSS])
                     - memInfo.getFreeSizeKb() - memInfo.getCachedSizeKb()
+                    // NR_SHMEM is subtracted twice (getCachedSizeKb() and getKernelUsedSizeKb())
+                    + memInfo.getShmemSizeKb()
                     - kernelUsed - memInfo.getZramTotalSizeKb();
             if (!opts.isCompact) {
                 pw.print(" Used RAM: "); pw.print(stringifyKBSize(ss[INDEX_TOTAL_PSS] - cachedPss
@@ -13337,6 +13339,8 @@
             long lostRAM = memInfo.getTotalSizeKb()
                     - (ss[INDEX_TOTAL_PSS] - ss[INDEX_TOTAL_SWAP_PSS])
                     - memInfo.getFreeSizeKb() - memInfo.getCachedSizeKb()
+                    // NR_SHMEM is subtracted twice (getCachedSizeKb() and getKernelUsedSizeKb())
+                    + memInfo.getShmemSizeKb()
                     - memInfo.getKernelUsedSizeKb() - memInfo.getZramTotalSizeKb();
             proto.write(MemInfoDumpProto.USED_PSS_KB, ss[INDEX_TOTAL_PSS] - cachedPss);
             proto.write(MemInfoDumpProto.USED_KERNEL_KB, memInfo.getKernelUsedSizeKb());
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index 2485626..5236b03 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -3629,42 +3629,68 @@
     }
 
     @GuardedBy({"mService", "mProcLock"})
-    private int updateLruProcessInternalLSP(ProcessRecord app, long now, int index,
-            int lruSeq, String what, Object obj, ProcessRecord srcApp) {
+    private int offerLruProcessInternalLSP(ProcessRecord app, long now, String what, Object obj,
+            ProcessRecord srcApp) {
         app.setLastActivityTime(now);
 
         if (app.hasActivitiesOrRecentTasks()) {
             // Don't want to touch dependent processes that are hosting activities.
-            return index;
+            return -1;
         }
 
-        int lrui = mLruProcesses.lastIndexOf(app);
+        final int lrui = mLruProcesses.lastIndexOf(app);
         if (lrui < 0) {
             Slog.wtf(TAG, "Adding dependent process " + app + " not on LRU list: "
                     + what + " " + obj + " from " + srcApp);
-            return index;
         }
+        return lrui;
+    }
 
-        if (lrui >= index) {
-            // Don't want to cause this to move dependent processes *back* in the
-            // list as if they were less frequently used.
-            return index;
-        }
+    /**
+     * This method is called after the indices array is populated by the indices offered by
+     * {@link #offerLruProcessInternalLSP} to actually move the processes to the desired locations
+     * in the LRU list. Since the indices array is a SparseBooleanArray, the indices are sorted
+     * and this allows us to preserve the previous order of the processes relative to each other.
+     * Key of the indices array holds the current index of the process in the LRU list and the value
+     * is a boolean indicating whether the process is an activity process or not. Activity processes
+     * are moved to the nextActivityIndex and non-activity processes are moved to the nextIndex
+     * positions, which are provided by the caller.
+     *
+     * @param indices The indices of the processes to move.
+     * @param nextActivityIndex The next index to insert an activity process.
+     * @param nextIndex The next index to insert a non-activity process.
+     */
+    @GuardedBy({"mService", "mProcLock"})
+    private void completeLruProcessInternalLSP(SparseBooleanArray indices, int nextActivityIndex,
+            int nextIndex) {
+        for (int i = indices.size() - 1; i >= 0; i--) {
+            final int lrui = indices.keyAt(i);
+            if (lrui < 0) {
+                // Rest of the indices are invalid, we can return early.
+                return;
+            }
+            final boolean isActivity = indices.valueAt(i);
+            int index = isActivity ? nextActivityIndex : nextIndex;
 
-        if (lrui >= mLruProcessActivityStart && index < mLruProcessActivityStart) {
-            // Don't want to touch dependent processes that are hosting activities.
-            return index;
-        }
+            if (lrui >= index) {
+                // Don't want to cause this to move dependent processes *back* in the
+                // list as if they were less frequently used.
+                continue;
+            }
 
-        mLruProcesses.remove(lrui);
-        if (index > 0) {
+            final ProcessRecord app = mLruProcesses.remove(lrui);
             index--;
+            if (DEBUG_LRU) Slog.d(TAG_LRU, "Moving dep from " + lrui + " to " + index
+                    + " in LRU list: " + app);
+            mLruProcesses.add(index, app);
+            app.setLruSeq(mLruSeq);
+
+            if (isActivity) {
+                nextActivityIndex = index;
+            } else {
+                nextIndex = index;
+            }
         }
-        if (DEBUG_LRU) Slog.d(TAG_LRU, "Moving dep from " + lrui + " to " + index
-                + " in LRU list: " + app);
-        mLruProcesses.add(index, app);
-        app.setLruSeq(lruSeq);
-        return index;
     }
 
     /**
@@ -4058,6 +4084,15 @@
 
         app.setLruSeq(mLruSeq);
 
+        // Key of the indices array holds the current index of the process in the LRU list and the
+        // value is a boolean indicating whether the process is an activity process or not.
+        // Activity processes will be moved to the nextActivityIndex and non-activity processes will
+        // be moved to the nextIndex positions when completeLruProcessInternalLSP is called.
+        // Since SparseBooleanArray's keys are sorted, we'll be able to keep the existing order of
+        // the processes relative to each other after the move.
+        final SparseBooleanArray indices = new SparseBooleanArray(psr.numberOfConnections()
+                + app.mProviders.numberOfProviderConnections());
+
         // If the app is currently using a content provider or service,
         // bump those processes as well.
         for (int j = psr.numberOfConnections() - 1; j >= 0; j--) {
@@ -4069,16 +4104,12 @@
                     && !cr.binding.service.app.isPersistent()) {
                 if (cr.binding.service.app.mServices.hasClientActivities()) {
                     if (nextActivityIndex >= 0) {
-                        nextActivityIndex = updateLruProcessInternalLSP(cr.binding.service.app,
-                                now,
-                                nextActivityIndex, mLruSeq,
-                                "service connection", cr, app);
+                        indices.append(offerLruProcessInternalLSP(cr.binding.service.app, now,
+                                "service connection", cr, app), true);
                     }
                 } else {
-                    nextIndex = updateLruProcessInternalLSP(cr.binding.service.app,
-                            now,
-                            nextIndex, mLruSeq,
-                            "service connection", cr, app);
+                    indices.append(offerLruProcessInternalLSP(cr.binding.service.app, now,
+                            "service connection", cr, app), false);
                 }
             }
         }
@@ -4086,10 +4117,11 @@
         for (int j = ppr.numberOfProviderConnections() - 1; j >= 0; j--) {
             ContentProviderRecord cpr = ppr.getProviderConnectionAt(j).provider;
             if (cpr.proc != null && cpr.proc.getLruSeq() != mLruSeq && !cpr.proc.isPersistent()) {
-                nextIndex = updateLruProcessInternalLSP(cpr.proc, now, nextIndex, mLruSeq,
-                        "provider reference", cpr, app);
+                indices.append(offerLruProcessInternalLSP(cpr.proc, now,
+                        "provider reference", cpr, app), false);
             }
         }
+        completeLruProcessInternalLSP(indices, nextActivityIndex, nextIndex);
     }
 
     @GuardedBy(anyOf = {"mService", "mProcLock"})
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index d206b20..fdf7dec 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -286,7 +286,6 @@
 import java.util.Set;
 import java.util.TreeSet;
 import java.util.concurrent.CancellationException;
-import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
@@ -8049,7 +8048,14 @@
         }
         synchronized (mAbsoluteVolumeDeviceInfoMapLock) {
             if (mAbsoluteVolumeDeviceInfoMap.containsKey(audioSystemDeviceOut)) {
-                return mAbsoluteVolumeDeviceInfoMap.get(audioSystemDeviceOut).mDeviceVolumeBehavior;
+                final AbsoluteVolumeDeviceInfo deviceInfo = mAbsoluteVolumeDeviceInfoMap.get(
+                        audioSystemDeviceOut);
+                if (deviceInfo != null) {
+                    return deviceInfo.mDeviceVolumeBehavior;
+                }
+
+                Log.e(TAG,
+                        "Null absolute volume device info stored for key " + audioSystemDeviceOut);
             }
         }
 
@@ -15043,6 +15049,11 @@
 
     private void addAudioSystemDeviceOutToAbsVolumeDevices(int audioSystemDeviceOut,
             AbsoluteVolumeDeviceInfo info) {
+        if (info == null) {
+            Log.e(TAG, "Cannot add null absolute volume info for audioSystemDeviceOut "
+                    + audioSystemDeviceOut);
+            return;
+        }
         if (DEBUG_VOL) {
             Log.d(TAG, "Adding DeviceType: 0x" + Integer.toHexString(audioSystemDeviceOut)
                     + " to mAbsoluteVolumeDeviceInfoMap with behavior "
diff --git a/services/core/java/com/android/server/compat/PlatformCompat.java b/services/core/java/com/android/server/compat/PlatformCompat.java
index a9fe8cb..8d64383 100644
--- a/services/core/java/com/android/server/compat/PlatformCompat.java
+++ b/services/core/java/com/android/server/compat/PlatformCompat.java
@@ -242,7 +242,8 @@
         boolean enabled = true;
         final int userId = UserHandle.getUserId(uid);
         for (String packageName : packages) {
-            final var appInfo = getApplicationInfo(packageName, userId);
+            final var appInfo =
+                fixTargetSdk(getApplicationInfo(packageName, userId), uid);
             enabled &= isChangeEnabledInternal(changeId, appInfo);
         }
         return enabled;
@@ -261,7 +262,8 @@
         boolean enabled = true;
         final int userId = UserHandle.getUserId(uid);
         for (String packageName : packages) {
-            final var appInfo = getApplicationInfo(packageName, userId);
+            final var appInfo =
+                fixTargetSdk(getApplicationInfo(packageName, userId), uid);
             enabled &= isChangeEnabledInternalNoLogging(changeId, appInfo);
         }
         return enabled;
@@ -504,6 +506,15 @@
                 packageName, 0, Process.myUid(), userId);
     }
 
+    private ApplicationInfo fixTargetSdk(ApplicationInfo appInfo, int uid) {
+        // b/282922910 - we don't want apps sharing system uid and targeting
+        // older target sdk to impact all system uid apps
+        if (Flags.systemUidTargetSystemSdk() && uid == Process.SYSTEM_UID) {
+            appInfo.targetSdkVersion = Build.VERSION.SDK_INT;
+        }
+        return appInfo;
+    }
+
     private void killPackage(String packageName) {
         int uid = LocalServices.getService(PackageManagerInternal.class).getPackageUid(packageName,
                 0, UserHandle.myUserId());
diff --git a/services/core/java/com/android/server/compat/platform_compat_flags.aconfig b/services/core/java/com/android/server/compat/platform_compat_flags.aconfig
new file mode 100644
index 0000000..fb32323
--- /dev/null
+++ b/services/core/java/com/android/server/compat/platform_compat_flags.aconfig
@@ -0,0 +1,10 @@
+package: "com.android.server.compat"
+container: "system"
+
+flag {
+    name: "system_uid_target_system_sdk"
+    namespace: "app_compat"
+    description: "Compat framework feature flag for forcing all system uid apps to target system sdk"
+    bug: "29702703"
+    is_fixed_read_only: true
+}
diff --git a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
index 6216a58..8b9c664 100644
--- a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
+++ b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
@@ -778,7 +778,8 @@
         processRecord.notifyRequestActiveAsync(request.getToken());
     }
 
-    private void registerProcess(int pid, IDeviceStateManagerCallback callback) {
+    @Nullable
+    private DeviceStateInfo registerProcess(int pid, IDeviceStateManagerCallback callback) {
         synchronized (mLock) {
             if (mProcessRecords.contains(pid)) {
                 throw new SecurityException("The calling process has already registered an"
@@ -794,15 +795,20 @@
             }
             mProcessRecords.put(pid, record);
 
-            // Callback clients should not be notified of invalid device states, so calls to
-            // #getDeviceStateInfoLocked should be gated on checks if a committed state is present
-            // before getting the device state info.
             final DeviceStateInfo currentInfo =
                     mCommittedState.isPresent() ? getDeviceStateInfoLocked() : null;
-            if (currentInfo != null) {
-                // If there is not a committed state we'll wait to notify the process of the initial
-                // value.
-                record.notifyDeviceStateInfoAsync(currentInfo);
+            if (com.android.window.flags.Flags.wlinfoOncreate()) {
+                return currentInfo;
+            } else {
+                // Callback clients should not be notified of invalid device states, so calls to
+                // #getDeviceStateInfoLocked should be gated on checks if a committed state is
+                // present before getting the device state info.
+                if (currentInfo != null) {
+                    // If there is not a committed state we'll wait to notify the process of the
+                    // initial value.
+                    record.notifyDeviceStateInfoAsync(currentInfo);
+                }
+                return null;
             }
         }
     }
@@ -1286,8 +1292,9 @@
             }
         }
 
+        @Nullable
         @Override // Binder call
-        public void registerCallback(IDeviceStateManagerCallback callback) {
+        public DeviceStateInfo registerCallback(IDeviceStateManagerCallback callback) {
             if (callback == null) {
                 throw new IllegalArgumentException("Device state callback must not be null.");
             }
@@ -1295,7 +1302,7 @@
             final int callingPid = Binder.getCallingPid();
             final long token = Binder.clearCallingIdentity();
             try {
-                registerProcess(callingPid, callback);
+                return registerProcess(callingPid, callback);
             } finally {
                 Binder.restoreCallingIdentity(token);
             }
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 179ec63..c7a70fa 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -1769,10 +1769,11 @@
             flags &= ~VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP;
         }
         // Put the display in the virtual device's display group only if it's not a mirror display,
-        // and if it doesn't need its own display group. So effectively, mirror displays go into the
-        // default display group.
+        // it is a trusted display, and it doesn't need its own display group. So effectively,
+        // mirror and untrusted displays go into the default display group.
         if ((flags & VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP) == 0
                 && (flags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) == 0
+                && (flags & VIRTUAL_DISPLAY_FLAG_TRUSTED) == VIRTUAL_DISPLAY_FLAG_TRUSTED
                 && virtualDevice != null) {
             flags |= VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP;
         }
@@ -1848,9 +1849,7 @@
 
         if (callingUid != Process.SYSTEM_UID
                 && (flags & VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP) != 0) {
-            // The virtualDevice instance has been validated above using isValidVirtualDevice
-            if (virtualDevice == null
-                    && !checkCallingPermission(ADD_TRUSTED_DISPLAY, "createVirtualDisplay()")) {
+            if (!checkCallingPermission(ADD_TRUSTED_DISPLAY, "createVirtualDisplay()")) {
                 throw new SecurityException("Requires ADD_TRUSTED_DISPLAY permission to "
                         + "create a virtual display which is not in the default DisplayGroup.");
             }
@@ -5667,6 +5666,11 @@
                 displayPowerController.stylusGestureStarted(eventTime);
             }
         }
+
+        @Override
+        public boolean isDisplayReadyForMirroring(int displayId) {
+            return mExternalDisplayPolicy.isDisplayReadyForMirroring(displayId);
+        }
     }
 
     class DesiredDisplayModeSpecsObserver
diff --git a/services/core/java/com/android/server/display/ExternalDisplayPolicy.java b/services/core/java/com/android/server/display/ExternalDisplayPolicy.java
index 28a0b28..f34d2cc 100644
--- a/services/core/java/com/android/server/display/ExternalDisplayPolicy.java
+++ b/services/core/java/com/android/server/display/ExternalDisplayPolicy.java
@@ -375,6 +375,54 @@
         }
     }
 
+    boolean isDisplayReadyForMirroring(int displayId) {
+        if (!mFlags.isWaitingConfirmationBeforeMirroringEnabled()) {
+            if (DEBUG) {
+                Slog.d(TAG, "isDisplayReadyForMirroring: mirroring CONFIRMED - "
+                        + " flag 'waiting for confirmation before mirroring' is disabled");
+            }
+            return true;
+        }
+
+        synchronized (mSyncRoot) {
+            if (!mIsBootCompleted) {
+                if (DEBUG) {
+                    Slog.d(TAG, "isDisplayReadyForMirroring: mirroring is not confirmed - "
+                            + "boot is in progress");
+                }
+                return false;
+            }
+
+            var logicalDisplay = mLogicalDisplayMapper.getDisplayLocked(displayId);
+            if (logicalDisplay == null) {
+                if (DEBUG) {
+                    Slog.d(TAG, "isDisplayReadyForMirroring: mirroring is not confirmed - "
+                            + "logicalDisplay is null");
+                }
+                return false;
+            }
+
+            if (!isExternalDisplayLocked(logicalDisplay)) {
+                if (DEBUG) {
+                    Slog.d(TAG, "isDisplayReadyForMirroring: mirroring is not confirmed - "
+                            + "logicalDisplay" + logicalDisplay.getDisplayIdLocked()
+                            + " type is " + logicalDisplay.getDisplayInfoLocked().type);
+                }
+                return false;
+            }
+
+            if (!logicalDisplay.isEnabledLocked()) {
+                if (DEBUG) {
+                    Slog.d(TAG, "isDisplayReadyForMirroring: mirroring is not confirmed - "
+                            + "logicalDisplay is disabled");
+                }
+                return false;
+            }
+        }
+
+        return true;
+    }
+
     private final class SkinThermalStatusObserver extends IThermalEventListener.Stub {
         @Override
         public void notifyThrottling(@NonNull final Temperature temp) {
diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
index 06a9103..09fa4e6 100644
--- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java
+++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
@@ -16,6 +16,7 @@
 
 package com.android.server.display;
 
+import static android.hardware.devicestate.DeviceState.PROPERTY_EMULATED_ONLY;
 import static android.hardware.devicestate.DeviceState.PROPERTY_POWER_CONFIGURATION_TRIGGER_SLEEP;
 import static android.hardware.devicestate.DeviceState.PROPERTY_POWER_CONFIGURATION_TRIGGER_WAKE;
 import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE;
@@ -594,6 +595,13 @@
     boolean shouldDeviceBeWoken(DeviceState pendingState, DeviceState currentState,
             boolean isInteractive, boolean isBootCompleted) {
         if (mDeviceStateManagerFlags.deviceStatePropertyMigration()) {
+            if (currentState.hasProperties(PROPERTY_EMULATED_ONLY)
+                    && !pendingState.hasProperties(PROPERTY_EMULATED_ONLY)) {
+                // Do not wake the device, since this transition may occur due to the user pressing
+                // the power button to exit an emulated state.
+                return false;
+            }
+
             return pendingState.hasProperty(PROPERTY_POWER_CONFIGURATION_TRIGGER_WAKE)
                     && !currentState.equals(INVALID_DEVICE_STATE)
                     && !currentState.hasProperty(PROPERTY_POWER_CONFIGURATION_TRIGGER_WAKE)
diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
index 99ced7f..b2e98bc 100644
--- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
+++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
@@ -217,6 +217,11 @@
             Flags::enableUserRefreshRateForExternalDisplay
     );
 
+    private final FlagState mEnableWaitingConfirmationBeforeMirroring = new FlagState(
+            Flags.FLAG_ENABLE_WAITING_CONFIRMATION_BEFORE_MIRRORING,
+            Flags::enableWaitingConfirmationBeforeMirroring
+    );
+
     private final FlagState mEnableBatteryStatsForAllDisplays = new FlagState(
             Flags.FLAG_ENABLE_BATTERY_STATS_FOR_ALL_DISPLAYS,
             Flags::enableBatteryStatsForAllDisplays
@@ -445,6 +450,14 @@
     }
 
     /**
+      * @return {@code true} if mirroring won't be enabled until boot completes and the user enables
+      * the display.
+      */
+    public boolean isWaitingConfirmationBeforeMirroringEnabled() {
+        return mEnableWaitingConfirmationBeforeMirroring.isEnabled();
+    }
+
+    /**
       * @return {@code true} if battery stats is enabled for all displays, not just the primary
       * display.
       */
@@ -511,6 +524,7 @@
         pw.println(" " + mVirtualDisplayLimit);
         pw.println(" " + mNormalBrightnessForDozeParameter);
         pw.println(" " + mIdleScreenConfigInSubscribingLightSensor);
+        pw.println(" " + mEnableWaitingConfirmationBeforeMirroring);
         pw.println(" " + mEnableBatteryStatsForAllDisplays);
         pw.println(" " + mBlockAutobrightnessChangesOnStylusUsage);
         pw.println(" " + mIsUserRefreshRateForExternalDisplayEnabled);
diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig
index 2f04d9e..df62638 100644
--- a/services/core/java/com/android/server/display/feature/display_flags.aconfig
+++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig
@@ -367,6 +367,17 @@
 }
 
 flag {
+    name: "enable_waiting_confirmation_before_mirroring"
+    namespace: "display_manager"
+    description: "Allow ContentRecorder checking whether user confirmed mirroring after boot"
+    bug: "361698995"
+    is_fixed_read_only: true
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
     name: "enable_battery_stats_for_all_displays"
     namespace: "display_manager"
     description: "Flag to enable battery stats for all displays."
diff --git a/services/core/java/com/android/server/input/InputSettingsObserver.java b/services/core/java/com/android/server/input/InputSettingsObserver.java
index d70bd8b..d1a6d3b 100644
--- a/services/core/java/com/android/server/input/InputSettingsObserver.java
+++ b/services/core/java/com/android/server/input/InputSettingsObserver.java
@@ -63,6 +63,12 @@
         mObservers = Map.ofEntries(
                 Map.entry(Settings.System.getUriFor(Settings.System.POINTER_SPEED),
                         (reason) -> updateMousePointerSpeed()),
+                Map.entry(Settings.System.getUriFor(
+                        Settings.System.MOUSE_REVERSE_VERTICAL_SCROLLING),
+                        (reason) -> updateMouseReverseVerticalScrolling()),
+                Map.entry(Settings.System.getUriFor(
+                                Settings.System.MOUSE_SWAP_PRIMARY_BUTTON),
+                        (reason) -> updateMouseSwapPrimaryButton()),
                 Map.entry(Settings.System.getUriFor(Settings.System.TOUCHPAD_POINTER_SPEED),
                         (reason) -> updateTouchpadPointerSpeed()),
                 Map.entry(Settings.System.getUriFor(Settings.System.TOUCHPAD_NATURAL_SCROLLING),
@@ -163,6 +169,16 @@
         mNative.setPointerSpeed(constrainPointerSpeedValue(speed));
     }
 
+    private void updateMouseReverseVerticalScrolling() {
+        mNative.setMouseReverseVerticalScrollingEnabled(
+                InputSettings.isMouseReverseVerticalScrollingEnabled(mContext));
+    }
+
+    private void updateMouseSwapPrimaryButton() {
+        mNative.setMouseSwapPrimaryButtonEnabled(
+                InputSettings.isMouseSwapPrimaryButtonEnabled(mContext));
+    }
+
     private void updateTouchpadPointerSpeed() {
         mNative.setTouchpadPointerSpeed(
                 constrainPointerSpeedValue(InputSettings.getTouchpadPointerSpeed(mContext)));
diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java
index 4404d63..21e8bcc 100644
--- a/services/core/java/com/android/server/input/NativeInputManagerService.java
+++ b/services/core/java/com/android/server/input/NativeInputManagerService.java
@@ -127,6 +127,10 @@
 
     void setMousePointerAccelerationEnabled(int displayId, boolean enabled);
 
+    void setMouseReverseVerticalScrollingEnabled(boolean enabled);
+
+    void setMouseSwapPrimaryButtonEnabled(boolean enabled);
+
     void setTouchpadPointerSpeed(int speed);
 
     void setTouchpadNaturalScrollingEnabled(boolean enabled);
@@ -388,6 +392,12 @@
         public native void setMousePointerAccelerationEnabled(int displayId, boolean enabled);
 
         @Override
+        public native void setMouseReverseVerticalScrollingEnabled(boolean enabled);
+
+        @Override
+        public native void setMouseSwapPrimaryButtonEnabled(boolean enabled);
+
+        @Override
         public native void setTouchpadPointerSpeed(int speed);
 
         @Override
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index f0fb33e..35b5171 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -84,6 +84,7 @@
 import android.content.pm.ServiceInfo;
 import android.content.pm.UserInfo;
 import android.content.res.Resources;
+import android.hardware.display.DisplayManagerInternal;
 import android.hardware.input.InputManager;
 import android.inputmethodservice.InputMethodService;
 import android.inputmethodservice.InputMethodService.BackDispositionMode;
@@ -119,6 +120,7 @@
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.proto.ProtoOutputStream;
+import android.view.Display;
 import android.view.InputChannel;
 import android.view.InputDevice;
 import android.view.MotionEvent;
@@ -448,6 +450,8 @@
     private AudioManagerInternal mAudioManagerInternal = null;
     @Nullable
     private VirtualDeviceManagerInternal mVdmInternal = null;
+    @Nullable
+    private DisplayManagerInternal mDisplayManagerInternal = null;
 
     // Mapping from deviceId to the device-specific imeId for that device.
     @GuardedBy("ImfLock.class")
@@ -2165,7 +2169,18 @@
         final var bindingController = getInputMethodBindingController(userId);
         final int oldDeviceId = bindingController.getDeviceIdToShowIme();
         final int displayIdToShowIme = bindingController.getDisplayIdToShowIme();
-        final int newDeviceId = mVdmInternal.getDeviceIdForDisplayId(displayIdToShowIme);
+        int newDeviceId = mVdmInternal.getDeviceIdForDisplayId(displayIdToShowIme);
+        if (newDeviceId != DEVICE_ID_DEFAULT) {
+            // Only show custom IME on trusted displays.
+            if (mDisplayManagerInternal == null) {
+                mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class);
+            }
+            int displayFlags = mDisplayManagerInternal.getDisplayInfo(displayIdToShowIme).flags;
+            if ((displayFlags & Display.FLAG_TRUSTED) != Display.FLAG_TRUSTED) {
+                // If the display is not trusted, fallback to the default device IME.
+                newDeviceId = DEVICE_ID_DEFAULT;
+            }
+        }
         bindingController.setDeviceIdToShowIme(newDeviceId);
         if (newDeviceId == DEVICE_ID_DEFAULT) {
             if (oldDeviceId == DEVICE_ID_DEFAULT) {
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySnapshotStorage.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySnapshotStorage.java
index c02b103..404c841 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySnapshotStorage.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySnapshotStorage.java
@@ -19,7 +19,6 @@
 import android.annotation.Nullable;
 import android.os.Environment;
 import android.security.keystore.recovery.KeyChainSnapshot;
-import android.util.Log;
 import android.util.SparseArray;
 
 import com.android.internal.annotations.GuardedBy;
@@ -29,9 +28,11 @@
 import com.android.server.locksettings.recoverablekeystore.serialization
         .KeyChainSnapshotParserException;
 import com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSerializer;
+import com.android.server.utils.Slogf;
 
 import java.io.File;
 import java.io.FileInputStream;
+import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.security.cert.CertificateEncodingException;
@@ -81,12 +82,14 @@
     public synchronized void put(int uid, KeyChainSnapshot snapshot) {
         mSnapshotByUid.put(uid, snapshot);
 
-        try {
-            writeToDisk(uid, snapshot);
+        File snapshotFile = getSnapshotFile(uid);
+        try (FileOutputStream fileOutputStream = new FileOutputStream(snapshotFile)) {
+            KeyChainSnapshotSerializer.serialize(snapshot, fileOutputStream);
         } catch (IOException | CertificateEncodingException e) {
-            Log.e(TAG,
-                    String.format(Locale.US, "Error persisting snapshot for %d to disk", uid),
-                    e);
+            // If we fail to write the latest snapshot, we should delete any older snapshot that
+            // happens to be around. Otherwise snapshot syncs might end up going 'back in time'.
+            snapshotFile.delete();
+            Slogf.e(TAG, e, "Error persisting snapshot for %d to disk", uid);
         }
     }
 
@@ -100,10 +103,17 @@
             return snapshot;
         }
 
-        try {
-            return readFromDisk(uid);
+        File snapshotFile = getSnapshotFile(uid);
+        try (FileInputStream fileInputStream = new FileInputStream(snapshotFile)) {
+            return KeyChainSnapshotDeserializer.deserialize(fileInputStream);
+        } catch (FileNotFoundException e) {
+            Slogf.i(TAG, "Snapshot for uid %d not found", uid);
+            return null;
         } catch (IOException | KeyChainSnapshotParserException e) {
-            Log.e(TAG, String.format(Locale.US, "Error reading snapshot for %d from disk", uid), e);
+            // If we fail to read the latest snapshot, we should delete it in case it is in some way
+            // corrupted. We can regenerate snapshots anyway.
+            snapshotFile.delete();
+            Slogf.e(TAG, e, "Error reading snapshot for %d from disk", uid);
             return null;
         }
     }
@@ -116,50 +126,6 @@
         getSnapshotFile(uid).delete();
     }
 
-    /**
-     * Writes the snapshot for recovery agent {@code uid} to disk.
-     *
-     * @throws IOException if an IO error occurs writing to disk.
-     */
-    private void writeToDisk(int uid, KeyChainSnapshot snapshot)
-            throws IOException, CertificateEncodingException {
-        File snapshotFile = getSnapshotFile(uid);
-
-        try (
-            FileOutputStream fileOutputStream = new FileOutputStream(snapshotFile)
-        ) {
-            KeyChainSnapshotSerializer.serialize(snapshot, fileOutputStream);
-        } catch (IOException | CertificateEncodingException e) {
-            // If we fail to write the latest snapshot, we should delete any older snapshot that
-            // happens to be around. Otherwise snapshot syncs might end up going 'back in time'.
-            snapshotFile.delete();
-            throw e;
-        }
-    }
-
-    /**
-     * Reads the last snapshot for recovery agent {@code uid} from disk.
-     *
-     * @return The snapshot, or null if none existed.
-     * @throws IOException if an IO error occurs reading from disk.
-     */
-    @Nullable
-    private KeyChainSnapshot readFromDisk(int uid)
-            throws IOException, KeyChainSnapshotParserException {
-        File snapshotFile = getSnapshotFile(uid);
-
-        try (
-            FileInputStream fileInputStream = new FileInputStream(snapshotFile)
-        ) {
-            return KeyChainSnapshotDeserializer.deserialize(fileInputStream);
-        } catch (IOException | KeyChainSnapshotParserException e) {
-            // If we fail to read the latest snapshot, we should delete it in case it is in some way
-            // corrupted. We can regenerate snapshots anyway.
-            snapshotFile.delete();
-            throw e;
-        }
-    }
-
     private File getSnapshotFile(int uid) {
         File folder = getStorageFolder();
         String fileName = getSnapshotFileName(uid);
diff --git a/services/core/java/com/android/server/pm/InstallRequest.java b/services/core/java/com/android/server/pm/InstallRequest.java
index 1f79ac0..089bbb7 100644
--- a/services/core/java/com/android/server/pm/InstallRequest.java
+++ b/services/core/java/com/android/server/pm/InstallRequest.java
@@ -16,7 +16,6 @@
 
 package com.android.server.pm;
 
-import static android.content.pm.Flags.improveInstallFreeze;
 import static android.content.pm.PackageInstaller.SessionParams.USER_ACTION_UNSPECIFIED;
 import static android.content.pm.PackageManager.INSTALL_REASON_UNKNOWN;
 import static android.content.pm.PackageManager.INSTALL_SCENARIO_DEFAULT;
@@ -1050,13 +1049,13 @@
     }
 
     public void onFreezeStarted() {
-        if (mPackageMetrics != null && improveInstallFreeze()) {
+        if (mPackageMetrics != null) {
             mPackageMetrics.onStepStarted(PackageMetrics.STEP_FREEZE_INSTALL);
         }
     }
 
     public void onFreezeCompleted() {
-        if (mPackageMetrics != null && improveInstallFreeze()) {
+        if (mPackageMetrics != null) {
             mPackageMetrics.onStepFinished(PackageMetrics.STEP_FREEZE_INSTALL);
         }
     }
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 8bab9de..708e067 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -1101,7 +1101,7 @@
         if (android.multiuser.Flags.cachesNotInvalidatedAtStartReadOnly()) {
             UserManager.invalidateIsUserUnlockedCache();
             UserManager.invalidateQuietModeEnabledCache();
-            UserManager.invalidateUserSerialNumberCache();
+            UserManager.invalidateCacheOnUserListChange();
         }
     }
 
@@ -4448,7 +4448,7 @@
 
                             if (userData != null) {
                                 synchronized (mUsersLock) {
-                                    mUsers.put(userData.info.id, userData);
+                                    addUserDataLU(userData);
                                     if (mNextSerialNumber < 0
                                             || mNextSerialNumber <= userData.info.id) {
                                         mNextSerialNumber = userData.info.id + 1;
@@ -5724,7 +5724,7 @@
                     userData.info = userInfo;
                     userData.userProperties = new UserProperties(
                             userTypeDetails.getDefaultUserPropertiesReference());
-                    mUsers.put(userId, userData);
+                    addUserDataLU(userData);
                 }
                 writeUserLP(userData);
                 writeUserListLP();
@@ -6138,7 +6138,7 @@
         final UserData userData = new UserData();
         userData.info = userInfo;
         synchronized (mUsersLock) {
-            mUsers.put(userInfo.id, userData);
+            addUserDataLU(userData);
         }
         updateUserIds();
         return userData;
@@ -6148,8 +6148,7 @@
     @VisibleForTesting
     void removeUserInfo(@UserIdInt int userId) {
         synchronized (mUsersLock) {
-            UserManager.invalidateUserSerialNumberCache();
-            mUsers.remove(userId);
+            removeUserDataLU(userId);
         }
     }
 
@@ -6579,8 +6578,7 @@
 
         // Remove this user from the list
         synchronized (mUsersLock) {
-            UserManager.invalidateUserSerialNumberCache();
-            mUsers.remove(userId);
+            removeUserDataLU(userId);
             mIsUserManaged.delete(userId);
         }
         synchronized (mUserStates) {
@@ -6969,6 +6967,26 @@
     }
 
     /**
+     * Adding user data to mUsers list in one place to invalidate related caches.
+     */
+    @GuardedBy("mUsersLock")
+    private void addUserDataLU(UserData userData) {
+        if (android.multiuser.Flags.invalidateCacheOnUsersChangedReadOnly()) {
+            UserManager.invalidateCacheOnUserListChange();
+        }
+        mUsers.put(userData.info.id, userData);
+    }
+
+    /**
+     * Removing user data to mUsers list in one place to invalidate related caches.
+     */
+    @GuardedBy("mUsersLock")
+    private void removeUserDataLU(@UserIdInt int userId) {
+        UserManager.invalidateCacheOnUserListChange();
+        mUsers.remove(userId);
+    }
+
+    /**
      * Caches the list of user ids in an array, adjusting the array size when necessary.
      */
     private void updateUserIds() {
diff --git a/services/core/java/com/android/server/wm/ConfigurationContainer.java b/services/core/java/com/android/server/wm/ConfigurationContainer.java
index 670a61d..05dcbb7 100644
--- a/services/core/java/com/android/server/wm/ConfigurationContainer.java
+++ b/services/core/java/com/android/server/wm/ConfigurationContainer.java
@@ -25,6 +25,7 @@
 import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 import static android.app.WindowConfiguration.activityTypeToString;
@@ -268,7 +269,16 @@
             }
             final DisplayPolicy.DecorInsets.Info decor =
                     displayContent.getDisplayPolicy().getDecorInsetsInfo(rotation, dw, dh);
-            outAppBounds.intersectUnchecked(decor.mOverrideNonDecorFrame);
+            if (!outAppBounds.intersect(decor.mOverrideNonDecorFrame)) {
+                // TODO (b/364883053): When a split screen is requested from an app intent for a new
+                //  task, the bounds is not the final bounds, and this is also not a bounds change
+                //  event handled correctly with the offset. Revert back to legacy method for this
+                //  case.
+                if (inOutConfig.windowConfiguration.getWindowingMode()
+                        == WINDOWING_MODE_MULTI_WINDOW) {
+                    outAppBounds.inset(decor.mOverrideNonDecorInsets);
+                }
+            }
             if (task != null && (task.mOffsetYForInsets != 0 || task.mOffsetXForInsets != 0)) {
                 outAppBounds.offset(-task.mOffsetXForInsets, -task.mOffsetYForInsets);
             }
diff --git a/services/core/java/com/android/server/wm/ContentRecorder.java b/services/core/java/com/android/server/wm/ContentRecorder.java
index bc33946..0b5872b 100644
--- a/services/core/java/com/android/server/wm/ContentRecorder.java
+++ b/services/core/java/com/android/server/wm/ContentRecorder.java
@@ -285,6 +285,11 @@
         }
     }
 
+    private boolean isDisplayReadyForMirroring() {
+        return mDisplayContent.getDisplayInfo().type != Display.TYPE_EXTERNAL
+                || mDisplayContent.mWmService.mDisplayManagerInternal.isDisplayReadyForMirroring(
+                        mDisplayContent.getDisplayId());
+    }
 
     /**
      * Ensure recording does not fall back to the display stack; ensure the recording is stopped
@@ -335,7 +340,7 @@
             return;
         }
 
-        if (mContentRecordingSession.isWaitingForConsent()) {
+        if (mContentRecordingSession.isWaitingForConsent() || !isDisplayReadyForMirroring()) {
             ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, "Content Recording: waiting to record, so do "
                     + "nothing");
             return;
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index efca902..248ed1a 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -337,6 +337,8 @@
     int32_t getMousePointerSpeed();
     void setPointerSpeed(int32_t speed);
     void setMousePointerAccelerationEnabled(ui::LogicalDisplayId displayId, bool enabled);
+    void setMouseReverseVerticalScrollingEnabled(bool enabled);
+    void setMouseSwapPrimaryButtonEnabled(bool enabled);
     void setTouchpadPointerSpeed(int32_t speed);
     void setTouchpadNaturalScrollingEnabled(bool enabled);
     void setTouchpadTapToClickEnabled(bool enabled);
@@ -482,6 +484,12 @@
         // True if stylus button reporting through motion events is enabled.
         bool stylusButtonMotionEventsEnabled{true};
 
+        // True if mouse vertical scrolling is reversed.
+        bool mouseReverseVerticalScrollingEnabled{false};
+
+        // True if the mouse primary button is swapped (left/right buttons).
+        bool mouseSwapPrimaryButtonEnabled{false};
+
         // The touchpad pointer speed, as a number from -7 (slowest) to 7 (fastest).
         int32_t touchpadPointerSpeed{0};
 
@@ -762,6 +770,10 @@
 
         outConfig->defaultPointerDisplayId = mLocked.pointerDisplayId;
 
+        outConfig->mouseReverseVerticalScrollingEnabled =
+                mLocked.mouseReverseVerticalScrollingEnabled;
+        outConfig->mouseSwapPrimaryButtonEnabled = mLocked.mouseSwapPrimaryButtonEnabled;
+
         outConfig->touchpadPointerSpeed = mLocked.touchpadPointerSpeed;
         outConfig->touchpadNaturalScrollingEnabled = mLocked.touchpadNaturalScrollingEnabled;
         outConfig->touchpadTapToClickEnabled = mLocked.touchpadTapToClickEnabled;
@@ -1317,6 +1329,36 @@
     return mLocked.pointerSpeed;
 }
 
+void NativeInputManager::setMouseReverseVerticalScrollingEnabled(bool enabled) {
+    { // acquire lock
+        std::scoped_lock _l(mLock);
+
+        if (mLocked.mouseReverseVerticalScrollingEnabled == enabled) {
+            return;
+        }
+
+        mLocked.mouseReverseVerticalScrollingEnabled = enabled;
+    } // release lock
+
+    mInputManager->getReader().requestRefreshConfiguration(
+            InputReaderConfiguration::Change::MOUSE_SETTINGS);
+}
+
+void NativeInputManager::setMouseSwapPrimaryButtonEnabled(bool enabled) {
+    { // acquire lock
+        std::scoped_lock _l(mLock);
+
+        if (mLocked.mouseSwapPrimaryButtonEnabled == enabled) {
+            return;
+        }
+
+        mLocked.mouseSwapPrimaryButtonEnabled = enabled;
+    } // release lock
+
+    mInputManager->getReader().requestRefreshConfiguration(
+            InputReaderConfiguration::Change::MOUSE_SETTINGS);
+}
+
 void NativeInputManager::setPointerSpeed(int32_t speed) {
     { // acquire lock
         std::scoped_lock _l(mLock);
@@ -3002,6 +3044,18 @@
     return static_cast<jint>(im->getInputManager()->getReader().getLastUsedInputDeviceId());
 }
 
+static void nativeSetMouseReverseVerticalScrollingEnabled(JNIEnv* env, jobject nativeImplObj,
+                                                          bool enabled) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
+    im->setMouseReverseVerticalScrollingEnabled(enabled);
+}
+
+static void nativeSetMouseSwapPrimaryButtonEnabled(JNIEnv* env, jobject nativeImplObj,
+                                                   bool enabled) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
+    im->setMouseSwapPrimaryButtonEnabled(enabled);
+}
+
 // ----------------------------------------------------------------------------
 
 static const JNINativeMethod gInputManagerMethods[] = {
@@ -3048,6 +3102,9 @@
         {"setPointerSpeed", "(I)V", (void*)nativeSetPointerSpeed},
         {"setMousePointerAccelerationEnabled", "(IZ)V",
          (void*)nativeSetMousePointerAccelerationEnabled},
+        {"setMouseReverseVerticalScrollingEnabled", "(Z)V",
+         (void*)nativeSetMouseReverseVerticalScrollingEnabled},
+        {"setMouseSwapPrimaryButtonEnabled", "(Z)V", (void*)nativeSetMouseSwapPrimaryButtonEnabled},
         {"setTouchpadPointerSpeed", "(I)V", (void*)nativeSetTouchpadPointerSpeed},
         {"setTouchpadNaturalScrollingEnabled", "(Z)V",
          (void*)nativeSetTouchpadNaturalScrollingEnabled},
diff --git a/services/core/jni/com_android_server_utils_AnrTimer.cpp b/services/core/jni/com_android_server_utils_AnrTimer.cpp
index 2836d46..2add5b0 100644
--- a/services/core/jni/com_android_server_utils_AnrTimer.cpp
+++ b/services/core/jni/com_android_server_utils_AnrTimer.cpp
@@ -349,7 +349,7 @@
         return nullptr;
     }
 
-    // Return the currently watched pids.  The lock must be held.
+    // Return the currently watched pids as a comma-separated list.  The lock must be held.
     std::string watchedPidsLocked() const {
         if (watched_.size() == 0) return "none";
         bool first = true;
@@ -357,6 +357,7 @@
         for (auto i = watched_.cbegin(); i != watched_.cend(); i++) {
             if (first) {
                 result += StringPrintf("%d", *i);
+                first = false;
             } else {
                 result += StringPrintf(",%d", *i);
             }
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
index 1a1c8e5..94eab9c 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -25,6 +25,7 @@
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION;
+import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED;
 import static android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY;
 import static android.view.ContentRecordingSession.RECORD_CONTENT_TASK;
 import static android.view.Display.HdrCapabilities.HDR_TYPE_INVALID;
@@ -1068,9 +1069,9 @@
                 firstDisplayId);
     }
 
-    /** Tests that the virtual device is created in a device display group. */
+    /** Tests that a trusted virtual display is created in a device display group. */
     @Test
-    public void createVirtualDisplay_addsDisplaysToDeviceDisplayGroups() throws Exception {
+    public void createVirtualDisplay_addsTrustedDisplaysToDeviceDisplayGroups() throws Exception {
         DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
         DisplayManagerInternal localService = displayManager.new LocalService();
 
@@ -1081,12 +1082,16 @@
         IVirtualDevice virtualDevice = mock(IVirtualDevice.class);
         when(virtualDevice.getDeviceId()).thenReturn(1);
         when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true);
+
+        when(mContext.checkCallingPermission(ADD_TRUSTED_DISPLAY))
+                .thenReturn(PackageManager.PERMISSION_GRANTED);
+
         // Create a first virtual display. A display group should be created for this display on the
         // virtual device.
         final VirtualDisplayConfig.Builder builder1 =
                 new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
-                        .setUniqueId("uniqueId --- device display group 1");
-
+                        .setUniqueId("uniqueId --- device display group")
+                        .setFlags(VIRTUAL_DISPLAY_FLAG_TRUSTED);
         int displayId1 =
                 localService.createVirtualDisplay(
                         builder1.build(),
@@ -1097,12 +1102,14 @@
         verify(mMockProjectionService, never()).setContentRecordingSession(any(),
                 nullable(IMediaProjection.class));
         int displayGroupId1 = localService.getDisplayInfo(displayId1).displayGroupId;
+        assertNotEquals(displayGroupId1, Display.DEFAULT_DISPLAY_GROUP);
 
         // Create a second virtual display. This should be added to the previously created display
         // group.
         final VirtualDisplayConfig.Builder builder2 =
                 new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
-                        .setUniqueId("uniqueId --- device display group 1");
+                        .setUniqueId("uniqueId --- device display group")
+                        .setFlags(VIRTUAL_DISPLAY_FLAG_TRUSTED);
 
         int displayId2 =
                 localService.createVirtualDisplay(
@@ -1121,6 +1128,36 @@
                 displayGroupId2);
     }
 
+    /** Tests that an untrusted virtual display is created in the default display group. */
+    @Test
+    public void createVirtualDisplay_addsUntrustedDisplayToDefaultDisplayGroups() throws Exception {
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        DisplayManagerInternal localService = displayManager.new LocalService();
+
+        registerDefaultDisplays(displayManager);
+        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
+
+        IVirtualDevice virtualDevice = mock(IVirtualDevice.class);
+        when(virtualDevice.getDeviceId()).thenReturn(1);
+        when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true);
+        // Create the virtual display. It is untrusted, so it should go into the default group.
+        final VirtualDisplayConfig.Builder builder =
+                new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
+                        .setUniqueId("uniqueId --- device display group");
+
+        int displayId =
+                localService.createVirtualDisplay(
+                        builder.build(),
+                        mMockAppToken /* callback */,
+                        virtualDevice /* virtualDeviceToken */,
+                        mock(DisplayWindowPolicyController.class),
+                        PACKAGE_NAME);
+        verify(mMockProjectionService, never()).setContentRecordingSession(any(),
+                nullable(IMediaProjection.class));
+        int displayGroupId = localService.getDisplayInfo(displayId).displayGroupId;
+        assertEquals(displayGroupId, Display.DEFAULT_DISPLAY_GROUP);
+    }
+
     /**
      * Tests that the virtual display is not added to the device display group when
      * VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP is set.
@@ -1138,11 +1175,15 @@
         when(virtualDevice.getDeviceId()).thenReturn(1);
         when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true);
 
+        when(mContext.checkCallingPermission(ADD_TRUSTED_DISPLAY))
+                .thenReturn(PackageManager.PERMISSION_GRANTED);
+
         // Create a first virtual display. A display group should be created for this display on the
         // virtual device.
         final VirtualDisplayConfig.Builder builder1 =
                 new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
-                        .setUniqueId("uniqueId --- device display group");
+                        .setUniqueId("uniqueId --- device display group")
+                        .setFlags(VIRTUAL_DISPLAY_FLAG_TRUSTED);
 
         int displayId1 =
                 localService.createVirtualDisplay(
@@ -1154,12 +1195,14 @@
         verify(mMockProjectionService, never()).setContentRecordingSession(any(),
                 nullable(IMediaProjection.class));
         int displayGroupId1 = localService.getDisplayInfo(displayId1).displayGroupId;
+        assertNotEquals(displayGroupId1, Display.DEFAULT_DISPLAY_GROUP);
 
         // Create a second virtual display. With the flag VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP,
         // the display should not be added to the previously created display group.
         final VirtualDisplayConfig.Builder builder2 =
                 new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
-                        .setFlags(VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP)
+                        .setFlags(VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP
+                                | VIRTUAL_DISPLAY_FLAG_TRUSTED)
                         .setUniqueId("uniqueId --- own display group");
 
         when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true);
@@ -1174,6 +1217,7 @@
         verify(mMockProjectionService, never()).setContentRecordingSession(any(),
                 nullable(IMediaProjection.class));
         int displayGroupId2 = localService.getDisplayInfo(displayId2).displayGroupId;
+        assertNotEquals(displayGroupId2, Display.DEFAULT_DISPLAY_GROUP);
 
         assertNotEquals(
                 "Display 1 should be in the device display group and display 2 in its own display"
@@ -1208,7 +1252,8 @@
         final VirtualDisplayConfig deviceDisplayGroupDisplayConfig =
                 new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
                         .setUniqueId("uniqueId --- device display group 1")
-                        .setFlags(VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED)
+                        .setFlags(VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED
+                                | VIRTUAL_DISPLAY_FLAG_TRUSTED)
                         .build();
 
         int deviceDisplayGroupDisplayId =
@@ -1235,6 +1280,7 @@
                         .setUniqueId("uniqueId --- own display group 1")
                         .setFlags(
                                 VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED
+                                        | VIRTUAL_DISPLAY_FLAG_TRUSTED
                                         | VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP)
                         .build();
 
@@ -1852,7 +1898,7 @@
 
     /**
      * Tests that specifying VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP is allowed when the permission
-     * ADD_TRUSTED_DISPLAY is granted.
+     * ADD_TRUSTED_DISPLAY is granted and that display is not in the default display group.
      */
     @Test
     public void testOwnDisplayGroup_allowCreationWithAddTrustedDisplayPermission()
@@ -1881,6 +1927,9 @@
         DisplayDeviceInfo ddi = displayManager.getDisplayDeviceInfoInternal(displayId);
         assertNotNull(ddi);
         assertNotEquals(0, ddi.flags & DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP);
+
+        int displayGroupId = bs.getDisplayInfo(displayId).displayGroupId;
+        assertNotEquals(displayGroupId, Display.DEFAULT_DISPLAY_GROUP);
     }
 
     /**
@@ -1915,11 +1964,11 @@
     }
 
     /**
-     * Tests that specifying VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP is allowed when called with
-     * a virtual device, even if ADD_TRUSTED_DISPLAY is not granted.
+     * Tests that specifying VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP is not allowed when called with
+     * a virtual device, if ADD_TRUSTED_DISPLAY is not granted.
      */
     @Test
-    public void testOwnDisplayGroup_allowCreationWithVirtualDevice() throws Exception {
+    public void testOwnDisplayGroup_disallowCreationWithVirtualDevice() throws Exception {
         DisplayManagerService displayManager =
                 new DisplayManagerService(mContext, mBasicInjector);
         DisplayManagerInternal localService = displayManager.new LocalService();
@@ -1940,16 +1989,16 @@
         when(virtualDevice.getDeviceId()).thenReturn(1);
         when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true);
 
-        int displayId = localService.createVirtualDisplay(builder.build(),
-                mMockAppToken /* callback */, virtualDevice /* virtualDeviceToken */,
-                mock(DisplayWindowPolicyController.class), PACKAGE_NAME);
-        verify(mMockProjectionService, never()).setContentRecordingSession(any(),
-                nullable(IMediaProjection.class));
-        performTraversalInternal(displayManager);
-        displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */);
-        DisplayDeviceInfo ddi = displayManager.getDisplayDeviceInfoInternal(displayId);
-        assertNotNull(ddi);
-        assertNotEquals(0, ddi.flags & DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP);
+        try {
+            localService.createVirtualDisplay(builder.build(),
+                    mMockAppToken /* callback */, virtualDevice /* virtualDeviceToken */,
+                    mock(DisplayWindowPolicyController.class), PACKAGE_NAME);
+            fail("Creating virtual display with VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP without "
+                    + "ADD_TRUSTED_DISPLAY permission should throw SecurityException even if "
+                    + "called with a virtual device.");
+        } catch (SecurityException e) {
+            // SecurityException is expected
+        }
     }
 
     /**
diff --git a/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java b/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java
index f728168..782262d 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java
@@ -18,6 +18,7 @@
 
 import static android.hardware.display.DisplayManagerGlobal.EVENT_DISPLAY_CONNECTED;
 import static android.view.Display.TYPE_EXTERNAL;
+import static android.view.Display.TYPE_INTERNAL;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -36,6 +37,7 @@
 import android.os.IThermalService;
 import android.os.RemoteException;
 import android.os.Temperature;
+import android.view.Display;
 import android.view.DisplayInfo;
 
 import androidx.test.filters.SmallTest;
@@ -97,6 +99,8 @@
     @Mock
     private LogicalDisplay mMockedLogicalDisplay;
     @Mock
+    private LogicalDisplay mMockedDefaultDisplay;
+    @Mock
     private DisplayNotificationManager mMockedDisplayNotificationManager;
     @Mock
     private ExternalDisplayStatsService mMockedExternalDisplayStatsService;
@@ -141,6 +145,15 @@
         when(mMockedLogicalDisplay.getDisplayInfoLocked()).thenReturn(mockedLogicalDisplayInfo);
         when(mMockedLogicalDisplayMapper.getDisplayLocked(EXTERNAL_DISPLAY_ID)).thenReturn(
                 mMockedLogicalDisplay);
+
+        // Initialize default logical display
+        when(mMockedDefaultDisplay.getDisplayIdLocked()).thenReturn(Display.DEFAULT_DISPLAY);
+        when(mMockedDefaultDisplay.isEnabledLocked()).thenReturn(true);
+        final var mockedDefaultDisplayInfo = new DisplayInfo();
+        mockedDefaultDisplayInfo.type = TYPE_INTERNAL;
+        when(mMockedDefaultDisplay.getDisplayInfoLocked()).thenReturn(mockedDefaultDisplayInfo);
+        when(mMockedLogicalDisplayMapper.getDisplayLocked(Display.DEFAULT_DISPLAY)).thenReturn(
+                mMockedDefaultDisplay);
     }
 
     @Test
@@ -293,6 +306,52 @@
         verify(mMockedLogicalDisplayMapper, never()).forEachLocked(any());
     }
 
+    @Test
+    public void testMirroringAlwaysConfirmedByUser_flagDisabled() {
+        when(mMockedFlags.isWaitingConfirmationBeforeMirroringEnabled()).thenReturn(false);
+        assertThat(mExternalDisplayPolicy.isDisplayReadyForMirroring(EXTERNAL_DISPLAY_ID)).isTrue();
+    }
+
+    @Test
+    public void testMirroringConfirmed_afterBootForEnabledDisplay() {
+        when(mMockedFlags.isWaitingConfirmationBeforeMirroringEnabled()).thenReturn(true);
+        mExternalDisplayPolicy.onBootCompleted();
+        assertThat(mExternalDisplayPolicy.isDisplayReadyForMirroring(EXTERNAL_DISPLAY_ID))
+                .isTrue();
+    }
+
+    @Test
+    public void testMirroringNotConfirmed_afterBootForDisabledDisplay() {
+        when(mMockedFlags.isWaitingConfirmationBeforeMirroringEnabled()).thenReturn(true);
+        mExternalDisplayPolicy.onBootCompleted();
+        when(mMockedLogicalDisplay.isEnabledLocked()).thenReturn(false);
+        assertThat(mExternalDisplayPolicy.isDisplayReadyForMirroring(EXTERNAL_DISPLAY_ID))
+                .isFalse();
+    }
+
+    @Test
+    public void testMirroringNeverConfirmed_forNonExternalDisplays() {
+        when(mMockedFlags.isWaitingConfirmationBeforeMirroringEnabled()).thenReturn(true);
+        mExternalDisplayPolicy.onBootCompleted();
+        assertThat(mExternalDisplayPolicy.isDisplayReadyForMirroring(Display.DEFAULT_DISPLAY))
+                .isFalse();
+    }
+
+    @Test
+    public void testMirroringNeverConfirmed_forNonExistingDisplays() {
+        when(mMockedFlags.isWaitingConfirmationBeforeMirroringEnabled()).thenReturn(true);
+        mExternalDisplayPolicy.onBootCompleted();
+        assertThat(mExternalDisplayPolicy.isDisplayReadyForMirroring(Display.INVALID_DISPLAY))
+                .isFalse();
+    }
+
+    @Test
+    public void testMirroringNeverConfirmed_duringBoot() {
+        when(mMockedFlags.isWaitingConfirmationBeforeMirroringEnabled()).thenReturn(true);
+        assertThat(mExternalDisplayPolicy.isDisplayReadyForMirroring(EXTERNAL_DISPLAY_ID))
+                .isFalse();
+    }
+
     private void setTemperature(final IThermalEventListener thermalEventListener,
             final List<Temperature> temperature) throws RemoteException {
         for (var t : temperature) {
diff --git a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java
index 1729ad5..d831cf8 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java
@@ -121,6 +121,8 @@
             Set.of(DeviceState.PROPERTY_POWER_CONFIGURATION_TRIGGER_WAKE), Collections.emptySet());
     private static final DeviceState DEVICE_STATE_OPEN = createDeviceState(2, "Two",
             Set.of(DeviceState.PROPERTY_POWER_CONFIGURATION_TRIGGER_WAKE), Collections.emptySet());
+    private static final DeviceState DEVICE_STATE_EMULATED = createDeviceState(3, "Three",
+            Set.of(DeviceState.PROPERTY_EMULATED_ONLY), Collections.emptySet());
     private static final int FLAG_GO_TO_SLEEP_ON_FOLD = 0;
     private static final int FLAG_GO_TO_SLEEP_FLAG_SOFT_SLEEP = 2;
     private static int sNextNonDefaultDisplayId = DEFAULT_DISPLAY + 1;
@@ -686,6 +688,14 @@
     }
 
     @Test
+    public void testDeviceShouldNotBeWokenWhenExitingEmulatedState() {
+        assertFalse(mLogicalDisplayMapper.shouldDeviceBeWoken(DEVICE_STATE_OPEN,
+                DEVICE_STATE_EMULATED,
+                /* isInteractive= */false,
+                /* isBootCompleted= */true));
+    }
+
+    @Test
     public void testDeviceShouldBePutToSleep() {
         assertTrue(mLogicalDisplayMapper.shouldDeviceBePutToSleep(DEVICE_STATE_CLOSED,
                 DEVICE_STATE_OPEN,
diff --git a/services/tests/displayservicetests/src/com/android/server/display/PersistentDataStoreTest.java b/services/tests/displayservicetests/src/com/android/server/display/PersistentDataStoreTest.java
index 5676a38..6d14065 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/PersistentDataStoreTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/PersistentDataStoreTest.java
@@ -459,7 +459,6 @@
         ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
         newInjector.setReadStream(bais);
         newDataStore.loadIfNeeded();
-        assertNotNull(newDataStore.getUserPreferredRefreshRate(testDisplayDevice));
         assertEquals(85.3f, mDataStore.getUserPreferredRefreshRate(testDisplayDevice), 01.f);
         assertEquals(85.3f, newDataStore.getUserPreferredRefreshRate(testDisplayDevice), 0.1f);
     }
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
index 584fd62..40b9c61 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
@@ -25,6 +25,7 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
 import static com.android.server.job.Flags.FLAG_COUNT_QUOTA_FIX;
+import static com.android.server.job.Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_FGS_JOBS;
 import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX;
 import static com.android.server.job.JobSchedulerService.EXEMPTED_INDEX;
 import static com.android.server.job.JobSchedulerService.FREQUENT_INDEX;
@@ -303,6 +304,12 @@
         }
     }
 
+    private int getProcessStateQuotaFreeThreshold() {
+        synchronized (mQuotaController.mLock) {
+            return mQuotaController.getProcessStateQuotaFreeThreshold();
+        }
+    }
+
     private void setProcessState(int procState) {
         setProcessState(procState, mSourceUid);
     }
@@ -315,7 +322,7 @@
             final boolean contained = foregroundUids.get(uid);
             mUidObserver.onUidStateChanged(uid, procState, 0,
                     ActivityManager.PROCESS_CAPABILITY_NONE);
-            if (procState <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) {
+            if (procState <= getProcessStateQuotaFreeThreshold()) {
                 if (!contained) {
                     verify(foregroundUids, timeout(2 * SECOND_IN_MILLIS).times(1))
                             .put(eq(uid), eq(true));
@@ -1371,7 +1378,7 @@
         }
 
         setDischarging();
-        setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+        setProcessState(getProcessStateQuotaFreeThreshold());
         synchronized (mQuotaController.mLock) {
             assertEquals(timeUntilQuotaConsumedMs,
                     mQuotaController.getMaxJobExecutionTimeMsLocked((job)));
@@ -1473,7 +1480,7 @@
         }
 
         setDischarging();
-        setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+        setProcessState(getProcessStateQuotaFreeThreshold());
         synchronized (mQuotaController.mLock) {
             assertEquals(mQcConstants.EJ_LIMIT_WORKING_MS / 2,
                     mQuotaController.getMaxJobExecutionTimeMsLocked(job));
@@ -1505,7 +1512,7 @@
                 createTimingSession(sElapsedRealtimeClock.millis() - mQcConstants.EJ_WINDOW_SIZE_MS,
                         timeUsedMs, 5), true);
 
-        setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+        setProcessState(getProcessStateQuotaFreeThreshold());
         synchronized (mQuotaController.mLock) {
             assertEquals(mQcConstants.EJ_LIMIT_WORKING_MS / 2,
                     mQuotaController.getMaxJobExecutionTimeMsLocked(job));
@@ -4126,7 +4133,7 @@
         }
         advanceElapsedClock(5 * SECOND_IN_MILLIS);
         // Change to a state that should still be considered foreground.
-        setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+        setProcessState(getProcessStateQuotaFreeThreshold());
         advanceElapsedClock(5 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
             mQuotaController.maybeStopTrackingJobLocked(jobStatus, null);
@@ -4134,6 +4141,36 @@
         assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
     }
 
+    /** Tests that Timers count FOREGROUND_SERVICE jobs. */
+    @Test
+    @RequiresFlagsEnabled(FLAG_ENFORCE_QUOTA_POLICY_TO_FGS_JOBS)
+    public void testTimerTracking_Fgs() {
+        setDischarging();
+
+        JobStatus jobStatus = createJobStatus("testTimerTracking_Fgs", 1);
+        setProcessState(ActivityManager.PROCESS_STATE_BOUND_TOP);
+        synchronized (mQuotaController.mLock) {
+            mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);
+        }
+
+        assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+
+        synchronized (mQuotaController.mLock) {
+            mQuotaController.prepareForExecutionLocked(jobStatus);
+        }
+        advanceElapsedClock(5 * SECOND_IN_MILLIS);
+        // Change to FOREGROUND_SERVICE state that should count.
+        setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+        long start = JobSchedulerService.sElapsedRealtimeClock.millis();
+        advanceElapsedClock(5 * SECOND_IN_MILLIS);
+        synchronized (mQuotaController.mLock) {
+            mQuotaController.maybeStopTrackingJobLocked(jobStatus, null);
+        }
+        List<TimingSession> expected = new ArrayList<>();
+        expected.add(createTimingSession(start, 5 * SECOND_IN_MILLIS, 1));
+        assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+    }
+
     /**
      * Tests that Timers properly track sessions when switching between foreground and background
      * states.
@@ -4180,7 +4217,7 @@
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
-        setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+        setProcessState(getProcessStateQuotaFreeThreshold());
         synchronized (mQuotaController.mLock) {
             mQuotaController.prepareForExecutionLocked(jobFg3);
         }
@@ -4213,7 +4250,7 @@
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
-        setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+        setProcessState(getProcessStateQuotaFreeThreshold());
         synchronized (mQuotaController.mLock) {
             mQuotaController.prepareForExecutionLocked(jobFg3);
         }
@@ -4262,7 +4299,7 @@
         }
         assertEquals(0, stats.jobCountInRateLimitingWindow);
 
-        setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+        setProcessState(getProcessStateQuotaFreeThreshold());
         synchronized (mQuotaController.mLock) {
             mQuotaController.prepareForExecutionLocked(jobFg1);
         }
@@ -4412,7 +4449,7 @@
             mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1);
         }
         advanceElapsedClock(5 * SECOND_IN_MILLIS);
-        setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+        setProcessState(getProcessStateQuotaFreeThreshold());
         synchronized (mQuotaController.mLock) {
             mQuotaController.prepareForExecutionLocked(jobFg1);
         }
@@ -4625,7 +4662,7 @@
 
         // App still in foreground so everything should be in quota.
         advanceElapsedClock(20 * SECOND_IN_MILLIS);
-        setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+        setProcessState(getProcessStateQuotaFreeThreshold());
         assertTrue(jobTop.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
         assertTrue(jobFg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
         assertTrue(jobBg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
@@ -5901,7 +5938,7 @@
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
-        setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+        setProcessState(getProcessStateQuotaFreeThreshold());
         synchronized (mQuotaController.mLock) {
             mQuotaController.prepareForExecutionLocked(jobFg3);
         }
@@ -5935,7 +5972,7 @@
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
-        setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+        setProcessState(getProcessStateQuotaFreeThreshold());
         synchronized (mQuotaController.mLock) {
             mQuotaController.prepareForExecutionLocked(jobFg3);
         }
@@ -6056,7 +6093,7 @@
             mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1);
         }
         advanceElapsedClock(5 * SECOND_IN_MILLIS);
-        setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+        setProcessState(getProcessStateQuotaFreeThreshold());
         synchronized (mQuotaController.mLock) {
             mQuotaController.prepareForExecutionLocked(jobFg1);
         }
@@ -6534,7 +6571,7 @@
 
         // App still in foreground so everything should be in quota.
         advanceElapsedClock(20 * SECOND_IN_MILLIS);
-        setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+        setProcessState(getProcessStateQuotaFreeThreshold());
         assertTrue(jobTop2.isExpeditedQuotaApproved());
         assertTrue(jobFg.isExpeditedQuotaApproved());
         assertTrue(jobBg.isExpeditedQuotaApproved());
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index 6ede334..359755a 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -67,6 +67,7 @@
         "androidx.test.ext.junit",
         "cts-wm-util",
         "platform-compat-test-rules",
+        "platform-parametric-runner-lib",
         "mockito-target-minus-junit4",
         "mockito-kotlin2",
         "platform-test-annotations",
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
index dad45b3..8a1bb00 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -161,8 +161,8 @@
     private static final int DISPLAY_ID_1 = 2;
     private static final int DISPLAY_ID_2 = 3;
     private static final int NON_EXISTENT_DISPLAY_ID = 42;
-    private static final int DEVICE_OWNER_UID_1 = 50;
-    private static final int DEVICE_OWNER_UID_2 = 51;
+    private static final int DEVICE_OWNER_UID_1 = Process.myUid();
+    private static final int DEVICE_OWNER_UID_2 = DEVICE_OWNER_UID_1 + 1;
     private static final int UID_1 = 0;
     private static final int UID_2 = 10;
     private static final int UID_3 = 10000;
@@ -245,6 +245,8 @@
     @Mock
     private IDisplayManager mIDisplayManager;
     @Mock
+    private WindowManager mWindowManager;
+    @Mock
     private VirtualDeviceImpl.PendingTrampolineCallback mPendingTrampolineCallback;
     @Mock
     private DevicePolicyManager mDevicePolicyManagerMock;
@@ -383,8 +385,7 @@
         // Allow virtual devices to be created on the looper thread for testing.
         final InputController.DeviceCreationThreadVerifier threadVerifier = () -> true;
         mInputController = new InputController(mNativeWrapperMock,
-                new Handler(TestableLooper.get(this).getLooper()),
-                mContext.getSystemService(WindowManager.class),
+                new Handler(TestableLooper.get(this).getLooper()), mWindowManager,
                 AttributionSource.myAttributionSource(), threadVerifier);
         mCameraAccessController =
                 new CameraAccessController(mContext, mLocalService, mCameraAccessBlockedCallback);
@@ -535,7 +536,7 @@
                 .build();
         mDeviceImpl.close();
         mDeviceImpl = createVirtualDevice(VIRTUAL_DEVICE_ID_1, DEVICE_OWNER_UID_1, params);
-        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
 
         GenericWindowPolicyController gwpc =
                 mDeviceImpl.getDisplayWindowPolicyControllerForTest(DISPLAY_ID_1);
@@ -543,6 +544,21 @@
     }
 
     @Test
+    public void getDevicePolicy_customRecentsPolicy_untrustedDisplaygwpcShowsRecentsOnHostDevice() {
+        VirtualDeviceParams params = new VirtualDeviceParams
+                .Builder()
+                .setDevicePolicy(POLICY_TYPE_RECENTS, DEVICE_POLICY_CUSTOM)
+                .build();
+        mDeviceImpl.close();
+        mDeviceImpl = createVirtualDevice(VIRTUAL_DEVICE_ID_1, DEVICE_OWNER_UID_1, params);
+        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+
+        GenericWindowPolicyController gwpc =
+                mDeviceImpl.getDisplayWindowPolicyControllerForTest(DISPLAY_ID_1);
+        assertThat(gwpc.canShowTasksInHostDeviceRecents()).isTrue();
+    }
+
+    @Test
     public void getDeviceOwnerUid_oneDevice_returnsCorrectId() {
         int ownerUid = mLocalService.getDeviceOwnerUid(mDeviceImpl.getDeviceId());
         assertThat(ownerUid).isEqualTo(mDeviceImpl.getOwnerUid());
@@ -660,7 +676,7 @@
     @Test
     public void getDeviceIdsForUid_twoDevicesUidOnOne_returnsCorrectId() {
         VirtualDeviceImpl secondDevice = createVirtualDevice(VIRTUAL_DEVICE_ID_2,
-                DEVICE_OWNER_UID_2);
+                DEVICE_OWNER_UID_1);
         addVirtualDisplay(secondDevice, DISPLAY_ID_2);
 
         secondDevice.getDisplayWindowPolicyControllerForTest(DISPLAY_ID_2).onRunningAppsChanged(
@@ -675,7 +691,7 @@
     public void getDeviceIdsForUid_twoDevicesUidOnBoth_returnsCorrectId() {
         addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
         VirtualDeviceImpl secondDevice = createVirtualDevice(VIRTUAL_DEVICE_ID_2,
-                DEVICE_OWNER_UID_2);
+                DEVICE_OWNER_UID_1);
         addVirtualDisplay(secondDevice, DISPLAY_ID_2);
 
 
@@ -692,7 +708,7 @@
 
     @Test
     public void getPreferredLocaleListForApp_keyboardAttached_returnLocaleHints() {
-        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
         mDeviceImpl.createVirtualKeyboard(KEYBOARD_CONFIG, BINDER);
 
         mVdms.notifyRunningAppsChanged(mDeviceImpl.getDeviceId(), Sets.newArraySet(UID_1));
@@ -713,7 +729,7 @@
     @Test
     public void getPreferredLocaleListForApp_appOnMultipleVD_localeOnFirstVDReturned() {
         VirtualDeviceImpl secondDevice = createVirtualDevice(VIRTUAL_DEVICE_ID_2,
-                DEVICE_OWNER_UID_2);
+                DEVICE_OWNER_UID_1);
         Binder secondBinder = new Binder("secondBinder");
         VirtualKeyboardConfig firstKeyboardConfig =
                 new VirtualKeyboardConfig.Builder()
@@ -732,8 +748,8 @@
                         .setLanguageTag("fr-FR")
                         .build();
 
-        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
-        addVirtualDisplay(secondDevice, DISPLAY_ID_2);
+        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
+        addVirtualDisplay(secondDevice, DISPLAY_ID_2, Display.FLAG_TRUSTED);
 
         mDeviceImpl.createVirtualKeyboard(firstKeyboardConfig, BINDER);
         secondDevice.createVirtualKeyboard(secondKeyboardConfig, secondBinder);
@@ -751,7 +767,7 @@
         assertThat(mCameraAccessController.getObserverCount()).isEqualTo(1);
 
         VirtualDeviceImpl secondDevice =
-                createVirtualDevice(VIRTUAL_DEVICE_ID_2, DEVICE_OWNER_UID_2);
+                createVirtualDevice(VIRTUAL_DEVICE_ID_2, DEVICE_OWNER_UID_1);
         assertThat(mCameraAccessController.getObserverCount()).isEqualTo(2);
 
         mDeviceImpl.close();
@@ -910,11 +926,24 @@
     }
 
     @Test
-    public void onVirtualDisplayCreatedLocked_wakeLockIsAcquired() throws RemoteException {
+    public void onVirtualDisplayCreatedLocked_notTrustedDisplay_noWakeLockIsAcquired()
+            throws RemoteException {
         verify(mIPowerManagerMock, never()).acquireWakeLock(any(Binder.class), anyInt(),
                 nullable(String.class), nullable(String.class), nullable(WorkSource.class),
                 nullable(String.class), anyInt(), eq(null));
         addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+        TestableLooper.get(this).processAllMessages();
+        verify(mIPowerManagerMock, never()).acquireWakeLock(any(Binder.class), anyInt(),
+                nullable(String.class), nullable(String.class), nullable(WorkSource.class),
+                nullable(String.class), anyInt(), eq(null));
+    }
+
+    @Test
+    public void onVirtualDisplayCreatedLocked_wakeLockIsAcquired() throws RemoteException {
+        verify(mIPowerManagerMock, never()).acquireWakeLock(any(Binder.class), anyInt(),
+                nullable(String.class), nullable(String.class), nullable(WorkSource.class),
+                nullable(String.class), anyInt(), eq(null));
+        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
         verify(mIPowerManagerMock).acquireWakeLock(any(Binder.class), anyInt(),
                 nullable(String.class), nullable(String.class), nullable(WorkSource.class),
                 nullable(String.class), eq(DISPLAY_ID_1), eq(null));
@@ -923,7 +952,7 @@
     @Test
     public void onVirtualDisplayCreatedLocked_duplicateCalls_onlyOneWakeLockIsAcquired()
             throws RemoteException {
-        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
         assertThrows(IllegalStateException.class,
                 () -> addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1));
         TestableLooper.get(this).processAllMessages();
@@ -934,7 +963,7 @@
 
     @Test
     public void onVirtualDisplayRemovedLocked_wakeLockIsReleased() throws RemoteException {
-        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
         ArgumentCaptor<IBinder> wakeLockCaptor = ArgumentCaptor.forClass(IBinder.class);
         TestableLooper.get(this).processAllMessages();
         verify(mIPowerManagerMock).acquireWakeLock(wakeLockCaptor.capture(),
@@ -949,7 +978,7 @@
 
     @Test
     public void addVirtualDisplay_displayNotReleased_wakeLockIsReleased() throws RemoteException {
-        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
         ArgumentCaptor<IBinder> wakeLockCaptor = ArgumentCaptor.forClass(IBinder.class);
         TestableLooper.get(this).processAllMessages();
         verify(mIPowerManagerMock).acquireWakeLock(wakeLockCaptor.capture(),
@@ -970,24 +999,52 @@
     }
 
     @Test
+    public void createVirtualDpad_untrustedDisplay_failsSecurityException() {
+        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+        assertThrows(SecurityException.class,
+                () -> mDeviceImpl.createVirtualDpad(DPAD_CONFIG, BINDER));
+    }
+
+    @Test
     public void createVirtualKeyboard_noDisplay_failsSecurityException() {
         assertThrows(SecurityException.class,
                 () -> mDeviceImpl.createVirtualKeyboard(KEYBOARD_CONFIG, BINDER));
     }
 
     @Test
+    public void createVirtualKeyboard_untrustedDisplay_failsSecurityException() {
+        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+        assertThrows(SecurityException.class,
+                () -> mDeviceImpl.createVirtualKeyboard(KEYBOARD_CONFIG, BINDER));
+    }
+
+    @Test
     public void createVirtualMouse_noDisplay_failsSecurityException() {
         assertThrows(SecurityException.class,
                 () -> mDeviceImpl.createVirtualMouse(MOUSE_CONFIG, BINDER));
     }
 
     @Test
+    public void createVirtualMouse_untrustedDisplay_failsSecurityException() {
+        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+        assertThrows(SecurityException.class,
+                () -> mDeviceImpl.createVirtualMouse(MOUSE_CONFIG, BINDER));
+    }
+
+    @Test
     public void createVirtualTouchscreen_noDisplay_failsSecurityException() {
         assertThrows(SecurityException.class,
                 () -> mDeviceImpl.createVirtualTouchscreen(TOUCHSCREEN_CONFIG, BINDER));
     }
 
     @Test
+    public void createVirtualTouchscreen_untrustedDisplay_failsSecurityException() {
+        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+        assertThrows(SecurityException.class,
+                () -> mDeviceImpl.createVirtualTouchscreen(TOUCHSCREEN_CONFIG, BINDER));
+    }
+
+    @Test
     public void createVirtualTouchscreen_zeroDisplayDimension_failsIllegalArgumentException() {
         assertThrows(IllegalArgumentException.class,
                 () -> new VirtualTouchscreenConfig.Builder(
@@ -1003,7 +1060,7 @@
 
     @Test
     public void createVirtualTouchscreen_positiveDisplayDimension_successful() {
-        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
         VirtualTouchscreenConfig positiveConfig =
                 new VirtualTouchscreenConfig.Builder(
                         /* touchscrenWidth= */ 600, /* touchscreenHeight= */ 800)
@@ -1026,6 +1083,14 @@
     }
 
     @Test
+    public void createVirtualNavigationTouchpad_untrustedDisplay_failsSecurityException() {
+        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+        assertThrows(SecurityException.class,
+                () -> mDeviceImpl.createVirtualNavigationTouchpad(NAVIGATION_TOUCHPAD_CONFIG,
+                        BINDER));
+    }
+
+    @Test
     public void createVirtualNavigationTouchpad_zeroDisplayDimension_failsWithException() {
         assertThrows(IllegalArgumentException.class,
                 () -> new VirtualNavigationTouchpadConfig.Builder(
@@ -1041,7 +1106,7 @@
 
     @Test
     public void createVirtualNavigationTouchpad_positiveDisplayDimension_successful() {
-        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
         VirtualNavigationTouchpadConfig positiveConfig =
                 new VirtualNavigationTouchpadConfig.Builder(
                         /* touchpadHeight= */ 50, /* touchpadWidth= */ 50)
@@ -1065,72 +1130,8 @@
     }
 
     @Test
-    public void createVirtualDpad_noPermission_failsSecurityException() {
-        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
-        // Shell doesn't have CREATE_VIRTUAL_DEVICE permission.
-        SystemUtil.runWithShellPermissionIdentity(() ->
-                assertThrows(SecurityException.class,
-                        () -> mDeviceImpl.createVirtualDpad(DPAD_CONFIG, BINDER)));
-    }
-
-    @Test
-    public void createVirtualKeyboard_noPermission_failsSecurityException() {
-        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
-        // Shell doesn't have CREATE_VIRTUAL_DEVICE permission.
-        SystemUtil.runWithShellPermissionIdentity(() ->
-                assertThrows(SecurityException.class,
-                        () -> mDeviceImpl.createVirtualKeyboard(KEYBOARD_CONFIG, BINDER)));
-    }
-
-    @Test
-    public void createVirtualMouse_noPermission_failsSecurityException() {
-        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
-        // Shell doesn't have CREATE_VIRTUAL_DEVICE permission.
-        SystemUtil.runWithShellPermissionIdentity(() ->
-                assertThrows(SecurityException.class,
-                        () -> mDeviceImpl.createVirtualMouse(MOUSE_CONFIG, BINDER)));
-    }
-
-    @Test
-    public void createVirtualTouchscreen_noPermission_failsSecurityException() {
-        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
-        // Shell doesn't have CREATE_VIRTUAL_DEVICE permission.
-        SystemUtil.runWithShellPermissionIdentity(() ->
-                assertThrows(SecurityException.class,
-                        () -> mDeviceImpl.createVirtualTouchscreen(TOUCHSCREEN_CONFIG, BINDER)));
-    }
-
-    @Test
-    public void createVirtualNavigationTouchpad_noPermission_failsSecurityException() {
-        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
-        // Shell doesn't have CREATE_VIRTUAL_DEVICE permission.
-        SystemUtil.runWithShellPermissionIdentity(() ->
-                assertThrows(SecurityException.class,
-                        () -> mDeviceImpl.createVirtualNavigationTouchpad(
-                                NAVIGATION_TOUCHPAD_CONFIG,
-                                BINDER)));
-    }
-
-    @Test
-    public void onAudioSessionStarting_noPermission_failsSecurityException() {
-        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
-        // Shell doesn't have CREATE_VIRTUAL_DEVICE permission.
-        SystemUtil.runWithShellPermissionIdentity(() ->
-                assertThrows(SecurityException.class,
-                        () -> mDeviceImpl.onAudioSessionStarting(
-                                DISPLAY_ID_1, mRoutingCallback, mConfigChangedCallback)));
-    }
-
-    @Test
-    public void onAudioSessionEnded_noPermission_failsSecurityException() {
-        // Shell doesn't have CREATE_VIRTUAL_DEVICE permission.
-        SystemUtil.runWithShellPermissionIdentity(() ->
-                assertThrows(SecurityException.class, () -> mDeviceImpl.onAudioSessionEnded()));
-    }
-
-    @Test
     public void createVirtualDpad_hasDisplay_obtainFileDescriptor() {
-        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
         mDeviceImpl.createVirtualDpad(DPAD_CONFIG, BINDER);
         assertWithMessage("Virtual dpad should register fd when the display matches").that(
                 mInputController.getInputDeviceDescriptors()).isNotEmpty();
@@ -1140,7 +1141,7 @@
 
     @Test
     public void createVirtualKeyboard_hasDisplay_obtainFileDescriptor() {
-        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
         mDeviceImpl.createVirtualKeyboard(KEYBOARD_CONFIG, BINDER);
         assertWithMessage("Virtual keyboard should register fd when the display matches").that(
                 mInputController.getInputDeviceDescriptors()).isNotEmpty();
@@ -1150,7 +1151,7 @@
 
     @Test
     public void createVirtualKeyboard_keyboardCreated_localeUpdated() {
-        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
         mDeviceImpl.createVirtualKeyboard(KEYBOARD_CONFIG, BINDER);
         assertWithMessage("Virtual keyboard should register fd when the display matches")
                 .that(mInputController.getInputDeviceDescriptors())
@@ -1171,7 +1172,7 @@
                         .setAssociatedDisplayId(DISPLAY_ID_1)
                         .build();
 
-        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
         mDeviceImpl.createVirtualKeyboard(configWithoutExplicitLayoutInfo, BINDER);
         assertWithMessage("Virtual keyboard should register fd when the display matches")
                 .that(mInputController.getInputDeviceDescriptors())
@@ -1192,7 +1193,7 @@
 
     @Test
     public void createVirtualMouse_hasDisplay_obtainFileDescriptor() {
-        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
         mDeviceImpl.createVirtualMouse(MOUSE_CONFIG, BINDER);
         assertWithMessage("Virtual mouse should register fd when the display matches").that(
                 mInputController.getInputDeviceDescriptors()).isNotEmpty();
@@ -1202,7 +1203,7 @@
 
     @Test
     public void createVirtualTouchscreen_hasDisplay_obtainFileDescriptor() {
-        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
         mDeviceImpl.createVirtualTouchscreen(TOUCHSCREEN_CONFIG, BINDER);
         assertWithMessage("Virtual touchscreen should register fd when the display matches").that(
                 mInputController.getInputDeviceDescriptors()).isNotEmpty();
@@ -1212,7 +1213,7 @@
 
     @Test
     public void createVirtualNavigationTouchpad_hasDisplay_obtainFileDescriptor() {
-        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
         mDeviceImpl.createVirtualNavigationTouchpad(NAVIGATION_TOUCHPAD_CONFIG, BINDER);
         assertWithMessage("Virtual navigation touchpad should register fd when the display matches")
                 .that(
@@ -1472,9 +1473,9 @@
 
     @Test
     public void setShowPointerIcon_setsValueForAllDisplays() {
-        addVirtualDisplay(mDeviceImpl, 1);
-        addVirtualDisplay(mDeviceImpl, 2);
-        addVirtualDisplay(mDeviceImpl, 3);
+        addVirtualDisplay(mDeviceImpl, 1, Display.FLAG_TRUSTED);
+        addVirtualDisplay(mDeviceImpl, 2, Display.FLAG_TRUSTED);
+        addVirtualDisplay(mDeviceImpl, 3, Display.FLAG_TRUSTED);
         VirtualMouseConfig config1 = new VirtualMouseConfig.Builder()
                 .setAssociatedDisplayId(1)
                 .setInputDeviceName(DEVICE_NAME_1)
@@ -1507,6 +1508,14 @@
     }
 
     @Test
+    public void setShowPointerIcon_untrustedDisplay_pointerIconIsAlwaysShown() {
+        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+        clearInvocations(mInputManagerInternalMock);
+        mDeviceImpl.setShowPointerIcon(false);
+        verify(mInputManagerInternalMock, times(0)).setPointerIconVisible(eq(false), anyInt());
+    }
+
+    @Test
     public void openNonBlockedAppOnVirtualDisplay_doesNotStartBlockedAlertActivity() {
         addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
         GenericWindowPolicyController gwpc = mDeviceImpl.getDisplayWindowPolicyControllerForTest(
@@ -1968,15 +1977,20 @@
     }
 
     private void addVirtualDisplay(VirtualDeviceImpl virtualDevice, int displayId) {
+        addVirtualDisplay(virtualDevice, displayId, /* flags= */ 0);
+    }
+
+    private void addVirtualDisplay(VirtualDeviceImpl virtualDevice, int displayId, int flags) {
         when(mDisplayManagerInternalMock.createVirtualDisplay(any(), eq(mVirtualDisplayCallback),
                 eq(virtualDevice), any(), any())).thenReturn(displayId);
-        virtualDevice.createVirtualDisplay(VIRTUAL_DISPLAY_CONFIG, mVirtualDisplayCallback);
         final String uniqueId = UNIQUE_ID + displayId;
         doAnswer(inv -> {
             final DisplayInfo displayInfo = new DisplayInfo();
             displayInfo.uniqueId = uniqueId;
+            displayInfo.flags = flags;
             return displayInfo;
         }).when(mDisplayManagerInternalMock).getDisplayInfo(eq(displayId));
+        virtualDevice.createVirtualDisplay(VIRTUAL_DISPLAY_CONFIG, mVirtualDisplayCallback);
         mInputManagerMockHelper.addDisplayIdMapping(uniqueId, displayId);
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/compat/PlatformCompatTest.java b/services/tests/servicestests/src/com/android/server/compat/PlatformCompatTest.java
index 9df7a36..1d07540 100644
--- a/services/tests/servicestests/src/com/android/server/compat/PlatformCompatTest.java
+++ b/services/tests/servicestests/src/com/android/server/compat/PlatformCompatTest.java
@@ -20,6 +20,7 @@
 
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyLong;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
@@ -33,6 +34,11 @@
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
 import android.os.Build;
+import android.os.Process;
+
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
 
 import androidx.test.runner.AndroidJUnit4;
 
@@ -43,6 +49,7 @@
 import com.android.server.LocalServices;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -55,6 +62,8 @@
 public class PlatformCompatTest {
     private static final String PACKAGE_NAME = "my.package";
 
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     @Mock
     private Context mContext;
     @Mock
@@ -441,4 +450,79 @@
         assertThat(mPlatformCompat.isChangeEnabled(3L, systemAppInfo)).isTrue();
         verify(mChangeReporter).reportChange(123, 3L, ChangeReporter.STATE_ENABLED, true, false);
     }
+
+    @DisableFlags(Flags.FLAG_SYSTEM_UID_TARGET_SYSTEM_SDK)
+    @Test
+    public void testSharedSystemUidFlagOff() throws Exception {
+        testSharedSystemUid(false);
+    }
+
+    @EnableFlags(Flags.FLAG_SYSTEM_UID_TARGET_SYSTEM_SDK)
+    @Test
+    public void testSharedSystemUidFlagOn() throws Exception {
+        testSharedSystemUid(true);
+    }
+
+    private void testSharedSystemUid(Boolean expectSystemUidTargetSystemSdk) throws Exception {
+        final String systemUidPackageNameTargetsR = "systemuid.package1";
+        final String systemUidPackageNameTargetsQ = "systemuid.package2";
+        final String nonSystemUidPackageNameTargetsR = "nonsystemuid.package1";
+        final String nonSystemUidPackageNameTargetsQ = "nonsystemuid.package2";
+        final int nonSystemUid = 123;
+
+        mCompatConfig =
+                CompatConfigBuilder.create(mBuildClassifier, mContext)
+                        .addEnableSinceSdkChangeWithId(Build.VERSION_CODES.R, 1L)
+                        .build();
+        mCompatConfig.forceNonDebuggableFinalForTest(true);
+        mPlatformCompat =
+                new PlatformCompat(mContext, mCompatConfig, mBuildClassifier, mChangeReporter);
+
+        ApplicationInfo systemUidAppInfo1 = ApplicationInfoBuilder.create()
+            .withPackageName(systemUidPackageNameTargetsR)
+            .withUid(Process.SYSTEM_UID)
+            .withTargetSdk(Build.VERSION_CODES.R)
+            .build();
+        when(mPackageManagerInternal.getApplicationInfo(
+                 eq(systemUidPackageNameTargetsR), anyLong(), anyInt(), anyInt()))
+            .thenReturn(systemUidAppInfo1);
+
+        ApplicationInfo systemUidAppInfo2 = ApplicationInfoBuilder.create()
+            .withPackageName(systemUidPackageNameTargetsQ)
+            .withUid(Process.SYSTEM_UID)
+            .withTargetSdk(Build.VERSION_CODES.Q)
+            .build();
+        when(mPackageManagerInternal.getApplicationInfo(
+                 eq(systemUidPackageNameTargetsQ), anyLong(), anyInt(), anyInt()))
+            .thenReturn(systemUidAppInfo2);
+
+        ApplicationInfo nonSystemUidAppInfo1 = ApplicationInfoBuilder.create()
+            .withPackageName(nonSystemUidPackageNameTargetsR)
+            .withUid(nonSystemUid)
+            .withTargetSdk(Build.VERSION_CODES.R)
+            .build();
+        when(mPackageManagerInternal.getApplicationInfo(
+                 eq(nonSystemUidPackageNameTargetsR), anyLong(), anyInt(), anyInt()))
+            .thenReturn(nonSystemUidAppInfo1);
+
+        ApplicationInfo nonSystemUidAppInfo2 = ApplicationInfoBuilder.create()
+            .withPackageName(nonSystemUidPackageNameTargetsQ)
+            .withUid(nonSystemUid)
+            .withTargetSdk(Build.VERSION_CODES.Q)
+            .build();
+        when(mPackageManagerInternal.getApplicationInfo(
+                 eq(nonSystemUidPackageNameTargetsQ), anyLong(), anyInt(), anyInt()))
+            .thenReturn(nonSystemUidAppInfo2);
+
+        when(mPackageManager.getPackagesForUid(eq(Process.SYSTEM_UID)))
+            .thenReturn(new String[] {systemUidPackageNameTargetsR, systemUidPackageNameTargetsQ});
+        when(mPackageManager.getPackagesForUid(eq(nonSystemUid)))
+            .thenReturn(new String[] {
+                            nonSystemUidPackageNameTargetsR, nonSystemUidPackageNameTargetsQ
+                        });
+
+        assertThat(mPlatformCompat.isChangeEnabledByUid(1L, Process.SYSTEM_UID))
+            .isEqualTo(expectSystemUidTargetSystemSdk);
+        assertThat(mPlatformCompat.isChangeEnabledByUid(1L, nonSystemUid)).isFalse();
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java
index b565f4b..ab5a5a9 100644
--- a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java
@@ -35,17 +35,20 @@
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.FlagsParameterization;
+import android.platform.test.flag.junit.SetFlagsRule;
 
 import androidx.annotation.NonNull;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.FlakyTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.compatibility.common.util.PollingCheck;
 import com.android.server.wm.ActivityTaskManagerInternal;
 import com.android.server.wm.WindowProcessController;
+import com.android.window.flags.Flags;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -58,6 +61,9 @@
 
 import javax.annotation.Nullable;
 
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
 /**
  * Unit tests for {@link DeviceStateManagerService}.
  *
@@ -65,7 +71,7 @@
  * atest FrameworksServicesTests:DeviceStateManagerServiceTest
  */
 @Presubmit
-@RunWith(AndroidJUnit4.class)
+@RunWith(ParameterizedAndroidJunit4.class)
 public final class DeviceStateManagerServiceTest {
     private static final DeviceState DEFAULT_DEVICE_STATE = new DeviceState(
             new DeviceState.Configuration.Builder(0, "DEFAULT").build());
@@ -99,6 +105,14 @@
 
     private static final int TIMEOUT = 2000;
 
+    @Rule
+    public final SetFlagsRule mSetFlagsRule;
+
+    @Parameters(name = "{0}")
+    public static List<FlagsParameterization> getParams() {
+        return FlagsParameterization.allCombinationsOf(Flags.FLAG_WLINFO_ONCREATE);
+    }
+
     private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
     @NonNull
     private TestDeviceStatePolicy mPolicy;
@@ -111,13 +125,19 @@
     @NonNull
     private WindowProcessController mWindowProcessController;
 
+    public DeviceStateManagerServiceTest(FlagsParameterization flags) {
+        mSetFlagsRule = new SetFlagsRule(flags);
+    }
+
     @Before
     public void setup() {
         mProvider = new TestDeviceStateProvider();
         mPolicy = new TestDeviceStatePolicy(mContext, mProvider);
         mSysPropSetter = new TestSystemPropertySetter();
         setupDeviceStateManagerService();
-        flushHandler(); // Flush the handler to ensure the initial values are committed.
+        if (!Flags.wlinfoOncreate()) {
+            flushHandler(); // Flush the handler to ensure the initial values are committed.
+        }
     }
 
     private void setupDeviceStateManagerService() {
@@ -255,9 +275,11 @@
         final TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback();
         mService.getBinderService().registerCallback(callback);
 
-        // An initial callback will be triggered on registration, so we clear it here.
-        flushHandler();
-        callback.clearLastNotifiedInfo();
+        if (!Flags.wlinfoOncreate()) {
+            // An initial callback will be triggered on registration, so we clear it here.
+            flushHandler();
+            callback.clearLastNotifiedInfo();
+        }
 
         assertThat(mService.getCommittedState()).hasValue(DEFAULT_DEVICE_STATE);
         assertThat(mService.getPendingState()).isEmpty();
@@ -301,7 +323,9 @@
         mProvider = new TestDeviceStateProvider(null /* initialState */);
         mPolicy = new TestDeviceStatePolicy(mContext, mProvider);
         setupDeviceStateManagerService();
-        flushHandler(); // Flush the handler to ensure the initial values are committed.
+        if (!Flags.wlinfoOncreate()) {
+            flushHandler(); // Flush the handler to ensure the initial values are committed.
+        }
 
         final DeviceStateInfo info = mService.getBinderService().getDeviceStateInfo();
 
@@ -317,6 +341,9 @@
         mService.getBinderService().registerCallback(callback);
 
         mProvider.setState(OTHER_DEVICE_STATE_IDENTIFIER);
+        if (Flags.wlinfoOncreate()) {
+            waitAndAssert(() -> callback.getLastNotifiedInfo() != null);
+        }
         waitAndAssert(() -> callback.getLastNotifiedInfo().baseState.getIdentifier()
                 == OTHER_DEVICE_STATE_IDENTIFIER);
         waitAndAssert(() -> callback.getLastNotifiedInfo().currentState.getIdentifier()
@@ -350,9 +377,14 @@
     public void registerCallback_initialValueAvailable_emitsDeviceState() throws RemoteException {
         final TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback();
 
-        mService.getBinderService().registerCallback(callback);
-        flushHandler();
-        final DeviceStateInfo stateInfo = callback.getLastNotifiedInfo();
+        final DeviceStateInfo stateInfo;
+        if (Flags.wlinfoOncreate()) {
+            stateInfo = mService.getBinderService().registerCallback(callback);
+        } else {
+            mService.getBinderService().registerCallback(callback);
+            flushHandler();
+            stateInfo = callback.getLastNotifiedInfo();
+        }
 
         assertThat(stateInfo).isNotNull();
         assertThat(stateInfo.baseState).isEqualTo(DEFAULT_DEVICE_STATE);
@@ -365,14 +397,22 @@
         mProvider = new TestDeviceStateProvider(null /* initialState */);
         mPolicy = new TestDeviceStatePolicy(mContext, mProvider);
         setupDeviceStateManagerService();
-        flushHandler(); // Flush the handler to ensure the initial values are committed.
+        if (!Flags.wlinfoOncreate()) {
+            flushHandler(); // Flush the handler to ensure the initial values are committed.
+        }
 
         final TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback();
-        mService.getBinderService().registerCallback(callback);
-        flushHandler();
-        final DeviceStateInfo stateInfo = callback.getLastNotifiedInfo();
+        final DeviceStateInfo stateInfo;
+        if (Flags.wlinfoOncreate()) {
+            // Return null when the base state is not set yet.
+            stateInfo = mService.getBinderService().registerCallback(callback);
+        } else {
+            mService.getBinderService().registerCallback(callback);
+            flushHandler();
+            // The callback should never be called when the base state is not set yet.
+            stateInfo = callback.getLastNotifiedInfo();
+        }
 
-        // The callback should never be called when the base state is not set yet.
         assertThat(stateInfo).isNull();
     }
 
@@ -380,7 +420,9 @@
     public void requestState() throws RemoteException {
         final TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback();
         mService.getBinderService().registerCallback(callback);
-        flushHandler();
+        if (!Flags.wlinfoOncreate()) {
+            flushHandler();
+        }
 
         final IBinder token = new Binder();
         assertThat(callback.getLastNotifiedStatus(token))
@@ -424,7 +466,9 @@
     public void requestState_pendingStateAtRequest() throws RemoteException {
         final TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback();
         mService.getBinderService().registerCallback(callback);
-        flushHandler();
+        if (!Flags.wlinfoOncreate()) {
+            flushHandler();
+        }
 
         mPolicy.blockConfigure();
 
@@ -498,7 +542,9 @@
     public void requestState_sameAsBaseState() throws RemoteException {
         final TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback();
         mService.getBinderService().registerCallback(callback);
-        flushHandler();
+        if (!Flags.wlinfoOncreate()) {
+            flushHandler();
+        }
 
         final IBinder token = new Binder();
         assertThat(callback.getLastNotifiedStatus(token))
@@ -516,7 +562,9 @@
     public void requestState_flagCancelWhenBaseChanges() throws RemoteException {
         final TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback();
         mService.getBinderService().registerCallback(callback);
-        flushHandler();
+        if (!Flags.wlinfoOncreate()) {
+            flushHandler();
+        }
 
         final IBinder token = new Binder();
         assertThat(callback.getLastNotifiedStatus(token))
@@ -613,7 +661,9 @@
     public void requestState_becomesUnsupported() throws RemoteException {
         final TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback();
         mService.getBinderService().registerCallback(callback);
-        flushHandler();
+        if (!Flags.wlinfoOncreate()) {
+            flushHandler();
+        }
 
         final IBinder token = new Binder();
         assertThat(callback.getLastNotifiedStatus(token))
@@ -687,7 +737,9 @@
     public void requestBaseStateOverride() throws RemoteException {
         final TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback();
         mService.getBinderService().registerCallback(callback);
-        flushHandler();
+        if (!Flags.wlinfoOncreate()) {
+            flushHandler();
+        }
 
         final IBinder token = new Binder();
         assertThat(callback.getLastNotifiedStatus(token))
@@ -833,7 +885,9 @@
     ) throws RemoteException {
         final TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback();
         mService.getBinderService().registerCallback(callback);
-        flushHandler();
+        if (!Flags.wlinfoOncreate()) {
+            flushHandler();
+        }
 
         final IBinder token = new Binder();
         assertThat(callback.getLastNotifiedStatus(token))
diff --git a/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java b/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java
index e443696..c51261f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java
@@ -52,6 +52,7 @@
 import android.os.IBinder;
 import android.platform.test.annotations.Presubmit;
 import android.view.ContentRecordingSession;
+import android.view.Display;
 import android.view.DisplayInfo;
 import android.view.Gravity;
 import android.view.SurfaceControl;
@@ -93,9 +94,11 @@
     private boolean mHandleAnisotropicDisplayMirroring = false;
 
     @Before public void setUp() {
+        mDisplayInfo.type = Display.TYPE_VIRTUAL;
         MockitoAnnotations.initMocks(this);
 
         doReturn(INVALID_DISPLAY).when(mWm.mDisplayManagerInternal).getDisplayIdToMirror(anyInt());
+        doReturn(false).when(mWm.mDisplayManagerInternal).isDisplayReadyForMirroring(anyInt());
 
         // Skip unnecessary operations of relayout.
         spyOn(mWm.mWindowPlacerLocked);
@@ -163,6 +166,25 @@
     }
 
     @Test
+    public void testUpdateRecording_externalDisplayWithoutUserConfirmation() {
+        mDisplayInfo.type = Display.TYPE_EXTERNAL;
+        defaultInit();
+        mContentRecorder.setContentRecordingSession(mDisplaySession);
+        mContentRecorder.updateRecording();
+        assertThat(mContentRecorder.isCurrentlyRecording()).isFalse();
+    }
+
+    @Test
+    public void testUpdateRecording_externalDisplayWithUserConfirmation() {
+        doReturn(true).when(mWm.mDisplayManagerInternal).isDisplayReadyForMirroring(anyInt());
+        mDisplayInfo.type = Display.TYPE_EXTERNAL;
+        defaultInit();
+        mContentRecorder.setContentRecordingSession(mDisplaySession);
+        mContentRecorder.updateRecording();
+        assertThat(mContentRecorder.isCurrentlyRecording()).isTrue();
+    }
+
+    @Test
     public void testUpdateRecording_display_invalidDisplayIdToMirror() {
         defaultInit();
         ContentRecordingSession session = ContentRecordingSession.createDisplaySession(
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicyTests.java
index 5e8f347..c8fc482 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicyTests.java
@@ -26,7 +26,6 @@
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
 
 import static org.junit.Assert.assertFalse;
@@ -73,7 +72,6 @@
         when(mMockWindowState.getRequestedVisibleTypes()).thenReturn(0);
         when(mMockActivityRecord.findMainWindow()).thenReturn(mMockWindowState);
 
-        spy(mDisplayContent);
         doReturn(mMockActivityRecord).when(mDisplayContent).topRunningActivity();
         when(mDisplayContent.getIgnoreOrientationRequest()).thenReturn(true);
 
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index 49ca6f3..44de65a 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -1965,13 +1965,14 @@
     }
 
     /**
-     * Inform whether the device is aligned with the satellite for demo mode.
+     * Inform whether the device is aligned with the satellite in both real and demo mode.
      *
-     * Framework can send datagram to modem only when device is aligned with the satellite.
-     * This method helps framework to simulate the experience of sending datagram over satellite.
+     * In demo mode, framework will send datagram to modem only when device is aligned with
+     * the satellite. This method helps framework to simulate the experience of sending datagram
+     * over satellite.
      *
-     * @param isAligned {@true} Device is aligned with the satellite for demo mode
-     *                  {@false} Device is not aligned with the satellite for demo mode
+     * @param isAligned {code @true} Device is aligned with the satellite
+     *                  {code @false} Device is not aligned with the satellite
      *
      * @throws SecurityException if the caller doesn't have required permission.
      * @throws IllegalStateException if the Telephony process is not currently available.
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 61f0146..231c8f5 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -2977,10 +2977,10 @@
     void requestTimeForNextSatelliteVisibility(in ResultReceiver receiver);
 
     /**
-     * Inform whether the device is aligned with the satellite within in margin for demo mode.
+     * Inform whether the device is aligned with the satellite in both real and demo mode.
      *
-     * @param isAligned {@true} Device is aligned with the satellite for demo mode
-     *                  {@false} Device is not aligned with the satellite for demo mode
+     * @param isAligned {@true} Device is aligned with the satellite.
+     *                  {@false} Device is not aligned with the satellite.
      */
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission("
             + "android.Manifest.permission.SATELLITE_COMMUNICATION)")
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
index 165bb57..6d8d7b7 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
@@ -27,7 +27,7 @@
 import com.android.hoststubgen.filters.KeepNativeFilter
 import com.android.hoststubgen.filters.OutputFilter
 import com.android.hoststubgen.filters.SanitizationFilter
-import com.android.hoststubgen.filters.createFilterFromTextPolicyFile
+import com.android.hoststubgen.filters.TextFileFilterPolicyParser
 import com.android.hoststubgen.filters.printAsTextPolicy
 import com.android.hoststubgen.utils.ClassFilter
 import com.android.hoststubgen.visitors.BaseAdapter
@@ -178,8 +178,10 @@
 
         // Next, "text based" filter, which allows to override polices without touching
         // the target code.
-        options.policyOverrideFile.ifSet {
-            filter = createFilterFromTextPolicyFile(it, allClasses, filter)
+        if (options.policyOverrideFiles.isNotEmpty()) {
+            val parser = TextFileFilterPolicyParser(allClasses, filter)
+            options.policyOverrideFiles.forEach(parser::parse)
+            filter = parser.createOutputFilter()
         }
 
         // Apply the implicit filter.
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
index b083d89..55e853e 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
@@ -100,7 +100,7 @@
         var defaultClassLoadHook: SetOnce<String?> = SetOnce(null),
         var defaultMethodCallHook: SetOnce<String?> = SetOnce(null),
 
-        var policyOverrideFile: SetOnce<String?> = SetOnce(null),
+        var policyOverrideFiles: MutableList<String> = mutableListOf(),
 
         var defaultPolicy: SetOnce<FilterPolicy> = SetOnce(FilterPolicy.Remove),
 
@@ -164,7 +164,7 @@
                         "--out-jar", "--out-impl-jar" -> ret.outJar.set(nextArg())
 
                         "--policy-override-file" ->
-                            ret.policyOverrideFile.set(nextArg())!!.ensureFileExists()
+                            ret.policyOverrideFiles.add(nextArg().ensureFileExists())
 
                         "--clean-up-on-error" -> ret.cleanUpOnError.set(true)
                         "--no-clean-up-on-error" -> ret.cleanUpOnError.set(false)
@@ -291,7 +291,7 @@
               annotationAllowedClassesFile=$annotationAllowedClassesFile,
               defaultClassLoadHook=$defaultClassLoadHook,
               defaultMethodCallHook=$defaultMethodCallHook,
-              policyOverrideFile=$policyOverrideFile,
+              policyOverrideFiles=${policyOverrideFiles.toTypedArray().contentToString()},
               defaultPolicy=$defaultPolicy,
               cleanUpOnError=$cleanUpOnError,
               enableClassChecker=$enableClassChecker,
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
index 073b503..caf80eb 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
@@ -23,13 +23,10 @@
 import com.android.hoststubgen.log
 import com.android.hoststubgen.normalizeTextLine
 import com.android.hoststubgen.whitespaceRegex
-import org.objectweb.asm.Opcodes
-import org.objectweb.asm.tree.ClassNode
-import java.io.BufferedReader
-import java.io.FileReader
+import java.io.File
 import java.io.PrintWriter
-import java.util.Objects
 import java.util.regex.Pattern
+import org.objectweb.asm.tree.ClassNode
 
 /**
  * Print a class node as a "keep" policy.
@@ -49,256 +46,56 @@
     }
 }
 
-/** Return true if [access] is either public or protected. */
-private fun isVisible(access: Int): Boolean {
-    return (access and (Opcodes.ACC_PUBLIC or Opcodes.ACC_PROTECTED)) != 0
-}
-
 private const val FILTER_REASON = "file-override"
 
-/**
- * Read a given "policy" file and return as an [OutputFilter]
- */
-fun createFilterFromTextPolicyFile(
-        filename: String,
-        classes: ClassNodes,
-        fallback: OutputFilter,
-        ): OutputFilter {
-    log.i("Loading offloaded annotations from $filename ...")
-    log.withIndent {
-        val subclassFilter = SubclassFilter(classes, fallback)
-        val packageFilter = PackageFilter(subclassFilter)
-        val imf = InMemoryOutputFilter(classes, packageFilter)
+private enum class SpecialClass {
+    NotSpecial,
+    Aidl,
+    FeatureFlags,
+    Sysprops,
+    RFile,
+}
 
-        var lineNo = 0
+class TextFileFilterPolicyParser(
+    private val classes: ClassNodes,
+    fallback: OutputFilter
+) {
+    private val subclassFilter = SubclassFilter(classes, fallback)
+    private val packageFilter = PackageFilter(subclassFilter)
+    private val imf = InMemoryOutputFilter(classes, packageFilter)
+    private var aidlPolicy: FilterPolicyWithReason? = null
+    private var featureFlagsPolicy: FilterPolicyWithReason? = null
+    private var syspropsPolicy: FilterPolicyWithReason? = null
+    private var rFilePolicy: FilterPolicyWithReason? = null
+    private val typeRenameSpec = mutableListOf<TextFilePolicyRemapperFilter.TypeRenameSpec>()
+    private val methodReplaceSpec =
+        mutableListOf<TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec>()
 
-        var aidlPolicy: FilterPolicyWithReason? = null
-        var featureFlagsPolicy: FilterPolicyWithReason? = null
-        var syspropsPolicy: FilterPolicyWithReason? = null
-        var rFilePolicy: FilterPolicyWithReason? = null
-        val typeRenameSpec = mutableListOf<TextFilePolicyRemapperFilter.TypeRenameSpec>()
-        val methodReplaceSpec =
-            mutableListOf<TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec>()
+    private lateinit var currentClassName: String
 
-        try {
-            BufferedReader(FileReader(filename)).use { reader ->
-                var className = ""
-
-                while (true) {
-                    var line = reader.readLine() ?: break
+    /**
+     * Read a given "policy" file and return as an [OutputFilter]
+     */
+    fun parse(file: String) {
+        log.i("Loading offloaded annotations from $file ...")
+        log.withIndent {
+            var lineNo = 0
+            try {
+                File(file).forEachLine {
                     lineNo++
-
-                    line = normalizeTextLine(line)
-
+                    val line = normalizeTextLine(it)
                     if (line.isEmpty()) {
-                        continue // skip empty lines.
+                        return@forEachLine // skip empty lines.
                     }
-
-
-                    // TODO: Method too long, break it up.
-
-                    val fields = line.split(whitespaceRegex).toTypedArray()
-                    when (fields[0].lowercase()) {
-                        "p", "package" -> {
-                            if (fields.size < 3) {
-                                throw ParseException("Package ('p') expects 2 fields.")
-                            }
-                            val name = fields[1]
-                            val rawPolicy = fields[2]
-                            if (resolveExtendingClass(name) != null) {
-                                throw ParseException("Package can't be a super class type")
-                            }
-                            if (resolveSpecialClass(name) != SpecialClass.NotSpecial) {
-                                throw ParseException("Package can't be a special class type")
-                            }
-                            if (rawPolicy.startsWith("!")) {
-                                throw ParseException("Package can't have a substitution")
-                            }
-                            if (rawPolicy.startsWith("~")) {
-                                throw ParseException("Package can't have a class load hook")
-                            }
-                            val policy = parsePolicy(rawPolicy)
-                            if (!policy.isUsableWithClasses) {
-                                throw ParseException("Package can't have policy '$policy'")
-                            }
-                            packageFilter.addPolicy(name, policy.withReason(FILTER_REASON))
-                        }
-
-                        "c", "class" -> {
-                            if (fields.size < 3) {
-                                throw ParseException("Class ('c') expects 2 fields.")
-                            }
-                            className = fields[1]
-
-                            // superClass is set when the class name starts with a "*".
-                            val superClass = resolveExtendingClass(className)
-
-                            // :aidl, etc?
-                            val classType = resolveSpecialClass(className)
-
-                            if (fields[2].startsWith("!")) {
-                                if (classType != SpecialClass.NotSpecial) {
-                                    // We could support it, but not needed at least for now.
-                                    throw ParseException(
-                                            "Special class can't have a substitution")
-                                }
-                                // It's a redirection class.
-                                val toClass = fields[2].substring(1)
-                                imf.setRedirectionClass(className, toClass)
-                            } else if (fields[2].startsWith("~")) {
-                                if (classType != SpecialClass.NotSpecial) {
-                                    // We could support it, but not needed at least for now.
-                                    throw ParseException(
-                                            "Special class can't have a class load hook")
-                                }
-                                // It's a class-load hook
-                                val callback = fields[2].substring(1)
-                                imf.setClassLoadHook(className, callback)
-                            } else {
-                                val policy = parsePolicy(fields[2])
-                                if (!policy.isUsableWithClasses) {
-                                    throw ParseException("Class can't have policy '$policy'")
-                                }
-                                Objects.requireNonNull(className)
-
-                                when (classType) {
-                                    SpecialClass.NotSpecial -> {
-                                        // TODO: Duplicate check, etc
-                                        if (superClass == null) {
-                                            imf.setPolicyForClass(
-                                                className, policy.withReason(FILTER_REASON)
-                                            )
-                                        } else {
-                                            subclassFilter.addPolicy(superClass,
-                                                policy.withReason("extends $superClass"))
-                                        }
-                                    }
-                                    SpecialClass.Aidl -> {
-                                        if (aidlPolicy != null) {
-                                            throw ParseException(
-                                                    "Policy for AIDL classes already defined")
-                                        }
-                                        aidlPolicy = policy.withReason(
-                                                "$FILTER_REASON (special-class AIDL)")
-                                    }
-                                    SpecialClass.FeatureFlags -> {
-                                        if (featureFlagsPolicy != null) {
-                                            throw ParseException(
-                                                    "Policy for feature flags already defined")
-                                        }
-                                        featureFlagsPolicy = policy.withReason(
-                                                "$FILTER_REASON (special-class feature flags)")
-                                    }
-                                    SpecialClass.Sysprops -> {
-                                        if (syspropsPolicy != null) {
-                                            throw ParseException(
-                                                    "Policy for sysprops already defined")
-                                        }
-                                        syspropsPolicy = policy.withReason(
-                                                "$FILTER_REASON (special-class sysprops)")
-                                    }
-                                    SpecialClass.RFile -> {
-                                        if (rFilePolicy != null) {
-                                            throw ParseException(
-                                                "Policy for R file already defined")
-                                        }
-                                        rFilePolicy = policy.withReason(
-                                            "$FILTER_REASON (special-class R file)")
-                                    }
-                                }
-                            }
-                        }
-
-                        "f", "field" -> {
-                            if (fields.size < 3) {
-                                throw ParseException("Field ('f') expects 2 fields.")
-                            }
-                            val name = fields[1]
-                            val policy = parsePolicy(fields[2])
-                            if (!policy.isUsableWithFields) {
-                                throw ParseException("Field can't have policy '$policy'")
-                            }
-                            Objects.requireNonNull(className)
-
-                            // TODO: Duplicate check, etc
-                            imf.setPolicyForField(className, name, policy.withReason(FILTER_REASON))
-                        }
-
-                        "m", "method" -> {
-                            if (fields.size < 4) {
-                                throw ParseException("Method ('m') expects 3 fields.")
-                            }
-                            val name = fields[1]
-                            val signature = fields[2]
-                            val policy = parsePolicy(fields[3])
-
-                            if (!policy.isUsableWithMethods) {
-                                throw ParseException("Method can't have policy '$policy'")
-                            }
-
-                            Objects.requireNonNull(className)
-
-                            imf.setPolicyForMethod(className, name, signature,
-                                    policy.withReason(FILTER_REASON))
-                            if (policy == FilterPolicy.Substitute) {
-                                val fromName = fields[3].substring(1)
-
-                                if (fromName == name) {
-                                    throw ParseException(
-                                            "Substitution must have a different name")
-                                }
-
-                                // Set the policy for the "from" method.
-                                imf.setPolicyForMethod(className, fromName, signature,
-                                    FilterPolicy.Keep.withReason(FILTER_REASON))
-
-                                val classAndMethod = splitWithLastPeriod(fromName)
-                                if (classAndMethod != null) {
-                                    // If the substitution target contains a ".", then
-                                    // it's a method call redirect.
-                                    methodReplaceSpec.add(
-                                        TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec(
-                                            className.toJvmClassName(),
-                                            name,
-                                            signature,
-                                            classAndMethod.first.toJvmClassName(),
-                                            classAndMethod.second,
-                                        )
-                                    )
-                                } else {
-                                    // It's an in-class replace.
-                                    // ("@RavenwoodReplace" equivalent)
-                                    imf.setRenameTo(className, fromName, signature, name)
-                                }
-                            }
-                        }
-                        "r", "rename" -> {
-                            if (fields.size < 3) {
-                                throw ParseException("Rename ('r') expects 2 fields.")
-                            }
-                            // Add ".*" to make it a prefix match.
-                            val pattern = Pattern.compile(fields[1] + ".*")
-
-                            // Removing the leading /'s from the prefix. This allows
-                            // using a single '/' as an empty suffix, which is useful to have a
-                            // "negative" rename rule to avoid subsequent raname's from getting
-                            // applied. (Which is needed for services.jar)
-                            val prefix = fields[2].trimStart('/')
-
-                            typeRenameSpec += TextFilePolicyRemapperFilter.TypeRenameSpec(
-                                pattern, prefix)
-                        }
-
-                        else -> {
-                            throw ParseException("Unknown directive \"${fields[0]}\"")
-                        }
-                    }
+                    parseLine(line)
                 }
+            } catch (e: ParseException) {
+                throw e.withSourceInfo(file, lineNo)
             }
-        } catch (e: ParseException) {
-            throw e.withSourceInfo(filename, lineNo)
         }
+    }
 
+    fun createOutputFilter(): OutputFilter {
         var ret: OutputFilter = imf
         if (typeRenameSpec.isNotEmpty()) {
             ret = TextFilePolicyRemapperFilter(typeRenameSpec, ret)
@@ -309,54 +106,271 @@
 
         // Wrap the in-memory-filter with AHF.
         ret = AndroidHeuristicsFilter(
-                classes, aidlPolicy, featureFlagsPolicy, syspropsPolicy, rFilePolicy, ret)
+            classes, aidlPolicy, featureFlagsPolicy, syspropsPolicy, rFilePolicy, ret
+        )
 
         return ret
     }
-}
 
-private enum class SpecialClass {
-    NotSpecial,
-    Aidl,
-    FeatureFlags,
-    Sysprops,
-    RFile,
-}
-
-private fun resolveSpecialClass(className: String): SpecialClass {
-    if (!className.startsWith(":")) {
-        return SpecialClass.NotSpecial
+    private fun parseLine(line: String) {
+        val fields = line.split(whitespaceRegex).toTypedArray()
+        when (fields[0].lowercase()) {
+            "p", "package" -> parsePackage(fields)
+            "c", "class" -> parseClass(fields)
+            "f", "field" -> parseField(fields)
+            "m", "method" -> parseMethod(fields)
+            "r", "rename" -> parseRename(fields)
+            else -> throw ParseException("Unknown directive \"${fields[0]}\"")
+        }
     }
-    when (className.lowercase()) {
-        ":aidl" -> return SpecialClass.Aidl
-        ":feature_flags" -> return SpecialClass.FeatureFlags
-        ":sysprops" -> return SpecialClass.Sysprops
-        ":r" -> return SpecialClass.RFile
-    }
-    throw ParseException("Invalid special class name \"$className\"")
-}
 
-private fun resolveExtendingClass(className: String): String? {
-    if (!className.startsWith("*")) {
-        return null
+    private fun resolveSpecialClass(className: String): SpecialClass {
+        if (!className.startsWith(":")) {
+            return SpecialClass.NotSpecial
+        }
+        when (className.lowercase()) {
+            ":aidl" -> return SpecialClass.Aidl
+            ":feature_flags" -> return SpecialClass.FeatureFlags
+            ":sysprops" -> return SpecialClass.Sysprops
+            ":r" -> return SpecialClass.RFile
+        }
+        throw ParseException("Invalid special class name \"$className\"")
     }
-    return className.substring(1)
-}
 
-private fun parsePolicy(s: String): FilterPolicy {
-    return when (s.lowercase()) {
-        "k", "keep" -> FilterPolicy.Keep
-        "t", "throw" -> FilterPolicy.Throw
-        "r", "remove" -> FilterPolicy.Remove
-        "kc", "keepclass" -> FilterPolicy.KeepClass
-        "i", "ignore" -> FilterPolicy.Ignore
-        "rdr", "redirect" -> FilterPolicy.Redirect
-        else -> {
-            if (s.startsWith("@")) {
-                FilterPolicy.Substitute
-            } else {
-                throw ParseException("Invalid policy \"$s\"")
+    private fun resolveExtendingClass(className: String): String? {
+        if (!className.startsWith("*")) {
+            return null
+        }
+        return className.substring(1)
+    }
+
+    private fun parsePolicy(s: String): FilterPolicy {
+        return when (s.lowercase()) {
+            "k", "keep" -> FilterPolicy.Keep
+            "t", "throw" -> FilterPolicy.Throw
+            "r", "remove" -> FilterPolicy.Remove
+            "kc", "keepclass" -> FilterPolicy.KeepClass
+            "i", "ignore" -> FilterPolicy.Ignore
+            "rdr", "redirect" -> FilterPolicy.Redirect
+            else -> {
+                if (s.startsWith("@")) {
+                    FilterPolicy.Substitute
+                } else {
+                    throw ParseException("Invalid policy \"$s\"")
+                }
             }
         }
     }
+
+    private fun parsePackage(fields: Array<String>) {
+        if (fields.size < 3) {
+            throw ParseException("Package ('p') expects 2 fields.")
+        }
+        val name = fields[1]
+        val rawPolicy = fields[2]
+        if (resolveExtendingClass(name) != null) {
+            throw ParseException("Package can't be a super class type")
+        }
+        if (resolveSpecialClass(name) != SpecialClass.NotSpecial) {
+            throw ParseException("Package can't be a special class type")
+        }
+        if (rawPolicy.startsWith("!")) {
+            throw ParseException("Package can't have a substitution")
+        }
+        if (rawPolicy.startsWith("~")) {
+            throw ParseException("Package can't have a class load hook")
+        }
+        val policy = parsePolicy(rawPolicy)
+        if (!policy.isUsableWithClasses) {
+            throw ParseException("Package can't have policy '$policy'")
+        }
+        packageFilter.addPolicy(name, policy.withReason(FILTER_REASON))
+    }
+
+    private fun parseClass(fields: Array<String>) {
+        if (fields.size < 3) {
+            throw ParseException("Class ('c') expects 2 fields.")
+        }
+        currentClassName = fields[1]
+
+        // superClass is set when the class name starts with a "*".
+        val superClass = resolveExtendingClass(currentClassName)
+
+        // :aidl, etc?
+        val classType = resolveSpecialClass(currentClassName)
+
+        if (fields[2].startsWith("!")) {
+            if (classType != SpecialClass.NotSpecial) {
+                // We could support it, but not needed at least for now.
+                throw ParseException(
+                    "Special class can't have a substitution"
+                )
+            }
+            // It's a redirection class.
+            val toClass = fields[2].substring(1)
+            imf.setRedirectionClass(currentClassName, toClass)
+        } else if (fields[2].startsWith("~")) {
+            if (classType != SpecialClass.NotSpecial) {
+                // We could support it, but not needed at least for now.
+                throw ParseException(
+                    "Special class can't have a class load hook"
+                )
+            }
+            // It's a class-load hook
+            val callback = fields[2].substring(1)
+            imf.setClassLoadHook(currentClassName, callback)
+        } else {
+            val policy = parsePolicy(fields[2])
+            if (!policy.isUsableWithClasses) {
+                throw ParseException("Class can't have policy '$policy'")
+            }
+
+            when (classType) {
+                SpecialClass.NotSpecial -> {
+                    // TODO: Duplicate check, etc
+                    if (superClass == null) {
+                        imf.setPolicyForClass(
+                            currentClassName, policy.withReason(FILTER_REASON)
+                        )
+                    } else {
+                        subclassFilter.addPolicy(
+                            superClass,
+                            policy.withReason("extends $superClass")
+                        )
+                    }
+                }
+
+                SpecialClass.Aidl -> {
+                    if (aidlPolicy != null) {
+                        throw ParseException(
+                            "Policy for AIDL classes already defined"
+                        )
+                    }
+                    aidlPolicy = policy.withReason(
+                        "$FILTER_REASON (special-class AIDL)"
+                    )
+                }
+
+                SpecialClass.FeatureFlags -> {
+                    if (featureFlagsPolicy != null) {
+                        throw ParseException(
+                            "Policy for feature flags already defined"
+                        )
+                    }
+                    featureFlagsPolicy = policy.withReason(
+                        "$FILTER_REASON (special-class feature flags)"
+                    )
+                }
+
+                SpecialClass.Sysprops -> {
+                    if (syspropsPolicy != null) {
+                        throw ParseException(
+                            "Policy for sysprops already defined"
+                        )
+                    }
+                    syspropsPolicy = policy.withReason(
+                        "$FILTER_REASON (special-class sysprops)"
+                    )
+                }
+
+                SpecialClass.RFile -> {
+                    if (rFilePolicy != null) {
+                        throw ParseException(
+                            "Policy for R file already defined"
+                        )
+                    }
+                    rFilePolicy = policy.withReason(
+                        "$FILTER_REASON (special-class R file)"
+                    )
+                }
+            }
+        }
+    }
+
+    private fun parseField(fields: Array<String>) {
+        if (fields.size < 3) {
+            throw ParseException("Field ('f') expects 2 fields.")
+        }
+        val name = fields[1]
+        val policy = parsePolicy(fields[2])
+        if (!policy.isUsableWithFields) {
+            throw ParseException("Field can't have policy '$policy'")
+        }
+        require(this::currentClassName.isInitialized)
+
+        // TODO: Duplicate check, etc
+        imf.setPolicyForField(currentClassName, name, policy.withReason(FILTER_REASON))
+    }
+
+    private fun parseMethod(fields: Array<String>) {
+        if (fields.size < 4) {
+            throw ParseException("Method ('m') expects 3 fields.")
+        }
+        val name = fields[1]
+        val signature = fields[2]
+        val policy = parsePolicy(fields[3])
+
+        if (!policy.isUsableWithMethods) {
+            throw ParseException("Method can't have policy '$policy'")
+        }
+
+        require(this::currentClassName.isInitialized)
+
+        imf.setPolicyForMethod(
+            currentClassName, name, signature,
+            policy.withReason(FILTER_REASON)
+        )
+        if (policy == FilterPolicy.Substitute) {
+            val fromName = fields[3].substring(1)
+
+            if (fromName == name) {
+                throw ParseException(
+                    "Substitution must have a different name"
+                )
+            }
+
+            // Set the policy for the "from" method.
+            imf.setPolicyForMethod(
+                currentClassName, fromName, signature,
+                FilterPolicy.Keep.withReason(FILTER_REASON)
+            )
+
+            val classAndMethod = splitWithLastPeriod(fromName)
+            if (classAndMethod != null) {
+                // If the substitution target contains a ".", then
+                // it's a method call redirect.
+                methodReplaceSpec.add(
+                    TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec(
+                        currentClassName.toJvmClassName(),
+                        name,
+                        signature,
+                        classAndMethod.first.toJvmClassName(),
+                        classAndMethod.second,
+                    )
+                )
+            } else {
+                // It's an in-class replace.
+                // ("@RavenwoodReplace" equivalent)
+                imf.setRenameTo(currentClassName, fromName, signature, name)
+            }
+        }
+    }
+
+    private fun parseRename(fields: Array<String>) {
+        if (fields.size < 3) {
+            throw ParseException("Rename ('r') expects 2 fields.")
+        }
+        // Add ".*" to make it a prefix match.
+        val pattern = Pattern.compile(fields[1] + ".*")
+
+        // Removing the leading /'s from the prefix. This allows
+        // using a single '/' as an empty suffix, which is useful to have a
+        // "negative" rename rule to avoid subsequent raname's from getting
+        // applied. (Which is needed for services.jar)
+        val prefix = fields[2].trimStart('/')
+
+        typeRenameSpec += TextFilePolicyRemapperFilter.TypeRenameSpec(
+            pattern, prefix
+        )
+    }
 }