Merge "Shortcut Helper - Use physical keyboard id when no id is specified" into main
diff --git a/Ravenwood.bp b/Ravenwood.bp
index 11da20a..159c17e 100644
--- a/Ravenwood.bp
+++ b/Ravenwood.bp
@@ -215,111 +215,37 @@
 
 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",
-    visibility: ["//visibility:private"],
-}
-
-java_library {
-    name: "services.fakes.ravenwood-jarjar",
-    installable: false,
-    srcs: [":services.fakes-sources"],
-    libs: [
-        "ravenwood-framework",
-        "services.core.ravenwood",
-    ],
-    jarjar_rules: ":ravenwood-services-jarjar-rules",
-    visibility: ["//visibility:private"],
-}
-
-java_library {
-    name: "mockito-ravenwood-prebuilt",
-    installable: false,
-    static_libs: [
-        "mockito-robolectric-prebuilt",
-    ],
-}
-
-java_library {
-    name: "inline-mockito-ravenwood-prebuilt",
-    installable: false,
-    static_libs: [
-        "inline-mockito-robolectric-prebuilt",
-    ],
 }
 
 // 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_genrule {
     name: "100-framework-minus-apex.ravenwood",
+    defaults: ["ravenwood-internal-only-visibility-genrule"],
     cmd: "cp $(in) $(out)",
     srcs: [":framework-minus-apex.ravenwood"],
     out: ["100-framework-minus-apex.ravenwood.jar"],
-    visibility: ["//visibility:private"],
 }
 
 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"],
-    visibility: ["//visibility:private"],
 }
 
 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"],
-    visibility: ["//visibility:private"],
-}
-
-android_ravenwood_libgroup {
-    name: "ravenwood-runtime",
-    libs: [
-        "100-framework-minus-apex.ravenwood",
-        "200-kxml2-android",
-
-        "ravenwood-runtime-common-ravenwood",
-
-        "android.test.mock.ravenwood",
-        "ravenwood-helper-runtime",
-        "hoststubgen-helper-runtime.ravenwood",
-        "services.core.ravenwood-jarjar",
-        "services.fakes.ravenwood-jarjar",
-
-        // Provide runtime versions of utils linked in below
-        "junit",
-        "truth",
-        "flag-junit",
-        "ravenwood-framework",
-        "ravenwood-junit-impl",
-        "ravenwood-junit-impl-flag",
-        "mockito-ravenwood-prebuilt",
-        "inline-mockito-ravenwood-prebuilt",
-
-        // It's a stub, so it should be towards the end.
-        "z00-all-updatable-modules-system-stubs",
-    ],
-    jni_libs: [
-        "libandroid_runtime",
-        "libravenwood_runtime",
-    ],
-}
-
-android_ravenwood_libgroup {
-    name: "ravenwood-utils",
-    libs: [
-        "junit",
-        "truth",
-        "flag-junit",
-        "ravenwood-framework",
-        "ravenwood-junit",
-        "mockito-ravenwood-prebuilt",
-        "inline-mockito-ravenwood-prebuilt",
-    ],
 }
diff --git a/SQLITE_OWNERS b/SQLITE_OWNERS
index 1ff72e7..783a0b6 100644
--- a/SQLITE_OWNERS
+++ b/SQLITE_OWNERS
@@ -1,2 +1,9 @@
+# Android platform SQLite owners are responsible for:
+# 1. Periodically updating libsqlite from upstream sqlite.org.
+# 2. Escalating libsqlite bug reports to upstream sqlite.org.
+# 3. Addressing bugs, performance regressions, and feature requests
+#    in Android SDK SQLite wrappers (android.database.sqlite.*).
+# 4. Reviewing proposed changes to said Android SDK SQLite wrappers.
+
 shayba@google.com
 shombert@google.com
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java b/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java
index 6840877..dc5e341 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java
@@ -206,14 +206,14 @@
      * @return true if an entry is found and the list of wakeups changes.
      */
     private boolean deleteWakeupFromUserStarts(int userId) {
-        int index;
         synchronized (mUserWakeupLock) {
-            index = mUserStarts.indexOfKey(userId);
+            final int index = mUserStarts.indexOfKey(userId);
             if (index >= 0) {
                 mUserStarts.removeAt(index);
+                return true;
             }
+            return false;
         }
-        return index >= 0;
     }
 
     /**
diff --git a/api/Android.bp b/api/Android.bp
index 3fa9c60..cd1997c 100644
--- a/api/Android.bp
+++ b/api/Android.bp
@@ -157,6 +157,7 @@
 genrule {
     name: "frameworks-base-api-system-current-compat",
     srcs: [
+        ":android.api.public.latest",
         ":android.api.system.latest",
         ":android-incompatibilities.api.system.latest",
         ":frameworks-base-api-current.txt",
@@ -165,33 +166,35 @@
     out: ["updated-baseline.txt"],
     tools: ["metalava"],
     cmd: metalava_cmd +
+        "--check-compatibility:api:released $(location :android.api.public.latest) " +
         "--check-compatibility:api:released $(location :android.api.system.latest) " +
-        "--check-compatibility:base $(location :frameworks-base-api-current.txt) " +
         "--baseline:compatibility:released $(location :android-incompatibilities.api.system.latest) " +
         "--update-baseline:compatibility:released $(genDir)/updated-baseline.txt " +
+        "$(location :frameworks-base-api-current.txt) " +
         "$(location :frameworks-base-api-system-current.txt)",
 }
 
 genrule {
     name: "frameworks-base-api-module-lib-current-compat",
     srcs: [
+        ":android.api.public.latest",
+        ":android.api.system.latest",
         ":android.api.module-lib.latest",
         ":android-incompatibilities.api.module-lib.latest",
         ":frameworks-base-api-current.txt",
+        ":frameworks-base-api-system-current.txt",
         ":frameworks-base-api-module-lib-current.txt",
     ],
     out: ["updated-baseline.txt"],
     tools: ["metalava"],
     cmd: metalava_cmd +
+        "--check-compatibility:api:released $(location :android.api.public.latest) " +
+        "--check-compatibility:api:released $(location :android.api.system.latest) " +
         "--check-compatibility:api:released $(location :android.api.module-lib.latest) " +
-        // Note: having "public" be the base of module-lib is not perfect -- it should
-        // ideally be a merged public+system (which metalava is not currently able to generate).
-        // This "base" will help when migrating from MODULE_LIBS -> public, but not when
-        // migrating from MODULE_LIBS -> system (where it needs to instead be listed as
-        // an incompatibility).
-        "--check-compatibility:base $(location :frameworks-base-api-current.txt) " +
         "--baseline:compatibility:released $(location :android-incompatibilities.api.module-lib.latest) " +
         "--update-baseline:compatibility:released $(genDir)/updated-baseline.txt " +
+        "$(location :frameworks-base-api-current.txt) " +
+        "$(location :frameworks-base-api-system-current.txt) " +
         "$(location :frameworks-base-api-module-lib-current.txt)",
 }
 
@@ -373,7 +376,6 @@
     high_mem: true, // Lots of sources => high memory use, see b/170701554
     installable: false,
     annotations_enabled: true,
-    previous_api: ":android.api.public.latest",
     merge_annotations_dirs: ["metalava-manual"],
     defaults_visibility: ["//frameworks/base/api"],
     visibility: [
diff --git a/api/StubLibraries.bp b/api/StubLibraries.bp
index 5b7e25b..12820f9 100644
--- a/api/StubLibraries.bp
+++ b/api/StubLibraries.bp
@@ -38,6 +38,9 @@
         "android-non-updatable-stubs-defaults",
         "module-classpath-stubs-defaults",
     ],
+    // Use full Android API not just the non-updatable API as the latter is incomplete
+    // and can result in incorrect behavior.
+    previous_api: ":android.api.combined.public.latest",
     check_api: {
         current: {
             api_file: ":non-updatable-current.txt",
@@ -118,6 +121,9 @@
         "module-classpath-stubs-defaults",
     ],
     flags: priv_apps,
+    // Use full Android API not just the non-updatable API as the latter is incomplete
+    // and can result in incorrect behavior.
+    previous_api: ":android.api.combined.system.latest",
     check_api: {
         current: {
             api_file: ":non-updatable-system-current.txt",
@@ -178,6 +184,9 @@
         "module-classpath-stubs-defaults",
     ],
     flags: test + priv_apps_in_stubs,
+    // Use full Android API not just the non-updatable API as the latter is incomplete
+    // and can result in incorrect behavior.
+    previous_api: ":android.api.combined.test.latest",
     check_api: {
         current: {
             api_file: ":non-updatable-test-current.txt",
@@ -257,6 +266,9 @@
         "module-classpath-stubs-defaults",
     ],
     flags: priv_apps_in_stubs + module_libs,
+    // Use full Android API not just the non-updatable API as the latter is incomplete
+    // and can result in incorrect behavior.
+    previous_api: ":android.api.combined.module-lib.latest",
     check_api: {
         current: {
             api_file: ":non-updatable-module-lib-current.txt",
@@ -571,6 +583,9 @@
     ],
     defaults: ["android-non-updatable_everything_from_text_defaults"],
     full_api_surface_stub: "android_stubs_current.from-text",
+    // Use full Android API not just the non-updatable API as the latter is incomplete
+    // and can result in incorrect behavior.
+    previous_api: ":android.api.combined.public.latest",
 }
 
 java_api_library {
@@ -582,6 +597,9 @@
     ],
     defaults: ["android-non-updatable_everything_from_text_defaults"],
     full_api_surface_stub: "android_system_stubs_current.from-text",
+    // Use full Android API not just the non-updatable API as the latter is incomplete
+    // and can result in incorrect behavior.
+    previous_api: ":android.api.combined.system.latest",
 }
 
 java_api_library {
@@ -594,6 +612,9 @@
     ],
     defaults: ["android-non-updatable_everything_from_text_defaults"],
     full_api_surface_stub: "android_test_stubs_current.from-text",
+    // Use full Android API not just the non-updatable API as the latter is incomplete
+    // and can result in incorrect behavior.
+    previous_api: ":android.api.combined.test.latest",
 }
 
 java_api_library {
@@ -606,6 +627,9 @@
     ],
     defaults: ["android-non-updatable_everything_from_text_defaults"],
     full_api_surface_stub: "android_module_lib_stubs_current_full.from-text",
+    // Use full Android API not just the non-updatable API as the latter is incomplete
+    // and can result in incorrect behavior.
+    previous_api: ":android.api.combined.module-lib.latest",
 }
 
 // This module generates a stub jar that is a union of the test and module lib
@@ -623,6 +647,8 @@
     defaults: ["android-non-updatable_everything_from_text_defaults"],
     full_api_surface_stub: "android_test_module_lib_stubs_current.from-text",
 
+    // No need to specify previous_api as this is not used for compiling against.
+
     // This module is only used for hiddenapi, and other modules should not
     // depend on this module.
     visibility: ["//visibility:private"],
@@ -922,7 +948,7 @@
         "i18n.module.public.api.stubs.source.system.api.contribution",
         "i18n.module.public.api.stubs.source.module_lib.api.contribution",
     ],
-    previous_api: ":android.api.public.latest",
+    previous_api: ":android.api.combined.module-lib.latest",
 }
 
 // Java API library definitions per API surface
diff --git a/api/api.go b/api/api.go
index 449fac6..d4db49e 100644
--- a/api/api.go
+++ b/api/api.go
@@ -478,7 +478,7 @@
 		props.Api_contributions = transformArray(
 			modules, "", fmt.Sprintf(".stubs.source%s.api.contribution", apiSuffix))
 		props.Defaults_visibility = []string{"//visibility:public"}
-		props.Previous_api = proptools.StringPtr(":android.api.public.latest")
+		props.Previous_api = proptools.StringPtr(":android.api.combined." + sdkKind.String() + ".latest")
 		ctx.CreateModule(java.DefaultsFactory, &props)
 	}
 }
diff --git a/core/api/current.txt b/core/api/current.txt
index 6e42fe3..69ead8f 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -2456,7 +2456,7 @@
     field public static final int config_longAnimTime = 17694722; // 0x10e0002
     field public static final int config_mediumAnimTime = 17694721; // 0x10e0001
     field public static final int config_shortAnimTime = 17694720; // 0x10e0000
-    field public static final int status_bar_notification_info_maxnum = 17694723; // 0x10e0003
+    field @Deprecated public static final int status_bar_notification_info_maxnum = 17694723; // 0x10e0003
   }
 
   public static final class R.interpolator {
@@ -2550,7 +2550,7 @@
     field public static final int search_go = 17039372; // 0x104000c
     field public static final int selectAll = 17039373; // 0x104000d
     field public static final int selectTextMode = 17039382; // 0x1040016
-    field public static final int status_bar_notification_info_overflow = 17039383; // 0x1040017
+    field @Deprecated public static final int status_bar_notification_info_overflow = 17039383; // 0x1040017
     field public static final int unknownName = 17039374; // 0x104000e
     field public static final int untitled = 17039375; // 0x104000f
     field @Deprecated public static final int yes = 17039379; // 0x1040013
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 5393475..9e2872f 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -11471,6 +11471,19 @@
 
 }
 
+package android.os.vibrator.persistence {
+
+  @FlaggedApi("android.os.vibrator.vibration_xml_apis") public final class ParsedVibration {
+    method @FlaggedApi("android.os.vibrator.vibration_xml_apis") @Nullable public android.os.VibrationEffect resolve(@NonNull android.os.Vibrator);
+  }
+
+  @FlaggedApi("android.os.vibrator.vibration_xml_apis") public final class VibrationXmlParser {
+    method @FlaggedApi("android.os.vibrator.vibration_xml_apis") @NonNull public static android.os.vibrator.persistence.ParsedVibration parse(@NonNull java.io.InputStream) throws java.io.IOException;
+    method @FlaggedApi("android.os.vibrator.vibration_xml_apis") @NonNull public static android.os.VibrationEffect parseVibrationEffect(@NonNull java.io.InputStream) throws java.io.IOException;
+  }
+
+}
+
 package android.permission {
 
   public final class AdminPermissionControlParams implements android.os.Parcelable {
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index d899511..1352465 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -2761,21 +2761,24 @@
 
 package android.os.vibrator.persistence {
 
-  public class ParsedVibration {
-    method @NonNull public java.util.List<android.os.VibrationEffect> getVibrationEffects();
-    method @Nullable public android.os.VibrationEffect resolve(@NonNull android.os.Vibrator);
+  @FlaggedApi("android.os.vibrator.vibration_xml_apis") public final class ParsedVibration {
+    ctor public ParsedVibration(@NonNull java.util.List<android.os.VibrationEffect>);
+    method @FlaggedApi("android.os.vibrator.vibration_xml_apis") @Nullable public android.os.VibrationEffect resolve(@NonNull android.os.Vibrator);
   }
 
-  public final class VibrationXmlParser {
-    method @Nullable public static android.os.vibrator.persistence.ParsedVibration parseDocument(@NonNull java.io.Reader) throws java.io.IOException;
-    method @Nullable public static android.os.VibrationEffect parseVibrationEffect(@NonNull java.io.Reader) throws java.io.IOException;
+  @FlaggedApi("android.os.vibrator.vibration_xml_apis") public final class VibrationXmlParser {
+    method @FlaggedApi("android.os.vibrator.vibration_xml_apis") @NonNull public static android.os.vibrator.persistence.ParsedVibration parse(@NonNull java.io.InputStream) throws java.io.IOException;
+    method @FlaggedApi("android.os.vibrator.vibration_xml_apis") @NonNull public static android.os.VibrationEffect parseVibrationEffect(@NonNull java.io.InputStream) throws java.io.IOException;
+  }
+
+  public static final class VibrationXmlParser.ParseFailedException extends java.io.IOException {
   }
 
   public final class VibrationXmlSerializer {
-    method public static void serialize(@NonNull android.os.VibrationEffect, @NonNull java.io.Writer) throws java.io.IOException, android.os.vibrator.persistence.VibrationXmlSerializer.SerializationFailedException;
+    method public static void serialize(@NonNull android.os.VibrationEffect, @NonNull java.io.Writer) throws java.io.IOException;
   }
 
-  public static final class VibrationXmlSerializer.SerializationFailedException extends java.lang.RuntimeException {
+  public static final class VibrationXmlSerializer.SerializationFailedException extends java.io.IOException {
   }
 
 }
diff --git a/core/java/android/credentials/selection/IntentFactory.java b/core/java/android/credentials/selection/IntentFactory.java
index b98a0d8..c521b96 100644
--- a/core/java/android/credentials/selection/IntentFactory.java
+++ b/core/java/android/credentials/selection/IntentFactory.java
@@ -232,7 +232,17 @@
                             oemComponentName,
                             PackageManager.ComponentInfoFlags.of(
                                     PackageManager.MATCH_SYSTEM_ONLY));
-                    if (info.enabled && info.exported) {
+                    boolean oemComponentEnabled = info.enabled;
+                    int runtimeComponentEnabledState = context.getPackageManager()
+                          .getComponentEnabledSetting(oemComponentName);
+                    if (runtimeComponentEnabledState == PackageManager
+                          .COMPONENT_ENABLED_STATE_ENABLED) {
+                          oemComponentEnabled = true;
+                    } else if (runtimeComponentEnabledState == PackageManager
+                          .COMPONENT_ENABLED_STATE_DISABLED) {
+                        oemComponentEnabled = false;
+                    }
+                    if (oemComponentEnabled && info.exported) {
                         intentResultBuilder.setOemUiUsageStatus(IntentCreationResult
                                 .OemUiUsageStatus.SUCCESS);
                         Slog.i(TAG,
diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java
index b2dcf90..91caedc 100644
--- a/core/java/android/hardware/display/DisplayManagerInternal.java
+++ b/core/java/android/hardware/display/DisplayManagerInternal.java
@@ -740,6 +740,12 @@
          */
         void onBlockingScreenOn(Runnable unblocker);
 
+        /**
+         * Called while display is turning to screen state other than state ON to notify that any
+         * pending work from the previous blockScreenOn call should have been cancelled.
+         */
+        void cancelBlockScreenOn();
+
         /** Whether auto brightness update in doze is allowed */
         boolean allowAutoBrightnessInDoze();
     }
@@ -774,6 +780,12 @@
         boolean blockScreenOn(Runnable unblocker);
 
         /**
+         * Called while display is turning to screen state other than state ON to notify that any
+         * pending work from the previous blockScreenOn call should have been cancelled.
+         */
+        void cancelBlockScreenOn();
+
+        /**
          * Get the brightness levels used to determine automatic brightness based on lux levels.
          * @param mode The auto-brightness mode
          *             (AutomaticBrightnessController.AutomaticBrightnessMode)
diff --git a/core/java/android/os/SharedMemory.java b/core/java/android/os/SharedMemory.java
index d008034..cba4423 100644
--- a/core/java/android/os/SharedMemory.java
+++ b/core/java/android/os/SharedMemory.java
@@ -25,8 +25,6 @@
 
 import dalvik.system.VMRuntime;
 
-import libcore.io.IoUtils;
-
 import java.io.Closeable;
 import java.io.FileDescriptor;
 import java.io.IOException;
@@ -65,7 +63,7 @@
 
         mMemoryRegistration = new MemoryRegistration(mSize);
         mCleaner = Cleaner.create(mFileDescriptor,
-                new Closer(mFileDescriptor, mMemoryRegistration));
+                new Closer(mFileDescriptor.getInt$(), mMemoryRegistration));
     }
 
     /**
@@ -328,20 +326,21 @@
      * Cleaner that closes the FD
      */
     private static final class Closer implements Runnable {
-        private FileDescriptor mFd;
+        private int mFd;
         private MemoryRegistration mMemoryReference;
 
-        private Closer(FileDescriptor fd, MemoryRegistration memoryReference) {
+        private Closer(int fd, MemoryRegistration memoryReference) {
             mFd = fd;
-            IoUtils.setFdOwner(mFd, this);
             mMemoryReference = memoryReference;
         }
 
         @Override
         public void run() {
-            IoUtils.closeQuietly(mFd);
-            mFd = null;
-
+            try {
+                FileDescriptor fd = new FileDescriptor();
+                fd.setInt$(mFd);
+                Os.close(fd);
+            } catch (ErrnoException e) { /* swallow error */ }
             mMemoryReference.release();
             mMemoryReference = null;
         }
diff --git a/core/java/android/os/vibrator/flags.aconfig b/core/java/android/os/vibrator/flags.aconfig
index b01ffe5..c73a422 100644
--- a/core/java/android/os/vibrator/flags.aconfig
+++ b/core/java/android/os/vibrator/flags.aconfig
@@ -42,3 +42,13 @@
         purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    namespace: "haptics"
+    name: "vibration_xml_apis"
+    description: "Enabled System APIs for vibration effect XML parser and serializer"
+    bug: "347273158"
+    metadata {
+        purpose: PURPOSE_FEATURE
+    }
+}
diff --git a/core/java/android/os/vibrator/persistence/ParsedVibration.java b/core/java/android/os/vibrator/persistence/ParsedVibration.java
index a16d21e..e5543ab 100644
--- a/core/java/android/os/vibrator/persistence/ParsedVibration.java
+++ b/core/java/android/os/vibrator/persistence/ParsedVibration.java
@@ -16,31 +16,35 @@
 
 package android.os.vibrator.persistence;
 
+import static android.os.vibrator.Flags.FLAG_VIBRATION_XML_APIS;
+
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
 import android.annotation.TestApi;
 import android.os.VibrationEffect;
 import android.os.Vibrator;
 import android.os.VibratorInfo;
 
-import com.android.internal.annotations.VisibleForTesting;
-
-import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 
 /**
- * The result of parsing a serialized vibration, which can be define by one or more
- * {@link VibrationEffect} and a resolution method.
+ * The result of parsing a serialized vibration.
+ *
+ * @see VibrationXmlParser
  *
  * @hide
  */
-@TestApi
-@SuppressLint("UnflaggedApi") // @TestApi without associated feature.
-public class ParsedVibration {
+@TestApi // This was used in CTS before the flag was introduced.
+@SystemApi
+@FlaggedApi(FLAG_VIBRATION_XML_APIS)
+public final class ParsedVibration {
     private final List<VibrationEffect> mEffects;
 
     /** @hide */
+    @TestApi
     public ParsedVibration(@NonNull List<VibrationEffect> effects) {
         mEffects = effects;
     }
@@ -49,40 +53,28 @@
     public ParsedVibration(@NonNull VibrationEffect effect) {
         mEffects = List.of(effect);
     }
+
     /**
      * Returns the first parsed vibration supported by {@code vibrator}, or {@code null} if none of
      * the parsed vibrations are supported.
      *
      * @hide
      */
-    @TestApi
+    @TestApi // This was used in CTS before the flag was introduced.
+    @SystemApi
+    @FlaggedApi(FLAG_VIBRATION_XML_APIS)
     @Nullable
     public VibrationEffect resolve(@NonNull Vibrator vibrator) {
         return resolve(vibrator.getInfo());
     }
 
     /**
-     * Returns the parsed vibrations for testing purposes.
-     *
-     * <p>Real callers should not use this method. Instead, they should resolve to a
-     * {@link VibrationEffect} via {@link #resolve(Vibrator)}.
-     *
-     * @hide
-     */
-    @TestApi
-    @VisibleForTesting
-    @NonNull
-    public List<VibrationEffect> getVibrationEffects() {
-        return Collections.unmodifiableList(mEffects);
-    }
-
-    /**
      * Same as {@link #resolve(Vibrator)}, but uses {@link VibratorInfo} instead for resolving.
      *
      * @hide
      */
     @Nullable
-    public final VibrationEffect resolve(@NonNull VibratorInfo info) {
+    public VibrationEffect resolve(@NonNull VibratorInfo info) {
         for (int i = 0; i < mEffects.size(); i++) {
             VibrationEffect effect = mEffects.get(i);
             if (info.areVibrationFeaturesSupported(effect)) {
@@ -91,4 +83,21 @@
         }
         return null;
     }
+
+    @Override
+    public boolean equals(@Nullable Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof ParsedVibration)) {
+            return false;
+        }
+        ParsedVibration other = (ParsedVibration) o;
+        return mEffects.equals(other.mEffects);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(mEffects);
+    }
 }
diff --git a/core/java/android/os/vibrator/persistence/VibrationXmlParser.java b/core/java/android/os/vibrator/persistence/VibrationXmlParser.java
index 7202d9a..e2312e0 100644
--- a/core/java/android/os/vibrator/persistence/VibrationXmlParser.java
+++ b/core/java/android/os/vibrator/persistence/VibrationXmlParser.java
@@ -16,13 +16,15 @@
 
 package android.os.vibrator.persistence;
 
+import static android.os.vibrator.Flags.FLAG_VIBRATION_XML_APIS;
+
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
 import android.annotation.TestApi;
 import android.os.VibrationEffect;
-import android.util.Slog;
 import android.util.Xml;
 
 import com.android.internal.vibrator.persistence.VibrationEffectXmlParser;
@@ -36,9 +38,12 @@
 import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
 import java.io.Reader;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -116,10 +121,10 @@
  *
  * @hide
  */
-@TestApi
-@SuppressLint("UnflaggedApi") // @TestApi without associated feature.
+@TestApi // This was used in CTS before the flag was introduced.
+@SystemApi
+@FlaggedApi(FLAG_VIBRATION_XML_APIS)
 public final class VibrationXmlParser {
-    private static final String TAG = "VibrationXmlParser";
 
     /**
      * The MIME type for a xml holding a vibration.
@@ -168,55 +173,12 @@
     }
 
     /**
-     * Parses XML content from given input stream into a {@link VibrationEffect}.
-     *
-     * <p>This method parses an XML content that contains a single, complete {@link VibrationEffect}
-     * serialization. As such, the root tag must be a "vibration" tag.
-     *
-     * <p>This parser fails silently and returns {@code null} if the content of the input stream
-     * does not follow the schema or has unsupported values.
-     *
-     * @return the {@link VibrationEffect} if parsed successfully, {@code null} otherwise.
-     * @throws IOException error reading from given {@link Reader}
-     *
-     * @hide
-     */
-    @TestApi
-    @Nullable
-    public static VibrationEffect parseVibrationEffect(@NonNull Reader reader) throws IOException {
-        return parseVibrationEffect(reader, /* flags= */ 0);
-    }
-
-    /**
-     * Parses XML content from given input stream into a {@link VibrationEffect}.
-     *
-     * <p>This method parses an XML content that contains a single, complete {@link VibrationEffect}
-     * serialization. As such, the root tag must be a "vibration" tag.
-     *
-     * <p>Same as {@link #parseVibrationEffect(Reader)}, with extra flags to control the parsing
-     * behavior.
-     *
-     * @hide
-     */
-    @Nullable
-    public static VibrationEffect parseVibrationEffect(@NonNull Reader reader, @Flags int flags)
-            throws IOException {
-        try {
-            return parseDocumentInternal(
-                    reader, flags, VibrationXmlParser::parseVibrationEffectInternal);
-        } catch (XmlParserException | XmlPullParserException e) {
-            Slog.w(TAG, "Error parsing vibration XML", e);
-            return null;
-        }
-    }
-
-    /**
      * Parses XML content from given input stream into a {@link ParsedVibration}.
      *
-     * <p>It supports both the "vibration" and "vibration-select" root tags.
+     * <p>It supports both the "vibration-effect" and "vibration-select" root tags.
      * <ul>
-     *     <li>If "vibration" is the root tag, the serialization provided through {@code reader}
-     *         should contain a valid serialization for a single vibration.
+     *     <li>If "vibration-effect" is the root tag, the serialization provided should contain a
+     *         valid serialization for a single vibration.
      *     <li>If "vibration-select" is the root tag, the serialization may contain one or more
      *         valid vibration serializations.
      * </ul>
@@ -225,36 +187,95 @@
      * vibration(s), and the caller can get a concrete {@link VibrationEffect} by resolving this
      * result to a specific vibrator.
      *
-     * <p>This parser fails silently and returns {@code null} if the content of the input does not
-     * follow the schema or has unsupported values.
+     * <p>This parser fails with an exception if the content of the input stream does not follow the
+     * schema or has unsupported values.
      *
      * @return a {@link ParsedVibration}
-     * @throws IOException error reading from given {@link Reader}
+     * @throws IOException error reading from given {@link InputStream} or parsing the content.
      *
      * @hide
      */
-    @TestApi
-    @Nullable
+    @TestApi // Replacing test APIs used in CTS before the flagged system APIs was introduced.
+    @SystemApi
+    @FlaggedApi(FLAG_VIBRATION_XML_APIS)
+    @NonNull
+    public static ParsedVibration parse(@NonNull InputStream inputStream) throws IOException {
+        return parseDocument(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
+    }
+
+    /**
+     * Parses XML content from given input stream into a single {@link VibrationEffect}.
+     *
+     * <p>This method parses an XML content that contains a single, complete {@link VibrationEffect}
+     * serialization. As such, the root tag must be a "vibration-effect" tag.
+     *
+     * <p>This parser fails with an exception if the content of the input stream does not follow the
+     * schema or has unsupported values.
+     *
+     * @return the parsed {@link VibrationEffect}
+     * @throws IOException error reading from given {@link InputStream} or parsing the content.
+     *
+     * @hide
+     */
+    @TestApi // Replacing test APIs used in CTS before the flagged system APIs was introduced.
+    @SystemApi
+    @FlaggedApi(FLAG_VIBRATION_XML_APIS)
+    @NonNull
+    public static VibrationEffect parseVibrationEffect(@NonNull InputStream inputStream)
+            throws IOException {
+        return parseVibrationEffect(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
+    }
+
+    /**
+     * Parses XML content from given {@link Reader} into a {@link VibrationEffect}.
+     *
+     * <p>Same as {@link #parseVibrationEffect(InputStream)}, but with a {@link Reader}.
+     *
+     * @hide
+     */
+    @NonNull
+    public static VibrationEffect parseVibrationEffect(@NonNull Reader reader) throws IOException {
+        return parseVibrationEffect(reader, /* flags= */ 0);
+    }
+
+    /**
+     * Parses XML content from given {@link Reader} into a {@link VibrationEffect}.
+     *
+     * <p>Same as {@link #parseVibrationEffect(Reader)}, with extra flags to control the parsing
+     * behavior.
+     *
+     * @hide
+     */
+    @NonNull
+    public static VibrationEffect parseVibrationEffect(@NonNull Reader reader, @Flags int flags)
+            throws IOException {
+        return parseDocumentInternal(reader, flags,
+                VibrationXmlParser::parseVibrationEffectInternal);
+    }
+
+    /**
+     * Parses XML content from given {@link Reader} into a {@link ParsedVibration}.
+     *
+     * <p>Same as {@link #parse(InputStream)}, but with a {@link Reader}.
+     *
+     * @hide
+     */
+    @NonNull
     public static ParsedVibration parseDocument(@NonNull Reader reader) throws IOException {
         return parseDocument(reader, /* flags= */ 0);
     }
 
     /**
-     * Parses XML content from given input stream into a {@link ParsedVibration}.
+     * Parses XML content from given {@link Reader} into a {@link ParsedVibration}.
      *
      * <p>Same as {@link #parseDocument(Reader)}, with extra flags to control the parsing behavior.
      *
      * @hide
      */
-    @Nullable
+    @NonNull
     public static ParsedVibration parseDocument(@NonNull Reader reader, @Flags int flags)
             throws IOException {
-        try {
-            return parseDocumentInternal(reader, flags, VibrationXmlParser::parseElementInternal);
-        } catch (XmlParserException | XmlPullParserException e) {
-            Slog.w(TAG, "Error parsing vibration/vibration-select XML", e);
-            return null;
-        }
+        return parseDocumentInternal(reader, flags, VibrationXmlParser::parseElementInternal);
     }
 
     /**
@@ -262,7 +283,7 @@
      * {@link ParsedVibration}.
      *
      * <p>Same as {@link #parseDocument(Reader, int)}, but, instead of parsing the full XML content,
-     * it takes a parser that points to either a <vibration-effect> or a <vibration-select> start
+     * it takes a parser that points to either a "vibration-effect" or a "vibration-select" start
      * tag. No other parser position, including start of document, is considered valid.
      *
      * <p>This method parses until an end "vibration-effect" or "vibration-select" tag (depending
@@ -270,37 +291,22 @@
      * will point to the end tag.
      *
      * @throws IOException error parsing from given {@link TypedXmlPullParser}.
-     * @throws VibrationXmlParserException if the XML tag cannot be parsed into a
-     *      {@link ParsedVibration}. The given {@code parser} might be pointing to a child XML tag
-     *      that caused the parser failure.
+     *         The given {@code parser} might be pointing to a child XML tag that caused the parser
+     *         failure.
      *
      * @hide
      */
     @NonNull
     public static ParsedVibration parseElement(@NonNull TypedXmlPullParser parser, @Flags int flags)
-            throws IOException, VibrationXmlParserException {
+            throws IOException {
         try {
             return parseElementInternal(parser, flags);
         } catch (XmlParserException e) {
-            throw new VibrationXmlParserException("Error parsing vibration-select.", e);
+            throw new ParseFailedException(e);
         }
     }
 
-    /**
-     * Represents an error while parsing a vibration XML input.
-     *
-     * @hide
-     */
-    public static final class VibrationXmlParserException extends Exception {
-        private VibrationXmlParserException(String message, Throwable cause) {
-            super(message, cause);
-        }
-
-        private VibrationXmlParserException(String message) {
-            super(message);
-        }
-    }
-
+    @NonNull
     private static ParsedVibration parseElementInternal(
                 @NonNull TypedXmlPullParser parser, @Flags int flags)
                         throws IOException, XmlParserException {
@@ -313,11 +319,12 @@
             case XmlConstants.TAG_VIBRATION_SELECT:
                 return parseVibrationSelectInternal(parser, flags);
             default:
-                throw new XmlParserException(
-                        "Unexpected tag name when parsing element: " + tagName);
+                throw new ParseFailedException(
+                        "Unexpected tag " + tagName + " when parsing a vibration");
         }
     }
 
+    @NonNull
     private static ParsedVibration parseVibrationSelectInternal(
             @NonNull TypedXmlPullParser parser, @Flags int flags)
                     throws IOException, XmlParserException {
@@ -332,7 +339,7 @@
         return new ParsedVibration(effects);
     }
 
-    /** Parses a single XML element for "vibration" tag into a {@link VibrationEffect}. */
+    @NonNull
     private static VibrationEffect parseVibrationEffectInternal(
             @NonNull TypedXmlPullParser parser, @Flags int flags)
                     throws IOException, XmlParserException {
@@ -347,32 +354,60 @@
      * This method parses a whole XML document (provided through a {@link Reader}). The root tag is
      * parsed as per a provided {@link ElementParser}.
      */
+    @NonNull
     private static <T> T parseDocumentInternal(
             @NonNull Reader reader, @Flags int flags, ElementParser<T> parseLogic)
-                    throws IOException, XmlParserException, XmlPullParserException {
-        TypedXmlPullParser parser = Xml.newFastPullParser();
-        parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
-        parser.setInput(reader);
+            throws IOException {
+        try {
+            TypedXmlPullParser parser = Xml.newFastPullParser();
+            parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+            parser.setInput(reader);
 
-        // Ensure XML starts with a document start tag.
-        XmlReader.readDocumentStart(parser);
+            // Ensure XML starts with a document start tag.
+            XmlReader.readDocumentStart(parser);
 
-        // Parse root tag.
-        T result = parseLogic.parse(parser, flags);
+            // Parse root tag.
+            T result = parseLogic.parse(parser, flags);
 
-        // Ensure XML ends after root tag is consumed.
-        XmlReader.readDocumentEndTag(parser);
+            // Ensure XML ends after root tag is consumed.
+            XmlReader.readDocumentEndTag(parser);
 
-        return result;
+            return result;
+        } catch (XmlPullParserException e) {
+            throw new ParseFailedException("Error initializing XMLPullParser", e);
+        } catch (XmlParserException e) {
+            throw new ParseFailedException(e);
+        }
     }
 
     /** Encapsulate a logic to parse an XML element from an open parser. */
     private interface ElementParser<T> {
         /** Parses a single XML element starting from the current position of the {@code parser}. */
+        @NonNull
         T parse(@NonNull TypedXmlPullParser parser, @Flags int flags)
                 throws IOException, XmlParserException;
     }
 
+    /**
+     * Represents an error while parsing a vibration XML input.
+     *
+     * @hide
+     */
+    @TestApi
+    public static final class ParseFailedException extends IOException {
+        private ParseFailedException(String message) {
+            super(message);
+        }
+
+        private ParseFailedException(XmlParserException parserException) {
+            this(parserException.getMessage(), parserException);
+        }
+
+        private ParseFailedException(String message, Throwable cause) {
+            super(message, cause);
+        }
+    }
+
     private VibrationXmlParser() {
     }
 }
diff --git a/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java b/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java
index 2065d5d..a26c6f4 100644
--- a/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java
+++ b/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java
@@ -18,9 +18,7 @@
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
-import android.annotation.SuppressLint;
 import android.annotation.TestApi;
-import android.os.CombinedVibration;
 import android.os.VibrationEffect;
 import android.util.Xml;
 
@@ -37,14 +35,13 @@
 import java.lang.annotation.RetentionPolicy;
 
 /**
- * Serializes {@link CombinedVibration} and {@link VibrationEffect} instances to XML.
+ * Serializes {@link VibrationEffect} instances to XML.
  *
  * <p>This uses the same schema expected by the {@link VibrationXmlParser}.
  *
  * @hide
  */
 @TestApi
-@SuppressLint("UnflaggedApi") // @TestApi without associated feature.
 public final class VibrationXmlSerializer {
 
     /**
@@ -80,20 +77,19 @@
             "http://xmlpull.org/v1/doc/features.html#indent-output";
 
     /**
-     * Serializes a {@link VibrationEffect} to XML and writes output to given {@link Writer}.
+     * Serializes a {@link VibrationEffect} to XML and writes output to given {@link Writer} using
+     * UTF-8 encoding.
      *
-     * <p>This method will only write into the {@link Writer} if the effect can successfully
-     * be represented by the XML serialization. It will throw an exception otherwise.
+     * <p>This method will only write to the stream if the effect can successfully be represented by
+     * the XML serialization. It will throw an exception otherwise.
      *
-     * @throws SerializationFailedException serialization of input effect failed, no data was
-     *                                      written into given {@link Writer}.
-     * @throws IOException error writing to given {@link Writer}.
+     * @throws IOException serialization of input effect failed or error writing to output stream.
      *
      * @hide
      */
     @TestApi
     public static void serialize(@NonNull VibrationEffect effect, @NonNull Writer writer)
-            throws SerializationFailedException, IOException {
+            throws IOException {
         serialize(effect, writer, /* flags= */ 0);
     }
 
@@ -106,7 +102,7 @@
      * @hide
      */
     public static void serialize(@NonNull VibrationEffect effect, @NonNull Writer writer,
-            @Flags int flags) throws SerializationFailedException, IOException {
+            @Flags int flags) throws IOException {
         // Serialize effect first to fail early.
         XmlSerializedVibration<VibrationEffect> serializedVibration =
                 toSerializedVibration(effect, flags);
@@ -138,17 +134,16 @@
     }
 
     /**
-     * Exception thrown when a {@link VibrationEffect} instance serialization fails.
+     * Exception thrown when a {@link VibrationEffect} serialization fails.
      *
      * <p>The serialization can fail if a given vibration cannot be represented using the public
-     * format, or if it uses hidden APIs that are not supported for serialization (e.g.
-     * {@link VibrationEffect.WaveformBuilder}).
+     * format, or if it uses a non-public representation that is not supported for serialization.
      *
      * @hide
      */
     @TestApi
-    public static final class SerializationFailedException extends RuntimeException {
-        SerializationFailedException(VibrationEffect effect, Throwable cause) {
+    public static final class SerializationFailedException extends IOException {
+        private SerializationFailedException(VibrationEffect effect, Throwable cause) {
             super("Serialization failed for vibration effect " + effect, cause);
         }
     }
diff --git a/core/java/android/service/chooser/flags.aconfig b/core/java/android/service/chooser/flags.aconfig
index d6425c3..ed20207 100644
--- a/core/java/android/service/chooser/flags.aconfig
+++ b/core/java/android/service/chooser/flags.aconfig
@@ -32,3 +32,14 @@
   description: "Provides additional callbacks with information about user actions in ChooserResult"
   bug: "263474465"
 }
+
+flag {
+  name: "fix_resolver_memory_leak"
+  is_exported: true
+  namespace: "intentresolver"
+  description: "ResolverActivity memory leak (through the AppPredictor callback) fix"
+  bug: "346671041"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index a3afcce..863a99a 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -262,15 +262,12 @@
     private static final String CONDITION_ATT_SOURCE = "source";
     private static final String CONDITION_ATT_FLAGS = "flags";
 
-    private static final String ZEN_POLICY_TAG = "zen_policy";
-
     private static final String MANUAL_TAG = "manual";
     private static final String AUTOMATIC_TAG = "automatic";
     private static final String AUTOMATIC_DELETED_TAG = "deleted";
 
     private static final String RULE_ATT_ID = "ruleId";
     private static final String RULE_ATT_ENABLED = "enabled";
-    private static final String RULE_ATT_SNOOZING = "snoozing";
     private static final String RULE_ATT_NAME = "name";
     private static final String RULE_ATT_PKG = "pkg";
     private static final String RULE_ATT_COMPONENT = "component";
@@ -286,6 +283,7 @@
     private static final String RULE_ATT_ICON = "rule_icon";
     private static final String RULE_ATT_TRIGGER_DESC = "triggerDesc";
     private static final String RULE_ATT_DELETION_INSTANT = "deletionInstant";
+    private static final String RULE_ATT_DISABLED_ORIGIN = "disabledOrigin";
 
     private static final String DEVICE_EFFECT_DISPLAY_GRAYSCALE = "zdeDisplayGrayscale";
     private static final String DEVICE_EFFECT_SUPPRESS_AMBIENT_DISPLAY =
@@ -1170,6 +1168,10 @@
             if (deletionInstant != null) {
                 rt.deletionInstant = Instant.ofEpochMilli(deletionInstant);
             }
+            if (Flags.modesUi()) {
+                rt.disabledOrigin = safeInt(parser, RULE_ATT_DISABLED_ORIGIN,
+                        UPDATE_ORIGIN_UNKNOWN);
+            }
         }
         return rt;
     }
@@ -1224,6 +1226,9 @@
                 out.attributeLong(null, RULE_ATT_DELETION_INSTANT,
                         rule.deletionInstant.toEpochMilli());
             }
+            if (Flags.modesUi()) {
+                out.attributeInt(null, RULE_ATT_DISABLED_ORIGIN, rule.disabledOrigin);
+            }
         }
     }
 
@@ -2514,6 +2519,8 @@
         @ZenPolicy.ModifiableField public int zenPolicyUserModifiedFields;
         @ZenDeviceEffects.ModifiableField public int zenDeviceEffectsUserModifiedFields;
         @Nullable public Instant deletionInstant; // Only set on deleted rules.
+        @FlaggedApi(Flags.FLAG_MODES_UI)
+        @ConfigChangeOrigin public int disabledOrigin = UPDATE_ORIGIN_UNKNOWN;
 
         public ZenRule() { }
 
@@ -2552,6 +2559,9 @@
                 if (source.readInt() == 1) {
                     deletionInstant = Instant.ofEpochMilli(source.readLong());
                 }
+                if (Flags.modesUi()) {
+                    disabledOrigin = source.readInt();
+                }
             }
         }
 
@@ -2626,6 +2636,9 @@
                 } else {
                     dest.writeInt(0);
                 }
+                if (Flags.modesUi()) {
+                    dest.writeInt(disabledOrigin);
+                }
             }
         }
 
@@ -2671,6 +2684,9 @@
                 if (deletionInstant != null) {
                     sb.append(",deletionInstant=").append(deletionInstant);
                 }
+                if (Flags.modesUi()) {
+                    sb.append(",disabledOrigin=").append(disabledOrigin);
+                }
             }
 
             return sb.append(']').toString();
@@ -2724,7 +2740,7 @@
                     && other.modified == modified;
 
             if (Flags.modesApi()) {
-                return finalEquals
+                finalEquals = finalEquals
                         && Objects.equals(other.zenDeviceEffects, zenDeviceEffects)
                         && other.allowManualInvocation == allowManualInvocation
                         && Objects.equals(other.iconResName, iconResName)
@@ -2735,6 +2751,11 @@
                         && other.zenDeviceEffectsUserModifiedFields
                             == zenDeviceEffectsUserModifiedFields
                         && Objects.equals(other.deletionInstant, deletionInstant);
+
+                if (Flags.modesUi()) {
+                    finalEquals = finalEquals
+                            && other.disabledOrigin == disabledOrigin;
+                }
             }
 
             return finalEquals;
@@ -2743,11 +2764,21 @@
         @Override
         public int hashCode() {
             if (Flags.modesApi()) {
-                return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition,
-                        component, configurationActivity, pkg, id, enabler, zenPolicy,
-                        zenDeviceEffects, modified, allowManualInvocation, iconResName,
-                        triggerDescription, type, userModifiedFields, zenPolicyUserModifiedFields,
-                        zenDeviceEffectsUserModifiedFields, deletionInstant);
+                if (Flags.modesUi()) {
+                    return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition,
+                            component, configurationActivity, pkg, id, enabler, zenPolicy,
+                            zenDeviceEffects, modified, allowManualInvocation, iconResName,
+                            triggerDescription, type, userModifiedFields,
+                            zenPolicyUserModifiedFields,
+                            zenDeviceEffectsUserModifiedFields, deletionInstant, disabledOrigin);
+                } else {
+                    return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition,
+                            component, configurationActivity, pkg, id, enabler, zenPolicy,
+                            zenDeviceEffects, modified, allowManualInvocation, iconResName,
+                            triggerDescription, type, userModifiedFields,
+                            zenPolicyUserModifiedFields,
+                            zenDeviceEffectsUserModifiedFields, deletionInstant);
+                }
             }
             return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition,
                     component, configurationActivity, pkg, id, enabler, zenPolicy, modified);
diff --git a/core/java/android/util/SequenceUtils.java b/core/java/android/util/SequenceUtils.java
new file mode 100644
index 0000000..f833ce3
--- /dev/null
+++ b/core/java/android/util/SequenceUtils.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+/**
+ * Utilities to manage an info change seq id to ensure the update is in sync between client and
+ * system server. This should be used for info that can be updated though multiple IPC channel.
+ *
+ * To use it:
+ * 1. The system server should store the current seq as the source of truth, with initializing to
+ * {@link #getInitSeq}.
+ * 2. Whenever a newer info needs to be sent to the client side, the system server should first
+ * update its seq with {@link #getNextSeq}, then send the new info with the new seq to the client.
+ * 3. On the client side, when receiving a new info, it should only consume it if it is newer than
+ * the last received info seq by checking {@link #isIncomingSeqNewer}.
+ *
+ * @hide
+ */
+public final class SequenceUtils {
+
+    private SequenceUtils() {
+    }
+
+    /**
+     * Returns {@code true} if the incomingSeq is newer than the curSeq.
+     */
+    public static boolean isIncomingSeqNewer(int curSeq, int incomingSeq) {
+        // Convert to long for comparison.
+        final long diff = (long) incomingSeq - curSeq;
+        // If there has been a sufficiently large jump, assume the sequence has wrapped around.
+        // For example, when the last seq is MAX_VALUE, the incoming seq will be MIN_VALUE + 1.
+        // diff = MIN_VALUE + 1 - MAX_VALUE. It is smaller than 0, but should be treated as newer.
+        return diff > 0 || diff < Integer.MIN_VALUE;
+    }
+
+    /** Returns the initial seq. */
+    public static int getInitSeq() {
+        return Integer.MIN_VALUE;
+    }
+
+    /** Returns the next seq. */
+    public static int getNextSeq(int seq) {
+        return seq == Integer.MAX_VALUE
+                // Skip the initial seq, so that when the app process is relaunched, the incoming
+                // seq from the server is always treated as newer.
+                ? getInitSeq() + 1
+                : ++seq;
+    }
+}
diff --git a/core/java/android/view/InsetsSourceControl.java b/core/java/android/view/InsetsSourceControl.java
index 487214c..2efa647 100644
--- a/core/java/android/view/InsetsSourceControl.java
+++ b/core/java/android/view/InsetsSourceControl.java
@@ -18,6 +18,7 @@
 
 import static android.graphics.PointProto.X;
 import static android.graphics.PointProto.Y;
+import static android.util.SequenceUtils.getInitSeq;
 import static android.view.InsetsSourceControlProto.LEASH;
 import static android.view.InsetsSourceControlProto.POSITION;
 import static android.view.InsetsSourceControlProto.TYPE_NUMBER;
@@ -266,6 +267,9 @@
 
         private @Nullable InsetsSourceControl[] mControls;
 
+        /** To make sure the info update between client and system server is in order. */
+        private int mSeq = getInitSeq();
+
         public Array() {
         }
 
@@ -280,9 +284,18 @@
             readFromParcel(in);
         }
 
+        public int getSeq() {
+            return mSeq;
+        }
+
+        public void setSeq(int seq) {
+            mSeq = seq;
+        }
+
         /** Updates the current Array to the given Array. */
         public void setTo(@NonNull Array other, boolean copyControls) {
             set(other.mControls, copyControls);
+            mSeq = other.mSeq;
         }
 
         /** Updates the current controls to the given controls. */
@@ -336,11 +349,13 @@
 
         public void readFromParcel(Parcel in) {
             mControls = in.createTypedArray(InsetsSourceControl.CREATOR);
+            mSeq = in.readInt();
         }
 
         @Override
         public void writeToParcel(Parcel out, int flags) {
             out.writeTypedArray(mControls, flags);
+            out.writeInt(mSeq);
         }
 
         public static final @NonNull Creator<Array> CREATOR = new Creator<>() {
@@ -362,6 +377,7 @@
                 return false;
             }
             final InsetsSourceControl.Array other = (InsetsSourceControl.Array) o;
+            // mSeq is for internal bookkeeping only.
             return Arrays.equals(mControls, other.mControls);
         }
 
diff --git a/core/java/android/view/InsetsState.java b/core/java/android/view/InsetsState.java
index 21eec67..bbd9acf 100644
--- a/core/java/android/view/InsetsState.java
+++ b/core/java/android/view/InsetsState.java
@@ -17,6 +17,7 @@
 package android.view;
 
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.util.SequenceUtils.getInitSeq;
 import static android.view.InsetsSource.FLAG_FORCE_CONSUMING;
 import static android.view.InsetsSource.FLAG_INSETS_ROUNDED_CORNER;
 import static android.view.InsetsStateProto.DISPLAY_CUTOUT;
@@ -95,6 +96,9 @@
     /** The display shape */
     private DisplayShape mDisplayShape = DisplayShape.NONE;
 
+    /** To make sure the info update between client and system server is in order. */
+    private int mSeq = getInitSeq();
+
     public InsetsState() {
         mSources = new SparseArray<>();
     }
@@ -586,6 +590,14 @@
         }
     }
 
+    public int getSeq() {
+        return mSeq;
+    }
+
+    public void setSeq(int seq) {
+        mSeq = seq;
+    }
+
     public void set(InsetsState other) {
         set(other, false /* copySources */);
     }
@@ -597,6 +609,7 @@
         mRoundedCornerFrame.set(other.mRoundedCornerFrame);
         mPrivacyIndicatorBounds = other.getPrivacyIndicatorBounds();
         mDisplayShape = other.getDisplayShape();
+        mSeq = other.mSeq;
         mSources.clear();
         for (int i = 0, size = other.mSources.size(); i < size; i++) {
             final InsetsSource otherSource = other.mSources.valueAt(i);
@@ -620,6 +633,7 @@
         mRoundedCornerFrame.set(other.mRoundedCornerFrame);
         mPrivacyIndicatorBounds = other.getPrivacyIndicatorBounds();
         mDisplayShape = other.getDisplayShape();
+        mSeq = other.mSeq;
         if (types == 0) {
             return;
         }
@@ -705,6 +719,7 @@
                 || !mRoundedCornerFrame.equals(state.mRoundedCornerFrame)
                 || !mPrivacyIndicatorBounds.equals(state.mPrivacyIndicatorBounds)
                 || !mDisplayShape.equals(state.mDisplayShape)) {
+            // mSeq is for internal bookkeeping only.
             return false;
         }
 
@@ -778,6 +793,7 @@
         mRoundedCornerFrame.writeToParcel(dest, flags);
         dest.writeTypedObject(mPrivacyIndicatorBounds, flags);
         dest.writeTypedObject(mDisplayShape, flags);
+        dest.writeInt(mSeq);
         final int size = mSources.size();
         dest.writeInt(size);
         for (int i = 0; i < size; i++) {
@@ -803,6 +819,7 @@
         mRoundedCornerFrame.readFromParcel(in);
         mPrivacyIndicatorBounds = in.readTypedObject(PrivacyIndicatorBounds.CREATOR);
         mDisplayShape = in.readTypedObject(DisplayShape.CREATOR);
+        mSeq = in.readInt();
         final int size = in.readInt();
         final SparseArray<InsetsSource> sources;
         if (mSources == null) {
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index 0bdb4ad..f653524 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -2812,6 +2812,10 @@
         private final ArrayMap<SurfaceControl, Point> mResizedSurfaces = new ArrayMap<>();
         private final ArrayMap<SurfaceControl, SurfaceControl> mReparentedSurfaces =
                  new ArrayMap<>();
+        // Only non-null if the SurfaceControlRegistry is enabled. This list tracks the set of calls
+        // made through this transaction object, and is dumped (and cleared) when the transaction is
+        // later applied.
+        ArrayList<String> mCalls;
 
         Runnable mFreeNativeResources;
         private static final float[] INVALID_COLOR = {-1, -1, -1};
@@ -2837,13 +2841,28 @@
         private Transaction(long nativeObject) {
             mNativeObject = nativeObject;
             mFreeNativeResources = sRegistry.registerNativeAllocation(this, mNativeObject);
-            if (!SurfaceControlRegistry.sCallStackDebuggingInitialized) {
-                SurfaceControlRegistry.initializeCallStackDebugging();
-            }
+            setUpForSurfaceControlRegistry();
         }
 
         private Transaction(Parcel in) {
             readFromParcel(in);
+            setUpForSurfaceControlRegistry();
+        }
+
+        /**
+         * Sets up this transaction for the SurfaceControlRegistry.
+         */
+        private void setUpForSurfaceControlRegistry() {
+            if (!SurfaceControlRegistry.sCallStackDebuggingInitialized) {
+                SurfaceControlRegistry.initializeCallStackDebugging();
+            }
+            mCalls = SurfaceControlRegistry.sLogAllTxCallsOnApply
+                    ? new ArrayList<>()
+                    : null;
+            if (SurfaceControlRegistry.sCallStackDebuggingEnabled) {
+                SurfaceControlRegistry.getProcessInstance().checkCallStackDebugging(
+                        "ctor", this, null, null);
+            }
         }
 
         /**
@@ -2893,6 +2912,9 @@
             if (mNativeObject != 0) {
                 nativeClearTransaction(mNativeObject);
             }
+            if (mCalls != null) {
+                mCalls.clear();
+            }
         }
 
         /**
@@ -2904,6 +2926,9 @@
             mReparentedSurfaces.clear();
             mFreeNativeResources.run();
             mNativeObject = 0;
+            if (mCalls != null) {
+                mCalls.clear();
+            }
         }
 
         /**
@@ -2921,7 +2946,10 @@
 
             if (SurfaceControlRegistry.sCallStackDebuggingEnabled) {
                 SurfaceControlRegistry.getProcessInstance().checkCallStackDebugging(
-                        "apply", this, null, null);
+                        SurfaceControlRegistry.APPLY, this, null, null);
+            }
+            if (mCalls != null) {
+                mCalls.clear();
             }
         }
 
@@ -4421,6 +4449,14 @@
             if (this == other) {
                 return this;
             }
+            if (SurfaceControlRegistry.sCallStackDebuggingEnabled) {
+                SurfaceControlRegistry.getProcessInstance().checkCallStackDebugging(
+                        "merge", this, null, "otherTx=" + other.getId());
+                if (mCalls != null) {
+                    mCalls.addAll(other.mCalls);
+                    other.mCalls.clear();
+                }
+            }
             mResizedSurfaces.putAll(other.mResizedSurfaces);
             other.mResizedSurfaces.clear();
             mReparentedSurfaces.putAll(other.mReparentedSurfaces);
@@ -4472,6 +4508,10 @@
                 Log.w(TAG, "addTransactionCompletedListener was called but flag is disabled");
                 return this;
             }
+            if (SurfaceControlRegistry.sCallStackDebuggingEnabled) {
+                SurfaceControlRegistry.getProcessInstance().checkCallStackDebugging(
+                        "setFrameTimeline", this, null, "vsyncId=" + vsyncId);
+            }
             nativeSetFrameTimelineVsync(mNativeObject, vsyncId);
             return this;
         }
@@ -4479,6 +4519,11 @@
         /** @hide */
         @NonNull
         public Transaction setFrameTimelineVsync(long frameTimelineVsyncId) {
+            if (SurfaceControlRegistry.sCallStackDebuggingEnabled) {
+                SurfaceControlRegistry.getProcessInstance().checkCallStackDebugging(
+                        "setFrameTimelineVsync", this, null, "frameTimelineVsyncId="
+                                + frameTimelineVsyncId);
+            }
             nativeSetFrameTimelineVsync(mNativeObject, frameTimelineVsyncId);
             return this;
         }
diff --git a/core/java/android/view/SurfaceControlRegistry.java b/core/java/android/view/SurfaceControlRegistry.java
index aa3654d..117b200 100644
--- a/core/java/android/view/SurfaceControlRegistry.java
+++ b/core/java/android/view/SurfaceControlRegistry.java
@@ -44,6 +44,8 @@
  */
 public class SurfaceControlRegistry {
     private static final String TAG = "SurfaceControlRegistry";
+    // Special constant for identifying the Transaction#apply() calls
+    static final String APPLY = "apply";
 
     /**
      * An interface for processing the registered SurfaceControls when the threshold is exceeded.
@@ -134,6 +136,10 @@
     // sCallStackDebuggingEnabled is true.  Can be combined with the match name.
     private static String sCallStackDebuggingMatchCall;
 
+    // When set, all calls on a SurfaceControl.Transaction will be stored and logged when the
+    // transaction is applied.
+    static boolean sLogAllTxCallsOnApply;
+
     // Mapping of the active SurfaceControls to the elapsed time when they were registered
     @GuardedBy("sLock")
     private final WeakHashMap<SurfaceControl, Long> mSurfaceControls;
@@ -185,6 +191,7 @@
     public void setCallStackDebuggingParams(String matchName, String matchCall) {
         sCallStackDebuggingMatchName = matchName.toLowerCase();
         sCallStackDebuggingMatchCall = matchCall.toLowerCase();
+        sLogAllTxCallsOnApply = sCallStackDebuggingMatchCall.contains("apply");
     }
 
     /**
@@ -294,13 +301,15 @@
         sCallStackDebuggingMatchName =
                 SystemProperties.get("persist.wm.debug.sc.tx.log_match_name", null)
                         .toLowerCase();
+        sLogAllTxCallsOnApply = sCallStackDebuggingMatchCall.contains("apply");
         // Only enable stack debugging if any of the match filters are set
-        sCallStackDebuggingEnabled = (!sCallStackDebuggingMatchCall.isEmpty()
-                || !sCallStackDebuggingMatchName.isEmpty());
+        sCallStackDebuggingEnabled = !sCallStackDebuggingMatchCall.isEmpty()
+                || !sCallStackDebuggingMatchName.isEmpty();
         if (sCallStackDebuggingEnabled) {
             Log.d(TAG, "Enabling transaction call stack debugging:"
                     + " matchCall=" + sCallStackDebuggingMatchCall
-                    + " matchName=" + sCallStackDebuggingMatchName);
+                    + " matchName=" + sCallStackDebuggingMatchName
+                    + " logCallsWithApply=" + sLogAllTxCallsOnApply);
         }
     }
 
@@ -319,15 +328,31 @@
         if (!sCallStackDebuggingEnabled) {
             return;
         }
-        if (!matchesForCallStackDebugging(sc != null ? sc.getName() : null, call)) {
-            return;
-        }
-        final String txMsg = tx != null ? "tx=" + tx.getId() + " ": "";
-        final String scMsg = sc != null ? " sc=" + sc.getName() + "": "";
+
+        final String txMsg = tx != null ? "tx=" + tx.getId() + " " : "";
+        final String scMsg = sc != null ? " sc=" + sc.getName() + "" : "";
         final String msg = details != null
                 ? call + " (" + txMsg + scMsg + ") " + details
                 : call + " (" + txMsg + scMsg + ")";
-        Log.e(TAG, msg, new Throwable());
+        if (sLogAllTxCallsOnApply && tx != null) {
+            if (call == APPLY) {
+                // Log the apply and dump the calls on that transaction
+                Log.e(TAG, msg, new Throwable());
+                for (int i = 0; i < tx.mCalls.size(); i++) {
+                    Log.d(TAG, "        " + tx.mCalls.get(i));
+                }
+            } else if (matchesForCallStackDebugging(sc != null ? sc.getName() : null, call)) {
+                // Otherwise log this call to the transaction if it matches the tracked calls
+                Log.e(TAG, msg, new Throwable());
+                tx.mCalls.add(msg);
+            }
+        } else {
+            // Log this call if it matches the tracked calls
+            if (!matchesForCallStackDebugging(sc != null ? sc.getName() : null, call)) {
+                return;
+            }
+            Log.e(TAG, msg, new Throwable());
+        }
     }
 
     /**
@@ -388,6 +413,7 @@
                 pw.println("sCallStackDebuggingEnabled=" + sCallStackDebuggingEnabled);
                 pw.println("sCallStackDebuggingMatchName=" + sCallStackDebuggingMatchName);
                 pw.println("sCallStackDebuggingMatchCall=" + sCallStackDebuggingMatchCall);
+                pw.println("sLogAllTxCallsOnApply=" + sLogAllTxCallsOnApply);
             }
         }
     }
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 3df72e8..8c3390c 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -30607,7 +30607,8 @@
      * {@link #setPointerIcon(PointerIcon)} for mouse devices. Subclasses may override this to
      * customize the icon for the given pointer.
      *
-     * For example, the pointer icon for a stylus pointer can be resolved in the following way:
+     * For example, to always show the PointerIcon.TYPE_HANDWRITING icon for a stylus pointer,
+     * the event can be resolved in the following way:
      * <code><pre>
      * &#64;Override
      * public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
@@ -30617,7 +30618,7 @@
      *             && (toolType == MotionEvent.TOOL_TYPE_STYLUS
      *                     || toolType == MotionEvent.TOOL_TYPE_ERASER)) {
      *         // Show this pointer icon only if this pointer is a stylus.
-     *         return PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_WAIT);
+     *         return PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_HANDWRITING);
      *     }
      *     // Use the default logic for determining the pointer icon for other non-stylus pointers,
      *     // like for the mouse cursor.
@@ -33898,8 +33899,19 @@
     protected int calculateFrameRateCategory() {
         int category;
         switch (getViewRootImpl().intermittentUpdateState()) {
-            case ViewRootImpl.INTERMITTENT_STATE_INTERMITTENT -> category =
-                    FRAME_RATE_CATEGORY_NORMAL | FRAME_RATE_CATEGORY_REASON_INTERMITTENT;
+            case ViewRootImpl.INTERMITTENT_STATE_INTERMITTENT -> {
+                if (!sToolkitFrameRateBySizeReadOnlyFlagValue) {
+                    category = FRAME_RATE_CATEGORY_NORMAL;
+                } else {
+                    // The size based frame rate category can only be LOW or NORMAL. If the size
+                    // based frame rate category is LOW, we shouldn't vote for NORMAL for
+                    // intermittent.
+                    category = Math.min(
+                            mSizeBasedFrameRateCategoryAndReason & ~FRAME_RATE_CATEGORY_REASON_MASK,
+                            FRAME_RATE_CATEGORY_NORMAL);
+                }
+                category |= FRAME_RATE_CATEGORY_REASON_INTERMITTENT;
+            }
             case ViewRootImpl.INTERMITTENT_STATE_NOT_INTERMITTENT ->
                     category = mSizeBasedFrameRateCategoryAndReason;
             default -> category = mLastFrameRateCategory;
diff --git a/core/java/android/window/ClientWindowFrames.java b/core/java/android/window/ClientWindowFrames.java
index d5398e6..781a901 100644
--- a/core/java/android/window/ClientWindowFrames.java
+++ b/core/java/android/window/ClientWindowFrames.java
@@ -16,6 +16,8 @@
 
 package android.window;
 
+import static android.util.SequenceUtils.getInitSeq;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.graphics.Rect;
@@ -53,6 +55,9 @@
 
     public float compatScale = 1f;
 
+    /** To make sure the info update between client and system server is in order. */
+    public int seq = getInitSeq();
+
     public ClientWindowFrames() {
     }
 
@@ -74,6 +79,7 @@
         }
         isParentFrameClippedByDisplayCutout = other.isParentFrameClippedByDisplayCutout;
         compatScale = other.compatScale;
+        seq = other.seq;
     }
 
     /** Needed for AIDL out parameters. */
@@ -84,6 +90,7 @@
         attachedFrame = in.readTypedObject(Rect.CREATOR);
         isParentFrameClippedByDisplayCutout = in.readBoolean();
         compatScale = in.readFloat();
+        seq = in.readInt();
     }
 
     @Override
@@ -94,6 +101,7 @@
         dest.writeTypedObject(attachedFrame, flags);
         dest.writeBoolean(isParentFrameClippedByDisplayCutout);
         dest.writeFloat(compatScale);
+        dest.writeInt(seq);
     }
 
     @Override
@@ -116,6 +124,7 @@
             return false;
         }
         final ClientWindowFrames other = (ClientWindowFrames) o;
+        // seq is for internal bookkeeping only.
         return frame.equals(other.frame)
                 && displayFrame.equals(other.displayFrame)
                 && parentFrame.equals(other.parentFrame)
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index b714682..985dc10 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -192,4 +192,15 @@
   metadata {
     purpose: PURPOSE_BUGFIX
   }
-}
\ No newline at end of file
+}
+
+flag {
+  name: "ensure_wallpaper_in_transitions"
+  namespace: "windowing_frontend"
+  description: "Ensure that wallpaper window tokens are always present/available for collection in transitions"
+  bug: "347593088"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
diff --git a/core/java/com/android/internal/accessibility/util/ShortcutUtils.java b/core/java/com/android/internal/accessibility/util/ShortcutUtils.java
index a7aef92..6b0ca9f 100644
--- a/core/java/com/android/internal/accessibility/util/ShortcutUtils.java
+++ b/core/java/com/android/internal/accessibility/util/ShortcutUtils.java
@@ -181,6 +181,27 @@
     }
 
     /**
+     * Converts {@link Settings.Secure} key to {@link UserShortcutType}.
+     *
+     * @param key The shortcut key in Settings.
+     * @return The mapped type
+     */
+    @UserShortcutType
+    public static int convertToType(String key) {
+        return switch (key) {
+            case Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS -> UserShortcutType.SOFTWARE;
+            case Settings.Secure.ACCESSIBILITY_QS_TARGETS -> UserShortcutType.QUICK_SETTINGS;
+            case Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE -> UserShortcutType.HARDWARE;
+            case Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED ->
+                    UserShortcutType.TRIPLETAP;
+            case Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED ->
+                    UserShortcutType.TWOFINGER_DOUBLETAP;
+            default -> throw new IllegalArgumentException(
+                    "Unsupported user shortcut key: " + key);
+        };
+    }
+
+    /**
      * Updates an accessibility state if the accessibility service is a Always-On a11y service,
      * a.k.a. AccessibilityServices that has FLAG_REQUEST_ACCESSIBILITY_BUTTON
      * <p>
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index 920981e..a194535 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -1209,9 +1209,19 @@
         if (!isChangingConfigurations() && mPickOptionRequest != null) {
             mPickOptionRequest.cancel();
         }
-        if (mMultiProfilePagerAdapter != null
-                && mMultiProfilePagerAdapter.getActiveListAdapter() != null) {
-            mMultiProfilePagerAdapter.getActiveListAdapter().onDestroy();
+        if (mMultiProfilePagerAdapter != null) {
+            ResolverListAdapter activeAdapter =
+                    mMultiProfilePagerAdapter.getActiveListAdapter();
+            if (activeAdapter != null) {
+                activeAdapter.onDestroy();
+            }
+            if (android.service.chooser.Flags.fixResolverMemoryLeak()) {
+                ResolverListAdapter inactiveAdapter =
+                        mMultiProfilePagerAdapter.getInactiveListAdapter();
+                if (inactiveAdapter != null) {
+                    inactiveAdapter.onDestroy();
+                }
+            }
         }
     }
 
diff --git a/core/jni/Android.bp b/core/jni/Android.bp
index 61eaa52..911bb19 100644
--- a/core/jni/Android.bp
+++ b/core/jni/Android.bp
@@ -111,6 +111,7 @@
         "libminikin",
         "libz",
         "server_configurable_flags",
+        "libaconfig_storage_read_api_cc",
         "android.database.sqlite-aconfig-cc",
         "android.media.audiopolicy-aconfig-cc",
     ],
@@ -127,6 +128,7 @@
     ],
 
     defaults: [
+        "aconfig_lib_cc_shared_link.defaults",
         "latest_android_media_audio_common_types_cpp_target_shared",
     ],
 
@@ -365,6 +367,7 @@
                 "libdl_android",
                 "libtimeinstate",
                 "server_configurable_flags",
+                "libaconfig_storage_read_api_cc",
                 "libimage_io",
                 "libultrahdr",
                 "libperfetto_c",
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 105b099..335b740 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2625,10 +2625,7 @@
          If false, not supported. -->
     <bool name="config_duplicate_port_omadm_wappush">false</bool>
 
-    <!-- Maximum numerical value that will be shown in a status bar
-         notification icon or in the notification itself. Will be replaced
-         with @string/status_bar_notification_info_overflow when shown in the
-         UI. -->
+    <!-- @deprecated No longer used. -->
     <integer name="status_bar_notification_info_maxnum">999</integer>
 
     <!-- Path to an ISO image to be shared with via USB mass storage.
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 05c46b9..9d1e86b 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -788,11 +788,7 @@
     <!-- label for item that locks the phone and enforces that it can't be unlocked without strong authentication. [CHAR LIMIT=24] -->
     <string name="global_action_lockdown">Lockdown</string>
 
-    <!-- Text to use when the number in a notification info is too large
-         (greater than status_bar_notification_info_maxnum, defined in
-         values/config.xml) and must be truncated. May need to be localized
-         for most appropriate textual indicator of "more than X".
-         [CHAR LIMIT=4] -->
+    <!-- @deprecated No longer used. -->
     <string name="status_bar_notification_info_overflow">999+</string>
 
     <!-- The divider symbol between different parts of the notification header. not translatable [CHAR LIMIT=1] -->
@@ -3837,6 +3833,11 @@
     <!-- Message of notification shown when Test Harness Mode is enabled. [CHAR LIMIT=NONE] -->
     <string name="test_harness_mode_notification_message">Perform a factory reset to disable Test Harness Mode.</string>
 
+    <!-- Title of notification shown when device is in the wrong Headless System User Mode configuration. [CHAR LIMIT=NONE] -->
+    <string name="wrong_hsum_configuration_notification_title">Wrong HSUM build configuration</string>
+    <!-- Message of notification shown when device is in the wrong Headless System User Mode configuration. [CHAR LIMIT=NONE] -->
+    <string name="wrong_hsum_configuration_notification_message">The Headless System User Mode state of this device differs from its build configuration. Please factory reset the device.</string>
+
     <!-- Title of notification shown when serial console is enabled. [CHAR LIMIT=NONE] -->
     <string name="console_running_notification_title">Serial console enabled</string>
     <!-- Message of notification shown when serial console is enabled. [CHAR LIMIT=NONE] -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 0f54d89..8823894 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2153,6 +2153,8 @@
   <java-symbol type="string" name="adbwifi_active_notification_title" />
   <java-symbol type="string" name="test_harness_mode_notification_title" />
   <java-symbol type="string" name="test_harness_mode_notification_message" />
+  <java-symbol type="string" name="wrong_hsum_configuration_notification_title" />
+  <java-symbol type="string" name="wrong_hsum_configuration_notification_message" />
   <java-symbol type="string" name="console_running_notification_title" />
   <java-symbol type="string" name="console_running_notification_message" />
   <java-symbol type="string" name="mte_override_notification_title" />
diff --git a/core/tests/bugreports/Android.bp b/core/tests/bugreports/Android.bp
index 7c1ac48..15e07e5 100644
--- a/core/tests/bugreports/Android.bp
+++ b/core/tests/bugreports/Android.bp
@@ -30,6 +30,7 @@
         "android.test.base",
     ],
     static_libs: [
+        "android.tracing.flags-aconfig-java",
         "androidx.test.rules",
         "androidx.test.uiautomator_uiautomator",
         "truth",
diff --git a/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java b/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java
index 8072d69..7294d4c 100644
--- a/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java
+++ b/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java
@@ -71,6 +71,7 @@
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
@@ -102,17 +103,11 @@
     // associated with the bugreport).
     private static final String INTENT_BUGREPORT_FINISHED =
             "com.android.internal.intent.action.BUGREPORT_FINISHED";
-    private static final String EXTRA_BUGREPORT = "android.intent.extra.BUGREPORT";
-    private static final String EXTRA_SCREENSHOT = "android.intent.extra.SCREENSHOT";
 
-    private static final Path[] UI_TRACES_PREDUMPED = {
+    private ArrayList<Path> mUiTracesPreDumped = new ArrayList<>(Arrays.asList(
             Paths.get("/data/misc/perfetto-traces/bugreport/systrace.pftrace"),
-            Paths.get("/data/misc/wmtrace/ime_trace_clients.winscope"),
-            Paths.get("/data/misc/wmtrace/ime_trace_managerservice.winscope"),
-            Paths.get("/data/misc/wmtrace/ime_trace_service.winscope"),
-            Paths.get("/data/misc/wmtrace/wm_trace.winscope"),
-            Paths.get("/data/misc/wmtrace/wm_log.winscope"),
-    };
+            Paths.get("/data/misc/wmtrace/wm_trace.winscope")
+    ));
 
     private Handler mHandler;
     private Executor mExecutor;
@@ -124,6 +119,17 @@
 
     @Before
     public void setup() throws Exception {
+        if (!android.tracing.Flags.perfettoIme()) {
+            mUiTracesPreDumped.add(Paths.get("/data/misc/wmtrace/ime_trace_clients.winscope"));
+            mUiTracesPreDumped.add(
+                    Paths.get("/data/misc/wmtrace/ime_trace_managerservice.winscope"));
+            mUiTracesPreDumped.add(Paths.get("/data/misc/wmtrace/ime_trace_service.winscope"));
+        }
+
+        if (!android.tracing.Flags.perfettoProtologTracing()) {
+            mUiTracesPreDumped.add(Paths.get("/data/misc/wmtrace/wm_log.winscope"));
+        }
+
         mHandler = createHandler();
         mExecutor = (runnable) -> {
             if (mHandler != null) {
@@ -206,7 +212,7 @@
 
         mBrm.preDumpUiData();
         waitTillDumpstateExitedOrTimeout();
-        List<File> expectedPreDumpedTraceFiles = copyFiles(UI_TRACES_PREDUMPED);
+        List<File> expectedPreDumpedTraceFiles = copyFiles(mUiTracesPreDumped);
 
         BugreportCallbackImpl callback = new BugreportCallbackImpl();
         mBrm.startBugreport(mBugreportFd, null, fullWithUsePreDumpFlag(), mExecutor,
@@ -220,9 +226,9 @@
         assertThat(mBugreportFile.length()).isGreaterThan(0L);
         assertFdsAreClosed(mBugreportFd);
 
-        assertThatBugreportContainsFiles(UI_TRACES_PREDUMPED);
+        assertThatBugreportContainsFiles(mUiTracesPreDumped);
 
-        List<File> actualPreDumpedTraceFiles = extractFilesFromBugreport(UI_TRACES_PREDUMPED);
+        List<File> actualPreDumpedTraceFiles = extractFilesFromBugreport(mUiTracesPreDumped);
         assertThatAllFileContentsAreEqual(actualPreDumpedTraceFiles, expectedPreDumpedTraceFiles);
     }
 
@@ -235,9 +241,9 @@
         // In some corner cases, data dumped as part of the full bugreport could be the same as the
         // pre-dumped data and this test would fail. Hence, here we create fake/artificial
         // pre-dumped data that we know it won't match with the full bugreport data.
-        createFakeTraceFiles(UI_TRACES_PREDUMPED);
+        createFakeTraceFiles(mUiTracesPreDumped);
 
-        List<File> preDumpedTraceFiles = copyFiles(UI_TRACES_PREDUMPED);
+        List<File> preDumpedTraceFiles = copyFiles(mUiTracesPreDumped);
 
         BugreportCallbackImpl callback = new BugreportCallbackImpl();
         mBrm.startBugreport(mBugreportFd, null, full(), mExecutor,
@@ -251,9 +257,9 @@
         assertThat(mBugreportFile.length()).isGreaterThan(0L);
         assertFdsAreClosed(mBugreportFd);
 
-        assertThatBugreportContainsFiles(UI_TRACES_PREDUMPED);
+        assertThatBugreportContainsFiles(mUiTracesPreDumped);
 
-        List<File> actualTraceFiles = extractFilesFromBugreport(UI_TRACES_PREDUMPED);
+        List<File> actualTraceFiles = extractFilesFromBugreport(mUiTracesPreDumped);
         assertThatAllFileContentsAreDifferent(preDumpedTraceFiles, actualTraceFiles);
     }
 
@@ -270,7 +276,7 @@
         // 1. Pre-dump data
         // 2. Start bugreport + "use pre-dump" flag (USE AND REMOVE THE PRE-DUMP FROM DISK)
         // 3. Start bugreport + "use pre-dump" flag (NO PRE-DUMP AVAILABLE ON DISK)
-        removeFilesIfNeeded(UI_TRACES_PREDUMPED);
+        removeFilesIfNeeded(mUiTracesPreDumped);
 
         // Start bugreport with "use predump" flag. Because the pre-dumped data is not available
         // the flag will be ignored and data will be dumped as in normal flow.
@@ -286,7 +292,7 @@
         assertThat(mBugreportFile.length()).isGreaterThan(0L);
         assertFdsAreClosed(mBugreportFd);
 
-        assertThatBugreportContainsFiles(UI_TRACES_PREDUMPED);
+        assertThatBugreportContainsFiles(mUiTracesPreDumped);
     }
 
     @Test
@@ -555,7 +561,7 @@
         );
     }
 
-    private void assertThatBugreportContainsFiles(Path[] paths)
+    private void assertThatBugreportContainsFiles(List<Path> paths)
             throws IOException {
         List<Path> entries = listZipArchiveEntries(mBugreportFile);
         for (Path pathInDevice : paths) {
@@ -564,7 +570,7 @@
         }
     }
 
-    private List<File> extractFilesFromBugreport(Path[] paths) throws Exception {
+    private List<File> extractFilesFromBugreport(List<Path> paths) throws Exception {
         List<File> files = new ArrayList<File>();
         for (Path pathInDevice : paths) {
             Path pathInArchive = Paths.get("FS" + pathInDevice.toString());
@@ -614,7 +620,7 @@
         return extractedFile;
     }
 
-    private static void createFakeTraceFiles(Path[] paths) throws Exception {
+    private static void createFakeTraceFiles(List<Path> paths) throws Exception {
         File src = createTempFile("fake", ".data");
         Files.write("fake data".getBytes(StandardCharsets.UTF_8), src);
 
@@ -631,7 +637,7 @@
         );
     }
 
-    private static List<File> copyFiles(Path[] paths) throws Exception {
+    private static List<File> copyFiles(List<Path> paths) throws Exception {
         ArrayList<File> files = new ArrayList<File>();
         for (Path src : paths) {
             File dst = createTempFile(src.getFileName().toString(), ".copy");
@@ -643,7 +649,7 @@
         return files;
     }
 
-    private static void removeFilesIfNeeded(Path[] paths) throws Exception {
+    private static void removeFilesIfNeeded(List<Path> paths) throws Exception {
         for (Path path : paths) {
             InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
                     "rm -f " + path.toString()
diff --git a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java
index 3104f16..0b270d4 100644
--- a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java
+++ b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java
@@ -20,8 +20,6 @@
 
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
-import static com.android.window.flags.Flags.FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG;
-
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -104,8 +102,6 @@
 
     @Before
     public void setup() {
-        mSetFlagsRule.enableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
         MockitoAnnotations.initMocks(this);
         mDisplayManager = new DisplayManagerGlobal(mIDisplayManager);
         mHandler = getInstrumentation().getContext().getMainThreadHandler();
diff --git a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionTests.java b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionTests.java
index 5272416..79659ca 100644
--- a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionTests.java
+++ b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionTests.java
@@ -16,22 +16,16 @@
 
 package android.app.servertransaction;
 
-import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
-
-import static com.android.window.flags.Flags.FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG;
-
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 
 import android.app.ClientTransactionHandler;
 import android.platform.test.annotations.Presubmit;
-import android.platform.test.flag.junit.SetFlagsRule;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -49,24 +43,8 @@
 @Presubmit
 public class ClientTransactionTests {
 
-    @Rule
-    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
-
     @Test
     public void testPreExecute() {
-        mSetFlagsRule.disableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
-        testPreExecuteInner();
-    }
-
-    @Test
-    public void testPreExecute_bundleClientTransaction() {
-        mSetFlagsRule.enableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
-        testPreExecuteInner();
-    }
-
-    private void testPreExecuteInner() {
         final ClientTransactionItem callback1 = mock(ClientTransactionItem.class);
         final ClientTransactionItem callback2 = mock(ClientTransactionItem.class);
         final ActivityLifecycleItem stateRequest = mock(ActivityLifecycleItem.class);
diff --git a/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java b/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java
index 935bc75..73b7447 100644
--- a/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java
+++ b/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java
@@ -25,9 +25,6 @@
 import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP;
 import static android.app.servertransaction.ActivityLifecycleItem.PRE_ON_CREATE;
 import static android.app.servertransaction.ActivityLifecycleItem.UNDEFINED;
-import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
-
-import static com.android.window.flags.Flags.FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG;
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
@@ -54,14 +51,12 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.platform.test.annotations.Presubmit;
-import android.platform.test.flag.junit.SetFlagsRule;
 import android.util.ArrayMap;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.InOrder;
@@ -88,9 +83,6 @@
 @Presubmit
 public class TransactionExecutorTests {
 
-    @Rule
-    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
-
     @Mock
     private ClientTransactionHandler mTransactionHandler;
     @Mock
@@ -248,19 +240,6 @@
 
     @Test
     public void testTransactionResolution() {
-        mSetFlagsRule.disableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
-        testTransactionResolutionInner();
-    }
-
-    @Test
-    public void testTransactionResolution_bundleClientTransaction() {
-        mSetFlagsRule.enableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
-        testTransactionResolutionInner();
-    }
-
-    private void testTransactionResolutionInner() {
         ClientTransactionItem callback1 = mock(ClientTransactionItem.class);
         when(callback1.getPostExecutionState()).thenReturn(UNDEFINED);
         ClientTransactionItem callback2 = mock(ClientTransactionItem.class);
@@ -284,19 +263,6 @@
 
     @Test
     public void testDoNotLaunchDestroyedActivity() {
-        mSetFlagsRule.disableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
-        testDoNotLaunchDestroyedActivityInner();
-    }
-
-    @Test
-    public void testDoNotLaunchDestroyedActivity_bundleClientTransaction() {
-        mSetFlagsRule.enableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
-        testDoNotLaunchDestroyedActivityInner();
-    }
-
-    private void testDoNotLaunchDestroyedActivityInner() {
         final Map<IBinder, DestroyActivityItem> activitiesToBeDestroyed = new ArrayMap<>();
         when(mTransactionHandler.getActivitiesToBeDestroyed()).thenReturn(activitiesToBeDestroyed);
         // Assume launch transaction is still in queue, so there is no client record.
@@ -329,19 +295,6 @@
 
     @Test
     public void testActivityResultRequiredStateResolution() {
-        mSetFlagsRule.disableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
-        testActivityResultRequiredStateResolutionInner();
-    }
-
-    @Test
-    public void testActivityResultRequiredStateResolution_bundleClientTransaction() {
-        mSetFlagsRule.enableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
-        testActivityResultRequiredStateResolutionInner();
-    }
-
-    private void testActivityResultRequiredStateResolutionInner() {
         when(mTransactionHandler.getActivity(any())).thenReturn(mock(Activity.class));
 
         PostExecItem postExecItem = new PostExecItem(ON_RESUME);
@@ -495,19 +448,6 @@
 
     @Test(expected = IllegalArgumentException.class)
     public void testActivityItemNullRecordThrowsException() {
-        mSetFlagsRule.disableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
-        testActivityItemNullRecordThrowsExceptionInner();
-    }
-
-    @Test(expected = IllegalArgumentException.class)
-    public void testActivityItemNullRecordThrowsException_bundleClientTransaction() {
-        mSetFlagsRule.enableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
-        testActivityItemNullRecordThrowsExceptionInner();
-    }
-
-    private void testActivityItemNullRecordThrowsExceptionInner() {
         final ActivityTransactionItem activityItem = mock(ActivityTransactionItem.class);
         when(activityItem.getPostExecutionState()).thenReturn(UNDEFINED);
         final IBinder token = mock(IBinder.class);
@@ -520,19 +460,6 @@
 
     @Test
     public void testActivityItemExecute() {
-        mSetFlagsRule.disableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
-        testActivityItemExecuteInner();
-    }
-
-    @Test
-    public void testActivityItemExecute_bundleClientTransaction() {
-        mSetFlagsRule.enableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
-        testActivityItemExecuteInner();
-    }
-
-    private void testActivityItemExecuteInner() {
         final ClientTransaction transaction = ClientTransaction.obtain(null /* client */);
         final ActivityTransactionItem activityItem = mock(ActivityTransactionItem.class);
         when(activityItem.getPostExecutionState()).thenReturn(UNDEFINED);
diff --git a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
index d451fe5..a4d7661 100644
--- a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
+++ b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
@@ -20,9 +20,6 @@
 import static android.app.servertransaction.TestUtils.mergedConfig;
 import static android.app.servertransaction.TestUtils.referrerIntentList;
 import static android.app.servertransaction.TestUtils.resultInfoList;
-import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
-
-import static com.android.window.flags.Flags.FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG;
 
 import static org.junit.Assert.assertEquals;
 
@@ -40,14 +37,12 @@
 import android.os.Parcelable;
 import android.os.PersistableBundle;
 import android.platform.test.annotations.Presubmit;
-import android.platform.test.flag.junit.SetFlagsRule;
 import android.window.ActivityWindowInfo;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -67,9 +62,6 @@
 @Presubmit
 public class TransactionParcelTests {
 
-    @Rule
-    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
-
     private Parcel mParcel;
     private IBinder mActivityToken;
 
@@ -296,8 +288,6 @@
 
     @Test
     public void testClientTransaction() {
-        mSetFlagsRule.enableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
         // Write to parcel
         NewIntentItem callback1 = NewIntentItem.obtain(mActivityToken, new ArrayList<>(), true);
         ActivityConfigurationChangeItem callback2 = ActivityConfigurationChangeItem.obtain(
@@ -320,49 +310,6 @@
         assertEquals(mActivityToken, result.getActivityToken());
     }
 
-    @Test
-    public void testClientTransactionCallbacksOnly() {
-        mSetFlagsRule.disableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
-        // Write to parcel
-        NewIntentItem callback1 = NewIntentItem.obtain(mActivityToken, new ArrayList<>(), true);
-        ActivityConfigurationChangeItem callback2 = ActivityConfigurationChangeItem.obtain(
-                mActivityToken, config(), new ActivityWindowInfo());
-
-        ClientTransaction transaction = ClientTransaction.obtain(null /* client */);
-        transaction.addTransactionItem(callback1);
-        transaction.addTransactionItem(callback2);
-
-        writeAndPrepareForReading(transaction);
-
-        // Read from parcel and assert
-        ClientTransaction result = ClientTransaction.CREATOR.createFromParcel(mParcel);
-
-        assertEquals(transaction.hashCode(), result.hashCode());
-        assertEquals(transaction, result);
-        assertEquals(mActivityToken, result.getActivityToken());
-    }
-
-    @Test
-    public void testClientTransactionLifecycleOnly() {
-        mSetFlagsRule.disableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
-        // Write to parcel
-        StopActivityItem lifecycleRequest = StopActivityItem.obtain(mActivityToken);
-
-        ClientTransaction transaction = ClientTransaction.obtain(null /* client */);
-        transaction.addTransactionItem(lifecycleRequest);
-
-        writeAndPrepareForReading(transaction);
-
-        // Read from parcel and assert
-        ClientTransaction result = ClientTransaction.CREATOR.createFromParcel(mParcel);
-
-        assertEquals(transaction.hashCode(), result.hashCode());
-        assertEquals(transaction, result);
-        assertEquals(mActivityToken, result.getActivityToken());
-    }
-
     /** Write to {@link #mParcel} and reset its position to prepare for reading from the start. */
     private void writeAndPrepareForReading(Parcelable parcelable) {
         parcelable.writeToParcel(mParcel, 0 /* flags */);
diff --git a/core/tests/coretests/src/android/content/ContentResolverTest.java b/core/tests/coretests/src/android/content/ContentResolverTest.java
index c8015d4..7b70b41 100644
--- a/core/tests/coretests/src/android/content/ContentResolverTest.java
+++ b/core/tests/coretests/src/android/content/ContentResolverTest.java
@@ -87,7 +87,7 @@
         bitmap.compress(Bitmap.CompressFormat.PNG, 90, mImage.getOutputStream());
 
         final AssetFileDescriptor afd = new AssetFileDescriptor(
-                ParcelFileDescriptor.dup(mImage.getFileDescriptor()), 0, mSize, null);
+                new ParcelFileDescriptor(mImage.getFileDescriptor()), 0, mSize, null);
         when(mProvider.openTypedAssetFile(any(), any(), any(), any(), any())).thenReturn(
                 afd);
     }
diff --git a/core/tests/coretests/src/android/os/MessageQueueTest.java b/core/tests/coretests/src/android/os/MessageQueueTest.java
index 851e612..8cd6773 100644
--- a/core/tests/coretests/src/android/os/MessageQueueTest.java
+++ b/core/tests/coretests/src/android/os/MessageQueueTest.java
@@ -16,7 +16,6 @@
 
 package android.os;
 
-import android.platform.test.annotations.IgnoreUnderRavenwood;
 import android.platform.test.ravenwood.RavenwoodRule;
 
 import androidx.test.filters.MediumTest;
@@ -154,7 +153,6 @@
 
     @Test
     @MediumTest
-    @IgnoreUnderRavenwood(reason = "Flaky test, b/315872700")
     public void testFieldIntegrity() throws Exception {
 
         TestHandlerThread tester = new TestFieldIntegrityHandler() {
diff --git a/core/tests/coretests/src/android/util/SequenceUtilsTest.java b/core/tests/coretests/src/android/util/SequenceUtilsTest.java
new file mode 100644
index 0000000..020520d
--- /dev/null
+++ b/core/tests/coretests/src/android/util/SequenceUtilsTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+
+import static android.util.SequenceUtils.getInitSeq;
+import static android.util.SequenceUtils.getNextSeq;
+import static android.util.SequenceUtils.isIncomingSeqNewer;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.platform.test.annotations.DisabledOnRavenwood;
+import android.platform.test.annotations.Presubmit;
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for subtypes of {@link SequenceUtils}.
+ *
+ * Build/Install/Run:
+ *  atest FrameworksCoreTests:SequenceUtilsTest
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Presubmit
+@DisabledOnRavenwood(blockedBy = SequenceUtils.class)
+public class SequenceUtilsTest {
+
+    // This is needed to disable the test in Ravenwood test, because SequenceUtils hasn't opted in
+    // for Ravenwood, which is still in experiment.
+    @Rule
+    public final RavenwoodRule mRavenwood = new RavenwoodRule();
+
+    @Test
+    public void testNextSeq() {
+        assertEquals(getInitSeq() + 1, getNextSeq(getInitSeq()));
+        assertEquals(getInitSeq() + 1, getNextSeq(Integer.MAX_VALUE));
+    }
+
+    @Test
+    public void testIsIncomingSeqNewer() {
+        assertTrue(isIncomingSeqNewer(getInitSeq() + 1, getInitSeq() + 10));
+        assertFalse(isIncomingSeqNewer(getInitSeq() + 10, getInitSeq() + 1));
+        assertTrue(isIncomingSeqNewer(-100, 100));
+        assertFalse(isIncomingSeqNewer(100, -100));
+        assertTrue(isIncomingSeqNewer(1, 2));
+        assertFalse(isIncomingSeqNewer(2, 1));
+
+        // Possible incoming seq are all newer than the initial seq.
+        assertTrue(isIncomingSeqNewer(getInitSeq(), getInitSeq() + 1));
+        assertTrue(isIncomingSeqNewer(getInitSeq(), -100));
+        assertTrue(isIncomingSeqNewer(getInitSeq(), 0));
+        assertTrue(isIncomingSeqNewer(getInitSeq(), 100));
+        assertTrue(isIncomingSeqNewer(getInitSeq(), Integer.MAX_VALUE));
+        assertTrue(isIncomingSeqNewer(getInitSeq(), getNextSeq(Integer.MAX_VALUE)));
+
+        // False for the same seq.
+        assertFalse(isIncomingSeqNewer(getInitSeq(), getInitSeq()));
+        assertFalse(isIncomingSeqNewer(100, 100));
+        assertFalse(isIncomingSeqNewer(Integer.MAX_VALUE, Integer.MAX_VALUE));
+
+        // True when there is a large jump (overflow).
+        assertTrue(isIncomingSeqNewer(Integer.MAX_VALUE, getInitSeq() + 1));
+        assertTrue(isIncomingSeqNewer(Integer.MAX_VALUE, getInitSeq() + 100));
+        assertTrue(isIncomingSeqNewer(Integer.MAX_VALUE, getNextSeq(Integer.MAX_VALUE)));
+    }
+}
diff --git a/core/tests/coretests/src/android/util/SparseSetArrayTest.java b/core/tests/coretests/src/android/util/SparseSetArrayTest.java
index 1c72185..a8dce70 100644
--- a/core/tests/coretests/src/android/util/SparseSetArrayTest.java
+++ b/core/tests/coretests/src/android/util/SparseSetArrayTest.java
@@ -17,7 +17,6 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import android.platform.test.annotations.IgnoreUnderRavenwood;
 import android.platform.test.ravenwood.RavenwoodRule;
 
 import androidx.test.filters.SmallTest;
@@ -37,7 +36,6 @@
     public final RavenwoodRule mRavenwood = new RavenwoodRule();
 
     @Test
-    @IgnoreUnderRavenwood(reason = "Flaky test, b/315872700")
     public void testAddAll() {
         final SparseSetArray<Integer> sparseSetArray = new SparseSetArray<>();
 
@@ -59,7 +57,6 @@
     }
 
     @Test
-    @IgnoreUnderRavenwood(reason = "b/315036461")
     public void testCopyConstructor() {
         final SparseSetArray<Integer> sparseSetArray = new SparseSetArray<>();
 
diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java
index 06cb0ee..b153700 100644
--- a/core/tests/coretests/src/android/view/ViewRootImplTest.java
+++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java
@@ -1250,6 +1250,81 @@
         });
     }
 
+    @Test
+    @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY,
+            FLAG_TOOLKIT_FRAME_RATE_FUNCTION_ENABLING_READ_ONLY,
+            FLAG_TOOLKIT_FRAME_RATE_VIEW_ENABLING_READ_ONLY,
+            FLAG_TOOLKIT_FRAME_RATE_BY_SIZE_READ_ONLY})
+    public void votePreferredFrameRate_infrequentLayer_smallView_voteForLow() throws Throwable {
+        if (!ViewProperties.vrr_enabled().orElse(true)) {
+            return;
+        }
+        final long delay = 200L;
+
+        mView = new View(sContext);
+        WindowManager.LayoutParams wmlp = new WindowManager.LayoutParams(TYPE_APPLICATION_OVERLAY);
+        wmlp.token = new Binder(); // Set a fake token to bypass 'is your activity running' check
+        wmlp.width = 1;
+        wmlp.height = 1;
+
+        // The view is a small view, and it should vote for category low only.
+        int expected = FRAME_RATE_CATEGORY_LOW;
+
+        sInstrumentation.runOnMainSync(() -> {
+            WindowManager wm = sContext.getSystemService(WindowManager.class);
+            wm.addView(mView, wmlp);
+        });
+        sInstrumentation.waitForIdleSync();
+
+        mViewRootImpl = mView.getViewRootImpl();
+        waitForFrameRateCategoryToSettle(mView);
+
+        // In transition from frequent update to infrequent update
+        Thread.sleep(delay);
+        sInstrumentation.runOnMainSync(() -> {
+            mView.invalidate();
+            runAfterDraw(() -> assertEquals(expected,
+                    mViewRootImpl.getLastPreferredFrameRateCategory()));
+        });
+        waitForAfterDraw();
+
+        // In transition from frequent update to infrequent update
+        Thread.sleep(delay);
+        sInstrumentation.runOnMainSync(() -> {
+            mView.invalidate();
+            runAfterDraw(() -> assertEquals(expected,
+                    mViewRootImpl.getLastPreferredFrameRateCategory()));
+        });
+
+        // Infrequent update
+        Thread.sleep(delay);
+
+        // The view is small, the expected category is still low for intermittent.
+        int intermittentExpected = FRAME_RATE_CATEGORY_LOW;
+
+        sInstrumentation.runOnMainSync(() -> {
+            mView.invalidate();
+            runAfterDraw(() -> assertEquals(intermittentExpected,
+                    mViewRootImpl.getLastPreferredFrameRateCategory()));
+        });
+        waitForAfterDraw();
+
+        // When the View vote, it's still considered as intermittent update state
+        sInstrumentation.runOnMainSync(() -> {
+            mView.invalidate();
+            runAfterDraw(() -> assertEquals(intermittentExpected,
+                    mViewRootImpl.getLastPreferredFrameRateCategory()));
+        });
+        waitForAfterDraw();
+
+        // Becomes frequent update state
+        sInstrumentation.runOnMainSync(() -> {
+            mView.invalidate();
+            runAfterDraw(() -> assertEquals(expected,
+                    mViewRootImpl.getLastPreferredFrameRateCategory()));
+        });
+    }
+
     /**
      * Test the IsFrameRatePowerSavingsBalanced values are properly set
      */
diff --git a/core/tests/utiltests/src/android/util/TimeUtilsTest.java b/core/tests/utiltests/src/android/util/TimeUtilsTest.java
index ac659e1..6c6feaf 100644
--- a/core/tests/utiltests/src/android/util/TimeUtilsTest.java
+++ b/core/tests/utiltests/src/android/util/TimeUtilsTest.java
@@ -18,17 +18,19 @@
 
 import static org.junit.Assert.assertEquals;
 
-import android.platform.test.annotations.IgnoreUnderRavenwood;
 import android.platform.test.ravenwood.RavenwoodRule;
 
 import androidx.test.runner.AndroidJUnit4;
 
+import org.junit.After;
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 import java.io.PrintWriter;
 import java.io.StringWriter;
+import java.util.TimeZone;
 import java.util.function.Consumer;
 
 @RunWith(AndroidJUnit4.class)
@@ -42,6 +44,22 @@
     public static final long DAY_IN_MILLIS = HOUR_IN_MILLIS * 24;
     public static final long WEEK_IN_MILLIS = DAY_IN_MILLIS * 7;
 
+    private TimeZone mOrigTimezone;
+
+    @Before
+    public void setUp() {
+        mOrigTimezone = TimeZone.getDefault();
+
+        TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
+    }
+
+    @After
+    public void tearDown() {
+        if (mOrigTimezone != null) {
+            TimeZone.setDefault(mOrigTimezone);
+        }
+    }
+
     @Test
     public void testFormatTime() {
         assertEquals("1672556400000 (now)",
@@ -85,32 +103,29 @@
     }
 
     @Test
-    @IgnoreUnderRavenwood(reason = "Flaky test, b/315872700")
     public void testDumpTime() {
-        assertEquals("2023-01-01 00:00:00.000", runWithPrintWriter((pw) -> {
+        assertEquals("2023-01-01 07:00:00.000", runWithPrintWriter((pw) -> {
             TimeUtils.dumpTime(pw, 1672556400000L);
         }));
-        assertEquals("2023-01-01 00:00:00.000 (now)", runWithPrintWriter((pw) -> {
+        assertEquals("2023-01-01 07:00:00.000 (now)", runWithPrintWriter((pw) -> {
             TimeUtils.dumpTimeWithDelta(pw, 1672556400000L, 1672556400000L);
         }));
-        assertEquals("2023-01-01 00:00:00.000 (-10ms)", runWithPrintWriter((pw) -> {
+        assertEquals("2023-01-01 07:00:00.000 (-10ms)", runWithPrintWriter((pw) -> {
             TimeUtils.dumpTimeWithDelta(pw, 1672556400000L, 1672556400000L + 10);
         }));
     }
 
     @Test
-    @IgnoreUnderRavenwood(reason = "Flaky test, b/315872700")
     public void testFormatForLogging() {
         assertEquals("unknown", TimeUtils.formatForLogging(0));
         assertEquals("unknown", TimeUtils.formatForLogging(-1));
         assertEquals("unknown", TimeUtils.formatForLogging(Long.MIN_VALUE));
-        assertEquals("2023-01-01 00:00:00", TimeUtils.formatForLogging(1672556400000L));
+        assertEquals("2023-01-01 07:00:00", TimeUtils.formatForLogging(1672556400000L));
     }
 
     @Test
-    @IgnoreUnderRavenwood(reason = "Flaky test, b/315872700")
     public void testLogTimeOfDay() {
-        assertEquals("01-01 00:00:00.000", TimeUtils.logTimeOfDay(1672556400000L));
+        assertEquals("01-01 07:00:00.000", TimeUtils.logTimeOfDay(1672556400000L));
     }
 
     public static String runWithPrintWriter(Consumer<PrintWriter> consumer) {
diff --git a/core/tests/vibrator/src/android/os/vibrator/persistence/ParsedVibrationTest.java b/core/tests/vibrator/src/android/os/vibrator/persistence/ParsedVibrationTest.java
index 94298dc..83a8f8f 100644
--- a/core/tests/vibrator/src/android/os/vibrator/persistence/ParsedVibrationTest.java
+++ b/core/tests/vibrator/src/android/os/vibrator/persistence/ParsedVibrationTest.java
@@ -63,6 +63,34 @@
     }
 
     @Test
+    public void testEquals() {
+        assertThat(new ParsedVibration(List.of())).isEqualTo(new ParsedVibration(List.of()));
+        assertThat(new ParsedVibration(List.of())).isNotEqualTo(new ParsedVibration(mEffect1));
+        assertThat(new ParsedVibration(mEffect1)).isEqualTo(new ParsedVibration(mEffect1));
+        assertThat(new ParsedVibration(mEffect1)).isNotEqualTo(new ParsedVibration(mEffect2));
+        assertThat(new ParsedVibration(List.of(mEffect1, mEffect2, mEffect3)))
+                .isEqualTo(new ParsedVibration(List.of(mEffect1, mEffect2, mEffect3)));
+        assertThat(new ParsedVibration(List.of(mEffect1, mEffect2)))
+                .isNotEqualTo(new ParsedVibration(List.of(mEffect2, mEffect1)));
+    }
+
+    @Test
+    public void testHashCode() {
+        assertThat(new ParsedVibration(mEffect1).hashCode())
+                .isEqualTo(new ParsedVibration(mEffect1).hashCode());
+        assertThat(new ParsedVibration(mEffect1).hashCode())
+                .isNotEqualTo(new ParsedVibration(mEffect2).hashCode());
+        assertThat(new ParsedVibration(List.of()).hashCode())
+                .isEqualTo(new ParsedVibration(List.of()).hashCode());
+        assertThat(new ParsedVibration(List.of()).hashCode())
+                .isNotEqualTo(new ParsedVibration(mEffect1).hashCode());
+        assertThat(new ParsedVibration(List.of(mEffect1, mEffect2, mEffect3)).hashCode())
+                .isEqualTo(new ParsedVibration(List.of(mEffect1, mEffect2, mEffect3)).hashCode());
+        assertThat(new ParsedVibration(List.of(mEffect1, mEffect2)).hashCode())
+                .isNotEqualTo(new ParsedVibration(List.of(mEffect2, mEffect1)).hashCode());
+    }
+
+    @Test
     public void testResolve_allUnsupportedVibrations() {
         when(mVibratorInfoMock.areVibrationFeaturesSupported(any())).thenReturn(false);
 
@@ -91,21 +119,6 @@
                 .isEqualTo(mEffect1);
     }
 
-    @Test
-    public void testGetVibrationEffects() {
-        ParsedVibration parsedVibration =
-                new ParsedVibration(List.of(mEffect1, mEffect2, mEffect3));
-        assertThat(parsedVibration.getVibrationEffects())
-                .containsExactly(mEffect1, mEffect2, mEffect3)
-                .inOrder();
-
-        parsedVibration = new ParsedVibration(List.of(mEffect1));
-        assertThat(parsedVibration.getVibrationEffects()).containsExactly(mEffect1);
-
-        parsedVibration = new ParsedVibration(List.of());
-        assertThat(parsedVibration.getVibrationEffects()).isEmpty();
-    }
-
     private Subject assertThatResolution(
             Vibrator vibrator, List<VibrationEffect> componentVibrations) {
         return assertThat(new ParsedVibration(componentVibrations).resolve(vibrator));
diff --git a/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java b/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java
index 7d8c53f..bf9a820 100644
--- a/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java
+++ b/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java
@@ -37,9 +37,9 @@
 import org.junit.runners.JUnit4;
 import org.xmlpull.v1.XmlPullParser;
 
-import java.io.IOException;
 import java.io.StringReader;
 import java.io.StringWriter;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Set;
@@ -74,18 +74,22 @@
                 .addPrimitive(PRIMITIVE_CLICK)
                 .addPrimitive(PRIMITIVE_TICK, 0.2497f)
                 .compose();
-        String xml = "<vibration-effect>"
-                + "<primitive-effect name=\"click\"/>"
-                + "<primitive-effect name=\"tick\" scale=\"0.2497\"/>"
-                + "</vibration-effect>";
+        String xml = """
+                <vibration-effect>
+                    <primitive-effect name="click"/>
+                    <primitive-effect name="tick" scale="0.2497"/>
+                </vibration-effect>
+                """.trim();
         VibrationEffect effect2 = VibrationEffect.startComposition()
                 .addPrimitive(PRIMITIVE_LOW_TICK, 1f, 356)
                 .addPrimitive(PRIMITIVE_SPIN, 0.6364f, 7)
                 .compose();
-        String xml2 = "<vibration-effect>"
-                + "<primitive-effect name=\"low_tick\" delayMs=\"356\"/>"
-                + "<primitive-effect name=\"spin\" scale=\"0.6364\" delayMs=\"7\"/>"
-                + "</vibration-effect>";
+        String xml2 = """
+                <vibration-effect>
+                    <primitive-effect name="low_tick" delayMs="356"/>
+                    <primitive-effect name="spin" scale="0.6364" delayMs="7"/>
+                </vibration-effect>
+                """.trim();
 
         TypedXmlPullParser parser = createXmlPullParser(xml);
         assertParseElementSucceeds(parser, effect);
@@ -114,7 +118,12 @@
         assertEndOfDocument(parser);
 
         // Check when there is comment before the end tag.
-        xml = "<vibration-effect><primitive-effect name=\"tick\"/><!-- hi --></vibration-effect>";
+        xml = """
+            <vibration-effect>
+                <primitive-effect name="tick"/>
+                <!-- hi -->
+            </vibration-effect>
+            """.trim();
         parser = createXmlPullParser(xml);
         assertParseElementSucceeds(
                 parser, VibrationEffect.startComposition().addPrimitive(PRIMITIVE_TICK).compose());
@@ -128,18 +137,22 @@
                 .addPrimitive(PRIMITIVE_CLICK)
                 .addPrimitive(PRIMITIVE_TICK, 0.2497f)
                 .compose();
-        String vibrationXml1 = "<vibration-effect>"
-                + "<primitive-effect name=\"click\"/>"
-                + "<primitive-effect name=\"tick\" scale=\"0.2497\"/>"
-                + "</vibration-effect>";
+        String vibrationXml1 = """
+                <vibration-effect>
+                    <primitive-effect name="click"/>
+                    <primitive-effect name="tick" scale="0.2497"/>
+                </vibration-effect>
+                """.trim();
         VibrationEffect effect2 = VibrationEffect.startComposition()
                 .addPrimitive(PRIMITIVE_LOW_TICK, 1f, 356)
                 .addPrimitive(PRIMITIVE_SPIN, 0.6364f, 7)
                 .compose();
-        String vibrationXml2 = "<vibration-effect>"
-                + "<primitive-effect name=\"low_tick\" delayMs=\"356\"/>"
-                + "<primitive-effect name=\"spin\" scale=\"0.6364\" delayMs=\"7\"/>"
-                + "</vibration-effect>";
+        String vibrationXml2 = """
+                <vibration-effect>
+                    <primitive-effect name="low_tick" delayMs="356"/>
+                    <primitive-effect name="spin" scale="0.6364" delayMs="7"/>
+                </vibration-effect>
+                """.trim();
 
         String xml = "<vibration-select>" + vibrationXml1 + vibrationXml2 + "</vibration-select>";
         TypedXmlPullParser parser = createXmlPullParser(xml);
@@ -183,8 +196,11 @@
     @Test
     public void testParseElement_withHiddenApis_onlySucceedsWithFlag() throws Exception {
         // Check when the root tag is "vibration".
-        String xml =
-                "<vibration-effect><predefined-effect name=\"texture_tick\"/></vibration-effect>";
+        String xml = """
+                <vibration-effect>
+                    <predefined-effect name="texture_tick"/>
+                </vibration-effect>
+                """.trim();
         assertParseElementSucceeds(createXmlPullParser(xml),
                 VibrationXmlSerializer.FLAG_ALLOW_HIDDEN_APIS,
                 VibrationEffect.get(VibrationEffect.EFFECT_TEXTURE_TICK));
@@ -199,131 +215,186 @@
     }
 
     @Test
-    public void testParseElement_badXml_throwsException() throws Exception {
+    public void testParseElement_badXml_throwsException() {
         // No "vibration-select" tag.
-        assertParseElementFails(
-                "<vibration-effect>rand text<primitive-effect name=\"click\"/></vibration-effect>");
-        assertParseElementFails("<bad-tag><primitive-effect name=\"click\"/></vibration-effect>");
-        assertParseElementFails("<primitive-effect name=\"click\"/></vibration-effect>");
-        assertParseElementFails("<vibration-effect><primitive-effect name=\"click\"/>");
+        assertParseElementFails("""
+                <vibration-effect>
+                    rand text
+                    <primitive-effect name="click"/>
+                </vibration-effect>
+                """);
+        assertParseElementFails("""
+                <bad-tag>
+                    <primitive-effect name="click"/>
+                </vibration-effect>
+                """);
+        assertParseElementFails("""
+                <primitive-effect name="click"/>
+                </vibration-effect>
+                """);
+        assertParseElementFails("""
+                <vibration-effect>
+                    <primitive-effect name="click"/>
+                """);
 
         // Incomplete XML.
-        assertParseElementFails("<vibration-select><primitive-effect name=\"click\"/>");
-        assertParseElementFails("<vibration-select>"
-                + "<vibration-effect>"
-                + "<primitive-effect name=\"low_tick\" delayMs=\"356\"/>"
-                + "</vibration-effect>");
+        assertParseElementFails("""
+                <vibration-select>
+                    <primitive-effect name="click"/>
+                """);
+        assertParseElementFails("""
+                <vibration-select>
+                    <vibration-effect>
+                        <primitive-effect name="low_tick" delayMs="356"/>
+                    </vibration-effect>
+                """);
 
         // Bad vibration XML.
-        assertParseElementFails("<vibration-select>"
-                + "<primitive-effect name=\"low_tick\" delayMs=\"356\"/>"
-                + "</vibration-effect>"
-                + "</vibration-select>");
+        assertParseElementFails("""
+                <vibration-select>
+                    <primitive-effect name="low_tick" delayMs="356"/>
+                    </vibration-effect>
+                </vibration-select>
+                """);
 
         // "vibration-select" tag should have no attributes.
-        assertParseElementFails("<vibration-select bad_attr=\"123\">"
-                + "<vibration-effect>"
-                + "<predefined-effect name=\"tick\"/>"
-                + "</vibration-effect>"
-                + "</vibration-select>");
+        assertParseElementFails("""
+                <vibration-select bad_attr="123">
+                    <vibration-effect>
+                        <predefined-effect name="tick"/>
+                    </vibration-effect>
+                </vibration-select>
+                """);
     }
 
     @Test
-    public void testPrimitives_allSucceed() throws IOException {
+    public void testInvalidEffects_allFail() {
+        // Invalid root tag.
+        String xml = """
+                <vibration>
+                    <predefined-effect name="click"/>
+                </vibration>
+                """;
+
+        assertPublicApisParserFails(xml);
+        assertHiddenApisParserFails(xml);
+
+        // Invalid effect name.
+        xml = """
+                <vibration-effect>
+                    <predefined-effect name="invalid"/>
+                </vibration-effect>
+                """;
+
+        assertPublicApisParserFails(xml);
+        assertHiddenApisParserFails(xml);
+    }
+
+    @Test
+    public void testVibrationSelectTag_onlyParseDocumentSucceeds() throws Exception {
+        VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
+        String xml = """
+                <vibration-select>
+                    <vibration-effect><predefined-effect name="click"/></vibration-effect>
+                </vibration-select>
+                """;
+
+        assertPublicApisParseDocumentSucceeds(xml, effect);
+        assertHiddenApisParseDocumentSucceeds(xml, effect);
+
+        assertPublicApisParseVibrationEffectFails(xml);
+        assertHiddenApisParseVibrationEffectFails(xml);
+    }
+
+    @Test
+    public void testPrimitives_allSucceed() throws Exception {
         VibrationEffect effect = VibrationEffect.startComposition()
                 .addPrimitive(PRIMITIVE_CLICK)
                 .addPrimitive(PRIMITIVE_TICK, 0.2497f)
                 .addPrimitive(PRIMITIVE_LOW_TICK, 1f, 356)
                 .addPrimitive(PRIMITIVE_SPIN, 0.6364f, 7)
                 .compose();
-        String xml = "<vibration-effect>"
-                + "<primitive-effect name=\"click\"/>"
-                + "<primitive-effect name=\"tick\" scale=\"0.2497\"/>"
-                + "<primitive-effect name=\"low_tick\" delayMs=\"356\"/>"
-                + "<primitive-effect name=\"spin\" scale=\"0.6364\" delayMs=\"7\"/>"
-                + "</vibration-effect>";
+        String xml = """
+                <vibration-effect>
+                    <primitive-effect name="click"/>
+                    <primitive-effect name="tick" scale="0.2497"/>
+                    <primitive-effect name="low_tick" delayMs="356"/>
+                    <primitive-effect name="spin" scale="0.6364" delayMs="7"/>
+                </vibration-effect>
+                """;
 
         assertPublicApisParserSucceeds(xml, effect);
         assertPublicApisSerializerSucceeds(effect, "click", "tick", "low_tick", "spin");
         assertPublicApisRoundTrip(effect);
 
-        assertHiddenApisParseVibrationEffectSucceeds(xml, effect);
+        assertHiddenApisParserSucceeds(xml, effect);
         assertHiddenApisSerializerSucceeds(effect, "click", "tick", "low_tick", "spin");
         assertHiddenApisRoundTrip(effect);
     }
 
     @Test
-    public void testParseDocument_withVibrationSelectTag_withHiddenApis_onlySucceedsWithFlag()
-            throws Exception {
-        // Check when the root tag is "vibration-effect".
-        String xml =
-                "<vibration-effect><predefined-effect name=\"texture_tick\"/></vibration-effect>";
-        assertParseDocumentSucceeds(xml,
-                VibrationXmlSerializer.FLAG_ALLOW_HIDDEN_APIS,
-                VibrationEffect.get(VibrationEffect.EFFECT_TEXTURE_TICK));
-        assertThat(parseDocument(xml, /* flags= */ 0)).isNull();
-
-        // Check when the root tag is "vibration-select".
-        xml = "<vibration-select>" + xml + "</vibration-select>";
-        assertParseDocumentSucceeds(xml,
-                VibrationXmlSerializer.FLAG_ALLOW_HIDDEN_APIS,
-                VibrationEffect.get(VibrationEffect.EFFECT_TEXTURE_TICK));
-        assertThat(parseDocument(xml, /* flags= */ 0)).isNull();
-    }
-
-    @Test
-    public void testWaveforms_allSucceed() throws IOException {
+    public void testWaveforms_allSucceed() throws Exception {
         VibrationEffect effect = VibrationEffect.createWaveform(new long[]{123, 456, 789, 0},
                 new int[]{254, 1, 255, 0}, /* repeat= */ 0);
-        String xml = "<vibration-effect>"
-                + "<waveform-effect><repeating>"
-                + "<waveform-entry durationMs=\"123\" amplitude=\"254\"/>"
-                + "<waveform-entry durationMs=\"456\" amplitude=\"1\"/>"
-                + "<waveform-entry durationMs=\"789\" amplitude=\"255\"/>"
-                + "<waveform-entry durationMs=\"0\" amplitude=\"0\"/>"
-                + "</repeating></waveform-effect>"
-                + "</vibration-effect>";
+        String xml = """
+                <vibration-effect>
+                    <waveform-effect>
+                        <repeating>
+                            <waveform-entry durationMs="123" amplitude="254"/>
+                            <waveform-entry durationMs="456" amplitude="1"/>
+                            <waveform-entry durationMs="789" amplitude="255"/>
+                            <waveform-entry durationMs="0" amplitude="0"/>
+                        </repeating>
+                    </waveform-effect>
+                </vibration-effect>
+                """;
 
         assertPublicApisParserSucceeds(xml, effect);
         assertPublicApisSerializerSucceeds(effect, "123", "456", "789", "254", "1", "255", "0");
         assertPublicApisRoundTrip(effect);
 
-        assertHiddenApisParseVibrationEffectSucceeds(xml, effect);
+        assertHiddenApisParserSucceeds(xml, effect);
         assertHiddenApisSerializerSucceeds(effect, "123", "456", "789", "254", "1", "255", "0");
         assertHiddenApisRoundTrip(effect);
     }
 
     @Test
     public void testPredefinedEffects_publicEffectsWithDefaultFallback_allSucceed()
-            throws IOException {
+            throws Exception {
         for (Map.Entry<String, Integer> entry : createPublicPredefinedEffectsMap().entrySet()) {
             VibrationEffect effect = VibrationEffect.get(entry.getValue());
-            String xml = String.format(
-                    "<vibration-effect><predefined-effect name=\"%s\"/></vibration-effect>",
+            String xml = String.format("""
+                    <vibration-effect>
+                        <predefined-effect name="%s"/>
+                    </vibration-effect>
+                    """,
                     entry.getKey());
 
             assertPublicApisParserSucceeds(xml, effect);
             assertPublicApisSerializerSucceeds(effect, entry.getKey());
             assertPublicApisRoundTrip(effect);
 
-            assertHiddenApisParseVibrationEffectSucceeds(xml, effect);
+            assertHiddenApisParserSucceeds(xml, effect);
             assertHiddenApisSerializerSucceeds(effect, entry.getKey());
             assertHiddenApisRoundTrip(effect);
         }
     }
 
     @Test
-    public void testPredefinedEffects_hiddenEffects_onlySucceedsWithFlag() throws IOException {
+    public void testPredefinedEffects_hiddenEffects_onlySucceedsWithFlag() throws Exception {
         for (Map.Entry<String, Integer> entry : createHiddenPredefinedEffectsMap().entrySet()) {
             VibrationEffect effect = VibrationEffect.get(entry.getValue());
-            String xml = String.format(
-                    "<vibration-effect><predefined-effect name=\"%s\"/></vibration-effect>",
+            String xml = String.format("""
+                    <vibration-effect>
+                        <predefined-effect name="%s"/>
+                    </vibration-effect>
+                    """,
                     entry.getKey());
 
             assertPublicApisParserFails(xml);
             assertPublicApisSerializerFails(effect);
 
-            assertHiddenApisParseVibrationEffectSucceeds(xml, effect);
+            assertHiddenApisParserSucceeds(xml, effect);
             assertHiddenApisSerializerSucceeds(effect, entry.getKey());
             assertHiddenApisRoundTrip(effect);
         }
@@ -331,33 +402,119 @@
 
     @Test
     public void testPredefinedEffects_allEffectsWithNonDefaultFallback_onlySucceedsWithFlag()
-            throws IOException {
+            throws Exception {
         for (Map.Entry<String, Integer> entry : createAllPredefinedEffectsMap().entrySet()) {
             boolean nonDefaultFallback = !PrebakedSegment.DEFAULT_SHOULD_FALLBACK;
             VibrationEffect effect = VibrationEffect.get(entry.getValue(), nonDefaultFallback);
-            String xml = String.format(
-                    "<vibration-effect><predefined-effect name=\"%s\" fallback=\"%s\"/>"
-                            + "</vibration-effect>",
+            String xml = String.format("""
+                    <vibration-effect>
+                        <predefined-effect name="%s" fallback="%s"/>
+                    </vibration-effect>
+                    """,
                     entry.getKey(), nonDefaultFallback);
 
             assertPublicApisParserFails(xml);
             assertPublicApisSerializerFails(effect);
 
-            assertHiddenApisParseVibrationEffectSucceeds(xml, effect);
+            assertHiddenApisParserSucceeds(xml, effect);
             assertHiddenApisSerializerSucceeds(effect, entry.getKey());
             assertHiddenApisRoundTrip(effect);
         }
     }
 
-    private void assertPublicApisParserFails(String xml) throws IOException {
-        assertThat(parseVibrationEffect(xml, /* flags= */ 0)).isNull();
+    private void assertPublicApisParserFails(String xml) {
+        assertThrows("Expected parseVibrationEffect to fail for " + xml,
+                VibrationXmlParser.ParseFailedException.class,
+                () -> parseVibrationEffect(xml, /* flags= */ 0));
+        assertThrows("Expected parseDocument to fail for " + xml,
+                VibrationXmlParser.ParseFailedException.class,
+                () -> parseDocument(xml, /* flags= */ 0));
+    }
+
+    private void assertPublicApisParseVibrationEffectFails(String xml) {
+        assertThrows("Expected parseVibrationEffect to fail for " + xml,
+                VibrationXmlParser.ParseFailedException.class,
+                () -> parseVibrationEffect(xml, /* flags= */ 0));
     }
 
     private void assertPublicApisParserSucceeds(String xml, VibrationEffect effect)
-            throws IOException {
+            throws Exception {
+        assertPublicApisParseDocumentSucceeds(xml, effect);
+        assertPublicApisParseVibrationEffectSucceeds(xml, effect);
+    }
+
+    private void assertPublicApisParseDocumentSucceeds(String xml, VibrationEffect... effects)
+            throws Exception {
+        assertThat(parseDocument(xml, /* flags= */ 0))
+                .isEqualTo(new ParsedVibration(Arrays.asList(effects)));
+    }
+
+    private void assertPublicApisParseVibrationEffectSucceeds(String xml, VibrationEffect effect)
+            throws Exception {
         assertThat(parseVibrationEffect(xml, /* flags= */ 0)).isEqualTo(effect);
     }
 
+    private void assertHiddenApisParserFails(String xml) {
+        assertThrows("Expected parseVibrationEffect to fail for " + xml,
+                VibrationXmlParser.ParseFailedException.class,
+                () -> parseVibrationEffect(xml, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS));
+        assertThrows("Expected parseDocument to fail for " + xml,
+                VibrationXmlParser.ParseFailedException.class,
+                () -> parseDocument(xml, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS));
+    }
+
+    private void assertHiddenApisParseVibrationEffectFails(String xml) {
+        assertThrows("Expected parseVibrationEffect to fail for " + xml,
+                VibrationXmlParser.ParseFailedException.class,
+                () -> parseVibrationEffect(xml, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS));
+    }
+
+    private void assertHiddenApisParserSucceeds(String xml, VibrationEffect effect)
+            throws Exception {
+        assertHiddenApisParseDocumentSucceeds(xml, effect);
+        assertHiddenApisParseVibrationEffectSucceeds(xml, effect);
+    }
+
+    private void assertHiddenApisParseDocumentSucceeds(String xml, VibrationEffect... effect)
+            throws Exception {
+        assertThat(parseDocument(xml, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS))
+                .isEqualTo(new ParsedVibration(Arrays.asList(effect)));
+    }
+
+    private void assertHiddenApisParseVibrationEffectSucceeds(String xml, VibrationEffect effect)
+            throws Exception {
+        assertThat(parseVibrationEffect(xml, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS))
+                .isEqualTo(effect);
+    }
+
+    private void assertPublicApisSerializerFails(VibrationEffect effect) {
+        assertThrows("Expected serialization to fail for " + effect,
+                VibrationXmlSerializer.SerializationFailedException.class,
+                () -> serialize(effect));
+    }
+
+    private void assertPublicApisSerializerSucceeds(VibrationEffect effect,
+            String... expectedSegments) throws Exception {
+        assertSerializationContainsSegments(serialize(effect), expectedSegments);
+    }
+
+    private void assertHiddenApisSerializerSucceeds(VibrationEffect effect,
+            String... expectedSegments) throws Exception {
+        assertSerializationContainsSegments(
+                serialize(effect, VibrationXmlSerializer.FLAG_ALLOW_HIDDEN_APIS), expectedSegments);
+    }
+
+    private void assertPublicApisRoundTrip(VibrationEffect effect) throws Exception {
+        assertThat(parseVibrationEffect(serialize(effect, /* flags= */ 0), /* flags= */ 0))
+                .isEqualTo(effect);
+    }
+
+    private void assertHiddenApisRoundTrip(VibrationEffect effect) throws Exception {
+        String xml = serialize(effect, VibrationXmlSerializer.FLAG_ALLOW_HIDDEN_APIS);
+        assertThat(parseVibrationEffect(xml, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS))
+                .isEqualTo(effect);
+    }
+
     private TypedXmlPullParser createXmlPullParser(String xml) throws Exception {
         TypedXmlPullParser parser = Xml.newFastPullParser();
         parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
@@ -366,11 +523,6 @@
         return parser;
     }
 
-    private void assertParseDocumentSucceeds(String xml, int flags, VibrationEffect... effects)
-            throws Exception {
-        assertThat(parseDocument(xml, flags).getVibrationEffects()).containsExactly(effects);
-    }
-
     /**
      * Asserts parsing vibration from an open TypedXmlPullParser succeeds, and that the parser
      * points to the end "vibration" or "vibration-select" tag.
@@ -385,7 +537,8 @@
         String tagName = parser.getName();
         assertThat(Set.of("vibration-effect", "vibration-select")).contains(tagName);
 
-        assertThat(parseElement(parser, flags).getVibrationEffects()).containsExactly(effects);
+        assertThat(parseElement(parser, flags))
+                .isEqualTo(new ParsedVibration(Arrays.asList(effects)));
         assertThat(parser.getEventType()).isEqualTo(XmlPullParser.END_TAG);
         assertThat(parser.getName()).isEqualTo(tagName);
     }
@@ -405,69 +558,40 @@
         assertThat(parser.getEventType()).isEqualTo(parser.END_DOCUMENT);
     }
 
-    private void assertHiddenApisParseVibrationEffectSucceeds(String xml, VibrationEffect effect)
-            throws IOException {
-        assertThat(parseVibrationEffect(xml, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS))
-                .isEqualTo(effect);
-    }
-
-    private void assertPublicApisSerializerFails(VibrationEffect effect) {
-        assertThrows("Expected serialization to fail for " + effect,
-                VibrationXmlSerializer.SerializationFailedException.class,
-                () -> serialize(effect, /* flags= */ 0));
-    }
-
     private void assertParseElementFails(String xml) {
         assertThrows("Expected parsing to fail for " + xml,
-                VibrationXmlParser.VibrationXmlParserException.class,
+                VibrationXmlParser.ParseFailedException.class,
                 () -> parseElement(createXmlPullParser(xml), /* flags= */ 0));
     }
 
-    private void assertPublicApisSerializerSucceeds(VibrationEffect effect,
-            String... expectedSegments) throws IOException {
-        assertSerializationContainsSegments(serialize(effect, /* flags= */ 0), expectedSegments);
-    }
-
-    private void assertHiddenApisSerializerSucceeds(VibrationEffect effect,
-            String... expectedSegments) throws IOException {
-        assertSerializationContainsSegments(
-                serialize(effect, VibrationXmlSerializer.FLAG_ALLOW_HIDDEN_APIS), expectedSegments);
-    }
-
     private void assertSerializationContainsSegments(String xml, String[] expectedSegments) {
         for (String expectedSegment : expectedSegments) {
             assertThat(xml).contains(expectedSegment);
         }
     }
 
-    private void assertPublicApisRoundTrip(VibrationEffect effect) throws IOException {
-        assertThat(parseVibrationEffect(serialize(effect, /* flags= */ 0), /* flags= */ 0))
-                .isEqualTo(effect);
-    }
-
-    private void assertHiddenApisRoundTrip(VibrationEffect effect) throws IOException {
-        String xml = serialize(effect, VibrationXmlSerializer.FLAG_ALLOW_HIDDEN_APIS);
-        assertThat(parseVibrationEffect(xml, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS))
-                .isEqualTo(effect);
-    }
-
     private static VibrationEffect parseVibrationEffect(
-            String xml, @VibrationXmlParser.Flags int flags) throws IOException {
+            String xml, @VibrationXmlParser.Flags int flags) throws Exception {
         return VibrationXmlParser.parseVibrationEffect(new StringReader(xml), flags);
     }
 
-    private static ParsedVibration parseDocument(String xml, int flags)
-            throws IOException {
+    private static ParsedVibration parseDocument(String xml, int flags) throws Exception {
         return VibrationXmlParser.parseDocument(new StringReader(xml), flags);
     }
 
     private static ParsedVibration parseElement(TypedXmlPullParser parser, int flags)
-            throws IOException, VibrationXmlParser.VibrationXmlParserException {
+            throws Exception {
         return VibrationXmlParser.parseElement(parser, flags);
     }
 
+    private static String serialize(VibrationEffect effect) throws Exception {
+        StringWriter writer = new StringWriter();
+        VibrationXmlSerializer.serialize(effect, writer);
+        return writer.toString();
+    }
+
     private static String serialize(VibrationEffect effect, @VibrationXmlSerializer.Flags int flags)
-            throws IOException {
+            throws Exception {
         StringWriter writer = new StringWriter();
         VibrationXmlSerializer.serialize(effect, writer, flags);
         return writer.toString();
diff --git a/graphics/java/android/graphics/SurfaceTexture.java b/graphics/java/android/graphics/SurfaceTexture.java
index 3256f31..5caedba 100644
--- a/graphics/java/android/graphics/SurfaceTexture.java
+++ b/graphics/java/android/graphics/SurfaceTexture.java
@@ -416,7 +416,8 @@
     }
 
     /**
-     * Retrieve the dataspace associated with the texture image.
+     * Retrieve the dataspace associated with the texture image
+     * set by the most recent call to {@link #updateTexImage}.
      */
     @SuppressLint("MethodNameUnits")
     public @NamedDataSpace int getDataSpace() {
diff --git a/libs/WindowManager/Shell/AndroidManifest.xml b/libs/WindowManager/Shell/AndroidManifest.xml
index bcb1d29..52ae93f 100644
--- a/libs/WindowManager/Shell/AndroidManifest.xml
+++ b/libs/WindowManager/Shell/AndroidManifest.xml
@@ -29,6 +29,7 @@
             android:name=".desktopmode.DesktopWallpaperActivity"
             android:excludeFromRecents="true"
             android:launchMode="singleInstance"
+            android:showForAllUsers="true"
             android:theme="@style/DesktopWallpaperTheme" />
 
         <activity
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
index 1279fc4..2aefc64 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
@@ -894,11 +894,22 @@
     }
 
     @Nullable
-    Intent getAppBubbleIntent() {
+    @VisibleForTesting
+    public Intent getAppBubbleIntent() {
         return mAppIntent;
     }
 
     /**
+     * Sets the intent for a bubble that is an app bubble (one for which {@link #mIsAppBubble} is
+     * true).
+     *
+     * @param appIntent The intent to set for the app bubble.
+     */
+    void setAppBubbleIntent(Intent appIntent) {
+        mAppIntent = appIntent;
+    }
+
+    /**
      * Returns whether this bubble is from an app versus a notification.
      */
     public boolean isAppBubble() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index d2c36e6..c853301 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -1450,6 +1450,8 @@
             if (b != null) {
                 // It's in the overflow, so remove it & reinflate
                 mBubbleData.dismissBubbleWithKey(appBubbleKey, Bubbles.DISMISS_NOTIF_CANCEL);
+                // Update the bubble entry in the overflow with the latest intent.
+                b.setAppBubbleIntent(intent);
             } else {
                 // App bubble does not exist, lets add and expand it
                 b = Bubble.createAppBubble(intent, user, icon, mMainExecutor);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 87bd840..1fcfa7f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -23,6 +23,7 @@
 import android.os.UserManager;
 import android.view.Choreographer;
 import android.view.IWindowManager;
+import android.view.SurfaceControl;
 import android.view.WindowManager;
 
 import com.android.internal.jank.InteractionJankMonitor;
@@ -400,7 +401,8 @@
             Optional<RecentTasksController> recentTasksController,
             HomeTransitionObserver homeTransitionObserver) {
         return new RecentsTransitionHandler(shellInit, transitions,
-                recentTasksController.orElse(null), homeTransitionObserver);
+                recentTasksController.orElse(null), homeTransitionObserver,
+                SurfaceControl.Transaction::new);
     }
 
     //
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
index 9bf244e..81891ce 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
@@ -47,7 +47,7 @@
         val visibleTasks: ArraySet<Int> = ArraySet(),
         val minimizedTasks: ArraySet<Int> = ArraySet(),
         // Tasks that are closing, but are still visible
-        //  TODO(b/332682201): Remove when the repository state is updated via TransitionObserver
+        // TODO(b/332682201): Remove when the repository state is updated via TransitionObserver
         val closingTasks: ArraySet<Int> = ArraySet(),
         // Tasks currently in freeform mode, ordered from top to bottom (top is at index 0).
         val freeformTasksInZOrder: ArrayList<Int> = ArrayList(),
@@ -234,17 +234,16 @@
     }
 
     /**
-     * Check if a task with the given [taskId] is the only active, non-closing, not-minimized task
+     * Check if a task with the given [taskId] is the only visible, non-closing, not-minimized task
      * on its display
      */
-    fun isOnlyActiveNonClosingTask(taskId: Int): Boolean {
-        return displayData.valueIterator().asSequence().any { data ->
-            data.activeTasks
+    fun isOnlyVisibleNonClosingTask(taskId: Int): Boolean =
+        displayData.valueIterator().asSequence().any { data ->
+            data.visibleTasks
                 .subtract(data.closingTasks)
                 .subtract(data.minimizedTasks)
                 .singleOrNull() == taskId
         }
-    }
 
     /** Get a set of the active tasks for given [displayId] */
     fun getActiveTasks(displayId: Int): ArraySet<Int> {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index d28cda3..14ae3a7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -40,7 +40,6 @@
 import android.view.WindowManager.TRANSIT_CHANGE
 import android.view.WindowManager.TRANSIT_NONE
 import android.view.WindowManager.TRANSIT_OPEN
-import android.view.WindowManager.TRANSIT_TO_BACK
 import android.view.WindowManager.TRANSIT_TO_FRONT
 import android.window.RemoteTransition
 import android.window.TransitionInfo
@@ -76,6 +75,7 @@
 import com.android.wm.shell.shared.DesktopModeStatus
 import com.android.wm.shell.shared.DesktopModeStatus.DESKTOP_DENSITY_OVERRIDE
 import com.android.wm.shell.shared.DesktopModeStatus.useDesktopOverrideDensity
+import com.android.wm.shell.shared.TransitionUtil
 import com.android.wm.shell.shared.annotations.ExternalThread
 import com.android.wm.shell.shared.annotations.ShellMainThread
 import com.android.wm.shell.splitscreen.SplitScreenController
@@ -446,7 +446,7 @@
      * @param taskId task id of the window that's being closed
      */
     fun onDesktopWindowClose(wct: WindowContainerTransaction, displayId: Int, taskId: Int) {
-        if (desktopModeTaskRepository.isOnlyActiveNonClosingTask(taskId)) {
+        if (desktopModeTaskRepository.isOnlyVisibleNonClosingTask(taskId)) {
             removeWallpaperActivity(wct)
         }
         if (!desktopModeTaskRepository.addClosingTask(displayId, taskId)) {
@@ -879,8 +879,8 @@
                     reason = "recents animation is running"
                     false
                 }
-                // Handle back navigation for the last window if wallpaper available
-                shouldHandleBackNavigation(request) -> true
+                // Handle task closing for the last window if wallpaper is available
+                shouldHandleTaskClosing(request) -> true
                 // Only handle open or to front transitions
                 request.type != TRANSIT_OPEN && request.type != TRANSIT_TO_FRONT -> {
                     reason = "transition type not handled (${request.type})"
@@ -918,7 +918,8 @@
         val result =
             triggerTask?.let { task ->
                 when {
-                    request.type == TRANSIT_TO_BACK -> handleBackNavigation(task)
+                    // Check if the closing task needs to be handled
+                    TransitionUtil.isClosingType(request.type) -> handleTaskClosing(task)
                     // Check if the task has a top transparent activity
                     shouldLaunchAsModal(task) -> handleIncompatibleTaskLaunch(task)
                     // Check if the task has a top systemUI activity
@@ -960,9 +961,10 @@
     private fun shouldLaunchAsModal(task: TaskInfo) =
         Flags.enableDesktopWindowingModalsPolicy() && isSingleTopActivityTranslucent(task)
 
-    private fun shouldHandleBackNavigation(request: TransitionRequestInfo): Boolean {
+    private fun shouldHandleTaskClosing(request: TransitionRequestInfo): Boolean {
         return Flags.enableDesktopWindowingWallpaperActivity() &&
-            request.type == TRANSIT_TO_BACK
+            TransitionUtil.isClosingType(request.type) &&
+            request.triggerTask != null
     }
 
     private fun handleFreeformTaskLaunch(
@@ -1029,10 +1031,10 @@
         return WindowContainerTransaction().also { wct -> addMoveToFullscreenChanges(wct, task) }
     }
 
-    /** Handle back navigation by removing wallpaper activity if it's the last active task */
-    private fun handleBackNavigation(task: RunningTaskInfo): WindowContainerTransaction? {
+    /** Handle task closing by removing wallpaper activity if it's the last active task */
+    private fun handleTaskClosing(task: RunningTaskInfo): WindowContainerTransaction? {
         val wct = if (
-            desktopModeTaskRepository.isOnlyActiveNonClosingTask(task.taskId) &&
+            desktopModeTaskRepository.isOnlyVisibleNonClosingTask(task.taskId) &&
                 desktopModeTaskRepository.wallpaperActivityToken != null
         ) {
             // Remove wallpaper activity when the last active task is removed
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt
index 88d0554..5335c0b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt
@@ -27,6 +27,8 @@
 import android.window.TransitionRequestInfo
 import android.window.WindowContainerTransaction
 import androidx.core.animation.addListener
+import com.android.internal.jank.Cuj
+import com.android.wm.shell.common.InteractionJankMonitorUtils
 import com.android.wm.shell.transition.Transitions
 import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_TOGGLE_RESIZE
 import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener
@@ -103,6 +105,8 @@
                             onTaskResizeAnimationListener.onAnimationEnd(taskId)
                             finishCallback.onTransitionFinished(null)
                             boundsAnimator = null
+                            InteractionJankMonitorUtils.endTracing(
+                                Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW)
                         }
                     )
                     addUpdateListener { anim ->
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md
index 438aa76..b1cbe8d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md
@@ -73,7 +73,7 @@
 following system properties for example:
 ```shell
 # Enabling
-adb shell setprop persist.wm.debug.sc.tx.log_match_call setAlpha  # matches the name of the SurfaceControlTransaction method
+adb shell setprop persist.wm.debug.sc.tx.log_match_call setAlpha,setPosition  # matches the name of the SurfaceControlTransaction methods
 adb shell setprop persist.wm.debug.sc.tx.log_match_name com.android.systemui # matches the name of the surface
 adb reboot
 adb logcat -s "SurfaceControlRegistry"
@@ -87,6 +87,16 @@
 It is not necessary to set both `log_match_call` and `log_match_name`, but note logs can be quite
 noisy if unfiltered.
 
+It can sometimes be useful to trace specific logs and when they are applied (sometimes we build
+transactions that can be applied later).  You can do this by adding the "merge" and "apply" calls to
+the set of requested calls:
+```shell
+# Enabling
+adb shell setprop persist.wm.debug.sc.tx.log_match_call setAlpha,merge,apply  # apply will dump logs of each setAlpha or merge call on that tx
+adb reboot
+adb logcat -s "SurfaceControlRegistry"
+```
+
 ## Tracing activity starts in the app process
 
 It's sometimes useful to know when to see a stack trace of when an activity starts in the app code
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
index b88afb9..b48aee5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
@@ -122,10 +122,6 @@
             mDesktopModeTaskRepository.ifPresent(repository -> {
                 repository.removeFreeformTask(taskInfo.displayId, taskInfo.taskId);
                 repository.unminimizeTask(taskInfo.displayId, taskInfo.taskId);
-                if (repository.removeClosingTask(taskInfo.taskId)) {
-                    ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
-                            "Removing closing freeform task: #%d", taskInfo.taskId);
-                }
                 if (repository.removeActiveTask(taskInfo.taskId)) {
                     ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
                             "Removing active freeform task: #%d", taskInfo.taskId);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
index 03c8cf8..9f3c519 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
@@ -409,10 +409,6 @@
             if (DesktopModeStatus.canEnterDesktopMode(mContext)
                     && mDesktopModeTaskRepository.isPresent()
                     && mDesktopModeTaskRepository.get().isActiveTask(taskInfo.taskId)) {
-                if (mDesktopModeTaskRepository.get().isMinimizedTask(taskInfo.taskId)) {
-                    // Minimized freeform tasks should not be shown at all.
-                    continue;
-                }
                 // Freeform tasks will be added as a separate entry
                 if (mostRecentFreeformTaskIndex == Integer.MAX_VALUE) {
                     mostRecentFreeformTaskIndex = recentTasks.size();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
index 3a266d9..c67cf1d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
@@ -74,6 +74,7 @@
 
 import java.util.ArrayList;
 import java.util.function.Consumer;
+import java.util.function.Supplier;
 
 /**
  * Handles the Recents (overview) animation. Only one of these can run at a time. A recents
@@ -84,6 +85,7 @@
 
     private final Transitions mTransitions;
     private final ShellExecutor mExecutor;
+    private final Supplier<SurfaceControl.Transaction> mTransactionSupplier;
     @Nullable
     private final RecentTasksController mRecentTasksController;
     private IApplicationThread mAnimApp = null;
@@ -101,11 +103,13 @@
 
     public RecentsTransitionHandler(ShellInit shellInit, Transitions transitions,
             @Nullable RecentTasksController recentTasksController,
-            HomeTransitionObserver homeTransitionObserver) {
+            HomeTransitionObserver homeTransitionObserver,
+            Supplier<SurfaceControl.Transaction> transactionSupplier) {
         mTransitions = transitions;
         mExecutor = transitions.getMainExecutor();
         mRecentTasksController = recentTasksController;
         mHomeTransitionObserver = homeTransitionObserver;
+        mTransactionSupplier = transactionSupplier;
         if (!Transitions.ENABLE_SHELL_TRANSITIONS) return;
         if (recentTasksController == null) return;
         shellInit.addInitCallback(() -> {
@@ -1056,7 +1060,7 @@
             final Transitions.TransitionFinishCallback finishCB = mFinishCB;
             mFinishCB = null;
 
-            final SurfaceControl.Transaction t = mFinishTransaction;
+            SurfaceControl.Transaction t = mFinishTransaction;
             final WindowContainerTransaction wct = new WindowContainerTransaction();
 
             if (mKeyguardLocked && mRecentsTask != null) {
@@ -1106,6 +1110,16 @@
                     }
                 }
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "  normal finish");
+                if (toHome && !mOpeningTasks.isEmpty()) {
+                    // Attempting to start a task after swipe to home, don't show it,
+                    // move recents to top
+                    ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+                            "  attempting to start a task after swipe to home");
+                    t = mTransactionSupplier.get();
+                    wct.reorder(mRecentsTask, true /*onTop*/);
+                    mClosingTasks.addAll(mOpeningTasks);
+                    mOpeningTasks.clear();
+                }
                 // The general case: committing to recents, going home, or switching tasks.
                 for (int i = 0; i < mOpeningTasks.size(); ++i) {
                     t.show(mOpeningTasks.get(i).mTaskSurface);
@@ -1174,6 +1188,10 @@
                     mPipTransaction = null;
                 }
             }
+            if (t != mFinishTransaction) {
+                // apply after merges because these changes are accounting for finishWCT changes.
+                mTransitions.setAfterMergeFinishTransaction(mTransition, t);
+            }
             cleanUp();
             finishCB.onTransitionFinished(wct.isEmpty() ? null : wct);
             if (runnerFinishCb != null) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index b6a18e5..45eff4a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -2649,7 +2649,7 @@
             @Nullable TransitionRequestInfo request) {
         final ActivityManager.RunningTaskInfo triggerTask = request.getTriggerTask();
         if (triggerTask == null) {
-            if (isSplitActive()) {
+            if (isSplitScreenVisible()) {
                 ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "handleRequest: transition=%d display rotation",
                         request.getDebugId());
                 // Check if the display is rotating.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java
index b03daaa..35427b9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java
@@ -94,6 +94,11 @@
         return rotatedBounds;
     }
 
+    /** Returns true if the change is put on a surface in previous rotation. */
+    public boolean isRotated(@NonNull TransitionInfo.Change change) {
+        return mLastRotationDelta != 0 && mRotatorMap.containsKey(change.getParent());
+    }
+
     /**
      * Removes the counter rotation surface in the finish transaction. No need to reparent the
      * children as the finish transaction should have already taken care of that.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index 018c904..9412b2b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -517,7 +517,8 @@
                     animRelOffset.y = Math.max(animRelOffset.y, change.getEndRelOffset().y);
                 }
 
-                if (change.getActivityComponent() != null && !isActivityLevel) {
+                if (change.getActivityComponent() != null && !isActivityLevel
+                        && !mRotator.isRotated(change)) {
                     // At this point, this is an independent activity change in a non-activity
                     // transition. This means that an activity transition got erroneously combined
                     // with another ongoing transition. This then means that the animation root may
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index f257e20..d2760ff 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -238,6 +238,13 @@
         /** Ordered list of transitions which have been merged into this one. */
         private ArrayList<ActiveTransition> mMerged;
 
+        /**
+         * @deprecated DO NOT USE THIS unless absolutely necessary. It will be removed once
+         * everything migrates off finishWCT.
+         */
+        @java.lang.Deprecated
+        SurfaceControl.Transaction mAfterMergeFinishT;
+
         ActiveTransition(IBinder token) {
             mToken = token;
         }
@@ -1018,6 +1025,20 @@
         return null;
     }
 
+    /** @deprecated */
+    @java.lang.Deprecated
+    public void setAfterMergeFinishTransaction(IBinder transition,
+            SurfaceControl.Transaction afterMergeFinishT) {
+        final ActiveTransition at = mKnownTransitions.get(transition);
+        if (at == null) return;
+        if (at.mAfterMergeFinishT != null) {
+            Log.e(TAG, "Setting after-merge-t >1 time on transition: " + at.mInfo.getDebugId());
+            at.mAfterMergeFinishT.merge(afterMergeFinishT);
+            return;
+        }
+        at.mAfterMergeFinishT = afterMergeFinishT;
+    }
+
     /** Aborts a transition. This will still queue it up to maintain order. */
     private void onAbort(ActiveTransition transition) {
         final Track track = mTracks.get(transition.getTrack());
@@ -1078,6 +1099,7 @@
         }
         // Merge all associated transactions together
         SurfaceControl.Transaction fullFinish = active.mFinishT;
+        SurfaceControl.Transaction afterMergeFinish = active.mAfterMergeFinishT;
         if (active.mMerged != null) {
             for (int iM = 0; iM < active.mMerged.size(); ++iM) {
                 final ActiveTransition toMerge = active.mMerged.get(iM);
@@ -1097,6 +1119,21 @@
                         fullFinish.merge(toMerge.mFinishT);
                     }
                 }
+                if (toMerge.mAfterMergeFinishT != null) {
+                    if (afterMergeFinish == null) {
+                        afterMergeFinish = toMerge.mAfterMergeFinishT;
+                    } else {
+                        afterMergeFinish.merge(toMerge.mAfterMergeFinishT);
+                    }
+                    toMerge.mAfterMergeFinishT = null;
+                }
+            }
+        }
+        if (afterMergeFinish != null) {
+            if (fullFinish == null) {
+                fullFinish = afterMergeFinish;
+            } else {
+                fullFinish.merge(afterMergeFinish);
             }
         }
         if (fullFinish != null) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index 5e7e5e6..e1009a0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -73,6 +73,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.jank.Cuj;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.window.flags.Flags;
 import com.android.wm.shell.R;
@@ -81,6 +82,7 @@
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.InteractionJankMonitorUtils;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource;
@@ -470,11 +472,17 @@
             } else if (id == R.id.collapse_menu_button) {
                 decoration.closeHandleMenu();
             } else if (id == R.id.maximize_window) {
+                InteractionJankMonitorUtils.beginTracing(
+                        Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW, /* view= */ v,
+                        /* tag= */ "caption_bar_button");
                 final RunningTaskInfo taskInfo = decoration.mTaskInfo;
                 decoration.closeHandleMenu();
                 decoration.closeMaximizeMenu();
                 mDesktopTasksController.toggleDesktopTaskSize(taskInfo);
             } else if (id == R.id.maximize_menu_maximize_button) {
+                InteractionJankMonitorUtils.beginTracing(
+                        Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW, /* view= */ v,
+                        /* tag= */ "maximize_menu_option");
                 final RunningTaskInfo taskInfo = decoration.mTaskInfo;
                 mDesktopTasksController.toggleDesktopTaskSize(taskInfo);
                 decoration.closeHandleMenu();
@@ -712,6 +720,9 @@
                 return false;
             }
             final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
+            InteractionJankMonitorUtils.beginTracing(
+                    Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW, mContext,
+                    /* surface= */ decoration.mTaskSurface, /* tag= */ "double_tap");
             mDesktopTasksController.toggleDesktopTaskSize(decoration.mTaskInfo);
             return true;
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
index fe1c9c3..d48ce53 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
@@ -28,6 +28,8 @@
 import android.util.DisplayMetrics;
 import android.view.SurfaceControl;
 
+import androidx.annotation.NonNull;
+
 import com.android.window.flags.Flags;
 import com.android.wm.shell.R;
 import com.android.wm.shell.common.DisplayController;
@@ -106,13 +108,15 @@
             repositionTaskBounds.bottom = (candidateBottom < stableBounds.bottom)
                     ? candidateBottom : oldBottom;
         }
-        // If width or height are negative or less than the minimum width or height, revert the
+        // If width or height are negative or exceeding the width or height constraints, revert the
         // respective bounds to use previous bound dimensions.
-        if (repositionTaskBounds.width() < getMinWidth(displayController, windowDecoration)) {
+        if (isExceedingWidthConstraint(repositionTaskBounds, stableBounds, displayController,
+                windowDecoration)) {
             repositionTaskBounds.right = oldRight;
             repositionTaskBounds.left = oldLeft;
         }
-        if (repositionTaskBounds.height() < getMinHeight(displayController, windowDecoration)) {
+        if (isExceedingHeightConstraint(repositionTaskBounds, stableBounds, displayController,
+                windowDecoration)) {
             repositionTaskBounds.top = oldTop;
             repositionTaskBounds.bottom = oldBottom;
         }
@@ -174,6 +178,30 @@
         return result;
     }
 
+    private static boolean isExceedingWidthConstraint(@NonNull Rect repositionTaskBounds,
+            Rect maxResizeBounds, DisplayController displayController,
+            WindowDecoration windowDecoration) {
+        // Check if width is less than the minimum width constraint.
+        if (repositionTaskBounds.width() < getMinWidth(displayController, windowDecoration)) {
+            return true;
+        }
+        // Check if width is more than the maximum resize bounds on desktop windowing mode.
+        return isSizeConstraintForDesktopModeEnabled(windowDecoration.mDecorWindowContext)
+                && repositionTaskBounds.width() > maxResizeBounds.width();
+    }
+
+    private static boolean isExceedingHeightConstraint(@NonNull Rect repositionTaskBounds,
+            Rect maxResizeBounds, DisplayController displayController,
+            WindowDecoration windowDecoration) {
+        // Check if height is less than the minimum height constraint.
+        if (repositionTaskBounds.height() < getMinHeight(displayController, windowDecoration)) {
+            return true;
+        }
+        // Check if height is more than the maximum resize bounds on desktop windowing mode.
+        return isSizeConstraintForDesktopModeEnabled(windowDecoration.mDecorWindowContext)
+                && repositionTaskBounds.height() > maxResizeBounds.height();
+    }
+
     private static float getMinWidth(DisplayController displayController,
             WindowDecoration windowDecoration) {
         return windowDecoration.mTaskInfo.minWidth < 0 ? getDefaultMinWidth(displayController,
@@ -210,7 +238,7 @@
 
     private static float getDefaultMinSize(DisplayController displayController,
             WindowDecoration windowDecoration) {
-        float density =  displayController.getDisplayLayout(windowDecoration.mTaskInfo.displayId)
+        float density = displayController.getDisplayLayout(windowDecoration.mTaskInfo.displayId)
                 .densityDpi() * DisplayMetrics.DENSITY_DEFAULT_SCALE;
         return windowDecoration.mTaskInfo.defaultMinSize * density;
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/Android.bp b/libs/WindowManager/Shell/tests/unittest/Android.bp
index 13f95cc..92be4f9 100644
--- a/libs/WindowManager/Shell/tests/unittest/Android.bp
+++ b/libs/WindowManager/Shell/tests/unittest/Android.bp
@@ -46,6 +46,7 @@
         "androidx.dynamicanimation_dynamicanimation",
         "dagger2",
         "frameworks-base-testutils",
+        "kotlin-test",
         "kotlinx-coroutines-android",
         "kotlinx-coroutines-core",
         "mockito-kotlin2",
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
index 193d614..6612aee 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
@@ -119,86 +119,91 @@
     }
 
     @Test
-    fun isOnlyActiveNonClosingTask_noActiveNonClosingTasks() {
-        // Not an active task
-        assertThat(repo.isOnlyActiveNonClosingTask(1)).isFalse()
+    fun isOnlyVisibleNonClosingTask_noTasks() {
+        // No visible tasks
+        assertThat(repo.isOnlyVisibleNonClosingTask(1)).isFalse()
         assertThat(repo.isClosingTask(1)).isFalse()
     }
 
     @Test
-    fun isOnlyActiveNonClosingTask_singleActiveNonClosingTask() {
-        repo.addActiveTask(DEFAULT_DISPLAY, 1)
-        // The only active task
-        assertThat(repo.isActiveTask(1)).isTrue()
+    fun isOnlyVisibleNonClosingTask_singleVisibleNonClosingTask() {
+        repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
+
+        // The only visible task
+        assertThat(repo.isVisibleTask(1)).isTrue()
         assertThat(repo.isClosingTask(1)).isFalse()
-        assertThat(repo.isOnlyActiveNonClosingTask(1)).isTrue()
-        // Not an active task
-        assertThat(repo.isActiveTask(99)).isFalse()
+        assertThat(repo.isOnlyVisibleNonClosingTask(1)).isTrue()
+        // Not a visible task
+        assertThat(repo.isVisibleTask(99)).isFalse()
         assertThat(repo.isClosingTask(99)).isFalse()
-        assertThat(repo.isOnlyActiveNonClosingTask(99)).isFalse()
+        assertThat(repo.isOnlyVisibleNonClosingTask(99)).isFalse()
     }
 
     @Test
-    fun isOnlyActiveNonClosingTask_singleActiveClosingTask() {
-        repo.addActiveTask(DEFAULT_DISPLAY, 1)
+    fun isOnlyVisibleNonClosingTask_singleVisibleClosingTask() {
+        repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
         repo.addClosingTask(DEFAULT_DISPLAY, 1)
-        // The active task that's closing
-        assertThat(repo.isActiveTask(1)).isTrue()
+
+        // A visible task that's closing
+        assertThat(repo.isVisibleTask(1)).isTrue()
         assertThat(repo.isClosingTask(1)).isTrue()
-        assertThat(repo.isOnlyActiveNonClosingTask(1)).isFalse()
-        // Not an active task
-        assertThat(repo.isActiveTask(99)).isFalse()
-        assertThat(repo.isOnlyActiveNonClosingTask(99)).isFalse()
+        assertThat(repo.isOnlyVisibleNonClosingTask(1)).isFalse()
+        // Not a visible task
+        assertThat(repo.isVisibleTask(99)).isFalse()
+        assertThat(repo.isOnlyVisibleNonClosingTask(99)).isFalse()
     }
 
     @Test
-    fun isOnlyActiveNonClosingTask_singleActiveMinimizedTask() {
-        repo.addActiveTask(DEFAULT_DISPLAY, 1)
+    fun isOnlyVisibleNonClosingTask_singleVisibleMinimizedTask() {
+        repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
         repo.minimizeTask(DEFAULT_DISPLAY, 1)
-        // The active task that's closing
-        assertThat(repo.isActiveTask(1)).isTrue()
+
+        // The visible task that's closing
+        assertThat(repo.isVisibleTask(1)).isTrue()
         assertThat(repo.isMinimizedTask(1)).isTrue()
-        assertThat(repo.isOnlyActiveNonClosingTask(1)).isFalse()
-        // Not an active task
-        assertThat(repo.isActiveTask(99)).isFalse()
-        assertThat(repo.isOnlyActiveNonClosingTask(99)).isFalse()
+        assertThat(repo.isOnlyVisibleNonClosingTask(1)).isFalse()
+        // Not a visible task
+        assertThat(repo.isVisibleTask(99)).isFalse()
+        assertThat(repo.isOnlyVisibleNonClosingTask(99)).isFalse()
     }
 
     @Test
-    fun isOnlyActiveNonClosingTask_multipleActiveNonClosingTasks() {
-        repo.addActiveTask(DEFAULT_DISPLAY, 1)
-        repo.addActiveTask(DEFAULT_DISPLAY, 2)
+    fun isOnlyVisibleNonClosingTask_multipleVisibleNonClosingTasks() {
+        repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
+        repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = true)
+
         // Not the only task
-        assertThat(repo.isActiveTask(1)).isTrue()
+        assertThat(repo.isVisibleTask(1)).isTrue()
         assertThat(repo.isClosingTask(1)).isFalse()
-        assertThat(repo.isOnlyActiveNonClosingTask(1)).isFalse()
+        assertThat(repo.isOnlyVisibleNonClosingTask(1)).isFalse()
         // Not the only task
-        assertThat(repo.isActiveTask(2)).isTrue()
+        assertThat(repo.isVisibleTask(2)).isTrue()
         assertThat(repo.isClosingTask(2)).isFalse()
-        assertThat(repo.isOnlyActiveNonClosingTask(2)).isFalse()
-        // Not an active task
-        assertThat(repo.isActiveTask(99)).isFalse()
+        assertThat(repo.isOnlyVisibleNonClosingTask(2)).isFalse()
+        // Not a visible task
+        assertThat(repo.isVisibleTask(99)).isFalse()
         assertThat(repo.isClosingTask(99)).isFalse()
-        assertThat(repo.isOnlyActiveNonClosingTask(99)).isFalse()
+        assertThat(repo.isOnlyVisibleNonClosingTask(99)).isFalse()
     }
 
     @Test
-    fun isOnlyActiveNonClosingTask_multipleDisplays() {
-        repo.addActiveTask(DEFAULT_DISPLAY, 1)
-        repo.addActiveTask(DEFAULT_DISPLAY, 2)
-        repo.addActiveTask(SECOND_DISPLAY, 3)
+    fun isOnlyVisibleNonClosingTask_multipleDisplays() {
+        repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
+        repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = true)
+        repo.updateVisibleFreeformTasks(SECOND_DISPLAY, taskId = 3, visible = true)
+
         // Not the only task on DEFAULT_DISPLAY
-        assertThat(repo.isActiveTask(1)).isTrue()
-        assertThat(repo.isOnlyActiveNonClosingTask(1)).isFalse()
+        assertThat(repo.isVisibleTask(1)).isTrue()
+        assertThat(repo.isOnlyVisibleNonClosingTask(1)).isFalse()
         // Not the only task on DEFAULT_DISPLAY
-        assertThat(repo.isActiveTask(2)).isTrue()
-        assertThat(repo.isOnlyActiveNonClosingTask(2)).isFalse()
-        // The only active task on SECOND_DISPLAY
-        assertThat(repo.isActiveTask(3)).isTrue()
-        assertThat(repo.isOnlyActiveNonClosingTask(3)).isTrue()
-        // Not an active task
-        assertThat(repo.isActiveTask(99)).isFalse()
-        assertThat(repo.isOnlyActiveNonClosingTask(99)).isFalse()
+        assertThat(repo.isVisibleTask(2)).isTrue()
+        assertThat(repo.isOnlyVisibleNonClosingTask(2)).isFalse()
+        // The only visible task on SECOND_DISPLAY
+        assertThat(repo.isVisibleTask(3)).isTrue()
+        assertThat(repo.isOnlyVisibleNonClosingTask(3)).isTrue()
+        // Not a visible task
+        assertThat(repo.isVisibleTask(99)).isFalse()
+        assertThat(repo.isOnlyVisibleNonClosingTask(99)).isFalse()
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index 392161f..a1a18a9 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -45,6 +45,7 @@
 import android.view.SurfaceControl
 import android.view.WindowManager
 import android.view.WindowManager.TRANSIT_CHANGE
+import android.view.WindowManager.TRANSIT_CLOSE
 import android.view.WindowManager.TRANSIT_OPEN
 import android.view.WindowManager.TRANSIT_TO_BACK
 import android.view.WindowManager.TRANSIT_TO_FRONT
@@ -102,6 +103,8 @@
 import java.util.Optional
 import junit.framework.Assert.assertFalse
 import junit.framework.Assert.assertTrue
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
 import org.junit.After
 import org.junit.Assume.assumeTrue
 import org.junit.Before
@@ -1254,72 +1257,205 @@
   }
 
   @Test
-  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_backTransition_singleActiveTask_noToken() {
+  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+  fun handleRequest_backTransition_singleActiveTaskNoTokenFlagDisabled_doesNotHandle() {
     val task = setUpFreeformTask()
+
     val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
-    // Doesn't handle request
-    assertThat(result).isNull()
+
+    assertNull(result, "Should not handle request")
+  }
+
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+  fun handleRequest_backTransition_singleActiveTaskNoTokenFlagEnabled_doesNotHandle() {
+    val task = setUpFreeformTask()
+
+    val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
+
+    assertNull(result, "Should not handle request")
   }
 
   @Test
   @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_backTransition_singleActiveTask_hasToken_desktopWallpaperDisabled() {
-    desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
-
+  fun handleRequest_backTransition_singleActiveTaskWithTokenFlagDisabled_doesNotHandle() {
     val task = setUpFreeformTask()
+
+    desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
     val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
-    // Doesn't handle request
-    assertThat(result).isNull()
+
+    assertNull(result, "Should not handle request")
   }
 
   @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_backTransition_singleActiveTask_hasToken_desktopWallpaperEnabled() {
+  fun handleRequest_backTransition_singleActiveTaskWithTokenFlagEnabled_handlesRequest() {
+    val task = setUpFreeformTask()
     val wallpaperToken = MockToken().token()
+
     desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
-
-    val task = setUpFreeformTask()
     val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
-    assertThat(result).isNotNull()
-    // Creates remove wallpaper transaction
-    result!!.assertRemoveAt(index = 0, wallpaperToken)
+
+    assertNotNull(result, "Should handle request")
+      // Should create remove wallpaper transaction
+      .assertRemoveAt(index = 0, wallpaperToken)
+  }
+
+  @Test
+  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+  fun handleRequest_backTransition_multipleActiveTasksFlagDisabled_doesNotHandle() {
+    val task1 = setUpFreeformTask()
+    setUpFreeformTask()
+
+    desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
+    val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
+
+    assertNull(result, "Should not handle request")
   }
 
   @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_backTransition_multipleActiveTasks() {
-    desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
-
+  fun handleRequest_backTransition_multipleActiveTasksFlagEnabled_doesNotHandle() {
     val task1 = setUpFreeformTask()
     setUpFreeformTask()
+
+    desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
     val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
-    // Doesn't handle request
-    assertThat(result).isNull()
+
+    assertNull(result, "Should not handle request")
   }
 
   @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_backTransition_multipleActiveTasks_singleNonClosing() {
-    desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
+  fun handleRequest_backTransition_multipleActiveTasksSingleNonClosing_handlesRequest() {
+    val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+    val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+    val wallpaperToken = MockToken().token()
 
-    val task1 = setUpFreeformTask()
-    setUpFreeformTask()
+    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+    desktopModeTaskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
     val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
-    // Doesn't handle request
-    assertThat(result).isNull()
+
+    assertNotNull(result, "Should handle request")
+      // Should create remove wallpaper transaction
+      .assertRemoveAt(index = 0, wallpaperToken)
   }
 
   @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
-  fun handleRequest_backTransition_multipleActiveTasks_singleNonMinimized() {
-    desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
+  fun handleRequest_backTransition_multipleActiveTasksSingleNonMinimized_handlesRequest() {
+    val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+    val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+    val wallpaperToken = MockToken().token()
 
+    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+    desktopModeTaskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
+    val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
+
+    assertNotNull(result, "Should handle request")
+      // Should create remove wallpaper transaction
+      .assertRemoveAt(index = 0, wallpaperToken)
+  }
+
+  @Test
+  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+  fun handleRequest_closeTransition_singleActiveTaskNoTokenFlagDisabled_doesNotHandle() {
+    val task = setUpFreeformTask()
+
+    val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
+
+    assertNull(result, "Should not handle request")
+  }
+
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+  fun handleRequest_closeTransition_singleActiveTaskNoTokenFlagEnabled_doesNotHandle() {
+    val task = setUpFreeformTask()
+
+    val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
+
+    assertNull(result, "Should not handle request")
+  }
+
+  @Test
+  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+  fun handleRequest_closeTransition_singleActiveTaskWithTokenFlagDisabled_doesNotHandle() {
+    val task = setUpFreeformTask()
+
+    desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
+    val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
+
+    assertNull(result, "Should not handle request")
+  }
+
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+  fun handleRequest_closeTransition_singleActiveTaskWithTokenFlagEnabled_handlesRequest() {
+    val task = setUpFreeformTask()
+    val wallpaperToken = MockToken().token()
+
+    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+    val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
+
+    assertNotNull(result, "Should handle request")
+      // Should create remove wallpaper transaction
+      .assertRemoveAt(index = 0, wallpaperToken)
+  }
+
+  @Test
+  @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+  fun handleRequest_closeTransition_multipleActiveTasksFlagDisabled_doesNotHandle() {
     val task1 = setUpFreeformTask()
     setUpFreeformTask()
-    val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
-    // Doesn't handle request
-    assertThat(result).isNull()
+
+    desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
+    val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
+
+    assertNull(result, "Should not handle request")
+  }
+
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+  fun handleRequest_closeTransition_multipleActiveTasksFlagEnabled_doesNotHandle() {
+    val task1 = setUpFreeformTask()
+    setUpFreeformTask()
+
+    desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
+    val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
+
+    assertNull(result, "Should not handle request")
+  }
+
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+  fun handleRequest_closeTransition_multipleActiveTasksSingleNonClosing_handlesRequest() {
+    val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+    val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+    val wallpaperToken = MockToken().token()
+
+    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+    desktopModeTaskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
+    val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
+
+    assertNotNull(result, "Should handle request")
+      // Should create remove wallpaper transaction
+      .assertRemoveAt(index = 0, wallpaperToken)
+  }
+
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+  fun handleRequest_closeTransition_multipleActiveTasksSingleNonMinimized_handlesRequest() {
+    val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+    val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+    val wallpaperToken = MockToken().token()
+
+    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+    desktopModeTaskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
+    val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
+
+    assertNotNull(result, "Should handle request")
+      // Should create remove wallpaper transaction
+      .assertRemoveAt(index = 0, wallpaperToken)
   }
 
   @Test
@@ -1693,6 +1829,7 @@
     task.topActivityInfo = activityInfo
     whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
     desktopModeTaskRepository.addActiveTask(displayId, task.taskId)
+    desktopModeTaskRepository.updateVisibleFreeformTasks(displayId, task.taskId, visible = true)
     desktopModeTaskRepository.addOrMoveFreeformTaskToTop(displayId, task.taskId)
     runningTasks.add(task)
     return task
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
index e291c0e..5c5a1a2 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
@@ -399,7 +399,7 @@
     }
 
     @Test
-    public void testGetRecentTasks_proto2Enabled_ignoresMinimizedFreeformTasks() {
+    public void testGetRecentTasks_proto2Enabled_includesMinimizedFreeformTasks() {
         ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1);
         ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2);
         ActivityManager.RecentTaskInfo t3 = makeTaskInfo(3);
@@ -415,8 +415,7 @@
         ArrayList<GroupedRecentTaskInfo> recentTasks = mRecentTasksController.getRecentTasks(
                 MAX_VALUE, RECENT_IGNORE_UNAVAILABLE, 0);
 
-        // 2 freeform tasks should be grouped into one, 1 task should be skipped, 3 total recents
-        // entries
+        // 3 freeform tasks should be grouped into one, 2 single tasks, 3 total recents entries
         assertEquals(3, recentTasks.size());
         GroupedRecentTaskInfo freeformGroup = recentTasks.get(0);
         GroupedRecentTaskInfo singleGroup1 = recentTasks.get(1);
@@ -428,9 +427,10 @@
         assertEquals(GroupedRecentTaskInfo.TYPE_SINGLE, singleGroup2.getType());
 
         // Check freeform group entries
-        assertEquals(2, freeformGroup.getTaskInfoList().size());
+        assertEquals(3, freeformGroup.getTaskInfoList().size());
         assertEquals(t1, freeformGroup.getTaskInfoList().get(0));
-        assertEquals(t5, freeformGroup.getTaskInfoList().get(1));
+        assertEquals(t3, freeformGroup.getTaskInfoList().get(1));
+        assertEquals(t5, freeformGroup.getTaskInfoList().get(2));
 
         // Check single entries
         assertEquals(t2, singleGroup1.getTaskInfo1());
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
index 964d86e..69a61ea 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
@@ -1192,7 +1192,8 @@
                         mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class));
         final RecentsTransitionHandler recentsHandler =
                 new RecentsTransitionHandler(shellInit, transitions,
-                        mock(RecentTasksController.class), mock(HomeTransitionObserver.class));
+                        mock(RecentTasksController.class), mock(HomeTransitionObserver.class),
+                        () -> mock(SurfaceControl.Transaction.class));
         transitions.replaceDefaultHandlerForTest(mDefaultHandler);
         shellInit.init();
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt
index f750e6b..86aded7 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt
@@ -21,7 +21,9 @@
 import android.graphics.PointF
 import android.graphics.Rect
 import android.os.IBinder
+import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
 import android.testing.AndroidTestingRunner
 import android.view.Display
 import android.window.WindowContainerToken
@@ -36,6 +38,7 @@
 import com.google.common.truth.Truth.assertThat
 import junit.framework.Assert.assertTrue
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
@@ -53,21 +56,32 @@
 class DragPositioningCallbackUtilityTest {
     @Mock
     private lateinit var mockWindowDecoration: WindowDecoration<*>
+
     @Mock
     private lateinit var taskToken: WindowContainerToken
+
     @Mock
     private lateinit var taskBinder: IBinder
+
     @Mock
     private lateinit var mockDisplayController: DisplayController
+
     @Mock
     private lateinit var mockDisplayLayout: DisplayLayout
+
     @Mock
     private lateinit var mockDisplay: Display
+
     @Mock
     private lateinit var mockContext: Context
+
     @Mock
     private lateinit var mockResources: Resources
 
+    @JvmField
+    @Rule
+    val setFlagsRule = SetFlagsRule()
+
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
@@ -323,6 +337,49 @@
         assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom - 50)
     }
 
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS)
+    fun testChangeBounds_windowSizeExceedsStableBounds_shouldBeAllowedToChangeBounds() {
+        val startingPoint =
+            PointF(OFF_CENTER_STARTING_BOUNDS.right.toFloat(),
+                OFF_CENTER_STARTING_BOUNDS.bottom.toFloat())
+        val repositionTaskBounds = Rect(OFF_CENTER_STARTING_BOUNDS)
+        // Increase height and width by STABLE_BOUNDS. Subtract by 5px so that it doesn't reach
+        // the disallowed drag area.
+        val offset = 5
+        val newX = STABLE_BOUNDS.right.toFloat() - offset
+        val newY = STABLE_BOUNDS.bottom.toFloat() - offset
+        val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
+
+        DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM,
+            repositionTaskBounds, OFF_CENTER_STARTING_BOUNDS, STABLE_BOUNDS, delta,
+            mockDisplayController, mockWindowDecoration)
+        assertThat(repositionTaskBounds.width()).isGreaterThan(STABLE_BOUNDS.right)
+        assertThat(repositionTaskBounds.height()).isGreaterThan(STABLE_BOUNDS.bottom)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS)
+    fun testChangeBoundsInDesktopMode_windowSizeExceedsStableBounds_shouldBeLimitedToDisplaySize() {
+        whenever(DesktopModeStatus.canEnterDesktopMode(mockContext)).thenReturn(true)
+        val startingPoint =
+            PointF(OFF_CENTER_STARTING_BOUNDS.right.toFloat(),
+                OFF_CENTER_STARTING_BOUNDS.bottom.toFloat())
+        val repositionTaskBounds = Rect(OFF_CENTER_STARTING_BOUNDS)
+        // Increase height and width by STABLE_BOUNDS. Subtract by 5px so that it doesn't reach
+        // the disallowed drag area.
+        val offset = 5
+        val newX = STABLE_BOUNDS.right.toFloat() - offset
+        val newY = STABLE_BOUNDS.bottom.toFloat() - offset
+        val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
+
+        DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM,
+            repositionTaskBounds, OFF_CENTER_STARTING_BOUNDS, STABLE_BOUNDS, delta,
+            mockDisplayController, mockWindowDecoration)
+        assertThat(repositionTaskBounds.width()).isLessThan(STABLE_BOUNDS.right)
+        assertThat(repositionTaskBounds.height()).isLessThan(STABLE_BOUNDS.bottom)
+    }
+
     private fun initializeTaskInfo(taskMinWidth: Int = MIN_WIDTH, taskMinHeight: Int = MIN_HEIGHT) {
         mockWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply {
             taskId = TASK_ID
@@ -347,6 +404,7 @@
         private const val NAVBAR_HEIGHT = 50
         private val DISPLAY_BOUNDS = Rect(0, 0, 2400, 1600)
         private val STARTING_BOUNDS = Rect(0, 0, 100, 100)
+        private val OFF_CENTER_STARTING_BOUNDS = Rect(-100, -100, 10, 10)
         private val DISALLOWED_RESIZE_AREA = Rect(
             DISPLAY_BOUNDS.left,
             DISPLAY_BOUNDS.bottom - NAVBAR_HEIGHT,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
index 48ac1e5..901ca90 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
@@ -17,6 +17,8 @@
 
 import android.app.ActivityManager
 import android.app.WindowConfiguration
+import android.content.Context
+import android.content.res.Resources
 import android.graphics.Point
 import android.graphics.Rect
 import android.os.IBinder
@@ -98,6 +100,10 @@
     private lateinit var mockFinishCallback: TransitionFinishCallback
     @Mock
     private lateinit var mockTransitions: Transitions
+    @Mock
+    private lateinit var mockContext: Context
+    @Mock
+    private lateinit var mockResources: Resources
 
     private lateinit var taskPositioner: VeiledResizeTaskPositioner
 
@@ -105,6 +111,9 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
+        mockDesktopWindowDecoration.mDisplay = mockDisplay
+        mockDesktopWindowDecoration.mDecorWindowContext = mockContext
+        whenever(mockContext.getResources()).thenReturn(mockResources)
         whenever(taskToken.asBinder()).thenReturn(taskBinder)
         whenever(mockDisplayController.getDisplayLayout(DISPLAY_ID)).thenReturn(mockDisplayLayout)
         whenever(mockDisplayLayout.densityDpi()).thenReturn(DENSITY_DPI)
diff --git a/libs/nativehelper_jvm/Android.bp b/libs/nativehelper_jvm/Android.bp
new file mode 100644
index 0000000..b5b7028
--- /dev/null
+++ b/libs/nativehelper_jvm/Android.bp
@@ -0,0 +1,19 @@
+package {
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+cc_library_host_static {
+    name: "libnativehelper_jvm",
+    srcs: [
+        "JNIPlatformHelp.c",
+        "JniConstants.c",
+        "file_descriptor_jni.c",
+    ],
+    whole_static_libs: ["libnativehelper_any_vm"],
+    export_static_lib_headers: ["libnativehelper_any_vm"],
+    target: {
+        windows: {
+            enabled: true,
+        },
+    },
+}
diff --git a/libs/nativehelper_jvm/JNIPlatformHelp.c b/libs/nativehelper_jvm/JNIPlatformHelp.c
new file mode 100644
index 0000000..9df31a8
--- /dev/null
+++ b/libs/nativehelper_jvm/JNIPlatformHelp.c
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <nativehelper/JNIPlatformHelp.h>
+
+#include <stddef.h>
+
+#include "JniConstants.h"
+
+static int GetBufferPosition(JNIEnv* env, jobject nioBuffer) {
+    return(*env)->GetIntField(env, nioBuffer, JniConstants_NioBuffer_position(env));
+}
+
+static int GetBufferLimit(JNIEnv* env, jobject nioBuffer) {
+    return(*env)->GetIntField(env, nioBuffer, JniConstants_NioBuffer_limit(env));
+}
+
+static int GetBufferElementSizeShift(JNIEnv* env, jobject nioBuffer) {
+    jclass byteBufferClass = JniConstants_NioByteBufferClass(env);
+    jclass shortBufferClass = JniConstants_NioShortBufferClass(env);
+    jclass charBufferClass = JniConstants_NioCharBufferClass(env);
+    jclass intBufferClass = JniConstants_NioIntBufferClass(env);
+    jclass floatBufferClass = JniConstants_NioFloatBufferClass(env);
+    jclass longBufferClass = JniConstants_NioLongBufferClass(env);
+    jclass doubleBufferClass = JniConstants_NioDoubleBufferClass(env);
+
+    // Check the type of the Buffer
+    if ((*env)->IsInstanceOf(env, nioBuffer, byteBufferClass)) {
+        return 0;
+    } else if ((*env)->IsInstanceOf(env, nioBuffer, shortBufferClass) ||
+               (*env)->IsInstanceOf(env, nioBuffer, charBufferClass)) {
+        return 1;
+    } else if ((*env)->IsInstanceOf(env, nioBuffer, intBufferClass) ||
+               (*env)->IsInstanceOf(env, nioBuffer, floatBufferClass)) {
+        return 2;
+    } else if ((*env)->IsInstanceOf(env, nioBuffer, longBufferClass) ||
+               (*env)->IsInstanceOf(env, nioBuffer, doubleBufferClass)) {
+        return 3;
+    }
+    return 0;
+}
+
+jarray jniGetNioBufferBaseArray(JNIEnv* env, jobject nioBuffer) {
+    jmethodID hasArrayMethod = JniConstants_NioBuffer_hasArray(env);
+    jboolean hasArray = (*env)->CallBooleanMethod(env, nioBuffer, hasArrayMethod);
+    if (hasArray) {
+        jmethodID arrayMethod = JniConstants_NioBuffer_array(env);
+        return (*env)->CallObjectMethod(env, nioBuffer, arrayMethod);
+    } else {
+        return NULL;
+    }
+}
+
+int jniGetNioBufferBaseArrayOffset(JNIEnv* env, jobject nioBuffer) {
+    jmethodID hasArrayMethod = JniConstants_NioBuffer_hasArray(env);
+    jboolean hasArray = (*env)->CallBooleanMethod(env, nioBuffer, hasArrayMethod);
+    if (hasArray) {
+        jmethodID arrayOffsetMethod = JniConstants_NioBuffer_arrayOffset(env);
+        jint arrayOffset = (*env)->CallIntMethod(env, nioBuffer, arrayOffsetMethod);
+        const int position = GetBufferPosition(env, nioBuffer);
+        jint elementSizeShift = GetBufferElementSizeShift(env, nioBuffer);
+        return (arrayOffset + position) << elementSizeShift;
+    } else {
+        return 0;
+    }
+}
+
+jlong jniGetNioBufferPointer(JNIEnv* env, jobject nioBuffer) {
+    // in Java 11, the address field of a HeapByteBuffer contains a non-zero value despite
+    // HeapByteBuffer being a non-direct buffer. In that case, this should still return 0.
+    jmethodID isDirectMethod = JniConstants_NioBuffer_isDirect(env);
+    jboolean isDirect = (*env)->CallBooleanMethod(env, nioBuffer, isDirectMethod);
+    if (isDirect == JNI_FALSE) {
+        return 0L;
+    }
+    jlong baseAddress = (*env)->GetLongField(env, nioBuffer, JniConstants_NioBuffer_address(env));
+    if (baseAddress != 0) {
+        const int position = GetBufferPosition(env, nioBuffer);
+        const int shift = GetBufferElementSizeShift(env, nioBuffer);
+        baseAddress += position << shift;
+    }
+    return baseAddress;
+}
+
+jlong jniGetNioBufferFields(JNIEnv* env, jobject nioBuffer,
+                            jint* position, jint* limit, jint* elementSizeShift) {
+    *position = GetBufferPosition(env, nioBuffer);
+    *limit = GetBufferLimit(env, nioBuffer);
+    *elementSizeShift = GetBufferElementSizeShift(env, nioBuffer);
+    return (*env)->GetLongField(env, nioBuffer, JniConstants_NioBuffer_address(env));
+}
diff --git a/libs/nativehelper_jvm/JniConstants.c b/libs/nativehelper_jvm/JniConstants.c
new file mode 100644
index 0000000..ca58f61
--- /dev/null
+++ b/libs/nativehelper_jvm/JniConstants.c
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "JniConstants.h"
+
+#include <pthread.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <string.h>
+
+#define LOG_TAG "JniConstants"
+#include <log/log.h>
+
+// jclass constants list:
+//   <class, signature, androidOnly>
+#define JCLASS_CONSTANTS_LIST(V)                                            \
+  V(FileDescriptor, "java/io/FileDescriptor", false)                        \
+  V(NioBuffer, "java/nio/Buffer", false)                                    \
+  V(NioByteBuffer, "java/nio/ByteBuffer", false)                            \
+  V(NioShortBuffer, "java/nio/ShortBuffer", false)                          \
+  V(NioCharBuffer, "java/nio/CharBuffer", false)                            \
+  V(NioIntBuffer, "java/nio/IntBuffer", false)                              \
+  V(NioFloatBuffer, "java/nio/FloatBuffer", false)                          \
+  V(NioLongBuffer, "java/nio/LongBuffer", false)                            \
+  V(NioDoubleBuffer, "java/nio/DoubleBuffer", false)
+
+// jmethodID's of public methods constants list:
+//   <Class, method, method-string, signature, is_static>
+#define JMETHODID_CONSTANTS_LIST(V)                                                         \
+  V(FileDescriptor, init, "<init>", "()V", false)                                           \
+  V(NioBuffer, array, "array", "()Ljava/lang/Object;", false)                               \
+  V(NioBuffer, hasArray, "hasArray", "()Z", false)                                          \
+  V(NioBuffer, isDirect, "isDirect", "()Z", false)                                          \
+  V(NioBuffer, arrayOffset, "arrayOffset", "()I", false)
+
+// jfieldID constants list:
+//   <Class, field, signature, is_static>
+#define JFIELDID_CONSTANTS_LIST(V)                                          \
+  V(FileDescriptor, fd, "I", false)                                         \
+  V(NioBuffer, address, "J", false)                                         \
+  V(NioBuffer, limit, "I", false)                                           \
+  V(NioBuffer, position, "I", false)
+
+#define CLASS_NAME(cls)             g_ ## cls
+#define METHOD_NAME(cls, method)    g_ ## cls ## _ ## method
+#define FIELD_NAME(cls, field)      g_ ## cls ## _ ## field
+
+//
+// Declare storage for cached classes, methods and fields.
+//
+
+#define JCLASS_DECLARE_STORAGE(cls, ...)                                    \
+  static jclass CLASS_NAME(cls) = NULL;
+JCLASS_CONSTANTS_LIST(JCLASS_DECLARE_STORAGE)
+#undef JCLASS_DECLARE_STORAGE
+
+#define JMETHODID_DECLARE_STORAGE(cls, method, ...)                         \
+  static jmethodID METHOD_NAME(cls, method) = NULL;
+JMETHODID_CONSTANTS_LIST(JMETHODID_DECLARE_STORAGE)
+#undef JMETHODID_DECLARE_STORAGE
+
+#define JFIELDID_DECLARE_STORAGE(cls, field, ...)                           \
+  static jfieldID FIELD_NAME(cls, field) = NULL;
+JFIELDID_CONSTANTS_LIST(JFIELDID_DECLARE_STORAGE)
+#undef JFIELDID_DECLARE_STORAGE
+
+//
+// Helper methods
+//
+
+static jclass FindClass(JNIEnv* env, const char* signature, bool androidOnly) {
+    jclass cls = (*env)->FindClass(env, signature);
+    if (cls == NULL) {
+        LOG_ALWAYS_FATAL_IF(!androidOnly, "Class not found: %s", signature);
+        return NULL;
+    }
+    return (*env)->NewGlobalRef(env, cls);
+}
+
+static jmethodID FindMethod(JNIEnv* env, jclass cls,
+                            const char* name, const char* signature, bool isStatic) {
+    jmethodID method;
+    if (isStatic) {
+        method = (*env)->GetStaticMethodID(env, cls, name, signature);
+    } else {
+        method = (*env)->GetMethodID(env, cls, name, signature);
+    }
+    LOG_ALWAYS_FATAL_IF(method == NULL, "Method not found: %s:%s", name, signature);
+    return method;
+}
+
+static jfieldID FindField(JNIEnv* env, jclass cls,
+                          const char* name, const char* signature, bool isStatic) {
+    jfieldID field;
+    if (isStatic) {
+        field = (*env)->GetStaticFieldID(env, cls, name, signature);
+    } else {
+        field = (*env)->GetFieldID(env, cls, name, signature);
+    }
+    LOG_ALWAYS_FATAL_IF(field == NULL, "Field not found: %s:%s", name, signature);
+    return field;
+}
+
+static pthread_once_t g_initialized = PTHREAD_ONCE_INIT;
+static JNIEnv* g_init_env;
+
+static void InitializeConstants() {
+    // Initialize cached classes.
+#define JCLASS_INITIALIZE(cls, signature, androidOnly)                      \
+    CLASS_NAME(cls) = FindClass(g_init_env, signature, androidOnly);
+    JCLASS_CONSTANTS_LIST(JCLASS_INITIALIZE)
+#undef JCLASS_INITIALIZE
+
+    // Initialize cached methods.
+#define JMETHODID_INITIALIZE(cls, method, name, signature, isStatic)        \
+    METHOD_NAME(cls, method) =                                              \
+        FindMethod(g_init_env, CLASS_NAME(cls), name, signature, isStatic);
+    JMETHODID_CONSTANTS_LIST(JMETHODID_INITIALIZE)
+#undef JMETHODID_INITIALIZE
+
+    // Initialize cached fields.
+#define JFIELDID_INITIALIZE(cls, field, signature, isStatic)                \
+    FIELD_NAME(cls, field) =                                                \
+        FindField(g_init_env, CLASS_NAME(cls), #field, signature, isStatic);
+    JFIELDID_CONSTANTS_LIST(JFIELDID_INITIALIZE)
+#undef JFIELDID_INITIALIZE
+}
+
+void EnsureInitialized(JNIEnv* env) {
+    // This method has to be called in every cache accesses because library can be built
+    // 2 different ways and existing usage for compat version doesn't have a good hook for
+    // initialization and is widely used.
+    g_init_env = env;
+    pthread_once(&g_initialized, InitializeConstants);
+}
+
+// API exported by libnativehelper_api.h.
+
+void jniUninitializeConstants() {
+    // Uninitialize cached classes, methods and fields.
+    //
+    // NB we assume the runtime is stopped at this point and do not delete global
+    // references.
+#define JCLASS_INVALIDATE(cls, ...) CLASS_NAME(cls) = NULL;
+    JCLASS_CONSTANTS_LIST(JCLASS_INVALIDATE);
+#undef JCLASS_INVALIDATE
+
+#define JMETHODID_INVALIDATE(cls, method, ...) METHOD_NAME(cls, method) = NULL;
+    JMETHODID_CONSTANTS_LIST(JMETHODID_INVALIDATE);
+#undef JMETHODID_INVALIDATE
+
+#define JFIELDID_INVALIDATE(cls, field, ...) FIELD_NAME(cls, field) = NULL;
+    JFIELDID_CONSTANTS_LIST(JFIELDID_INVALIDATE);
+#undef JFIELDID_INVALIDATE
+
+    // If jniConstantsUninitialize is called, runtime has shutdown. Reset
+    // state as some tests re-start the runtime.
+    pthread_once_t o = PTHREAD_ONCE_INIT;
+    memcpy(&g_initialized, &o, sizeof(o));
+}
+
+//
+// Accessors
+//
+
+#define JCLASS_ACCESSOR_IMPL(cls, ...)                                      \
+jclass JniConstants_ ## cls ## Class(JNIEnv* env) {                         \
+    EnsureInitialized(env);                                                 \
+    return CLASS_NAME(cls);                                                 \
+}
+JCLASS_CONSTANTS_LIST(JCLASS_ACCESSOR_IMPL)
+#undef JCLASS_ACCESSOR_IMPL
+
+#define JMETHODID_ACCESSOR_IMPL(cls, method, ...)                           \
+jmethodID JniConstants_ ## cls ## _ ## method(JNIEnv* env) {                \
+    EnsureInitialized(env);                                                 \
+    return METHOD_NAME(cls, method);                                        \
+}
+JMETHODID_CONSTANTS_LIST(JMETHODID_ACCESSOR_IMPL)
+
+#define JFIELDID_ACCESSOR_IMPL(cls, field, ...)                             \
+jfieldID JniConstants_ ## cls ## _ ## field(JNIEnv* env) {                  \
+    EnsureInitialized(env);                                                 \
+    return FIELD_NAME(cls, field);                                          \
+}
+JFIELDID_CONSTANTS_LIST(JFIELDID_ACCESSOR_IMPL)
diff --git a/libs/nativehelper_jvm/JniConstants.h b/libs/nativehelper_jvm/JniConstants.h
new file mode 100644
index 0000000..e7a266d
--- /dev/null
+++ b/libs/nativehelper_jvm/JniConstants.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <sys/cdefs.h>
+
+#include <jni.h>
+
+__BEGIN_DECLS
+
+//
+// Classes in constants cache.
+//
+// NB The implementations of these methods are generated by the JCLASS_ACCESSOR_IMPL macro in
+// JniConstants.c.
+//
+jclass JniConstants_FileDescriptorClass(JNIEnv* env);
+jclass JniConstants_NioByteBufferClass(JNIEnv* env);
+jclass JniConstants_NioShortBufferClass(JNIEnv* env);
+jclass JniConstants_NioCharBufferClass(JNIEnv* env);
+jclass JniConstants_NioIntBufferClass(JNIEnv* env);
+jclass JniConstants_NioFloatBufferClass(JNIEnv* env);
+jclass JniConstants_NioLongBufferClass(JNIEnv* env);
+jclass JniConstants_NioDoubleBufferClass(JNIEnv* env);
+
+//
+// Methods in the constants cache.
+//
+// NB The implementations of these methods are generated by the JMETHODID_ACCESSOR_IMPL macro in
+// JniConstants.c.
+//
+jmethodID JniConstants_FileDescriptor_init(JNIEnv* env);
+jmethodID JniConstants_NioBuffer_array(JNIEnv* env);
+jmethodID JniConstants_NioBuffer_arrayOffset(JNIEnv* env);
+jmethodID JniConstants_NioBuffer_hasArray(JNIEnv* env);
+jmethodID JniConstants_NioBuffer_isDirect(JNIEnv* env);
+
+//
+// Fields in the constants cache.
+//
+// NB The implementations of these methods are generated by the JFIELDID_ACCESSOR_IMPL macro in
+// JniConstants.c.
+//
+jfieldID JniConstants_FileDescriptor_fd(JNIEnv* env);
+jfieldID JniConstants_NioBuffer_address(JNIEnv* env);
+jfieldID JniConstants_NioBuffer_limit(JNIEnv* env);
+jfieldID JniConstants_NioBuffer_position(JNIEnv* env);
+
+__END_DECLS
diff --git a/libs/nativehelper_jvm/OWNERS b/libs/nativehelper_jvm/OWNERS
new file mode 100644
index 0000000..5d55f6e
--- /dev/null
+++ b/libs/nativehelper_jvm/OWNERS
@@ -0,0 +1,7 @@
+# Bug component: 326772
+
+include /libs/hwui/OWNERS
+include platform/libnativehelper:/OWNERS
+
+diegoperez@google.com
+jgaillard@google.com
diff --git a/libs/nativehelper_jvm/README b/libs/nativehelper_jvm/README
new file mode 100644
index 0000000..755c422
--- /dev/null
+++ b/libs/nativehelper_jvm/README
@@ -0,0 +1,2 @@
+libnativehelper_jvm is a JVM-compatible version of libnativehelper.
+It should be used instead of libnativehelper whenever a host library is meant to run on a JVM.
\ No newline at end of file
diff --git a/libs/nativehelper_jvm/file_descriptor_jni.c b/libs/nativehelper_jvm/file_descriptor_jni.c
new file mode 100644
index 0000000..36880cd
--- /dev/null
+++ b/libs/nativehelper_jvm/file_descriptor_jni.c
@@ -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.
+ */
+
+#include <android/file_descriptor_jni.h>
+
+#include <stddef.h>
+
+#define LOG_TAG "file_descriptor_jni"
+#include <log/log.h>
+
+#include "JniConstants.h"
+
+static void EnsureArgumentIsFileDescriptor(JNIEnv* env, jobject instance) {
+    LOG_ALWAYS_FATAL_IF(instance == NULL, "FileDescriptor is NULL");
+    jclass jifd = JniConstants_FileDescriptorClass(env);
+    LOG_ALWAYS_FATAL_IF(!(*env)->IsInstanceOf(env, instance, jifd),
+                         "Argument is not a FileDescriptor");
+}
+
+JNIEXPORT _Nullable jobject AFileDescriptor_create(JNIEnv* env) {
+    return (*env)->NewObject(env,
+                             JniConstants_FileDescriptorClass(env),
+                             JniConstants_FileDescriptor_init(env));
+}
+
+JNIEXPORT int AFileDescriptor_getFd(JNIEnv* env, jobject fileDescriptor) {
+    EnsureArgumentIsFileDescriptor(env, fileDescriptor);
+    return (*env)->GetIntField(env, fileDescriptor, JniConstants_FileDescriptor_fd(env));
+}
+
+JNIEXPORT void AFileDescriptor_setFd(JNIEnv* env, jobject fileDescriptor, int fd) {
+    EnsureArgumentIsFileDescriptor(env, fileDescriptor);
+    (*env)->SetIntField(env, fileDescriptor, JniConstants_FileDescriptor_fd(env), fd);
+}
diff --git a/native/graphics/jni/Android.bp b/native/graphics/jni/Android.bp
index 746c280..8f16f76 100644
--- a/native/graphics/jni/Android.bp
+++ b/native/graphics/jni/Android.bp
@@ -23,6 +23,9 @@
 
 cc_library_shared {
     name: "libjnigraphics",
+    defaults: [
+        "bug_24465209_workaround",
+    ],
 
     cflags: [
         "-Wall",
@@ -47,13 +50,6 @@
 
     static_libs: ["libarect"],
 
-    arch: {
-        arm: {
-            // TODO: This is to work around b/24465209. Remove after root cause is fixed
-            pack_relocations: false,
-            ldflags: ["-Wl,--hash-style=both"],
-        },
-    },
     host_supported: true,
     target: {
         android: {
diff --git a/packages/CtsShim/Android.bp b/packages/CtsShim/Android.bp
index baafe7b..a94c8c5 100644
--- a/packages/CtsShim/Android.bp
+++ b/packages/CtsShim/Android.bp
@@ -61,7 +61,6 @@
         "com.android.apex.cts.shim.v1",
         "com.android.apex.cts.shim.v2",
         "com.android.apex.cts.shim.v2_legacy",
-        "com.android.apex.cts.shim.v2_no_hashtree",
         "com.android.apex.cts.shim.v2_sdk_target_p",
         "com.android.apex.cts.shim.v3",
     ],
@@ -102,7 +101,6 @@
         "com.android.apex.cts.shim.v1",
         "com.android.apex.cts.shim.v2",
         "com.android.apex.cts.shim.v2_legacy",
-        "com.android.apex.cts.shim.v2_no_hashtree",
         "com.android.apex.cts.shim.v2_sdk_target_p",
         "com.android.apex.cts.shim.v3",
     ],
diff --git a/packages/CtsShim/build/Android.bp b/packages/CtsShim/build/Android.bp
index d6b7ecf..5b3d47e 100644
--- a/packages/CtsShim/build/Android.bp
+++ b/packages/CtsShim/build/Android.bp
@@ -93,7 +93,6 @@
         "com.android.apex.cts.shim.v1",
         "com.android.apex.cts.shim.v2",
         "com.android.apex.cts.shim.v2_apk_in_apex_upgrades",
-        "com.android.apex.cts.shim.v2_no_hashtree",
         "com.android.apex.cts.shim.v2_legacy",
         "com.android.apex.cts.shim.v2_sdk_target_p",
         "com.android.apex.cts.shim.v2_unsigned_payload",
@@ -200,7 +199,6 @@
         "com.android.apex.cts.shim.v1",
         "com.android.apex.cts.shim.v2",
         "com.android.apex.cts.shim.v2_apk_in_apex_upgrades",
-        "com.android.apex.cts.shim.v2_no_hashtree",
         "com.android.apex.cts.shim.v2_legacy",
         "com.android.apex.cts.shim.v2_sdk_target_p",
         "com.android.apex.cts.shim.v2_unsigned_payload",
diff --git a/packages/CtsShim/build/jni/Android.bp b/packages/CtsShim/build/jni/Android.bp
index 2dbf2a2..ac85d2b 100644
--- a/packages/CtsShim/build/jni/Android.bp
+++ b/packages/CtsShim/build/jni/Android.bp
@@ -33,7 +33,6 @@
         "com.android.apex.cts.shim.v1",
         "com.android.apex.cts.shim.v2",
         "com.android.apex.cts.shim.v2_apk_in_apex_upgrades",
-        "com.android.apex.cts.shim.v2_no_hashtree",
         "com.android.apex.cts.shim.v2_legacy",
         "com.android.apex.cts.shim.v2_sdk_target_p",
         "com.android.apex.cts.shim.v2_unsigned_payload",
diff --git a/packages/SettingsLib/OWNERS b/packages/SettingsLib/OWNERS
index 62ed66c..e4bc7b4 100644
--- a/packages/SettingsLib/OWNERS
+++ b/packages/SettingsLib/OWNERS
@@ -13,4 +13,4 @@
 per-file *.xml=*
 
 # Notification-related utilities
-per-file */notification/* = file:/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS
+per-file **/notification/** = file:/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS
diff --git a/packages/SettingsLib/aconfig/settingslib.aconfig b/packages/SettingsLib/aconfig/settingslib.aconfig
index a158756..4ac3e67 100644
--- a/packages/SettingsLib/aconfig/settingslib.aconfig
+++ b/packages/SettingsLib/aconfig/settingslib.aconfig
@@ -89,3 +89,13 @@
         purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    name: "volume_panel_broadcast_fix"
+    namespace: "systemui"
+    description: "Make the volume panel's repository listen for the new ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED broadcast instead of ACTION_NOTIFICATION_POLICY_CHANGED"
+    bug: "347707024"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/packages/SettingsLib/res/drawable/ic_do_not_disturb_on_24dp.xml b/packages/SettingsLib/res/drawable/ic_do_not_disturb_on_24dp.xml
new file mode 100644
index 0000000..06c0d8c
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_do_not_disturb_on_24dp.xml
@@ -0,0 +1,28 @@
+<!--
+    Copyright (C) 2024 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0"
+        android:tint="?android:attr/colorControlNormal">
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M12,2C6.48,2 2,6.48 2,12c0,5.52 4.48,10 10,10c5.52,0 10,-4.48 10,-10C22,6.48 17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8c0,-4.41 3.59,-8 8,-8c4.41,0 8,3.59 8,8C20,16.41 16.41,20 12,20z"/>
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M7,11h10v2h-10z"/>
+</vector>
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index d373201..27c386e 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -1361,6 +1361,9 @@
     <!-- Keywords for setting screen for controlling apps that can schedule alarms [CHAR LIMIT=100] -->
     <string name="keywords_alarms_and_reminders">schedule, alarm, reminder, clock</string>
 
+    <!-- Sound: Title for the Do not Disturb option and associated settings page. [CHAR LIMIT=50]-->
+    <string name="zen_mode_settings_title">Do Not Disturb</string>
+
     <!--  Do not disturb: Label for button in enable zen dialog that will turn on zen mode. [CHAR LIMIT=30] -->
     <string name="zen_mode_enable_dialog_turn_on">Turn on</string>
     <!-- Do not disturb: Title for the Do not Disturb dialog to turn on Do not disturb. [CHAR LIMIT=50]-->
@@ -1387,6 +1390,9 @@
     <!-- Do not disturb: Duration option to always have DND on until it is manually turned off [CHAR LIMIT=60] -->
     <string name="zen_mode_forever">Until you turn off</string>
 
+    <!-- [CHAR LIMIT=50] Zen mode settings: placeholder for a Contact name when the name is empty -->
+    <string name="zen_mode_starred_contacts_empty_name">(No name)</string>
+
     <!-- time label for event have that happened very recently [CHAR LIMIT=60] -->
     <string name="time_unit_just_now">Just now</string>
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java
index 1597a4b..fc163ce 100644
--- a/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java
+++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java
@@ -430,13 +430,21 @@
         return null;
     }
 
+    /**
+     * Retrieves the user ID of a managed profile associated with a specific user.
+     *
+     * <p>This method iterates over the users in the profile group associated with the given user ID
+     * and returns the ID of the user that is identified as a managed profile user.
+     * If no managed profile is found, it returns {@link UserHandle#USER_NULL}.
+     *
+     * @param context The context used to obtain the {@link UserManager} system service.
+     * @param userId  The ID of the user for whom to find the managed profile.
+     * @return The user ID of the managed profile, or {@link UserHandle#USER_NULL} if none exists.
+     */
     private static int getManagedProfileId(Context context, int userId) {
         UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE);
         List<UserInfo> userProfiles = um.getProfiles(userId);
         for (UserInfo uInfo : userProfiles) {
-            if (uInfo.id == userId) {
-                continue;
-            }
             if (uInfo.isManagedProfile()) {
                 return uInfo.id;
             }
@@ -821,11 +829,11 @@
         }
         EnforcedAdmin admin =
                 RestrictedLockUtils.getProfileOrDeviceOwner(
-                        context, UserHandle.of(UserHandle.USER_SYSTEM));
+                        context, context.getUser());
         if (admin != null) {
             return admin;
         }
-        int profileId = getManagedProfileId(context, UserHandle.USER_SYSTEM);
+        int profileId = getManagedProfileId(context, context.getUserId());
         return RestrictedLockUtils.getProfileOrDeviceOwner(context, UserHandle.of(profileId));
     }
 
@@ -848,7 +856,7 @@
         if (admin != null) {
             return admin;
         }
-        int profileId = getManagedProfileId(context, UserHandle.USER_SYSTEM);
+        int profileId = getManagedProfileId(context, context.getUserId());
         return RestrictedLockUtils.getProfileOrDeviceOwner(context, UserHandle.of(profileId));
     }
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenIconLoader.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenIconLoader.java
new file mode 100644
index 0000000..3f19830
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenIconLoader.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.notification.modes;
+
+import static com.google.common.util.concurrent.Futures.immediateFuture;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.Nullable;
+import android.app.AutomaticZenRule;
+import android.content.Context;
+import android.graphics.drawable.AdaptiveIconDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.InsetDrawable;
+import android.service.notification.SystemZenRules;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.LruCache;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import androidx.appcompat.content.res.AppCompatResources;
+
+import com.google.common.util.concurrent.FluentFuture;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class ZenIconLoader {
+
+    private static final String TAG = "ZenIconLoader";
+
+    private static final Drawable MISSING = new ColorDrawable();
+
+    @Nullable // Until first usage
+    private static ZenIconLoader sInstance;
+
+    private final LruCache<String, Drawable> mCache;
+    private final ListeningExecutorService mBackgroundExecutor;
+
+    public static ZenIconLoader getInstance() {
+        if (sInstance == null) {
+            sInstance = new ZenIconLoader();
+        }
+        return sInstance;
+    }
+
+    private ZenIconLoader() {
+        this(Executors.newFixedThreadPool(4));
+    }
+
+    @VisibleForTesting
+    ZenIconLoader(ExecutorService backgroundExecutor) {
+        mCache = new LruCache<>(50);
+        mBackgroundExecutor =
+                MoreExecutors.listeningDecorator(backgroundExecutor);
+    }
+
+    @NonNull
+    ListenableFuture<Drawable> getIcon(Context context, @NonNull AutomaticZenRule rule) {
+        if (rule.getIconResId() == 0) {
+            return Futures.immediateFuture(getFallbackIcon(context, rule.getType()));
+        }
+
+        return FluentFuture.from(loadIcon(context, rule.getPackageName(), rule.getIconResId()))
+                .transform(icon ->
+                                icon != null ? icon : getFallbackIcon(context, rule.getType()),
+                        MoreExecutors.directExecutor());
+    }
+
+    @NonNull
+    private ListenableFuture</* @Nullable */ Drawable> loadIcon(Context context, String pkg,
+            int iconResId) {
+        String cacheKey = pkg + ":" + iconResId;
+        synchronized (mCache) {
+            Drawable cachedValue = mCache.get(cacheKey);
+            if (cachedValue != null) {
+                return immediateFuture(cachedValue != MISSING ? cachedValue : null);
+            }
+        }
+
+        return FluentFuture.from(mBackgroundExecutor.submit(() -> {
+            if (TextUtils.isEmpty(pkg) || SystemZenRules.PACKAGE_ANDROID.equals(pkg)) {
+                return context.getDrawable(iconResId);
+            } else {
+                Context appContext = context.createPackageContext(pkg, 0);
+                Drawable appDrawable = AppCompatResources.getDrawable(appContext, iconResId);
+                return getMonochromeIconIfPresent(appDrawable);
+            }
+        })).catching(Exception.class, ex -> {
+            // If we cannot resolve the icon, then store MISSING in the cache below, so
+            // we don't try again.
+            Log.e(TAG, "Error while loading icon " + cacheKey, ex);
+            return null;
+        }, MoreExecutors.directExecutor()).transform(drawable -> {
+            synchronized (mCache) {
+                mCache.put(cacheKey, drawable != null ? drawable : MISSING);
+            }
+            return drawable;
+        }, MoreExecutors.directExecutor());
+    }
+
+    private static Drawable getFallbackIcon(Context context, int ruleType) {
+        int iconResIdFromType = switch (ruleType) {
+            case AutomaticZenRule.TYPE_UNKNOWN ->
+                    com.android.internal.R.drawable.ic_zen_mode_type_unknown;
+            case AutomaticZenRule.TYPE_OTHER ->
+                    com.android.internal.R.drawable.ic_zen_mode_type_other;
+            case AutomaticZenRule.TYPE_SCHEDULE_TIME ->
+                    com.android.internal.R.drawable.ic_zen_mode_type_schedule_time;
+            case AutomaticZenRule.TYPE_SCHEDULE_CALENDAR ->
+                    com.android.internal.R.drawable.ic_zen_mode_type_schedule_calendar;
+            case AutomaticZenRule.TYPE_BEDTIME ->
+                    com.android.internal.R.drawable.ic_zen_mode_type_bedtime;
+            case AutomaticZenRule.TYPE_DRIVING ->
+                    com.android.internal.R.drawable.ic_zen_mode_type_driving;
+            case AutomaticZenRule.TYPE_IMMERSIVE ->
+                    com.android.internal.R.drawable.ic_zen_mode_type_immersive;
+            case AutomaticZenRule.TYPE_THEATER ->
+                    com.android.internal.R.drawable.ic_zen_mode_type_theater;
+            case AutomaticZenRule.TYPE_MANAGED ->
+                    com.android.internal.R.drawable.ic_zen_mode_type_managed;
+            default -> com.android.internal.R.drawable.ic_zen_mode_type_unknown;
+        };
+        return requireNonNull(context.getDrawable(iconResIdFromType));
+    }
+
+    private static Drawable getMonochromeIconIfPresent(Drawable icon) {
+        // For created rules, the app should've provided a monochrome Drawable. However, implicit
+        // rules have the app's icon, which is not -- but might have a monochrome layer. Thus
+        // we choose it, if present.
+        if (icon instanceof AdaptiveIconDrawable adaptiveIcon) {
+            if (adaptiveIcon.getMonochrome() != null) {
+                // Wrap with negative inset => scale icon (inspired from BaseIconFactory)
+                return new InsetDrawable(adaptiveIcon.getMonochrome(),
+                        -2.0f * AdaptiveIconDrawable.getExtraInsetFraction());
+            }
+        }
+        return icon;
+    }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java
new file mode 100644
index 0000000..33d39f0
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java
@@ -0,0 +1,307 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.notification.modes;
+
+import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL;
+import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
+import static android.service.notification.SystemZenRules.getTriggerDescriptionForScheduleEvent;
+import static android.service.notification.SystemZenRules.getTriggerDescriptionForScheduleTime;
+import static android.service.notification.ZenModeConfig.tryParseEventConditionId;
+import static android.service.notification.ZenModeConfig.tryParseScheduleConditionId;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.SuppressLint;
+import android.app.AutomaticZenRule;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.service.notification.SystemZenRules;
+import android.service.notification.ZenDeviceEffects;
+import android.service.notification.ZenModeConfig;
+import android.service.notification.ZenPolicy;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settingslib.R;
+
+import com.google.common.base.Strings;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.Objects;
+
+/**
+ * Represents either an {@link AutomaticZenRule} or the manual DND rule in a unified way.
+ *
+ * <p>It also adapts other rule features that we don't want to expose in the UI, such as
+ * interruption filters other than {@code PRIORITY}, rules without specific icons, etc.
+ */
+public class ZenMode {
+
+    private static final String TAG = "ZenMode";
+
+    static final String MANUAL_DND_MODE_ID = "manual_dnd";
+
+    // Must match com.android.server.notification.ZenModeHelper#applyCustomPolicy.
+    private static final ZenPolicy POLICY_INTERRUPTION_FILTER_ALARMS =
+            new ZenPolicy.Builder()
+                    .disallowAllSounds()
+                    .allowAlarms(true)
+                    .allowMedia(true)
+                    .allowPriorityChannels(false)
+                    .build();
+
+    // Must match com.android.server.notification.ZenModeHelper#applyCustomPolicy.
+    private static final ZenPolicy POLICY_INTERRUPTION_FILTER_NONE =
+            new ZenPolicy.Builder()
+                    .disallowAllSounds()
+                    .hideAllVisualEffects()
+                    .allowPriorityChannels(false)
+                    .build();
+
+    public enum Status {
+        ENABLED,
+        ENABLED_AND_ACTIVE,
+        DISABLED_BY_USER,
+        DISABLED_BY_OTHER
+    }
+
+    private final String mId;
+    private final AutomaticZenRule mRule;
+    private final Status mStatus;
+    private final boolean mIsManualDnd;
+
+    /**
+     * Initializes a {@link ZenMode}, mainly based on the information from the
+     * {@link AutomaticZenRule}.
+     *
+     * <p>Some pieces which are not part of the public API (such as whether the mode is currently
+     * active, or the reason it was disabled) are read from the {@link ZenModeConfig.ZenRule} --
+     * see {@link #computeStatus}.
+     */
+    public ZenMode(String id, @NonNull AutomaticZenRule rule,
+            @NonNull ZenModeConfig.ZenRule zenRuleExtraData) {
+        this(id, rule, computeStatus(zenRuleExtraData), false);
+    }
+
+    private static Status computeStatus(@NonNull ZenModeConfig.ZenRule zenRuleExtraData) {
+        if (zenRuleExtraData.enabled) {
+            if (zenRuleExtraData.isAutomaticActive()) {
+                return Status.ENABLED_AND_ACTIVE;
+            } else {
+                return Status.ENABLED;
+            }
+        } else {
+            if (zenRuleExtraData.disabledOrigin == ZenModeConfig.UPDATE_ORIGIN_USER) {
+                return Status.DISABLED_BY_USER;
+            } else {
+                return Status.DISABLED_BY_OTHER; // by APP, SYSTEM, UNKNOWN.
+            }
+        }
+    }
+
+    public static ZenMode manualDndMode(AutomaticZenRule manualRule, boolean isActive) {
+        return new ZenMode(MANUAL_DND_MODE_ID, manualRule,
+                isActive ? Status.ENABLED_AND_ACTIVE : Status.ENABLED, true);
+    }
+
+    private ZenMode(String id, @NonNull AutomaticZenRule rule, Status status, boolean isManualDnd) {
+        mId = id;
+        mRule = rule;
+        mStatus = status;
+        mIsManualDnd = isManualDnd;
+    }
+
+    @NonNull
+    public String getId() {
+        return mId;
+    }
+
+    @NonNull
+    public AutomaticZenRule getRule() {
+        return mRule;
+    }
+
+    @NonNull
+    public String getName() {
+        return Strings.nullToEmpty(mRule.getName());
+    }
+
+    @NonNull
+    public Status getStatus() {
+        return mStatus;
+    }
+
+    @AutomaticZenRule.Type
+    public int getType() {
+        return mRule.getType();
+    }
+
+    @Nullable
+    public String getTriggerDescription() {
+        return mRule.getTriggerDescription();
+    }
+
+    @NonNull
+    public ListenableFuture<Drawable> getIcon(@NonNull Context context,
+            @NonNull ZenIconLoader iconLoader) {
+        if (mIsManualDnd) {
+            return Futures.immediateFuture(requireNonNull(
+                    context.getDrawable(R.drawable.ic_do_not_disturb_on_24dp)));
+        }
+
+        return iconLoader.getIcon(context, mRule);
+    }
+
+    @NonNull
+    public ZenPolicy getPolicy() {
+        switch (mRule.getInterruptionFilter()) {
+            case INTERRUPTION_FILTER_PRIORITY:
+            case NotificationManager.INTERRUPTION_FILTER_ALL:
+                return requireNonNull(mRule.getZenPolicy());
+
+            case NotificationManager.INTERRUPTION_FILTER_ALARMS:
+                return POLICY_INTERRUPTION_FILTER_ALARMS;
+
+            case NotificationManager.INTERRUPTION_FILTER_NONE:
+                return POLICY_INTERRUPTION_FILTER_NONE;
+
+            case NotificationManager.INTERRUPTION_FILTER_UNKNOWN:
+            default:
+                Log.wtf(TAG, "Rule " + mId + " with unexpected interruptionFilter "
+                        + mRule.getInterruptionFilter());
+                return requireNonNull(mRule.getZenPolicy());
+        }
+    }
+
+    /**
+     * Updates the {@link ZenPolicy} of the associated {@link AutomaticZenRule} based on the
+     * supplied policy. In some cases this involves conversions, so that the following call
+     * to {@link #getPolicy} might return a different policy from the one supplied here.
+     */
+    @SuppressLint("WrongConstant")
+    public void setPolicy(@NonNull ZenPolicy policy) {
+        ZenPolicy currentPolicy = getPolicy();
+        if (currentPolicy.equals(policy)) {
+            return;
+        }
+
+        if (mRule.getInterruptionFilter() == INTERRUPTION_FILTER_ALL) {
+            Log.wtf(TAG, "Able to change policy without filtering being enabled");
+        }
+
+        // If policy is customized from any of the "special" ones, make the rule PRIORITY.
+        if (mRule.getInterruptionFilter() != INTERRUPTION_FILTER_PRIORITY) {
+            mRule.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY);
+        }
+        mRule.setZenPolicy(policy);
+    }
+
+    @NonNull
+    public ZenDeviceEffects getDeviceEffects() {
+        return mRule.getDeviceEffects() != null
+                ? mRule.getDeviceEffects()
+                : new ZenDeviceEffects.Builder().build();
+    }
+
+    public void setCustomModeConditionId(Context context, Uri conditionId) {
+        checkState(SystemZenRules.PACKAGE_ANDROID.equals(mRule.getPackageName()),
+                "Trying to change condition of non-system-owned rule %s (to %s)",
+                mRule, conditionId);
+
+        Uri oldCondition = mRule.getConditionId();
+        mRule.setConditionId(conditionId);
+
+        ZenModeConfig.ScheduleInfo scheduleInfo = tryParseScheduleConditionId(conditionId);
+        if (scheduleInfo != null) {
+            mRule.setType(AutomaticZenRule.TYPE_SCHEDULE_TIME);
+            mRule.setOwner(ZenModeConfig.getScheduleConditionProvider());
+            mRule.setTriggerDescription(
+                    getTriggerDescriptionForScheduleTime(context, scheduleInfo));
+            return;
+        }
+
+        ZenModeConfig.EventInfo eventInfo = tryParseEventConditionId(conditionId);
+        if (eventInfo != null) {
+            mRule.setType(AutomaticZenRule.TYPE_SCHEDULE_CALENDAR);
+            mRule.setOwner(ZenModeConfig.getEventConditionProvider());
+            mRule.setTriggerDescription(getTriggerDescriptionForScheduleEvent(context, eventInfo));
+            return;
+        }
+
+        if (ZenModeConfig.isValidCustomManualConditionId(conditionId)) {
+            mRule.setType(AutomaticZenRule.TYPE_OTHER);
+            mRule.setOwner(ZenModeConfig.getCustomManualConditionProvider());
+            mRule.setTriggerDescription("");
+            return;
+        }
+
+        Log.wtf(TAG, String.format(
+                "Changed condition of rule %s (%s -> %s) but cannot recognize which kind of "
+                        + "condition it was!",
+                mRule, oldCondition, conditionId));
+    }
+
+    public boolean canEditName() {
+        return !isManualDnd();
+    }
+
+    public boolean canEditIcon() {
+        return !isManualDnd();
+    }
+
+    public boolean canBeDeleted() {
+        return !isManualDnd();
+    }
+
+    public boolean isManualDnd() {
+        return mIsManualDnd;
+    }
+
+    public boolean isActive() {
+        return mStatus == Status.ENABLED_AND_ACTIVE;
+    }
+
+    public boolean isSystemOwned() {
+        return SystemZenRules.PACKAGE_ANDROID.equals(mRule.getPackageName());
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        return obj instanceof ZenMode other
+                && mId.equals(other.mId)
+                && mRule.equals(other.mRule)
+                && mStatus.equals(other.mStatus)
+                && mIsManualDnd == other.mIsManualDnd;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mId, mRule, mStatus, mIsManualDnd);
+    }
+
+    @Override
+    public String toString() {
+        return mId + " (" + mStatus + ") -> " + mRule;
+    }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModesBackend.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModesBackend.java
new file mode 100644
index 0000000..5529da0
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModesBackend.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.notification.modes;
+
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.AutomaticZenRule;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.net.Uri;
+import android.provider.Settings;
+import android.service.notification.Condition;
+import android.service.notification.ZenModeConfig;
+import android.util.Log;
+
+import com.android.settingslib.R;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Class used for Settings-NMS interactions related to Mode management.
+ *
+ * <p>This class converts {@link AutomaticZenRule} instances, as well as the manual zen mode,
+ * into the unified {@link ZenMode} format.
+ */
+public class ZenModesBackend {
+
+    private static final String TAG = "ZenModeBackend";
+
+    @Nullable // Until first usage
+    private static ZenModesBackend sInstance;
+
+    private final NotificationManager mNotificationManager;
+
+    private final Context mContext;
+
+    public static ZenModesBackend getInstance(Context context) {
+        if (sInstance == null) {
+            sInstance = new ZenModesBackend(context.getApplicationContext());
+        }
+        return sInstance;
+    }
+
+    ZenModesBackend(Context context) {
+        mContext = context;
+        mNotificationManager = context.getSystemService(NotificationManager.class);
+    }
+
+    public List<ZenMode> getModes() {
+        Map<String, AutomaticZenRule> zenRules = mNotificationManager.getAutomaticZenRules();
+        ZenModeConfig currentConfig = mNotificationManager.getZenModeConfig();
+
+        ArrayList<ZenMode> modes = new ArrayList<>();
+        modes.add(getManualDndMode(currentConfig));
+
+        for (Map.Entry<String, AutomaticZenRule> zenRuleEntry : zenRules.entrySet()) {
+            String ruleId = zenRuleEntry.getKey();
+            ZenModeConfig.ZenRule extraData = currentConfig.automaticRules.get(ruleId);
+            if (extraData != null) {
+                modes.add(new ZenMode(ruleId, zenRuleEntry.getValue(), extraData));
+            } else {
+                Log.w(TAG, "Found AZR " + zenRuleEntry.getValue()
+                        + " but no corresponding entry in ZenModeConfig (" + currentConfig
+                        + "). Skipping");
+            }
+        }
+
+        // Manual DND first, then alphabetically.
+        modes.sort(Comparator.comparing(ZenMode::isManualDnd).reversed()
+                .thenComparing(ZenMode::getName));
+
+        return modes;
+    }
+
+    @Nullable
+    public ZenMode getMode(String id) {
+        ZenModeConfig currentConfig = mNotificationManager.getZenModeConfig();
+        if (ZenMode.MANUAL_DND_MODE_ID.equals(id)) {
+            return getManualDndMode(currentConfig);
+        } else {
+            AutomaticZenRule rule = mNotificationManager.getAutomaticZenRule(id);
+            ZenModeConfig.ZenRule extraData = currentConfig.automaticRules.get(id);
+            if (rule == null || extraData == null) {
+                return null;
+            }
+            return new ZenMode(id, rule, extraData);
+        }
+    }
+
+    private ZenMode getManualDndMode(ZenModeConfig config) {
+        ZenModeConfig.ZenRule manualRule = config.manualRule;
+        // TODO: b/333682392 - Replace with final strings for name & trigger description
+        AutomaticZenRule manualDndRule = new AutomaticZenRule.Builder(
+                mContext.getString(R.string.zen_mode_settings_title), manualRule.conditionId)
+                .setType(manualRule.type)
+                .setZenPolicy(manualRule.zenPolicy)
+                .setDeviceEffects(manualRule.zenDeviceEffects)
+                .setManualInvocationAllowed(manualRule.allowManualInvocation)
+                .setConfigurationActivity(null) // No further settings
+                .setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY)
+                .build();
+
+        return ZenMode.manualDndMode(manualDndRule, config != null && config.isManualActive());
+    }
+
+    public void updateMode(ZenMode mode) {
+        if (mode.isManualDnd()) {
+            try {
+                NotificationManager.Policy dndPolicy =
+                        new ZenModeConfig().toNotificationPolicy(mode.getPolicy());
+                mNotificationManager.setNotificationPolicy(dndPolicy, /* fromUser= */ true);
+
+                mNotificationManager.setManualZenRuleDeviceEffects(
+                        mode.getRule().getDeviceEffects());
+            } catch (Exception e) {
+                Log.w(TAG, "Error updating manual mode", e);
+            }
+        } else {
+            mNotificationManager.updateAutomaticZenRule(mode.getId(), mode.getRule(),
+                    /* fromUser= */ true);
+        }
+    }
+
+    public void activateMode(ZenMode mode, @Nullable Duration forDuration) {
+        if (mode.isManualDnd()) {
+            Uri durationConditionId = null;
+            if (forDuration != null) {
+                durationConditionId = ZenModeConfig.toTimeCondition(mContext,
+                        (int) forDuration.toMinutes(), ActivityManager.getCurrentUser(), true).id;
+            }
+            mNotificationManager.setZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS,
+                    durationConditionId, TAG, /* fromUser= */ true);
+
+        } else {
+            if (forDuration != null) {
+                throw new IllegalArgumentException(
+                        "Only the manual DND mode can be activated for a specific duration");
+            }
+            mNotificationManager.setAutomaticZenRuleState(mode.getId(),
+                    new Condition(mode.getRule().getConditionId(), "", Condition.STATE_TRUE,
+                            Condition.SOURCE_USER_ACTION));
+        }
+    }
+
+    public void deactivateMode(ZenMode mode) {
+        if (mode.isManualDnd()) {
+            // When calling with fromUser=true this will not snooze other modes.
+            mNotificationManager.setZenMode(Settings.Global.ZEN_MODE_OFF, null, TAG,
+                    /* fromUser= */ true);
+        } else {
+            // TODO: b/333527800 - This should (potentially) snooze the rule if it was active.
+            mNotificationManager.setAutomaticZenRuleState(mode.getId(),
+                    new Condition(mode.getRule().getConditionId(), "", Condition.STATE_FALSE,
+                            Condition.SOURCE_USER_ACTION));
+        }
+    }
+
+    public void removeMode(ZenMode mode) {
+        if (!mode.canBeDeleted()) {
+            throw new IllegalArgumentException("Mode " + mode + " cannot be deleted!");
+        }
+        mNotificationManager.removeAutomaticZenRule(mode.getId(), /* fromUser= */ true);
+    }
+
+    /**
+     * Creates a new custom mode with the provided {@code name}. The mode will be "manual" (i.e.
+     * not have a schedule), this can be later updated by the user in the mode settings page.
+     *
+     * @return the created mode. Only {@code null} if creation failed due to an internal error
+     */
+    @Nullable
+    public ZenMode addCustomMode(String name) {
+        AutomaticZenRule rule = new AutomaticZenRule.Builder(name,
+                ZenModeConfig.toCustomManualConditionId())
+                .setPackage(ZenModeConfig.getCustomManualConditionProvider().getPackageName())
+                .setType(AutomaticZenRule.TYPE_OTHER)
+                .setOwner(ZenModeConfig.getCustomManualConditionProvider())
+                .setManualInvocationAllowed(true)
+                .build();
+
+        String ruleId = mNotificationManager.addAutomaticZenRule(rule);
+        return getMode(ruleId);
+    }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/statusbar/notification/data/model/ZenMode.kt b/packages/SettingsLib/src/com/android/settingslib/statusbar/notification/data/model/ZenMode.kt
deleted file mode 100644
index a696f8c..0000000
--- a/packages/SettingsLib/src/com/android/settingslib/statusbar/notification/data/model/ZenMode.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *       http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-
-package com.android.settingslib.statusbar.notification.data.model
-
-import android.provider.Settings.Global
-
-/** Validating wrapper for [android.app.NotificationManager.getZenMode] values. */
-@JvmInline
-value class ZenMode(val zenMode: Int) {
-
-    init {
-        require(zenMode in supportedModes) { "Unsupported zenMode=$zenMode" }
-    }
-
-    private companion object {
-
-        val supportedModes =
-            listOf(
-                Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS,
-                Global.ZEN_MODE_NO_INTERRUPTIONS,
-                Global.ZEN_MODE_ALARMS,
-                Global.ZEN_MODE_OFF,
-            )
-    }
-}
diff --git a/packages/SettingsLib/src/com/android/settingslib/statusbar/notification/data/repository/FakeNotificationsSoundPolicyRepository.kt b/packages/SettingsLib/src/com/android/settingslib/statusbar/notification/data/repository/FakeZenModeRepository.kt
similarity index 80%
rename from packages/SettingsLib/src/com/android/settingslib/statusbar/notification/data/repository/FakeNotificationsSoundPolicyRepository.kt
rename to packages/SettingsLib/src/com/android/settingslib/statusbar/notification/data/repository/FakeZenModeRepository.kt
index a939ed1..775e2fc 100644
--- a/packages/SettingsLib/src/com/android/settingslib/statusbar/notification/data/repository/FakeNotificationsSoundPolicyRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/statusbar/notification/data/repository/FakeZenModeRepository.kt
@@ -18,19 +18,18 @@
 
 import android.app.NotificationManager
 import android.provider.Settings
-import com.android.settingslib.statusbar.notification.data.model.ZenMode
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 
-class FakeNotificationsSoundPolicyRepository : NotificationsSoundPolicyRepository {
+class FakeZenModeRepository : ZenModeRepository {
 
     private val mutableNotificationPolicy = MutableStateFlow<NotificationManager.Policy?>(null)
-    override val notificationPolicy: StateFlow<NotificationManager.Policy?>
+    override val consolidatedNotificationPolicy: StateFlow<NotificationManager.Policy?>
         get() = mutableNotificationPolicy.asStateFlow()
 
-    private val mutableZenMode = MutableStateFlow<ZenMode?>(ZenMode(Settings.Global.ZEN_MODE_OFF))
-    override val zenMode: StateFlow<ZenMode?>
+    private val mutableZenMode = MutableStateFlow(Settings.Global.ZEN_MODE_OFF)
+    override val globalZenMode: StateFlow<Int>
         get() = mutableZenMode.asStateFlow()
 
     init {
@@ -41,12 +40,12 @@
         mutableNotificationPolicy.value = policy
     }
 
-    fun updateZenMode(zenMode: ZenMode?) {
+    fun updateZenMode(zenMode: Int) {
         mutableZenMode.value = zenMode
     }
 }
 
-fun FakeNotificationsSoundPolicyRepository.updateNotificationPolicy(
+fun FakeZenModeRepository.updateNotificationPolicy(
     priorityCategories: Int = 0,
     priorityCallSenders: Int = NotificationManager.Policy.PRIORITY_SENDERS_ANY,
     priorityMessageSenders: Int = NotificationManager.Policy.CONVERSATION_SENDERS_NONE,
diff --git a/packages/SettingsLib/src/com/android/settingslib/statusbar/notification/data/repository/NotificationsSoundPolicyRepository.kt b/packages/SettingsLib/src/com/android/settingslib/statusbar/notification/data/repository/NotificationsSoundPolicyRepository.kt
deleted file mode 100644
index 0fb8c3f..0000000
--- a/packages/SettingsLib/src/com/android/settingslib/statusbar/notification/data/repository/NotificationsSoundPolicyRepository.kt
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *       http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-
-package com.android.settingslib.statusbar.notification.data.repository
-
-import android.app.NotificationManager
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-import android.content.IntentFilter
-import com.android.settingslib.statusbar.notification.data.model.ZenMode
-import kotlin.coroutines.CoroutineContext
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.callbackFlow
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.flow.shareIn
-import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.launch
-
-/** Provides state of volume policy and restrictions imposed by notifications. */
-interface NotificationsSoundPolicyRepository {
-
-    /** @see NotificationManager.getNotificationPolicy */
-    val notificationPolicy: StateFlow<NotificationManager.Policy?>
-
-    /** @see NotificationManager.getZenMode */
-    val zenMode: StateFlow<ZenMode?>
-}
-
-class NotificationsSoundPolicyRepositoryImpl(
-    private val context: Context,
-    private val notificationManager: NotificationManager,
-    scope: CoroutineScope,
-    backgroundCoroutineContext: CoroutineContext,
-) : NotificationsSoundPolicyRepository {
-
-    private val notificationBroadcasts =
-        callbackFlow {
-                val receiver =
-                    object : BroadcastReceiver() {
-                        override fun onReceive(context: Context?, intent: Intent?) {
-                            intent?.action?.let { action -> launch { send(action) } }
-                        }
-                    }
-
-                context.registerReceiver(
-                    receiver,
-                    IntentFilter().apply {
-                        addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED)
-                        addAction(NotificationManager.ACTION_NOTIFICATION_POLICY_CHANGED)
-                    }
-                )
-
-                awaitClose { context.unregisterReceiver(receiver) }
-            }
-            .shareIn(
-                started = SharingStarted.WhileSubscribed(),
-                scope = scope,
-            )
-
-    override val notificationPolicy: StateFlow<NotificationManager.Policy?> =
-        notificationBroadcasts
-            .filter { NotificationManager.ACTION_NOTIFICATION_POLICY_CHANGED == it }
-            .map { notificationManager.consolidatedNotificationPolicy }
-            .onStart { emit(notificationManager.consolidatedNotificationPolicy) }
-            .flowOn(backgroundCoroutineContext)
-            .stateIn(scope, SharingStarted.WhileSubscribed(), null)
-
-    override val zenMode: StateFlow<ZenMode?> =
-        notificationBroadcasts
-            .filter { NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED == it }
-            .map { ZenMode(notificationManager.zenMode) }
-            .onStart { emit(ZenMode(notificationManager.zenMode)) }
-            .flowOn(backgroundCoroutineContext)
-            .stateIn(scope, SharingStarted.WhileSubscribed(), null)
-}
diff --git a/packages/SettingsLib/src/com/android/settingslib/statusbar/notification/data/repository/ZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/statusbar/notification/data/repository/ZenModeRepository.kt
new file mode 100644
index 0000000..4d25237
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/statusbar/notification/data/repository/ZenModeRepository.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package com.android.settingslib.statusbar.notification.data.repository
+
+import android.app.NotificationManager
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import com.android.settingslib.flags.Flags
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+/** Provides state of volume policy and restrictions imposed by notifications. */
+interface ZenModeRepository {
+    /** @see NotificationManager.getConsolidatedNotificationPolicy */
+    val consolidatedNotificationPolicy: StateFlow<NotificationManager.Policy?>
+
+    /** @see NotificationManager.getZenMode */
+    val globalZenMode: StateFlow<Int?>
+}
+
+class ZenModeRepositoryImpl(
+    private val context: Context,
+    private val notificationManager: NotificationManager,
+    val scope: CoroutineScope,
+    val backgroundCoroutineContext: CoroutineContext,
+) : ZenModeRepository {
+
+    private val notificationBroadcasts =
+        callbackFlow {
+                val receiver =
+                    object : BroadcastReceiver() {
+                        override fun onReceive(context: Context?, intent: Intent?) {
+                            intent?.action?.let { action -> launch { send(action) } }
+                        }
+                    }
+
+                context.registerReceiver(
+                    receiver,
+                    IntentFilter().apply {
+                        addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED)
+                        addAction(NotificationManager.ACTION_NOTIFICATION_POLICY_CHANGED)
+                        if (Flags.volumePanelBroadcastFix() && android.app.Flags.modesApi())
+                            addAction(
+                                NotificationManager.ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED)
+                    })
+
+                awaitClose { context.unregisterReceiver(receiver) }
+            }
+            .apply {
+                if (Flags.volumePanelBroadcastFix()) {
+                    flowOn(backgroundCoroutineContext)
+                    stateIn(scope, SharingStarted.WhileSubscribed(), null)
+                } else {
+                    shareIn(
+                        started = SharingStarted.WhileSubscribed(),
+                        scope = scope,
+                    )
+                }
+            }
+
+    override val consolidatedNotificationPolicy: StateFlow<NotificationManager.Policy?> =
+        if (Flags.volumePanelBroadcastFix() && android.app.Flags.modesApi())
+            flowFromBroadcast(NotificationManager.ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED) {
+                notificationManager.consolidatedNotificationPolicy
+            }
+        else
+            flowFromBroadcast(NotificationManager.ACTION_NOTIFICATION_POLICY_CHANGED) {
+                notificationManager.consolidatedNotificationPolicy
+            }
+
+    override val globalZenMode: StateFlow<Int?> =
+        flowFromBroadcast(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED) {
+            notificationManager.zenMode
+        }
+
+    private fun <T> flowFromBroadcast(intentAction: String, mapper: () -> T) =
+        notificationBroadcasts
+            .filter { intentAction == it }
+            .map { mapper() }
+            .onStart { emit(mapper()) }
+            .flowOn(backgroundCoroutineContext)
+            .stateIn(scope, SharingStarted.WhileSubscribed(), null)
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/statusbar/notification/domain/interactor/NotificationsSoundPolicyInteractor.kt b/packages/SettingsLib/src/com/android/settingslib/statusbar/notification/domain/interactor/NotificationsSoundPolicyInteractor.kt
index 7719c4b..953c90d 100644
--- a/packages/SettingsLib/src/com/android/settingslib/statusbar/notification/domain/interactor/NotificationsSoundPolicyInteractor.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/statusbar/notification/domain/interactor/NotificationsSoundPolicyInteractor.kt
@@ -20,8 +20,7 @@
 import android.media.AudioManager
 import android.provider.Settings
 import android.service.notification.ZenModeConfig
-import com.android.settingslib.statusbar.notification.data.model.ZenMode
-import com.android.settingslib.statusbar.notification.data.repository.NotificationsSoundPolicyRepository
+import com.android.settingslib.statusbar.notification.data.repository.ZenModeRepository
 import com.android.settingslib.volume.shared.model.AudioStream
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.StateFlow
@@ -30,17 +29,15 @@
 import kotlinx.coroutines.flow.map
 
 /** Determines notification sounds state and limitations. */
-class NotificationsSoundPolicyInteractor(
-    private val repository: NotificationsSoundPolicyRepository
-) {
+class NotificationsSoundPolicyInteractor(private val repository: ZenModeRepository) {
 
     /** @see NotificationManager.getNotificationPolicy */
-    val notificationPolicy: StateFlow<NotificationManager.Policy?>
-        get() = repository.notificationPolicy
+    private val notificationPolicy: StateFlow<NotificationManager.Policy?>
+        get() = repository.consolidatedNotificationPolicy
 
     /** @see NotificationManager.getZenMode */
-    val zenMode: StateFlow<ZenMode?>
-        get() = repository.zenMode
+    val zenMode: StateFlow<Int?>
+        get() = repository.globalZenMode
 
     /** Checks if [notificationPolicy] allows alarms. */
     val areAlarmsAllowed: Flow<Boolean?> = notificationPolicy.map { it?.allowAlarms() }
@@ -67,7 +64,7 @@
             isRingerAllowed.filterNotNull(),
             isSystemAllowed.filterNotNull(),
         ) { zenMode, areAlarmsAllowed, isMediaAllowed, isRingerAllowed, isSystemAllowed ->
-            when (zenMode.zenMode) {
+            when (zenMode) {
                 // Everything is muted
                 Settings.Global.ZEN_MODE_NO_INTERRUPTIONS -> return@combine true
                 Settings.Global.ZEN_MODE_ALARMS ->
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/statusbar/notification/data/repository/NotificationsSoundPolicyRepositoryTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/statusbar/notification/data/repository/ZenModeRepositoryTest.kt
similarity index 66%
rename from packages/SettingsLib/tests/integ/src/com/android/settingslib/statusbar/notification/data/repository/NotificationsSoundPolicyRepositoryTest.kt
rename to packages/SettingsLib/tests/integ/src/com/android/settingslib/statusbar/notification/data/repository/ZenModeRepositoryTest.kt
index dfc4c0a..688bebb 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/statusbar/notification/data/repository/NotificationsSoundPolicyRepositoryTest.kt
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/statusbar/notification/data/repository/ZenModeRepositoryTest.kt
@@ -20,10 +20,12 @@
 import android.content.BroadcastReceiver
 import android.content.Context
 import android.content.Intent
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import android.provider.Settings.Global
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.settingslib.statusbar.notification.data.model.ZenMode
+import com.android.settingslib.flags.Flags
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.launchIn
@@ -45,13 +47,15 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(AndroidJUnit4::class)
 @SmallTest
-class NotificationsSoundPolicyRepositoryTest {
+class ZenModeRepositoryTest {
 
     @Mock private lateinit var context: Context
+
     @Mock private lateinit var notificationManager: NotificationManager
+
     @Captor private lateinit var receiverCaptor: ArgumentCaptor<BroadcastReceiver>
 
-    private lateinit var underTest: NotificationsSoundPolicyRepository
+    private lateinit var underTest: ZenModeRepository
 
     private val testScope: TestScope = TestScope()
 
@@ -60,7 +64,7 @@
         MockitoAnnotations.initMocks(this)
 
         underTest =
-            NotificationsSoundPolicyRepositoryImpl(
+            ZenModeRepositoryImpl(
                 context,
                 notificationManager,
                 testScope.backgroundScope,
@@ -68,15 +72,18 @@
             )
     }
 
+    @DisableFlags(android.app.Flags.FLAG_MODES_API, Flags.FLAG_VOLUME_PANEL_BROADCAST_FIX)
     @Test
-    fun policyChanges_repositoryEmits() {
+    fun consolidatedPolicyChanges_repositoryEmits_flagsOff() {
         testScope.runTest {
             val values = mutableListOf<NotificationManager.Policy?>()
-            `when`(notificationManager.notificationPolicy).thenReturn(testPolicy1)
-            underTest.notificationPolicy.onEach { values.add(it) }.launchIn(backgroundScope)
+            `when`(notificationManager.consolidatedNotificationPolicy).thenReturn(testPolicy1)
+            underTest.consolidatedNotificationPolicy
+                .onEach { values.add(it) }
+                .launchIn(backgroundScope)
             runCurrent()
 
-            `when`(notificationManager.notificationPolicy).thenReturn(testPolicy2)
+            `when`(notificationManager.consolidatedNotificationPolicy).thenReturn(testPolicy2)
             triggerIntent(NotificationManager.ACTION_NOTIFICATION_POLICY_CHANGED)
             runCurrent()
 
@@ -86,12 +93,33 @@
         }
     }
 
+    @EnableFlags(android.app.Flags.FLAG_MODES_API, Flags.FLAG_VOLUME_PANEL_BROADCAST_FIX)
+    @Test
+    fun consolidatedPolicyChanges_repositoryEmits_flagsOn() {
+        testScope.runTest {
+            val values = mutableListOf<NotificationManager.Policy?>()
+            `when`(notificationManager.consolidatedNotificationPolicy).thenReturn(testPolicy1)
+            underTest.consolidatedNotificationPolicy
+                .onEach { values.add(it) }
+                .launchIn(backgroundScope)
+            runCurrent()
+
+            `when`(notificationManager.consolidatedNotificationPolicy).thenReturn(testPolicy2)
+            triggerIntent(NotificationManager.ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED)
+            runCurrent()
+
+            assertThat(values)
+                .containsExactlyElementsIn(listOf(null, testPolicy1, testPolicy2))
+                .inOrder()
+        }
+    }
+
     @Test
     fun zenModeChanges_repositoryEmits() {
         testScope.runTest {
-            val values = mutableListOf<ZenMode?>()
+            val values = mutableListOf<Int?>()
             `when`(notificationManager.zenMode).thenReturn(Global.ZEN_MODE_OFF)
-            underTest.zenMode.onEach { values.add(it) }.launchIn(backgroundScope)
+            underTest.globalZenMode.onEach { values.add(it) }.launchIn(backgroundScope)
             runCurrent()
 
             `when`(notificationManager.zenMode).thenReturn(Global.ZEN_MODE_ALARMS)
@@ -100,8 +128,7 @@
 
             assertThat(values)
                 .containsExactlyElementsIn(
-                    listOf(null, ZenMode(Global.ZEN_MODE_OFF), ZenMode(Global.ZEN_MODE_ALARMS))
-                )
+                    listOf(null, Global.ZEN_MODE_OFF, Global.ZEN_MODE_ALARMS))
                 .inOrder()
         }
     }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenIconLoaderTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenIconLoaderTest.java
new file mode 100644
index 0000000..20461e3
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenIconLoaderTest.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.notification.modes;
+
+import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.AutomaticZenRule;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.service.notification.ZenPolicy;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public class ZenIconLoaderTest {
+
+    private Context mContext;
+    private ZenIconLoader mLoader;
+
+    @Before
+    public void setUp() {
+        mContext = RuntimeEnvironment.application;
+        mLoader = new ZenIconLoader(MoreExecutors.newDirectExecutorService());
+    }
+
+    @Test
+    public void getIcon_systemOwnedRuleWithIcon_loads() throws Exception {
+        AutomaticZenRule systemRule = newRuleBuilder()
+                .setPackage("android")
+                .setIconResId(android.R.drawable.ic_media_play)
+                .build();
+
+        ListenableFuture<Drawable> loadFuture = mLoader.getIcon(mContext, systemRule);
+        assertThat(loadFuture.isDone()).isTrue();
+        assertThat(loadFuture.get()).isNotNull();
+    }
+
+    @Test
+    public void getIcon_ruleWithoutSpecificIcon_loadsFallback() throws Exception {
+        AutomaticZenRule rule = newRuleBuilder()
+                .setType(AutomaticZenRule.TYPE_DRIVING)
+                .setPackage("com.blah")
+                .build();
+
+        ListenableFuture<Drawable> loadFuture = mLoader.getIcon(mContext, rule);
+        assertThat(loadFuture.isDone()).isTrue();
+        assertThat(loadFuture.get()).isNotNull();
+    }
+
+    @Test
+    public void getIcon_ruleWithAppIconWithLoadFailure_loadsFallback() throws Exception {
+        AutomaticZenRule rule = newRuleBuilder()
+                .setType(AutomaticZenRule.TYPE_DRIVING)
+                .setPackage("com.blah")
+                .setIconResId(-123456)
+                .build();
+
+        ListenableFuture<Drawable> loadFuture = mLoader.getIcon(mContext, rule);
+        assertThat(loadFuture.get()).isNotNull();
+    }
+
+    private static AutomaticZenRule.Builder newRuleBuilder() {
+        return new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
+                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                .setZenPolicy(new ZenPolicy.Builder().build());
+    }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java
new file mode 100644
index 0000000..32cdb98
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.notification.modes;
+
+import static android.app.NotificationManager.INTERRUPTION_FILTER_ALARMS;
+import static android.app.NotificationManager.INTERRUPTION_FILTER_NONE;
+import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.AutomaticZenRule;
+import android.net.Uri;
+import android.service.notification.Condition;
+import android.service.notification.ZenModeConfig;
+import android.service.notification.ZenPolicy;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class ZenModeTest {
+
+    private static final ZenPolicy ZEN_POLICY = new ZenPolicy.Builder().allowAllSounds().build();
+
+    private static final AutomaticZenRule ZEN_RULE =
+            new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
+                    .setPackage("com.some.driving.thing")
+                    .setType(AutomaticZenRule.TYPE_DRIVING)
+                    .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                    .setZenPolicy(ZEN_POLICY)
+                    .build();
+
+    @Test
+    public void testBasicMethods() {
+        ZenMode zenMode = new ZenMode("id", ZEN_RULE, zenConfigRuleFor(ZEN_RULE, true));
+
+        assertThat(zenMode.getId()).isEqualTo("id");
+        assertThat(zenMode.getRule()).isEqualTo(ZEN_RULE);
+        assertThat(zenMode.isManualDnd()).isFalse();
+        assertThat(zenMode.canBeDeleted()).isTrue();
+        assertThat(zenMode.isActive()).isTrue();
+
+        ZenMode manualMode = ZenMode.manualDndMode(ZEN_RULE, false);
+        assertThat(manualMode.getId()).isEqualTo(ZenMode.MANUAL_DND_MODE_ID);
+        assertThat(manualMode.isManualDnd()).isTrue();
+        assertThat(manualMode.canBeDeleted()).isFalse();
+        assertThat(manualMode.isActive()).isFalse();
+    }
+
+    @Test
+    public void constructor_enabledRule_statusEnabled() {
+        AutomaticZenRule azr = new AutomaticZenRule.Builder(ZEN_RULE).setEnabled(true).build();
+        ZenModeConfig.ZenRule configZenRule = zenConfigRuleFor(azr, false);
+
+        ZenMode mode = new ZenMode("id", azr, configZenRule);
+        assertThat(mode.getStatus()).isEqualTo(ZenMode.Status.ENABLED);
+        assertThat(mode.isActive()).isFalse();
+    }
+
+    @Test
+    public void constructor_activeRule_statusActive() {
+        AutomaticZenRule azr = new AutomaticZenRule.Builder(ZEN_RULE).setEnabled(true).build();
+        ZenModeConfig.ZenRule configZenRule = zenConfigRuleFor(azr, true);
+
+        ZenMode mode = new ZenMode("id", azr, configZenRule);
+        assertThat(mode.getStatus()).isEqualTo(ZenMode.Status.ENABLED_AND_ACTIVE);
+        assertThat(mode.isActive()).isTrue();
+    }
+
+    @Test
+    public void constructor_disabledRuleByUser_statusDisabledByUser() {
+        AutomaticZenRule azr = new AutomaticZenRule.Builder(ZEN_RULE).setEnabled(false).build();
+        ZenModeConfig.ZenRule configZenRule = zenConfigRuleFor(azr, false);
+        configZenRule.disabledOrigin = ZenModeConfig.UPDATE_ORIGIN_USER;
+
+        ZenMode mode = new ZenMode("id", azr, configZenRule);
+        assertThat(mode.getStatus()).isEqualTo(ZenMode.Status.DISABLED_BY_USER);
+    }
+
+    @Test
+    public void constructor_disabledRuleByOther_statusDisabledByOther() {
+        AutomaticZenRule azr = new AutomaticZenRule.Builder(ZEN_RULE).setEnabled(false).build();
+        ZenModeConfig.ZenRule configZenRule = zenConfigRuleFor(azr, false);
+        configZenRule.disabledOrigin = ZenModeConfig.UPDATE_ORIGIN_APP;
+
+        ZenMode mode = new ZenMode("id", azr, configZenRule);
+        assertThat(mode.getStatus()).isEqualTo(ZenMode.Status.DISABLED_BY_OTHER);
+    }
+
+    @Test
+    public void getPolicy_interruptionFilterPriority_returnsZenPolicy() {
+        AutomaticZenRule azr = new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
+                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                .setZenPolicy(ZEN_POLICY)
+                .build();
+        ZenMode zenMode = new ZenMode("id", azr, zenConfigRuleFor(azr, false));
+
+        assertThat(zenMode.getPolicy()).isEqualTo(ZEN_POLICY);
+    }
+
+    @Test
+    public void getPolicy_interruptionFilterAlarms_returnsPolicyAllowingAlarms() {
+        AutomaticZenRule azr = new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
+                .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
+                .setZenPolicy(ZEN_POLICY) // should be ignored
+                .build();
+        ZenMode zenMode = new ZenMode("id", azr, zenConfigRuleFor(azr, false));
+
+        assertThat(zenMode.getPolicy()).isEqualTo(
+                new ZenPolicy.Builder()
+                        .disallowAllSounds()
+                        .allowAlarms(true)
+                        .allowMedia(true)
+                        .allowPriorityChannels(false)
+                        .build());
+    }
+
+    @Test
+    public void getPolicy_interruptionFilterNone_returnsPolicyAllowingNothing() {
+        AutomaticZenRule azr = new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
+                .setInterruptionFilter(INTERRUPTION_FILTER_NONE)
+                .setZenPolicy(ZEN_POLICY) // should be ignored
+                .build();
+        ZenMode zenMode = new ZenMode("id", azr, zenConfigRuleFor(azr, false));
+
+        assertThat(zenMode.getPolicy()).isEqualTo(
+                new ZenPolicy.Builder()
+                        .disallowAllSounds()
+                        .hideAllVisualEffects()
+                        .allowPriorityChannels(false)
+                        .build());
+    }
+
+    @Test
+    public void setPolicy_setsInterruptionFilterPriority() {
+        AutomaticZenRule azr = new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
+                .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
+                .build();
+        ZenMode zenMode = new ZenMode("id", azr, zenConfigRuleFor(azr, false));
+
+        zenMode.setPolicy(ZEN_POLICY);
+
+        assertThat(zenMode.getRule().getInterruptionFilter()).isEqualTo(
+                INTERRUPTION_FILTER_PRIORITY);
+        assertThat(zenMode.getPolicy()).isEqualTo(ZEN_POLICY);
+        assertThat(zenMode.getRule().getZenPolicy()).isEqualTo(ZEN_POLICY);
+    }
+
+    private static ZenModeConfig.ZenRule zenConfigRuleFor(AutomaticZenRule azr, boolean isActive) {
+        ZenModeConfig.ZenRule zenRule = new ZenModeConfig.ZenRule();
+        zenRule.pkg = azr.getPackageName();
+        zenRule.conditionId = azr.getConditionId();
+        zenRule.enabled = azr.isEnabled();
+        if (isActive) {
+            zenRule.condition = new Condition(azr.getConditionId(), "active", Condition.STATE_TRUE);
+        }
+        return zenRule;
+    }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModesBackendTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModesBackendTest.java
new file mode 100644
index 0000000..00c7ae3
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModesBackendTest.java
@@ -0,0 +1,382 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.notification.modes;
+
+import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
+import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
+import static android.provider.Settings.Global.ZEN_MODE_OFF;
+import static android.service.notification.Condition.SOURCE_UNKNOWN;
+import static android.service.notification.Condition.STATE_FALSE;
+import static android.service.notification.Condition.STATE_TRUE;
+import static android.service.notification.ZenAdapters.notificationPolicyToZenPolicy;
+import static android.service.notification.ZenPolicy.STATE_ALLOW;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.AutomaticZenRule;
+import android.app.Flags;
+import android.app.NotificationManager;
+import android.app.NotificationManager.Policy;
+import android.content.Context;
+import android.net.Uri;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.provider.Settings;
+import android.service.notification.Condition;
+import android.service.notification.ZenDeviceEffects;
+import android.service.notification.ZenModeConfig;
+import android.service.notification.ZenPolicy;
+
+import com.android.settingslib.R;
+
+import com.google.common.collect.ImmutableMap;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadows.ShadowApplication;
+
+import java.time.Duration;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+@EnableFlags(Flags.FLAG_MODES_UI)
+public class ZenModesBackendTest {
+
+    private static final String ZEN_RULE_ID = "rule";
+    private static final AutomaticZenRule ZEN_RULE =
+            new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
+                    .setType(AutomaticZenRule.TYPE_DRIVING)
+                    .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                    .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
+                    .build();
+
+    private static final AutomaticZenRule MANUAL_DND_RULE =
+            new AutomaticZenRule.Builder("Do Not Disturb", Uri.EMPTY)
+                    .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                    .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
+                    .build();
+
+    @Mock
+    private NotificationManager mNm;
+
+    private Context mContext;
+    private ZenModesBackend mBackend;
+
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(
+            SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT);
+
+    // Helper methods to add active/inactive rule state to a config. Returns a copy.
+    private static ZenModeConfig configWithManualRule(ZenModeConfig base, boolean active) {
+        ZenModeConfig out = base.copy();
+
+        if (active) {
+            out.manualRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
+            out.manualRule.condition =
+                    new Condition(out.manualRule.conditionId, "", STATE_TRUE, SOURCE_UNKNOWN);
+        } else {
+            out.manualRule.zenMode = ZEN_MODE_OFF;
+            out.manualRule.condition =
+                    new Condition(out.manualRule.conditionId, "", STATE_FALSE, SOURCE_UNKNOWN);
+        }
+        return out;
+    }
+
+    private static ZenModeConfig configWithRule(ZenModeConfig base, String ruleId,
+            AutomaticZenRule rule, boolean active) {
+        ZenModeConfig out = base.copy();
+        out.automaticRules.put(ruleId, zenConfigRuleForRule(ruleId, rule, active));
+        return out;
+    }
+
+    private static ZenModeConfig.ZenRule zenConfigRuleForRule(String id, AutomaticZenRule azr,
+            boolean active) {
+        // Note that there are many other fields of zenRule, but here we only set the ones
+        // relevant to determining whether or not it is active.
+        ZenModeConfig.ZenRule zenRule = new ZenModeConfig.ZenRule();
+        zenRule.id = id;
+        zenRule.pkg = "package";
+        zenRule.enabled = azr.isEnabled();
+        zenRule.snoozing = false;
+        zenRule.conditionId = azr.getConditionId();
+        zenRule.condition = new Condition(azr.getConditionId(), "",
+                active ? Condition.STATE_TRUE : Condition.STATE_FALSE,
+                Condition.SOURCE_USER_ACTION);
+        return zenRule;
+    }
+
+    private static ZenMode newZenMode(String id, AutomaticZenRule azr, boolean active) {
+        return new ZenMode(id, azr, zenConfigRuleForRule(id, azr, active));
+    }
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        ShadowApplication shadowApplication = ShadowApplication.getInstance();
+        shadowApplication.setSystemService(Context.NOTIFICATION_SERVICE, mNm);
+
+        mContext = RuntimeEnvironment.application;
+        mBackend = new ZenModesBackend(mContext);
+
+        // Default catch-all case with no data. This isn't realistic, but tests below that rely
+        // on the config to get data on rules active will create those individually.
+        when(mNm.getZenModeConfig()).thenReturn(new ZenModeConfig());
+    }
+
+    @Test
+    public void getModes_containsManualDndAndZenRules() {
+        AutomaticZenRule rule2 = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
+                .setType(AutomaticZenRule.TYPE_BEDTIME)
+                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                .setZenPolicy(new ZenPolicy.Builder().disallowAllSounds().build())
+                .build();
+        Policy dndPolicy = new Policy(Policy.PRIORITY_CATEGORY_ALARMS,
+                Policy.PRIORITY_SENDERS_CONTACTS, Policy.PRIORITY_SENDERS_CONTACTS);
+        when(mNm.getAutomaticZenRules()).thenReturn(
+                ImmutableMap.of("rule1", ZEN_RULE, "rule2", rule2));
+
+        ZenModeConfig config = new ZenModeConfig();
+        config.applyNotificationPolicy(dndPolicy);
+        config = configWithRule(config, "rule1", ZEN_RULE, false);
+        config = configWithRule(config, "rule2", rule2, false);
+        assertThat(config.manualRule.zenPolicy.getPriorityCategoryAlarms()).isEqualTo(STATE_ALLOW);
+        when(mNm.getZenModeConfig()).thenReturn(config);
+
+        List<ZenMode> modes = mBackend.getModes();
+
+        // all modes exist, but none of them are currently active
+        assertThat(modes).containsExactly(
+                        ZenMode.manualDndMode(
+                                new AutomaticZenRule.Builder(
+                                        mContext.getString(R.string.zen_mode_settings_title),
+                                        Uri.EMPTY)
+                                        .setType(AutomaticZenRule.TYPE_OTHER)
+                                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                                        .setZenPolicy(notificationPolicyToZenPolicy(dndPolicy))
+                                        .setManualInvocationAllowed(true)
+                                        .build(),
+                                false),
+                        newZenMode("rule2", rule2, false),
+                        newZenMode("rule1", ZEN_RULE, false))
+                .inOrder();
+    }
+
+    @Test
+    public void getMode_manualDnd_returnsMode() {
+        Policy dndPolicy = new Policy(Policy.PRIORITY_CATEGORY_ALARMS,
+                Policy.PRIORITY_SENDERS_CONTACTS, Policy.PRIORITY_SENDERS_CONTACTS);
+        ZenModeConfig config = new ZenModeConfig();
+        config.applyNotificationPolicy(dndPolicy);
+        when(mNm.getZenModeConfig()).thenReturn(config);
+
+        ZenMode mode = mBackend.getMode(ZenMode.MANUAL_DND_MODE_ID);
+
+        assertThat(mode).isEqualTo(
+                ZenMode.manualDndMode(
+                        new AutomaticZenRule.Builder(
+                                mContext.getString(R.string.zen_mode_settings_title), Uri.EMPTY)
+                                .setType(AutomaticZenRule.TYPE_OTHER)
+                                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                                .setZenPolicy(notificationPolicyToZenPolicy(dndPolicy))
+                                .setManualInvocationAllowed(true)
+                                .build(), false));
+    }
+
+    @Test
+    public void getMode_zenRule_returnsMode() {
+        when(mNm.getAutomaticZenRule(eq(ZEN_RULE_ID))).thenReturn(ZEN_RULE);
+        when(mNm.getZenModeConfig()).thenReturn(
+                configWithRule(new ZenModeConfig(), ZEN_RULE_ID, ZEN_RULE, false));
+
+        ZenMode mode = mBackend.getMode(ZEN_RULE_ID);
+
+        assertThat(mode).isEqualTo(newZenMode(ZEN_RULE_ID, ZEN_RULE, false));
+    }
+
+    @Test
+    public void getMode_missingRule_returnsNull() {
+        when(mNm.getAutomaticZenRule(any())).thenReturn(null);
+
+        ZenMode mode = mBackend.getMode(ZEN_RULE_ID);
+
+        assertThat(mode).isNull();
+        verify(mNm).getAutomaticZenRule(eq(ZEN_RULE_ID));
+    }
+
+    @Test
+    public void getMode_manualDnd_returnsCorrectActiveState() {
+        // Set up a base config with an active rule to make sure we're looking at the correct info
+        ZenModeConfig configWithActiveRule = configWithRule(new ZenModeConfig(), ZEN_RULE_ID,
+                ZEN_RULE, true);
+
+        // Equivalent to disallowAllSounds()
+        Policy dndPolicy = new Policy(0, 0, 0);
+        configWithActiveRule.applyNotificationPolicy(dndPolicy);
+        when(mNm.getZenModeConfig()).thenReturn(configWithActiveRule);
+
+        ZenMode mode = mBackend.getMode(ZenMode.MANUAL_DND_MODE_ID);
+
+        // By default, manual rule is inactive
+        assertThat(mode).isNotNull();
+        assertThat(mode.isActive()).isFalse();
+
+        // Now the returned config will represent the manual rule being active
+        when(mNm.getZenModeConfig()).thenReturn(configWithManualRule(configWithActiveRule, true));
+        ZenMode activeMode = mBackend.getMode(ZenMode.MANUAL_DND_MODE_ID);
+        assertThat(activeMode).isNotNull();
+        assertThat(activeMode.isActive()).isTrue();
+    }
+
+    @Test
+    public void getMode_zenRule_returnsCorrectActiveState() {
+        // Set up a base config that has an active manual rule and "rule2", to make sure we're
+        // looking at the correct rule's info.
+        ZenModeConfig configWithActiveRules = configWithRule(
+                configWithManualRule(new ZenModeConfig(), true),  // active manual rule
+                "rule2", ZEN_RULE, true);  // active rule 2
+
+        when(mNm.getAutomaticZenRule(eq(ZEN_RULE_ID))).thenReturn(ZEN_RULE);
+        when(mNm.getZenModeConfig()).thenReturn(
+                configWithRule(configWithActiveRules, ZEN_RULE_ID, ZEN_RULE, false));
+
+        // Round 1: the current config should indicate that the rule is not active
+        ZenMode mode = mBackend.getMode(ZEN_RULE_ID);
+        assertThat(mode).isNotNull();
+        assertThat(mode.isActive()).isFalse();
+
+        when(mNm.getZenModeConfig()).thenReturn(
+                configWithRule(configWithActiveRules, ZEN_RULE_ID, ZEN_RULE, true));
+        ZenMode activeMode = mBackend.getMode(ZEN_RULE_ID);
+        assertThat(activeMode).isNotNull();
+        assertThat(activeMode.isActive()).isTrue();
+    }
+
+    @Test
+    public void updateMode_manualDnd_setsDeviceEffects() throws Exception {
+        ZenMode manualDnd = ZenMode.manualDndMode(
+                new AutomaticZenRule.Builder("DND", Uri.EMPTY)
+                        .setZenPolicy(new ZenPolicy())
+                        .setDeviceEffects(new ZenDeviceEffects.Builder()
+                                .setShouldDimWallpaper(true)
+                                .build())
+                        .build(), false);
+
+        mBackend.updateMode(manualDnd);
+
+        verify(mNm).setManualZenRuleDeviceEffects(new ZenDeviceEffects.Builder()
+                .setShouldDimWallpaper(true)
+                .build());
+    }
+
+    @Test
+    public void updateMode_manualDnd_setsNotificationPolicy() {
+        ZenMode manualDnd = ZenMode.manualDndMode(
+                new AutomaticZenRule.Builder("DND", Uri.EMPTY)
+                        .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
+                        .build(), false);
+
+        mBackend.updateMode(manualDnd);
+
+        verify(mNm).setNotificationPolicy(eq(new ZenModeConfig().toNotificationPolicy(
+                new ZenPolicy.Builder().allowAllSounds().build())), eq(true));
+    }
+
+    @Test
+    public void updateMode_zenRule_updatesRule() {
+        ZenMode ruleMode = newZenMode("rule", ZEN_RULE, false);
+
+        mBackend.updateMode(ruleMode);
+
+        verify(mNm).updateAutomaticZenRule(eq("rule"), eq(ZEN_RULE), eq(true));
+    }
+
+    @Test
+    public void activateMode_manualDnd_setsZenModeImportant() {
+        mBackend.activateMode(ZenMode.manualDndMode(MANUAL_DND_RULE, false), null);
+
+        verify(mNm).setZenMode(eq(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS), eq(null),
+                any(), eq(true));
+    }
+
+    @Test
+    public void activateMode_manualDndWithDuration_setsZenModeImportantWithCondition() {
+        mBackend.activateMode(ZenMode.manualDndMode(MANUAL_DND_RULE, false),
+                Duration.ofMinutes(30));
+
+        verify(mNm).setZenMode(eq(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS),
+                eq(ZenModeConfig.toTimeCondition(mContext, 30, 0, true).id),
+                any(),
+                eq(true));
+    }
+
+    @Test
+    public void activateMode_zenRule_setsRuleStateActive() {
+        mBackend.activateMode(newZenMode(ZEN_RULE_ID, ZEN_RULE, false), null);
+
+        verify(mNm).setAutomaticZenRuleState(eq(ZEN_RULE_ID),
+                eq(new Condition(ZEN_RULE.getConditionId(), "", Condition.STATE_TRUE,
+                        Condition.SOURCE_USER_ACTION)));
+    }
+
+    @Test
+    public void activateMode_zenRuleWithDuration_fails() {
+        assertThrows(IllegalArgumentException.class,
+                () -> mBackend.activateMode(newZenMode(ZEN_RULE_ID, ZEN_RULE, false),
+                        Duration.ofMinutes(30)));
+    }
+
+    @Test
+    public void deactivateMode_manualDnd_setsZenModeOff() {
+        mBackend.deactivateMode(ZenMode.manualDndMode(MANUAL_DND_RULE, true));
+
+        verify(mNm).setZenMode(eq(ZEN_MODE_OFF), eq(null), any(), eq(true));
+    }
+
+    @Test
+    public void deactivateMode_zenRule_setsRuleStateInactive() {
+        mBackend.deactivateMode(newZenMode(ZEN_RULE_ID, ZEN_RULE, false));
+
+        verify(mNm).setAutomaticZenRuleState(eq(ZEN_RULE_ID),
+                eq(new Condition(ZEN_RULE.getConditionId(), "", Condition.STATE_FALSE,
+                        Condition.SOURCE_USER_ACTION)));
+    }
+
+    @Test
+    public void removeMode_zenRule_deletesRule() {
+        mBackend.removeMode(newZenMode(ZEN_RULE_ID, ZEN_RULE, false));
+
+        verify(mNm).removeAutomaticZenRule(ZEN_RULE_ID, true);
+    }
+
+    @Test
+    public void removeMode_manualDnd_fails() {
+        assertThrows(IllegalArgumentException.class,
+                () -> mBackend.removeMode(ZenMode.manualDndMode(MANUAL_DND_RULE, false)));
+    }
+}
diff --git a/packages/SystemUI/TEST_MAPPING b/packages/SystemUI/TEST_MAPPING
index deab818..16dd4e5 100644
--- a/packages/SystemUI/TEST_MAPPING
+++ b/packages/SystemUI/TEST_MAPPING
@@ -81,7 +81,7 @@
       ]
     }
   ],
-  
+
   "postsubmit": [
     {
       // Permission indicators
@@ -93,7 +93,7 @@
       ]
     }
   ],
-  
+
   // v2/sysui/suite/test-mapping-sysui-screenshot-test
   "sysui-screenshot-test": [
     {
@@ -131,7 +131,7 @@
       ]
     }
   ],
-  
+
   // v2/sysui/suite/test-mapping-sysui-screenshot-test-staged
   "sysui-screenshot-test-staged": [
     {
@@ -156,5 +156,13 @@
         }
       ]
     }
+  ],
+  "sysui-robo-test": [
+    {
+      "name": "SystemUIGoogleRoboRNGTests"
+    },
+    {
+      "name": "SystemUIGoogleRobo2RNGTests"
+    }
   ]
 }
diff --git a/packages/SystemUI/aconfig/accessibility.aconfig b/packages/SystemUI/aconfig/accessibility.aconfig
index f63a896..0861454 100644
--- a/packages/SystemUI/aconfig/accessibility.aconfig
+++ b/packages/SystemUI/aconfig/accessibility.aconfig
@@ -73,13 +73,10 @@
 }
 
 flag {
-    name: "redesign_magnifier_window_size"
+    name: "redesign_magnification_window_size"
     namespace: "accessibility"
     description: "Redesigns the window magnification magnifier sizes provided in the settings panel."
     bug: "288056772"
-    metadata {
-      purpose: PURPOSE_BUGFIX
-    }
 }
 
 flag {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index 7062489..9ea435e 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -49,6 +49,7 @@
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.RowScope
 import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
@@ -129,7 +130,6 @@
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
 import androidx.compose.ui.unit.times
 import androidx.compose.ui.viewinterop.AndroidView
 import androidx.compose.ui.window.Popup
@@ -994,7 +994,8 @@
         shape = RoundedCornerShape(68.dp, 34.dp, 68.dp, 34.dp)
     ) {
         Column(
-            modifier = Modifier.fillMaxSize().padding(vertical = 38.dp, horizontal = 70.dp),
+            modifier = Modifier.fillMaxSize().padding(vertical = 32.dp, horizontal = 50.dp),
+            verticalArrangement = Arrangement.Center,
             horizontalAlignment = Alignment.CenterHorizontally,
         ) {
             Icon(
@@ -1005,41 +1006,43 @@
             Spacer(modifier = Modifier.size(6.dp))
             Text(
                 text = stringResource(R.string.cta_label_to_edit_widget),
-                style = MaterialTheme.typography.titleMedium,
-                textAlign = TextAlign.Center,
+                style = MaterialTheme.typography.titleLarge,
+                fontSize = nonScalableTextSize(22.dp),
+                lineHeight = nonScalableTextSize(28.dp),
             )
-            Spacer(modifier = Modifier.size(20.dp))
+            Spacer(modifier = Modifier.size(16.dp))
             Row(
-                modifier = Modifier.fillMaxWidth(),
-                horizontalArrangement = Arrangement.Center,
+                modifier = Modifier.fillMaxWidth().height(56.dp),
+                horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally),
             ) {
                 OutlinedButton(
+                    modifier = Modifier.fillMaxHeight(),
                     colors =
                         ButtonDefaults.buttonColors(
                             contentColor = colors.onPrimary,
                         ),
                     border = BorderStroke(width = 1.0.dp, color = colors.primaryContainer),
-                    contentPadding = Dimensions.ButtonPadding,
+                    contentPadding = PaddingValues(26.dp, 8.dp),
                     onClick = viewModel::onDismissCtaTile,
                 ) {
                     Text(
                         text = stringResource(R.string.cta_tile_button_to_dismiss),
-                        fontSize = 12.sp,
+                        fontSize = nonScalableTextSize(14.dp),
                     )
                 }
-                Spacer(modifier = Modifier.size(14.dp))
                 Button(
+                    modifier = Modifier.fillMaxHeight(),
                     colors =
                         ButtonDefaults.buttonColors(
                             containerColor = colors.primaryContainer,
                             contentColor = colors.onPrimaryContainer,
                         ),
-                    contentPadding = Dimensions.ButtonPadding,
+                    contentPadding = PaddingValues(26.dp, 8.dp),
                     onClick = viewModel::onOpenWidgetEditor
                 ) {
                     Text(
                         text = stringResource(R.string.cta_tile_button_to_open_widget_editor),
-                        fontSize = 12.sp,
+                        fontSize = nonScalableTextSize(14.dp),
                     )
                 }
             }
@@ -1352,6 +1355,13 @@
 }
 
 /**
+ * Text size converted from dp value to the equivalent sp value using the current screen density,
+ * ensuring it does not scale with the font size setting.
+ */
+@Composable
+private fun nonScalableTextSize(sizeInDp: Dp) = with(LocalDensity.current) { sizeInDp.toSp() }
+
+/**
  * Returns the `contentPadding` of the grid. Use the vertical padding to push the grid content area
  * below the toolbar and let the grid take the max size. This ensures the item can be dragged
  * outside the grid over the toolbar, without part of it getting clipped by the container.
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt
index 899b256..db98bc8f 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt
@@ -96,6 +96,12 @@
                     shadeMode = ShadeMode.Dual,
                     modifier = Modifier.fillMaxWidth(),
                 )
+
+                // Communicates the bottom position of the drawable area within the shade to NSSL.
+                NotificationStackCutoffGuideline(
+                    stackScrollView = stackScrollView.get(),
+                    viewModel = notificationsPlaceholderViewModel,
+                )
             }
         }
     }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt
index 4914aea..c066ae5 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt
@@ -154,6 +154,7 @@
             viewModel = viewModel.tileGridViewModel,
             modifier =
                 Modifier.fillMaxWidth().heightIn(max = QuickSettingsShade.Dimensions.GridMaxHeight),
+            viewModel.editModeViewModel::startEditing,
         )
         Button(
             onClick = { viewModel.editModeViewModel.startEditing() },
@@ -168,7 +169,7 @@
     object Dimensions {
         val Padding = 16.dp
         val BrightnessSliderHeight = 64.dp
-        val GridMaxHeight = 400.dp
+        val GridMaxHeight = 800.dp
     }
 
     object Transitions {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
index dbf6cd3..e433d32 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
@@ -50,30 +50,22 @@
     from(Scenes.Gone, to = Scenes.NotificationsShade, key = OpenBottomShade) {
         goneToNotificationsShadeTransition(Edge.Bottom)
     }
-    from(Scenes.Gone, to = Scenes.Shade) { goneToShadeTransition() }
-    from(
-        Scenes.Gone,
-        to = Scenes.Shade,
-        key = ToSplitShade,
-    ) {
-        goneToSplitShadeTransition()
+    from(Scenes.Gone, to = Scenes.QuickSettingsShade) {
+        goneToQuickSettingsShadeTransition(Edge.Top)
     }
-    from(
-        Scenes.Gone,
-        to = Scenes.Shade,
-        key = SlightlyFasterShadeCollapse,
-    ) {
+    from(Scenes.Gone, to = Scenes.QuickSettingsShade, key = OpenBottomShade) {
+        goneToQuickSettingsShadeTransition(Edge.Bottom)
+    }
+    from(Scenes.Gone, to = Scenes.Shade) { goneToShadeTransition() }
+    from(Scenes.Gone, to = Scenes.Shade, key = ToSplitShade) { goneToSplitShadeTransition() }
+    from(Scenes.Gone, to = Scenes.Shade, key = SlightlyFasterShadeCollapse) {
         goneToShadeTransition(durationScale = 0.9)
     }
     from(Scenes.Gone, to = Scenes.QuickSettings) { goneToQuickSettingsTransition() }
-    from(
-        Scenes.Gone,
-        to = Scenes.QuickSettings,
-        key = SlightlyFasterShadeCollapse,
-    ) {
+    from(Scenes.Gone, to = Scenes.QuickSettings, key = SlightlyFasterShadeCollapse) {
         goneToQuickSettingsTransition(durationScale = 0.9)
     }
-    from(Scenes.Gone, to = Scenes.QuickSettingsShade) { goneToQuickSettingsShadeTransition() }
+
     from(Scenes.Lockscreen, to = Scenes.Bouncer) { lockscreenToBouncerTransition() }
     from(Scenes.Lockscreen, to = Scenes.Communal) { lockscreenToCommunalTransition() }
     from(Scenes.Lockscreen, to = Scenes.NotificationsShade) {
@@ -83,18 +75,10 @@
         lockscreenToQuickSettingsShadeTransition()
     }
     from(Scenes.Lockscreen, to = Scenes.Shade) { lockscreenToShadeTransition() }
-    from(
-        Scenes.Lockscreen,
-        to = Scenes.Shade,
-        key = ToSplitShade,
-    ) {
+    from(Scenes.Lockscreen, to = Scenes.Shade, key = ToSplitShade) {
         lockscreenToSplitShadeTransition()
     }
-    from(
-        Scenes.Lockscreen,
-        to = Scenes.Shade,
-        key = SlightlyFasterShadeCollapse,
-    ) {
+    from(Scenes.Lockscreen, to = Scenes.Shade, key = SlightlyFasterShadeCollapse) {
         lockscreenToShadeTransition(durationScale = 0.9)
     }
     from(Scenes.Lockscreen, to = Scenes.QuickSettings) { lockscreenToQuickSettingsTransition() }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToQuickSettingsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToQuickSettingsShadeTransition.kt
index 225ca4e..8a03e29 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToQuickSettingsShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToQuickSettingsShadeTransition.kt
@@ -16,10 +16,12 @@
 
 package com.android.systemui.scene.ui.composable.transitions
 
+import com.android.compose.animation.scene.Edge
 import com.android.compose.animation.scene.TransitionBuilder
 
 fun TransitionBuilder.goneToQuickSettingsShadeTransition(
+    edge: Edge = Edge.Top,
     durationScale: Double = 1.0,
 ) {
-    toQuickSettingsShadeTransition(durationScale)
+    toQuickSettingsShadeTransition(edge, durationScale)
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToQuickSettingsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToQuickSettingsShadeTransition.kt
index ce24f5e..19aa3a7 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToQuickSettingsShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToQuickSettingsShadeTransition.kt
@@ -16,10 +16,11 @@
 
 package com.android.systemui.scene.ui.composable.transitions
 
+import com.android.compose.animation.scene.Edge
 import com.android.compose.animation.scene.TransitionBuilder
 
 fun TransitionBuilder.lockscreenToQuickSettingsShadeTransition(
     durationScale: Double = 1.0,
 ) {
-    toQuickSettingsShadeTransition(durationScale)
+    toQuickSettingsShadeTransition(Edge.Top, durationScale)
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt
index ec2f14f..9d13647 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt
@@ -19,17 +19,15 @@
 import androidx.compose.animation.core.Spring
 import androidx.compose.animation.core.spring
 import androidx.compose.animation.core.tween
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.ui.unit.IntSize
 import com.android.compose.animation.scene.Edge
 import com.android.compose.animation.scene.TransitionBuilder
 import com.android.compose.animation.scene.UserActionDistance
-import com.android.compose.animation.scene.UserActionDistanceScope
 import com.android.systemui.shade.ui.composable.OverlayShade
 import com.android.systemui.shade.ui.composable.Shade
 import kotlin.time.Duration.Companion.milliseconds
 
 fun TransitionBuilder.toQuickSettingsShadeTransition(
+    edge: Edge = Edge.Top,
     durationScale: Double = 1.0,
 ) {
     spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt())
@@ -38,17 +36,9 @@
             stiffness = Spring.StiffnessMediumLow,
             visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold,
         )
-    distance =
-        object : UserActionDistance {
-            override fun UserActionDistanceScope.absoluteDistance(
-                fromSceneSize: IntSize,
-                orientation: Orientation,
-            ): Float {
-                return fromSceneSize.height.toFloat() * 2 / 3f
-            }
-        }
+    distance = UserActionDistance { fromSceneSize, _ -> fromSceneSize.height.toFloat() * 2 / 3f }
 
-    translate(OverlayShade.Elements.Panel, Edge.Top)
+    translate(OverlayShade.Elements.Panel, edge)
 
     fractionRange(end = .5f) { fade(OverlayShade.Elements.Scrim) }
 }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt
index c2dd803..ea740a8 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt
@@ -48,8 +48,15 @@
     }
 
     return when (transitionState) {
-        is TransitionState.Idle ->
-            animate(layoutState, target, transitionKey, isInitiatedByUserInput = false)
+        is TransitionState.Idle -> {
+            animate(
+                layoutState,
+                target,
+                transitionKey,
+                isInitiatedByUserInput = false,
+                replacedTransition = null,
+            )
+        }
         is TransitionState.Transition -> {
             val isInitiatedByUserInput = transitionState.isInitiatedByUserInput
 
@@ -79,6 +86,7 @@
                         isInitiatedByUserInput,
                         initialProgress = progress,
                         initialVelocity = transitionState.progressVelocity,
+                        replacedTransition = transitionState,
                     )
                 }
             } else if (transitionState.fromScene == target) {
@@ -101,6 +109,7 @@
                         initialProgress = progress,
                         initialVelocity = transitionState.progressVelocity,
                         reversed = true,
+                        replacedTransition = transitionState,
                     )
                 }
             } else {
@@ -137,6 +146,7 @@
                     isInitiatedByUserInput,
                     fromScene = animateFrom,
                     chain = chain,
+                    replacedTransition = null,
                 )
             }
         }
@@ -148,6 +158,7 @@
     targetScene: SceneKey,
     transitionKey: TransitionKey?,
     isInitiatedByUserInput: Boolean,
+    replacedTransition: TransitionState.Transition?,
     initialProgress: Float = 0f,
     initialVelocity: Float = 0f,
     reversed: Boolean = false,
@@ -164,6 +175,7 @@
                 currentScene = targetScene,
                 isInitiatedByUserInput = isInitiatedByUserInput,
                 isUserInputOngoing = false,
+                replacedTransition = replacedTransition,
             )
         } else {
             OneOffTransition(
@@ -173,6 +185,7 @@
                 currentScene = targetScene,
                 isInitiatedByUserInput = isInitiatedByUserInput,
                 isUserInputOngoing = false,
+                replacedTransition = replacedTransition,
             )
         }
 
@@ -214,7 +227,8 @@
     override val currentScene: SceneKey,
     override val isInitiatedByUserInput: Boolean,
     override val isUserInputOngoing: Boolean,
-) : TransitionState.Transition(fromScene, toScene) {
+    replacedTransition: TransitionState.Transition?,
+) : TransitionState.Transition(fromScene, toScene, replacedTransition) {
     /**
      * The animatable used to animate this transition.
      *
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
index da968ac..e8fdfc8 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
@@ -37,7 +37,7 @@
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.launch
 
-interface DraggableHandler {
+internal interface DraggableHandler {
     /**
      * Start a drag in the given [startedPosition], with the given [overSlop] and number of
      * [pointersDown].
@@ -51,7 +51,7 @@
  * The [DragController] provides control over the transition between two scenes through the [onDrag]
  * and [onStop] methods.
  */
-interface DragController {
+internal interface DragController {
     /** Drag the current scene by [delta] pixels. */
     fun onDrag(delta: Float)
 
@@ -537,6 +537,7 @@
         orientation = orientation,
         isUpOrLeft = isUpOrLeft,
         requiresFullDistanceSwipe = result.requiresFullDistanceSwipe,
+        replacedTransition = null,
     )
 }
 
@@ -553,6 +554,7 @@
             isUpOrLeft = old.isUpOrLeft,
             lastDistance = old.lastDistance,
             requiresFullDistanceSwipe = old.requiresFullDistanceSwipe,
+            replacedTransition = old,
         )
         .apply {
             _currentScene = old._currentScene
@@ -571,9 +573,10 @@
     override val orientation: Orientation,
     override val isUpOrLeft: Boolean,
     val requiresFullDistanceSwipe: Boolean,
+    replacedTransition: SwipeTransition?,
     var lastDistance: Float = DistanceUnspecified,
 ) :
-    TransitionState.Transition(_fromScene.key, _toScene.key),
+    TransitionState.Transition(_fromScene.key, _toScene.key, replacedTransition),
     TransitionState.HasOverscrollProperties {
     var _currentScene by mutableStateOf(_fromScene)
     override val currentScene: SceneKey
@@ -910,7 +913,6 @@
     private val topOrLeftBehavior: NestedScrollBehavior,
     private val bottomOrRightBehavior: NestedScrollBehavior,
     private val isExternalOverscrollGesture: () -> Boolean,
-    private val pointersInfo: () -> PointersInfo,
 ) {
     private val layoutState = layoutImpl.state
     private val draggableHandler = layoutImpl.draggableHandler(orientation)
@@ -922,36 +924,34 @@
         // moving on to the next scene.
         var canChangeScene = false
 
+        val actionUpOrLeft =
+            Swipe(
+                direction =
+                    when (orientation) {
+                        Orientation.Horizontal -> SwipeDirection.Left
+                        Orientation.Vertical -> SwipeDirection.Up
+                    },
+                pointerCount = 1,
+            )
+
+        val actionDownOrRight =
+            Swipe(
+                direction =
+                    when (orientation) {
+                        Orientation.Horizontal -> SwipeDirection.Right
+                        Orientation.Vertical -> SwipeDirection.Down
+                    },
+                pointerCount = 1,
+            )
+
         fun hasNextScene(amount: Float): Boolean {
             val transitionState = layoutState.transitionState
             val scene = transitionState.currentScene
             val fromScene = layoutImpl.scene(scene)
             val nextScene =
                 when {
-                    amount < 0f -> {
-                        val actionUpOrLeft =
-                            Swipe(
-                                direction =
-                                    when (orientation) {
-                                        Orientation.Horizontal -> SwipeDirection.Left
-                                        Orientation.Vertical -> SwipeDirection.Up
-                                    },
-                                pointerCount = pointersInfo().pointersDown,
-                            )
-                        fromScene.userActions[actionUpOrLeft]
-                    }
-                    amount > 0f -> {
-                        val actionDownOrRight =
-                            Swipe(
-                                direction =
-                                    when (orientation) {
-                                        Orientation.Horizontal -> SwipeDirection.Right
-                                        Orientation.Vertical -> SwipeDirection.Down
-                                    },
-                                pointerCount = pointersInfo().pointersDown,
-                            )
-                        fromScene.userActions[actionDownOrRight]
-                    }
+                    amount < 0f -> fromScene.userActions[actionUpOrLeft]
+                    amount > 0f -> fromScene.userActions[actionDownOrRight]
                     else -> null
                 }
             if (nextScene != null) return true
@@ -1049,11 +1049,10 @@
             canContinueScroll = { true },
             canScrollOnFling = false,
             onStart = { offsetAvailable ->
-                val pointers = pointersInfo()
                 dragController =
                     draggableHandler.onDragStarted(
-                        pointersDown = pointers.pointersDown,
-                        startedPosition = pointers.startedPosition,
+                        pointersDown = 1,
+                        startedPosition = null,
                         overSlop = if (isIntercepting) 0f else offsetAvailable,
                     )
             },
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
index d4f1ad1..69124c1 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
@@ -495,6 +495,10 @@
     transition: TransitionState.Transition,
     previousTransition: TransitionState.Transition,
 ) {
+    if (transition.replacedTransition == previousTransition) {
+        return
+    }
+
     val sceneStates = element.sceneStates
     fun updatedSceneState(key: SceneKey): Element.SceneState? {
         return sceneStates[key]?.also { it.selfUpdateValuesBeforeInterruption() }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt
index dd795cd..1fa6b3f7 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt
@@ -18,21 +18,12 @@
 
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.input.nestedscroll.nestedScrollModifierNode
-import androidx.compose.ui.input.pointer.PointerEventPass
-import androidx.compose.ui.input.pointer.PointerInputChange
-import androidx.compose.ui.input.pointer.PointerInputScope
-import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode
 import androidx.compose.ui.node.DelegatableNode
 import androidx.compose.ui.node.DelegatingNode
 import androidx.compose.ui.node.ModifierNodeElement
 import androidx.compose.ui.platform.InspectorInfo
-import androidx.compose.ui.util.fastMap
-import androidx.compose.ui.util.fastReduce
 import com.android.compose.nestedscroll.PriorityNestedScrollConnection
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.isActive
 
 /**
  * Defines the behavior of the [SceneTransitionLayout] when a scrollable component is scrolled.
@@ -130,11 +121,6 @@
     }
 }
 
-internal data class PointersInfo(
-    val pointersDown: Int,
-    val startedPosition: Offset,
-)
-
 private class NestedScrollToSceneNode(
     layoutImpl: SceneTransitionLayoutImpl,
     orientation: Orientation,
@@ -149,49 +135,23 @@
             topOrLeftBehavior = topOrLeftBehavior,
             bottomOrRightBehavior = bottomOrRightBehavior,
             isExternalOverscrollGesture = isExternalOverscrollGesture,
-            pointersInfo = pointerInfo()
         )
 
-    private var lastPointers: List<PointerInputChange>? = null
-
-    private fun pointerInfo(): () -> PointersInfo = {
-        val pointers =
-            requireNotNull(lastPointers) { "NestedScroll API was called before PointerInput API" }
-        PointersInfo(
-            pointersDown = pointers.size,
-            startedPosition = pointers.fastMap { it.position }.fastReduce { a, b -> (a + b) / 2f },
-        )
-    }
-
-    private val pointerInputHandler: suspend PointerInputScope.() -> Unit = {
-        coroutineScope {
-            awaitPointerEventScope {
-                // Await this scope to guarantee that the PointerInput API receives touch events
-                // before the NestedScroll API.
-                delegate(nestedScrollNode)
-
-                try {
-                    while (isActive) {
-                        // During the initial phase, we receive the event after our ancestors.
-                        lastPointers = awaitPointerEvent(PointerEventPass.Initial).changes
-                    }
-                } finally {
-                    // Clean up the nested scroll connection
-                    priorityNestedScrollConnection.reset()
-                    undelegate(nestedScrollNode)
-                }
-            }
-        }
-    }
-
-    private val pointerInputNode = delegate(SuspendingPointerInputModifierNode(pointerInputHandler))
-
     private var nestedScrollNode: DelegatableNode =
         nestedScrollModifierNode(
             connection = priorityNestedScrollConnection,
             dispatcher = null,
         )
 
+    override fun onAttach() {
+        delegate(nestedScrollNode)
+    }
+
+    override fun onDetach() {
+        // Make sure we reset the scroll connection when this modifier is removed from composition
+        priorityNestedScrollConnection.reset()
+    }
+
     fun update(
         layoutImpl: SceneTransitionLayoutImpl,
         orientation: Orientation,
@@ -201,7 +161,7 @@
     ) {
         // Clean up the old nested scroll connection
         priorityNestedScrollConnection.reset()
-        pointerInputNode.resetPointerInputHandler()
+        undelegate(nestedScrollNode)
 
         // Create a new nested scroll connection
         priorityNestedScrollConnection =
@@ -211,13 +171,13 @@
                 topOrLeftBehavior = topOrLeftBehavior,
                 bottomOrRightBehavior = bottomOrRightBehavior,
                 isExternalOverscrollGesture = isExternalOverscrollGesture,
-                pointersInfo = pointerInfo(),
             )
         nestedScrollNode =
             nestedScrollModifierNode(
                 connection = priorityNestedScrollConnection,
                 dispatcher = null,
             )
+        delegate(nestedScrollNode)
     }
 }
 
@@ -227,7 +187,6 @@
     topOrLeftBehavior: NestedScrollBehavior,
     bottomOrRightBehavior: NestedScrollBehavior,
     isExternalOverscrollGesture: () -> Boolean,
-    pointersInfo: () -> PointersInfo,
 ) =
     NestedScrollHandlerImpl(
             layoutImpl = layoutImpl,
@@ -235,6 +194,5 @@
             topOrLeftBehavior = topOrLeftBehavior,
             bottomOrRightBehavior = bottomOrRightBehavior,
             isExternalOverscrollGesture = isExternalOverscrollGesture,
-            pointersInfo = pointersInfo,
         )
         .connection
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
index a8df6f4..5b4fbf0 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
@@ -224,6 +224,9 @@
 
         /** The scene this transition is going to. Can't be the same as fromScene */
         val toScene: SceneKey,
+
+        /** The transition that `this` transition is replacing, if any. */
+        internal val replacedTransition: Transition? = null,
     ) : TransitionState {
         /**
          * The key of this transition. This should usually be null, but it can be specified to use a
@@ -279,6 +282,11 @@
 
         init {
             check(fromScene != toScene)
+            check(
+                replacedTransition == null ||
+                    (replacedTransition.fromScene == fromScene &&
+                        replacedTransition.toScene == toScene)
+            )
         }
 
         /**
@@ -321,6 +329,10 @@
                 return 0f
             }
 
+            if (replacedTransition != null) {
+                return replacedTransition.interruptionProgress(layoutImpl)
+            }
+
             fun create(): Animatable<Float, AnimationVector1D> {
                 val animatable = Animatable(1f, visibilityThreshold = ProgressVisibilityThreshold)
                 layoutImpl.coroutineScope.launch {
@@ -521,6 +533,10 @@
                     check(transitionStates.size == 1)
                     check(transitionStates[0] is TransitionState.Idle)
                     transitionStates = listOf(transition)
+                } else if (currentState == transition.replacedTransition) {
+                    // Replace the transition.
+                    transitionStates =
+                        transitionStates.subList(0, transitionStates.lastIndex) + transition
                 } else {
                     // Append the new transition.
                     transitionStates = transitionStates + transition
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
index c738ad3..65b388f 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
@@ -113,8 +113,7 @@
                     orientation = draggableHandler.orientation,
                     topOrLeftBehavior = nestedScrollBehavior,
                     bottomOrRightBehavior = nestedScrollBehavior,
-                    isExternalOverscrollGesture = { isExternalOverscrollGesture },
-                    pointersInfo = { PointersInfo(pointersDown = 1, startedPosition = Offset.Zero) }
+                    isExternalOverscrollGesture = { isExternalOverscrollGesture }
                 )
                 .connection
 
@@ -1233,4 +1232,17 @@
         advanceUntilIdle()
         assertIdle(SceneB)
     }
+
+    @Test
+    fun interceptingTransitionReplacesCurrentTransition() = runGestureTest {
+        val controller = onDragStarted(overSlop = up(fractionOfScreen = 0.5f))
+        val transition = assertThat(layoutState.transitionState).isTransition()
+        controller.onDragStopped(velocity = 0f)
+
+        // Intercept the transition.
+        onDragStartedImmediately()
+        val newTransition = assertThat(layoutState.transitionState).isTransition()
+        assertThat(newTransition).isNotSameInstanceAs(transition)
+        assertThat(newTransition.replacedTransition).isSameInstanceAs(transition)
+    }
 }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
index 7c20a97..fcdf76e 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
@@ -839,80 +839,6 @@
     }
 
     @Test
-    fun elementTransitionDuringNestedScrollWith2Pointers() {
-        // The draggable touch slop, i.e. the min px distance a touch pointer must move before it is
-        // detected as a drag event.
-        var touchSlop = 0f
-        val translateY = 10.dp
-        val layoutWidth = 200.dp
-        val layoutHeight = 400.dp
-
-        val state =
-            rule.runOnUiThread {
-                MutableSceneTransitionLayoutState(
-                    initialScene = SceneA,
-                    transitions =
-                        transitions {
-                            from(SceneA, to = SceneB) {
-                                translate(TestElements.Foo, y = translateY)
-                            }
-                        },
-                )
-                    as MutableSceneTransitionLayoutStateImpl
-            }
-
-        rule.setContent {
-            touchSlop = LocalViewConfiguration.current.touchSlop
-            SceneTransitionLayout(
-                state = state,
-                modifier = Modifier.size(layoutWidth, layoutHeight)
-            ) {
-                scene(
-                    SceneA,
-                    userActions = mapOf(Swipe(SwipeDirection.Down, pointerCount = 2) to SceneB)
-                ) {
-                    Box(
-                        Modifier
-                            // Unconsumed scroll gesture will be intercepted by STL
-                            .verticalNestedScrollToScene()
-                            // A scrollable that does not consume the scroll gesture
-                            .scrollable(
-                                rememberScrollableState(consumeScrollDelta = { 0f }),
-                                Orientation.Vertical
-                            )
-                            .fillMaxSize()
-                    ) {
-                        Spacer(Modifier.element(TestElements.Foo).fillMaxSize())
-                    }
-                }
-                scene(SceneB) { Spacer(Modifier.fillMaxSize()) }
-            }
-        }
-
-        assertThat(state.transitionState).isIdle()
-        val fooElement = rule.onNodeWithTag(TestElements.Foo.testTag)
-        fooElement.assertTopPositionInRootIsEqualTo(0.dp)
-
-        // Swipe down with 2 pointers by half of verticalSwipeDistance.
-        rule.onRoot().performTouchInput {
-            val middleTop = Offset((layoutWidth / 2).toPx(), 0f)
-            repeat(2) { i -> down(pointerId = i, middleTop) }
-            repeat(2) { i ->
-                // Scroll 50%
-                moveBy(
-                    pointerId = i,
-                    delta = Offset(0f, touchSlop + layoutHeight.toPx() * 0.5f),
-                    delayMillis = 1_000,
-                )
-            }
-        }
-
-        val transition = assertThat(state.transitionState).isTransition()
-        assertThat(transition).hasProgress(0.5f)
-        fooElement.assertTopPositionInRootIsEqualTo(translateY * 0.5f)
-    }
-
-    @Test
     fun elementTransitionWithDistanceDuringOverscroll() {
         val layoutWidth = 200.dp
         val layoutHeight = 400.dp
@@ -2088,4 +2014,42 @@
         rule.onNode(isElement(TestElements.Foo, SceneA)).assertPositionInRootIsEqualTo(20.dp, 30.dp)
         rule.onNode(isElement(TestElements.Foo, SceneB)).assertIsNotDisplayed()
     }
+
+    @Test
+    fun replacedTransitionDoesNotTriggerInterruption() = runTest {
+        val state = rule.runOnIdle { MutableSceneTransitionLayoutStateImpl(SceneA) }
+
+        @Composable
+        fun SceneScope.Foo(modifier: Modifier = Modifier) {
+            Box(modifier.element(TestElements.Foo).size(10.dp))
+        }
+
+        rule.setContent {
+            SceneTransitionLayout(state) {
+                scene(SceneA) { Foo() }
+                scene(SceneB) { Foo(Modifier.offset(40.dp, 60.dp)) }
+            }
+        }
+
+        // Start A => B at 50%.
+        val aToB1 =
+            transition(from = SceneA, to = SceneB, progress = { 0.5f }, onFinish = neverFinish())
+        rule.runOnUiThread { state.startTransition(aToB1) }
+        rule.onNode(isElement(TestElements.Foo, SceneA)).assertIsNotDisplayed()
+        rule.onNode(isElement(TestElements.Foo, SceneB)).assertPositionInRootIsEqualTo(20.dp, 30.dp)
+
+        // Replace A => B by another A => B at 100%. Even with interruption progress at 100%, Foo
+        // should be at (40dp, 60dp) given that aToB1 was replaced by aToB2.
+        val aToB2 =
+            transition(
+                from = SceneA,
+                to = SceneB,
+                progress = { 1f },
+                interruptionProgress = { 1f },
+                replacedTransition = aToB1,
+            )
+        rule.runOnUiThread { state.startTransition(aToB2) }
+        rule.onNode(isElement(TestElements.Foo, SceneA)).assertIsNotDisplayed()
+        rule.onNode(isElement(TestElements.Foo, SceneB)).assertPositionInRootIsEqualTo(40.dp, 60.dp)
+    }
 }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt
index 09d1a82..3552d3d 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt
@@ -131,10 +131,6 @@
         assertThat(state.currentTransitions)
             .comparingElementsUsing(FromToCurrentTriple)
             .containsExactly(
-                // Initial transition A to B. This transition will never be consumed by anyone given
-                // that it has the same (from, to) pair as the next transition.
-                Triple(SceneA, SceneB, SceneB),
-
                 // Initial transition reversed, B back to A.
                 Triple(SceneA, SceneB, SceneA),
 
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/Transition.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/Transition.kt
index 322b035..65f4f9e 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/Transition.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/Transition.kt
@@ -37,8 +37,11 @@
     bouncingScene: SceneKey? = null,
     orientation: Orientation = Orientation.Horizontal,
     onFinish: ((TransitionState.Transition) -> Job)? = null,
+    replacedTransition: TransitionState.Transition? = null,
 ): TransitionState.Transition {
-    return object : TransitionState.Transition(from, to), TransitionState.HasOverscrollProperties {
+    return object :
+        TransitionState.Transition(from, to, replacedTransition),
+        TransitionState.HasOverscrollProperties {
         override val currentScene: SceneKey
             get() = current()
 
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepository.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepository.kt
index e39d7ed..9e857deb 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepository.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepository.kt
@@ -21,16 +21,16 @@
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.withContext
 
 /** Provides access to state related to notification settings. */
 class NotificationSettingsRepository(
-    scope: CoroutineScope,
+    private val scope: CoroutineScope,
     private val backgroundDispatcher: CoroutineDispatcher,
     private val secureSettingsRepository: SecureSettingsRepository,
 ) {
@@ -41,16 +41,15 @@
             .distinctUntilChanged()
 
     /** The current state of the notification setting. */
-    val isShowNotificationsOnLockScreenEnabled: StateFlow<Boolean> =
+    suspend fun isShowNotificationsOnLockScreenEnabled(): StateFlow<Boolean> =
         secureSettingsRepository
             .intSetting(
                 name = Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS,
             )
             .map { it == 1 }
+            .flowOn(backgroundDispatcher)
             .stateIn(
                 scope = scope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = false,
             )
 
     suspend fun setShowNotificationsOnLockscreenEnabled(enabled: Boolean) {
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractor.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractor.kt
index 04e8090..b4105bd 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractor.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractor.kt
@@ -26,8 +26,8 @@
     val isNotificationHistoryEnabled = repository.isNotificationHistoryEnabled
 
     /** Should notifications be visible on the lockscreen? */
-    val isShowNotificationsOnLockScreenEnabled: StateFlow<Boolean> =
-        repository.isShowNotificationsOnLockScreenEnabled
+    suspend fun isShowNotificationsOnLockScreenEnabled(): StateFlow<Boolean> =
+        repository.isShowNotificationsOnLockScreenEnabled()
 
     suspend fun setShowNotificationsOnLockscreenEnabled(enabled: Boolean) {
         repository.setShowNotificationsOnLockscreenEnabled(enabled)
@@ -35,7 +35,7 @@
 
     /** Toggles the setting to show or hide notifications on the lock screen. */
     suspend fun toggleShowNotificationsOnLockscreenEnabled() {
-        val current = repository.isShowNotificationsOnLockScreenEnabled.value
+        val current = repository.isShowNotificationsOnLockScreenEnabled().value
         repository.setShowNotificationsOnLockscreenEnabled(!current)
     }
 }
diff --git a/packages/SystemUI/lint-baseline.xml b/packages/SystemUI/lint-baseline.xml
index 4def93f..2fd7f1b 100644
--- a/packages/SystemUI/lint-baseline.xml
+++ b/packages/SystemUI/lint-baseline.xml
@@ -27088,17 +27088,6 @@
 
     <issue
         id="UselessParent"
-        message="This `FrameLayout` layout or its `LinearLayout` parent is unnecessary"
-        errorLine1="    &lt;FrameLayout"
-        errorLine2="     ~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/res/layout/media_output_list_item.xml"
-            line="24"
-            column="6"/>
-    </issue>
-
-    <issue
-        id="UselessParent"
         message="This `LinearLayout` layout or its `FrameLayout` parent is possibly unnecessary; transfer the `background` attribute to the other view"
         errorLine1="    &lt;LinearLayout"
         errorLine2="     ~~~~~~~~~~~~">
@@ -30587,17 +30576,6 @@
     <issue
         id="ContentDescription"
         message="Missing `contentDescription` attribute on image"
-        errorLine1="            &lt;ImageView"
-        errorLine2="             ~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/res/layout/media_output_list_item.xml"
-            line="54"
-            column="14"/>
-    </issue>
-
-    <issue
-        id="ContentDescription"
-        message="Missing `contentDescription` attribute on image"
         errorLine1="    &lt;ImageView"
         errorLine2="     ~~~~~~~~~">
         <location
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
index a5acf72..ccddc9c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
@@ -38,7 +38,7 @@
 import com.android.systemui.flags.Flags
 import com.android.systemui.flags.fakeFeatureFlagsClassic
 import com.android.systemui.kosmos.testScope
-import com.android.systemui.scene.domain.interactor.sceneContainerStartable
+import com.android.systemui.scene.domain.startable.sceneContainerStartable
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.shared.model.fakeSceneDataSource
 import com.android.systemui.testKosmos
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
index cf14547..fbe2c2e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
@@ -451,6 +451,24 @@
             }
         }
 
+    @Test
+    fun transitionFromDozingToGlanceableHub_forcesCommunal() =
+        with(kosmos) {
+            testScope.runTest {
+                val scene by collectLastValue(communalSceneInteractor.currentScene)
+                communalSceneInteractor.changeScene(CommunalScenes.Blank)
+                assertThat(scene).isEqualTo(CommunalScenes.Blank)
+
+                fakeKeyguardTransitionRepository.sendTransitionSteps(
+                    from = KeyguardState.DOZING,
+                    to = KeyguardState.GLANCEABLE_HUB,
+                    testScope = this
+                )
+
+                assertThat(scene).isEqualTo(CommunalScenes.Communal)
+            }
+        }
+
     private fun TestScope.updateDocked(docked: Boolean) =
         with(kosmos) {
             runCurrent()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt
index 4587ea6..c5ba02d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt
@@ -528,6 +528,30 @@
     }
 
     @Test
+    fun userChange_isFingerprintEnrolledAndEnabledUpdated() =
+        testScope.runTest {
+            createBiometricSettingsRepository()
+            whenever(authController.isFingerprintEnrolled(ANOTHER_USER_ID)).thenReturn(false)
+            whenever(authController.isFingerprintEnrolled(PRIMARY_USER_ID)).thenReturn(true)
+
+            verify(biometricManager)
+                .registerEnabledOnKeyguardCallback(biometricManagerCallback.capture())
+            val isFingerprintEnrolledAndEnabled =
+                collectLastValue(underTest.isFingerprintEnrolledAndEnabled)
+            biometricManagerCallback.value.onChanged(true, ANOTHER_USER_ID)
+            runCurrent()
+            userRepository.setSelectedUserInfo(ANOTHER_USER)
+            runCurrent()
+            assertThat(isFingerprintEnrolledAndEnabled()).isFalse()
+
+            biometricManagerCallback.value.onChanged(true, PRIMARY_USER_ID)
+            runCurrent()
+            userRepository.setSelectedUserInfo(PRIMARY_USER)
+            runCurrent()
+            assertThat(isFingerprintEnrolledAndEnabled()).isTrue()
+        }
+
+    @Test
     fun userChange_biometricEnabledChange_handlesRaceCondition() =
         testScope.runTest {
             createBiometricSettingsRepository()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt
index 5f0f24d..2d12150 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
@@ -61,7 +62,8 @@
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
-        testScope = TestScope()
+        val testDispatcher = StandardTestDispatcher()
+        testScope = TestScope(testDispatcher)
         userRepository = FakeUserRepository()
         userRepository.setUserInfos(users)
         val logger =
@@ -69,7 +71,13 @@
                 LogBuffer("TestBuffer", 1, mock(LogcatEchoTracker::class.java), false)
             )
         underTest =
-            TrustRepositoryImpl(testScope.backgroundScope, userRepository, trustManager, logger)
+            TrustRepositoryImpl(
+                testScope.backgroundScope,
+                testDispatcher,
+                userRepository,
+                trustManager,
+                logger,
+            )
     }
 
     fun TestScope.init() {
@@ -275,4 +283,11 @@
             userRepository.setSelectedUserInfo(users[1])
             assertThat(trustUsuallyManaged).isFalse()
         }
+
+    @Test
+    fun reportKeyguardShowingChanged() =
+        testScope.runTest {
+            underTest.reportKeyguardShowingChanged()
+            verify(trustManager).reportKeyguardShowingChanged()
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
index 612f2e7..0792a50 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
@@ -34,12 +34,14 @@
 
 import android.os.PowerManager
 import android.platform.test.annotations.EnableFlags
+import android.service.dream.dreamManager
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository
+import com.android.systemui.communal.domain.interactor.setCommunalAvailable
 import com.android.systemui.communal.shared.model.CommunalScenes
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
@@ -64,8 +66,10 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mockito.anyBoolean
 import org.mockito.Mockito.reset
 import org.mockito.Mockito.spy
+import org.mockito.kotlin.whenever
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
@@ -120,6 +124,66 @@
 
     @Test
     @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToLockscreen_onWakeup_canDream_glanceableHubAvailable() =
+        testScope.runTest {
+            whenever(kosmos.dreamManager.canStartDreaming(anyBoolean())).thenReturn(true)
+            kosmos.setCommunalAvailable(true)
+            runCurrent()
+
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // If dreaming is possible and communal is available, then we should transition to
+            // GLANCEABLE_HUB when waking up.
+            assertThat(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.DOZING,
+                    to = KeyguardState.GLANCEABLE_HUB,
+                )
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToLockscreen_onWakeup_canNotDream_glanceableHubAvailable() =
+        testScope.runTest {
+            whenever(kosmos.dreamManager.canStartDreaming(anyBoolean())).thenReturn(false)
+            kosmos.setCommunalAvailable(true)
+            runCurrent()
+
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // If dreaming is NOT possible but communal is available, then we should transition to
+            // LOCKSCREEN when waking up.
+            assertThat(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.DOZING,
+                    to = KeyguardState.LOCKSCREEN,
+                )
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToLockscreen_onWakeup_canNDream_glanceableHubNotAvailable() =
+        testScope.runTest {
+            whenever(kosmos.dreamManager.canStartDreaming(anyBoolean())).thenReturn(true)
+            kosmos.setCommunalAvailable(false)
+            runCurrent()
+
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // If dreaming is possible but communal is NOT available, then we should transition to
+            // LOCKSCREEN when waking up.
+            assertThat(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.DOZING,
+                    to = KeyguardState.LOCKSCREEN,
+                )
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
     fun testTransitionToGlanceableHub_onWakeup_ifIdleOnCommunal_noOccludingActivity() =
         testScope.runTest {
             kosmos.fakeCommunalSceneRepository.setTransitionState(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt
index 3777e40..6f74ed3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt
@@ -16,98 +16,108 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.flag.junit.FlagsParameterization
 import androidx.test.filters.SmallTest
-import com.android.systemui.Flags as AConfigFlags
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR
+import com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
-import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
+import com.android.systemui.common.ui.domain.interactor.configurationInteractor
+import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository
+import com.android.systemui.communal.domain.interactor.communalSceneInteractor
+import com.android.systemui.communal.shared.model.CommunalScenes
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.doze.util.BurnInHelperWrapper
-import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.domain.interactor.BurnInInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
+import com.android.systemui.keyguard.domain.interactor.keyguardBottomAreaInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.BurnInModel
+import com.android.systemui.keyguard.shared.model.StatusBarState
 import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.testKosmos
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.Mock
-import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
-@RunWith(AndroidJUnit4::class)
-class KeyguardIndicationAreaViewModelTest : SysuiTestCase() {
+@RunWith(ParameterizedAndroidJunit4::class)
+class KeyguardIndicationAreaViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
 
-    @Mock private lateinit var burnInHelperWrapper: BurnInHelperWrapper
-    @Mock private lateinit var shortcutsCombinedViewModel: KeyguardQuickAffordancesCombinedViewModel
-
-    @Mock private lateinit var burnInInteractor: BurnInInteractor
-    private val burnInFlow = MutableStateFlow(BurnInModel())
-
-    private lateinit var bottomAreaInteractor: KeyguardBottomAreaInteractor
+    private val bottomAreaInteractor = kosmos.keyguardBottomAreaInteractor
     private lateinit var underTest: KeyguardIndicationAreaViewModel
-    private lateinit var repository: FakeKeyguardRepository
+    private val keyguardRepository = kosmos.fakeKeyguardRepository
+    private val communalSceneRepository = kosmos.fakeCommunalSceneRepository
 
     private val startButtonFlow =
-        MutableStateFlow<KeyguardQuickAffordanceViewModel>(
+        MutableStateFlow(
             KeyguardQuickAffordanceViewModel(
                 slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId()
             )
         )
     private val endButtonFlow =
-        MutableStateFlow<KeyguardQuickAffordanceViewModel>(
+        MutableStateFlow(
             KeyguardQuickAffordanceViewModel(
                 slotId = KeyguardQuickAffordancePosition.BOTTOM_END.toSlotId()
             )
         )
-    private val alphaFlow = MutableStateFlow<Float>(1f)
+    private val alphaFlow = MutableStateFlow(1f)
+
+    init {
+        mSetFlagsRule.setFlagsParameterization(flags)
+    }
 
     @Before
     fun setUp() {
-        MockitoAnnotations.initMocks(this)
-
-        mSetFlagsRule.disableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
-        mSetFlagsRule.disableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
-
-        whenever(burnInHelperWrapper.burnInOffset(anyInt(), any()))
-            .thenReturn(RETURNED_BURN_IN_OFFSET)
-        whenever(burnInInteractor.burnIn(anyInt(), anyInt())).thenReturn(burnInFlow)
-
-        val withDeps = KeyguardInteractorFactory.create()
-        val keyguardInteractor = withDeps.keyguardInteractor
-        repository = withDeps.repository
-
-        val bottomAreaViewModel: KeyguardBottomAreaViewModel = mock()
-        whenever(bottomAreaViewModel.startButton).thenReturn(startButtonFlow)
-        whenever(bottomAreaViewModel.endButton).thenReturn(endButtonFlow)
-        whenever(bottomAreaViewModel.alpha).thenReturn(alphaFlow)
-        bottomAreaInteractor = KeyguardBottomAreaInteractor(repository = repository)
+        val bottomAreaViewModel =
+            mock<KeyguardBottomAreaViewModel> {
+                on { startButton } doReturn startButtonFlow
+                on { endButton } doReturn endButtonFlow
+                on { alpha } doReturn alphaFlow
+            }
+        val burnInInteractor =
+            mock<BurnInInteractor> {
+                on { burnIn(anyInt(), anyInt()) } doReturn flowOf(BurnInModel())
+            }
+        val burnInHelperWrapper =
+            mock<BurnInHelperWrapper> {
+                on { burnInOffset(anyInt(), any()) } doReturn RETURNED_BURN_IN_OFFSET
+            }
+        val shortcutsCombinedViewModel =
+            mock<KeyguardQuickAffordancesCombinedViewModel> {
+                on { startButton } doReturn startButtonFlow
+                on { endButton } doReturn endButtonFlow
+            }
         underTest =
             KeyguardIndicationAreaViewModel(
-                keyguardInteractor = keyguardInteractor,
+                keyguardInteractor = kosmos.keyguardInteractor,
                 bottomAreaInteractor = bottomAreaInteractor,
                 keyguardBottomAreaViewModel = bottomAreaViewModel,
                 burnInHelperWrapper = burnInHelperWrapper,
                 burnInInteractor = burnInInteractor,
                 shortcutsCombinedViewModel = shortcutsCombinedViewModel,
-                configurationInteractor = ConfigurationInteractor(FakeConfigurationRepository()),
+                configurationInteractor = kosmos.configurationInteractor,
                 keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor,
-                backgroundCoroutineContext = kosmos.testDispatcher,
+                backgroundDispatcher = kosmos.testDispatcher,
+                communalSceneInteractor = kosmos.communalSceneInteractor,
                 mainDispatcher = kosmos.testDispatcher
             )
     }
@@ -115,77 +125,120 @@
     @Test
     fun alpha() =
         testScope.runTest {
-            val value = collectLastValue(underTest.alpha)
+            val alpha by collectLastValue(underTest.alpha)
 
-            assertThat(value()).isEqualTo(1f)
+            assertThat(alpha).isEqualTo(1f)
             alphaFlow.value = 0.1f
-            assertThat(value()).isEqualTo(0.1f)
+            assertThat(alpha).isEqualTo(0.1f)
             alphaFlow.value = 0.5f
-            assertThat(value()).isEqualTo(0.5f)
+            assertThat(alpha).isEqualTo(0.5f)
             alphaFlow.value = 0.2f
-            assertThat(value()).isEqualTo(0.2f)
+            assertThat(alpha).isEqualTo(0.2f)
             alphaFlow.value = 0f
-            assertThat(value()).isEqualTo(0f)
+            assertThat(alpha).isEqualTo(0f)
         }
 
     @Test
+    @DisableFlags(FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
     fun isIndicationAreaPadded() =
         testScope.runTest {
-            repository.setKeyguardShowing(true)
-            val value = collectLastValue(underTest.isIndicationAreaPadded)
+            keyguardRepository.setKeyguardShowing(true)
+            val isIndicationAreaPadded by collectLastValue(underTest.isIndicationAreaPadded)
 
-            assertThat(value()).isFalse()
+            assertThat(isIndicationAreaPadded).isFalse()
             startButtonFlow.value = startButtonFlow.value.copy(isVisible = true)
-            assertThat(value()).isTrue()
+            assertThat(isIndicationAreaPadded).isTrue()
             endButtonFlow.value = endButtonFlow.value.copy(isVisible = true)
-            assertThat(value()).isTrue()
+            assertThat(isIndicationAreaPadded).isTrue()
             startButtonFlow.value = startButtonFlow.value.copy(isVisible = false)
-            assertThat(value()).isTrue()
+            assertThat(isIndicationAreaPadded).isTrue()
             endButtonFlow.value = endButtonFlow.value.copy(isVisible = false)
-            assertThat(value()).isFalse()
+            assertThat(isIndicationAreaPadded).isFalse()
         }
 
     @Test
+    @DisableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
     fun indicationAreaTranslationX() =
         testScope.runTest {
-            val value = collectLastValue(underTest.indicationAreaTranslationX)
+            val translationX by collectLastValue(underTest.indicationAreaTranslationX)
 
-            assertThat(value()).isEqualTo(0f)
+            assertThat(translationX).isEqualTo(0f)
             bottomAreaInteractor.setClockPosition(100, 100)
-            assertThat(value()).isEqualTo(100f)
+            assertThat(translationX).isEqualTo(100f)
             bottomAreaInteractor.setClockPosition(200, 100)
-            assertThat(value()).isEqualTo(200f)
+            assertThat(translationX).isEqualTo(200f)
             bottomAreaInteractor.setClockPosition(200, 200)
-            assertThat(value()).isEqualTo(200f)
+            assertThat(translationX).isEqualTo(200f)
             bottomAreaInteractor.setClockPosition(300, 100)
-            assertThat(value()).isEqualTo(300f)
+            assertThat(translationX).isEqualTo(300f)
         }
 
     @Test
+    @DisableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     fun indicationAreaTranslationY() =
         testScope.runTest {
-            val value =
+            val translationY by
                 collectLastValue(underTest.indicationAreaTranslationY(DEFAULT_BURN_IN_OFFSET))
 
             // Negative 0 - apparently there's a difference in floating point arithmetic - FML
-            assertThat(value()).isEqualTo(-0f)
+            assertThat(translationY).isEqualTo(-0f)
             val expected1 = setDozeAmountAndCalculateExpectedTranslationY(0.1f)
-            assertThat(value()).isEqualTo(expected1)
+            assertThat(translationY).isEqualTo(expected1)
             val expected2 = setDozeAmountAndCalculateExpectedTranslationY(0.2f)
-            assertThat(value()).isEqualTo(expected2)
+            assertThat(translationY).isEqualTo(expected2)
             val expected3 = setDozeAmountAndCalculateExpectedTranslationY(0.5f)
-            assertThat(value()).isEqualTo(expected3)
+            assertThat(translationY).isEqualTo(expected3)
             val expected4 = setDozeAmountAndCalculateExpectedTranslationY(1f)
-            assertThat(value()).isEqualTo(expected4)
+            assertThat(translationY).isEqualTo(expected4)
+        }
+
+    @Test
+    fun visibilityWhenCommunalNotShowing() =
+        testScope.runTest {
+            keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
+            val visible by collectLastValue(underTest.visible)
+
+            assertThat(visible).isTrue()
+            keyguardRepository.setStatusBarState(StatusBarState.SHADE)
+            assertThat(visible).isFalse()
+        }
+
+    @Test
+    fun visibilityWhenCommunalShowing() =
+        testScope.runTest {
+            keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
+            communalSceneRepository.setTransitionState(
+                flowOf(ObservableTransitionState.Idle(CommunalScenes.Communal))
+            )
+
+            val visible by collectLastValue(underTest.visible)
+
+            assertThat(visible).isTrue()
+            keyguardRepository.setStatusBarState(StatusBarState.SHADE)
+            assertThat(visible).isTrue()
+
+            communalSceneRepository.setTransitionState(
+                flowOf(ObservableTransitionState.Idle(CommunalScenes.Blank))
+            )
+            assertThat(visible).isFalse()
         }
 
     private fun setDozeAmountAndCalculateExpectedTranslationY(dozeAmount: Float): Float {
-        repository.setDozeAmount(dozeAmount)
+        keyguardRepository.setDozeAmount(dozeAmount)
         return dozeAmount * (RETURNED_BURN_IN_OFFSET - DEFAULT_BURN_IN_OFFSET)
     }
 
     companion object {
         private const val DEFAULT_BURN_IN_OFFSET = 5
         private const val RETURNED_BURN_IN_OFFSET = 3
+
+        @JvmStatic
+        @Parameters(name = "{0}")
+        fun getParams(): List<FlagsParameterization> {
+            return FlagsParameterization.allCombinationsOf(
+                FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR,
+                FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT,
+            )
+        }
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/PaginatedGridRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/PaginatedGridRepositoryTest.kt
new file mode 100644
index 0000000..14d6094
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/PaginatedGridRepositoryTest.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.data.repository
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testCase
+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.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class PaginatedGridRepositoryTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+
+    val underTest = kosmos.paginatedGridRepository
+
+    @Test
+    fun rows_followsConfig() =
+        with(kosmos) {
+            testScope.runTest {
+                val rows by collectLastValue(underTest.rows)
+
+                setRowsInConfig(3)
+                assertThat(rows).isEqualTo(3)
+
+                setRowsInConfig(6)
+                assertThat(rows).isEqualTo(6)
+            }
+        }
+
+    private fun setRowsInConfig(rows: Int) =
+        with(kosmos) {
+            testCase.context.orCreateTestableResources.addOverride(
+                R.integer.quick_settings_max_rows,
+                rows,
+            )
+            fakeConfigurationRepository.onConfigurationChange()
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayoutTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayoutTest.kt
new file mode 100644
index 0000000..914a095
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayoutTest.kt
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.ui.compose
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.qs.panels.data.repository.IconTilesRepository
+import com.android.systemui.qs.panels.data.repository.iconTilesRepository
+import com.android.systemui.qs.panels.ui.viewmodel.MockTileViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.fixedColumnsSizeViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.iconTilesViewModel
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class InfiniteGridLayoutTest : SysuiTestCase() {
+    private val kosmos =
+        testKosmos().apply {
+            iconTilesRepository =
+                object : IconTilesRepository {
+                    override fun isIconTile(spec: TileSpec): Boolean {
+                        return spec.spec.startsWith("small")
+                    }
+                }
+        }
+
+    private val underTest =
+        with(kosmos) {
+            InfiniteGridLayout(
+                iconTilesViewModel,
+                fixedColumnsSizeViewModel,
+            )
+        }
+
+    @Test
+    fun correctPagination_underOnePage_sameOrder() =
+        with(kosmos) {
+            testScope.runTest {
+                val rows = 3
+                val columns = 4
+
+                val tiles =
+                    listOf(
+                        largeTile(),
+                        smallTile(),
+                        smallTile(),
+                        largeTile(),
+                        largeTile(),
+                        smallTile()
+                    )
+
+                val pages = underTest.splitIntoPages(tiles, rows = rows, columns = columns)
+
+                assertThat(pages).hasSize(1)
+                assertThat(pages[0]).isEqualTo(tiles)
+            }
+        }
+
+    @Test
+    fun correctPagination_twoPages_sameOrder() =
+        with(kosmos) {
+            testScope.runTest {
+                val rows = 3
+                val columns = 4
+
+                val tiles =
+                    listOf(
+                        largeTile(),
+                        smallTile(),
+                        smallTile(),
+                        largeTile(),
+                        largeTile(),
+                        smallTile(),
+                        smallTile(),
+                        largeTile(),
+                        largeTile(),
+                        smallTile(),
+                        smallTile(),
+                        largeTile(),
+                    )
+                // --- Page 1 ---
+                // [L L] [S] [S]
+                // [L L] [L L]
+                // [S] [S] [L L]
+                // --- Page 2 ---
+                // [L L] [S] [S]
+                // [L L]
+
+                val pages = underTest.splitIntoPages(tiles, rows = rows, columns = columns)
+
+                assertThat(pages).hasSize(2)
+                assertThat(pages[0]).isEqualTo(tiles.take(8))
+                assertThat(pages[1]).isEqualTo(tiles.drop(8))
+            }
+        }
+
+    companion object {
+        fun largeTile() = MockTileViewModel(TileSpec.create("large"))
+
+        fun smallTile() = MockTileViewModel(TileSpec.create("small"))
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/PaginatableGridLayoutTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/PaginatableGridLayoutTest.kt
new file mode 100644
index 0000000..6df3f8d
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/PaginatableGridLayoutTest.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.ui.compose
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.qs.panels.shared.model.SizedTile
+import com.android.systemui.qs.panels.ui.viewmodel.MockTileViewModel
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class PaginatableGridLayoutTest : SysuiTestCase() {
+    @Test
+    fun correctRows_gapsAtEnd() {
+        val columns = 6
+
+        val sizedTiles =
+            listOf(
+                largeTile(),
+                extraLargeTile(),
+                largeTile(),
+                smallTile(),
+                largeTile(),
+            )
+
+        // [L L] [XL XL XL]
+        // [L L] [S] [L L]
+
+        val rows = PaginatableGridLayout.splitInRows(sizedTiles, columns)
+
+        assertThat(rows).hasSize(2)
+        assertThat(rows[0]).isEqualTo(sizedTiles.take(2))
+        assertThat(rows[1]).isEqualTo(sizedTiles.drop(2))
+    }
+
+    @Test
+    fun correctRows_fullLastRow_noEmptyRow() {
+        val columns = 6
+
+        val sizedTiles =
+            listOf(
+                largeTile(),
+                extraLargeTile(),
+                smallTile(),
+            )
+
+        // [L L] [XL XL XL] [S]
+
+        val rows = PaginatableGridLayout.splitInRows(sizedTiles, columns)
+
+        assertThat(rows).hasSize(1)
+        assertThat(rows[0]).isEqualTo(sizedTiles)
+    }
+
+    companion object {
+        fun extraLargeTile() = SizedTile(MockTileViewModel(TileSpec.create("XLarge")), 3)
+
+        fun largeTile() = SizedTile(MockTileViewModel(TileSpec.create("large")), 2)
+
+        fun smallTile() = SizedTile(MockTileViewModel(TileSpec.create("small")), 1)
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayoutTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayoutTest.kt
new file mode 100644
index 0000000..3354b4d4
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayoutTest.kt
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.ui.compose
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.qs.panels.data.repository.IconTilesRepository
+import com.android.systemui.qs.panels.data.repository.iconTilesRepository
+import com.android.systemui.qs.panels.ui.viewmodel.MockTileViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.iconTilesViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.partitionedGridViewModel
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class PartitionedGridLayoutTest : SysuiTestCase() {
+    private val kosmos =
+        testKosmos().apply {
+            iconTilesRepository =
+                object : IconTilesRepository {
+                    override fun isIconTile(spec: TileSpec): Boolean {
+                        return spec.spec.startsWith("small")
+                    }
+                }
+        }
+
+    private val underTest = with(kosmos) { PartitionedGridLayout(partitionedGridViewModel) }
+
+    @Test
+    fun correctPagination_underOnePage_partitioned_sameRelativeOrder() =
+        with(kosmos) {
+            testScope.runTest {
+                val rows = 3
+                val columns = 4
+
+                val tiles =
+                    listOf(
+                        largeTile(),
+                        smallTile(),
+                        smallTile(),
+                        largeTile(),
+                        largeTile(),
+                        smallTile()
+                    )
+                val (smallTiles, largeTiles) =
+                    tiles.partition { iconTilesViewModel.isIconTile(it.spec) }
+
+                // [L L] [L L]
+                // [L L]
+                // [S] [S] [S]
+
+                val pages = underTest.splitIntoPages(tiles, rows = rows, columns = columns)
+
+                Truth.assertThat(pages).hasSize(1)
+                Truth.assertThat(pages[0]).isEqualTo(largeTiles + smallTiles)
+            }
+        }
+
+    @Test
+    fun correctPagination_twoPages_partitioned_sameRelativeOrder() =
+        with(kosmos) {
+            testScope.runTest {
+                val rows = 3
+                val columns = 4
+
+                val tiles =
+                    listOf(
+                        largeTile(),
+                        smallTile(),
+                        smallTile(),
+                        largeTile(),
+                        smallTile(),
+                        smallTile(),
+                        largeTile(),
+                        smallTile(),
+                        smallTile(),
+                    )
+                // --- Page 1 ---
+                // [L L] [L L]
+                // [L L]
+                // [S] [S] [S] [S]
+                // --- Page 2 ---
+                // [S] [S]
+
+                val (smallTiles, largeTiles) =
+                    tiles.partition { iconTilesViewModel.isIconTile(it.spec) }
+
+                val pages = underTest.splitIntoPages(tiles, rows = rows, columns = columns)
+
+                val expectedPage0 = largeTiles + smallTiles.take(4)
+                val expectedPage1 = smallTiles.drop(4)
+
+                Truth.assertThat(pages).hasSize(2)
+                Truth.assertThat(pages[0]).isEqualTo(expectedPage0)
+                Truth.assertThat(pages[1]).isEqualTo(expectedPage1)
+            }
+        }
+
+    companion object {
+        fun largeTile() = MockTileViewModel(TileSpec.create("large"))
+
+        fun smallTile() = MockTileViewModel(TileSpec.create("small"))
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingDataInteractorTest.kt
new file mode 100644
index 0000000..2194c75
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingDataInteractorTest.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.irecording
+
+import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.recordissue.IssueRecordingState
+import com.android.systemui.settings.fakeUserFileManager
+import com.android.systemui.settings.userTracker
+import com.google.common.truth.Truth
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class IssueRecordingDataInteractorTest : SysuiTestCase() {
+
+    private val kosmos = Kosmos().also { it.testCase = this }
+    private val userTracker = kosmos.userTracker
+    private val userFileManager = kosmos.fakeUserFileManager
+    private val testUser = UserHandle.of(1)
+
+    lateinit var state: IssueRecordingState
+    private lateinit var underTest: IssueRecordingDataInteractor
+
+    @Before
+    fun setup() {
+        state = IssueRecordingState(userTracker, userFileManager)
+        underTest = IssueRecordingDataInteractor(state, kosmos.testScope.testScheduler)
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun emitsEvent_whenIsRecordingStatusChanges_correctly() {
+        kosmos.testScope.runTest {
+            val data by
+                collectLastValue(
+                    underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+                )
+            runCurrent()
+            Truth.assertThat(data?.isRecording).isFalse()
+
+            state.isRecording = true
+            runCurrent()
+            Truth.assertThat(data?.isRecording).isTrue()
+
+            state.isRecording = false
+            runCurrent()
+            Truth.assertThat(data?.isRecording).isFalse()
+        }
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingMapperTest.kt
new file mode 100644
index 0000000..2444229
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingMapperTest.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.irecording
+
+import android.content.res.mainResources
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.qsEventLogger
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig
+import com.android.systemui.recordissue.RecordIssueModule
+import com.android.systemui.res.R
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class IssueRecordingMapperTest : SysuiTestCase() {
+    private val kosmos = Kosmos().also { it.testCase = this }
+    private val uiConfig =
+        QSTileUIConfig.Resource(R.drawable.qs_record_issue_icon_off, R.string.qs_record_issue_label)
+    private val config =
+        QSTileConfig(
+            TileSpec.create(RecordIssueModule.TILE_SPEC),
+            uiConfig,
+            kosmos.qsEventLogger.getNewInstanceId()
+        )
+    private val resources = kosmos.mainResources
+    private val theme = resources.newTheme()
+
+    @Test
+    fun whenData_isRecording_useCorrectResources() {
+        val underTest = IssueRecordingMapper(resources, theme)
+        val tileState = underTest.map(config, IssueRecordingModel(true))
+        Truth.assertThat(tileState.activationState).isEqualTo(QSTileState.ActivationState.ACTIVE)
+    }
+
+    @Test
+    fun whenData_isNotRecording_useCorrectResources() {
+        val underTest = IssueRecordingMapper(resources, theme)
+        val tileState = underTest.map(config, IssueRecordingModel(false))
+        Truth.assertThat(tileState.activationState).isEqualTo(QSTileState.ActivationState.INACTIVE)
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingUserActionInteractorTest.kt
new file mode 100644
index 0000000..4e58069
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingUserActionInteractorTest.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.irecording
+
+import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.dialogTransitionAnimator
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.activityStarter
+import com.android.systemui.plugins.statusbar.statusBarStateController
+import com.android.systemui.qs.pipeline.domain.interactor.panelInteractor
+import com.android.systemui.qs.tiles.base.interactor.QSTileInput
+import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
+import com.android.systemui.recordissue.RecordIssueDialogDelegate
+import com.android.systemui.settings.UserContextProvider
+import com.android.systemui.settings.userTracker
+import com.android.systemui.statusbar.phone.KeyguardDismissUtil
+import com.android.systemui.statusbar.policy.keyguardStateController
+import com.google.common.truth.Truth
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class IssueRecordingUserActionInteractorTest : SysuiTestCase() {
+
+    val user = UserHandle(1)
+    val kosmos = Kosmos().also { it.testCase = this }
+
+    private lateinit var userContextProvider: UserContextProvider
+    private lateinit var underTest: IssueRecordingUserActionInteractor
+
+    private var hasCreatedDialogDelegate: Boolean = false
+
+    @Before
+    fun setup() {
+        hasCreatedDialogDelegate = false
+        with(kosmos) {
+            val factory =
+                object : RecordIssueDialogDelegate.Factory {
+                    override fun create(onStarted: Runnable): RecordIssueDialogDelegate {
+                        hasCreatedDialogDelegate = true
+
+                        // Inside some tests in presubmit, createDialog throws an error because
+                        // the test thread's looper hasn't been prepared, and Dialog.class
+                        // internally is creating a new handler. For testing, we only care that the
+                        // dialog is created, so using a mock is acceptable here.
+                        return mock(RecordIssueDialogDelegate::class.java)
+                    }
+                }
+
+            userContextProvider = userTracker
+            underTest =
+                IssueRecordingUserActionInteractor(
+                    testDispatcher,
+                    KeyguardDismissUtil(
+                        keyguardStateController,
+                        statusBarStateController,
+                        activityStarter
+                    ),
+                    keyguardStateController,
+                    dialogTransitionAnimator,
+                    panelInteractor,
+                    userTracker,
+                    factory
+                )
+        }
+    }
+
+    @Test
+    fun handleInput_showsPromptToStartRecording_whenNotRecordingAlready() {
+        kosmos.testScope.runTest {
+            underTest.handleInput(
+                QSTileInput(user, QSTileUserAction.Click(null), IssueRecordingModel(false))
+            )
+            Truth.assertThat(hasCreatedDialogDelegate).isTrue()
+        }
+    }
+
+    @Test
+    fun handleInput_attemptsToStopRecording_whenRecording() {
+        kosmos.testScope.runTest {
+            val input = QSTileInput(user, QSTileUserAction.Click(null), IssueRecordingModel(true))
+            try {
+                underTest.handleInput(input)
+            } catch (e: NullPointerException) {
+                // As of 06/07/2024, PendingIntent.startService is not easily mockable and throws
+                // an NPE inside IActivityManager. Catching that here and ignore it, then verify
+                // mock interactions were done correctly
+            }
+            Truth.assertThat(hasCreatedDialogDelegate).isFalse()
+        }
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
index 5b6fea5..d43d50a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
@@ -42,9 +42,9 @@
 import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter
 import com.android.systemui.res.R
 import com.android.systemui.scene.domain.interactor.sceneBackInteractor
-import com.android.systemui.scene.domain.interactor.sceneContainerStartable
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.domain.resolver.homeSceneFamilyResolver
+import com.android.systemui.scene.domain.startable.sceneContainerStartable
 import com.android.systemui.scene.shared.model.SceneFamilies
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.settings.brightness.ui.viewmodel.brightnessMirrorViewModel
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
index 4d5d22c..412505d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -58,9 +58,9 @@
 import com.android.systemui.qs.footerActionsController
 import com.android.systemui.qs.footerActionsViewModelFactory
 import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter
-import com.android.systemui.scene.domain.interactor.sceneContainerStartable
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.domain.resolver.homeSceneFamilyResolver
+import com.android.systemui.scene.domain.startable.sceneContainerStartable
 import com.android.systemui.scene.shared.model.SceneFamilies
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.shared.model.fakeSceneDataSource
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneBackInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneBackInteractorTest.kt
index e3108ad..1f3454d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneBackInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneBackInteractorTest.kt
@@ -28,6 +28,7 @@
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.scene.domain.startable.sceneContainerStartable
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
index ec7150b..5242fe3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
@@ -26,6 +26,7 @@
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor
 import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.scene.data.repository.Idle
@@ -450,4 +451,16 @@
             progress.value = 0.9f
             assertThat(transitionValue).isEqualTo(0f)
         }
+
+    @Test
+    fun changeScene_toGone_whenKeyguardDisabled_doesNotThrow() =
+        testScope.runTest {
+            val currentScene by collectLastValue(underTest.currentScene)
+            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+            kosmos.keyguardEnabledInteractor.notifyKeyguardEnabled(false)
+
+            underTest.changeScene(Scenes.Gone, "")
+
+            assertThat(currentScene).isEqualTo(Scenes.Gone)
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/KeyguardStateCallbackStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/KeyguardStateCallbackStartableTest.kt
new file mode 100644
index 0000000..695edaf
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/KeyguardStateCallbackStartableTest.kt
@@ -0,0 +1,248 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.scene.domain.startable
+
+import android.content.pm.UserInfo
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.internal.policy.IKeyguardStateCallback
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeTrustRepository
+import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.scene.data.repository.setSceneTransition
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository
+import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.atLeastOnce
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@EnableSceneContainer
+class KeyguardStateCallbackStartableTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+
+    private val underTest = kosmos.keyguardStateCallbackStartable
+
+    @Test
+    fun addCallback_hydratesAllWithCurrentState() =
+        testScope.runTest {
+            val testState = setUpTest()
+            val callback = mockCallback()
+
+            underTest.addCallback(callback)
+            runCurrent()
+
+            with(testState) {
+                val captor = argumentCaptor<Boolean>()
+                verify(callback, atLeastOnce()).onShowingStateChanged(captor.capture(), eq(userId))
+                assertThat(captor.lastValue).isEqualTo(isKeyguardShowing)
+                verify(callback, atLeastOnce()).onInputRestrictedStateChanged(captor.capture())
+                assertThat(captor.lastValue).isEqualTo(isInputRestricted)
+                verify(callback, atLeastOnce()).onSimSecureStateChanged(captor.capture())
+                assertThat(captor.lastValue).isEqualTo(isSimSecure)
+                verify(callback, atLeastOnce()).onTrustedChanged(captor.capture())
+                assertThat(captor.lastValue).isEqualTo(isTrusted)
+            }
+        }
+
+    @Test
+    fun hydrateKeyguardShowingState() =
+        testScope.runTest {
+            setUpTest(isKeyguardShowing = true)
+            val callback = mockCallback()
+            underTest.addCallback(callback)
+            runCurrent()
+            verify(callback, atLeastOnce()).onShowingStateChanged(eq(true), anyInt())
+
+            unlockDevice()
+            runCurrent()
+
+            verify(callback).onShowingStateChanged(eq(false), anyInt())
+        }
+
+    @Test
+    fun hydrateInputRestrictedState() =
+        testScope.runTest {
+            setUpTest(isKeyguardShowing = true)
+            val callback = mockCallback()
+            underTest.addCallback(callback)
+            runCurrent()
+            val captor = argumentCaptor<Boolean>()
+            verify(callback, atLeastOnce()).onInputRestrictedStateChanged(captor.capture())
+            assertThat(captor.lastValue).isTrue()
+
+            unlockDevice()
+            runCurrent()
+
+            verify(callback, atLeastOnce()).onInputRestrictedStateChanged(captor.capture())
+            assertThat(captor.lastValue).isFalse()
+        }
+
+    @Test
+    fun hydrateSimSecureState() =
+        testScope.runTest {
+            setUpTest(isSimSecure = false)
+            val callback = mockCallback()
+            underTest.addCallback(callback)
+            runCurrent()
+            val captor = argumentCaptor<Boolean>()
+            verify(callback, atLeastOnce()).onSimSecureStateChanged(captor.capture())
+            assertThat(captor.lastValue).isFalse()
+
+            kosmos.fakeMobileConnectionsRepository.isAnySimSecure.value = true
+            runCurrent()
+
+            verify(callback, atLeastOnce()).onSimSecureStateChanged(captor.capture())
+            assertThat(captor.lastValue).isTrue()
+        }
+
+    @Test
+    fun notifyWhenKeyguardShowingChanged() =
+        testScope.runTest {
+            setUpTest(isKeyguardShowing = true)
+            val callback = mockCallback()
+            underTest.addCallback(callback)
+            runCurrent()
+            assertThat(kosmos.fakeTrustRepository.keyguardShowingChangeEventCount).isEqualTo(1)
+
+            unlockDevice()
+            runCurrent()
+
+            assertThat(kosmos.fakeTrustRepository.keyguardShowingChangeEventCount).isEqualTo(2)
+        }
+
+    @Test
+    fun notifyWhenTrustChanged() =
+        testScope.runTest {
+            setUpTest(isTrusted = false)
+            val callback = mockCallback()
+            underTest.addCallback(callback)
+            runCurrent()
+            val captor = argumentCaptor<Boolean>()
+            verify(callback, atLeastOnce()).onTrustedChanged(captor.capture())
+            assertThat(captor.lastValue).isFalse()
+
+            kosmos.fakeTrustRepository.setCurrentUserTrusted(true)
+            runCurrent()
+
+            verify(callback, atLeastOnce()).onTrustedChanged(captor.capture())
+            assertThat(captor.lastValue).isTrue()
+        }
+
+    private suspend fun TestScope.setUpTest(
+        isKeyguardShowing: Boolean = true,
+        userId: Int = selectedUser.id,
+        isInputRestricted: Boolean = true,
+        isSimSecure: Boolean = false,
+        isTrusted: Boolean = false,
+    ): TestState {
+        val testState =
+            TestState(
+                isKeyguardShowing = isKeyguardShowing,
+                userId = userId,
+                isInputRestricted = isInputRestricted,
+                isSimSecure = isSimSecure,
+                isTrusted = isTrusted,
+            )
+
+        if (isKeyguardShowing) {
+            lockDevice()
+        } else {
+            unlockDevice()
+        }
+
+        kosmos.fakeUserRepository.setUserInfos(listOf(selectedUser))
+        kosmos.fakeUserRepository.setSelectedUserInfo(selectedUser)
+
+        if (isInputRestricted && !isKeyguardShowing) {
+            // TODO(b/348644111): add support for mNeedToReshowWhenReenabled
+        } else if (!isInputRestricted) {
+            assertWithMessage(
+                    "If isInputRestricted is false, isKeyguardShowing must also be false!"
+                )
+                .that(isKeyguardShowing)
+                .isFalse()
+        }
+
+        kosmos.fakeMobileConnectionsRepository.isAnySimSecure.value = isSimSecure
+
+        kosmos.fakeTrustRepository.setCurrentUserTrusted(isTrusted)
+
+        runCurrent()
+
+        underTest.start()
+
+        return testState
+    }
+
+    private fun lockDevice() {
+        kosmos.setSceneTransition(ObservableTransitionState.Idle(Scenes.Lockscreen))
+        kosmos.sceneInteractor.changeScene(Scenes.Lockscreen, "")
+    }
+
+    private fun unlockDevice() {
+        kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+            SuccessFingerprintAuthenticationStatus(0, true)
+        )
+        kosmos.setSceneTransition(ObservableTransitionState.Idle(Scenes.Gone))
+        kosmos.sceneInteractor.changeScene(Scenes.Gone, "")
+    }
+
+    private fun mockCallback(): IKeyguardStateCallback {
+        return mock()
+    }
+
+    private data class TestState(
+        val isKeyguardShowing: Boolean,
+        val userId: Int,
+        val isInputRestricted: Boolean,
+        val isSimSecure: Boolean,
+        val isTrusted: Boolean,
+    )
+
+    companion object {
+        private val selectedUser =
+            UserInfo(
+                /* id= */ 100,
+                /* name= */ "First user",
+                /* flags= */ 0,
+            )
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index e40c8ee..9edc3af 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -51,7 +51,6 @@
 import com.android.systemui.power.domain.interactor.powerInteractor
 import com.android.systemui.power.shared.model.WakeSleepReason
 import com.android.systemui.power.shared.model.WakefulnessState
-import com.android.systemui.scene.domain.interactor.sceneContainerStartable
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.shared.model.fakeSceneDataSource
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsSoundPolicyInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsSoundPolicyInteractorTest.kt
index 8e765f7..9ef42c3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsSoundPolicyInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsSoundPolicyInteractorTest.kt
@@ -21,13 +21,13 @@
 import android.provider.Settings.Global
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.settingslib.statusbar.notification.data.model.ZenMode
 import com.android.settingslib.statusbar.notification.data.repository.updateNotificationPolicy
 import com.android.settingslib.statusbar.notification.domain.interactor.NotificationsSoundPolicyInteractor
 import com.android.settingslib.volume.shared.model.AudioStream
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.policy.data.repository.zenModeRepository
 import com.android.systemui.testKosmos
 import com.google.common.truth.Expect
 import com.google.common.truth.Truth.assertThat
@@ -52,24 +52,20 @@
 
     @Before
     fun setup() {
-        with(kosmos) {
-            underTest = NotificationsSoundPolicyInteractor(notificationsSoundPolicyRepository)
-        }
+        with(kosmos) { underTest = NotificationsSoundPolicyInteractor(zenModeRepository) }
     }
 
     @Test
     fun onlyAlarmsCategory_areAlarmsAllowed_isTrue() {
         with(kosmos) {
             testScope.runTest {
-                notificationsSoundPolicyRepository.updateZenMode(ZenMode(Global.ZEN_MODE_OFF))
+                zenModeRepository.updateZenMode(Global.ZEN_MODE_OFF)
                 val expectedByCategory =
                     NotificationManager.Policy.ALL_PRIORITY_CATEGORIES.associateWith {
                         it == NotificationManager.Policy.PRIORITY_CATEGORY_ALARMS
                     }
                 expectedByCategory.forEach { entry ->
-                    notificationsSoundPolicyRepository.updateNotificationPolicy(
-                        priorityCategories = entry.key
-                    )
+                    zenModeRepository.updateNotificationPolicy(priorityCategories = entry.key)
 
                     val areAlarmsAllowed by collectLastValue(underTest.areAlarmsAllowed)
                     runCurrent()
@@ -84,15 +80,13 @@
     fun onlyMediaCategory_areAlarmsAllowed_isTrue() {
         with(kosmos) {
             testScope.runTest {
-                notificationsSoundPolicyRepository.updateZenMode(ZenMode(Global.ZEN_MODE_OFF))
+                zenModeRepository.updateZenMode(Global.ZEN_MODE_OFF)
                 val expectedByCategory =
                     NotificationManager.Policy.ALL_PRIORITY_CATEGORIES.associateWith {
                         it == NotificationManager.Policy.PRIORITY_CATEGORY_MEDIA
                     }
                 expectedByCategory.forEach { entry ->
-                    notificationsSoundPolicyRepository.updateNotificationPolicy(
-                        priorityCategories = entry.key
-                    )
+                    zenModeRepository.updateNotificationPolicy(priorityCategories = entry.key)
 
                     val isMediaAllowed by collectLastValue(underTest.isMediaAllowed)
                     runCurrent()
@@ -108,7 +102,7 @@
         with(kosmos) {
             testScope.runTest {
                 for (category in NotificationManager.Policy.ALL_PRIORITY_CATEGORIES) {
-                    notificationsSoundPolicyRepository.updateNotificationPolicy(
+                    zenModeRepository.updateNotificationPolicy(
                         priorityCategories = category,
                         state = NotificationManager.Policy.STATE_UNSET,
                     )
@@ -126,7 +120,7 @@
     fun allCategoriesAllowed_isRingerAllowed_isTrue() {
         with(kosmos) {
             testScope.runTest {
-                notificationsSoundPolicyRepository.updateNotificationPolicy(
+                zenModeRepository.updateNotificationPolicy(
                     priorityCategories =
                         NotificationManager.Policy.ALL_PRIORITY_CATEGORIES.reduce { acc, value ->
                             acc or value
@@ -146,7 +140,7 @@
     fun noCategoriesAndBlocked_isRingerAllowed_isFalse() {
         with(kosmos) {
             testScope.runTest {
-                notificationsSoundPolicyRepository.updateNotificationPolicy(
+                zenModeRepository.updateNotificationPolicy(
                     priorityCategories = 0,
                     state = NotificationManager.Policy.STATE_PRIORITY_CHANNELS_BLOCKED,
                 )
@@ -163,10 +157,8 @@
     fun zenModeNoInterruptions_allStreams_muted() {
         with(kosmos) {
             testScope.runTest {
-                notificationsSoundPolicyRepository.updateNotificationPolicy()
-                notificationsSoundPolicyRepository.updateZenMode(
-                    ZenMode(Global.ZEN_MODE_NO_INTERRUPTIONS)
-                )
+                zenModeRepository.updateNotificationPolicy()
+                zenModeRepository.updateZenMode(Global.ZEN_MODE_NO_INTERRUPTIONS)
 
                 for (stream in AudioStream.supportedStreamTypes) {
                     val isZenMuted by collectLastValue(underTest.isZenMuted(AudioStream(stream)))
@@ -182,8 +174,8 @@
     fun zenModeOff_allStreams_notMuted() {
         with(kosmos) {
             testScope.runTest {
-                notificationsSoundPolicyRepository.updateNotificationPolicy()
-                notificationsSoundPolicyRepository.updateZenMode(ZenMode(Global.ZEN_MODE_OFF))
+                zenModeRepository.updateNotificationPolicy()
+                zenModeRepository.updateZenMode(Global.ZEN_MODE_OFF)
 
                 for (stream in AudioStream.supportedStreamTypes) {
                     val isZenMuted by collectLastValue(underTest.isZenMuted(AudioStream(stream)))
@@ -205,8 +197,8 @@
                     AudioManager.STREAM_SYSTEM,
                 )
             testScope.runTest {
-                notificationsSoundPolicyRepository.updateNotificationPolicy()
-                notificationsSoundPolicyRepository.updateZenMode(ZenMode(Global.ZEN_MODE_ALARMS))
+                zenModeRepository.updateNotificationPolicy()
+                zenModeRepository.updateZenMode(Global.ZEN_MODE_ALARMS)
 
                 for (stream in AudioStream.supportedStreamTypes) {
                     val isZenMuted by collectLastValue(underTest.isZenMuted(AudioStream(stream)))
@@ -222,10 +214,8 @@
     fun alarms_allowed_notMuted() {
         with(kosmos) {
             testScope.runTest {
-                notificationsSoundPolicyRepository.updateZenMode(
-                    ZenMode(Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
-                )
-                notificationsSoundPolicyRepository.updateNotificationPolicy(
+                zenModeRepository.updateZenMode(Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
+                zenModeRepository.updateNotificationPolicy(
                     priorityCategories = NotificationManager.Policy.PRIORITY_CATEGORY_ALARMS
                 )
 
@@ -242,10 +232,8 @@
     fun media_allowed_notMuted() {
         with(kosmos) {
             testScope.runTest {
-                notificationsSoundPolicyRepository.updateZenMode(
-                    ZenMode(Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
-                )
-                notificationsSoundPolicyRepository.updateNotificationPolicy(
+                zenModeRepository.updateZenMode(Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
+                zenModeRepository.updateNotificationPolicy(
                     priorityCategories = NotificationManager.Policy.PRIORITY_CATEGORY_MEDIA
                 )
 
@@ -262,10 +250,8 @@
     fun ringer_allowed_notificationsNotMuted() {
         with(kosmos) {
             testScope.runTest {
-                notificationsSoundPolicyRepository.updateZenMode(
-                    ZenMode(Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
-                )
-                notificationsSoundPolicyRepository.updateNotificationPolicy(
+                zenModeRepository.updateZenMode(Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
+                zenModeRepository.updateNotificationPolicy(
                     priorityCategories =
                         NotificationManager.Policy.ALL_PRIORITY_CATEGORIES.reduce { acc, value ->
                             acc or value
@@ -288,10 +274,8 @@
     fun ringer_allowed_ringNotMuted() {
         with(kosmos) {
             testScope.runTest {
-                notificationsSoundPolicyRepository.updateZenMode(
-                    ZenMode(Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
-                )
-                notificationsSoundPolicyRepository.updateNotificationPolicy(
+                zenModeRepository.updateZenMode(Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
+                zenModeRepository.updateNotificationPolicy(
                     priorityCategories =
                         NotificationManager.Policy.ALL_PRIORITY_CATEGORIES.reduce { acc, value ->
                             acc or value
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelTest.kt
new file mode 100644
index 0000000..5e87f46
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelTest.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.statusbar.notification.row.ui.viewmodel
+
+import android.app.PendingIntent
+import android.platform.test.annotations.EnableFlags
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.notification.row.data.repository.fakeNotificationRowRepository
+import com.android.systemui.statusbar.notification.row.shared.IconModel
+import com.android.systemui.statusbar.notification.row.shared.RichOngoingNotificationFlag
+import com.android.systemui.statusbar.notification.row.shared.TimerContentModel
+import com.android.systemui.statusbar.notification.row.shared.TimerContentModel.TimerState.Paused
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import java.time.Duration
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+@EnableFlags(RichOngoingNotificationFlag.FLAG_NAME)
+class TimerViewModelTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val repository = kosmos.fakeNotificationRowRepository
+
+    private var contentModel: TimerContentModel?
+        get() = repository.richOngoingContentModel.value as? TimerContentModel
+        set(value) {
+            repository.richOngoingContentModel.value = value
+        }
+
+    private lateinit var underTest: TimerViewModel
+
+    @Before
+    fun setup() {
+        underTest = kosmos.getTimerViewModel(repository)
+    }
+
+    @Test
+    fun labelShowsTheTimerName() =
+        testScope.runTest {
+            val label by collectLastValue(underTest.label)
+            contentModel = pausedTimer(name = "Example Timer Name")
+            assertThat(label).isEqualTo("Example Timer Name")
+        }
+
+    @Test
+    fun pausedTimeRemainingFormatsWell() =
+        testScope.runTest {
+            val label by collectLastValue(underTest.pausedTime)
+            contentModel = pausedTimer(timeRemaining = Duration.ofMinutes(3))
+            assertThat(label).isEqualTo("3:00")
+            contentModel = pausedTimer(timeRemaining = Duration.ofSeconds(119))
+            assertThat(label).isEqualTo("1:59")
+            contentModel = pausedTimer(timeRemaining = Duration.ofSeconds(121))
+            assertThat(label).isEqualTo("2:01")
+            contentModel = pausedTimer(timeRemaining = Duration.ofHours(1))
+            assertThat(label).isEqualTo("1:00:00")
+            contentModel = pausedTimer(timeRemaining = Duration.ofHours(24))
+            assertThat(label).isEqualTo("24:00:00")
+        }
+
+    private fun pausedTimer(
+        icon: IconModel = mock(),
+        name: String = "example",
+        timeRemaining: Duration = Duration.ofMinutes(3),
+        resumeIntent: PendingIntent? = null,
+        resetIntent: PendingIntent? = null
+    ) =
+        TimerContentModel(
+            icon = icon,
+            name = name,
+            state =
+                Paused(
+                    timeRemaining = timeRemaining,
+                    resumeIntent = resumeIntent,
+                    resetIntent = resetIntent,
+                )
+        )
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
index 9fde116..40315a2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
@@ -23,6 +23,7 @@
 import android.platform.test.flag.junit.FlagsParameterization
 import android.provider.Settings
 import androidx.test.filters.SmallTest
+import com.android.settingslib.statusbar.notification.data.repository.updateNotificationPolicy
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.flags.Flags
@@ -56,7 +57,6 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.MockitoAnnotations
 import platform.test.runner.parameterized.ParameterizedAndroidJunit4
 import platform.test.runner.parameterized.Parameters
 
@@ -97,7 +97,9 @@
 
     @Before
     fun setUp() {
-        MockitoAnnotations.initMocks(this)
+        // "Why is this not lazily initialised above?" you may ask. There's a simple answer: likely
+        // due to some timing issue with how the flags are getting initialised for parameterization,
+        // some tests start failing when this isn't initialised this way. You can just leave it be.
         underTest = kosmos.notificationListViewModel
     }
 
@@ -146,36 +148,40 @@
             assertThat(important).isTrue()
         }
 
+    // NOTE: The empty shade view and the footer view should be mutually exclusive.
+
     @Test
-    fun shouldIncludeEmptyShadeView_trueWhenNoNotifs() =
+    fun shouldShowEmptyShadeView_trueWhenNoNotifs() =
         testScope.runTest {
-            val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
+            val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView)
+            val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView)
 
             // WHEN has no notifs
             activeNotificationListRepository.setActiveNotifs(count = 0)
             runCurrent()
 
             // THEN empty shade is visible
-            assertThat(shouldInclude).isTrue()
+            assertThat(shouldShowEmptyShadeView).isTrue()
+            assertThat(shouldIncludeFooterView?.value).isFalse()
         }
 
     @Test
-    fun shouldIncludeEmptyShadeView_falseWhenNotifs() =
+    fun shouldShowEmptyShadeView_falseWhenNotifs() =
         testScope.runTest {
-            val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
+            val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView)
 
             // WHEN has notifs
             activeNotificationListRepository.setActiveNotifs(count = 2)
             runCurrent()
 
             // THEN empty shade is not visible
-            assertThat(shouldInclude).isFalse()
+            assertThat(shouldShowEmptyShadeView).isFalse()
         }
 
     @Test
-    fun shouldIncludeEmptyShadeView_falseWhenQsExpandedDefault() =
+    fun shouldShowEmptyShadeView_falseWhenQsExpandedDefault() =
         testScope.runTest {
-            val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
+            val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
 
             // WHEN has no notifs
             activeNotificationListRepository.setActiveNotifs(count = 0)
@@ -184,13 +190,14 @@
             runCurrent()
 
             // THEN empty shade is not visible
-            assertThat(shouldInclude).isFalse()
+            assertThat(shouldShow).isFalse()
         }
 
     @Test
-    fun shouldIncludeEmptyShadeView_trueWhenQsExpandedInSplitShade() =
+    fun shouldShowEmptyShadeView_trueWhenQsExpandedInSplitShade() =
         testScope.runTest {
-            val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
+            val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView)
+            val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView)
 
             // WHEN has no notifs
             activeNotificationListRepository.setActiveNotifs(count = 0)
@@ -203,13 +210,15 @@
             runCurrent()
 
             // THEN empty shade is visible
-            assertThat(shouldInclude).isTrue()
+            assertThat(shouldShowEmptyShadeView).isTrue()
+            assertThat(shouldIncludeFooterView?.value).isFalse()
         }
 
     @Test
-    fun shouldIncludeEmptyShadeView_trueWhenLockedShade() =
+    fun shouldShowEmptyShadeView_trueWhenLockedShade() =
         testScope.runTest {
-            val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
+            val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView)
+            val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView)
 
             // WHEN has no notifs
             activeNotificationListRepository.setActiveNotifs(count = 0)
@@ -218,13 +227,14 @@
             runCurrent()
 
             // THEN empty shade is visible
-            assertThat(shouldInclude).isTrue()
+            assertThat(shouldShowEmptyShadeView).isTrue()
+            assertThat(shouldIncludeFooterView?.value).isFalse()
         }
 
     @Test
-    fun shouldIncludeEmptyShadeView_falseWhenKeyguard() =
+    fun shouldShowEmptyShadeView_falseWhenKeyguard() =
         testScope.runTest {
-            val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
+            val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
 
             // WHEN has no notifs
             activeNotificationListRepository.setActiveNotifs(count = 0)
@@ -233,13 +243,13 @@
             runCurrent()
 
             // THEN empty shade is not visible
-            assertThat(shouldInclude).isFalse()
+            assertThat(shouldShow).isFalse()
         }
 
     @Test
-    fun shouldIncludeEmptyShadeView_falseWhenStartingToSleep() =
+    fun shouldShowEmptyShadeView_falseWhenStartingToSleep() =
         testScope.runTest {
-            val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
+            val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
 
             // WHEN has no notifs
             activeNotificationListRepository.setActiveNotifs(count = 0)
@@ -250,7 +260,7 @@
             runCurrent()
 
             // THEN empty shade is not visible
-            assertThat(shouldInclude).isFalse()
+            assertThat(shouldShow).isFalse()
         }
 
     @Test
@@ -258,8 +268,10 @@
         testScope.runTest {
             val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
 
-            zenModeRepository.setSuppressedVisualEffects(Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST)
-            zenModeRepository.zenMode.value = Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS
+            zenModeRepository.updateNotificationPolicy(
+                suppressedVisualEffects = Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST
+            )
+            zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
             runCurrent()
 
             assertThat(hidden).isTrue()
@@ -270,8 +282,10 @@
         testScope.runTest {
             val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
 
-            zenModeRepository.setSuppressedVisualEffects(Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST)
-            zenModeRepository.zenMode.value = Settings.Global.ZEN_MODE_OFF
+            zenModeRepository.updateNotificationPolicy(
+                suppressedVisualEffects = Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST
+            )
+            zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_OFF)
             runCurrent()
 
             assertThat(hidden).isFalse()
@@ -302,7 +316,8 @@
     @Test
     fun shouldIncludeFooterView_trueWhenShade() =
         testScope.runTest {
-            val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
+            val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView)
+            val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView)
 
             // WHEN has notifs
             activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -312,13 +327,15 @@
             runCurrent()
 
             // THEN footer is visible
-            assertThat(shouldInclude?.value).isTrue()
+            assertThat(shouldIncludeFooterView?.value).isTrue()
+            assertThat(shouldShowEmptyShadeView).isFalse()
         }
 
     @Test
     fun shouldIncludeFooterView_trueWhenLockedShade() =
         testScope.runTest {
-            val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
+            val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView)
+            val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView)
 
             // WHEN has notifs
             activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -328,7 +345,8 @@
             runCurrent()
 
             // THEN footer is visible
-            assertThat(shouldInclude?.value).isTrue()
+            assertThat(shouldIncludeFooterView?.value).isTrue()
+            assertThat(shouldShowEmptyShadeView).isFalse()
         }
 
     @Test
@@ -404,7 +422,8 @@
     @Test
     fun shouldIncludeFooterView_trueWhenQsExpandedSplitShade() =
         testScope.runTest {
-            val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
+            val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView)
+            val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView)
 
             // WHEN has notifs
             activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -419,7 +438,8 @@
             runCurrent()
 
             // THEN footer is visible
-            assertThat(shouldInclude?.value).isTrue()
+            assertThat(shouldIncludeFooterView?.value).isTrue()
+            assertThat(shouldShowEmptyShadeView).isFalse()
         }
 
     @Test
@@ -528,9 +548,7 @@
                     FakeHeadsUpRowRepository(key = "1"),
                     FakeHeadsUpRowRepository(key = "2"),
                 )
-            headsUpRepository.setNotifications(
-                rows,
-            )
+            headsUpRepository.setNotifications(rows)
             runCurrent()
 
             // THEN the list is empty
@@ -566,7 +584,7 @@
 
             headsUpRepository.setNotifications(
                 FakeHeadsUpRowRepository(key = "0", isPinned = true),
-                FakeHeadsUpRowRepository(key = "1")
+                FakeHeadsUpRowRepository(key = "1"),
             )
             runCurrent()
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
new file mode 100644
index 0000000..f1fed19
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy.domain.interactor
+
+import android.app.NotificationManager.Policy
+import android.provider.Settings
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.settingslib.statusbar.notification.data.repository.updateNotificationPolicy
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class ZenModeInteractorTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val repository = kosmos.fakeZenModeRepository
+
+    private val underTest = kosmos.zenModeInteractor
+
+    @Test
+    fun isZenModeEnabled_off() =
+        testScope.runTest {
+            val enabled by collectLastValue(underTest.isZenModeEnabled)
+
+            repository.updateZenMode(Settings.Global.ZEN_MODE_OFF)
+            runCurrent()
+
+            assertThat(enabled).isFalse()
+        }
+
+    @Test
+    fun isZenModeEnabled_alarms() =
+        testScope.runTest {
+            val enabled by collectLastValue(underTest.isZenModeEnabled)
+
+            repository.updateZenMode(Settings.Global.ZEN_MODE_ALARMS)
+            runCurrent()
+
+            assertThat(enabled).isTrue()
+        }
+
+    @Test
+    fun isZenModeEnabled_importantInterruptions() =
+        testScope.runTest {
+            val enabled by collectLastValue(underTest.isZenModeEnabled)
+
+            repository.updateZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
+            runCurrent()
+
+            assertThat(enabled).isTrue()
+        }
+
+    @Test
+    fun isZenModeEnabled_noInterruptions() =
+        testScope.runTest {
+            val enabled by collectLastValue(underTest.isZenModeEnabled)
+
+            repository.updateZenMode(Settings.Global.ZEN_MODE_NO_INTERRUPTIONS)
+            runCurrent()
+
+            assertThat(enabled).isTrue()
+        }
+
+    @Test
+    fun testIsZenModeEnabled_unknown() =
+        testScope.runTest {
+            val enabled by collectLastValue(underTest.isZenModeEnabled)
+
+            repository.updateZenMode(4) // this should fail if we ever add another zen mode type
+            runCurrent()
+
+            assertThat(enabled).isFalse()
+        }
+
+    @Test
+    fun areNotificationsHiddenInShade_noPolicy() =
+        testScope.runTest {
+            val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
+
+            repository.updateNotificationPolicy(null)
+            repository.updateZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
+            runCurrent()
+
+            assertThat(hidden).isFalse()
+        }
+
+    @Test
+    fun areNotificationsHiddenInShade_zenOffShadeSuppressed() =
+        testScope.runTest {
+            val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
+
+            repository.updateNotificationPolicy(
+                suppressedVisualEffects = Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST
+            )
+            repository.updateZenMode(Settings.Global.ZEN_MODE_OFF)
+            runCurrent()
+
+            assertThat(hidden).isFalse()
+        }
+
+    @Test
+    fun areNotificationsHiddenInShade_zenOnShadeNotSuppressed() =
+        testScope.runTest {
+            val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
+
+            repository.updateNotificationPolicy(
+                suppressedVisualEffects = Policy.SUPPRESSED_EFFECT_STATUS_BAR
+            )
+            repository.updateZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
+            runCurrent()
+
+            assertThat(hidden).isFalse()
+        }
+
+    @Test
+    fun areNotificationsHiddenInShade_zenOnShadeSuppressed() =
+        testScope.runTest {
+            val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
+
+            repository.updateNotificationPolicy(
+                suppressedVisualEffects = Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST
+            )
+            repository.updateZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
+            runCurrent()
+
+            assertThat(hidden).isTrue()
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt
index d620639..6e49e43 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt
@@ -20,7 +20,6 @@
 import android.provider.Settings
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.settingslib.statusbar.notification.data.model.ZenMode
 import com.android.settingslib.statusbar.notification.data.repository.updateNotificationPolicy
 import com.android.settingslib.volume.domain.interactor.AudioVolumeInteractor
 import com.android.settingslib.volume.shared.model.AudioStream
@@ -29,7 +28,7 @@
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.statusbar.notification.domain.interactor.notificationsSoundPolicyInteractor
-import com.android.systemui.statusbar.notification.domain.interactor.notificationsSoundPolicyRepository
+import com.android.systemui.statusbar.policy.data.repository.zenModeRepository
 import com.android.systemui.testKosmos
 import com.android.systemui.volume.data.repository.audioRepository
 import com.google.common.truth.Truth.assertThat
@@ -104,10 +103,8 @@
     fun zenMuted_cantChange() {
         with(kosmos) {
             testScope.runTest {
-                notificationsSoundPolicyRepository.updateNotificationPolicy()
-                notificationsSoundPolicyRepository.updateZenMode(
-                    ZenMode(Settings.Global.ZEN_MODE_NO_INTERRUPTIONS)
-                )
+                zenModeRepository.updateNotificationPolicy()
+                zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_NO_INTERRUPTIONS)
 
                 val canChangeVolume by
                     collectLastValue(
@@ -141,9 +138,7 @@
         with(kosmos) {
             testScope.runTest {
                 audioRepository.setLastAudibleVolume(audioStream, 30)
-                notificationsSoundPolicyRepository.updateZenMode(
-                    ZenMode(Settings.Global.ZEN_MODE_NO_INTERRUPTIONS)
-                )
+                zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_NO_INTERRUPTIONS)
 
                 val model by collectLastValue(underTest.getAudioStream(audioStream))
                 runCurrent()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt
index 2f69942..ebc78d8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.volume.panel.component.spatial.domain
 
+import android.bluetooth.BluetoothDevice
+import android.bluetooth.BluetoothProfile
 import android.media.AudioDeviceAttributes
 import android.media.AudioDeviceInfo
 import android.media.session.MediaSession
@@ -24,12 +26,14 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.LeAudioProfile
 import com.android.settingslib.media.BluetoothMediaDevice
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.media.spatializerRepository
 import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.android.systemui.volume.localMediaController
@@ -56,8 +60,15 @@
     @Before
     fun setup() {
         with(kosmos) {
+            val leAudioProfile =
+                mock<LeAudioProfile> {
+                    whenever(profileId).thenReturn(BluetoothProfile.LE_AUDIO)
+                    whenever(isEnabled(any())).thenReturn(true)
+                }
             val cachedBluetoothDevice: CachedBluetoothDevice = mock {
                 whenever(address).thenReturn("test_address")
+                whenever(profiles).thenReturn(listOf(leAudioProfile))
+                whenever(device).thenReturn(mock<BluetoothDevice> {})
             }
             localMediaRepository.updateCurrentConnectedDevice(
                 mock<BluetoothMediaDevice> {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt
index 555d77c..d5566ad 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt
@@ -65,16 +65,20 @@
 
     private val kosmos = testKosmos()
     private lateinit var underTest: SpatialAudioComponentInteractor
+
+    private val bluetoothDevice: BluetoothDevice = mock {}
     private val a2dpProfile: A2dpProfile = mock {
         whenever(profileId).thenReturn(BluetoothProfile.A2DP)
+        whenever(isEnabled(bluetoothDevice)).thenReturn(false)
     }
     private val leAudioProfile: LeAudioProfile = mock {
         whenever(profileId).thenReturn(BluetoothProfile.LE_AUDIO)
+        whenever(isEnabled(bluetoothDevice)).thenReturn(true)
     }
     private val hearingAidProfile: HearingAidProfile = mock {
         whenever(profileId).thenReturn(BluetoothProfile.HEARING_AID)
+        whenever(isEnabled(bluetoothDevice)).thenReturn(false)
     }
-    private val bluetoothDevice: BluetoothDevice = mock {}
 
     @Before
     fun setup() {
diff --git a/packages/SystemUI/res/drawable/ic_check_box.xml b/packages/SystemUI/res/drawable/ic_check_box.xml
deleted file mode 100644
index a8d1a65..0000000
--- a/packages/SystemUI/res/drawable/ic_check_box.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<!--
-  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
-  -->
-
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item
-        android:id="@+id/checked"
-        android:state_checked="true"
-        android:drawable="@drawable/ic_check_box_blue_24dp" />
-    <item
-        android:id="@+id/unchecked"
-        android:state_checked="false"
-        android:drawable="@drawable/ic_check_box_outline_24dp" />
-</selector>
diff --git a/packages/SystemUI/res/drawable/ic_check_box_blue_24dp.xml b/packages/SystemUI/res/drawable/ic_check_box_blue_24dp.xml
deleted file mode 100644
index 43cae69..0000000
--- a/packages/SystemUI/res/drawable/ic_check_box_blue_24dp.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<!--
-  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
-  -->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="24dp"
-        android:height="24dp"
-        android:viewportWidth="24"
-        android:viewportHeight="24">
-    <path
-        android:pathData="M19,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.11,0 2,-0.9 2,-2L21,5c0,-1.1 -0.89,-2 -2,-2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z"
-        android:fillColor="#4285F4"/>
-</vector>
-
diff --git a/packages/SystemUI/res/drawable/ic_check_box_outline_24dp.xml b/packages/SystemUI/res/drawable/ic_check_box_outline_24dp.xml
deleted file mode 100644
index f6f453a..0000000
--- a/packages/SystemUI/res/drawable/ic_check_box_outline_24dp.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<!--
-  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
-  -->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="24dp"
-        android:height="24dp"
-        android:viewportWidth="24"
-        android:viewportHeight="24">
-    <path
-        android:pathData="M19,5v14H5V5h14m0,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z"
-        android:fillColor="#757575"/>
-</vector>
-
diff --git a/packages/SystemUI/res/drawable/ic_speaker_group_black_24dp.xml b/packages/SystemUI/res/drawable/ic_speaker_group_black_24dp.xml
deleted file mode 100644
index ae0d562..0000000
--- a/packages/SystemUI/res/drawable/ic_speaker_group_black_24dp.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<!--
-  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
-  -->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="24dp"
-        android:height="24dp"
-        android:viewportWidth="24"
-        android:viewportHeight="24">
-    <path
-        android:pathData="M18.2,1L9.8,1C8.81,1 8,1.81 8,2.8v14.4c0,0.99 0.81,1.79 1.8,1.79l8.4,0.01c0.99,0 1.8,-0.81 1.8,-1.8L20,2.8c0,-0.99 -0.81,-1.8 -1.8,-1.8zM14,3c1.1,0 2,0.89 2,2s-0.9,2 -2,2 -2,-0.89 -2,-2 0.9,-2 2,-2zM14,16.5c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4 4,1.79 4,4 -1.79,4 -4,4z"
-        android:fillColor="#000000"/>
-    <path
-        android:pathData="M14,12.5m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0"
-        android:fillColor="#000000"/>
-    <path
-        android:pathData="M6,5H4v16c0,1.1 0.89,2 2,2h10v-2H6V5z"
-        android:fillColor="#000000"/>
-</vector>
diff --git a/packages/SystemUI/res/layout/media_output_list_item.xml b/packages/SystemUI/res/layout/media_output_list_item.xml
deleted file mode 100644
index 5b1ec7f..0000000
--- a/packages/SystemUI/res/layout/media_output_list_item.xml
+++ /dev/null
@@ -1,146 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ 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.
-  -->
-
-<LinearLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/device_container"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:orientation="vertical">
-    <FrameLayout
-        android:layout_width="match_parent"
-        android:layout_height="64dp"
-        android:layout_marginStart="16dp"
-        android:layout_marginEnd="16dp"
-        android:layout_marginBottom="12dp">
-        <FrameLayout
-            android:id="@+id/item_layout"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:background="@drawable/media_output_item_background"
-            android:layout_gravity="center_vertical|start">
-            <com.android.systemui.media.dialog.MediaOutputSeekbar
-                android:id="@+id/volume_seekbar"
-                android:splitTrack="false"
-                android:visibility="gone"
-                android:paddingStart="0dp"
-                android:paddingEnd="0dp"
-                android:background="@null"
-                android:contentDescription="@string/media_output_dialog_accessibility_seekbar"
-                android:progressDrawable="@drawable/media_output_dialog_seekbar_background"
-                android:thumb="@null"
-                android:layout_width="match_parent"
-                android:layout_height="match_parent"/>
-        </FrameLayout>
-
-        <FrameLayout
-            android:layout_width="56dp"
-            android:layout_height="64dp"
-            android:layout_gravity="center_vertical|start">
-            <ImageView
-                android:id="@+id/title_icon"
-                android:layout_width="24dp"
-                android:layout_height="24dp"
-                android:layout_gravity="center"/>
-        </FrameLayout>
-
-        <TextView
-            android:id="@+id/title"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_gravity="center_vertical|start"
-            android:layout_marginStart="56dp"
-            android:layout_marginEnd="56dp"
-            android:ellipsize="end"
-            android:maxLines="1"
-            android:fontFamily="@*android:string/config_headlineFontFamilyMedium"
-            android:textSize="16sp"/>
-
-        <LinearLayout
-            android:id="@+id/two_line_layout"
-            android:orientation="vertical"
-            android:layout_width="wrap_content"
-            android:layout_gravity="center_vertical|start"
-            android:layout_height="48dp"
-            android:layout_marginEnd="56dp"
-            android:layout_marginStart="56dp">
-            <TextView
-                android:id="@+id/two_line_title"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:ellipsize="end"
-                android:maxLines="1"
-                android:fontFamily="@*android:string/config_headlineFontFamilyMedium"
-                android:textColor="@color/media_dialog_item_main_content"
-                android:textSize="16sp"/>
-            <TextView
-                android:id="@+id/subtitle"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:ellipsize="end"
-                android:maxLines="1"
-                android:textColor="@color/media_dialog_item_main_content"
-                android:textSize="14sp"
-                android:fontFamily="@*android:string/config_bodyFontFamily"
-                android:visibility="gone"/>
-        </LinearLayout>
-
-        <ProgressBar
-            android:id="@+id/volume_indeterminate_progress"
-            style="?android:attr/progressBarStyleSmallTitle"
-            android:layout_width="24dp"
-            android:layout_height="24dp"
-            android:layout_marginEnd="16dp"
-            android:indeterminate="true"
-            android:layout_gravity="end|center"
-            android:indeterminateOnly="true"
-            android:visibility="gone"/>
-
-        <ImageView
-            android:id="@+id/media_output_item_status"
-            android:layout_width="24dp"
-            android:layout_height="24dp"
-            android:layout_marginEnd="16dp"
-            android:indeterminate="true"
-            android:layout_gravity="end|center"
-            android:indeterminateOnly="true"
-            android:importantForAccessibility="no"
-            android:visibility="gone"/>
-
-        <LinearLayout
-            android:id="@+id/end_action_area"
-            android:visibility="gone"
-            android:orientation="vertical"
-            android:layout_width="48dp"
-            android:layout_height="64dp"
-            android:layout_gravity="end|center"
-            android:gravity="center_vertical">
-            <CheckBox
-                android:id="@+id/check_box"
-                android:focusable="false"
-                android:importantForAccessibility="no"
-                android:layout_width="24dp"
-                android:layout_height="24dp"
-                android:layout_marginEnd="16dp"
-                android:layout_gravity="end"
-                android:button="@drawable/ic_circle_check_box"
-                android:visibility="gone"
-            />
-
-        </LinearLayout>
-    </FrameLayout>
-</LinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/ongoing_activity_chip.xml b/packages/SystemUI/res/layout/ongoing_activity_chip.xml
index cd5c37d..beb16b3 100644
--- a/packages/SystemUI/res/layout/ongoing_activity_chip.xml
+++ b/packages/SystemUI/res/layout/ongoing_activity_chip.xml
@@ -46,6 +46,8 @@
             android:tint="?android:attr/colorPrimary"
         />
 
+        <!-- Only one of [ongoing_activity_chip_time, ongoing_activity_chip_text] will ever
+             be shown at one time. -->
         <com.android.systemui.statusbar.chips.ui.view.ChipChronometer
             android:id="@+id/ongoing_activity_chip_time"
             android:layout_width="wrap_content"
@@ -58,5 +60,19 @@
             android:textColor="?android:attr/colorPrimary"
         />
 
+        <!-- Used to show generic text in the chip instead of a timer. -->
+        <TextView
+            android:id="@+id/ongoing_activity_chip_text"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:singleLine="true"
+            android:gravity="center|start"
+            android:paddingStart="@dimen/ongoing_activity_chip_icon_text_padding"
+            android:textAppearance="@android:style/TextAppearance.Material.Small"
+            android:fontFamily="@*android:string/config_headlineFontFamily"
+            android:textColor="?android:attr/colorPrimary"
+            android:visibility="gone"
+            />
+
     </com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer>
 </FrameLayout>
diff --git a/packages/SystemUI/res/layout/rich_ongoing_timer_notification.xml b/packages/SystemUI/res/layout/rich_ongoing_timer_notification.xml
new file mode 100644
index 0000000..f2bfbe5c9
--- /dev/null
+++ b/packages/SystemUI/res/layout/rich_ongoing_timer_notification.xml
@@ -0,0 +1,116 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+<com.android.systemui.statusbar.notification.row.ui.view.TimerView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <androidx.constraintlayout.widget.Guideline
+        android:id="@+id/topBaseline"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        app:layout_constraintGuide_begin="22sp"
+        />
+
+    <ImageView
+        android:id="@+id/icon"
+        android:layout_width="24dp"
+        android:layout_height="24dp"
+        android:src="@drawable/ic_close"
+        app:tint="@android:color/white"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toStartOf="@id/label"
+        android:baseline="18dp"
+        app:layout_constraintBaseline_toTopOf="@id/topBaseline"
+        />
+    <TextView
+        android:id="@+id/label"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        app:layout_constraintStart_toEndOf="@id/icon"
+        app:layout_constraintEnd_toStartOf="@id/chronoRemaining"
+        android:singleLine="true"
+        tools:text="15s Timer"
+        app:layout_constraintBaseline_toTopOf="@id/topBaseline"
+        android:paddingEnd="4dp"
+        />
+    <Chronometer
+        android:id="@+id/chronoRemaining"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:singleLine="true"
+        android:textSize="20sp"
+        android:gravity="end"
+        tools:text="0:12"
+        app:layout_constraintBaseline_toTopOf="@id/topBaseline"
+        app:layout_constraintEnd_toStartOf="@id/pausedTimeRemaining"
+        app:layout_constraintStart_toEndOf="@id/label"
+        android:countDown="true"
+        android:paddingEnd="4dp"
+        />
+    <TextView
+        android:id="@+id/pausedTimeRemaining"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:singleLine="true"
+        android:textSize="20sp"
+        android:gravity="end"
+        tools:text="0:12"
+        app:layout_constraintBaseline_toTopOf="@id/topBaseline"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toEndOf="@id/chronoRemaining"
+        android:paddingEnd="4dp"
+        />
+
+    <androidx.constraintlayout.widget.Barrier
+        android:id="@+id/bottomOfTop"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        app:barrierDirection="bottom"
+        app:constraint_referenced_ids="icon,label,chronoRemaining,pausedTimeRemaining"
+        />
+
+    <com.android.systemui.statusbar.notification.row.ui.view.TimerButtonView
+        android:id="@+id/mainButton"
+        android:layout_width="124dp"
+        android:layout_height="wrap_content"
+        tools:text="Reset"
+        tools:drawableStart="@android:drawable/ic_menu_add"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toStartOf="@id/altButton"
+        app:layout_constraintTop_toBottomOf="@id/bottomOfTop"
+        app:layout_constraintHorizontal_chainStyle="spread"
+        android:paddingEnd="4dp"
+        />
+
+    <com.android.systemui.statusbar.notification.row.ui.view.TimerButtonView
+        android:id="@+id/altButton"
+        tools:text="Reset"
+        tools:drawableStart="@android:drawable/ic_menu_add"
+        android:drawablePadding="2dp"
+        android:drawableTint="@android:color/white"
+        android:layout_width="124dp"
+        android:layout_height="wrap_content"
+        app:layout_constraintTop_toBottomOf="@id/bottomOfTop"
+        app:layout_constraintStart_toEndOf="@id/mainButton"
+        app:layout_constraintEnd_toEndOf="parent"
+        android:paddingEnd="4dp"
+        />
+</com.android.systemui.statusbar.notification.row.ui.view.TimerView>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml
index ba59c2f9..b3d3021 100644
--- a/packages/SystemUI/res/values/colors.xml
+++ b/packages/SystemUI/res/values/colors.xml
@@ -18,7 +18,6 @@
 -->
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
            xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
-    <drawable name="notification_number_text_color">#ffffffff</drawable>
     <drawable name="system_bar_background">@color/system_bar_background_opaque</drawable>
     <color name="system_bar_background_opaque">#ff000000</color>
     <color name="system_bar_background_transparent">#00000000</color>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 4ef9442..80b9ec7 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -25,9 +25,6 @@
      (package/class)  -->
     <string name="config_recentsComponent" translatable="false">com.android.systemui.recents.OverviewProxyRecentsImpl</string>
 
-    <!-- Whether or not we show the number in the bar. -->
-    <bool name="config_statusBarShowNumber">false</bool>
-
     <!-- For how long the lock screen can be on before the display turns off. -->
     <integer name="config_lockScreenDisplayTimeout">10000</integer>
 
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java
index f06e333..78d4fc8 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java
@@ -246,6 +246,9 @@
     public ActivityManager.RecentTaskInfo.PersistedTaskSnapshotData lastSnapshotData =
             new ActivityManager.RecentTaskInfo.PersistedTaskSnapshotData();
 
+    @ViewDebug.ExportedProperty(category="recents")
+    public boolean isVisible;
+
     public Task() {
         // Do nothing
     }
@@ -279,6 +282,7 @@
         lastSnapshotData.set(other.lastSnapshotData);
         positionInParent = other.positionInParent;
         appBounds = other.appBounds;
+        isVisible = other.isVisible;
     }
 
     /**
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java
index 660f0db..2eac393 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java
@@ -53,6 +53,9 @@
 import android.view.animation.Interpolator;
 import android.view.animation.LinearInterpolator;
 
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.UiEvent;
 import com.android.internal.logging.UiEventLogger;
@@ -142,12 +145,14 @@
     };
 
     private final IRotationWatcher.Stub mRotationWatcher = new IRotationWatcher.Stub() {
+        @WorkerThread
         @Override
         public void onRotationChanged(final int rotation) {
+            @Nullable Boolean rotationLocked = RotationPolicyUtil.isRotationLocked(mContext);
             // We need this to be scheduled as early as possible to beat the redrawing of
             // window in response to the orientation change.
             mMainThreadHandler.postAtFrontOfQueue(() -> {
-                onRotationWatcherChanged(rotation);
+                onRotationWatcherChanged(rotation, rotationLocked);
             });
         }
     };
@@ -281,8 +286,8 @@
         TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener);
     }
 
-    public void setRotationLockedAtAngle(int rotationSuggestion, String caller) {
-        final Boolean isLocked = isRotationLocked();
+    public void setRotationLockedAtAngle(
+            @Nullable Boolean isLocked, int rotationSuggestion, String caller) {
         if (isLocked == null) {
             // Ignore if we can't read the setting for the current user
             return;
@@ -291,21 +296,6 @@
                 /* rotation= */ rotationSuggestion, caller);
     }
 
-    /**
-     * @return whether rotation is currently locked, or <code>null</code> if the setting couldn't
-     *         be read
-     */
-    public Boolean isRotationLocked() {
-        try {
-            return RotationPolicy.isRotationLocked(mContext);
-        } catch (SecurityException e) {
-            // TODO(b/279561841): RotationPolicy uses the current user to resolve the setting which
-            //                    may change before the rotation watcher can be unregistered
-            Log.e(TAG, "Failed to get isRotationLocked", e);
-            return null;
-        }
-    }
-
     public void setRotateSuggestionButtonState(boolean visible) {
         setRotateSuggestionButtonState(visible, false /* force */);
     }
@@ -469,7 +459,7 @@
      * Called when the rotation watcher rotation changes, either from the watcher registered
      * internally in this class, or a signal propagated from NavBarHelper.
      */
-    public void onRotationWatcherChanged(int rotation) {
+    public void onRotationWatcherChanged(int rotation, @Nullable Boolean isRotationLocked) {
         if (!mListenersRegistered) {
             // Ignore if not registered
             return;
@@ -477,17 +467,16 @@
 
         // If the screen rotation changes while locked, potentially update lock to flow with
         // new screen rotation and hide any showing suggestions.
-        Boolean rotationLocked = isRotationLocked();
-        if (rotationLocked == null) {
+        if (isRotationLocked == null) {
             // Ignore if we can't read the setting for the current user
             return;
         }
         // The isVisible check makes the rotation button disappear when we are not locked
         // (e.g. for tabletop auto-rotate).
-        if (rotationLocked || mRotationButton.isVisible()) {
+        if (isRotationLocked || mRotationButton.isVisible()) {
             // Do not allow a change in rotation to set user rotation when docked.
-            if (shouldOverrideUserLockPrefs(rotation) && rotationLocked && !mDocked) {
-                setRotationLockedAtAngle(rotation, /* caller= */
+            if (shouldOverrideUserLockPrefs(rotation) && isRotationLocked && !mDocked) {
+                setRotationLockedAtAngle(true, rotation, /* caller= */
                         "RotationButtonController#onRotationWatcherChanged");
             }
             setRotateSuggestionButtonState(false /* visible */, true /* forced */);
@@ -592,7 +581,8 @@
     private void onRotateSuggestionClick(View v) {
         mUiEventLogger.log(RotationButtonEvent.ROTATION_SUGGESTION_ACCEPTED);
         incrementNumAcceptedRotationSuggestionsIfNeeded();
-        setRotationLockedAtAngle(mLastRotationSuggestion,
+        setRotationLockedAtAngle(
+                RotationPolicyUtil.isRotationLocked(mContext), mLastRotationSuggestion,
                 /* caller= */ "RotationButtonController#onRotateSuggestionClick");
         Log.i(TAG, "onRotateSuggestionClick() mLastRotationSuggestion=" + mLastRotationSuggestion);
         v.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationPolicyUtil.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationPolicyUtil.kt
new file mode 100644
index 0000000..eac4a10
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationPolicyUtil.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shared.rotation
+
+import android.content.Context
+import android.util.Log
+import com.android.internal.view.RotationPolicy
+
+class RotationPolicyUtil {
+    companion object {
+        /**
+         * Recommend to be called on bg thread, or reuse the results. It's because
+         * [RotationPolicy.isRotationLocked] may make a binder call to query settings.
+         *
+         * @return whether rotation is currently locked, or <code>null</code> if the setting
+         *   couldn't be read
+         */
+        @JvmStatic
+        fun isRotationLocked(context: Context): Boolean? {
+            try {
+                return RotationPolicy.isRotationLocked(context)
+            } catch (e: SecurityException) {
+                // TODO(b/279561841): RotationPolicy uses the current user to resolve the setting
+                // which may change before the rotation watcher can be unregistered
+                Log.e("RotationPolicy", "Failed to get isRotationLocked", e)
+                return null
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index 70465bc..4217820 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -1097,7 +1097,8 @@
             int yTranslation = mResources.getDimensionPixelSize(R.dimen.disappear_y_translation);
 
             AnimatorSet anims = new AnimatorSet();
-            ObjectAnimator yAnim = ObjectAnimator.ofFloat(mView, View.TRANSLATION_Y, yTranslation);
+            ObjectAnimator yAnim = ObjectAnimator.ofFloat(mViewFlipper, View.TRANSLATION_Y,
+                    yTranslation);
             ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(mUserSwitcherViewGroup, View.ALPHA,
                     0f);
 
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
index 25ad385..b37ba89 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
@@ -1633,7 +1633,7 @@
 
         int maxHeightSize;
         int maxWidthSize;
-        if (Flags.redesignMagnifierWindowSize()) {
+        if (Flags.redesignMagnificationWindowSize()) {
             // mOuterBorderSize = transparent margin area
             // mMirrorSurfaceMargin = transparent margin area + orange border width
             // We would like to allow the width and height to be full size. Therefore, the max
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt
index c08756f..5e2b5ff 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt
@@ -74,7 +74,7 @@
     val isConfirmationRequired: Flow<Boolean>
 
     /** Fingerprint sensor type */
-    val sensorType: Flow<FingerprintSensorType>
+    val fingerprintSensorType: Flow<FingerprintSensorType>
 
     /** Switch to the credential view. */
     fun onSwitchToCredential()
@@ -154,7 +154,8 @@
             }
         }
 
-    override val sensorType: Flow<FingerprintSensorType> = fingerprintPropertyRepository.sensorType
+    override val fingerprintSensorType: Flow<FingerprintSensorType> =
+        fingerprintPropertyRepository.sensorType
 
     override fun onSwitchToCredential() {
         val modalities: BiometricModalities =
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt
index 7081661..6c6ef5a 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt
@@ -21,7 +21,6 @@
 import android.annotation.RawRes
 import android.content.res.Configuration
 import android.graphics.Rect
-import android.hardware.face.Face
 import android.util.RotationUtils
 import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
 import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor
@@ -137,7 +136,7 @@
                         displayStateInteractor.currentRotation,
                         displayStateInteractor.isFolded,
                         displayStateInteractor.isInRearDisplayMode,
-                        promptSelectorInteractor.sensorType,
+                        promptSelectorInteractor.fingerprintSensorType,
                         promptViewModel.isAuthenticated,
                         promptViewModel.isAuthenticating,
                         promptViewModel.showingError
@@ -183,7 +182,7 @@
                         displayStateInteractor.currentRotation,
                         displayStateInteractor.isFolded,
                         displayStateInteractor.isInRearDisplayMode,
-                        promptSelectorInteractor.sensorType,
+                        promptSelectorInteractor.fingerprintSensorType,
                         promptViewModel.isAuthenticated,
                         promptViewModel.isAuthenticating,
                         promptViewModel.isPendingConfirmation,
@@ -330,7 +329,7 @@
                 AuthType.Coex ->
                     combine(
                         displayStateInteractor.currentRotation,
-                        promptSelectorInteractor.sensorType,
+                        promptSelectorInteractor.fingerprintSensorType,
                         promptViewModel.isAuthenticated,
                         promptViewModel.isAuthenticating,
                         promptViewModel.showingError
@@ -430,7 +429,7 @@
                 AuthType.Fingerprint,
                 AuthType.Coex ->
                     combine(
-                        promptSelectorInteractor.sensorType,
+                        promptSelectorInteractor.fingerprintSensorType,
                         promptViewModel.isAuthenticated,
                         promptViewModel.isAuthenticating,
                         promptViewModel.isPendingConfirmation,
@@ -508,7 +507,7 @@
             when (activeAuthType) {
                 AuthType.Fingerprint ->
                     combine(
-                        promptSelectorInteractor.sensorType,
+                        promptSelectorInteractor.fingerprintSensorType,
                         promptViewModel.isAuthenticated,
                         promptViewModel.isAuthenticating,
                         promptViewModel.showingError
@@ -546,7 +545,7 @@
                     }
                 AuthType.Coex ->
                     combine(
-                        promptSelectorInteractor.sensorType,
+                        promptSelectorInteractor.fingerprintSensorType,
                         promptViewModel.isAuthenticated,
                         promptViewModel.isAuthenticating,
                         promptViewModel.isPendingConfirmation,
@@ -606,7 +605,7 @@
                 AuthType.Fingerprint,
                 AuthType.Coex ->
                     combine(
-                        promptSelectorInteractor.sensorType,
+                        promptSelectorInteractor.fingerprintSensorType,
                         promptViewModel.isAuthenticated,
                         promptViewModel.isAuthenticating,
                         promptViewModel.showingError
@@ -642,7 +641,7 @@
                 AuthType.Fingerprint,
                 AuthType.Coex ->
                     combine(
-                        promptSelectorInteractor.sensorType,
+                        promptSelectorInteractor.fingerprintSensorType,
                         displayStateInteractor.currentRotation
                     ) { sensorType: FingerprintSensorType, rotation: DisplayRotation ->
                         when (sensorType) {
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractor.kt
index 7d3075a..ed931bd 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractor.kt
@@ -43,10 +43,11 @@
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 
@@ -68,7 +69,12 @@
     mobileConnectionsRepository: MobileConnectionsRepository,
 ) {
     val subId: StateFlow<Int> = repository.subscriptionId
-    val isAnySimSecure: Flow<Boolean> = mobileConnectionsRepository.isAnySimSecure
+    val isAnySimSecure: StateFlow<Boolean> =
+        mobileConnectionsRepository.isAnySimSecure.stateIn(
+            scope = applicationScope,
+            started = SharingStarted.WhileSubscribed(),
+            initialValue = mobileConnectionsRepository.getIsAnySimSecure(),
+        )
     val isLockedEsim: StateFlow<Boolean?> = repository.isLockedEsim
     val errorDialogMessage: StateFlow<String?> = repository.errorDialogMessage
 
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/binder/IconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/common/ui/binder/IconViewBinder.kt
index 108e22b..64dedea 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/binder/IconViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/binder/IconViewBinder.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.common.ui.binder
 
+import android.view.View
 import android.widget.ImageView
 import com.android.systemui.common.shared.model.Icon
 
@@ -30,4 +31,13 @@
             is Icon.Resource -> view.setImageResource(icon.res)
         }
     }
+
+    fun bindNullable(icon: Icon?, view: ImageView) {
+        if (icon != null) {
+            view.visibility = View.VISIBLE
+            bind(icon, view)
+        } else {
+            view.visibility = View.GONE
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
index 88c3f9f6..e31f1ad 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
@@ -18,6 +18,7 @@
 
 import android.provider.Settings
 import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.TransitionKey
 import com.android.systemui.CoreStartable
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
@@ -91,8 +92,8 @@
         keyguardTransitionInteractor.startedKeyguardTransitionStep
             .mapLatest(::determineSceneAfterTransition)
             .filterNotNull()
-            .onEach { nextScene ->
-                communalSceneInteractor.changeScene(nextScene, CommunalTransitionKeys.SimpleFade)
+            .onEach { (nextScene, nextTransition) ->
+                communalSceneInteractor.changeScene(nextScene, nextTransition)
             }
             .launchIn(applicationScope)
 
@@ -188,7 +189,7 @@
 
     private suspend fun determineSceneAfterTransition(
         lastStartedTransition: TransitionStep,
-    ): SceneKey? {
+    ): Pair<SceneKey, TransitionKey>? {
         val to = lastStartedTransition.to
         val from = lastStartedTransition.from
         val docked = dockManager.isDocked
@@ -201,22 +202,27 @@
                 // underneath the hub is shown. When launching activities over lockscreen, we only
                 // change scenes once the activity launch animation is finished, so avoid
                 // changing the scene here.
-                CommunalScenes.Blank
+                Pair(CommunalScenes.Blank, CommunalTransitionKeys.SimpleFade)
             }
             to == KeyguardState.GLANCEABLE_HUB && from == KeyguardState.OCCLUDED -> {
                 // When transitioning to the hub from an occluded state, fade out the hub without
                 // doing any translation.
-                CommunalScenes.Communal
+                Pair(CommunalScenes.Communal, CommunalTransitionKeys.SimpleFade)
             }
             // Transitioning to Blank scene when entering the edit mode will be handled separately
             // with custom animations.
             to == KeyguardState.GONE && !communalInteractor.editModeOpen.value ->
-                CommunalScenes.Blank
+                Pair(CommunalScenes.Blank, CommunalTransitionKeys.SimpleFade)
             !docked && !KeyguardState.deviceIsAwakeInState(to) -> {
                 // If the user taps the screen and wakes the device within this timeout, we don't
                 // want to dismiss the hub
                 delay(AWAKE_DEBOUNCE_DELAY)
-                CommunalScenes.Blank
+                Pair(CommunalScenes.Blank, CommunalTransitionKeys.SimpleFade)
+            }
+            from == KeyguardState.DOZING && to == KeyguardState.GLANCEABLE_HUB -> {
+                // Make sure the communal hub is showing (immediately, not fading in) when
+                // transitioning from dozing to hub.
+                Pair(CommunalScenes.Communal, CommunalTransitionKeys.Immediately)
             }
             else -> null
         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
index 209bc7a..c4b70d8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
@@ -89,6 +89,7 @@
 import com.android.systemui.power.domain.interactor.PowerInteractor;
 import com.android.systemui.power.shared.model.ScreenPowerState;
 import com.android.systemui.scene.domain.interactor.SceneInteractor;
+import com.android.systemui.scene.domain.startable.KeyguardStateCallbackStartable;
 import com.android.systemui.scene.shared.flag.SceneContainerFlag;
 import com.android.systemui.scene.shared.model.Scenes;
 import com.android.systemui.settings.DisplayTracker;
@@ -122,6 +123,7 @@
     private final KeyguardInteractor mKeyguardInteractor;
     private final Lazy<SceneInteractor> mSceneInteractorLazy;
     private final Executor mMainExecutor;
+    private final Lazy<KeyguardStateCallbackStartable> mKeyguardStateCallbackStartableLazy;
 
     private static RemoteAnimationTarget[] wrap(TransitionInfo info, boolean wallpapers,
             SurfaceControl.Transaction t, ArrayMap<SurfaceControl, SurfaceControl> leashMap,
@@ -341,7 +343,8 @@
             Lazy<SceneInteractor> sceneInteractorLazy,
             @Main Executor mainExecutor,
             KeyguardInteractor keyguardInteractor,
-            KeyguardEnabledInteractor keyguardEnabledInteractor) {
+            KeyguardEnabledInteractor keyguardEnabledInteractor,
+            Lazy<KeyguardStateCallbackStartable> keyguardStateCallbackStartableLazy) {
         super();
         mKeyguardViewMediator = keyguardViewMediator;
         mKeyguardLifecyclesDispatcher = keyguardLifecyclesDispatcher;
@@ -353,6 +356,7 @@
         mKeyguardInteractor = keyguardInteractor;
         mSceneInteractorLazy = sceneInteractorLazy;
         mMainExecutor = mainExecutor;
+        mKeyguardStateCallbackStartableLazy = keyguardStateCallbackStartableLazy;
 
         if (KeyguardWmStateRefactor.isEnabled()) {
             WindowManagerLockscreenVisibilityViewBinder.bind(
@@ -440,7 +444,11 @@
         public void addStateMonitorCallback(IKeyguardStateCallback callback) {
             trace("addStateMonitorCallback");
             checkPermission();
-            mKeyguardViewMediator.addStateMonitorCallback(callback);
+            if (SceneContainerFlag.isEnabled()) {
+                mKeyguardStateCallbackStartableLazy.get().addCallback(callback);
+            } else {
+                mKeyguardViewMediator.addStateMonitorCallback(callback);
+            }
         }
 
         @Override // Binder interface
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index b70dbe2..36b7ed2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -3801,6 +3801,10 @@
     }
 
     private void notifyDefaultDisplayCallbacks(boolean showing) {
+        if (SceneContainerFlag.isEnabled()) {
+            return;
+        }
+
         // TODO(b/140053364)
         whitelistIpcs(() -> {
             int size = mKeyguardStateCallbacks.size();
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt
index 882f231..dd3e619 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt
@@ -188,35 +188,33 @@
         )
 
     private val isFingerprintEnrolled: Flow<Boolean> =
-        selectedUserId
-            .flatMapLatest { currentUserId ->
-                conflatedCallbackFlow {
-                    val callback =
-                        object : AuthController.Callback {
-                            override fun onEnrollmentsChanged(
-                                sensorBiometricType: BiometricType,
-                                userId: Int,
-                                hasEnrollments: Boolean
-                            ) {
-                                if (sensorBiometricType.isFingerprint && userId == currentUserId) {
-                                    trySendWithFailureLogging(
-                                        hasEnrollments,
-                                        TAG,
-                                        "update fpEnrollment"
-                                    )
-                                }
+        selectedUserId.flatMapLatest { currentUserId ->
+            conflatedCallbackFlow {
+                val callback =
+                    object : AuthController.Callback {
+                        override fun onEnrollmentsChanged(
+                            sensorBiometricType: BiometricType,
+                            userId: Int,
+                            hasEnrollments: Boolean
+                        ) {
+                            if (sensorBiometricType.isFingerprint && userId == currentUserId) {
+                                trySendWithFailureLogging(
+                                    hasEnrollments,
+                                    TAG,
+                                    "update fpEnrollment"
+                                )
                             }
                         }
-                    authController.addCallback(callback)
-                    awaitClose { authController.removeCallback(callback) }
-                }
+                    }
+                authController.addCallback(callback)
+                trySendWithFailureLogging(
+                    authController.isFingerprintEnrolled(currentUserId),
+                    TAG,
+                    "Initial value of fingerprint enrollment"
+                )
+                awaitClose { authController.removeCallback(callback) }
             }
-            .stateIn(
-                scope,
-                started = SharingStarted.Eagerly,
-                initialValue =
-                    authController.isFingerprintEnrolled(userRepository.getSelectedUserInfo().id)
-            )
+        }
 
     private val isFaceEnrolled: Flow<Boolean> =
         selectedUserId.flatMapLatest { selectedUserId: Int ->
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/TrustRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/TrustRepository.kt
index 6522439..bd5d096 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/TrustRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/TrustRepository.kt
@@ -23,11 +23,13 @@
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.keyguard.shared.model.ActiveUnlockModel
 import com.android.systemui.keyguard.shared.model.TrustManagedModel
 import com.android.systemui.keyguard.shared.model.TrustModel
 import com.android.systemui.user.data.repository.UserRepository
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.channels.awaitClose
@@ -44,6 +46,7 @@
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.shareIn
 import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.withContext
 
 /** Encapsulates any state relevant to trust agents and trust grants. */
 interface TrustRepository {
@@ -51,7 +54,7 @@
     val isCurrentUserTrustUsuallyManaged: StateFlow<Boolean>
 
     /** Flow representing whether the current user is trusted. */
-    val isCurrentUserTrusted: Flow<Boolean>
+    val isCurrentUserTrusted: StateFlow<Boolean>
 
     /** Flow representing whether active unlock is running for the current user. */
     val isCurrentUserActiveUnlockRunning: Flow<Boolean>
@@ -63,6 +66,9 @@
 
     /** A trust agent is requesting to dismiss the keyguard from a trust change. */
     val trustAgentRequestingToDismissKeyguard: Flow<TrustModel>
+
+    /** Reports a keyguard visibility change. */
+    suspend fun reportKeyguardShowingChanged()
 }
 
 @OptIn(ExperimentalCoroutinesApi::class)
@@ -71,6 +77,7 @@
 @Inject
 constructor(
     @Application private val applicationScope: CoroutineScope,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
     private val userRepository: UserRepository,
     private val trustManager: TrustManager,
     private val logger: TrustRepositoryLogger,
@@ -191,11 +198,26 @@
     private fun isUserTrustManaged(userId: Int) =
         trustManagedForUser[userId]?.isTrustManaged ?: false
 
-    override val isCurrentUserTrusted: Flow<Boolean>
+    override val isCurrentUserTrusted: StateFlow<Boolean>
         get() =
             combine(trust, userRepository.selectedUserInfo, ::Pair)
-                .map { latestTrustModelForUser[it.second.id]?.isTrusted ?: false }
+                .map { isCurrentUserTrusted(it.second.id) }
                 .distinctUntilChanged()
                 .onEach { logger.isCurrentUserTrusted(it) }
                 .onStart { emit(false) }
+                .stateIn(
+                    scope = applicationScope,
+                    started = SharingStarted.WhileSubscribed(),
+                    initialValue = isCurrentUserTrusted(),
+                )
+
+    private fun isCurrentUserTrusted(
+        selectedUserId: Int = userRepository.getSelectedUserInfo().id
+    ): Boolean {
+        return latestTrustModelForUser[selectedUserId]?.isTrusted ?: false
+    }
+
+    override suspend fun reportKeyguardShowingChanged() {
+        withContext(backgroundDispatcher) { trustManager.reportKeyguardShowingChanged() }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
index f3692bd..2f40c99 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
@@ -17,8 +17,10 @@
 package com.android.systemui.keyguard.domain.interactor
 
 import android.animation.ValueAnimator
+import android.app.DreamManager
 import com.android.app.animation.Interpolators
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
@@ -51,8 +53,10 @@
     keyguardInteractor: KeyguardInteractor,
     powerInteractor: PowerInteractor,
     private val communalInteractor: CommunalInteractor,
+    private val communalSceneInteractor: CommunalSceneInteractor,
     keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
     val deviceEntryRepository: DeviceEntryRepository,
+    private val dreamManager: DreamManager,
 ) :
     TransitionInteractor(
         fromState = KeyguardState.DOZING,
@@ -119,7 +123,8 @@
                 .filterRelevantKeyguardStateAnd { isAwake -> isAwake }
                 .sample(
                     keyguardInteractor.isKeyguardOccluded,
-                    communalInteractor.isIdleOnCommunal,
+                    communalInteractor.isCommunalAvailable,
+                    communalSceneInteractor.isIdleOnCommunal,
                     canTransitionToGoneOnWake,
                     keyguardInteractor.primaryBouncerShowing,
                 )
@@ -127,6 +132,7 @@
                     (
                         _,
                         occluded,
+                        isCommunalAvailable,
                         isIdleOnCommunal,
                         canTransitionToGoneOnWake,
                         primaryBouncerShowing) ->
@@ -141,6 +147,10 @@
                             KeyguardState.OCCLUDED
                         } else if (isIdleOnCommunal) {
                             KeyguardState.GLANCEABLE_HUB
+                        } else if (isCommunalAvailable && dreamManager.canStartDreaming(true)) {
+                            // This case handles tapping the power button to transition through
+                            // dream -> off -> hub.
+                            KeyguardState.GLANCEABLE_HUB
                         } else {
                             KeyguardState.LOCKSCREEN
                         }
@@ -159,7 +169,8 @@
             powerInteractor.detailedWakefulness
                 .filterRelevantKeyguardStateAnd { it.isAwake() }
                 .sample(
-                    communalInteractor.isIdleOnCommunal,
+                    communalInteractor.isCommunalAvailable,
+                    communalSceneInteractor.isIdleOnCommunal,
                     keyguardInteractor.biometricUnlockState,
                     canTransitionToGoneOnWake,
                     keyguardInteractor.primaryBouncerShowing,
@@ -167,6 +178,7 @@
                 .collect {
                     (
                         _,
+                        isCommunalAvailable,
                         isIdleOnCommunal,
                         biometricUnlockState,
                         canDismissLockscreen,
@@ -188,6 +200,10 @@
                                 KeyguardState.PRIMARY_BOUNCER
                             } else if (isIdleOnCommunal) {
                                 KeyguardState.GLANCEABLE_HUB
+                            } else if (isCommunalAvailable && dreamManager.canStartDreaming(true)) {
+                                // This case handles tapping the power button to transition through
+                                // dream -> off -> hub.
+                                KeyguardState.GLANCEABLE_HUB
                             } else {
                                 KeyguardState.LOCKSCREEN
                             },
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardEnabledInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardEnabledInteractor.kt
index 8dede01..9cc0b3c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardEnabledInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardEnabledInteractor.kt
@@ -25,6 +25,7 @@
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.launch
@@ -48,6 +49,37 @@
     transitionInteractor: KeyguardTransitionInteractor,
 ) {
 
+    /**
+     * Whether the keyguard is enabled, per [KeyguardService]. If the keyguard is not enabled, the
+     * lockscreen cannot be shown and the device will go from AOD/DOZING directly to GONE.
+     *
+     * Keyguard can be disabled by selecting Security: "None" in settings, or by apps that hold
+     * permission to do so (such as Phone).
+     *
+     * If the keyguard is disabled while we're locked, we will transition to GONE unless we're in
+     * lockdown mode. If the keyguard is re-enabled, we'll transition back to LOCKSCREEN if we were
+     * locked when it was disabled.
+     */
+    val isKeyguardEnabled: StateFlow<Boolean> = repository.isKeyguardEnabled
+
+    /**
+     * Whether we need to show the keyguard when the keyguard is re-enabled, since we hid it when it
+     * became disabled.
+     */
+    val showKeyguardWhenReenabled: Flow<Boolean> =
+        repository.isKeyguardEnabled
+            // Whenever the keyguard is disabled...
+            .filter { enabled -> !enabled }
+            .sampleCombine(
+                transitionInteractor.currentTransitionInfoInternal,
+                biometricSettingsRepository.isCurrentUserInLockdown
+            )
+            .map { (_, transitionInfo, inLockdown) ->
+                // ...we hide the keyguard, if it's showing and we're not in lockdown. In that case,
+                // we want to remember that and re-show it when keyguard is enabled again.
+                transitionInfo.to != KeyguardState.GONE && !inLockdown
+            }
+
     init {
         /**
          * Whenever keyguard is disabled, transition to GONE unless we're in lockdown or already
@@ -68,24 +100,6 @@
         }
     }
 
-    /**
-     * Whether we need to show the keyguard when the keyguard is re-enabled, since we hid it when it
-     * became disabled.
-     */
-    val showKeyguardWhenReenabled: Flow<Boolean> =
-        repository.isKeyguardEnabled
-            // Whenever the keyguard is disabled...
-            .filter { enabled -> !enabled }
-            .sampleCombine(
-                transitionInteractor.currentTransitionInfoInternal,
-                biometricSettingsRepository.isCurrentUserInLockdown
-            )
-            .map { (_, transitionInfo, inLockdown) ->
-                // ...we hide the keyguard, if it's showing and we're not in lockdown. In that case,
-                // we want to remember that and re-show it when keyguard is enabled again.
-                transitionInfo.to != KeyguardState.GONE && !inLockdown
-            }
-
     fun notifyKeyguardEnabled(enabled: Boolean) {
         repository.setKeyguardEnabled(enabled)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TrustInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TrustInteractor.kt
index 2ff6e16..73248bb 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TrustInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TrustInteractor.kt
@@ -17,14 +17,21 @@
 package com.android.systemui.keyguard.domain.interactor
 
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.keyguard.data.repository.TrustRepository
 import javax.inject.Inject
-import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
 
 /** Encapsulates any state relevant to trust agents and trust grants. */
 @SysUISingleton
-class TrustInteractor @Inject constructor(repository: TrustRepository) {
+class TrustInteractor
+@Inject
+constructor(
+    @Application private val applicationScope: CoroutineScope,
+    private val repository: TrustRepository,
+) {
     /**
      * Whether the current user has a trust agent enabled. This is true if the user has at least one
      * trust agent enabled in settings.
@@ -39,5 +46,10 @@
     val isTrustAgentCurrentlyAllowed: StateFlow<Boolean> = repository.isCurrentUserTrustManaged
 
     /** Whether the current user is trusted by any of the enabled trust agents. */
-    val isTrusted: Flow<Boolean> = repository.isCurrentUserTrusted
+    val isTrusted: StateFlow<Boolean> = repository.isCurrentUserTrusted
+
+    /** Reports a keyguard visibility change. */
+    fun reportKeyguardShowingChanged() {
+        applicationScope.launch { repository.reportKeyguardShowingChanged() }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt
index 3e4253b..ba94f45 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt
@@ -133,6 +133,12 @@
                             configurationBasedDimensions.value = loadFromResources(view)
                         }
                     }
+
+                    launch("$TAG#viewModel.visible") {
+                        viewModel.visible.collect { visible ->
+                            indicationController.setVisible(visible)
+                        }
+                    }
                 }
             }
         return disposables
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt
index a758720d..609b571d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.keyguard.ui.viewmodel
 
 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.doze.util.BurnInHelperWrapper
@@ -28,9 +29,10 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.BurnInModel
 import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.StatusBarState
 import com.android.systemui.res.R
+import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf
 import javax.inject.Inject
-import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
@@ -44,14 +46,15 @@
 @Inject
 constructor(
     private val keyguardInteractor: KeyguardInteractor,
-    private val bottomAreaInteractor: KeyguardBottomAreaInteractor,
+    bottomAreaInteractor: KeyguardBottomAreaInteractor,
     keyguardBottomAreaViewModel: KeyguardBottomAreaViewModel,
     private val burnInHelperWrapper: BurnInHelperWrapper,
-    private val burnInInteractor: BurnInInteractor,
-    private val shortcutsCombinedViewModel: KeyguardQuickAffordancesCombinedViewModel,
+    burnInInteractor: BurnInInteractor,
+    shortcutsCombinedViewModel: KeyguardQuickAffordancesCombinedViewModel,
     configurationInteractor: ConfigurationInteractor,
     keyguardTransitionInteractor: KeyguardTransitionInteractor,
-    @Background private val backgroundCoroutineContext: CoroutineContext,
+    communalSceneInteractor: CommunalSceneInteractor,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
     @Main private val mainDispatcher: CoroutineDispatcher,
 ) {
 
@@ -61,6 +64,13 @@
     /** An observable for the alpha level for the entire bottom area. */
     val alpha: Flow<Float> = keyguardBottomAreaViewModel.alpha
 
+    /** An observable for the visibility value for the indication area view. */
+    val visible: Flow<Boolean> =
+        anyOf(
+            keyguardInteractor.statusBarState.map { state -> state == StatusBarState.KEYGUARD },
+            communalSceneInteractor.isCommunalVisible
+        )
+
     /** An observable for whether the indication area should be padded. */
     val isIndicationAreaPadded: Flow<Boolean> =
         if (KeyguardBottomAreaRefactor.isEnabled) {
@@ -97,7 +107,7 @@
                 )
             }
             .distinctUntilChanged()
-            .flowOn(backgroundCoroutineContext)
+            .flowOn(backgroundDispatcher)
 
     /** An observable for the x-offset by which the indication area should be translated. */
     val indicationAreaTranslationX: Flow<Float> =
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt b/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt
index a2d7fb1..e8a2334 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
 import com.android.systemui.media.controls.shared.model.SmartspaceMediaLoadingModel
 import com.android.systemui.media.controls.util.MediaSmartspaceLogger
+import com.android.systemui.media.controls.util.MediaSmartspaceLogger.Companion.SMARTSPACE_CARD_DISMISS_EVENT
 import com.android.systemui.media.controls.util.SmallHash
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.time.SystemClock
@@ -362,6 +363,77 @@
         return _smartspaceMediaData.value.isActive
     }
 
+    /** Log user event on media card if smartspace logging is enabled. */
+    fun logSmartspaceCardUserEvent(
+        eventId: Int,
+        location: Int,
+        interactedSubCardRank: Int = 0,
+        interactedSubCardCardinality: Int = 0,
+        instanceId: InstanceId? = null,
+        isRec: Boolean = false
+    ) {
+        _currentMedia.value.forEachIndexed { index, mediaCommonModel ->
+            when (mediaCommonModel) {
+                is MediaCommonModel.MediaControl -> {
+                    if (mediaCommonModel.mediaLoadedModel.instanceId == instanceId) {
+                        if (isSmartspaceLoggingEnabled(mediaCommonModel, index)) {
+                            logSmartspaceMediaCardUserEvent(
+                                instanceId,
+                                index,
+                                eventId,
+                                location,
+                                mediaCommonModel.mediaLoadedModel.isSsReactivated,
+                                interactedSubCardRank,
+                                interactedSubCardCardinality
+                            )
+                        }
+                        return
+                    }
+                }
+                is MediaCommonModel.MediaRecommendations -> {
+                    if (isRec) {
+                        if (isSmartspaceLoggingEnabled(mediaCommonModel, index)) {
+                            logSmarspaceRecommendationCardUserEvent(
+                                eventId,
+                                location,
+                                index,
+                                interactedSubCardRank,
+                                interactedSubCardCardinality
+                            )
+                        }
+                        return
+                    }
+                }
+            }
+        }
+    }
+
+    /** Log media and recommendation cards dismissal if smartspace logging is enabled for each. */
+    fun logSmartspaceCardsOnSwipeToDismiss(location: Int) {
+        _currentMedia.value.forEachIndexed { index, mediaCommonModel ->
+            if (isSmartspaceLoggingEnabled(mediaCommonModel, index)) {
+                when (mediaCommonModel) {
+                    is MediaCommonModel.MediaControl ->
+                        logSmartspaceMediaCardUserEvent(
+                            mediaCommonModel.mediaLoadedModel.instanceId,
+                            index,
+                            SMARTSPACE_CARD_DISMISS_EVENT,
+                            location,
+                            mediaCommonModel.mediaLoadedModel.isSsReactivated,
+                            isSwipeToDismiss = true
+                        )
+                    is MediaCommonModel.MediaRecommendations ->
+                        logSmarspaceRecommendationCardUserEvent(
+                            SMARTSPACE_CARD_DISMISS_EVENT,
+                            location,
+                            index,
+                            isSwipeToDismiss = true
+                        )
+                }
+            }
+        }
+    }
+
     private fun canBeRemoved(data: MediaData): Boolean {
         return data.isPlaying?.let { !it } ?: data.isClearable && !data.active
     }
@@ -394,6 +466,54 @@
         }
     }
 
+    private fun logSmartspaceMediaCardUserEvent(
+        instanceId: InstanceId,
+        index: Int,
+        eventId: Int,
+        location: Int,
+        isReactivated: Boolean,
+        interactedSubCardRank: Int = 0,
+        interactedSubCardCardinality: Int = 0,
+        isSwipeToDismiss: Boolean = false
+    ) {
+        _selectedUserEntries.value[instanceId]?.let {
+            smartspaceLogger.logSmartspaceCardUIEvent(
+                eventId,
+                it.smartspaceId,
+                it.appUid,
+                location,
+                _currentMedia.value.size,
+                isSsReactivated = isReactivated,
+                interactedSubcardRank = interactedSubCardRank,
+                interactedSubcardCardinality = interactedSubCardCardinality,
+                rank = index,
+                isSwipeToDismiss = isSwipeToDismiss,
+            )
+        }
+    }
+
+    private fun logSmarspaceRecommendationCardUserEvent(
+        eventId: Int,
+        location: Int,
+        index: Int,
+        interactedSubCardRank: Int = 0,
+        interactedSubCardCardinality: Int = 0,
+        isSwipeToDismiss: Boolean = false
+    ) {
+        smartspaceLogger.logSmartspaceCardUIEvent(
+            eventId,
+            SmallHash.hash(_smartspaceMediaData.value.targetId),
+            _smartspaceMediaData.value.getUid(applicationContext),
+            location,
+            _currentMedia.value.size,
+            isRecommendationCard = true,
+            interactedSubcardRank = interactedSubCardRank,
+            interactedSubcardCardinality = interactedSubCardCardinality,
+            rank = index,
+            isSwipeToDismiss = isSwipeToDismiss,
+        )
+    }
+
     private fun isSmartspaceLoggingEnabled(commonModel: MediaCommonModel, index: Int): Boolean {
         return sortedMedia.size > index &&
             (_smartspaceMediaData.value.expiryTimeMs != 0L ||
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaSmartspaceLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaSmartspaceLogger.kt
index 01fbf4a..d1184b6 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaSmartspaceLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaSmartspaceLogger.kt
@@ -18,6 +18,8 @@
 
 import android.util.Log
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.shared.system.SysUiStatsLog
 import javax.inject.Inject
 
@@ -85,6 +87,8 @@
         cardinality: Int,
         isRecommendationCard: Boolean = false,
         isSsReactivated: Boolean = false,
+        interactedSubcardRank: Int = 0,
+        interactedSubcardCardinality: Int = 0,
         rank: Int = 0,
         isSwipeToDismiss: Boolean = false,
     ) {
@@ -96,6 +100,8 @@
             cardinality,
             isRecommendationCard,
             isSsReactivated,
+            interactedSubcardRank,
+            interactedSubcardCardinality,
             rank = rank,
             isSwipeToDismiss = isSwipeToDismiss,
         )
@@ -187,5 +193,27 @@
         const val SMARTSPACE_CARD_CLICK_EVENT = 760
         const val SMARTSPACE_CARD_DISMISS_EVENT = 761
         const val SMARTSPACE_CARD_SEEN_EVENT = 800
+
+        /**
+         * Get the location of media view given [currentEndLocation]
+         *
+         * @return location used for Smartspace logging
+         */
+        fun getSurface(location: Int): Int {
+            SceneContainerFlag.isUnexpectedlyInLegacyMode()
+            return when (location) {
+                MediaHierarchyManager.LOCATION_QQS,
+                MediaHierarchyManager.LOCATION_QS -> {
+                    SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE
+                }
+                MediaHierarchyManager.LOCATION_LOCKSCREEN -> {
+                    SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN
+                }
+                MediaHierarchyManager.LOCATION_DREAM_OVERLAY -> {
+                    SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY
+                }
+                else -> SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DEFAULT_SURFACE
+            }
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
index 80c4379..2e5ff9d 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
@@ -56,6 +56,8 @@
 import android.view.accessibility.AccessibilityManager;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
 
 import com.android.internal.accessibility.common.ShortcutConstants;
 import com.android.systemui.Dumpable;
@@ -64,12 +66,14 @@
 import com.android.systemui.accessibility.SystemActions;
 import com.android.systemui.assist.AssistManager;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler;
 import com.android.systemui.recents.OverviewProxyService;
 import com.android.systemui.settings.DisplayTracker;
 import com.android.systemui.settings.UserTracker;
+import com.android.systemui.shared.rotation.RotationPolicyUtil;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
@@ -108,6 +112,7 @@
 
     private final Handler mHandler = new Handler(Looper.getMainLooper());
     private final Executor mMainExecutor;
+    private final Handler mBgHandler;
     private final AccessibilityManager mAccessibilityManager;
     private final Lazy<AssistManager> mAssistManagerLazy;
     private final Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy;
@@ -160,13 +165,15 @@
 
     // Listens for changes to display rotation
     private final IRotationWatcher mRotationWatcher = new IRotationWatcher.Stub() {
+        @WorkerThread
         @Override
         public void onRotationChanged(final int rotation) {
             // We need this to be scheduled as early as possible to beat the redrawing of
             // window in response to the orientation change.
+            @Nullable Boolean isRotationLocked = RotationPolicyUtil.isRotationLocked(mContext);
             mHandler.postAtFrontOfQueue(() -> {
                 mRotationWatcherRotation = rotation;
-                dispatchRotationChanged(rotation);
+                dispatchRotationChanged(rotation, isRotationLocked);
             });
         }
     };
@@ -194,7 +201,8 @@
             ConfigurationController configurationController,
             DumpManager dumpManager,
             CommandQueue commandQueue,
-            @Main Executor mainExecutor) {
+            @Main Executor mainExecutor,
+            @Background Handler bgHandler) {
         // b/319489709: This component shouldn't be running for a non-primary user
         if (!Process.myUserHandle().equals(UserHandle.SYSTEM)) {
             Log.wtf(TAG, "Unexpected initialization for non-primary user", new Throwable());
@@ -215,6 +223,7 @@
         mDefaultDisplayId = displayTracker.getDefaultDisplayId();
         mEdgeBackGestureHandler = edgeBackGestureHandlerFactory.create(context);
         mMainExecutor = mainExecutor;
+        mBgHandler = bgHandler;
 
         mNavBarMode = navigationModeController.addListener(this);
         mCommandQueue.addCallback(this);
@@ -322,7 +331,13 @@
             listener.updateAssistantAvailable(mAssistantAvailable, mLongPressHomeEnabled);
         }
         listener.updateWallpaperVisibility(mWallpaperVisible, mDefaultDisplayId);
-        listener.updateRotationWatcherState(mRotationWatcherRotation);
+
+        mBgHandler.post(() -> {
+            Boolean isRotationLocked = RotationPolicyUtil.isRotationLocked(mContext);
+            mMainExecutor.execute(
+                    () -> listener.updateRotationWatcherState(
+                            mRotationWatcherRotation, isRotationLocked));
+        });
     }
 
     /**
@@ -526,9 +541,9 @@
         }
     }
 
-    private void dispatchRotationChanged(int rotation) {
+    private void dispatchRotationChanged(int rotation, @Nullable Boolean isRotationLocked) {
         for (NavbarTaskbarStateUpdater listener : mStateListeners) {
-            listener.updateRotationWatcherState(rotation);
+            listener.updateRotationWatcherState(rotation, isRotationLocked);
         }
     }
 
@@ -544,7 +559,7 @@
         void updateAccessibilityServicesState();
         void updateAssistantAvailable(boolean available, boolean longPressHomeEnabled);
         default void updateWallpaperVisibility(boolean visible, int displayId) {}
-        default void updateRotationWatcherState(int rotation) {}
+        default void updateRotationWatcherState(int rotation, @Nullable Boolean isRotationLocked) {}
     }
 
     /** Data class to help Taskbar/Navbar initiate state correctly when switching between the two.*/
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
index 07289cb..69aa450 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
@@ -136,6 +136,7 @@
 import com.android.systemui.shared.navigationbar.RegionSamplingHelper;
 import com.android.systemui.shared.recents.utilities.Utilities;
 import com.android.systemui.shared.rotation.RotationButtonController;
+import com.android.systemui.shared.rotation.RotationPolicyUtil;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.shared.system.SysUiStatsLog;
 import com.android.systemui.shared.system.TaskStackChangeListener;
@@ -366,9 +367,11 @@
                 }
 
                 @Override
-                public void updateRotationWatcherState(int rotation) {
+                public void updateRotationWatcherState(
+                        int rotation, @Nullable Boolean isRotationLocked) {
                     if (mIsOnDefaultDisplay && mView != null) {
-                        mView.getRotationButtonController().onRotationWatcherChanged(rotation);
+                        RotationButtonController controller = mView.getRotationButtonController();
+                        controller.onRotationWatcherChanged(rotation, isRotationLocked);
                         if (mView.needsReorient(rotation)) {
                             repositionNavigationBar(rotation);
                         }
@@ -819,8 +822,9 @@
 
             // Reset user rotation pref to match that of the WindowManager if starting in locked
             // mode. This will automatically happen when switching from auto-rotate to locked mode.
-            if (display != null && rotationButtonController.isRotationLocked()) {
-                rotationButtonController.setRotationLockedAtAngle(
+            @Nullable Boolean isRotationLocked = RotationPolicyUtil.isRotationLocked(mContext);
+            if (display != null && isRotationLocked) {
+                rotationButtonController.setRotationLockedAtAngle(isRotationLocked,
                         display.getRotation(), /* caller= */ "NavigationBar#onViewAttached");
             }
         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java
index b177b0b..1c2a087 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java
@@ -23,6 +23,7 @@
 import static com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler.DEBUG_MISSING_GESTURE_TAG;
 import static com.android.systemui.shared.recents.utilities.Utilities.isLargeScreen;
 import static com.android.wm.shell.Flags.enableTaskbarNavbarUnification;
+import static com.android.wm.shell.Flags.enableTaskbarOnPhones;
 
 import android.content.Context;
 import android.content.pm.ActivityInfo;
@@ -285,8 +286,10 @@
 
     @VisibleForTesting
     boolean supportsTaskbar() {
-        // Enable for tablets, unfolded state on a foldable device or (non handheld AND flag is set)
-        return mIsLargeScreen || (!mIsPhone && enableTaskbarNavbarUnification());
+        // Enable for tablets, unfolded state on a foldable device, (non handheld AND flag is set),
+        // or handheld when enableTaskbarOnPhones() returns true.
+        boolean foldedOrPhone = !mIsPhone || enableTaskbarOnPhones();
+        return mIsLargeScreen || (foldedOrPhone && enableTaskbarNavbarUnification());
     }
 
     private final CommandQueue.Callbacks mCommandQueueCallbacks = new CommandQueue.Callbacks() {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
index 24b7a01..96df728 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
@@ -224,6 +224,10 @@
         });
     }
 
+    boolean isBound() {
+        return mBound.get();
+    }
+
     @WorkerThread
     private void setBindService(boolean bind) {
         if (mBound.get() && mUnbindImmediate.get()) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java
index 6bc5095..d10471d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java
@@ -217,7 +217,11 @@
             Log.e(TAG, "Service already bound");
             return;
         }
-        mPendingBind = true;
+        if (!mStateManager.isBound()) {
+            // If we are bound, we don't need to set a pending bind. There's either one already or
+            // we are fully bound.
+            mPendingBind = true;
+        }
         mBound = true;
         mJustBound = true;
         mHandler.postDelayed(mJustBoundOver, MIN_BIND_TIME);
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepositoryKosmos.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PaginatedBaseLayoutType.kt
similarity index 76%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepositoryKosmos.kt
copy to packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PaginatedBaseLayoutType.kt
index d8af3fa..0285cbd 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepositoryKosmos.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PaginatedBaseLayoutType.kt
@@ -14,8 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.systemui.qs.panels.data.repository
+package com.android.systemui.qs.panels.dagger
 
-import com.android.systemui.kosmos.Kosmos
+import javax.inject.Qualifier
 
-val Kosmos.infiniteGridSizeRepository by Kosmos.Fixture { InfiniteGridSizeRepository() }
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class PaginatedBaseLayoutType
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PanelsModule.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PanelsModule.kt
index 7b67993..c214361 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PanelsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PanelsModule.kt
@@ -30,18 +30,21 @@
 import com.android.systemui.qs.panels.shared.model.GridLayoutType
 import com.android.systemui.qs.panels.shared.model.IconLabelVisibilityLog
 import com.android.systemui.qs.panels.shared.model.InfiniteGridLayoutType
+import com.android.systemui.qs.panels.shared.model.PaginatedGridLayoutType
 import com.android.systemui.qs.panels.shared.model.PartitionedGridLayoutType
 import com.android.systemui.qs.panels.shared.model.StretchedGridLayoutType
 import com.android.systemui.qs.panels.ui.compose.GridLayout
 import com.android.systemui.qs.panels.ui.compose.InfiniteGridLayout
+import com.android.systemui.qs.panels.ui.compose.PaginatableGridLayout
+import com.android.systemui.qs.panels.ui.compose.PaginatedGridLayout
 import com.android.systemui.qs.panels.ui.compose.PartitionedGridLayout
 import com.android.systemui.qs.panels.ui.compose.StretchedGridLayout
+import com.android.systemui.qs.panels.ui.viewmodel.FixedColumnsSizeViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.FixedColumnsSizeViewModelImpl
 import com.android.systemui.qs.panels.ui.viewmodel.IconLabelVisibilityViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.IconLabelVisibilityViewModelImpl
 import com.android.systemui.qs.panels.ui.viewmodel.IconTilesViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.IconTilesViewModelImpl
-import com.android.systemui.qs.panels.ui.viewmodel.InfiniteGridSizeViewModel
-import com.android.systemui.qs.panels.ui.viewmodel.InfiniteGridSizeViewModelImpl
 import dagger.Binds
 import dagger.Module
 import dagger.Provides
@@ -62,14 +65,24 @@
 
     @Binds fun bindIconTilesViewModel(impl: IconTilesViewModelImpl): IconTilesViewModel
 
-    @Binds fun bindGridSizeViewModel(impl: InfiniteGridSizeViewModelImpl): InfiniteGridSizeViewModel
+    @Binds fun bindGridSizeViewModel(impl: FixedColumnsSizeViewModelImpl): FixedColumnsSizeViewModel
 
     @Binds
     fun bindIconLabelVisibilityViewModel(
         impl: IconLabelVisibilityViewModelImpl
     ): IconLabelVisibilityViewModel
 
-    @Binds @Named("Default") fun bindDefaultGridLayout(impl: PartitionedGridLayout): GridLayout
+    @Binds
+    @PaginatedBaseLayoutType
+    fun bindPaginatedBaseGridLayout(impl: PartitionedGridLayout): PaginatableGridLayout
+
+    @Binds
+    @PaginatedBaseLayoutType
+    fun bindPaginatedBaseConsistencyInteractor(
+        impl: NoopGridConsistencyInteractor
+    ): GridTypeConsistencyInteractor
+
+    @Binds @Named("Default") fun bindDefaultGridLayout(impl: PaginatedGridLayout): GridLayout
 
     companion object {
         @Provides
@@ -109,6 +122,14 @@
         }
 
         @Provides
+        @IntoSet
+        fun providePaginatedGridLayout(
+            gridLayout: PaginatedGridLayout
+        ): Pair<GridLayoutType, GridLayout> {
+            return Pair(PaginatedGridLayoutType, gridLayout)
+        }
+
+        @Provides
         fun provideGridLayoutMap(
             entries: Set<@JvmSuppressWildcards Pair<GridLayoutType, GridLayout>>
         ): Map<GridLayoutType, GridLayout> {
@@ -147,6 +168,14 @@
         }
 
         @Provides
+        @IntoSet
+        fun providePaginatedGridConsistencyInteractor(
+            @PaginatedBaseLayoutType consistencyInteractor: GridTypeConsistencyInteractor,
+        ): Pair<GridLayoutType, GridTypeConsistencyInteractor> {
+            return Pair(PaginatedGridLayoutType, consistencyInteractor)
+        }
+
+        @Provides
         fun provideGridConsistencyInteractorMap(
             entries: Set<@JvmSuppressWildcards Pair<GridLayoutType, GridTypeConsistencyInteractor>>
         ): Map<GridLayoutType, GridTypeConsistencyInteractor> {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/FixedColumnsRepository.kt
similarity index 94%
rename from packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepository.kt
rename to packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/FixedColumnsRepository.kt
index 43ccdf66..32ce973 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/FixedColumnsRepository.kt
@@ -23,7 +23,7 @@
 import kotlinx.coroutines.flow.asStateFlow
 
 @SysUISingleton
-class InfiniteGridSizeRepository @Inject constructor() {
+class FixedColumnsRepository @Inject constructor() {
     // Number of columns in the narrowest state for consistency
     private val _columns = MutableStateFlow(4)
     val columns: StateFlow<Int> = _columns.asStateFlow()
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/GridLayoutTypeRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/GridLayoutTypeRepository.kt
index 44d8688..47c4ffd 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/GridLayoutTypeRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/GridLayoutTypeRepository.kt
@@ -18,7 +18,7 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.qs.panels.shared.model.GridLayoutType
-import com.android.systemui.qs.panels.shared.model.PartitionedGridLayoutType
+import com.android.systemui.qs.panels.shared.model.PaginatedGridLayoutType
 import javax.inject.Inject
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
@@ -26,13 +26,14 @@
 
 interface GridLayoutTypeRepository {
     val layout: StateFlow<GridLayoutType>
+
     fun setLayout(type: GridLayoutType)
 }
 
 @SysUISingleton
 class GridLayoutTypeRepositoryImpl @Inject constructor() : GridLayoutTypeRepository {
     private val _layout: MutableStateFlow<GridLayoutType> =
-        MutableStateFlow(PartitionedGridLayoutType)
+        MutableStateFlow(PaginatedGridLayoutType)
     override val layout = _layout.asStateFlow()
 
     override fun setLayout(type: GridLayoutType) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/PaginatedGridRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/PaginatedGridRepository.kt
new file mode 100644
index 0000000..26b2e2b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/PaginatedGridRepository.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.qs.panels.data.repository
+
+import android.content.res.Resources
+import com.android.systemui.common.ui.data.repository.ConfigurationRepository
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.res.R
+import com.android.systemui.util.kotlin.emitOnStart
+import javax.inject.Inject
+import kotlinx.coroutines.flow.map
+
+/**
+ * Provides the number of [rows] to use with a paginated grid, by tracking the resource
+ * [R.integer.quick_settings_max_rows].
+ */
+@SysUISingleton
+class PaginatedGridRepository
+@Inject
+constructor(
+    @Main private val resources: Resources,
+    configurationRepository: ConfigurationRepository,
+) {
+    val rows =
+        configurationRepository.onConfigurationChange.emitOnStart().map {
+            resources.getInteger(R.integer.quick_settings_max_rows)
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridSizeInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/FixedColumnsSizeInteractor.kt
similarity index 83%
rename from packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridSizeInteractor.kt
rename to packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/FixedColumnsSizeInteractor.kt
index 13c6072..9591002 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridSizeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/FixedColumnsSizeInteractor.kt
@@ -17,11 +17,11 @@
 package com.android.systemui.qs.panels.domain.interactor
 
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.qs.panels.data.repository.InfiniteGridSizeRepository
+import com.android.systemui.qs.panels.data.repository.FixedColumnsRepository
 import javax.inject.Inject
 import kotlinx.coroutines.flow.StateFlow
 
 @SysUISingleton
-class InfiniteGridSizeInteractor @Inject constructor(repo: InfiniteGridSizeRepository) {
+class FixedColumnsSizeInteractor @Inject constructor(repo: FixedColumnsRepository) {
     val columns: StateFlow<Int> = repo.columns
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractor.kt
index e99c64c..0fe79af 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractor.kt
@@ -28,7 +28,7 @@
 @Inject
 constructor(
     private val iconTilesInteractor: IconTilesInteractor,
-    private val gridSizeInteractor: InfiniteGridSizeInteractor
+    private val gridSizeInteractor: FixedColumnsSizeInteractor
 ) : GridTypeConsistencyInteractor {
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/NoopConsistencyInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/PaginatedGridInteractor.kt
similarity index 75%
rename from packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/NoopConsistencyInteractor.kt
rename to packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/PaginatedGridInteractor.kt
index 97ceacc..d7d1ce9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/NoopConsistencyInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/PaginatedGridInteractor.kt
@@ -17,10 +17,14 @@
 package com.android.systemui.qs.panels.domain.interactor
 
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.panels.data.repository.PaginatedGridRepository
 import javax.inject.Inject
 
 @SysUISingleton
-class NoopConsistencyInteractor @Inject constructor() : GridTypeConsistencyInteractor {
-    override fun reconcileTiles(tiles: List<TileSpec>): List<TileSpec> = tiles
+class PaginatedGridInteractor
+@Inject
+constructor(paginatedGridRepository: PaginatedGridRepository) {
+    val rows = paginatedGridRepository.rows
+
+    val defaultRows = 4
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/GridLayoutType.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/GridLayoutType.kt
index 9550ddb..b1942fe 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/GridLayoutType.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/GridLayoutType.kt
@@ -34,3 +34,6 @@
 
 /** Grid type grouping large tiles on top and icon tiles at the bottom. */
 data object PartitionedGridLayoutType : GridLayoutType
+
+/** Grid type for a paginated list of tiles. It will delegate to some other layout type. */
+data object PaginatedGridLayoutType : GridLayoutType
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt
index 8806931..e2f6bcf 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt
@@ -18,15 +18,19 @@
 
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
+import com.android.systemui.qs.panels.shared.model.SizedTile
+import com.android.systemui.qs.panels.shared.model.TileRow
 import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
 import com.android.systemui.qs.pipeline.shared.TileSpec
 
+/** A layout of tiles, indicating how they should be composed when showing in QS or in edit mode. */
 interface GridLayout {
     @Composable
     fun TileGrid(
         tiles: List<TileViewModel>,
         modifier: Modifier,
+        editModeStart: () -> Unit,
     )
 
     @Composable
@@ -37,3 +41,49 @@
         onRemoveTile: (TileSpec) -> Unit,
     )
 }
+
+/**
+ * A type of [GridLayout] that can be paginated, to use together with [PaginatedGridLayout].
+ *
+ * [splitIntoPages] determines how to split a list of tiles based on the number of rows and columns
+ * available.
+ */
+interface PaginatableGridLayout : GridLayout {
+    fun splitIntoPages(
+        tiles: List<TileViewModel>,
+        rows: Int,
+        columns: Int,
+    ): List<List<TileViewModel>>
+
+    companion object {
+
+        /**
+         * Splits a list of [SizedTile] into rows, each with at most [columns] occupied.
+         *
+         * It will leave gaps at the end of a row if the next [SizedTile] has [SizedTile.width] that
+         * is larger than the space remaining in the row.
+         */
+        fun splitInRows(
+            tiles: List<SizedTile<TileViewModel>>,
+            columns: Int
+        ): List<List<SizedTile<TileViewModel>>> {
+            val row = TileRow<TileViewModel>(columns)
+
+            return buildList {
+                for (tile in tiles) {
+                    check(tile.width <= columns)
+                    if (!row.maybeAddTile(tile)) {
+                        // Couldn't add tile to previous row, create a row with the current tiles
+                        // and start a new one
+                        add(row.tiles)
+                        row.clear()
+                        row.maybeAddTile(tile)
+                    }
+                }
+                if (row.tiles.isNotEmpty()) {
+                    add(row.tiles)
+                }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt
index 2f0fe22..ea97f0d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt
@@ -26,9 +26,10 @@
 import androidx.compose.ui.res.dimensionResource
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.panels.shared.model.SizedTile
 import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.FixedColumnsSizeViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.IconTilesViewModel
-import com.android.systemui.qs.panels.ui.viewmodel.InfiniteGridSizeViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.res.R
@@ -39,13 +40,14 @@
 @Inject
 constructor(
     private val iconTilesViewModel: IconTilesViewModel,
-    private val gridSizeViewModel: InfiniteGridSizeViewModel,
-) : GridLayout {
+    private val gridSizeViewModel: FixedColumnsSizeViewModel,
+) : PaginatableGridLayout {
 
     @Composable
     override fun TileGrid(
         tiles: List<TileViewModel>,
         modifier: Modifier,
+        editModeStart: () -> Unit,
     ) {
         DisposableEffect(tiles) {
             val token = Any()
@@ -55,16 +57,8 @@
         val columns by gridSizeViewModel.columns.collectAsStateWithLifecycle()
 
         TileLazyGrid(modifier = modifier, columns = GridCells.Fixed(columns)) {
-            items(
-                tiles.size,
-                span = { index ->
-                    if (iconTilesViewModel.isIconTile(tiles[index].spec)) {
-                        GridItemSpan(1)
-                    } else {
-                        GridItemSpan(2)
-                    }
-                }
-            ) { index ->
+            items(tiles.size, span = { index -> GridItemSpan(tiles[index].spec.width()) }) { index
+                ->
                 Tile(
                     tile = tiles[index],
                     iconOnly = iconTilesViewModel.isIconTile(tiles[index].spec),
@@ -92,4 +86,22 @@
             onRemoveTile = onRemoveTile,
         )
     }
+
+    override fun splitIntoPages(
+        tiles: List<TileViewModel>,
+        rows: Int,
+        columns: Int,
+    ): List<List<TileViewModel>> {
+
+        return PaginatableGridLayout.splitInRows(
+                tiles.map { SizedTile(it, it.spec.width()) },
+                columns,
+            )
+            .chunked(rows)
+            .map { it.flatten().map { it.tile } }
+    }
+
+    private fun TileSpec.width(): Int {
+        return if (iconTilesViewModel.isIconTile(this)) 1 else 2
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PagerDots.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PagerDots.kt
new file mode 100644
index 0000000..7de22161
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PagerDots.kt
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.ui.compose
+
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.layout.Arrangement.spacedBy
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.foundation.pager.PagerState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.CornerRadius
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.semantics.pageLeft
+import androidx.compose.ui.semantics.pageRight
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.stateDescription
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import kotlin.math.absoluteValue
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+@Composable
+fun PagerDots(
+    pagerState: PagerState,
+    activeColor: Color,
+    nonActiveColor: Color,
+    modifier: Modifier = Modifier,
+    dotSize: Dp = 6.dp,
+    spaceSize: Dp = 4.dp,
+) {
+    if (pagerState.pageCount < 2) {
+        return
+    }
+    val inPageTransition by
+        remember(pagerState) {
+            derivedStateOf {
+                pagerState.currentPageOffsetFraction.absoluteValue > 0.01 &&
+                    !pagerState.isOverscrolling()
+            }
+        }
+    val coroutineScope = rememberCoroutineScope()
+    Row(
+        modifier =
+            modifier
+                .wrapContentWidth()
+                .pagerDotsSemantics(
+                    pagerState,
+                    coroutineScope,
+                ),
+        horizontalArrangement = spacedBy(spaceSize),
+        verticalAlignment = Alignment.CenterVertically
+    ) {
+        if (!inPageTransition) {
+            repeat(pagerState.pageCount) { i ->
+                // We use canvas directly to only invalidate the draw phase when the page is
+                // changing.
+                Canvas(Modifier.size(dotSize)) {
+                    if (pagerState.currentPage == i) {
+                        drawCircle(activeColor)
+                    } else {
+                        drawCircle(nonActiveColor)
+                    }
+                }
+            }
+        } else {
+            val doubleDotWidth = dotSize * 2 + spaceSize
+            val cornerRadius = dotSize / 2
+            val width by
+                animateDpAsState(targetValue = if (inPageTransition) doubleDotWidth else dotSize)
+
+            fun DrawScope.drawDoubleRect() {
+                drawRoundRect(
+                    color = activeColor,
+                    size = Size(width.toPx(), dotSize.toPx()),
+                    cornerRadius = CornerRadius(cornerRadius.toPx(), cornerRadius.toPx())
+                )
+            }
+
+            repeat(pagerState.pageCount) { page ->
+                Canvas(Modifier.size(dotSize)) {
+                    val withPrevious = pagerState.currentPageOffsetFraction < 0
+                    val ltr = layoutDirection == LayoutDirection.Ltr
+                    if (
+                        withPrevious && page == (pagerState.currentPage - 1) ||
+                            !withPrevious && page == pagerState.currentPage
+                    ) {
+                        if (ltr) {
+                            drawDoubleRect()
+                        }
+                    } else if (
+                        withPrevious && page == pagerState.currentPage ||
+                            !withPrevious && page == (pagerState.currentPage + 1)
+                    ) {
+                        if (!ltr) {
+                            drawDoubleRect()
+                        }
+                    } else {
+                        drawCircle(nonActiveColor)
+                    }
+                }
+            }
+        }
+    }
+}
+
+private fun Modifier.pagerDotsSemantics(
+    pagerState: PagerState,
+    coroutineScope: CoroutineScope,
+): Modifier {
+    return then(
+        Modifier.semantics {
+            pageLeft {
+                if (pagerState.canScrollBackward) {
+                    coroutineScope.launch {
+                        pagerState.animateScrollToPage(pagerState.currentPage - 1)
+                    }
+                    true
+                } else {
+                    false
+                }
+            }
+            pageRight {
+                if (pagerState.canScrollForward) {
+                    coroutineScope.launch {
+                        pagerState.animateScrollToPage(pagerState.currentPage + 1)
+                    }
+                    true
+                } else {
+                    false
+                }
+            }
+            stateDescription = "Page ${pagerState.settledPage + 1} of ${pagerState.pageCount}"
+        }
+    )
+}
+
+private fun PagerState.isOverscrolling(): Boolean {
+    val position = currentPage + currentPageOffsetFraction
+    return position < 0 || position > pageCount - 1
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt
new file mode 100644
index 0000000..2ee957e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.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.qs.panels.ui.compose
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.systemui.qs.panels.dagger.PaginatedBaseLayoutType
+import com.android.systemui.qs.panels.ui.compose.PaginatedGridLayout.Dimensions.FooterHeight
+import com.android.systemui.qs.panels.ui.compose.PaginatedGridLayout.Dimensions.InterPageSpacing
+import com.android.systemui.qs.panels.ui.viewmodel.PaginatedGridViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
+import com.android.systemui.res.R
+import javax.inject.Inject
+
+class PaginatedGridLayout
+@Inject
+constructor(
+    private val viewModel: PaginatedGridViewModel,
+    @PaginatedBaseLayoutType private val delegateGridLayout: PaginatableGridLayout,
+) : GridLayout by delegateGridLayout {
+    @Composable
+    override fun TileGrid(
+        tiles: List<TileViewModel>,
+        modifier: Modifier,
+        editModeStart: () -> Unit,
+    ) {
+        DisposableEffect(tiles) {
+            val token = Any()
+            tiles.forEach { it.startListening(token) }
+            onDispose { tiles.forEach { it.stopListening(token) } }
+        }
+        val columns by viewModel.columns.collectAsStateWithLifecycle()
+        val rows by viewModel.rows.collectAsStateWithLifecycle()
+
+        val pages =
+            remember(tiles, columns, rows) {
+                delegateGridLayout.splitIntoPages(tiles, rows = rows, columns = columns)
+            }
+
+        val pagerState = rememberPagerState(0) { pages.size }
+
+        Column {
+            HorizontalPager(
+                state = pagerState,
+                modifier = Modifier,
+                pageSpacing = if (pages.size > 1) InterPageSpacing else 0.dp,
+                beyondViewportPageCount = 1,
+                verticalAlignment = Alignment.Top,
+            ) {
+                val page = pages[it]
+
+                delegateGridLayout.TileGrid(tiles = page, modifier = Modifier, editModeStart = {})
+            }
+            Box(
+                modifier = Modifier.height(FooterHeight).fillMaxWidth(),
+            ) {
+                PagerDots(
+                    pagerState = pagerState,
+                    activeColor = MaterialTheme.colorScheme.primary,
+                    nonActiveColor = MaterialTheme.colorScheme.surfaceVariant,
+                    modifier = Modifier.align(Alignment.Center)
+                )
+                CompositionLocalProvider(value = LocalContentColor provides Color.White) {
+                    IconButton(
+                        onClick = editModeStart,
+                        modifier = Modifier.align(Alignment.CenterEnd),
+                    ) {
+                        Icon(
+                            imageVector = Icons.Default.Edit,
+                            contentDescription = stringResource(id = R.string.qs_edit)
+                        )
+                    }
+                }
+            }
+        }
+    }
+
+    private object Dimensions {
+        val FooterHeight = 48.dp
+        val InterPageSpacing = 16.dp
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt
index 9233e76..7f5e474 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt
@@ -53,6 +53,7 @@
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.android.compose.modifiers.background
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.panels.shared.model.SizedTile
 import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.PartitionedGridViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
@@ -63,9 +64,13 @@
 
 @SysUISingleton
 class PartitionedGridLayout @Inject constructor(private val viewModel: PartitionedGridViewModel) :
-    GridLayout {
+    PaginatableGridLayout {
     @Composable
-    override fun TileGrid(tiles: List<TileViewModel>, modifier: Modifier) {
+    override fun TileGrid(
+        tiles: List<TileViewModel>,
+        modifier: Modifier,
+        editModeStart: () -> Unit,
+    ) {
         DisposableEffect(tiles) {
             val token = Any()
             tiles.forEach { it.startListening(token) }
@@ -169,6 +174,20 @@
         }
     }
 
+    override fun splitIntoPages(
+        tiles: List<TileViewModel>,
+        rows: Int,
+        columns: Int,
+    ): List<List<TileViewModel>> {
+        val (smallTiles, largeTiles) = tiles.partition { viewModel.isIconTile(it.spec) }
+
+        val sizedLargeTiles = largeTiles.map { SizedTile(it, 2) }
+        val sizedSmallTiles = smallTiles.map { SizedTile(it, 1) }
+        val largeTilesRows = PaginatableGridLayout.splitInRows(sizedLargeTiles, columns)
+        val smallTilesRows = PaginatableGridLayout.splitInRows(sizedSmallTiles, columns)
+        return (largeTilesRows + smallTilesRows).chunked(rows).map { it.flatten().map { it.tile } }
+    }
+
     @Composable
     private fun CurrentTiles(
         tiles: List<EditTileViewModel>,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/StretchedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/StretchedGridLayout.kt
index 7f4e0a7..4a90102 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/StretchedGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/StretchedGridLayout.kt
@@ -30,8 +30,8 @@
 import com.android.systemui.qs.panels.shared.model.SizedTile
 import com.android.systemui.qs.panels.shared.model.TileRow
 import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.FixedColumnsSizeViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.IconTilesViewModel
-import com.android.systemui.qs.panels.ui.viewmodel.InfiniteGridSizeViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.res.R
@@ -42,13 +42,14 @@
 @Inject
 constructor(
     private val iconTilesViewModel: IconTilesViewModel,
-    private val gridSizeViewModel: InfiniteGridSizeViewModel,
+    private val gridSizeViewModel: FixedColumnsSizeViewModel,
 ) : GridLayout {
 
     @Composable
     override fun TileGrid(
         tiles: List<TileViewModel>,
         modifier: Modifier,
+        editModeStart: () -> Unit,
     ) {
         DisposableEffect(tiles) {
             val token = Any()
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileGrid.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileGrid.kt
index 2dab7c3..8c57d41 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileGrid.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileGrid.kt
@@ -23,9 +23,13 @@
 import com.android.systemui.qs.panels.ui.viewmodel.TileGridViewModel
 
 @Composable
-fun TileGrid(viewModel: TileGridViewModel, modifier: Modifier = Modifier) {
+fun TileGrid(
+    viewModel: TileGridViewModel,
+    modifier: Modifier = Modifier,
+    editModeStart: () -> Unit
+) {
     val gridLayout by viewModel.gridLayout.collectAsStateWithLifecycle()
     val tiles by viewModel.tileViewModels.collectAsStateWithLifecycle(emptyList())
 
-    gridLayout.TileGrid(tiles, modifier)
+    gridLayout.TileGrid(tiles, modifier, editModeStart)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridSizeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/FixedColumnsSizeViewModel.kt
similarity index 78%
rename from packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridSizeViewModel.kt
rename to packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/FixedColumnsSizeViewModel.kt
index a4ee58f..865c86b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridSizeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/FixedColumnsSizeViewModel.kt
@@ -17,16 +17,16 @@
 package com.android.systemui.qs.panels.ui.viewmodel
 
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.qs.panels.domain.interactor.InfiniteGridSizeInteractor
+import com.android.systemui.qs.panels.domain.interactor.FixedColumnsSizeInteractor
 import javax.inject.Inject
 import kotlinx.coroutines.flow.StateFlow
 
-interface InfiniteGridSizeViewModel {
+interface FixedColumnsSizeViewModel {
     val columns: StateFlow<Int>
 }
 
 @SysUISingleton
-class InfiniteGridSizeViewModelImpl @Inject constructor(interactor: InfiniteGridSizeInteractor) :
-    InfiniteGridSizeViewModel {
+class FixedColumnsSizeViewModelImpl @Inject constructor(interactor: FixedColumnsSizeInteractor) :
+    FixedColumnsSizeViewModel {
     override val columns: StateFlow<Int> = interactor.columns
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModel.kt
new file mode 100644
index 0000000..28bf474
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModel.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.qs.panels.domain.interactor.PaginatedGridInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.stateIn
+
+@SysUISingleton
+class PaginatedGridViewModel
+@Inject
+constructor(
+    iconTilesViewModel: IconTilesViewModel,
+    gridSizeViewModel: FixedColumnsSizeViewModel,
+    iconLabelVisibilityViewModel: IconLabelVisibilityViewModel,
+    paginatedGridInteractor: PaginatedGridInteractor,
+    @Application applicationScope: CoroutineScope,
+) :
+    IconTilesViewModel by iconTilesViewModel,
+    FixedColumnsSizeViewModel by gridSizeViewModel,
+    IconLabelVisibilityViewModel by iconLabelVisibilityViewModel {
+    val rows =
+        paginatedGridInteractor.rows.stateIn(
+            applicationScope,
+            SharingStarted.WhileSubscribed(),
+            paginatedGridInteractor.defaultRows,
+        )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PartitionedGridViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PartitionedGridViewModel.kt
index 730cf63..2049edb 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PartitionedGridViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PartitionedGridViewModel.kt
@@ -24,9 +24,9 @@
 @Inject
 constructor(
     iconTilesViewModel: IconTilesViewModel,
-    gridSizeViewModel: InfiniteGridSizeViewModel,
+    gridSizeViewModel: FixedColumnsSizeViewModel,
     iconLabelVisibilityViewModel: IconLabelVisibilityViewModel,
 ) :
     IconTilesViewModel by iconTilesViewModel,
-    InfiniteGridSizeViewModel by gridSizeViewModel,
+    FixedColumnsSizeViewModel by gridSizeViewModel,
     IconLabelVisibilityViewModel by iconLabelVisibilityViewModel
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt
index 70f3b84..a3feb2b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt
@@ -46,6 +46,7 @@
 import com.android.systemui.recordissue.IssueRecordingService
 import com.android.systemui.recordissue.IssueRecordingState
 import com.android.systemui.recordissue.RecordIssueDialogDelegate
+import com.android.systemui.recordissue.RecordIssueModule.Companion.TILE_SPEC
 import com.android.systemui.recordissue.TraceurMessageSender
 import com.android.systemui.res.R
 import com.android.systemui.screenrecord.RecordingService
@@ -197,8 +198,4 @@
             expandedAccessibilityClassName = Switch::class.java.name
         }
     }
-
-    companion object {
-        const val TILE_SPEC = "record_issue"
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
index 4715230..284239a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
@@ -47,6 +47,7 @@
 import com.android.systemui.qs.tileimpl.QSTileImpl;
 import com.android.systemui.res.R;
 import com.android.systemui.screenrecord.RecordingController;
+import com.android.systemui.screenrecord.data.model.ScreenRecordModel;
 import com.android.systemui.settings.UserContextProvider;
 import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -146,8 +147,9 @@
         if (isRecording) {
             state.secondaryLabel = mContext.getString(R.string.quick_settings_screen_record_stop);
         } else if (isStarting) {
-            // round, since the timer isn't exact
-            int countdown = (int) Math.floorDiv(mMillisUntilFinished + 500, 1000);
+            int countdown =
+                    (int) ScreenRecordModel.Starting.Companion.toCountdownSeconds(
+                            mMillisUntilFinished);
             state.secondaryLabel = String.format("%d...", countdown);
         } else {
             state.secondaryLabel = mContext.getString(R.string.quick_settings_screen_record_start);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingDataInteractor.kt
new file mode 100644
index 0000000..1af328e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingDataInteractor.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.irecording
+
+import android.os.UserHandle
+import com.android.systemui.Flags
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
+import com.android.systemui.recordissue.IssueRecordingState
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.onStart
+
+class IssueRecordingDataInteractor
+@Inject
+constructor(
+    private val state: IssueRecordingState,
+    @Background private val bgCoroutineContext: CoroutineContext,
+) : QSTileDataInteractor<IssueRecordingModel> {
+
+    override fun tileData(
+        user: UserHandle,
+        triggers: Flow<DataUpdateTrigger>
+    ): Flow<IssueRecordingModel> =
+        conflatedCallbackFlow {
+                val listener = Runnable { trySend(IssueRecordingModel(state.isRecording)) }
+                state.addListener(listener)
+                awaitClose { state.removeListener(listener) }
+            }
+            .onStart { emit(IssueRecordingModel(state.isRecording)) }
+            .distinctUntilChanged()
+            .flowOn(bgCoroutineContext)
+
+    override fun availability(user: UserHandle): Flow<Boolean> =
+        flowOf(android.os.Build.IS_DEBUGGABLE && Flags.recordIssueQsTile())
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingMapper.kt
new file mode 100644
index 0000000..ff931b3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingMapper.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.irecording
+
+import android.content.res.Resources
+import android.content.res.Resources.Theme
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.res.R
+import javax.inject.Inject
+
+class IssueRecordingMapper
+@Inject
+constructor(
+    @Main private val resources: Resources,
+    private val theme: Theme,
+) : QSTileDataToStateMapper<IssueRecordingModel> {
+    override fun map(config: QSTileConfig, data: IssueRecordingModel): QSTileState =
+        QSTileState.build(resources, theme, config.uiConfig) {
+            if (data.isRecording) {
+                activationState = QSTileState.ActivationState.ACTIVE
+                secondaryLabel = resources.getString(R.string.qs_record_issue_stop)
+                icon = { Icon.Resource(R.drawable.qs_record_issue_icon_on, null) }
+            } else {
+                icon = { Icon.Resource(R.drawable.qs_record_issue_icon_off, null) }
+                activationState = QSTileState.ActivationState.INACTIVE
+                secondaryLabel = resources.getString(R.string.qs_record_issue_start)
+            }
+            supportedActions = setOf(QSTileState.UserAction.CLICK)
+            contentDescription = "$label, $secondaryLabel"
+        }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepositoryKosmos.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingModel.kt
similarity index 76%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepositoryKosmos.kt
copy to packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingModel.kt
index d8af3fa..260729b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepositoryKosmos.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingModel.kt
@@ -14,8 +14,6 @@
  * limitations under the License.
  */
 
-package com.android.systemui.qs.panels.data.repository
+package com.android.systemui.qs.tiles.impl.irecording
 
-import com.android.systemui.kosmos.Kosmos
-
-val Kosmos.infiniteGridSizeRepository by Kosmos.Fixture { InfiniteGridSizeRepository() }
+@JvmInline value class IssueRecordingModel(val isRecording: Boolean)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingUserActionInteractor.kt
new file mode 100644
index 0000000..4971fef
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingUserActionInteractor.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.irecording
+
+import android.app.AlertDialog
+import android.app.BroadcastOptions
+import android.app.PendingIntent
+import android.util.Log
+import com.android.internal.jank.InteractionJankMonitor
+import com.android.systemui.animation.DialogCuj
+import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.animation.Expandable
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor
+import com.android.systemui.qs.tiles.base.interactor.QSTileInput
+import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
+import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
+import com.android.systemui.recordissue.IssueRecordingService
+import com.android.systemui.recordissue.RecordIssueDialogDelegate
+import com.android.systemui.recordissue.RecordIssueModule.Companion.TILE_SPEC
+import com.android.systemui.screenrecord.RecordingService
+import com.android.systemui.settings.UserContextProvider
+import com.android.systemui.statusbar.phone.KeyguardDismissUtil
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.withContext
+
+private const val TAG = "IssueRecordingActionInteractor"
+
+class IssueRecordingUserActionInteractor
+@Inject
+constructor(
+    @Main private val mainCoroutineContext: CoroutineContext,
+    private val keyguardDismissUtil: KeyguardDismissUtil,
+    private val keyguardStateController: KeyguardStateController,
+    private val dialogTransitionAnimator: DialogTransitionAnimator,
+    private val panelInteractor: PanelInteractor,
+    private val userContextProvider: UserContextProvider,
+    private val delegateFactory: RecordIssueDialogDelegate.Factory,
+) : QSTileUserActionInteractor<IssueRecordingModel> {
+
+    override suspend fun handleInput(input: QSTileInput<IssueRecordingModel>) {
+        if (input.action is QSTileUserAction.Click) {
+            if (input.data.isRecording) {
+                stopIssueRecordingService()
+            } else {
+                withContext(mainCoroutineContext) { showPrompt(input.action.expandable) }
+            }
+        } else {
+            Log.v(TAG, "the RecordIssueTile doesn't handle ${input.action} events yet.")
+        }
+    }
+
+    private fun showPrompt(expandable: Expandable?) {
+        val dialog: AlertDialog =
+            delegateFactory
+                .create {
+                    startIssueRecordingService()
+                    dialogTransitionAnimator.disableAllCurrentDialogsExitAnimations()
+                    panelInteractor.collapsePanels()
+                }
+                .createDialog()
+        val dismissAction =
+            ActivityStarter.OnDismissAction {
+                // We animate from the touched view only if we are not on the keyguard, given
+                // that if we are we will dismiss it which will also collapse the shade.
+                if (expandable != null && !keyguardStateController.isShowing) {
+                    expandable
+                        .dialogTransitionController(
+                            DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, TILE_SPEC)
+                        )
+                        ?.let { dialogTransitionAnimator.show(dialog, it) } ?: dialog.show()
+                } else {
+                    dialog.show()
+                }
+                false
+            }
+        keyguardDismissUtil.executeWhenUnlocked(dismissAction, false, true)
+    }
+
+    private fun startIssueRecordingService() =
+        PendingIntent.getForegroundService(
+                userContextProvider.userContext,
+                RecordingService.REQUEST_CODE,
+                IssueRecordingService.getStartIntent(userContextProvider.userContext),
+                PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+            )
+            .send(BroadcastOptions.makeBasic().apply { isInteractive = true }.toBundle())
+
+    private fun stopIssueRecordingService() =
+        PendingIntent.getService(
+                userContextProvider.userContext,
+                RecordingService.REQUEST_CODE,
+                IssueRecordingService.getStopIntent(userContextProvider.userContext),
+                PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+            )
+            .send(BroadcastOptions.makeBasic().apply { isInteractive = true }.toBundle())
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/ui/ScreenRecordTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/ui/ScreenRecordTileMapper.kt
index 7446708..e74e77f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/ui/ScreenRecordTileMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/ui/ScreenRecordTileMapper.kt
@@ -61,7 +61,7 @@
                             contentDescription = null
                         )
                     icon = { loadedIcon }
-                    val countDown = Math.floorDiv(data.millisUntilStarted + 500, 1000)
+                    val countDown = data.countdownSeconds
                     sideViewIcon = QSTileState.SideViewIcon.None
                     secondaryLabel = String.format("%d...", countDown)
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt
index 4ea3345..b077349 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt
@@ -18,7 +18,7 @@
 
 import android.content.Context
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.qs.tiles.RecordIssueTile
+import com.android.systemui.recordissue.RecordIssueModule.Companion.TILE_SPEC
 import com.android.systemui.res.R
 import com.android.systemui.settings.UserFileManager
 import com.android.systemui.settings.UserTracker
@@ -35,11 +35,7 @@
 ) {
 
     private val prefs =
-        userFileManager.getSharedPreferences(
-            RecordIssueTile.TILE_SPEC,
-            Context.MODE_PRIVATE,
-            userTracker.userId
-        )
+        userFileManager.getSharedPreferences(TILE_SPEC, Context.MODE_PRIVATE, userTracker.userId)
 
     var takeBugreport
         get() = prefs.getBoolean(KEY_TAKE_BUG_REPORT, false)
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueModule.kt b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueModule.kt
index 26af9a7..907b92c 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueModule.kt
@@ -16,12 +16,20 @@
 
 package com.android.systemui.recordissue
 
+import com.android.systemui.Flags
 import com.android.systemui.qs.QsEventLogger
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.qs.tileimpl.QSTileImpl
 import com.android.systemui.qs.tiles.RecordIssueTile
+import com.android.systemui.qs.tiles.base.viewmodel.QSTileViewModelFactory
+import com.android.systemui.qs.tiles.impl.irecording.IssueRecordingDataInteractor
+import com.android.systemui.qs.tiles.impl.irecording.IssueRecordingMapper
+import com.android.systemui.qs.tiles.impl.irecording.IssueRecordingModel
+import com.android.systemui.qs.tiles.impl.irecording.IssueRecordingUserActionInteractor
 import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
 import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileViewModel
+import com.android.systemui.qs.tiles.viewmodel.StubQSTileViewModel
 import com.android.systemui.res.R
 import dagger.Binds
 import dagger.Module
@@ -34,19 +42,19 @@
     /** Inject RecordIssueTile into tileMap in QSModule */
     @Binds
     @IntoMap
-    @StringKey(RecordIssueTile.TILE_SPEC)
+    @StringKey(TILE_SPEC)
     fun bindRecordIssueTile(recordIssueTile: RecordIssueTile): QSTileImpl<*>
 
     companion object {
 
-        const val RECORD_ISSUE_TILE_SPEC = "record_issue"
+        const val TILE_SPEC = "record_issue"
 
         @Provides
         @IntoMap
-        @StringKey(RECORD_ISSUE_TILE_SPEC)
+        @StringKey(TILE_SPEC)
         fun provideRecordIssueTileConfig(uiEventLogger: QsEventLogger): QSTileConfig =
             QSTileConfig(
-                tileSpec = TileSpec.create(RECORD_ISSUE_TILE_SPEC),
+                tileSpec = TileSpec.create(TILE_SPEC),
                 uiConfig =
                     QSTileUIConfig.Resource(
                         iconRes = R.drawable.qs_record_issue_icon_off,
@@ -54,5 +62,24 @@
                     ),
                 instanceId = uiEventLogger.getNewInstanceId(),
             )
+
+        /** Inject FlashlightTile into tileViewModelMap in QSModule */
+        @Provides
+        @IntoMap
+        @StringKey(TILE_SPEC)
+        fun provideIssueRecordingTileViewModel(
+            factory: QSTileViewModelFactory.Static<IssueRecordingModel>,
+            mapper: IssueRecordingMapper,
+            stateInteractor: IssueRecordingDataInteractor,
+            userActionInteractor: IssueRecordingUserActionInteractor
+        ): QSTileViewModel =
+            if (Flags.qsNewTilesFuture())
+                factory.create(
+                    TileSpec.create(TILE_SPEC),
+                    userActionInteractor,
+                    stateInteractor,
+                    mapper,
+                )
+            else StubQSTileViewModel
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
index 08462d7..6e89973 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.scene.domain.resolver.HomeSceneFamilyResolverModule
 import com.android.systemui.scene.domain.resolver.NotifShadeSceneFamilyResolverModule
 import com.android.systemui.scene.domain.resolver.QuickSettingsSceneFamilyResolverModule
+import com.android.systemui.scene.domain.startable.KeyguardStateCallbackStartable
 import com.android.systemui.scene.domain.startable.SceneContainerStartable
 import com.android.systemui.scene.domain.startable.ScrimStartable
 import com.android.systemui.scene.domain.startable.StatusBarStartable
@@ -72,6 +73,11 @@
 
     @Binds
     @IntoMap
+    @ClassKey(KeyguardStateCallbackStartable::class)
+    fun keyguardStateCallbackStartable(impl: KeyguardStateCallbackStartable): CoreStartable
+
+    @Binds
+    @IntoMap
     @ClassKey(WindowRootViewVisibilityInteractor::class)
     fun bindWindowRootViewVisibilityInteractor(
         impl: WindowRootViewVisibilityInteractor
diff --git a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
index 17dc9a5..7d63b4c 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.scene.domain.resolver.HomeSceneFamilyResolverModule
 import com.android.systemui.scene.domain.resolver.NotifShadeSceneFamilyResolverModule
 import com.android.systemui.scene.domain.resolver.QuickSettingsSceneFamilyResolverModule
+import com.android.systemui.scene.domain.startable.KeyguardStateCallbackStartable
 import com.android.systemui.scene.domain.startable.SceneContainerStartable
 import com.android.systemui.scene.domain.startable.ScrimStartable
 import com.android.systemui.scene.domain.startable.StatusBarStartable
@@ -78,6 +79,11 @@
 
     @Binds
     @IntoMap
+    @ClassKey(KeyguardStateCallbackStartable::class)
+    fun keyguardStateCallbackStartable(impl: KeyguardStateCallbackStartable): CoreStartable
+
+    @Binds
+    @IntoMap
     @ClassKey(WindowRootViewVisibilityInteractor::class)
     fun bindWindowRootViewVisibilityInteractor(
         impl: WindowRootViewVisibilityInteractor
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
index 4738dbd..25a9e9e 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardEnabledInteractor
 import com.android.systemui.scene.data.repository.SceneContainerRepository
 import com.android.systemui.scene.domain.resolver.SceneResolver
 import com.android.systemui.scene.shared.logger.SceneLogger
@@ -60,6 +61,7 @@
     private val logger: SceneLogger,
     private val sceneFamilyResolvers: Lazy<Map<SceneKey, @JvmSuppressWildcards SceneResolver>>,
     private val deviceUnlockedInteractor: DeviceUnlockedInteractor,
+    private val keyguardEnabledInteractor: KeyguardEnabledInteractor,
 ) {
 
     interface OnSceneAboutToChangeListener {
@@ -381,7 +383,8 @@
         val isChangeAllowed =
             to != Scenes.Gone ||
                 inMidTransitionFromGone ||
-                deviceUnlockedInteractor.deviceUnlockStatus.value.isUnlocked
+                deviceUnlockedInteractor.deviceUnlockStatus.value.isUnlocked ||
+                !keyguardEnabledInteractor.isKeyguardEnabled.value
         check(isChangeAllowed) {
             "Cannot change to the Gone scene while the device is locked and not currently" +
                 " transitioning from Gone. Current transition state is ${transitionState.value}." +
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/KeyguardStateCallbackStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/KeyguardStateCallbackStartable.kt
new file mode 100644
index 0000000..6d1c1a7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/KeyguardStateCallbackStartable.kt
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.scene.domain.startable
+
+import android.os.DeadObjectException
+import android.os.RemoteException
+import com.android.internal.policy.IKeyguardStateCallback
+import com.android.systemui.CoreStartable
+import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
+import com.android.systemui.keyguard.domain.interactor.TrustInteractor
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+/** Keeps all [IKeyguardStateCallback]s hydrated with the latest state. */
+@SysUISingleton
+class KeyguardStateCallbackStartable
+@Inject
+constructor(
+    @Application private val applicationScope: CoroutineScope,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+    private val sceneInteractor: SceneInteractor,
+    private val selectedUserInteractor: SelectedUserInteractor,
+    private val deviceEntryInteractor: DeviceEntryInteractor,
+    private val simBouncerInteractor: SimBouncerInteractor,
+    private val trustInteractor: TrustInteractor,
+) : CoreStartable {
+
+    private val callbacks = mutableListOf<IKeyguardStateCallback>()
+
+    override fun start() {
+        if (!SceneContainerFlag.isEnabled) {
+            return
+        }
+
+        hydrateKeyguardShowingAndInputRestrictionStates()
+        hydrateSimSecureState()
+        notifyWhenKeyguardShowingChanged()
+        notifyWhenTrustChanged()
+    }
+
+    fun addCallback(callback: IKeyguardStateCallback) {
+        SceneContainerFlag.assertInNewMode()
+
+        callbacks.add(callback)
+
+        applicationScope.launch(backgroundDispatcher) {
+            callback.onShowingStateChanged(
+                !deviceEntryInteractor.isDeviceEntered.value,
+                selectedUserInteractor.getSelectedUserId(),
+            )
+            callback.onTrustedChanged(trustInteractor.isTrusted.value)
+            callback.onSimSecureStateChanged(simBouncerInteractor.isAnySimSecure.value)
+            // TODO(b/348644111): add support for mNeedToReshowWhenReenabled
+            callback.onInputRestrictedStateChanged(!deviceEntryInteractor.isDeviceEntered.value)
+        }
+    }
+
+    private fun hydrateKeyguardShowingAndInputRestrictionStates() {
+        applicationScope.launch {
+            combine(
+                    selectedUserInteractor.selectedUser,
+                    deviceEntryInteractor.isDeviceEntered,
+                    ::Pair
+                )
+                .collectLatest { (selectedUserId, isDeviceEntered) ->
+                    val iterator = callbacks.iterator()
+                    withContext(backgroundDispatcher) {
+                        while (iterator.hasNext()) {
+                            val callback = iterator.next()
+                            try {
+                                callback.onShowingStateChanged(!isDeviceEntered, selectedUserId)
+                                // TODO(b/348644111): add support for mNeedToReshowWhenReenabled
+                                callback.onInputRestrictedStateChanged(!isDeviceEntered)
+                            } catch (e: RemoteException) {
+                                if (e is DeadObjectException) {
+                                    iterator.remove()
+                                }
+                            }
+                        }
+                    }
+                }
+        }
+    }
+
+    private fun hydrateSimSecureState() {
+        applicationScope.launch {
+            simBouncerInteractor.isAnySimSecure.collectLatest { isSimSecured ->
+                val iterator = callbacks.iterator()
+                withContext(backgroundDispatcher) {
+                    while (iterator.hasNext()) {
+                        val callback = iterator.next()
+                        try {
+                            callback.onSimSecureStateChanged(isSimSecured)
+                        } catch (e: RemoteException) {
+                            if (e is DeadObjectException) {
+                                iterator.remove()
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private fun notifyWhenKeyguardShowingChanged() {
+        applicationScope.launch {
+            // This is equivalent to isDeviceEntered but it waits for the full transition animation
+            // to finish before emitting a new value and not just for the current scene to be
+            // switched.
+            sceneInteractor.transitionState
+                .filter { it.isIdle(Scenes.Gone) || it.isIdle(Scenes.Lockscreen) }
+                .map { it.isIdle(Scenes.Lockscreen) }
+                .distinctUntilChanged()
+                .collectLatest { trustInteractor.reportKeyguardShowingChanged() }
+        }
+    }
+
+    private fun notifyWhenTrustChanged() {
+        applicationScope.launch {
+            trustInteractor.isTrusted.collectLatest { isTrusted ->
+                val iterator = callbacks.iterator()
+                withContext(backgroundDispatcher) {
+                    while (iterator.hasNext()) {
+                        val callback = iterator.next()
+                        try {
+                            callback.onTrustedChanged(isTrusted)
+                        } catch (e: RemoteException) {
+                            if (e is DeadObjectException) {
+                                iterator.remove()
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
index 1e689bd..218853d7 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
@@ -22,6 +22,7 @@
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.compose.animation.scene.SceneKey
 import com.android.internal.logging.UiEventLogger
+import com.android.internal.policy.IKeyguardStateCallback
 import com.android.systemui.CoreStartable
 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
@@ -127,6 +128,8 @@
     private val centralSurfaces: CentralSurfaces?
         get() = centralSurfacesOptLazy.get().getOrNull()
 
+    private val keyguardStateCallbacks = mutableListOf<IKeyguardStateCallback>()
+
     override fun start() {
         if (SceneContainerFlag.isEnabled) {
             sceneLogger.logFrameworkEnabled(isEnabled = true)
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt
index cf33c4a..6c63c97 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt
@@ -39,13 +39,14 @@
     inline val isEnabled
         get() =
             sceneContainer() && // mainAconfigFlag
-            ComposeLockscreen.isEnabled &&
+                ComposeLockscreen.isEnabled &&
                 KeyguardBottomAreaRefactor.isEnabled &&
                 KeyguardWmStateRefactor.isEnabled &&
                 MigrateClocksToBlueprint.isEnabled &&
                 NotificationsHeadsUpRefactor.isEnabled &&
                 PredictiveBackSysUiFlag.isEnabled &&
                 DeviceEntryUdfpsRefactor.isEnabled
+
     // NOTE: Changes should also be made in getSecondaryFlags and @EnableSceneContainer
 
     /** The main aconfig flag. */
@@ -91,6 +92,14 @@
     @JvmStatic
     inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, DESCRIPTION)
 
+    /**
+     * Called to ensure the new code is only run when the flag is enabled. This will throw an
+     * exception if the flag is disabled to ensure that the refactor author catches issues in
+     * testing.
+     */
+    @JvmStatic
+    inline fun assertInNewMode() = RefactorFlagUtils.assertInNewMode(isEnabled, DESCRIPTION)
+
     /** Returns a developer-readable string that describes the current requirement list. */
     @JvmStatic
     fun requirementDescription(): String {
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt
index ac91337..9f48ee9 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt
@@ -64,14 +64,25 @@
                     // TODO(b/338577208): Remove this once we add Dual Shade invocation zones.
                     shadeMode is ShadeMode.Dual
             ) {
-                put(
-                    Swipe(
-                        pointerCount = 2,
-                        fromSource = Edge.Top,
-                        direction = SwipeDirection.Down,
-                    ),
-                    UserActionResult(SceneFamilies.QuickSettings)
-                )
+                if (shadeInteractor.shadeAlignment == Alignment.BottomEnd) {
+                    put(
+                        Swipe(
+                            pointerCount = 2,
+                            fromSource = Edge.Bottom,
+                            direction = SwipeDirection.Up,
+                        ),
+                        UserActionResult(SceneFamilies.QuickSettings, OpenBottomShade)
+                    )
+                } else {
+                    put(
+                        Swipe(
+                            pointerCount = 2,
+                            fromSource = Edge.Top,
+                            direction = SwipeDirection.Down,
+                        ),
+                        UserActionResult(SceneFamilies.QuickSettings)
+                    )
+                }
             }
 
             if (shadeInteractor.shadeAlignment == Alignment.BottomEnd) {
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/data/model/ScreenRecordModel.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/data/model/ScreenRecordModel.kt
index b225444..ada5d8c0 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/data/model/ScreenRecordModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/data/model/ScreenRecordModel.kt
@@ -22,7 +22,17 @@
     data object Recording : ScreenRecordModel
 
     /** A screen recording will begin in [millisUntilStarted] ms. */
-    data class Starting(val millisUntilStarted: Long) : ScreenRecordModel
+    data class Starting(val millisUntilStarted: Long) : ScreenRecordModel {
+        val countdownSeconds = millisUntilStarted.toCountdownSeconds()
+
+        companion object {
+            /**
+             * Returns the number of seconds until screen recording will start, used to show a 3-2-1
+             * countdown.
+             */
+            fun Long.toCountdownSeconds() = Math.floorDiv(this + 500, 1000)
+        }
+    }
 
     /** There's nothing related to screen recording happening. */
     data object DoingNothing : ScreenRecordModel
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/HeadlessScreenshotHandler.kt b/packages/SystemUI/src/com/android/systemui/screenshot/HeadlessScreenshotHandler.kt
new file mode 100644
index 0000000..6730d2d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/HeadlessScreenshotHandler.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot
+
+import android.net.Uri
+import android.os.UserManager
+import android.util.Log
+import android.view.WindowManager
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.res.R
+import com.google.common.util.concurrent.ListenableFuture
+import java.util.UUID
+import java.util.concurrent.Executor
+import java.util.concurrent.Executors
+import java.util.function.Consumer
+import javax.inject.Inject
+
+/**
+ * A ScreenshotHandler that just saves the screenshot and calls back as appropriate, with no UI.
+ *
+ * Basically, ScreenshotController with all the UI bits ripped out.
+ */
+class HeadlessScreenshotHandler
+@Inject
+constructor(
+    private val imageExporter: ImageExporter,
+    @Main private val mainExecutor: Executor,
+    private val imageCapture: ImageCapture,
+    private val userManager: UserManager,
+    private val uiEventLogger: UiEventLogger,
+    private val notificationsControllerFactory: ScreenshotNotificationsController.Factory,
+) : ScreenshotHandler {
+
+    override fun handleScreenshot(
+        screenshot: ScreenshotData,
+        finisher: Consumer<Uri?>,
+        requestCallback: TakeScreenshotService.RequestCallback
+    ) {
+        if (screenshot.type == WindowManager.TAKE_SCREENSHOT_FULLSCREEN) {
+            screenshot.bitmap = imageCapture.captureDisplay(screenshot.displayId, crop = null)
+        }
+
+        if (screenshot.bitmap == null) {
+            Log.e(TAG, "handleScreenshot: Screenshot bitmap was null")
+            notificationsControllerFactory
+                .create(screenshot.displayId)
+                .notifyScreenshotError(R.string.screenshot_failed_to_capture_text)
+            requestCallback.reportError()
+            return
+        }
+
+        val future: ListenableFuture<ImageExporter.Result> =
+            imageExporter.export(
+                Executors.newSingleThreadExecutor(),
+                UUID.randomUUID(),
+                screenshot.bitmap,
+                screenshot.getUserOrDefault(),
+                screenshot.displayId
+            )
+        future.addListener(
+            {
+                try {
+                    val result = future.get()
+                    Log.d(TAG, "Saved screenshot: $result")
+                    logScreenshotResultStatus(result.uri, screenshot)
+                    finisher.accept(result.uri)
+                    requestCallback.onFinish()
+                } catch (e: Exception) {
+                    Log.d(TAG, "Failed to store screenshot", e)
+                    finisher.accept(null)
+                    requestCallback.reportError()
+                }
+            },
+            mainExecutor
+        )
+    }
+
+    private fun logScreenshotResultStatus(uri: Uri?, screenshot: ScreenshotData) {
+        if (uri == null) {
+            uiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED, 0, screenshot.packageNameString)
+            notificationsControllerFactory
+                .create(screenshot.displayId)
+                .notifyScreenshotError(R.string.screenshot_failed_to_save_text)
+        } else {
+            uiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED, 0, screenshot.packageNameString)
+            if (userManager.isManagedProfile(screenshot.getUserOrDefault().identifier)) {
+                uiEventLogger.log(
+                    ScreenshotEvent.SCREENSHOT_SAVED_TO_WORK_PROFILE,
+                    0,
+                    screenshot.packageNameString
+                )
+            }
+        }
+    }
+
+    companion object {
+        const val TAG = "HeadlessScreenshotHandler"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index e8dfac8..c87b1f5 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -101,7 +101,7 @@
 /**
  * Controls the state and flow for screenshots.
  */
-public class ScreenshotController {
+public class ScreenshotController implements ScreenshotHandler {
     private static final String TAG = logTag(ScreenshotController.class);
 
     /**
@@ -351,7 +351,8 @@
         mShowUIOnExternalDisplay = showUIOnExternalDisplay;
     }
 
-    void handleScreenshot(ScreenshotData screenshot, Consumer<Uri> finisher,
+    @Override
+    public void handleScreenshot(ScreenshotData screenshot, Consumer<Uri> finisher,
             RequestCallback requestCallback) {
         Assert.isMainThread();
 
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
index 3c3797b..2699657 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
 package com.android.systemui.screenshot
 
 import android.net.Uri
@@ -7,12 +23,12 @@
 import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE
 import com.android.internal.logging.UiEventLogger
 import com.android.internal.util.ScreenshotRequest
-import com.android.systemui.Flags.screenshotShelfUi2
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.display.data.repository.DisplayRepository
 import com.android.systemui.res.R
 import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_CAPTURE_FAILED
+import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER
 import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback
 import java.util.function.Consumer
 import javax.inject.Inject
@@ -26,9 +42,13 @@
         onSaved: (Uri?) -> Unit,
         requestCallback: RequestCallback
     )
+
     fun onCloseSystemDialogsReceived()
+
     fun removeWindows()
+
     fun onDestroy()
+
     fun executeScreenshotsAsync(
         screenshotRequest: ScreenshotRequest,
         onSaved: Consumer<Uri?>,
@@ -36,6 +56,14 @@
     )
 }
 
+interface ScreenshotHandler {
+    fun handleScreenshot(
+        screenshot: ScreenshotData,
+        finisher: Consumer<Uri?>,
+        requestCallback: RequestCallback
+    )
+}
+
 /**
  * Receives the signal to take a screenshot from [TakeScreenshotService], and calls back with the
  * result.
@@ -52,10 +80,10 @@
     private val screenshotRequestProcessor: ScreenshotRequestProcessor,
     private val uiEventLogger: UiEventLogger,
     private val screenshotNotificationControllerFactory: ScreenshotNotificationsController.Factory,
+    private val headlessScreenshotHandler: HeadlessScreenshotHandler,
 ) : TakeScreenshotExecutor {
-
     private val displays = displayRepository.displays
-    private val screenshotControllers = mutableMapOf<Int, ScreenshotController>()
+    private var screenshotController: ScreenshotController? = null
     private val notificationControllers = mutableMapOf<Int, ScreenshotNotificationsController>()
 
     /**
@@ -73,9 +101,15 @@
         val resultCallbackWrapper = MultiResultCallbackWrapper(requestCallback)
         displays.forEach { display ->
             val displayId = display.displayId
+            var screenshotHandler: ScreenshotHandler =
+                if (displayId == Display.DEFAULT_DISPLAY) {
+                    getScreenshotController(display)
+                } else {
+                    headlessScreenshotHandler
+                }
             Log.d(TAG, "Executing screenshot for display $displayId")
             dispatchToController(
-                display = display,
+                screenshotHandler,
                 rawScreenshotData = ScreenshotData.fromRequest(screenshotRequest, displayId),
                 onSaved =
                     if (displayId == Display.DEFAULT_DISPLAY) {
@@ -88,7 +122,7 @@
 
     /** All logging should be triggered only by this method. */
     private suspend fun dispatchToController(
-        display: Display,
+        screenshotHandler: ScreenshotHandler,
         rawScreenshotData: ScreenshotData,
         onSaved: (Uri?) -> Unit,
         callback: RequestCallback
@@ -102,13 +136,12 @@
                     logScreenshotRequested(rawScreenshotData)
                     onFailedScreenshotRequest(rawScreenshotData, callback)
                 }
-                .getOrNull()
-                ?: return
+                .getOrNull() ?: return
 
         logScreenshotRequested(screenshotData)
         Log.d(TAG, "Screenshot request: $screenshotData")
         try {
-            getScreenshotController(display).handleScreenshot(screenshotData, onSaved, callback)
+            screenshotHandler.handleScreenshot(screenshotData, onSaved, callback)
         } catch (e: IllegalStateException) {
             Log.e(TAG, "Error while ScreenshotController was handling ScreenshotData!", e)
             onFailedScreenshotRequest(screenshotData, callback)
@@ -140,44 +173,32 @@
 
     private suspend fun getDisplaysToScreenshot(requestType: Int): List<Display> {
         val allDisplays = displays.first()
-        return if (requestType == TAKE_SCREENSHOT_PROVIDED_IMAGE || screenshotShelfUi2()) {
-            // If this is a provided image or using the shelf UI, just screenshot th default display
+        return if (requestType == TAKE_SCREENSHOT_PROVIDED_IMAGE) {
+            // If this is a provided image just screenshot th default display
             allDisplays.filter { it.displayId == Display.DEFAULT_DISPLAY }
         } else {
             allDisplays.filter { it.type in ALLOWED_DISPLAY_TYPES }
         }
     }
 
-    /** Propagates the close system dialog signal to all controllers. */
+    /** Propagates the close system dialog signal to the ScreenshotController. */
     override fun onCloseSystemDialogsReceived() {
-        screenshotControllers.forEach { (_, screenshotController) ->
-            if (!screenshotController.isPendingSharedTransition) {
-                screenshotController.requestDismissal(ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER)
-            }
+        if (screenshotController?.isPendingSharedTransition == false) {
+            screenshotController?.requestDismissal(SCREENSHOT_DISMISSED_OTHER)
         }
     }
 
     /** Removes all screenshot related windows. */
     override fun removeWindows() {
-        screenshotControllers.forEach { (_, screenshotController) ->
-            screenshotController.removeWindow()
-        }
+        screenshotController?.removeWindow()
     }
 
     /**
      * Destroys the executor. Afterwards, this class is not expected to work as intended anymore.
      */
     override fun onDestroy() {
-        screenshotControllers.forEach { (_, screenshotController) ->
-            screenshotController.onDestroy()
-        }
-        screenshotControllers.clear()
-    }
-
-    private fun getScreenshotController(display: Display): ScreenshotController {
-        return screenshotControllers.computeIfAbsent(display.displayId) {
-            screenshotControllerFactory.create(display, /* showUIOnExternalDisplay= */ false)
-        }
+        screenshotController?.onDestroy()
+        screenshotController = null
     }
 
     private fun getNotificationController(id: Int): ScreenshotNotificationsController {
@@ -197,6 +218,12 @@
         }
     }
 
+    private fun getScreenshotController(display: Display): ScreenshotController {
+        val controller = screenshotController ?: screenshotControllerFactory.create(display, false)
+        screenshotController = controller
+        return controller
+    }
+
     /**
      * Returns a [RequestCallback] that wraps [originalCallback].
      *
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
index ee8161c..ce321dc 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
@@ -182,7 +182,11 @@
     }
 
     override fun expandToQs() {
-        sceneInteractor.changeScene(SceneFamilies.QuickSettings, "ShadeController.animateExpandQs")
+        sceneInteractor.changeScene(
+            SceneFamilies.QuickSettings,
+            "ShadeController.animateExpandQs",
+            OpenBottomShade.takeIf { shadeInteractor.shadeAlignment == Alignment.BottomEnd }
+        )
     }
 
     override fun setVisibilityListener(listener: ShadeVisibilityListener) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModel.kt
index b946129..6551854 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModel.kt
@@ -51,7 +51,7 @@
                 initialValue = Scenes.Lockscreen,
             )
 
-    /** Dictates whether the panel is aligned to the top or the bottom. */
+    /** Dictates the alignment of the overlay shade panel on the screen. */
     val panelAlignment = shadeInteractor.shadeAlignment
 
     /** Notifies that the user has clicked the semi-transparent background scrim. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index 1a7871a..2f3fc729 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -1670,11 +1670,6 @@
     private final StatusBarStateController.StateListener mStatusBarStateListener =
             new StatusBarStateController.StateListener() {
         @Override
-        public void onStateChanged(int newState) {
-            setVisible(newState == StatusBarState.KEYGUARD);
-        }
-
-        @Override
         public void onDozingChanged(boolean dozing) {
             if (mDozing == dozing) {
                 return;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
index 04a413a..240953d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
@@ -291,8 +291,9 @@
     }
 
     private void updateMediaMetaData(MediaListener callback) {
-        callback.onPrimaryMetadataOrStateChanged(mMediaMetadata,
-                getMediaControllerPlaybackState(mMediaController));
+        int playbackState = getMediaControllerPlaybackState(mMediaController);
+        mHandler.post(
+                () -> callback.onPrimaryMetadataOrStateChanged(mMediaMetadata, playbackState));
     }
 
     public void removeCallback(MediaListener callback) {
@@ -437,9 +438,11 @@
 
     private void updateMediaMetaData(List<MediaListener> callbacks) {
         @PlaybackState.State int state = getMediaControllerPlaybackState(mMediaController);
-        for (int i = 0; i < callbacks.size(); i++) {
-            callbacks.get(i).onPrimaryMetadataOrStateChanged(mMediaMetadata, state);
-        }
+        mHandler.post(() -> {
+            for (int i = 0; i < callbacks.size(); i++) {
+                callbacks.get(i).onPrimaryMetadataOrStateChanged(mMediaMetadata, state);
+            }
+        });
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
index 855798c..28e3a83 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
@@ -94,7 +94,6 @@
     private float mCornerAnimationDistance;
     private float mActualWidth = -1;
     private int mMaxIconsOnLockscreen;
-    private int mNotificationScrimPadding;
     private boolean mCanModifyColorOfNotifications;
     private boolean mCanInteract;
     private NotificationStackScrollLayout mHostLayout;
@@ -138,7 +137,6 @@
         mStatusBarHeight = SystemBarUtils.getStatusBarHeight(mContext);
         mPaddingBetweenElements = res.getDimensionPixelSize(R.dimen.notification_divider_height);
         mMaxIconsOnLockscreen = res.getInteger(R.integer.max_notif_icons_on_lockscreen);
-        mNotificationScrimPadding = res.getDimensionPixelSize(R.dimen.notification_side_paddings);
 
         ViewGroup.LayoutParams layoutParams = getLayoutParams();
         final int newShelfHeight = res.getDimensionPixelOffset(R.dimen.notification_shelf_height);
@@ -265,7 +263,7 @@
         }
 
         final float stackBottom = SceneContainerFlag.isEnabled()
-                ? getStackBottom(ambientState)
+                ? ambientState.getStackTop() + ambientState.getStackHeight()
                 : ambientState.getStackY() + ambientState.getStackHeight();
 
         if (viewState.hidden) {
@@ -278,19 +276,6 @@
         }
     }
 
-    /**
-     * bottom-most position, where we can draw the stack
-     */
-    private float getStackBottom(AmbientState ambientState) {
-        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return 0f;
-        float stackBottom = ambientState.getStackCutoff() - mNotificationScrimPadding;
-        if (ambientState.isExpansionChanging()) {
-            stackBottom = MathUtils.lerp(stackBottom * StackScrollAlgorithm.START_FRACTION,
-                    stackBottom, ambientState.getExpansionFraction());
-        }
-        return stackBottom;
-    }
-
     private int getSpeedBumpIndex() {
         NotificationIconContainerRefactor.assertInLegacyMode();
         return mHostLayout.getSpeedBumpIndex();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java
index d0702fc..bbf0ae1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java
@@ -69,7 +69,6 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
-import java.text.NumberFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
 
@@ -148,11 +147,6 @@
     @VisibleForTesting float mScaleToFitNewIconSize = 1;
     private StatusBarIcon mIcon;
     @ViewDebug.ExportedProperty private String mSlot;
-    private Drawable mNumberBackground;
-    private Paint mNumberPain;
-    private int mNumberX;
-    private int mNumberY;
-    private String mNumberText;
     private StatusBarNotification mNotification;
     private final boolean mBlocked;
     private Configuration mConfiguration;
@@ -201,10 +195,6 @@
         mDozer = new NotificationDozeHelper();
         mBlocked = blocked;
         mSlot = slot;
-        mNumberPain = new Paint();
-        mNumberPain.setTextAlign(Paint.Align.CENTER);
-        mNumberPain.setColor(context.getColor(R.drawable.notification_number_text_color));
-        mNumberPain.setAntiAlias(true);
         setNotification(sbn);
         setScaleType(ScaleType.CENTER);
         mConfiguration = new Configuration(context.getResources().getConfiguration());
@@ -410,8 +400,6 @@
                 && mIcon.iconLevel == icon.iconLevel;
         final boolean visibilityEquals = mIcon != null
                 && mIcon.visible == icon.visible;
-        final boolean numberEquals = mIcon != null
-                && mIcon.number == icon.number;
         mIcon = icon.clone();
         setContentDescription(icon.contentDescription);
         if (!iconEquals) {
@@ -425,20 +413,6 @@
             setImageLevel(icon.iconLevel);
         }
 
-        if (!numberEquals) {
-            if (icon.number > 0 && getContext().getResources().getBoolean(
-                        R.bool.config_statusBarShowNumber)) {
-                if (mNumberBackground == null) {
-                    mNumberBackground = getContext().getResources().getDrawable(
-                            R.drawable.ic_notification_overlay);
-                }
-                placeNumber();
-            } else {
-                mNumberBackground = null;
-                mNumberText = null;
-            }
-            invalidate();
-        }
         if (!visibilityEquals) {
             setVisibility(icon.visible && !mBlocked ? VISIBLE : GONE);
         }
@@ -568,14 +542,6 @@
     }
 
     @Override
-    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
-        super.onSizeChanged(w, h, oldw, oldh);
-        if (mNumberBackground != null) {
-            placeNumber();
-        }
-    }
-
-    @Override
     public void onRtlPropertiesChanged(int layoutDirection) {
         super.onRtlPropertiesChanged(layoutDirection);
         updateDrawable();
@@ -609,10 +575,6 @@
             canvas.restore();
         }
 
-        if (mNumberBackground != null) {
-            mNumberBackground.draw(canvas);
-            canvas.drawText(mNumberText, mNumberX, mNumberY, mNumberPain);
-        }
         if (mDotAppearAmount != 0.0f) {
             float radius;
             float alpha = Color.alpha(mDecorColor) / 255.f;
@@ -640,39 +602,6 @@
         Log.d("View", debugIndent(depth) + "icon=" + mIcon);
     }
 
-    void placeNumber() {
-        final String str;
-        final int tooBig = getContext().getResources().getInteger(
-                android.R.integer.status_bar_notification_info_maxnum);
-        if (mIcon.number > tooBig) {
-            str = getContext().getResources().getString(
-                        android.R.string.status_bar_notification_info_overflow);
-        } else {
-            NumberFormat f = NumberFormat.getIntegerInstance();
-            str = f.format(mIcon.number);
-        }
-        mNumberText = str;
-
-        final int w = getWidth();
-        final int h = getHeight();
-        final Rect r = new Rect();
-        mNumberPain.getTextBounds(str, 0, str.length(), r);
-        final int tw = r.right - r.left;
-        final int th = r.bottom - r.top;
-        mNumberBackground.getPadding(r);
-        int dw = r.left + tw + r.right;
-        if (dw < mNumberBackground.getMinimumWidth()) {
-            dw = mNumberBackground.getMinimumWidth();
-        }
-        mNumberX = w-r.right-((dw-r.right-r.left)/2);
-        int dh = r.top + th + r.bottom;
-        if (dh < mNumberBackground.getMinimumWidth()) {
-            dh = mNumberBackground.getMinimumWidth();
-        }
-        mNumberY = h-r.bottom-((dh-r.top-th-r.bottom)/2);
-        mNumberBackground.setBounds(w-dw, h-dh, w, h);
-    }
-
     @Override
     public String toString() {
         return "StatusBarIconView("
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt
index ba3fde6..79f1874 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.chips.call.ui.viewmodel
 
+import android.view.View
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.systemui.animation.ActivityTransitionAnimator
 import com.android.systemui.common.shared.model.Icon
@@ -60,7 +61,7 @@
                         val startTimeInElapsedRealtime =
                             state.startTimeMs - systemClock.currentTimeMillis() +
                                 systemClock.elapsedRealtime()
-                        OngoingActivityChipModel.Shown(
+                        OngoingActivityChipModel.Shown.Timer(
                             icon =
                                 Icon.Resource(
                                     com.android.internal.R.drawable.ic_phone,
@@ -68,26 +69,30 @@
                                 ),
                             colors = ColorsModel.Themed,
                             startTimeMs = startTimeInElapsedRealtime,
-                        ) {
-                            if (state.intent != null) {
-                                val backgroundView =
-                                    it.requireViewById<ChipBackgroundContainer>(
-                                        R.id.ongoing_activity_chip_background
-                                    )
-                                // TODO(b/332662551): Log the click event.
-                                // This mimics OngoingCallController#updateChipClickListener.
-                                activityStarter.postStartActivityDismissingKeyguard(
-                                    state.intent,
-                                    ActivityTransitionAnimator.Controller.fromView(
-                                        backgroundView,
-                                        InteractionJankMonitor
-                                            .CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP,
-                                    )
-                                )
-                            }
-                        }
+                            getOnClickListener(state),
+                        )
                     }
                 }
             }
             .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden)
+
+    private fun getOnClickListener(state: OngoingCallModel.InCall): View.OnClickListener? {
+        if (state.intent == null) {
+            return null
+        }
+
+        return View.OnClickListener { view ->
+            val backgroundView =
+                view.requireViewById<ChipBackgroundContainer>(R.id.ongoing_activity_chip_background)
+            // TODO(b/332662551): Log the click event.
+            // This mimics OngoingCallController#updateChipClickListener.
+            activityStarter.postStartActivityDismissingKeyguard(
+                state.intent,
+                ActivityTransitionAnimator.Controller.fromView(
+                    backgroundView,
+                    InteractionJankMonitor.CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP,
+                )
+            )
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt
index 53b1e75..42e921e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt
@@ -70,7 +70,8 @@
                     }
                 }
             }
-            .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden)
+            // See b/347726238.
+            .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden)
 
     /** Stops the currently active projection. */
     private fun stopProjecting() {
@@ -80,7 +81,7 @@
     private fun createCastToOtherDeviceChip(
         state: ProjectionChipModel.Projecting,
     ): OngoingActivityChipModel.Shown {
-        return OngoingActivityChipModel.Shown(
+        return OngoingActivityChipModel.Shown.Timer(
             icon =
                 Icon.Resource(
                     CAST_TO_OTHER_DEVICE_ICON,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt
index 1e9f0a1..43b1d16 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt
@@ -50,7 +50,6 @@
             ) { screenRecordState, mediaProjectionState ->
                 when (screenRecordState) {
                     is ScreenRecordModel.DoingNothing -> ScreenRecordChipModel.DoingNothing
-                    // TODO(b/332662551): Implement the 3-2-1 countdown chip.
                     is ScreenRecordModel.Starting ->
                         ScreenRecordChipModel.Starting(screenRecordState.millisUntilStarted)
                     is ScreenRecordModel.Recording -> {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt
index 9d54c75..af6d7f2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.res.R
+import com.android.systemui.screenrecord.data.model.ScreenRecordModel.Starting.Companion.toCountdownSeconds
 import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndMediaProjectionDialogHelper
 import com.android.systemui.statusbar.chips.screenrecord.domain.interactor.ScreenRecordChipInteractor
 import com.android.systemui.statusbar.chips.screenrecord.domain.model.ScreenRecordChipModel
@@ -55,10 +56,14 @@
             .map { state ->
                 when (state) {
                     is ScreenRecordChipModel.DoingNothing -> OngoingActivityChipModel.Hidden
-                    // TODO(b/332662551): Implement the 3-2-1 countdown chip.
-                    is ScreenRecordChipModel.Starting -> OngoingActivityChipModel.Hidden
+                    is ScreenRecordChipModel.Starting -> {
+                        OngoingActivityChipModel.Shown.Countdown(
+                            colors = ColorsModel.Red,
+                            secondsUntilStarted = state.millisUntilStarted.toCountdownSeconds(),
+                        )
+                    }
                     is ScreenRecordChipModel.Recording -> {
-                        OngoingActivityChipModel.Shown(
+                        OngoingActivityChipModel.Shown.Timer(
                             // TODO(b/332662551): Also provide a content description.
                             icon = Icon.Resource(ICON, contentDescription = null),
                             colors = ColorsModel.Red,
@@ -71,7 +76,8 @@
                     }
                 }
             }
-            .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden)
+            // See b/347726238.
+            .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden)
 
     private fun createDelegate(
         recordedTask: ActivityManager.RunningTaskInfo?
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt
index 0c24a70..c3b1456 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt
@@ -66,7 +66,8 @@
                     }
                 }
             }
-            .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden)
+            // See b/347726238.
+            .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden)
 
     /** Stops the currently active projection. */
     private fun stopProjecting() {
@@ -76,7 +77,7 @@
     private fun createShareToAppChip(
         state: ProjectionChipModel.Projecting,
     ): OngoingActivityChipModel.Shown {
-        return OngoingActivityChipModel.Shown(
+        return OngoingActivityChipModel.Shown.Timer(
             // TODO(b/332662551): Use the right content description.
             icon = Icon.Resource(SHARE_TO_APP_ICON, contentDescription = null),
             colors = ColorsModel.Red,
@@ -97,7 +98,6 @@
         )
 
     companion object {
-        // TODO(b/332662551): Use the right icon.
-        @DrawableRes val SHARE_TO_APP_ICON = R.drawable.ic_screenshot_share
+        @DrawableRes val SHARE_TO_APP_ICON = R.drawable.ic_present_to_all
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt
index 4ea674a..57f609b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt
@@ -25,22 +25,42 @@
     data object Hidden : OngoingActivityChipModel()
 
     /** This chip should be shown with the given information. */
-    data class Shown(
-        /** The icon to show on the chip. */
-        val icon: Icon,
+    abstract class Shown(
+        /** The icon to show on the chip. If null, no icon will be shown. */
+        open val icon: Icon?,
         /** What colors to use for the chip. */
-        val colors: ColorsModel,
+        open val colors: ColorsModel,
         /**
-         * The time this event started, used to show the timer.
-         *
-         * This time should be relative to
-         * [com.android.systemui.util.time.SystemClock.elapsedRealtime], *not*
-         * [com.android.systemui.util.time.SystemClock.currentTimeMillis] because the
-         * [ChipChronometer] is based off of elapsed realtime. See
-         * [android.widget.Chronometer.setBase].
+         * Listener method to invoke when this chip is clicked. If null, the chip won't be
+         * clickable.
          */
-        val startTimeMs: Long,
-        /** Listener method to invoke when this chip is clicked. */
-        val onClickListener: View.OnClickListener,
-    ) : OngoingActivityChipModel()
+        open val onClickListener: View.OnClickListener?,
+    ) : OngoingActivityChipModel() {
+        /** The chip shows a timer, counting up from [startTimeMs]. */
+        data class Timer(
+            override val icon: Icon,
+            override val colors: ColorsModel,
+            /**
+             * The time this event started, used to show the timer.
+             *
+             * This time should be relative to
+             * [com.android.systemui.util.time.SystemClock.elapsedRealtime], *not*
+             * [com.android.systemui.util.time.SystemClock.currentTimeMillis] because the
+             * [ChipChronometer] is based off of elapsed realtime. See
+             * [android.widget.Chronometer.setBase].
+             */
+            val startTimeMs: Long,
+            override val onClickListener: View.OnClickListener?,
+        ) : Shown(icon, colors, onClickListener)
+
+        /**
+         * This chip shows a countdown using [secondsUntilStarted]. Used to inform users that an
+         * event is about to start. Typically, a [Countdown] chip will turn into a [Timer] chip.
+         */
+        data class Countdown(
+            override val colors: ColorsModel,
+            /** The number of seconds until an event is started. */
+            val secondsUntilStarted: Long,
+        ) : Shown(icon = null, colors, onClickListener = null)
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt
index 1b79ce4..9c8086f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt
@@ -72,5 +72,8 @@
                     else -> call
                 }
             }
-            .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden)
+            // Some of the chips could have timers in them and we don't want the start time
+            // for those timers to get reset for any reason. So, as soon as any subscriber has
+            // requested the chip information, we need to maintain it forever. See b/347726238.
+            .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
index 4c66f66..0bb18d7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
@@ -58,7 +58,7 @@
 constructor(
     @Application applicationScope: CoroutineScope,
     dumpManager: DumpManager,
-    private val mHeadsUpManager: HeadsUpManager,
+    private val headsUpManager: HeadsUpManager,
     private val statusBarStateController: StatusBarStateController,
     private val bypassController: KeyguardBypassController,
     private val dozeParameters: DozeParameters,
@@ -71,8 +71,8 @@
     StatusBarStateController.StateListener,
     ShadeExpansionListener,
     Dumpable {
-    private lateinit var mStackScrollerController: NotificationStackScrollLayoutController
-    private var mVisibilityInterpolator = Interpolators.FAST_OUT_SLOW_IN_REVERSE
+    private lateinit var stackScrollerController: NotificationStackScrollLayoutController
+    private var visibilityInterpolator = Interpolators.FAST_OUT_SLOW_IN_REVERSE
 
     private var inputLinearDozeAmount: Float = 0.0f
     private var inputEasedDozeAmount: Float = 0.0f
@@ -85,13 +85,13 @@
     private var outputEasedDozeAmount: Float = 0.0f
     @VisibleForTesting val dozeAmountInterpolator: Interpolator = Interpolators.FAST_OUT_SLOW_IN
 
-    private var mNotificationVisibleAmount = 0.0f
-    private var mNotificationsVisible = false
-    private var mNotificationsVisibleForExpansion = false
-    private var mVisibilityAnimator: ObjectAnimator? = null
-    private var mVisibilityAmount = 0.0f
-    private var mLinearVisibilityAmount = 0.0f
-    private val mEntrySetToClearWhenFinished = mutableSetOf<NotificationEntry>()
+    private var notificationVisibleAmount = 0.0f
+    private var notificationsVisible = false
+    private var notificationsVisibleForExpansion = false
+    private var visibilityAnimator: ObjectAnimator? = null
+    private var visibilityAmount = 0.0f
+    private var linearVisibilityAmount = 0.0f
+    private val entrySetToClearWhenFinished = mutableSetOf<NotificationEntry>()
     private var pulseExpanding: Boolean = false
     private val wakeUpListeners = arrayListOf<WakeUpListener>()
     private var state: Int = StatusBarState.KEYGUARD
@@ -104,14 +104,14 @@
             willWakeUp = false
             if (value) {
                 if (
-                    mNotificationsVisible &&
-                        !mNotificationsVisibleForExpansion &&
+                    notificationsVisible &&
+                        !notificationsVisibleForExpansion &&
                         !bypassController.bypassEnabled
                 ) {
                     // We're waking up while pulsing, let's make sure the animation looks nice
-                    mStackScrollerController.wakeUpFromPulse()
+                    stackScrollerController.wakeUpFromPulse()
                 }
-                if (bypassController.bypassEnabled && !mNotificationsVisible) {
+                if (bypassController.bypassEnabled && !notificationsVisible) {
                     // Let's make sure our huns become visible once we are waking up in case
                     // they were blocked by the proximity sensor
                     updateNotificationVisibility(
@@ -186,13 +186,13 @@
 
     init {
         dumpManager.registerDumpable(this)
-        mHeadsUpManager.addListener(this)
+        headsUpManager.addListener(this)
         statusBarStateController.addCallback(this)
         bypassController.registerOnBypassStateChangedListener(bypassStateChangedListener)
         addListener(
             object : WakeUpListener {
                 override fun onFullyHiddenChanged(isFullyHidden: Boolean) {
-                    if (isFullyHidden && mNotificationsVisibleForExpansion) {
+                    if (isFullyHidden && notificationsVisibleForExpansion) {
                         // When the notification becomes fully invisible, let's make sure our
                         // expansion
                         // flag also changes. This can happen if the bouncer shows when dragging
@@ -217,7 +217,7 @@
     }
 
     fun setStackScroller(stackScrollerController: NotificationStackScrollLayoutController) {
-        mStackScrollerController = stackScrollerController
+        this.stackScrollerController = stackScrollerController
         pulseExpanding = stackScrollerController.isPulseExpanding
         stackScrollerController.setOnPulseHeightChangedListener {
             val nowExpanding = isPulseExpanding()
@@ -237,7 +237,7 @@
         }
     }
 
-    fun isPulseExpanding(): Boolean = mStackScrollerController.isPulseExpanding
+    fun isPulseExpanding(): Boolean = stackScrollerController.isPulseExpanding
 
     /**
      * @param visible should notifications be visible
@@ -249,13 +249,13 @@
         animate: Boolean,
         increaseSpeed: Boolean
     ) {
-        mNotificationsVisibleForExpansion = visible
+        notificationsVisibleForExpansion = visible
         updateNotificationVisibility(animate, increaseSpeed)
-        if (!visible && mNotificationsVisible) {
+        if (!visible && notificationsVisible) {
             // If we stopped expanding and we're still visible because we had a pulse that hasn't
             // times out, let's release them all to make sure were not stuck in a state where
             // notifications are visible
-            mHeadsUpManager.releaseAllImmediately()
+            headsUpManager.releaseAllImmediately()
         }
     }
 
@@ -269,12 +269,12 @@
 
     private fun updateNotificationVisibility(animate: Boolean, increaseSpeed: Boolean) {
         // TODO: handle Lockscreen wakeup for bypass when we're not pulsing anymore
-        var visible = mNotificationsVisibleForExpansion || mHeadsUpManager.hasNotifications()
+        var visible = notificationsVisibleForExpansion || headsUpManager.hasNotifications()
         visible = visible && canShowPulsingHuns
 
         if (
             !visible &&
-                mNotificationsVisible &&
+                notificationsVisible &&
                 (wakingUp || willWakeUp) &&
                 outputLinearDozeAmount != 0.0f
         ) {
@@ -290,11 +290,11 @@
         animate: Boolean,
         increaseSpeed: Boolean
     ) {
-        if (mNotificationsVisible == visible) {
+        if (notificationsVisible == visible) {
             return
         }
-        mNotificationsVisible = visible
-        mVisibilityAnimator?.cancel()
+        notificationsVisible = visible
+        visibilityAnimator?.cancel()
         if (animate) {
             notifyAnimationStart(visible)
             startVisibilityAnimation(increaseSpeed)
@@ -371,7 +371,7 @@
             state = statusBarStateController.state,
             changed = changed
         )
-        mStackScrollerController.setDozeAmount(outputEasedDozeAmount)
+        stackScrollerController.setDozeAmount(outputEasedDozeAmount)
         updateHideAmount()
         if (changed && outputLinearDozeAmount == 0.0f) {
             setNotificationsVisible(visible = false, animate = false, increaseSpeed = false)
@@ -475,7 +475,7 @@
             this.collapsedEnoughToHide = collapsedEnough
             if (couldShowPulsingHuns && !canShowPulsingHuns) {
                 updateNotificationVisibility(animate = true, increaseSpeed = true)
-                mHeadsUpManager.releaseAllImmediately()
+                headsUpManager.releaseAllImmediately()
             }
         }
     }
@@ -562,12 +562,12 @@
     }
 
     private fun startVisibilityAnimation(increaseSpeed: Boolean) {
-        if (mNotificationVisibleAmount == 0f || mNotificationVisibleAmount == 1f) {
-            mVisibilityInterpolator =
-                if (mNotificationsVisible) Interpolators.TOUCH_RESPONSE
+        if (notificationVisibleAmount == 0f || notificationVisibleAmount == 1f) {
+            visibilityInterpolator =
+                if (notificationsVisible) Interpolators.TOUCH_RESPONSE
                 else Interpolators.FAST_OUT_SLOW_IN_REVERSE
         }
-        val target = if (mNotificationsVisible) 1.0f else 0.0f
+        val target = if (notificationsVisible) 1.0f else 0.0f
         val visibilityAnimator = ObjectAnimator.ofFloat(this, notificationVisibility, target)
         visibilityAnimator.interpolator = InterpolatorsAndroidX.LINEAR
         var duration = StackStateAnimator.ANIMATION_DURATION_WAKEUP.toLong()
@@ -576,34 +576,34 @@
         }
         visibilityAnimator.duration = duration
         visibilityAnimator.start()
-        mVisibilityAnimator = visibilityAnimator
+        this.visibilityAnimator = visibilityAnimator
     }
 
     private fun setVisibilityAmount(visibilityAmount: Float) {
         logger.logSetVisibilityAmount(visibilityAmount)
-        mLinearVisibilityAmount = visibilityAmount
-        mVisibilityAmount = mVisibilityInterpolator.getInterpolation(visibilityAmount)
+        linearVisibilityAmount = visibilityAmount
+        this.visibilityAmount = visibilityInterpolator.getInterpolation(visibilityAmount)
         handleAnimationFinished()
         updateHideAmount()
     }
 
     private fun handleAnimationFinished() {
-        if (outputLinearDozeAmount == 0.0f || mLinearVisibilityAmount == 0.0f) {
-            mEntrySetToClearWhenFinished.forEach { it.setHeadsUpAnimatingAway(false) }
-            mEntrySetToClearWhenFinished.clear()
+        if (outputLinearDozeAmount == 0.0f || linearVisibilityAmount == 0.0f) {
+            entrySetToClearWhenFinished.forEach { it.setHeadsUpAnimatingAway(false) }
+            entrySetToClearWhenFinished.clear()
         }
     }
 
     private fun updateHideAmount() {
-        val linearAmount = min(1.0f - mLinearVisibilityAmount, outputLinearDozeAmount)
-        val amount = min(1.0f - mVisibilityAmount, outputEasedDozeAmount)
+        val linearAmount = min(1.0f - linearVisibilityAmount, outputLinearDozeAmount)
+        val amount = min(1.0f - visibilityAmount, outputEasedDozeAmount)
         logger.logSetHideAmount(linearAmount)
-        mStackScrollerController.setHideAmount(linearAmount, amount)
+        stackScrollerController.setHideAmount(linearAmount, amount)
         notificationsFullyHidden = linearAmount == 1.0f
     }
 
     private fun notifyAnimationStart(awake: Boolean) {
-        mStackScrollerController.notifyHideAnimationStart(!awake)
+        stackScrollerController.notifyHideAnimationStart(!awake)
     }
 
     override fun onDozingChanged(isDozing: Boolean) {
@@ -615,7 +615,7 @@
     override fun onHeadsUpStateChanged(entry: NotificationEntry, isHeadsUp: Boolean) {
         var animate = shouldAnimateVisibility()
         if (!isHeadsUp) {
-            if (outputLinearDozeAmount != 0.0f && mLinearVisibilityAmount != 0.0f) {
+            if (outputLinearDozeAmount != 0.0f && linearVisibilityAmount != 0.0f) {
                 if (entry.isRowDismissed) {
                     // if we animate, we see the shelf briefly visible. Instead we fully animate
                     // the notification and its background out
@@ -623,11 +623,11 @@
                 } else if (!wakingUp && !willWakeUp) {
                     // TODO: look that this is done properly and not by anyone else
                     entry.setHeadsUpAnimatingAway(true)
-                    mEntrySetToClearWhenFinished.add(entry)
+                    entrySetToClearWhenFinished.add(entry)
                 }
             }
-        } else if (mEntrySetToClearWhenFinished.contains(entry)) {
-            mEntrySetToClearWhenFinished.remove(entry)
+        } else if (entrySetToClearWhenFinished.contains(entry)) {
+            entrySetToClearWhenFinished.remove(entry)
             entry.setHeadsUpAnimatingAway(false)
         }
         updateNotificationVisibility(animate, increaseSpeed = false)
@@ -644,11 +644,11 @@
         pw.println("hardDozeAmountOverrideSource: $hardDozeAmountOverrideSource")
         pw.println("outputLinearDozeAmount: $outputLinearDozeAmount")
         pw.println("outputEasedDozeAmount: $outputEasedDozeAmount")
-        pw.println("mNotificationVisibleAmount: $mNotificationVisibleAmount")
-        pw.println("mNotificationsVisible: $mNotificationsVisible")
-        pw.println("mNotificationsVisibleForExpansion: $mNotificationsVisibleForExpansion")
-        pw.println("mVisibilityAmount: $mVisibilityAmount")
-        pw.println("mLinearVisibilityAmount: $mLinearVisibilityAmount")
+        pw.println("notificationVisibleAmount: $notificationVisibleAmount")
+        pw.println("notificationsVisible: $notificationsVisible")
+        pw.println("notificationsVisibleForExpansion: $notificationsVisibleForExpansion")
+        pw.println("visibilityAmount: $visibilityAmount")
+        pw.println("linearVisibilityAmount: $linearVisibilityAmount")
         pw.println("pulseExpanding: $pulseExpanding")
         pw.println("state: ${StatusBarState.toString(state)}")
         pw.println("fullyAwake: $fullyAwake")
@@ -698,7 +698,7 @@
                 }
 
                 override fun get(coordinator: NotificationWakeUpCoordinator): Float {
-                    return coordinator.mLinearVisibilityAmount
+                    return coordinator.linearVisibilityAmount
                 }
             }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
index 1adfef0..f98a88f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
@@ -68,9 +68,11 @@
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowController;
 import com.android.systemui.statusbar.notification.row.NotificationGuts;
+import com.android.systemui.statusbar.notification.row.data.repository.NotificationRowRepository;
 import com.android.systemui.statusbar.notification.row.shared.HeadsUpStatusBarModel;
 import com.android.systemui.statusbar.notification.row.shared.NotificationContentModel;
 import com.android.systemui.statusbar.notification.row.shared.NotificationRowContentBinderRefactor;
+import com.android.systemui.statusbar.notification.row.shared.RichOngoingContentModel;
 import com.android.systemui.statusbar.notification.stack.PriorityBucket;
 import com.android.systemui.util.ListenerSet;
 
@@ -97,7 +99,7 @@
  * At the moment, there are many things here that shouldn't be and vice-versa. Hopefully we can
  * clean this up in the future.
  */
-public final class NotificationEntry extends ListEntry {
+public final class NotificationEntry extends ListEntry implements NotificationRowRepository {
 
     private final String mKey;
     private StatusBarNotification mSbn;
@@ -159,6 +161,8 @@
             StateFlowKt.MutableStateFlow(null);
     private final MutableStateFlow<CharSequence> mHeadsUpStatusBarTextPublic =
             StateFlowKt.MutableStateFlow(null);
+    private final MutableStateFlow<RichOngoingContentModel> mRichOngoingContentModel =
+            StateFlowKt.MutableStateFlow(null);
 
     // indicates when this entry's view was first attached to a window
     // this value will reset when the view is completely removed from the shade (ie: filtered out)
@@ -945,6 +949,7 @@
     }
 
     /** @see #setHeadsUpStatusBarText(CharSequence) */
+    @NonNull
     public StateFlow<CharSequence> getHeadsUpStatusBarText() {
         return mHeadsUpStatusBarText;
     }
@@ -959,10 +964,17 @@
     }
 
     /** @see #setHeadsUpStatusBarTextPublic(CharSequence) */
+    @NonNull
     public StateFlow<CharSequence> getHeadsUpStatusBarTextPublic() {
         return mHeadsUpStatusBarTextPublic;
     }
 
+    /** Gets the current RON content model, which may be null */
+    @NonNull
+    public StateFlow<RichOngoingContentModel> getRichOngoingContentModel() {
+        return mRichOngoingContentModel;
+    }
+
     /**
      * Sets the text to be displayed on the StatusBar, when this notification is the top pinned
      * heads up, and its content is sensitive right now.
@@ -1047,6 +1059,7 @@
         HeadsUpStatusBarModel headsUpStatusBarModel = contentModel.getHeadsUpStatusBarModel();
         this.mHeadsUpStatusBarText.setValue(headsUpStatusBarModel.getPrivateText());
         this.mHeadsUpStatusBarTextPublic.setValue(headsUpStatusBarModel.getPublicText());
+        this.mRichOngoingContentModel.setValue(contentModel.getRichOngoingContentModel());
     }
 
     /** Information about a suggestion that is being edited. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
index 91bb28e..762c9a1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
@@ -21,8 +21,8 @@
 import android.service.notification.NotificationListenerService;
 
 import com.android.internal.jank.InteractionJankMonitor;
-import com.android.settingslib.statusbar.notification.data.repository.NotificationsSoundPolicyRepository;
-import com.android.settingslib.statusbar.notification.data.repository.NotificationsSoundPolicyRepositoryImpl;
+import com.android.settingslib.statusbar.notification.data.repository.ZenModeRepository;
+import com.android.settingslib.statusbar.notification.data.repository.ZenModeRepositoryImpl;
 import com.android.settingslib.statusbar.notification.domain.interactor.NotificationsSoundPolicyInteractor;
 import com.android.systemui.CoreStartable;
 import com.android.systemui.dagger.SysUISingleton;
@@ -279,19 +279,19 @@
 
     @Provides
     @SysUISingleton
-    public static NotificationsSoundPolicyRepository provideNotificationsSoundPolicyRepository(
+    static ZenModeRepository provideZenModeRepository(
             Context context,
             NotificationManager notificationManager,
             @Application CoroutineScope coroutineScope,
             @Background CoroutineContext coroutineContext) {
-        return new NotificationsSoundPolicyRepositoryImpl(context, notificationManager,
+        return new ZenModeRepositoryImpl(context, notificationManager,
                 coroutineScope, coroutineContext);
     }
 
     @Provides
     @SysUISingleton
-    public static NotificationsSoundPolicyInteractor provideNotificationsSoundPolicyInteractror(
-            NotificationsSoundPolicyRepository repository) {
+    static NotificationsSoundPolicyInteractor provideNotificationsSoundPolicyInteractor(
+            ZenModeRepository repository) {
         return new NotificationsSoundPolicyInteractor(repository);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index 4ba673d..05d7196 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -2950,24 +2950,21 @@
         if (mShowingPublicInitialized && mShowingPublic == oldShowingPublic) {
             return;
         }
-        float oldAlpha = getContentView().getAlpha();
 
         if (!animated) {
-            mPublicLayout.animate().cancel();
-            mPrivateLayout.animate().cancel();
-            if (mChildrenContainer != null) {
-                mChildrenContainer.animate().cancel();
+            if (!NotificationContentAlphaOptimization.isEnabled()
+                    || mShowingPublic != oldShowingPublic) {
+                // Don't reset the alpha or cancel the animation if the showing layout doesn't
+                // change
+                mPublicLayout.animate().cancel();
+                mPrivateLayout.animate().cancel();
+                if (mChildrenContainer != null) {
+                    mChildrenContainer.animate().cancel();
+                }
+                resetAllContentAlphas();
             }
-            resetAllContentAlphas();
             mPublicLayout.setVisibility(mShowingPublic ? View.VISIBLE : View.INVISIBLE);
             updateChildrenVisibility();
-            if (NotificationContentAlphaOptimization.isEnabled()) {
-                // We want to set the old alpha to the now-showing layout to avoid breaking an
-                // on-going animation
-                if (oldAlpha != 1f) {
-                    setAlphaAndLayerType(mShowingPublic ? mPublicLayout : mPrivateLayout, oldAlpha);
-                }
-            }
         } else {
             animateShowingPublic(delay, duration, mShowingPublic);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index 6f00d96..7fc331d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -72,6 +72,8 @@
 import com.android.systemui.util.Compile;
 import com.android.systemui.util.DumpUtilsKt;
 
+import kotlinx.coroutines.DisposableHandle;
+
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -109,6 +111,8 @@
     private View mHeadsUpChild;
     private HybridNotificationView mSingleLineView;
 
+    @Nullable public DisposableHandle mContractedBinderHandle;
+
     private RemoteInputView mExpandedRemoteInput;
     private RemoteInputView mHeadsUpRemoteInput;
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
index e704140..492d802 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
@@ -46,6 +46,10 @@
 import com.android.systemui.statusbar.notification.ConversationNotificationProcessor
 import com.android.systemui.statusbar.notification.InflationException
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED
+import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_EXPANDED
+import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP
+import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_SINGLELINE
 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.BindParams
 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED
 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED
@@ -62,6 +66,7 @@
 import com.android.systemui.statusbar.notification.row.shared.NewRemoteViews
 import com.android.systemui.statusbar.notification.row.shared.NotificationContentModel
 import com.android.systemui.statusbar.notification.row.shared.NotificationRowContentBinderRefactor
+import com.android.systemui.statusbar.notification.row.shared.RichOngoingContentModel
 import com.android.systemui.statusbar.notification.row.ui.viewbinder.SingleLineConversationViewBinder
 import com.android.systemui.statusbar.notification.row.ui.viewbinder.SingleLineViewBinder
 import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper
@@ -86,6 +91,8 @@
     private val remoteViewCache: NotifRemoteViewCache,
     private val remoteInputManager: NotificationRemoteInputManager,
     private val conversationProcessor: ConversationNotificationProcessor,
+    private val ronExtractor: RichOngoingNotificationContentExtractor,
+    private val ronInflater: RichOngoingNotificationViewInflater,
     @NotifInflation private val inflationExecutor: Executor,
     private val smartReplyStateInflater: SmartReplyStateInflater,
     private val notifLayoutInflaterFactoryProvider: NotifLayoutInflaterFactory.Provider,
@@ -133,6 +140,8 @@
                 remoteViewCache,
                 entry,
                 conversationProcessor,
+                ronExtractor,
+                ronInflater,
                 row,
                 bindParams.isMinimized,
                 bindParams.usesIncreasedHeight,
@@ -177,6 +186,7 @@
                 notifLayoutInflaterFactoryProvider = notifLayoutInflaterFactoryProvider,
                 headsUpStyleProvider = headsUpStyleProvider,
                 conversationProcessor = conversationProcessor,
+                ronExtractor = ronExtractor,
                 logger = logger,
             )
         inflateSmartReplyViews(
@@ -255,39 +265,31 @@
     ) {
         when (inflateFlag) {
             FLAG_CONTENT_VIEW_CONTRACTED ->
-                row.privateLayout.performWhenContentInactive(
-                    NotificationContentView.VISIBLE_TYPE_CONTRACTED
-                ) {
+                row.privateLayout.performWhenContentInactive(VISIBLE_TYPE_CONTRACTED) {
+                    row.privateLayout.mContractedBinderHandle?.dispose()
+                    row.privateLayout.mContractedBinderHandle = null
                     row.privateLayout.setContractedChild(null)
                     remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED)
                 }
             FLAG_CONTENT_VIEW_EXPANDED ->
-                row.privateLayout.performWhenContentInactive(
-                    NotificationContentView.VISIBLE_TYPE_EXPANDED
-                ) {
+                row.privateLayout.performWhenContentInactive(VISIBLE_TYPE_EXPANDED) {
                     row.privateLayout.setExpandedChild(null)
                     remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED)
                 }
             FLAG_CONTENT_VIEW_HEADS_UP ->
-                row.privateLayout.performWhenContentInactive(
-                    NotificationContentView.VISIBLE_TYPE_HEADSUP
-                ) {
+                row.privateLayout.performWhenContentInactive(VISIBLE_TYPE_HEADSUP) {
                     row.privateLayout.setHeadsUpChild(null)
                     remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP)
                     row.privateLayout.setHeadsUpInflatedSmartReplies(null)
                 }
             FLAG_CONTENT_VIEW_PUBLIC ->
-                row.publicLayout.performWhenContentInactive(
-                    NotificationContentView.VISIBLE_TYPE_CONTRACTED
-                ) {
+                row.publicLayout.performWhenContentInactive(VISIBLE_TYPE_CONTRACTED) {
                     row.publicLayout.setContractedChild(null)
                     remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC)
                 }
             FLAG_CONTENT_VIEW_SINGLE_LINE -> {
                 if (AsyncHybridViewInflation.isEnabled) {
-                    row.privateLayout.performWhenContentInactive(
-                        NotificationContentView.VISIBLE_TYPE_SINGLELINE
-                    ) {
+                    row.privateLayout.performWhenContentInactive(VISIBLE_TYPE_SINGLELINE) {
                         row.privateLayout.setSingleLineView(null)
                     }
                 }
@@ -308,32 +310,22 @@
         @InflationFlag contentViews: Int
     ) {
         if (contentViews and FLAG_CONTENT_VIEW_CONTRACTED != 0) {
-            row.privateLayout.removeContentInactiveRunnable(
-                NotificationContentView.VISIBLE_TYPE_CONTRACTED
-            )
+            row.privateLayout.removeContentInactiveRunnable(VISIBLE_TYPE_CONTRACTED)
         }
         if (contentViews and FLAG_CONTENT_VIEW_EXPANDED != 0) {
-            row.privateLayout.removeContentInactiveRunnable(
-                NotificationContentView.VISIBLE_TYPE_EXPANDED
-            )
+            row.privateLayout.removeContentInactiveRunnable(VISIBLE_TYPE_EXPANDED)
         }
         if (contentViews and FLAG_CONTENT_VIEW_HEADS_UP != 0) {
-            row.privateLayout.removeContentInactiveRunnable(
-                NotificationContentView.VISIBLE_TYPE_HEADSUP
-            )
+            row.privateLayout.removeContentInactiveRunnable(VISIBLE_TYPE_HEADSUP)
         }
         if (contentViews and FLAG_CONTENT_VIEW_PUBLIC != 0) {
-            row.publicLayout.removeContentInactiveRunnable(
-                NotificationContentView.VISIBLE_TYPE_CONTRACTED
-            )
+            row.publicLayout.removeContentInactiveRunnable(VISIBLE_TYPE_CONTRACTED)
         }
         if (
             AsyncHybridViewInflation.isEnabled &&
                 contentViews and FLAG_CONTENT_VIEW_SINGLE_LINE != 0
         ) {
-            row.privateLayout.removeContentInactiveRunnable(
-                NotificationContentView.VISIBLE_TYPE_SINGLELINE
-            )
+            row.privateLayout.removeContentInactiveRunnable(VISIBLE_TYPE_SINGLELINE)
         }
     }
 
@@ -353,6 +345,8 @@
         private val remoteViewCache: NotifRemoteViewCache,
         private val entry: NotificationEntry,
         private val conversationProcessor: ConversationNotificationProcessor,
+        private val ronExtractor: RichOngoingNotificationContentExtractor,
+        private val ronInflater: RichOngoingNotificationViewInflater,
         private val row: ExpandableNotificationRow,
         private val isMinimized: Boolean,
         private val usesIncreasedHeight: Boolean,
@@ -432,6 +426,7 @@
                     notifLayoutInflaterFactoryProvider = notifLayoutInflaterFactoryProvider,
                     headsUpStyleProvider = headsUpStyleProvider,
                     conversationProcessor = conversationProcessor,
+                    ronExtractor = ronExtractor,
                     logger = logger
                 )
             logger.logAsyncTaskProgress(
@@ -463,6 +458,21 @@
                         )
                     }
             }
+
+            if (reInflateFlags and CONTENT_VIEWS_TO_CREATE_RICH_ONGOING != 0) {
+                logger.logAsyncTaskProgress(entry, "inflating RON view")
+                inflationProgress.richOngoingNotificationViewHolder =
+                    inflationProgress.contentModel.richOngoingContentModel?.let {
+                        ronInflater.inflateView(
+                            contentModel = it,
+                            existingView = row.privateLayout.contractedChild,
+                            entry = entry,
+                            systemUiContext = context,
+                            parentView = row.privateLayout
+                        )
+                    }
+            }
+
             logger.logAsyncTaskProgress(entry, "getting row image resolver (on wrong thread!)")
             val imageResolver = row.imageResolver
             // wait for image resolver to finish preloading
@@ -568,6 +578,7 @@
         var inflatedSmartReplyState: InflatedSmartReplyState? = null
         var expandedInflatedSmartReplies: InflatedSmartReplyViewHolder? = null
         var headsUpInflatedSmartReplies: InflatedSmartReplyViewHolder? = null
+        var richOngoingNotificationViewHolder: InflatedContentViewHolder? = null
 
         // Inflated SingleLineView that lacks the UI State
         var inflatedSingleLineView: HybridNotificationView? = null
@@ -602,6 +613,7 @@
             val inflateHeadsUp =
                 (reInflateFlags and FLAG_CONTENT_VIEW_HEADS_UP != 0 &&
                     result.remoteViews.headsUp != null)
+
             if (inflateContracted || inflateExpanded || inflateHeadsUp) {
                 logger.logAsyncTaskProgress(entry, "inflating contracted smart reply state")
                 result.inflatedSmartReplyState = inflater.inflateSmartReplyState(entry)
@@ -643,6 +655,7 @@
             notifLayoutInflaterFactoryProvider: NotifLayoutInflaterFactory.Provider,
             headsUpStyleProvider: HeadsUpStyleProvider,
             conversationProcessor: ConversationNotificationProcessor,
+            ronExtractor: RichOngoingNotificationContentExtractor,
             logger: NotificationRowContentBinderLogger
         ): InflationProgress {
             // process conversations and extract the messaging style
@@ -651,9 +664,24 @@
                     conversationProcessor.processNotification(entry, builder, logger)
                 } else null
 
+            val richOngoingContentModel =
+                if (reInflateFlags and CONTENT_VIEWS_TO_CREATE_RICH_ONGOING != 0) {
+                    ronExtractor.extractContentModel(
+                        entry = entry,
+                        builder = builder,
+                        systemUIContext = systemUIContext,
+                        packageContext = packageContext
+                    )
+                } else {
+                    // if we're not re-inflating any RON views, make sure the model doesn't change
+                    entry.richOngoingContentModel.value
+                }
+
+            val remoteViewsFlags = getRemoteViewsFlags(reInflateFlags, richOngoingContentModel)
+
             val remoteViews =
                 createRemoteViews(
-                    reInflateFlags = reInflateFlags,
+                    reInflateFlags = remoteViewsFlags,
                     builder = builder,
                     isMinimized = isMinimized,
                     usesIncreasedHeight = usesIncreasedHeight,
@@ -688,6 +716,7 @@
                 NotificationContentModel(
                     headsUpStatusBarModel = headsUpStatusBarModel,
                     singleLineViewModel = singleLineViewModel,
+                    richOngoingContentModel = richOngoingContentModel,
                 )
 
             return InflationProgress(
@@ -815,7 +844,7 @@
             val publicLayout = row.publicLayout
             val runningInflations = HashMap<Int, CancellationSignal>()
             var flag = FLAG_CONTENT_VIEW_CONTRACTED
-            if (reInflateFlags and flag != 0) {
+            if (reInflateFlags and flag != 0 && result.remoteViews.contracted != null) {
                 val isNewView =
                     !canReapplyRemoteView(
                         newView = result.remoteViews.contracted,
@@ -829,7 +858,7 @@
                         }
 
                         override val remoteView: RemoteViews
-                            get() = result.remoteViews.contracted!!
+                            get() = result.remoteViews.contracted
                     }
                 logger.logAsyncTaskProgress(entry, "applying contracted view")
                 applyRemoteView(
@@ -847,104 +876,89 @@
                     callback = callback,
                     parentLayout = privateLayout,
                     existingView = privateLayout.contractedChild,
-                    existingWrapper =
-                        privateLayout.getVisibleWrapper(
-                            NotificationContentView.VISIBLE_TYPE_CONTRACTED
-                        ),
+                    existingWrapper = privateLayout.getVisibleWrapper(VISIBLE_TYPE_CONTRACTED),
                     runningInflations = runningInflations,
                     applyCallback = applyCallback,
                     logger = logger
                 )
             }
             flag = FLAG_CONTENT_VIEW_EXPANDED
-            if (reInflateFlags and flag != 0) {
-                if (result.remoteViews.expanded != null) {
-                    val isNewView =
-                        !canReapplyRemoteView(
-                            newView = result.remoteViews.expanded,
-                            oldView =
-                                remoteViewCache.getCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED)
-                        )
-                    val applyCallback: ApplyCallback =
-                        object : ApplyCallback() {
-                            override fun setResultView(v: View) {
-                                logger.logAsyncTaskProgress(entry, "expanded view applied")
-                                result.inflatedExpandedView = v
-                            }
-
-                            override val remoteView: RemoteViews
-                                get() = result.remoteViews.expanded
-                        }
-                    logger.logAsyncTaskProgress(entry, "applying expanded view")
-                    applyRemoteView(
-                        inflationExecutor = inflationExecutor,
-                        inflateSynchronously = inflateSynchronously,
-                        isMinimized = isMinimized,
-                        result = result,
-                        reInflateFlags = reInflateFlags,
-                        inflationId = flag,
-                        remoteViewCache = remoteViewCache,
-                        entry = entry,
-                        row = row,
-                        isNewView = isNewView,
-                        remoteViewClickHandler = remoteViewClickHandler,
-                        callback = callback,
-                        parentLayout = privateLayout,
-                        existingView = privateLayout.expandedChild,
-                        existingWrapper =
-                            privateLayout.getVisibleWrapper(
-                                NotificationContentView.VISIBLE_TYPE_EXPANDED
-                            ),
-                        runningInflations = runningInflations,
-                        applyCallback = applyCallback,
-                        logger = logger
+            if (reInflateFlags and flag != 0 && result.remoteViews.expanded != null) {
+                val isNewView =
+                    !canReapplyRemoteView(
+                        newView = result.remoteViews.expanded,
+                        oldView = remoteViewCache.getCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED)
                     )
-                }
+                val applyCallback: ApplyCallback =
+                    object : ApplyCallback() {
+                        override fun setResultView(v: View) {
+                            logger.logAsyncTaskProgress(entry, "expanded view applied")
+                            result.inflatedExpandedView = v
+                        }
+
+                        override val remoteView: RemoteViews
+                            get() = result.remoteViews.expanded
+                    }
+                logger.logAsyncTaskProgress(entry, "applying expanded view")
+                applyRemoteView(
+                    inflationExecutor = inflationExecutor,
+                    inflateSynchronously = inflateSynchronously,
+                    isMinimized = isMinimized,
+                    result = result,
+                    reInflateFlags = reInflateFlags,
+                    inflationId = flag,
+                    remoteViewCache = remoteViewCache,
+                    entry = entry,
+                    row = row,
+                    isNewView = isNewView,
+                    remoteViewClickHandler = remoteViewClickHandler,
+                    callback = callback,
+                    parentLayout = privateLayout,
+                    existingView = privateLayout.expandedChild,
+                    existingWrapper = privateLayout.getVisibleWrapper(VISIBLE_TYPE_EXPANDED),
+                    runningInflations = runningInflations,
+                    applyCallback = applyCallback,
+                    logger = logger
+                )
             }
             flag = FLAG_CONTENT_VIEW_HEADS_UP
-            if (reInflateFlags and flag != 0) {
-                if (result.remoteViews.headsUp != null) {
-                    val isNewView =
-                        !canReapplyRemoteView(
-                            newView = result.remoteViews.headsUp,
-                            oldView =
-                                remoteViewCache.getCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP)
-                        )
-                    val applyCallback: ApplyCallback =
-                        object : ApplyCallback() {
-                            override fun setResultView(v: View) {
-                                logger.logAsyncTaskProgress(entry, "heads up view applied")
-                                result.inflatedHeadsUpView = v
-                            }
-
-                            override val remoteView: RemoteViews
-                                get() = result.remoteViews.headsUp
-                        }
-                    logger.logAsyncTaskProgress(entry, "applying heads up view")
-                    applyRemoteView(
-                        inflationExecutor = inflationExecutor,
-                        inflateSynchronously = inflateSynchronously,
-                        isMinimized = isMinimized,
-                        result = result,
-                        reInflateFlags = reInflateFlags,
-                        inflationId = flag,
-                        remoteViewCache = remoteViewCache,
-                        entry = entry,
-                        row = row,
-                        isNewView = isNewView,
-                        remoteViewClickHandler = remoteViewClickHandler,
-                        callback = callback,
-                        parentLayout = privateLayout,
-                        existingView = privateLayout.headsUpChild,
-                        existingWrapper =
-                            privateLayout.getVisibleWrapper(
-                                NotificationContentView.VISIBLE_TYPE_HEADSUP
-                            ),
-                        runningInflations = runningInflations,
-                        applyCallback = applyCallback,
-                        logger = logger
+            if (reInflateFlags and flag != 0 && result.remoteViews.headsUp != null) {
+                val isNewView =
+                    !canReapplyRemoteView(
+                        newView = result.remoteViews.headsUp,
+                        oldView = remoteViewCache.getCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP)
                     )
-                }
+                val applyCallback: ApplyCallback =
+                    object : ApplyCallback() {
+                        override fun setResultView(v: View) {
+                            logger.logAsyncTaskProgress(entry, "heads up view applied")
+                            result.inflatedHeadsUpView = v
+                        }
+
+                        override val remoteView: RemoteViews
+                            get() = result.remoteViews.headsUp
+                    }
+                logger.logAsyncTaskProgress(entry, "applying heads up view")
+                applyRemoteView(
+                    inflationExecutor = inflationExecutor,
+                    inflateSynchronously = inflateSynchronously,
+                    isMinimized = isMinimized,
+                    result = result,
+                    reInflateFlags = reInflateFlags,
+                    inflationId = flag,
+                    remoteViewCache = remoteViewCache,
+                    entry = entry,
+                    row = row,
+                    isNewView = isNewView,
+                    remoteViewClickHandler = remoteViewClickHandler,
+                    callback = callback,
+                    parentLayout = privateLayout,
+                    existingView = privateLayout.headsUpChild,
+                    existingWrapper = privateLayout.getVisibleWrapper(VISIBLE_TYPE_HEADSUP),
+                    runningInflations = runningInflations,
+                    applyCallback = applyCallback,
+                    logger = logger
+                )
             }
             flag = FLAG_CONTENT_VIEW_PUBLIC
             if (reInflateFlags and flag != 0) {
@@ -979,10 +993,7 @@
                     callback = callback,
                     parentLayout = publicLayout,
                     existingView = publicLayout.contractedChild,
-                    existingWrapper =
-                        publicLayout.getVisibleWrapper(
-                            NotificationContentView.VISIBLE_TYPE_CONTRACTED
-                        ),
+                    existingWrapper = publicLayout.getVisibleWrapper(VISIBLE_TYPE_CONTRACTED),
                     runningInflations = runningInflations,
                     applyCallback = applyCallback,
                     logger = logger
@@ -1359,79 +1370,34 @@
             if (runningInflations.isNotEmpty()) {
                 return false
             }
-            val privateLayout = row.privateLayout
-            val publicLayout = row.publicLayout
             logger.logAsyncTaskProgress(entry, "finishing")
-            if (reInflateFlags and FLAG_CONTENT_VIEW_CONTRACTED != 0) {
-                if (result.inflatedContentView != null) {
-                    // New view case
-                    privateLayout.setContractedChild(result.inflatedContentView)
-                    remoteViewCache.putCachedView(
-                        entry,
-                        FLAG_CONTENT_VIEW_CONTRACTED,
-                        result.remoteViews.contracted
-                    )
-                } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED)) {
-                    // Reinflation case. Only update if it's still cached (i.e. view has not been
-                    // freed while inflating).
-                    remoteViewCache.putCachedView(
-                        entry,
-                        FLAG_CONTENT_VIEW_CONTRACTED,
-                        result.remoteViews.contracted
-                    )
-                }
+
+            // before updating the content model, stop existing binding if necessary
+            val hasRichOngoingContentModel = result.contentModel.richOngoingContentModel != null
+            val requestedRichOngoing = reInflateFlags and CONTENT_VIEWS_TO_CREATE_RICH_ONGOING != 0
+            val rejectedRichOngoing = requestedRichOngoing && !hasRichOngoingContentModel
+            if (result.richOngoingNotificationViewHolder != null || rejectedRichOngoing) {
+                row.privateLayout.mContractedBinderHandle?.dispose()
+                row.privateLayout.mContractedBinderHandle = null
             }
-            if (reInflateFlags and FLAG_CONTENT_VIEW_EXPANDED != 0) {
-                if (result.inflatedExpandedView != null) {
-                    privateLayout.setExpandedChild(result.inflatedExpandedView)
-                    remoteViewCache.putCachedView(
-                        entry,
-                        FLAG_CONTENT_VIEW_EXPANDED,
-                        result.remoteViews.expanded
-                    )
-                } else if (result.remoteViews.expanded == null) {
-                    privateLayout.setExpandedChild(null)
-                    remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED)
-                } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED)) {
-                    remoteViewCache.putCachedView(
-                        entry,
-                        FLAG_CONTENT_VIEW_EXPANDED,
-                        result.remoteViews.expanded
-                    )
-                }
-                if (result.remoteViews.expanded != null) {
-                    privateLayout.setExpandedInflatedSmartReplies(
-                        result.expandedInflatedSmartReplies
-                    )
-                } else {
-                    privateLayout.setExpandedInflatedSmartReplies(null)
-                }
-                row.setExpandable(result.remoteViews.expanded != null)
-            }
-            if (reInflateFlags and FLAG_CONTENT_VIEW_HEADS_UP != 0) {
-                if (result.inflatedHeadsUpView != null) {
-                    privateLayout.setHeadsUpChild(result.inflatedHeadsUpView)
-                    remoteViewCache.putCachedView(
-                        entry,
-                        FLAG_CONTENT_VIEW_HEADS_UP,
-                        result.remoteViews.headsUp
-                    )
-                } else if (result.remoteViews.headsUp == null) {
-                    privateLayout.setHeadsUpChild(null)
-                    remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP)
-                } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP)) {
-                    remoteViewCache.putCachedView(
-                        entry,
-                        FLAG_CONTENT_VIEW_HEADS_UP,
-                        result.remoteViews.headsUp
-                    )
-                }
-                if (result.remoteViews.headsUp != null) {
-                    privateLayout.setHeadsUpInflatedSmartReplies(result.headsUpInflatedSmartReplies)
-                } else {
-                    privateLayout.setHeadsUpInflatedSmartReplies(null)
-                }
-            }
+
+            // set the content model after disposal and before setting new rich ongoing view
+            entry.setContentModel(result.contentModel)
+            result.inflatedSmartReplyState?.let { row.privateLayout.setInflatedSmartReplyState(it) }
+
+            // set normal remote views (skipping rich ongoing states when that model exists)
+            val remoteViewsFlags =
+                getRemoteViewsFlags(reInflateFlags, result.contentModel.richOngoingContentModel)
+            setContentViewsFromRemoteViews(
+                remoteViewsFlags,
+                entry,
+                remoteViewCache,
+                result,
+                row,
+                isMinimized,
+            )
+
+            // set single line view
             if (
                 AsyncHybridViewInflation.isEnabled &&
                     reInflateFlags and FLAG_CONTENT_VIEW_SINGLE_LINE != 0
@@ -1444,80 +1410,144 @@
                     } else {
                         SingleLineViewBinder.bind(viewModel, singleLineView)
                     }
-                    privateLayout.setSingleLineView(result.inflatedSingleLineView)
+                    row.privateLayout.setSingleLineView(result.inflatedSingleLineView)
                 }
             }
-            result.inflatedSmartReplyState?.let { privateLayout.setInflatedSmartReplyState(it) }
-            if (reInflateFlags and FLAG_CONTENT_VIEW_PUBLIC != 0) {
-                if (result.inflatedPublicView != null) {
-                    publicLayout.setContractedChild(result.inflatedPublicView)
-                    remoteViewCache.putCachedView(
-                        entry,
-                        FLAG_CONTENT_VIEW_PUBLIC,
-                        result.remoteViews.public
-                    )
-                } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC)) {
-                    remoteViewCache.putCachedView(
-                        entry,
-                        FLAG_CONTENT_VIEW_PUBLIC,
-                        result.remoteViews.public
-                    )
-                }
+
+            // after updating the content model, set the view, then start the new binder
+            result.richOngoingNotificationViewHolder?.let { viewHolder ->
+                row.privateLayout.contractedChild = viewHolder.view
+                row.privateLayout.expandedChild = null
+                row.privateLayout.headsUpChild = null
+                row.privateLayout.setExpandedInflatedSmartReplies(null)
+                row.privateLayout.setHeadsUpInflatedSmartReplies(null)
+                row.privateLayout.mContractedBinderHandle =
+                    viewHolder.binder.setupContentViewBinder()
+                row.setExpandable(false)
+                remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED)
+                remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED)
+                remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP)
             }
-            if (AsyncGroupHeaderViewInflation.isEnabled) {
-                if (reInflateFlags and FLAG_GROUP_SUMMARY_HEADER != 0) {
-                    if (result.inflatedGroupHeaderView != null) {
-                        // We need to set if the row is minimized before setting the group header to
-                        // make sure the setting of header view works correctly
-                        row.setIsMinimized(isMinimized)
-                        row.setGroupHeader(/* headerView= */ result.inflatedGroupHeaderView)
-                        remoteViewCache.putCachedView(
-                            entry,
-                            FLAG_GROUP_SUMMARY_HEADER,
-                            result.remoteViews.normalGroupHeader
-                        )
-                    } else if (remoteViewCache.hasCachedView(entry, FLAG_GROUP_SUMMARY_HEADER)) {
-                        // Re-inflation case. Only update if it's still cached (i.e. view has not
-                        // been freed while inflating).
-                        remoteViewCache.putCachedView(
-                            entry,
-                            FLAG_GROUP_SUMMARY_HEADER,
-                            result.remoteViews.normalGroupHeader
-                        )
-                    }
-                }
-                if (reInflateFlags and FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER != 0) {
-                    if (result.inflatedMinimizedGroupHeaderView != null) {
-                        // We need to set if the row is minimized before setting the group header to
-                        // make sure the setting of header view works correctly
-                        row.setIsMinimized(isMinimized)
-                        row.setMinimizedGroupHeader(
-                            /* headerView= */ result.inflatedMinimizedGroupHeaderView
-                        )
-                        remoteViewCache.putCachedView(
-                            entry,
-                            FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER,
-                            result.remoteViews.minimizedGroupHeader
-                        )
-                    } else if (
-                        remoteViewCache.hasCachedView(entry, FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER)
-                    ) {
-                        // Re-inflation case. Only update if it's still cached (i.e. view has not
-                        // been freed while inflating).
-                        remoteViewCache.putCachedView(
-                            entry,
-                            FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER,
-                            result.remoteViews.normalGroupHeader
-                        )
-                    }
-                }
-            }
-            entry.setContentModel(result.contentModel)
+
             Trace.endAsyncSection(APPLY_TRACE_METHOD, System.identityHashCode(row))
             endListener?.onAsyncInflationFinished(entry)
             return true
         }
 
+        private fun setContentViewsFromRemoteViews(
+            @InflationFlag reInflateFlags: Int,
+            entry: NotificationEntry,
+            remoteViewCache: NotifRemoteViewCache,
+            result: InflationProgress,
+            row: ExpandableNotificationRow,
+            isMinimized: Boolean,
+        ) {
+            val privateLayout = row.privateLayout
+            val publicLayout = row.publicLayout
+            val remoteViewsUpdater = RemoteViewsUpdater(reInflateFlags, entry, remoteViewCache)
+            remoteViewsUpdater.setContentView(
+                FLAG_CONTENT_VIEW_CONTRACTED,
+                result.remoteViews.contracted,
+                result.inflatedContentView,
+                privateLayout::setContractedChild
+            )
+            remoteViewsUpdater.setContentView(
+                FLAG_CONTENT_VIEW_EXPANDED,
+                result.remoteViews.expanded,
+                result.inflatedExpandedView,
+                privateLayout::setExpandedChild
+            )
+            remoteViewsUpdater.setSmartReplies(
+                FLAG_CONTENT_VIEW_EXPANDED,
+                result.remoteViews.expanded,
+                result.expandedInflatedSmartReplies,
+                privateLayout::setExpandedInflatedSmartReplies
+            )
+            if (reInflateFlags and FLAG_CONTENT_VIEW_EXPANDED != 0) {
+                row.setExpandable(result.remoteViews.expanded != null)
+            }
+            remoteViewsUpdater.setContentView(
+                FLAG_CONTENT_VIEW_HEADS_UP,
+                result.remoteViews.headsUp,
+                result.inflatedHeadsUpView,
+                privateLayout::setHeadsUpChild
+            )
+            remoteViewsUpdater.setSmartReplies(
+                FLAG_CONTENT_VIEW_HEADS_UP,
+                result.remoteViews.headsUp,
+                result.headsUpInflatedSmartReplies,
+                privateLayout::setHeadsUpInflatedSmartReplies
+            )
+            remoteViewsUpdater.setContentView(
+                FLAG_CONTENT_VIEW_PUBLIC,
+                result.remoteViews.public,
+                result.inflatedPublicView,
+                publicLayout::setContractedChild
+            )
+            if (AsyncGroupHeaderViewInflation.isEnabled) {
+                remoteViewsUpdater.setContentView(
+                    FLAG_GROUP_SUMMARY_HEADER,
+                    result.remoteViews.normalGroupHeader,
+                    result.inflatedGroupHeaderView,
+                ) { views ->
+                    row.setIsMinimized(isMinimized)
+                    row.setGroupHeader(views)
+                }
+                remoteViewsUpdater.setContentView(
+                    FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER,
+                    result.remoteViews.minimizedGroupHeader,
+                    result.inflatedMinimizedGroupHeaderView,
+                ) { views ->
+                    row.setIsMinimized(isMinimized)
+                    row.setMinimizedGroupHeader(views)
+                }
+            }
+        }
+
+        private class RemoteViewsUpdater(
+            @InflationFlag private val reInflateFlags: Int,
+            private val entry: NotificationEntry,
+            private val remoteViewCache: NotifRemoteViewCache,
+        ) {
+            fun <V : View> setContentView(
+                @InflationFlag flagState: Int,
+                remoteViews: RemoteViews?,
+                view: V?,
+                setView: (V?) -> Unit,
+            ) {
+                val clearViewFlags = FLAG_CONTENT_VIEW_HEADS_UP or FLAG_CONTENT_VIEW_EXPANDED
+                val shouldClearView = flagState and clearViewFlags != 0
+                if (reInflateFlags and flagState != 0) {
+                    if (view != null) {
+                        setView(view)
+                        remoteViewCache.putCachedView(entry, flagState, remoteViews)
+                    } else if (shouldClearView && remoteViews == null) {
+                        setView(null)
+                        remoteViewCache.removeCachedView(entry, flagState)
+                    } else if (remoteViewCache.hasCachedView(entry, flagState)) {
+                        // Re-inflation case. Only update if it's still cached (i.e. view has not
+                        // been freed while inflating).
+                        remoteViewCache.putCachedView(entry, flagState, remoteViews)
+                    }
+                }
+            }
+
+            fun setSmartReplies(
+                @InflationFlag flagState: Int,
+                remoteViews: RemoteViews?,
+                smartReplies: InflatedSmartReplyViewHolder?,
+                setSmartReplies: (InflatedSmartReplyViewHolder?) -> Unit,
+            ) {
+                if (reInflateFlags and flagState != 0) {
+                    if (remoteViews != null) {
+                        setSmartReplies(smartReplies)
+                    } else {
+                        setSmartReplies(null)
+                    }
+                }
+            }
+        }
+
         private fun createExpandedView(
             builder: Notification.Builder,
             isMinimized: Boolean
@@ -1562,6 +1592,21 @@
                     !oldView.hasFlags(RemoteViews.FLAG_REAPPLY_DISALLOWED)
         }
 
+        @InflationFlag
+        private fun getRemoteViewsFlags(
+            @InflationFlag reInflateFlags: Int,
+            richOngoingContentModel: RichOngoingContentModel?
+        ): Int =
+            if (richOngoingContentModel != null) {
+                reInflateFlags and CONTENT_VIEWS_TO_CREATE_RICH_ONGOING.inv()
+            } else {
+                reInflateFlags
+            }
+
+        @InflationFlag
+        private const val CONTENT_VIEWS_TO_CREATE_RICH_ONGOING =
+            FLAG_CONTENT_VIEW_CONTRACTED or FLAG_CONTENT_VIEW_EXPANDED or FLAG_CONTENT_VIEW_HEADS_UP
+
         private const val ASYNC_TASK_TRACE_METHOD =
             "NotificationRowContentBinderImpl.AsyncInflationTask"
         private const val APPLY_TRACE_METHOD = "NotificationRowContentBinderImpl#apply"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java
index 84f2f66..c630c4d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java
@@ -18,6 +18,8 @@
 
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.statusbar.notification.row.shared.NotificationRowContentBinderRefactor;
+import com.android.systemui.statusbar.notification.row.shared.RichOngoingNotificationFlag;
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.RichOngoingViewModelComponent;
 
 import dagger.Binds;
 import dagger.Module;
@@ -28,7 +30,7 @@
 /**
  * Dagger Module containing notification row and view inflation implementations.
  */
-@Module
+@Module(subcomponents = {RichOngoingViewModelComponent.class})
 public abstract class NotificationRowModule {
 
     /**
@@ -47,6 +49,25 @@
         }
     }
 
+    /** Provides ron content model extractor. */
+    @Provides
+    @SysUISingleton
+    public static RichOngoingNotificationContentExtractor provideRonContentExtractor(
+            Provider<RichOngoingNotificationContentExtractorImpl> realImpl
+    ) {
+        if (RichOngoingNotificationFlag.isEnabled()) {
+            return realImpl.get();
+        } else {
+            return new NoOpRichOngoingNotificationContentExtractor();
+        }
+    }
+
+    /** Provides ron view inflater. */
+    @Binds
+    @SysUISingleton
+    public abstract RichOngoingNotificationViewInflater provideRonViewInflater(
+            RichOngoingNotificationViewInflaterImpl impl);
+
     /**
      * Provides notification remote view cache instance.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt
new file mode 100644
index 0000000..b5ea861
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row
+
+import android.app.Notification
+import android.app.PendingIntent
+import android.content.Context
+import android.util.Log
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.row.shared.IconModel
+import com.android.systemui.statusbar.notification.row.shared.RichOngoingContentModel
+import com.android.systemui.statusbar.notification.row.shared.RichOngoingNotificationFlag
+import com.android.systemui.statusbar.notification.row.shared.TimerContentModel
+import java.time.Duration
+import java.time.LocalDate
+import java.time.LocalDateTime
+import java.time.LocalTime
+import java.time.ZoneId
+import javax.inject.Inject
+
+/**
+ * Interface which provides a [RichOngoingContentModel] for a given [Notification] when one is
+ * applicable to the given style.
+ */
+interface RichOngoingNotificationContentExtractor {
+    fun extractContentModel(
+        entry: NotificationEntry,
+        builder: Notification.Builder,
+        systemUIContext: Context,
+        packageContext: Context
+    ): RichOngoingContentModel?
+}
+
+class NoOpRichOngoingNotificationContentExtractor : RichOngoingNotificationContentExtractor {
+    override fun extractContentModel(
+        entry: NotificationEntry,
+        builder: Notification.Builder,
+        systemUIContext: Context,
+        packageContext: Context
+    ): RichOngoingContentModel? = null
+}
+
+@SysUISingleton
+class RichOngoingNotificationContentExtractorImpl @Inject constructor() :
+    RichOngoingNotificationContentExtractor {
+
+    init {
+        /* check if */ RichOngoingNotificationFlag.isUnexpectedlyInLegacyMode()
+    }
+
+    override fun extractContentModel(
+        entry: NotificationEntry,
+        builder: Notification.Builder,
+        systemUIContext: Context,
+        packageContext: Context
+    ): RichOngoingContentModel? =
+        try {
+            val sbn = entry.sbn
+            val notification = sbn.notification
+            val icon = IconModel(notification.smallIcon)
+            if (sbn.packageName == "com.google.android.deskclock") {
+                when (notification.channelId) {
+                    "Timers v2" -> {
+                        parseTimerNotification(notification, icon)
+                    }
+                    "Stopwatch v2" -> {
+                        Log.i("RONs", "Can't process stopwatch yet")
+                        null
+                    }
+                    else -> {
+                        Log.i("RONs", "Can't process channel '${notification.channelId}'")
+                        null
+                    }
+                }
+            } else null
+        } catch (e: Exception) {
+            Log.e("RONs", "Error parsing RON", e)
+            null
+        }
+
+    /**
+     * FOR PROTOTYPING ONLY: create a RON TimerContentModel using the time information available
+     * inside the sortKey of the clock app's timer notifications.
+     */
+    private fun parseTimerNotification(
+        notification: Notification,
+        icon: IconModel
+    ): TimerContentModel {
+        // sortKey=1 0|↺7|RUNNING|â–¶16:21:58.523|Σ0:05:00|Δ0:00:03|⏳0:04:57
+        // sortKey=1 0|↺7|PAUSED|Σ0:05:00|Δ0:04:54|⏳0:00:06
+        // sortKey=1 1|↺7|RUNNING|â–¶16:30:28.433|Σ0:04:05|Δ0:00:06|⏳0:03:59
+        // sortKey=1 0|↺7|RUNNING|â–¶16:36:18.350|Σ0:05:00|Δ0:01:42|⏳0:03:18
+        // sortKey=1 2|↺7|RUNNING|â–¶16:38:37.816|Σ0:02:00|Δ0:01:09|⏳0:00:51
+        // â–¶ = "current" time (when updated)
+        // Σ = total time
+        // Δ = time elapsed
+        // ⏳ = time remaining
+        val sortKey = notification.sortKey
+        val (_, _, state, extra) = sortKey.split("|", limit = 4)
+        return when (state) {
+            "PAUSED" -> {
+                val (total, _, remaining) = extra.split("|")
+                val timeRemaining = parseTimeDelta(remaining)
+                TimerContentModel(
+                    icon = icon,
+                    name = total,
+                    state =
+                        TimerContentModel.TimerState.Paused(
+                            timeRemaining = timeRemaining,
+                            resumeIntent = notification.findActionWithName("Resume"),
+                            resetIntent = notification.findActionWithName("Reset"),
+                        )
+                )
+            }
+            "RUNNING" -> {
+                val (current, total, _, remaining) = extra.split("|")
+                val finishTime = parseCurrentTime(current) + parseTimeDelta(remaining).toMillis()
+                TimerContentModel(
+                    icon = icon,
+                    name = total,
+                    state =
+                        TimerContentModel.TimerState.Running(
+                            finishTime = finishTime,
+                            pauseIntent = notification.findActionWithName("Pause"),
+                            addOneMinuteIntent = notification.findActionWithName("Add 1 min"),
+                        )
+                )
+            }
+            else -> error("unknown state ($state) in sortKey=$sortKey")
+        }
+    }
+
+    private fun Notification.findActionWithName(name: String): PendingIntent? {
+        return actions.firstOrNull { name == it.title?.toString() }?.actionIntent
+    }
+
+    private fun parseCurrentTime(current: String): Long {
+        val (hour, minute, second, millis) = current.replace("â–¶", "").split(":", ".")
+        // NOTE: this won't work correctly at/around midnight.  It's just for prototyping.
+        val localDateTime =
+            LocalDateTime.of(
+                LocalDate.now(),
+                LocalTime.of(hour.toInt(), minute.toInt(), second.toInt(), millis.toInt() * 1000000)
+            )
+        val offset = ZoneId.systemDefault().rules.getOffset(localDateTime)
+        return localDateTime.toInstant(offset).toEpochMilli()
+    }
+
+    private fun parseTimeDelta(delta: String): Duration {
+        val (hour, minute, second) = delta.replace("Σ", "").replace("⏳", "").split(":")
+        return Duration.ofHours(hour.toLong())
+            .plusMinutes(minute.toLong())
+            .plusSeconds(second.toLong())
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationViewInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationViewInflater.kt
new file mode 100644
index 0000000..e9c4960
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationViewInflater.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row
+
+import android.app.Notification
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.row.shared.RichOngoingContentModel
+import com.android.systemui.statusbar.notification.row.shared.RichOngoingNotificationFlag
+import com.android.systemui.statusbar.notification.row.shared.StopwatchContentModel
+import com.android.systemui.statusbar.notification.row.shared.TimerContentModel
+import com.android.systemui.statusbar.notification.row.ui.view.TimerView
+import com.android.systemui.statusbar.notification.row.ui.viewbinder.TimerViewBinder
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.RichOngoingViewModelComponent
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.TimerViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.DisposableHandle
+
+fun interface DeferredContentViewBinder {
+    fun setupContentViewBinder(): DisposableHandle
+}
+
+class InflatedContentViewHolder(val view: View, val binder: DeferredContentViewBinder)
+
+/**
+ * Interface which provides a [RichOngoingContentModel] for a given [Notification] when one is
+ * applicable to the given style.
+ */
+interface RichOngoingNotificationViewInflater {
+    fun inflateView(
+        contentModel: RichOngoingContentModel,
+        existingView: View?,
+        entry: NotificationEntry,
+        systemUiContext: Context,
+        parentView: ViewGroup,
+    ): InflatedContentViewHolder?
+}
+
+@SysUISingleton
+class RichOngoingNotificationViewInflaterImpl
+@Inject
+constructor(
+    private val viewModelComponentFactory: RichOngoingViewModelComponent.Factory,
+) : RichOngoingNotificationViewInflater {
+
+    override fun inflateView(
+        contentModel: RichOngoingContentModel,
+        existingView: View?,
+        entry: NotificationEntry,
+        systemUiContext: Context,
+        parentView: ViewGroup,
+    ): InflatedContentViewHolder? {
+        if (RichOngoingNotificationFlag.isUnexpectedlyInLegacyMode()) return null
+        val component = viewModelComponentFactory.create(entry)
+        return when (contentModel) {
+            is TimerContentModel ->
+                inflateTimerView(
+                    existingView,
+                    component::createTimerViewModel,
+                    systemUiContext,
+                    parentView
+                )
+            is StopwatchContentModel -> TODO("Not yet implemented")
+        }
+    }
+
+    private fun inflateTimerView(
+        existingView: View?,
+        createViewModel: () -> TimerViewModel,
+        systemUiContext: Context,
+        parentView: ViewGroup,
+    ): InflatedContentViewHolder? {
+        if (existingView is TimerView && !existingView.isReinflateNeeded()) return null
+        val newView =
+            LayoutInflater.from(systemUiContext)
+                .inflate(
+                    R.layout.rich_ongoing_timer_notification,
+                    parentView,
+                    /* attachToRoot= */ false
+                ) as TimerView
+        return InflatedContentViewHolder(newView) {
+            TimerViewBinder.bindWhileAttached(newView, createViewModel())
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/data/repository/NotificationRowRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/data/repository/NotificationRowRepository.kt
new file mode 100644
index 0000000..bac887b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/data/repository/NotificationRowRepository.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.statusbar.notification.row.data.repository
+
+import com.android.systemui.statusbar.notification.row.shared.RichOngoingContentModel
+import kotlinx.coroutines.flow.StateFlow
+
+/** A repository of states relating to a specific notification row. */
+interface NotificationRowRepository {
+    /**
+     * A flow of an immutable data class with the current state of the Rich Ongoing Notification
+     * content, if applicable.
+     */
+    val richOngoingContentModel: StateFlow<RichOngoingContentModel?>
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractor.kt
new file mode 100644
index 0000000..4705ace
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractor.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.statusbar.notification.row.domain.interactor
+
+import com.android.systemui.statusbar.notification.row.data.repository.NotificationRowRepository
+import com.android.systemui.statusbar.notification.row.shared.TimerContentModel
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filterIsInstance
+
+/** Interactor specific to a particular notification row. */
+class NotificationRowInteractor @Inject constructor(repository: NotificationRowRepository) {
+    /** Content of a rich ongoing timer notification. */
+    val timerContentModel: Flow<TimerContentModel> =
+        repository.richOngoingContentModel.filterIsInstance<TimerContentModel>()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/IconModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/IconModel.kt
new file mode 100644
index 0000000..e611938
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/IconModel.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.statusbar.notification.row.shared
+
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.Icon
+
+// TODO: figure out how to support lazy resolution of the drawable, e.g. on unrelated text change
+class IconModel(val icon: Icon) {
+    var drawable: Drawable? = null
+
+    override fun equals(other: Any?): Boolean =
+        when (other) {
+            null -> false
+            (other === this) -> true
+            !is IconModel -> false
+            else -> other.icon.sameAs(icon)
+        }
+
+    override fun toString(): String = "IconModel(icon=$icon, drawable=$drawable)"
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/NotificationContentModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/NotificationContentModel.kt
index b2421bc..46010a1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/NotificationContentModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/NotificationContentModel.kt
@@ -21,4 +21,7 @@
 data class NotificationContentModel(
     val headsUpStatusBarModel: HeadsUpStatusBarModel,
     val singleLineViewModel: SingleLineViewModel? = null,
+    val richOngoingContentModel: RichOngoingContentModel? = null,
 )
+
+sealed interface RichOngoingContentModel
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingClock.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingClock.kt
new file mode 100644
index 0000000..5584701
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingClock.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row.shared
+
+import android.app.PendingIntent
+import java.time.Duration
+
+/**
+ * Represents a simple timer that counts down to a time.
+ *
+ * @param name the label for the timer
+ * @param state state of the timer, including time and whether it is paused or running
+ */
+data class TimerContentModel(
+    val icon: IconModel,
+    val name: String,
+    val state: TimerState,
+) : RichOngoingContentModel {
+    /** The state (paused or running) of the timer, and relevant time */
+    sealed interface TimerState {
+        /**
+         * Indicates a running timer
+         *
+         * @param finishTime the time in ms since epoch that the timer will finish
+         * @param pauseIntent the action for pausing the timer
+         */
+        data class Running(
+            val finishTime: Long,
+            val pauseIntent: PendingIntent?,
+            val addOneMinuteIntent: PendingIntent?,
+        ) : TimerState
+
+        /**
+         * Indicates a paused timer
+         *
+         * @param timeRemaining the time in ms remaining on the paused timer
+         * @param resumeIntent the action for resuming the timer
+         */
+        data class Paused(
+            val timeRemaining: Duration,
+            val resumeIntent: PendingIntent?,
+            val resetIntent: PendingIntent?,
+        ) : TimerState
+    }
+}
+
+/**
+ * Represents a simple stopwatch that counts up and allows tracking laps.
+ *
+ * @param state state of the stopwatch, including time and whether it is paused or running
+ * @param lapDurations a list of durations of each completed lap
+ */
+data class StopwatchContentModel(
+    val icon: IconModel,
+    val state: StopwatchState,
+    val lapDurations: List<Long>,
+) : RichOngoingContentModel {
+    /** The state (paused or running) of the stopwatch, and relevant time */
+    sealed interface StopwatchState {
+        /**
+         * Indicates a running stopwatch
+         *
+         * @param startTime the time in ms since epoch that the stopwatch started, plus any
+         *   accumulated pause time
+         * @param pauseIntent the action for pausing the stopwatch
+         */
+        data class Running(
+            val startTime: Long,
+            val pauseIntent: PendingIntent,
+        ) : StopwatchState
+
+        /**
+         * Indicates a paused stopwatch
+         *
+         * @param timeElapsed the time in ms elapsed on the stopwatch
+         * @param resumeIntent the action for resuming the stopwatch
+         */
+        data class Paused(
+            val timeElapsed: Duration,
+            val resumeIntent: PendingIntent,
+        ) : StopwatchState
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingNotificationFlag.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingNotificationFlag.kt
new file mode 100644
index 0000000..4a7f7cd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingNotificationFlag.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row.shared
+
+import android.app.Flags
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading or using the api rich ongoing flag state. */
+@Suppress("NOTHING_TO_INLINE")
+object RichOngoingNotificationFlag {
+    /** The aconfig flag name */
+    const val FLAG_NAME = Flags.FLAG_API_RICH_ONGOING
+
+    /** A token used for dependency declaration */
+    val token: FlagToken
+        get() = FlagToken(FLAG_NAME, isEnabled)
+
+    /** Is the refactor enabled */
+    @JvmStatic
+    inline val isEnabled
+        get() = Flags.apiRichOngoing()
+
+    /**
+     * Called to ensure code is only run when the flag is enabled. This protects users from the
+     * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+     * build to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun isUnexpectedlyInLegacyMode() =
+        RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+    /**
+     * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+     * the flag is enabled to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/ConfigurationTracker.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/ConfigurationTracker.kt
new file mode 100644
index 0000000..95c507c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/ConfigurationTracker.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row.ui.view
+
+import android.content.pm.ActivityInfo.CONFIG_ASSETS_PATHS
+import android.content.pm.ActivityInfo.CONFIG_DENSITY
+import android.content.pm.ActivityInfo.CONFIG_FONT_SCALE
+import android.content.pm.ActivityInfo.CONFIG_LAYOUT_DIRECTION
+import android.content.pm.ActivityInfo.CONFIG_LOCALE
+import android.content.pm.ActivityInfo.CONFIG_UI_MODE
+import android.content.res.Configuration
+import android.content.res.Resources
+
+/**
+ * Tracks the active configuration when constructed and returns (when queried) whether the
+ * configuration has unhandled changes.
+ */
+class ConfigurationTracker(
+    private val resources: Resources,
+    private val unhandledConfigChanges: Int
+) {
+    private val initialConfig = Configuration(resources.configuration)
+
+    constructor(
+        resources: Resources,
+        handlesDensityFontScale: Boolean = false,
+        handlesTheme: Boolean = false,
+        handlesLocaleAndLayout: Boolean = true,
+    ) : this(
+        resources,
+        unhandledConfigChanges =
+            (if (handlesDensityFontScale) 0 else CONFIG_DENSITY or CONFIG_FONT_SCALE) or
+                (if (handlesTheme) 0 else CONFIG_ASSETS_PATHS or CONFIG_UI_MODE) or
+                (if (handlesLocaleAndLayout) 0 else CONFIG_LOCALE or CONFIG_LAYOUT_DIRECTION)
+    )
+
+    /**
+     * Whether the current configuration has unhandled changes relative to the initial configuration
+     */
+    fun hasUnhandledConfigChange(): Boolean =
+        initialConfig.diff(resources.configuration) and unhandledConfigChanges != 0
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerButtonView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerButtonView.kt
new file mode 100644
index 0000000..0d83ace
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerButtonView.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row.ui.view
+
+import android.annotation.DrawableRes
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.Button
+
+class TimerButtonView
+@JvmOverloads
+constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = 0,
+    defStyleRes: Int = 0,
+) : Button(context, attrs, defStyleAttr, defStyleRes) {
+
+    private val Int.dp: Int
+        get() = (this * context.resources.displayMetrics.density).toInt()
+
+    fun setIcon(@DrawableRes icon: Int) {
+        val drawable = context.getDrawable(icon)
+        drawable?.setBounds(0, 0, 24.dp, 24.dp)
+        setCompoundDrawablesRelative(drawable, null, null, null)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerView.kt
new file mode 100644
index 0000000..2e164d6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerView.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row.ui.view
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.os.SystemClock
+import android.util.AttributeSet
+import android.widget.Chronometer
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.view.isVisible
+import com.android.systemui.res.R
+
+class TimerView
+@JvmOverloads
+constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = 0,
+    defStyleRes: Int = 0,
+) : ConstraintLayout(context, attrs, defStyleAttr, defStyleRes) {
+
+    private val configTracker = ConfigurationTracker(resources)
+
+    private lateinit var icon: ImageView
+    private lateinit var label: TextView
+    private lateinit var chronometer: Chronometer
+    private lateinit var pausedTimeRemaining: TextView
+    lateinit var mainButton: TimerButtonView
+        private set
+
+    lateinit var altButton: TimerButtonView
+        private set
+
+    override fun onFinishInflate() {
+        super.onFinishInflate()
+        icon = requireViewById(R.id.icon)
+        label = requireViewById(R.id.label)
+        chronometer = requireViewById(R.id.chronoRemaining)
+        pausedTimeRemaining = requireViewById(R.id.pausedTimeRemaining)
+        mainButton = requireViewById(R.id.mainButton)
+        altButton = requireViewById(R.id.altButton)
+    }
+
+    /** the resources configuration has changed such that the view needs to be reinflated */
+    fun isReinflateNeeded(): Boolean = configTracker.hasUnhandledConfigChange()
+
+    fun setIcon(iconDrawable: Drawable?) {
+        this.icon.setImageDrawable(iconDrawable)
+    }
+
+    fun setLabel(label: String) {
+        this.label.text = label
+    }
+
+    fun setPausedTime(pausedTime: String?) {
+        if (pausedTime != null) {
+            pausedTimeRemaining.text = pausedTime
+            pausedTimeRemaining.isVisible = true
+        } else {
+            pausedTimeRemaining.isVisible = false
+        }
+    }
+
+    fun setCountdownTime(countdownTimeMs: Long?) {
+        if (countdownTimeMs != null) {
+            chronometer.base =
+                countdownTimeMs - System.currentTimeMillis() + SystemClock.elapsedRealtime()
+            chronometer.isVisible = true
+            chronometer.start()
+        } else {
+            chronometer.isVisible = false
+            chronometer.stop()
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/TimerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/TimerViewBinder.kt
new file mode 100644
index 0000000..c9ff589
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/TimerViewBinder.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row.ui.viewbinder
+
+import android.view.View
+import androidx.core.view.isGone
+import androidx.lifecycle.lifecycleScope
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.statusbar.notification.row.ui.view.TimerButtonView
+import com.android.systemui.statusbar.notification.row.ui.view.TimerView
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.TimerViewModel
+import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+
+/** Binds a [TimerView] to its [view model][TimerViewModel]. */
+object TimerViewBinder {
+    fun bindWhileAttached(
+        view: TimerView,
+        viewModel: TimerViewModel,
+    ): DisposableHandle {
+        return view.repeatWhenAttached { lifecycleScope.launch { bind(view, viewModel) } }
+    }
+
+    suspend fun bind(
+        view: TimerView,
+        viewModel: TimerViewModel,
+    ) = coroutineScope {
+        launch { viewModel.icon.collect { view.setIcon(it) } }
+        launch { viewModel.label.collect { view.setLabel(it) } }
+        launch { viewModel.pausedTime.collect { view.setPausedTime(it) } }
+        launch { viewModel.countdownTime.collect { view.setCountdownTime(it) } }
+        launch { viewModel.mainButtonModel.collect { bind(view.mainButton, it) } }
+        launch { viewModel.altButtonModel.collect { bind(view.altButton, it) } }
+    }
+
+    fun bind(buttonView: TimerButtonView, model: TimerViewModel.ButtonViewModel?) {
+        if (model != null) {
+            buttonView.setIcon(model.iconRes)
+            buttonView.setText(model.labelRes)
+            buttonView.setOnClickListener(
+                model.pendingIntent?.let { pendingIntent ->
+                    View.OnClickListener { pendingIntent.send() }
+                }
+            )
+            buttonView.isEnabled = model.pendingIntent != null
+        }
+        buttonView.isGone = model == null
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/RichOngoingViewModelComponent.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/RichOngoingViewModelComponent.kt
new file mode 100644
index 0000000..dad52a3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/RichOngoingViewModelComponent.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row.ui.viewmodel
+
+// noinspection CleanArchitectureDependencyViolation
+import com.android.systemui.statusbar.notification.row.data.repository.NotificationRowRepository
+import dagger.BindsInstance
+import dagger.Subcomponent
+
+@Subcomponent
+interface RichOngoingViewModelComponent {
+
+    @Subcomponent.Factory
+    interface Factory {
+        /** Creates an instance of [RichOngoingViewModelComponent]. */
+        fun create(
+            @BindsInstance repository: NotificationRowRepository
+        ): RichOngoingViewModelComponent
+    }
+
+    fun createTimerViewModel(): TimerViewModel
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModel.kt
new file mode 100644
index 0000000..a85c87f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModel.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row.ui.viewmodel
+
+import android.annotation.DrawableRes
+import android.annotation.StringRes
+import android.app.PendingIntent
+import android.graphics.drawable.Drawable
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.statusbar.notification.row.domain.interactor.NotificationRowInteractor
+import com.android.systemui.statusbar.notification.row.shared.RichOngoingNotificationFlag
+import com.android.systemui.statusbar.notification.row.shared.TimerContentModel.TimerState
+import com.android.systemui.util.kotlin.FlowDumperImpl
+import java.time.Duration
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapNotNull
+
+/** A view model for Timer notifications. */
+class TimerViewModel
+@Inject
+constructor(
+    dumpManager: DumpManager,
+    rowInteractor: NotificationRowInteractor,
+) : FlowDumperImpl(dumpManager) {
+    init {
+        /* check if */ RichOngoingNotificationFlag.isUnexpectedlyInLegacyMode()
+    }
+
+    private val state: Flow<TimerState> = rowInteractor.timerContentModel.mapNotNull { it.state }
+
+    val icon: Flow<Drawable?> = rowInteractor.timerContentModel.mapNotNull { it.icon.drawable }
+
+    val label: Flow<String> = rowInteractor.timerContentModel.mapNotNull { it.name }
+
+    val countdownTime: Flow<Long?> = state.map { (it as? TimerState.Running)?.finishTime }
+
+    val pausedTime: Flow<String?> =
+        state.map { (it as? TimerState.Paused)?.timeRemaining?.format() }
+
+    val mainButtonModel: Flow<ButtonViewModel> =
+        state.map {
+            when (it) {
+                is TimerState.Paused ->
+                    ButtonViewModel(
+                        it.resumeIntent,
+                        com.android.systemui.res.R.string.controls_media_resume, // "Resume",
+                        com.android.systemui.res.R.drawable.ic_media_play
+                    )
+                is TimerState.Running ->
+                    ButtonViewModel(
+                        it.pauseIntent,
+                        com.android.systemui.res.R.string.controls_media_button_pause, // "Pause",
+                        com.android.systemui.res.R.drawable.ic_media_pause
+                    )
+            }
+        }
+
+    val altButtonModel: Flow<ButtonViewModel?> =
+        state.map {
+            when (it) {
+                is TimerState.Paused ->
+                    it.resetIntent?.let { resetIntent ->
+                        ButtonViewModel(
+                            resetIntent,
+                            com.android.systemui.res.R.string.reset, // "Reset",
+                            com.android.systemui.res.R.drawable.ic_close_white_rounded
+                        )
+                    }
+                is TimerState.Running ->
+                    it.addOneMinuteIntent?.let { addOneMinuteIntent ->
+                        ButtonViewModel(
+                            addOneMinuteIntent,
+                            com.android.systemui.res.R.string.add, // "Add 1 minute",
+                            com.android.systemui.res.R.drawable.ic_add
+                        )
+                    }
+            }
+        }
+
+    data class ButtonViewModel(
+        val pendingIntent: PendingIntent?,
+        @StringRes val labelRes: Int,
+        @DrawableRes val iconRes: Int,
+    )
+}
+
+private fun Duration.format(): String {
+    val hours = this.toHours()
+    return if (hours > 0) {
+        String.format("%d:%02d:%02d", hours, toMinutesPart(), toSecondsPart())
+    } else {
+        String.format("%d:%02d", toMinutes(), toSecondsPart())
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
index 456c321..fbddc06 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
@@ -64,6 +64,7 @@
      *  Used to read bouncer states.
      */
     private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+    private float mStackTop;
     private float mStackCutoff;
     private int mScrollY;
     private float mOverScrollTopAmount;
@@ -130,13 +131,13 @@
     /** Distance of top of notifications panel from top of screen. */
     private float mStackY = 0;
 
-    /** Height of notifications panel. */
+    /** Height of notifications panel interpolated by the expansion fraction. */
     private float mStackHeight = 0;
 
     /** Fraction of shade expansion. */
     private float mExpansionFraction;
 
-    /** Height of the notifications panel without top padding when expansion completes. */
+    /** Height of the notifications panel when expansion completes. */
     private float mStackEndHeight;
 
     /** Whether we are swiping up. */
@@ -175,8 +176,7 @@
     }
 
     /**
-     * @param stackEndHeight Height of the notifications panel without top padding
-     *                       when expansion completes.
+     * @see #getStackEndHeight()
      */
     public void setStackEndHeight(float stackEndHeight) {
         mStackEndHeight = stackEndHeight;
@@ -186,6 +186,7 @@
      * @param stackY Distance of top of notifications panel from top of screen.
      */
     public void setStackY(float stackY) {
+        SceneContainerFlag.assertInLegacyMode();
         mStackY = stackY;
     }
 
@@ -193,6 +194,7 @@
      * @return Distance of top of notifications panel from top of screen.
      */
     public float getStackY() {
+        SceneContainerFlag.assertInLegacyMode();
         return mStackY;
     }
 
@@ -254,14 +256,14 @@
     }
 
     /**
-     * @param stackHeight Height of notifications panel.
+     * @see #getStackHeight()
      */
     public void setStackHeight(float stackHeight) {
         mStackHeight = stackHeight;
     }
 
     /**
-     * @return Height of notifications panel.
+     * @return Height of notifications panel interpolated by the expansion fraction.
      */
     public float getStackHeight() {
         return mStackHeight;
@@ -348,6 +350,18 @@
         return mZDistanceBetweenElements;
     }
 
+    /** Y coordinate in view pixels of the top of the notification stack */
+    public float getStackTop() {
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return 0f;
+        return mStackTop;
+    }
+
+    /** @see #getStackTop() */
+    public void setStackTop(float mStackTop) {
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
+        this.mStackTop = mStackTop;
+    }
+
     /**
      * Y coordinate in view pixels above which the bottom of the notification stack / shelf / footer
      * must be.
@@ -769,6 +783,8 @@
 
     @Override
     public void dump(PrintWriter pw, String[] args) {
+        pw.println("mStackTop=" + mStackTop);
+        pw.println("mStackCutoff" + mStackCutoff);
         pw.println("mTopPadding=" + mTopPadding);
         pw.println("mStackTopMargin=" + mStackTopMargin);
         pw.println("mStackTranslation=" + mStackTranslation);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 0e77ed4..d54e66e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -834,7 +834,7 @@
         drawDebugInfo(canvas, y, Color.RED, /* label= */ "y = " + y);
 
         if (SceneContainerFlag.isEnabled()) {
-            y = (int) mScrollViewFields.getStackTop();
+            y = (int) mAmbientState.getStackTop();
             drawDebugInfo(canvas, y, Color.RED, /* label= */ "getStackTop() = " + y);
 
             y = (int) mAmbientState.getStackCutoff();
@@ -1181,9 +1181,11 @@
         updateAlgorithmLayoutMinHeight();
         updateOwnTranslationZ();
 
-        // Give The Algorithm information regarding the QS height so it can layout notifications
-        // properly. Needed for some devices that grows notifications down-to-top
-        mStackScrollAlgorithm.updateQSFrameTop(mQsHeader == null ? 0 : mQsHeader.getHeight());
+        if (!SceneContainerFlag.isEnabled()) {
+            // Give The Algorithm information regarding the QS height so it can layout notifications
+            // properly. Needed for some devices that grows notifications down-to-top
+            mStackScrollAlgorithm.updateQSFrameTop(mQsHeader == null ? 0 : mQsHeader.getHeight());
+        }
 
         // Once the layout has finished, we don't need to animate any scrolling clampings anymore.
         mAnimateStackYForContentHeightChange = false;
@@ -1214,7 +1216,7 @@
 
     @Override
     public void setStackTop(float stackTop) {
-        mScrollViewFields.setStackTop(stackTop);
+        mAmbientState.setStackTop(stackTop);
         // TODO(b/332574413): replace the following with using stackTop
         updateTopPadding(stackTop, isAddOrRemoveAnimationPending());
     }
@@ -1424,11 +1426,7 @@
         if (mAmbientState.isBouncerInTransit() && mQsExpansionFraction > 0f) {
             fraction = BouncerPanelExpansionCalculator.aboutToShowBouncerProgress(fraction);
         }
-        // TODO(b/322228881): Clean up scene container vs legacy behavior in NSSL
-        if (SceneContainerFlag.isEnabled()) {
-            // stackY should be driven by scene container, not NSSL
-            mAmbientState.setStackY(getTopPadding());
-        } else {
+        if (!SceneContainerFlag.isEnabled()) {
             final float stackY = MathUtils.lerp(0, endTopPosition, fraction);
             mAmbientState.setStackY(stackY);
         }
@@ -1442,22 +1440,40 @@
     @VisibleForTesting
     public void updateStackEndHeightAndStackHeight(float fraction) {
         final float oldStackHeight = mAmbientState.getStackHeight();
-        if (mQsExpansionFraction <= 0 && !shouldSkipHeightUpdate()) {
-            final float endHeight = updateStackEndHeight(
-                    getHeight(), getEmptyBottomMargin(), getTopPadding());
+        if (SceneContainerFlag.isEnabled()) {
+            final float endHeight;
+            if (!shouldSkipHeightUpdate()) {
+                endHeight = updateStackEndHeight();
+            } else {
+                endHeight = mAmbientState.getStackEndHeight();
+            }
             updateStackHeight(endHeight, fraction);
         } else {
-            // Always updateStackHeight to prevent jumps in the stack height when this fraction
-            // suddenly reapplies after a freeze.
-            final float endHeight = mAmbientState.getStackEndHeight();
-            updateStackHeight(endHeight, fraction);
+            if (mQsExpansionFraction <= 0 && !shouldSkipHeightUpdate()) {
+                final float endHeight = updateStackEndHeight(
+                        getHeight(), getEmptyBottomMargin(), getTopPadding());
+                updateStackHeight(endHeight, fraction);
+            } else {
+                // Always updateStackHeight to prevent jumps in the stack height when this fraction
+                // suddenly reapplies after a freeze.
+                final float endHeight = mAmbientState.getStackEndHeight();
+                updateStackHeight(endHeight, fraction);
+            }
         }
         if (oldStackHeight != mAmbientState.getStackHeight()) {
             requestChildrenUpdate();
         }
     }
 
+    private float updateStackEndHeight() {
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return 0f;
+        float height = Math.max(0f, mAmbientState.getStackCutoff() - mAmbientState.getStackTop());
+        mAmbientState.setStackEndHeight(height);
+        return height;
+    }
+
     private float updateStackEndHeight(float height, float bottomMargin, float topPadding) {
+        SceneContainerFlag.assertInLegacyMode();
         final float stackEndHeight;
         if (mMaxDisplayedNotifications != -1) {
             // The stack intrinsic height already contains the correct value when there is a limit
@@ -1811,6 +1827,7 @@
     }
 
     public void setQsHeader(ViewGroup qsHeader) {
+        SceneContainerFlag.assertInLegacyMode();
         mQsHeader = qsHeader;
     }
 
@@ -2662,6 +2679,7 @@
     }
 
     public void setMaxTopPadding(int maxTopPadding) {
+        SceneContainerFlag.assertInLegacyMode();
         mMaxTopPadding = maxTopPadding;
     }
 
@@ -2682,6 +2700,7 @@
     }
 
     public float getTopPaddingOverflow() {
+        SceneContainerFlag.assertInLegacyMode();
         return mTopPaddingOverflow;
     }
 
@@ -3721,7 +3740,7 @@
 
     protected boolean isInsideQsHeader(MotionEvent ev) {
         if (SceneContainerFlag.isEnabled()) {
-            return ev.getY() < mScrollViewFields.getStackTop();
+            return ev.getY() < mAmbientState.getStackTop();
         }
 
         mQsHeader.getBoundsOnScreen(mQsHeaderBound);
@@ -4641,6 +4660,7 @@
     }
 
     public boolean isEmptyShadeViewVisible() {
+        SceneContainerFlag.assertInLegacyMode();
         return mEmptyShadeView.isVisible();
     }
 
@@ -4919,6 +4939,7 @@
     }
 
     public void setQsFullScreen(boolean qsFullScreen) {
+        SceneContainerFlag.assertInLegacyMode();
         if (FooterViewRefactor.isEnabled()) {
             if (qsFullScreen == mQsFullScreen) {
                 return;  // no change
@@ -5095,6 +5116,7 @@
     }
 
     public void setExpandingVelocity(float expandingVelocity) {
+        SceneContainerFlag.assertInLegacyMode();
         mAmbientState.setExpandingVelocity(expandingVelocity);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt
index 2e86ad9..97ec391 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt
@@ -32,8 +32,6 @@
 class ScrollViewFields {
     /** Used to produce the clipping path */
     var scrimClippingShape: ShadeScrimShape? = null
-    /** Y coordinate in view pixels of the top of the notification stack */
-    var stackTop: Float = 0f
     /** Y coordinate in view pixels of the top of the HUN */
     var headsUpTop: Float = 0f
     /** Whether the notifications are scrolled all the way to the top (i.e. when freshly opened) */
@@ -76,7 +74,6 @@
     fun dump(pw: IndentingPrintWriter) {
         pw.printSection("StackViewStates") {
             pw.println("scrimClippingShape", scrimClippingShape)
-            pw.println("stackTop", stackTop)
             pw.println("headsUpTop", headsUpTop)
             pw.println("isScrolledToTop", isScrolledToTop)
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index f9efc07..ee7b5c4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -29,6 +29,7 @@
 import com.android.keyguard.BouncerPanelExpansionCalculator;
 import com.android.systemui.animation.ShadeInterpolation;
 import com.android.systemui.res.R;
+import com.android.systemui.scene.shared.flag.SceneContainerFlag;
 import com.android.systemui.shade.transition.LargeScreenShadeInterpolator;
 import com.android.systemui.statusbar.EmptyShadeView;
 import com.android.systemui.statusbar.NotificationShelf;
@@ -332,8 +333,10 @@
 
     private void updateClipping(StackScrollAlgorithmState algorithmState,
             AmbientState ambientState) {
-        float drawStart = ambientState.isOnKeyguard() ? 0
+        float stackTop = SceneContainerFlag.isEnabled() ? ambientState.getStackTop()
                 : ambientState.getStackY() - ambientState.getScrollY();
+        float drawStart = ambientState.isOnKeyguard() ? 0
+                : stackTop;
         float clipStart = 0;
         int childCount = algorithmState.visibleChildren.size();
         boolean firstHeadsUp = true;
@@ -442,12 +445,26 @@
         state.visibleChildren.clear();
         state.visibleChildren.ensureCapacity(childCount);
         int notGoneIndex = 0;
+        boolean emptyShadeVisible = false;
         for (int i = 0; i < childCount; i++) {
             ExpandableView v = (ExpandableView) mHostView.getChildAt(i);
             if (v.getVisibility() != View.GONE) {
                 if (v == ambientState.getShelf()) {
                     continue;
                 }
+                if (FooterViewRefactor.isEnabled()) {
+                    if (v instanceof EmptyShadeView) {
+                        emptyShadeVisible = true;
+                    }
+                    if (v instanceof FooterView) {
+                        if (emptyShadeVisible || notGoneIndex == 0) {
+                            // if the empty shade is visible or the footer is the first visible
+                            // view, we're in a transitory state so let's leave the footer alone.
+                            continue;
+                        }
+                    }
+                }
+
                 notGoneIndex = updateNotGoneIndex(state, notGoneIndex, v);
                 if (v instanceof ExpandableNotificationRow row) {
 
@@ -641,7 +658,10 @@
         // Incoming views have yTranslation=0 by default.
         viewState.setYTranslation(algorithmState.mCurrentYPosition);
 
-        float viewEnd = viewState.getYTranslation() + viewState.height + ambientState.getStackY();
+        float stackTop = SceneContainerFlag.isEnabled()
+                ? ambientState.getStackTop()
+                : ambientState.getStackY();
+        float viewEnd = stackTop + viewState.getYTranslation() + viewState.height;
         maybeUpdateHeadsUpIsVisible(viewState, ambientState.isShadeExpanded(),
                 view.mustStayOnScreen(),
                 /* topVisible= */ viewState.getYTranslation() >= mNotificationScrimPadding,
@@ -681,7 +701,9 @@
             }
         } else {
             if (view instanceof EmptyShadeView) {
-                float fullHeight = ambientState.getLayoutMaxHeight() + mMarginBottom
+                float fullHeight = SceneContainerFlag.isEnabled()
+                        ? ambientState.getStackCutoff() - ambientState.getStackTop()
+                        : ambientState.getLayoutMaxHeight() + mMarginBottom
                         - ambientState.getStackY();
                 viewState.setYTranslation((fullHeight - getMaxAllowedChildHeight(view)) / 2f);
             } else if (view != ambientState.getTrackedHeadsUpRow()) {
@@ -726,7 +748,7 @@
                 + mPaddingBetweenElements;
 
         setLocation(view.getViewState(), algorithmState.mCurrentYPosition, i);
-        viewState.setYTranslation(viewState.getYTranslation() + ambientState.getStackY());
+        viewState.setYTranslation(viewState.getYTranslation() + stackTop);
     }
 
     @VisibleForTesting
@@ -1002,8 +1024,11 @@
         // Animate pinned HUN bottom corners to and from original roundness.
         final float originalCornerRadius =
                 row.isLastInSection() ? 1f : (mSmallCornerRadius / mLargeCornerRadius);
+        final float stackTop = SceneContainerFlag.isEnabled()
+                ? ambientState.getStackTop()
+                : ambientState.getStackY();
         final float bottomValue = computeCornerRoundnessForPinnedHun(mHostView.getHeight(),
-                ambientState.getStackY(), getMaxAllowedChildHeight(row), originalCornerRadius);
+                stackTop, getMaxAllowedChildHeight(row), originalCornerRadius);
         row.requestBottomRoundness(bottomValue, STACK_SCROLL_ALGO);
         row.addOnDetachResetRoundness(STACK_SCROLL_ALGO);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
index 2371eed..3ba62b1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
@@ -44,6 +44,7 @@
 
 import androidx.lifecycle.Observer;
 
+import com.android.systemui.Flags;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.qualifiers.DisplayId;
 import com.android.systemui.dagger.qualifiers.Main;
@@ -57,6 +58,7 @@
 import com.android.systemui.qs.tiles.RotationLockTile;
 import com.android.systemui.res.R;
 import com.android.systemui.screenrecord.RecordingController;
+import com.android.systemui.screenrecord.data.model.ScreenRecordModel;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.phone.ui.StatusBarIconController;
@@ -354,7 +356,11 @@
         mProvisionedController.addCallback(this);
         mCurrentUserSetup = mProvisionedController.isCurrentUserSetup();
         mZenController.addCallback(this);
-        mCast.addCallback(mCastCallback);
+        if (!Flags.statusBarScreenSharingChips()) {
+            // If the flag is enabled, the cast icon is handled in the new screen sharing chips
+            // instead of here so we don't need to listen for events here.
+            mCast.addCallback(mCastCallback);
+        }
         mHotspot.addCallback(mHotspotCallback);
         mNextAlarmController.addCallback(mNextAlarmCallback);
         mDataSaver.addCallback(this);
@@ -362,7 +368,11 @@
         mPrivacyItemController.addCallback(this);
         mSensorPrivacyController.addCallback(mSensorPrivacyListener);
         mLocationController.addCallback(this);
-        mRecordingController.addCallback(this);
+        if (!Flags.statusBarScreenSharingChips()) {
+            // If the flag is enabled, the screen record icon is handled in the new screen sharing
+            // chips instead of here so we don't need to listen for events here.
+            mRecordingController.addCallback(this);
+        }
         mJavaAdapter.alwaysCollectFlow(mConnectedDisplayInteractor.getConnectedDisplayState(),
                 this::onConnectedDisplayAvailabilityChanged);
 
@@ -519,6 +529,11 @@
     }
 
     private void updateCast() {
+        if (Flags.statusBarScreenSharingChips()) {
+            // The cast icon is handled in the new screen sharing chips instead of here.
+            return;
+        }
+
         boolean isCasting = false;
         for (CastDevice device : mCast.getCastDevices()) {
             if (device.isCasting()) {
@@ -788,6 +803,10 @@
     private Runnable mRemoveCastIconRunnable = new Runnable() {
         @Override
         public void run() {
+            if (Flags.statusBarScreenSharingChips()) {
+                // The cast icon is handled in the new screen sharing chips instead of here.
+                return;
+            }
             if (DEBUG) Log.v(TAG, "updateCast: hiding icon NOW");
             mIconController.setIconVisibility(mSlotCast, false);
         }
@@ -796,8 +815,13 @@
     // Screen Recording
     @Override
     public void onCountdown(long millisUntilFinished) {
+        if (Flags.statusBarScreenSharingChips()) {
+            // The screen record icon is handled in the new screen sharing chips instead of here.
+            return;
+        }
         if (DEBUG) Log.d(TAG, "screenrecord: countdown " + millisUntilFinished);
-        int countdown = (int) Math.floorDiv(millisUntilFinished + 500, 1000);
+        int countdown =
+                (int) ScreenRecordModel.Starting.Companion.toCountdownSeconds(millisUntilFinished);
         int resourceId = R.drawable.stat_sys_screen_record;
         String description = Integer.toString(countdown);
         switch (countdown) {
@@ -820,6 +844,10 @@
 
     @Override
     public void onCountdownEnd() {
+        if (Flags.statusBarScreenSharingChips()) {
+            // The screen record icon is handled in the new screen sharing chips instead of here.
+            return;
+        }
         if (DEBUG) Log.d(TAG, "screenrecord: hiding icon during countdown");
         mHandler.post(() -> mIconController.setIconVisibility(mSlotScreenRecord, false));
         // Reset talkback priority
@@ -829,6 +857,10 @@
 
     @Override
     public void onRecordingStart() {
+        if (Flags.statusBarScreenSharingChips()) {
+            // The screen record icon is handled in the new screen sharing chips instead of here.
+            return;
+        }
         if (DEBUG) Log.d(TAG, "screenrecord: showing icon");
         mIconController.setIcon(mSlotScreenRecord,
                 R.drawable.stat_sys_screen_record,
@@ -838,6 +870,10 @@
 
     @Override
     public void onRecordingEnd() {
+        if (Flags.statusBarScreenSharingChips()) {
+            // The screen record icon is handled in the new screen sharing chips instead of here.
+            return;
+        }
         // Ensure this is on the main thread
         if (DEBUG) Log.d(TAG, "screenrecord: hiding icon");
         mHandler.post(() -> mIconController.setIconVisibility(mSlotScreenRecord, false));
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt
index d607ce0..68983a1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt
@@ -22,6 +22,7 @@
 import android.graphics.drawable.GradientDrawable
 import android.view.View
 import android.widget.ImageView
+import android.widget.TextView
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.systemui.Flags
@@ -92,6 +93,8 @@
                         chipView.requireViewById(R.id.ongoing_activity_chip_icon)
                     val chipTimeView: ChipChronometer =
                         chipView.requireViewById(R.id.ongoing_activity_chip_time)
+                    val chipTextView: TextView =
+                        chipView.requireViewById(R.id.ongoing_activity_chip_text)
                     val chipBackgroundView =
                         chipView.requireViewById<ChipBackgroundContainer>(
                             R.id.ongoing_activity_chip_background
@@ -101,14 +104,15 @@
                             when (chipModel) {
                                 is OngoingActivityChipModel.Shown -> {
                                     // Data
-                                    IconViewBinder.bind(chipModel.icon, chipIconView)
-                                    ChipChronometerBinder.bind(chipModel.startTimeMs, chipTimeView)
+                                    IconViewBinder.bindNullable(chipModel.icon, chipIconView)
+                                    setChipMainContent(chipModel, chipTextView, chipTimeView)
                                     chipView.setOnClickListener(chipModel.onClickListener)
 
                                     // Colors
                                     val textColor = chipModel.colors.text(chipContext)
                                     chipIconView.imageTintList = ColorStateList.valueOf(textColor)
                                     chipTimeView.setTextColor(textColor)
+                                    chipTextView.setTextColor(textColor)
                                     (chipBackgroundView.background as GradientDrawable).color =
                                         chipModel.colors.background(chipContext)
 
@@ -117,6 +121,8 @@
                                     )
                                 }
                                 is OngoingActivityChipModel.Hidden -> {
+                                    // The Chronometer should be stopped to prevent leaks -- see
+                                    // b/192243808 and [Chronometer.start].
                                     chipTimeView.stop()
                                     listener.onOngoingActivityStatusChanged(
                                         hasOngoingActivity = false
@@ -130,6 +136,61 @@
         }
     }
 
+    private fun setChipMainContent(
+        chipModel: OngoingActivityChipModel.Shown,
+        chipTextView: TextView,
+        chipTimeView: ChipChronometer,
+    ) {
+        when (chipModel) {
+            is OngoingActivityChipModel.Shown.Countdown -> {
+                chipTextView.text = chipModel.secondsUntilStarted.toString()
+                chipTextView.visibility = View.VISIBLE
+
+                // The Chronometer should be stopped to prevent leaks -- see b/192243808 and
+                // [Chronometer.start].
+                chipTimeView.stop()
+                chipTimeView.visibility = View.GONE
+            }
+            is OngoingActivityChipModel.Shown.Timer -> {
+                ChipChronometerBinder.bind(chipModel.startTimeMs, chipTimeView)
+                chipTimeView.visibility = View.VISIBLE
+
+                chipTextView.visibility = View.GONE
+            }
+        }
+        updateChipTextPadding(chipModel, chipTextView, chipTimeView)
+    }
+
+    private fun updateChipTextPadding(
+        chipModel: OngoingActivityChipModel.Shown,
+        chipTextView: TextView,
+        chipTimeView: ChipChronometer,
+    ) {
+        val requiresPadding = chipModel.icon != null
+        if (requiresPadding) {
+            chipTextView.addChipTextPaddingStart()
+            chipTimeView.addChipTextPaddingStart()
+        } else {
+            chipTextView.removeChipTextPaddingStart()
+            chipTimeView.removeChipTextPaddingStart()
+        }
+    }
+
+    private fun View.addChipTextPaddingStart() {
+        this.setPaddingRelative(
+            this.context.resources.getDimensionPixelSize(
+                R.dimen.ongoing_activity_chip_icon_text_padding
+            ),
+            paddingTop,
+            paddingEnd,
+            paddingBottom,
+        )
+    }
+
+    private fun View.removeChipTextPaddingStart() {
+        this.setPaddingRelative(/* start= */ 0, paddingTop, paddingEnd, paddingBottom)
+    }
+
     private fun animateLightsOutView(view: View, visible: Boolean) {
         view.animate().cancel()
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DevicePostureControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DevicePostureControllerImpl.java
index de0eb49..528ef49 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DevicePostureControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DevicePostureControllerImpl.java
@@ -31,8 +31,8 @@
 
 import kotlin.Unit;
 
-import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.Executor;
 
 import javax.inject.Inject;
@@ -42,7 +42,13 @@
 public class DevicePostureControllerImpl implements DevicePostureController {
     /** From androidx.window.common.COMMON_STATE_USE_BASE_STATE */
     private static final int COMMON_STATE_USE_BASE_STATE = 1000;
-    private final List<Callback> mListeners = new ArrayList<>();
+    /**
+     * Despite this is always used only from the main thread, it might be that some listener
+     * unregisters itself while we're sending the update, ending up modifying this while we're
+     * iterating it.
+     * Keeping a threadsafe list of listeners helps preventing ConcurrentModificationExceptions.
+     */
+    private final List<Callback> mListeners = new CopyOnWriteArrayList<>();
     private final List<DeviceState> mSupportedStates;
     private DeviceState mCurrentDeviceState;
     private int mCurrentDevicePosture = DEVICE_POSTURE_UNKNOWN;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeController.java
index ad19729..23e40b2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeController.java
@@ -24,7 +24,14 @@
 import android.service.notification.ZenModeConfig.ZenRule;
 
 import com.android.systemui.statusbar.policy.ZenModeController.Callback;
+import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor;
 
+/**
+ * Callback-based controller for listening to (or making) zen mode changes. Please prefer using the
+ * Flow-based {@link ZenModeInteractor} for new code instead of this.
+ *
+ * TODO(b/308591859): This should eventually be replaced by ZenModeInteractor/ZenModeRepository.
+ */
 public interface ZenModeController extends CallbackController<Callback> {
     void setZen(int zen, Uri conditionId, String reason);
     int getZen();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
index 15200bd..e08e4d7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
@@ -73,7 +73,6 @@
 import com.android.systemui.statusbar.policy.bluetooth.BluetoothRepository;
 import com.android.systemui.statusbar.policy.bluetooth.BluetoothRepositoryImpl;
 import com.android.systemui.statusbar.policy.data.repository.DeviceProvisioningRepositoryModule;
-import com.android.systemui.statusbar.policy.data.repository.ZenModeRepositoryModule;
 
 import dagger.Binds;
 import dagger.Module;
@@ -84,7 +83,7 @@
 import javax.inject.Named;
 
 /** Dagger Module for code in the statusbar.policy package. */
-@Module(includes = { DeviceProvisioningRepositoryModule.class, ZenModeRepositoryModule.class })
+@Module(includes = {DeviceProvisioningRepositoryModule.class})
 public interface StatusBarPolicyModule {
 
     String DEVICE_STATE_ROTATION_LOCK_DEFAULTS = "DEVICE_STATE_ROTATION_LOCK_DEFAULTS";
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepository.kt
deleted file mode 100644
index 94ab58a..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepository.kt
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.policy.data.repository
-
-import android.app.NotificationManager
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
-import com.android.systemui.statusbar.policy.ZenModeController
-import dagger.Binds
-import dagger.Module
-import javax.inject.Inject
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.Flow
-
-/**
- * A repository that holds information about the status and configuration of Zen Mode (or Do Not
- * Disturb/DND Mode).
- */
-interface ZenModeRepository {
-    val zenMode: Flow<Int>
-    val consolidatedNotificationPolicy: Flow<NotificationManager.Policy?>
-}
-
-class ZenModeRepositoryImpl
-@Inject
-constructor(
-    private val zenModeController: ZenModeController,
-) : ZenModeRepository {
-    // TODO(b/308591859): ZenModeController should use flows instead of callbacks. The
-    // conflatedCallbackFlows here should be replaced eventually, see:
-    // https://docs.google.com/document/d/1gAiuYupwUAFdbxkDXa29A4aFNu7XoCd7sCIk31WTnHU/edit?resourcekey=0-J4ZBiUhLhhQnNobAcI2vIw
-
-    override val zenMode: Flow<Int> = conflatedCallbackFlow {
-        val callback =
-            object : ZenModeController.Callback {
-                override fun onZenChanged(zen: Int) {
-                    trySend(zen)
-                }
-            }
-        zenModeController.addCallback(callback)
-        trySend(zenModeController.zen)
-
-        awaitClose { zenModeController.removeCallback(callback) }
-    }
-
-    override val consolidatedNotificationPolicy: Flow<NotificationManager.Policy?> =
-        conflatedCallbackFlow {
-            val callback =
-                object : ZenModeController.Callback {
-                    override fun onConsolidatedPolicyChanged(policy: NotificationManager.Policy?) {
-                        trySend(policy)
-                    }
-                }
-            zenModeController.addCallback(callback)
-            trySend(zenModeController.consolidatedPolicy)
-
-            awaitClose { zenModeController.removeCallback(callback) }
-        }
-}
-
-@Module
-interface ZenModeRepositoryModule {
-    @Binds fun bindImpl(impl: ZenModeRepositoryImpl): ZenModeRepository
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
index ae31851..f5d7d00 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.statusbar.policy.domain.interactor
 
 import android.provider.Settings
-import com.android.systemui.statusbar.policy.data.repository.ZenModeRepository
+import com.android.settingslib.statusbar.notification.data.repository.ZenModeRepository
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
@@ -30,9 +30,9 @@
  */
 class ZenModeInteractor @Inject constructor(repository: ZenModeRepository) {
     val isZenModeEnabled: Flow<Boolean> =
-        repository.zenMode
+        repository.globalZenMode
             .map {
-                when (it) {
+                when (it ?: Settings.Global.ZEN_MODE_OFF) {
                     Settings.Global.ZEN_MODE_ALARMS -> true
                     Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS -> true
                     Settings.Global.ZEN_MODE_NO_INTERRUPTIONS -> true
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/FoldLightRevealOverlayAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/FoldLightRevealOverlayAnimation.kt
index 9faa84e..c5e98a1 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/FoldLightRevealOverlayAnimation.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/FoldLightRevealOverlayAnimation.kt
@@ -91,7 +91,7 @@
             controllerFactory.create(
                 displaySelector = { minByOrNull { it.naturalWidth } },
                 effectFactory = { LinearSideLightRevealEffect(it.isVerticalRotation()) },
-                overlayContainerName = SURFACE_CONTAINER_NAME
+                overlayContainerName = OVERLAY_TITLE
             )
         controller.init()
 
@@ -196,7 +196,7 @@
     private companion object {
         const val TAG = "FoldLightRevealOverlayAnimation"
         const val WAIT_FOR_ANIMATION_TIMEOUT_MS = 2000L
-        const val SURFACE_CONTAINER_NAME = "fold-overlay-container"
+        const val OVERLAY_TITLE = "fold-animation-overlay"
         val ANIMATION_DURATION: Long
             get() = SystemProperties.getLong("persist.fold_animation_duration", 200L)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/FullscreenLightRevealAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/FullscreenLightRevealAnimation.kt
index f368cac..a921377 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/FullscreenLightRevealAnimation.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/FullscreenLightRevealAnimation.kt
@@ -73,7 +73,7 @@
     @Main private val executor: Executor,
     @Assisted private val displaySelector: List<DisplayInfo>.() -> DisplayInfo?,
     @Assisted private val lightRevealEffectFactory: (rotation: Int) -> LightRevealEffect,
-    @Assisted private val overlayContainerName: String
+    @Assisted private val overlayTitle: String
 ) {
 
     private lateinit var bgExecutor: Executor
@@ -156,7 +156,7 @@
         val containerBuilder =
             SurfaceControl.Builder(SurfaceSession())
                 .setContainerLayer()
-                .setName(overlayContainerName)
+                .setName("FoldUnfoldAnimationContainer")
 
         displayAreaHelper
             .get()
@@ -224,7 +224,7 @@
             }
             format = PixelFormat.TRANSLUCENT
             type = WindowManager.LayoutParams.TYPE_DISPLAY_OVERLAY
-            title = javaClass.simpleName
+            title = overlayTitle
             layoutInDisplayCutoutMode =
                 WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
             fitInsetsTypes = 0
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
index f355dd8..666e75f 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
@@ -71,7 +71,7 @@
             fullscreenLightRevealAnimationControllerFactory.create(
                 displaySelector = { maxByOrNull { it.naturalWidth } },
                 effectFactory = { LinearLightRevealEffect(it.isVerticalRotation()) },
-                overlayContainerName = SURFACE_CONTAINER_NAME,
+                overlayContainerName = OVERLAY_TITLE,
             )
         controller.init()
         bgExecutor = threadFactory.buildDelayableExecutorOnHandler(unfoldProgressHandler)
@@ -194,7 +194,7 @@
 
     private companion object {
         const val TAG = "UnfoldLightRevealOverlayAnimation"
-        const val SURFACE_CONTAINER_NAME = "unfold-overlay-container"
+        const val OVERLAY_TITLE = "unfold-animation-overlay"
         const val UNFOLD_BLOCK_TOUCHES_UNTIL_PROGRESS = 0.8f
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt
index d32d12c..a4936e6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt
@@ -37,7 +37,7 @@
 import com.android.systemui.flags.Flags
 import com.android.systemui.flags.fakeFeatureFlagsClassic
 import com.android.systemui.motion.createSysUiComposeMotionTestRule
-import com.android.systemui.scene.domain.interactor.sceneContainerStartable
+import com.android.systemui.scene.domain.startable.sceneContainerStartable
 import com.android.systemui.testKosmos
 import org.junit.Before
 import org.junit.Rule
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java
index 2ff660f..bfbb7ce 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java
@@ -36,6 +36,7 @@
 
 import android.content.ComponentName;
 import android.content.res.Configuration;
+import android.os.Handler;
 import android.view.IWindowManager;
 import android.view.accessibility.AccessibilityManager;
 
@@ -65,6 +66,8 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -120,6 +123,10 @@
     EdgeBackGestureHandler.Factory mEdgeBackGestureHandlerFactory;
     @Mock
     NotificationShadeWindowController mNotificationShadeWindowController;
+    @Mock
+    Handler mBgHandler;
+
+    @Captor ArgumentCaptor<Runnable> mRunnableArgumentCaptor;
     ConfigurationController mConfigurationController = new FakeConfigurationController();
 
     private AccessibilityManager.AccessibilityServicesStateChangeListener
@@ -149,7 +156,7 @@
                 () -> Optional.of(mock(CentralSurfaces.class)), mock(KeyguardStateController.class),
                 mNavigationModeController, mEdgeBackGestureHandlerFactory, mWm, mUserTracker,
                 mDisplayTracker, mNotificationShadeWindowController, mConfigurationController,
-                mDumpManager, mCommandQueue, mSynchronousExecutor);
+                mDumpManager, mCommandQueue, mSynchronousExecutor, mBgHandler);
     }
 
     @Test
@@ -203,8 +210,10 @@
                 .updateAccessibilityServicesState();
         verify(mNavbarTaskbarStateUpdater, times(1))
                 .updateAssistantAvailable(anyBoolean(), anyBoolean());
+        verify(mBgHandler).post(mRunnableArgumentCaptor.capture());
+        mRunnableArgumentCaptor.getValue().run();
         verify(mNavbarTaskbarStateUpdater, times(1))
-                .updateRotationWatcherState(anyInt());
+                .updateRotationWatcherState(anyInt(), anyBoolean());
         verify(mNavbarTaskbarStateUpdater, times(1))
                 .updateWallpaperVisibility(anyBoolean(), anyInt());
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerImplTest.java
index d5361ac..df8eafe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerImplTest.java
@@ -21,6 +21,7 @@
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
 import static com.android.wm.shell.Flags.enableTaskbarNavbarUnification;
+import static com.android.wm.shell.Flags.enableTaskbarOnPhones;
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -142,10 +143,11 @@
 
     @Test
     public void testCreateNavigationBarsIncludeDefaultTrue() {
-        assumeFalse(enableTaskbarNavbarUnification());
+        assumeFalse(enableTaskbarNavbarUnification() && enableTaskbarOnPhones());
 
         // Large screens may be using taskbar and the logic is different
         mNavigationBarController.mIsLargeScreen = false;
+        mNavigationBarController.mIsPhone = true;
         doNothing().when(mNavigationBarController).createNavigationBar(any(), any(), any());
 
         mNavigationBarController.createNavigationBars(true, null);
@@ -291,6 +293,17 @@
 
     @Test
     public void testShouldRenderTaskbar_taskbarNotRenderedOnPhone() {
+        assumeFalse(enableTaskbarOnPhones());
+
+        mNavigationBarController.mIsLargeScreen = false;
+        mNavigationBarController.mIsPhone = true;
+        assertFalse(mNavigationBarController.supportsTaskbar());
+    }
+
+    @Test
+    public void testShouldRenderTaskbar_taskbarRenderedOnPhone() {
+        assumeTrue(enableTaskbarNavbarUnification() && enableTaskbarOnPhones());
+
         mNavigationBarController.mIsLargeScreen = false;
         mNavigationBarController.mIsPhone = true;
         assertFalse(mNavigationBarController.supportsTaskbar());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
index 2b60f65..982a269 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
@@ -179,6 +179,9 @@
     private SysUiState mMockSysUiState;
     @Mock
     private Handler mHandler;
+
+    @Mock
+    private Handler mBgHandler;
     @Mock
     private UserTracker mUserTracker;
     @Mock
@@ -277,7 +280,8 @@
                     mEdgeBackGestureHandlerFactory, mock(IWindowManager.class),
                     mock(UserTracker.class), mock(DisplayTracker.class),
                     mNotificationShadeWindowController, mock(ConfigurationController.class),
-                    mock(DumpManager.class), mock(CommandQueue.class), mSynchronousExecutor));
+                    mock(DumpManager.class), mock(CommandQueue.class), mSynchronousExecutor,
+                    mBgHandler));
             mNavigationBar = createNavBar(mContext);
             mExternalDisplayNavigationBar = createNavBar(mSysuiTestableContextExternal);
         });
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceManagerTest.java
index 1c86638..03483c9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceManagerTest.java
@@ -20,6 +20,8 @@
 import static com.android.systemui.Flags.FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX;
 import static com.android.systemui.util.concurrency.MockExecutorHandlerKt.mockExecutorHandler;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
@@ -279,4 +281,23 @@
         verify(mTileLifecycle, never()).onStopListening();
         verify(mTileLifecycle, never()).executeSetBindService(false);
     }
+
+    @Test
+    public void testNoExtraPendingBindIfAlreadyBound() {
+        mTileServiceManager.startLifecycleManagerAndAddTile();
+
+        // As part of adding the tile, it will be bound and it will send a start successful to
+        // TileServices. startSuccessful will clear pending bind
+        mTileServiceManager.clearPendingBind();
+
+        // Assume we are still bound
+        when(mTileLifecycle.isBound()).thenReturn(true);
+
+        // And we want to bind again
+        mTileServiceManager.setBindAllowed(true);
+        mTileServiceManager.setBindRequested(true);
+
+        // Then the tile doesn't have pending bind
+        assertThat(mTileServiceManager.hasPendingBind()).isFalse();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
index 503c52f..ce1a885 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.recordissue
 
 import android.app.Dialog
-import android.content.SharedPreferences
 import android.os.UserHandle
 import android.testing.TestableLooper
 import android.widget.Button
@@ -57,6 +56,7 @@
 import org.mockito.Mock
 import org.mockito.Mockito.never
 import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
@@ -71,7 +71,6 @@
     @Mock private lateinit var mediaProjectionMetricsLogger: MediaProjectionMetricsLogger
     @Mock private lateinit var userTracker: UserTracker
     @Mock private lateinit var state: IssueRecordingState
-    @Mock private lateinit var sharedPreferences: SharedPreferences
     @Mock
     private lateinit var screenCaptureDisabledDialogDelegate: ScreenCaptureDisabledDialogDelegate
     @Mock private lateinit var screenCaptureDisabledDialog: SystemUIDialog
@@ -192,7 +191,7 @@
                 anyInt(),
                 eq(SessionCreationSource.SYSTEM_UI_SCREEN_RECORDER)
             )
-        verify(factory).create(any<ScreenCapturePermissionDialogDelegate>())
+        verify(factory, times(2)).create(any(SystemUIDialog.Delegate::class.java))
     }
 
     @Test
@@ -213,7 +212,7 @@
                 anyInt(),
                 eq(SessionCreationSource.SYSTEM_UI_SCREEN_RECORDER)
             )
-        verify(factory, never()).create(any<ScreenCapturePermissionDialogDelegate>())
+        verify(factory, never()).create()
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/data/model/ScreenRecordModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/data/model/ScreenRecordModelTest.kt
new file mode 100644
index 0000000..9331c8d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/data/model/ScreenRecordModelTest.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.screenrecord.data.model
+
+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 kotlin.test.Test
+
+@SmallTest
+class ScreenRecordModelTest : SysuiTestCase() {
+    @Test
+    fun countdownSeconds_millis0_is0() {
+        assertThat(0L.toCountdownSeconds()).isEqualTo(0)
+        assertThat(ScreenRecordModel.Starting(0L).countdownSeconds).isEqualTo(0)
+    }
+
+    @Test
+    fun countdownSeconds_millis500_isOne() {
+        assertThat(500L.toCountdownSeconds()).isEqualTo(1)
+        assertThat(ScreenRecordModel.Starting(500L).countdownSeconds).isEqualTo(1)
+    }
+
+    @Test
+    fun countdownSeconds_millis999_isOne() {
+        assertThat(999L.toCountdownSeconds()).isEqualTo(1)
+        assertThat(ScreenRecordModel.Starting(999L).countdownSeconds).isEqualTo(1)
+    }
+
+    @Test
+    fun countdownSeconds_millis1000_isOne() {
+        assertThat(1000L.toCountdownSeconds()).isEqualTo(1)
+        assertThat(ScreenRecordModel.Starting(1000L).countdownSeconds).isEqualTo(1)
+    }
+
+    @Test
+    fun countdownSeconds_millis1499_isOne() {
+        assertThat(1499L.toCountdownSeconds()).isEqualTo(1)
+        assertThat(ScreenRecordModel.Starting(1499L).countdownSeconds).isEqualTo(1)
+    }
+
+    @Test
+    fun countdownSeconds_millis1500_isTwo() {
+        assertThat(1500L.toCountdownSeconds()).isEqualTo(2)
+        assertThat(ScreenRecordModel.Starting(1500L).countdownSeconds).isEqualTo(2)
+    }
+
+    @Test
+    fun countdownSeconds_millis1999_isTwo() {
+        assertThat(1599L.toCountdownSeconds()).isEqualTo(2)
+        assertThat(ScreenRecordModel.Starting(1599L).countdownSeconds).isEqualTo(2)
+    }
+
+    @Test
+    fun countdownSeconds_millis2000_isTwo() {
+        assertThat(2000L.toCountdownSeconds()).isEqualTo(2)
+        assertThat(ScreenRecordModel.Starting(2000L).countdownSeconds).isEqualTo(2)
+    }
+
+    @Test
+    fun countdownSeconds_millis2500_isThree() {
+        assertThat(2500L.toCountdownSeconds()).isEqualTo(3)
+        assertThat(ScreenRecordModel.Starting(2500L).countdownSeconds).isEqualTo(3)
+    }
+
+    @Test
+    fun countdownSeconds_millis2999_isThree() {
+        assertThat(2999L.toCountdownSeconds()).isEqualTo(3)
+        assertThat(ScreenRecordModel.Starting(2999L).countdownSeconds).isEqualTo(3)
+    }
+
+    @Test
+    fun countdownSeconds_millis3000_isThree() {
+        assertThat(3000L.toCountdownSeconds()).isEqualTo(3)
+        assertThat(ScreenRecordModel.Starting(3000L).countdownSeconds).isEqualTo(3)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
index ec5589e..0b81b5e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
@@ -3,9 +3,6 @@
 import android.content.ComponentName
 import android.graphics.Bitmap
 import android.net.Uri
-import android.platform.test.annotations.DisableFlags
-import android.platform.test.annotations.EnableFlags
-import android.testing.AndroidTestingRunner
 import android.view.Display
 import android.view.Display.TYPE_EXTERNAL
 import android.view.Display.TYPE_INTERNAL
@@ -18,7 +15,6 @@
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.testing.UiEventLoggerFake
 import com.android.internal.util.ScreenshotRequest
-import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.display.data.repository.FakeDisplayRepository
 import com.android.systemui.display.data.repository.display
@@ -26,7 +22,6 @@
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.kotlinArgumentCaptor as ArgumentCaptor
 import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.nullable
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import java.lang.IllegalStateException
@@ -47,8 +42,7 @@
 @SmallTest
 class TakeScreenshotExecutorTest : SysuiTestCase() {
 
-    private val controller0 = mock<ScreenshotController>()
-    private val controller1 = mock<ScreenshotController>()
+    private val controller = mock<ScreenshotController>()
     private val notificationsController0 = mock<ScreenshotNotificationsController>()
     private val notificationsController1 = mock<ScreenshotNotificationsController>()
     private val controllerFactory = mock<ScreenshotController.Factory>()
@@ -60,6 +54,7 @@
     private val topComponent = ComponentName(mContext, TakeScreenshotExecutorTest::class.java)
     private val testScope = TestScope(UnconfinedTestDispatcher())
     private val eventLogger = UiEventLoggerFake()
+    private val headlessHandler = mock<HeadlessScreenshotHandler>()
 
     private val screenshotExecutor =
         TakeScreenshotExecutorImpl(
@@ -68,20 +63,18 @@
             testScope,
             requestProcessor,
             eventLogger,
-            notificationControllerFactory
+            notificationControllerFactory,
+            headlessHandler,
         )
 
     @Before
     fun setUp() {
-        whenever(controllerFactory.create(any(), any())).thenAnswer {
-            if (it.getArgument<Display>(0).displayId == 0) controller0 else controller1
-        }
+        whenever(controllerFactory.create(any(), any())).thenReturn(controller)
         whenever(notificationControllerFactory.create(eq(0))).thenReturn(notificationsController0)
         whenever(notificationControllerFactory.create(eq(1))).thenReturn(notificationsController1)
     }
 
     @Test
-    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_severalDisplays_callsControllerForEachOne() =
         testScope.runTest {
             val internalDisplay = display(TYPE_INTERNAL, id = 0)
@@ -91,14 +84,14 @@
             screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
 
             verify(controllerFactory).create(eq(internalDisplay), any())
-            verify(controllerFactory).create(eq(externalDisplay), any())
+            verify(controllerFactory, never()).create(eq(externalDisplay), any())
 
             val capturer = ArgumentCaptor<ScreenshotData>()
 
-            verify(controller0).handleScreenshot(capturer.capture(), any(), any())
+            verify(controller).handleScreenshot(capturer.capture(), any(), any())
             assertThat(capturer.value.displayId).isEqualTo(0)
             // OnSaved callback should be different.
-            verify(controller1).handleScreenshot(capturer.capture(), any(), any())
+            verify(headlessHandler).handleScreenshot(capturer.capture(), any(), any())
             assertThat(capturer.value.displayId).isEqualTo(1)
 
             assertThat(eventLogger.numLogs()).isEqualTo(2)
@@ -113,32 +106,6 @@
         }
 
     @Test
-    @EnableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
-    fun executeScreenshots_severalDisplaysShelfUi_justCallsOne() =
-        testScope.runTest {
-            val internalDisplay = display(TYPE_INTERNAL, id = 0)
-            val externalDisplay = display(TYPE_EXTERNAL, id = 1)
-            setDisplays(internalDisplay, externalDisplay)
-            val onSaved = { _: Uri? -> }
-            screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
-
-            verify(controllerFactory).create(eq(internalDisplay), any())
-
-            val capturer = ArgumentCaptor<ScreenshotData>()
-
-            verify(controller0).handleScreenshot(capturer.capture(), any(), any())
-            assertThat(capturer.value.displayId).isEqualTo(0)
-
-            assertThat(eventLogger.numLogs()).isEqualTo(1)
-            assertThat(eventLogger.get(0).eventId)
-                .isEqualTo(ScreenshotEvent.SCREENSHOT_REQUESTED_KEY_OTHER.id)
-            assertThat(eventLogger.get(0).packageName).isEqualTo(topComponent.packageName)
-
-            screenshotExecutor.onDestroy()
-        }
-
-    @Test
-    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_providedImageType_callsOnlyDefaultDisplayController() =
         testScope.runTest {
             val internalDisplay = display(TYPE_INTERNAL, id = 0)
@@ -156,10 +123,10 @@
 
             val capturer = ArgumentCaptor<ScreenshotData>()
 
-            verify(controller0).handleScreenshot(capturer.capture(), any(), any())
+            verify(controller).handleScreenshot(capturer.capture(), any(), any())
             assertThat(capturer.value.displayId).isEqualTo(0)
             // OnSaved callback should be different.
-            verify(controller1, never()).handleScreenshot(any(), any(), any())
+            verify(headlessHandler, never()).handleScreenshot(any(), any(), any())
 
             assertThat(eventLogger.numLogs()).isEqualTo(1)
             assertThat(eventLogger.get(0).eventId)
@@ -170,7 +137,6 @@
         }
 
     @Test
-    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_onlyVirtualDisplays_noInteractionsWithControllers() =
         testScope.runTest {
             setDisplays(display(TYPE_VIRTUAL, id = 0), display(TYPE_VIRTUAL, id = 1))
@@ -178,14 +144,14 @@
             screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
 
             verifyNoMoreInteractions(controllerFactory)
+            verify(headlessHandler, never()).handleScreenshot(any(), any(), any())
             screenshotExecutor.onDestroy()
         }
 
     @Test
-    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_allowedTypes_allCaptured() =
         testScope.runTest {
-            whenever(controllerFactory.create(any(), any())).thenReturn(controller0)
+            whenever(controllerFactory.create(any(), any())).thenReturn(controller)
 
             setDisplays(
                 display(TYPE_INTERNAL, id = 0),
@@ -196,12 +162,12 @@
             val onSaved = { _: Uri? -> }
             screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
 
-            verify(controller0, times(4)).handleScreenshot(any(), any(), any())
+            verify(controller, times(1)).handleScreenshot(any(), any(), any())
+            verify(headlessHandler, times(3)).handleScreenshot(any(), any(), any())
             screenshotExecutor.onDestroy()
         }
 
     @Test
-    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_reportsOnFinishedOnlyWhenBothFinished() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -211,8 +177,8 @@
             val capturer0 = ArgumentCaptor<TakeScreenshotService.RequestCallback>()
             val capturer1 = ArgumentCaptor<TakeScreenshotService.RequestCallback>()
 
-            verify(controller0).handleScreenshot(any(), any(), capturer0.capture())
-            verify(controller1).handleScreenshot(any(), any(), capturer1.capture())
+            verify(controller).handleScreenshot(any(), any(), capturer0.capture())
+            verify(headlessHandler).handleScreenshot(any(), any(), capturer1.capture())
 
             verify(callback, never()).onFinish()
 
@@ -227,7 +193,6 @@
         }
 
     @Test
-    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_oneFinishesOtherFails_reportFailsOnlyAtTheEnd() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -237,8 +202,8 @@
             val capturer0 = ArgumentCaptor<TakeScreenshotService.RequestCallback>()
             val capturer1 = ArgumentCaptor<TakeScreenshotService.RequestCallback>()
 
-            verify(controller0).handleScreenshot(any(), any(), capturer0.capture())
-            verify(controller1).handleScreenshot(any(), nullable(), capturer1.capture())
+            verify(controller).handleScreenshot(any(), any(), capturer0.capture())
+            verify(headlessHandler).handleScreenshot(any(), any(), capturer1.capture())
 
             verify(callback, never()).onFinish()
 
@@ -255,7 +220,6 @@
         }
 
     @Test
-    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_allDisplaysFail_reportsFail() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -265,8 +229,8 @@
             val capturer0 = ArgumentCaptor<TakeScreenshotService.RequestCallback>()
             val capturer1 = ArgumentCaptor<TakeScreenshotService.RequestCallback>()
 
-            verify(controller0).handleScreenshot(any(), any(), capturer0.capture())
-            verify(controller1).handleScreenshot(any(), any(), capturer1.capture())
+            verify(controller).handleScreenshot(any(), any(), capturer0.capture())
+            verify(headlessHandler).handleScreenshot(any(), any(), capturer1.capture())
 
             verify(callback, never()).onFinish()
 
@@ -283,7 +247,6 @@
         }
 
     @Test
-    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun onDestroy_propagatedToControllers() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -291,59 +254,50 @@
             screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
 
             screenshotExecutor.onDestroy()
-            verify(controller0).onDestroy()
-            verify(controller1).onDestroy()
+            verify(controller).onDestroy()
         }
 
     @Test
-    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
-    fun removeWindows_propagatedToControllers() =
+    fun removeWindows_propagatedToController() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
             val onSaved = { _: Uri? -> }
             screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
 
             screenshotExecutor.removeWindows()
-            verify(controller0).removeWindow()
-            verify(controller1).removeWindow()
+            verify(controller).removeWindow()
 
             screenshotExecutor.onDestroy()
         }
 
     @Test
-    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
-    fun onCloseSystemDialogsReceived_propagatedToControllers() =
+    fun onCloseSystemDialogsReceived_propagatedToController() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
             val onSaved = { _: Uri? -> }
             screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
 
             screenshotExecutor.onCloseSystemDialogsReceived()
-            verify(controller0).requestDismissal(any())
-            verify(controller1).requestDismissal(any())
+            verify(controller).requestDismissal(any())
 
             screenshotExecutor.onDestroy()
         }
 
     @Test
-    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
-    fun onCloseSystemDialogsReceived_someControllerHavePendingTransitions() =
+    fun onCloseSystemDialogsReceived_controllerHasPendingTransitions() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
-            whenever(controller0.isPendingSharedTransition).thenReturn(true)
-            whenever(controller1.isPendingSharedTransition).thenReturn(false)
+            whenever(controller.isPendingSharedTransition).thenReturn(true)
             val onSaved = { _: Uri? -> }
             screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
 
             screenshotExecutor.onCloseSystemDialogsReceived()
-            verify(controller0, never()).requestDismissal(any())
-            verify(controller1).requestDismissal(any())
+            verify(controller, never()).requestDismissal(any())
 
             screenshotExecutor.onDestroy()
         }
 
     @Test
-    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_controllerCalledWithRequestProcessorReturnValue() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0))
@@ -358,14 +312,13 @@
                 .isEqualTo(ScreenshotData.fromRequest(screenshotRequest))
 
             val capturer = ArgumentCaptor<ScreenshotData>()
-            verify(controller0).handleScreenshot(capturer.capture(), any(), any())
+            verify(controller).handleScreenshot(capturer.capture(), any(), any())
             assertThat(capturer.value).isEqualTo(toBeReturnedByProcessor)
 
             screenshotExecutor.onDestroy()
         }
 
     @Test
-    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_errorFromProcessor_logsScreenshotRequested() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -383,7 +336,6 @@
         }
 
     @Test
-    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_errorFromProcessor_logsUiError() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -401,7 +353,6 @@
         }
 
     @Test
-    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_errorFromProcessorOnDefaultDisplay_showsErrorNotification() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -428,14 +379,13 @@
         }
 
     @Test
-    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_errorFromScreenshotController_reportsRequested() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
             val onSaved = { _: Uri? -> }
-            whenever(controller0.handleScreenshot(any(), any(), any()))
+            whenever(controller.handleScreenshot(any(), any(), any()))
                 .thenThrow(IllegalStateException::class.java)
-            whenever(controller1.handleScreenshot(any(), any(), any()))
+            whenever(headlessHandler.handleScreenshot(any(), any(), any()))
                 .thenThrow(IllegalStateException::class.java)
 
             screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
@@ -449,14 +399,13 @@
         }
 
     @Test
-    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_errorFromScreenshotController_reportsError() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
             val onSaved = { _: Uri? -> }
-            whenever(controller0.handleScreenshot(any(), any(), any()))
+            whenever(controller.handleScreenshot(any(), any(), any()))
                 .thenThrow(IllegalStateException::class.java)
-            whenever(controller1.handleScreenshot(any(), any(), any()))
+            whenever(headlessHandler.handleScreenshot(any(), any(), any()))
                 .thenThrow(IllegalStateException::class.java)
 
             screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
@@ -470,14 +419,13 @@
         }
 
     @Test
-    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_errorFromScreenshotController_showsErrorNotification() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
             val onSaved = { _: Uri? -> }
-            whenever(controller0.handleScreenshot(any(), any(), any()))
+            whenever(controller.handleScreenshot(any(), any(), any()))
                 .thenThrow(IllegalStateException::class.java)
-            whenever(controller1.handleScreenshot(any(), any(), any()))
+            whenever(headlessHandler.handleScreenshot(any(), any(), any()))
                 .thenThrow(IllegalStateException::class.java)
 
             screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
@@ -496,7 +444,7 @@
                 assertThat(it).isNull()
                 onSavedCallCount += 1
             }
-            whenever(controller0.handleScreenshot(any(), any(), any())).thenAnswer {
+            whenever(controller.handleScreenshot(any(), any(), any())).thenAnswer {
                 (it.getArgument(1) as Consumer<Uri?>).accept(null)
             }
 
@@ -525,6 +473,7 @@
         var processed: ScreenshotData? = null
         var toReturn: ScreenshotData? = null
         var shouldThrowException = false
+
         override suspend fun process(screenshot: ScreenshotData): ScreenshotData {
             if (shouldThrowException) throw RequestProcessorException("")
             processed = screenshot
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryTest.kt
index 8bfb07b..69cc9d5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryTest.kt
@@ -56,7 +56,7 @@
     @Test
     fun testGetIsShowNotificationsOnLockscreenEnabled() =
         testScope.runTest {
-            val showNotifs by collectLastValue(underTest.isShowNotificationsOnLockScreenEnabled)
+            val showNotifs by collectLastValue(underTest.isShowNotificationsOnLockScreenEnabled())
 
             secureSettingsRepository.setInt(
                 name = Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS,
@@ -74,7 +74,7 @@
     @Test
     fun testSetIsShowNotificationsOnLockscreenEnabled() =
         testScope.runTest {
-            val showNotifs by collectLastValue(underTest.isShowNotificationsOnLockScreenEnabled)
+            val showNotifs by collectLastValue(underTest.isShowNotificationsOnLockScreenEnabled())
 
             underTest.setShowNotificationsOnLockscreenEnabled(true)
             assertThat(showNotifs).isEqualTo(true)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
index 3606b1b..c3e810e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
@@ -69,13 +69,13 @@
         }
 
     @Test
-    fun chip_inCall_isShown() =
+    fun chip_inCall_isShownAsTimer() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
             repo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 345, intent = null))
 
-            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
         }
 
     @Test
@@ -92,7 +92,8 @@
             // started 2000ms ago (1000 - 3000). The OngoingActivityChipModel start time needs to be
             // relative to elapsedRealtime, so it should be 2000ms before the elapsed realtime set
             // on the clock.
-            assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(398_000)
+            assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs)
+                .isEqualTo(398_000)
         }
 
     @Test
@@ -127,7 +128,8 @@
             // Start a call
             repo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 1000, intent = null))
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
-            assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(398_000)
+            assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs)
+                .isEqualTo(398_000)
 
             // End the call
             repo.setOngoingCallState(OngoingCallModel.NoCall)
@@ -140,20 +142,18 @@
             // Start a new call, which started 1000ms ago
             repo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 102_000, intent = null))
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
-            assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(499_000)
+            assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs)
+                .isEqualTo(499_000)
         }
 
     @Test
-    fun chip_inCall_nullIntent_clickListenerDoesNothing() =
+    fun chip_inCall_nullIntent_nullClickListener() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
             repo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 1000, intent = null))
 
-            val clickListener = (latest as OngoingActivityChipModel.Shown).onClickListener
-
-            clickListener.onClick(chipView)
-            // Just verify nothing crashes
+            assertThat((latest as OngoingActivityChipModel.Shown).onClickListener).isNull()
         }
 
     @Test
@@ -164,8 +164,9 @@
             val intent = mock<PendingIntent>()
             repo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 1000, intent = intent))
             val clickListener = (latest as OngoingActivityChipModel.Shown).onClickListener
+            assertThat(clickListener).isNotNull()
 
-            clickListener.onClick(chipView)
+            clickListener!!.onClick(chipView)
 
             verify(kosmos.activityStarter).postStartActivityDismissingKeyguard(intent, null)
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
index d7935e5..bde668e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
@@ -91,7 +91,7 @@
         }
 
     @Test
-    fun chip_singleTaskState_otherDevicesPackage_isShown() =
+    fun chip_singleTaskState_otherDevicesPackage_isShownAsTimer() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
@@ -101,20 +101,20 @@
                     createTask(taskId = 1),
                 )
 
-            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
             val icon = (latest as OngoingActivityChipModel.Shown).icon
             assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_cast_connected)
         }
 
     @Test
-    fun chip_entireScreenState_otherDevicesPackage_isShown() =
+    fun chip_entireScreenState_otherDevicesPackage_isShownAsTimer() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
             mediaProjectionRepo.mediaProjectionState.value =
                 MediaProjectionState.Projecting.EntireScreen(CAST_TO_OTHER_DEVICES_PACKAGE)
 
-            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
             val icon = (latest as OngoingActivityChipModel.Shown).icon
             assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_cast_connected)
         }
@@ -162,7 +162,7 @@
                 MediaProjectionState.Projecting.EntireScreen(CAST_TO_OTHER_DEVICES_PACKAGE)
 
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
-            assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(1234)
+            assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs).isEqualTo(1234)
 
             mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.NotProjecting
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
@@ -175,7 +175,7 @@
                 )
 
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
-            assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(5678)
+            assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs).isEqualTo(5678)
         }
 
     @Test
@@ -186,8 +186,9 @@
                 MediaProjectionState.Projecting.EntireScreen(CAST_TO_OTHER_DEVICES_PACKAGE)
 
             val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+            assertThat(clickListener).isNotNull()
 
-            clickListener.onClick(chipView)
+            clickListener!!.onClick(chipView)
             verify(kosmos.mockDialogTransitionAnimator)
                 .showFromView(
                     eq(mockCastDialog),
@@ -209,8 +210,9 @@
                 )
 
             val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+            assertThat(clickListener).isNotNull()
 
-            clickListener.onClick(chipView)
+            clickListener!!.onClick(chipView)
             verify(kosmos.mockDialogTransitionAnimator)
                 .showFromView(
                     eq(mockCastDialog),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt
index fdf0e5d..8e8b082 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt
@@ -88,29 +88,75 @@
         }
 
     @Test
-    fun chip_startingState_isHidden() =
+    fun chip_startingState_isShownAsCountdownWithoutIconOrClickListener() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
             screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(400)
 
-            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Countdown::class.java)
+            assertThat((latest as OngoingActivityChipModel.Shown).icon).isNull()
+            assertThat((latest as OngoingActivityChipModel.Shown).onClickListener).isNull()
+        }
+
+    // The millis we typically get from [ScreenRecordRepository] are around 2995, 1995, and 995.
+    @Test
+    fun chip_startingState_millis2995_is3() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+
+            screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(2995)
+
+            assertThat((latest as OngoingActivityChipModel.Shown.Countdown).secondsUntilStarted)
+                .isEqualTo(3)
         }
 
     @Test
-    fun chip_recordingState_isShownWithIcon() =
+    fun chip_startingState_millis1995_is2() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+
+            screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(1995)
+
+            assertThat((latest as OngoingActivityChipModel.Shown.Countdown).secondsUntilStarted)
+                .isEqualTo(2)
+        }
+
+    @Test
+    fun chip_startingState_millis995_is1() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+
+            screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(995)
+
+            assertThat((latest as OngoingActivityChipModel.Shown.Countdown).secondsUntilStarted)
+                .isEqualTo(1)
+        }
+
+    @Test
+    fun chip_recordingState_isShownAsTimerWithIcon() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
             screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording
 
-            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
             val icon = (latest as OngoingActivityChipModel.Shown).icon
             assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_screenrecord)
         }
 
     @Test
-    fun chip_colorsAreRed() =
+    fun chip_startingState_colorsAreRed() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+
+            screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(2000L)
+
+            assertThat((latest as OngoingActivityChipModel.Shown).colors).isEqualTo(ColorsModel.Red)
+        }
+
+    @Test
+    fun chip_recordingState_colorsAreRed() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
@@ -128,7 +174,7 @@
             screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording
 
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
-            assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(1234)
+            assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs).isEqualTo(1234)
 
             screenRecordRepo.screenRecordState.value = ScreenRecordModel.DoingNothing
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
@@ -137,7 +183,7 @@
             screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording
 
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
-            assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(5678)
+            assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs).isEqualTo(5678)
         }
 
     @Test
@@ -148,8 +194,9 @@
             mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.NotProjecting
 
             val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+            assertThat(clickListener).isNotNull()
 
-            clickListener.onClick(chipView)
+            clickListener!!.onClick(chipView)
             // EndScreenRecordingDialogDelegate will test that the dialog has the right message
             verify(kosmos.mockDialogTransitionAnimator)
                 .showFromView(
@@ -169,8 +216,9 @@
                 MediaProjectionState.Projecting.EntireScreen("host.package")
 
             val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+            assertThat(clickListener).isNotNull()
 
-            clickListener.onClick(chipView)
+            clickListener!!.onClick(chipView)
             // EndScreenRecordingDialogDelegate will test that the dialog has the right message
             verify(kosmos.mockDialogTransitionAnimator)
                 .showFromView(
@@ -193,8 +241,9 @@
                 )
 
             val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+            assertThat(clickListener).isNotNull()
 
-            clickListener.onClick(chipView)
+            clickListener!!.onClick(chipView)
             // EndScreenRecordingDialogDelegate will test that the dialog has the right message
             verify(kosmos.mockDialogTransitionAnimator)
                 .showFromView(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt
index 4c2546e..63c29ac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt
@@ -59,7 +59,7 @@
 
         underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
 
-        verify(sysuiDialog).setIcon(R.drawable.ic_screenshot_share)
+        verify(sysuiDialog).setIcon(R.drawable.ic_present_to_all)
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
index 8ea3f4a..2e5f7f5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
@@ -116,7 +116,7 @@
         }
 
     @Test
-    fun chip_singleTaskState_normalPackage_isShown() =
+    fun chip_singleTaskState_normalPackage_isShownAsTimer() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
@@ -126,22 +126,22 @@
                     createTask(taskId = 1),
                 )
 
-            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
             val icon = (latest as OngoingActivityChipModel.Shown).icon
-            assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_screenshot_share)
+            assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_present_to_all)
         }
 
     @Test
-    fun chip_entireScreenState_normalPackage_isShown() =
+    fun chip_entireScreenState_normalPackage_isShownAsTimer() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
             mediaProjectionRepo.mediaProjectionState.value =
                 MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
 
-            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
             val icon = (latest as OngoingActivityChipModel.Shown).icon
-            assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_screenshot_share)
+            assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_present_to_all)
         }
 
     @Test
@@ -165,7 +165,7 @@
                 MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
 
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
-            assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(1234)
+            assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs).isEqualTo(1234)
 
             mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.NotProjecting
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
@@ -178,7 +178,7 @@
                 )
 
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
-            assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(5678)
+            assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs).isEqualTo(5678)
         }
 
     @Test
@@ -189,8 +189,9 @@
                 MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
 
             val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+            assertThat(clickListener).isNotNull()
 
-            clickListener.onClick(chipView)
+            clickListener!!.onClick(chipView)
             verify(kosmos.mockDialogTransitionAnimator)
                 .showFromView(
                     eq(mockShareDialog),
@@ -211,8 +212,9 @@
                 )
 
             val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+            assertThat(clickListener).isNotNull()
 
-            clickListener.onClick(chipView)
+            clickListener!!.onClick(chipView)
             verify(kosmos.mockDialogTransitionAnimator)
                 .showFromView(
                     eq(mockShareDialog),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
index 912a10a..8bc83cf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
@@ -35,7 +35,11 @@
 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
 import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository
 import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
+import com.android.systemui.util.time.fakeSystemClock
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
@@ -46,6 +50,7 @@
 class OngoingActivityChipsViewModelTest : SysuiTestCase() {
     private val kosmos = Kosmos().also { it.testCase = this }
     private val testScope = kosmos.testScope
+    private val systemClock = kosmos.fakeSystemClock
 
     private val screenRecordState = kosmos.screenRecordRepository.screenRecordState
     private val mediaProjectionState = kosmos.fakeMediaProjectionRepository.mediaProjectionState
@@ -191,6 +196,39 @@
             assertIsCallChip(latest)
         }
 
+    /** Regression test for b/347726238. */
+    @Test
+    fun chip_timerDoesNotResetAfterSubscribersRestart() =
+        testScope.runTest {
+            var latest: OngoingActivityChipModel? = null
+
+            val job1 = underTest.chip.onEach { latest = it }.launchIn(this)
+
+            // Start a chip with a timer
+            systemClock.setElapsedRealtime(1234)
+            screenRecordState.value = ScreenRecordModel.Recording
+
+            runCurrent()
+
+            assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs).isEqualTo(1234)
+
+            // Stop subscribing to the chip flow
+            job1.cancel()
+
+            // Let time pass
+            systemClock.setElapsedRealtime(5678)
+
+            // WHEN we re-subscribe to the chip flow
+            val job2 = underTest.chip.onEach { latest = it }.launchIn(this)
+
+            runCurrent()
+
+            // THEN the old start time is still used
+            assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs).isEqualTo(1234)
+
+            job2.cancel()
+        }
+
     companion object {
         fun assertIsScreenRecordChip(latest: OngoingActivityChipModel?) {
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
@@ -201,7 +239,7 @@
         fun assertIsShareToAppChip(latest: OngoingActivityChipModel?) {
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
             val icon = (latest as OngoingActivityChipModel.Shown).icon
-            assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_screenshot_share)
+            assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_present_to_all)
         }
 
         fun assertIsCallChip(latest: OngoingActivityChipModel?) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
index 7304bd6..b8f8026 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
@@ -328,7 +328,7 @@
 
     @Test
     @EnableFlags(NotificationContentAlphaOptimization.FLAG_NAME)
-    public void setHideSensitive_changeContent_shouldNotDisturbAnimation() throws Exception {
+    public void setHideSensitive_changeContent_shouldResetAlpha() throws Exception {
 
         // Given: A sensitive row that has public version but is not hiding sensitive,
         // and is during an animation that sets its alpha value to be 0.5f
@@ -351,12 +351,12 @@
 
         // Then: The alpha value of private layout should be reset to 1, private layout be
         // INVISIBLE;
-        // The alpha value of public layout should be 0.5 to preserve the animation state, public
-        // layout should be VISIBLE
+        // The alpha value of public layout should be reset to 1 to avoid remaining transparent,
+        // public layout should be VISIBLE
         assertEquals(View.INVISIBLE, row.getPrivateLayout().getVisibility());
         assertEquals(1f, row.getPrivateLayout().getAlpha(), 0);
         assertEquals(View.VISIBLE, row.getPublicLayout().getVisibility());
-        assertEquals(0.5f, row.getPublicLayout().getAlpha(), 0);
+        assertEquals(1f, row.getPublicLayout().getAlpha(), 0);
     }
 
     @Test
@@ -793,6 +793,73 @@
     }
 
     @Test
+    public void isExpanded_onKeyguard_allowOnKeyguardExpanded() throws Exception {
+        // GIVEN
+        final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+        row.setOnKeyguard(true);
+        row.setUserExpanded(true);
+
+        // THEN
+        assertThat(row.isExpanded(/*allowOnKeyguard =*/ true)).isTrue();
+    }
+    @Test
+    public void isExpanded_onKeyguard_notAllowOnKeyguardNotExpanded() throws Exception {
+        // GIVEN
+        final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+        row.setOnKeyguard(true);
+        row.setUserExpanded(true);
+
+        // THEN
+        assertThat(row.isExpanded(/*allowOnKeyguard =*/ false)).isFalse();
+    }
+
+    @Test
+    public void isExpanded_systemExpanded_expanded() throws Exception {
+        // GIVEN
+        final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+        row.setOnKeyguard(false);
+        row.setSystemExpanded(true);
+
+        // THEN
+        assertThat(row.isExpanded()).isTrue();
+    }
+
+    @Test
+    public void isExpanded_systemChildExpanded_expanded() throws Exception {
+        // GIVEN
+        final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+        row.setOnKeyguard(false);
+        row.setSystemChildExpanded(true);
+
+        // THEN
+        assertThat(row.isExpanded()).isTrue();
+    }
+
+    @Test
+    public void isExpanded_userExpanded_expanded() throws Exception {
+        // GIVEN
+        final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+        row.setOnKeyguard(false);
+        row.setSystemExpanded(true);
+        row.setUserExpanded(true);
+
+        // THEN
+        assertThat(row.isExpanded()).isTrue();
+    }
+
+    @Test
+    public void isExpanded_userExpandedFalse_notExpanded() throws Exception {
+        // GIVEN
+        final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+        row.setOnKeyguard(false);
+        row.setSystemExpanded(true);
+        row.setUserExpanded(false);
+
+        // THEN
+        assertThat(row.isExpanded()).isFalse();
+    }
+
+    @Test
     public void onDisappearAnimationFinished_shouldSetFalse_headsUpAnimatingAway()
             throws Exception {
         final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
index e6cba1c..54a26f7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
@@ -23,6 +23,7 @@
 import android.platform.test.annotations.EnableFlags
 import android.testing.TestableLooper.RunWithLooper
 import android.util.TypedValue
+import android.util.TypedValue.COMPLEX_UNIT_SP
 import android.view.View
 import android.view.ViewGroup
 import android.widget.RemoteViews
@@ -34,27 +35,39 @@
 import com.android.systemui.statusbar.notification.ConversationNotificationProcessor
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.BindParams
+import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL
+import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED
+import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED
+import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_HEADS_UP
 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationCallback
 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag
 import com.android.systemui.statusbar.notification.row.shared.HeadsUpStatusBarModel
 import com.android.systemui.statusbar.notification.row.shared.NewRemoteViews
 import com.android.systemui.statusbar.notification.row.shared.NotificationContentModel
 import com.android.systemui.statusbar.notification.row.shared.NotificationRowContentBinderRefactor
+import com.android.systemui.statusbar.notification.row.shared.RichOngoingContentModel
+import com.android.systemui.statusbar.notification.row.shared.TimerContentModel
 import com.android.systemui.statusbar.policy.InflatedSmartReplyState
 import com.android.systemui.statusbar.policy.InflatedSmartReplyViewHolder
 import com.android.systemui.statusbar.policy.SmartReplyStateInflater
-import com.android.systemui.util.concurrency.mockExecutorHandler
+import com.google.common.truth.Truth.assertThat
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.Executor
 import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.DisposableHandle
 import org.junit.Assert
 import org.junit.Before
 import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.kotlin.any
+import org.mockito.kotlin.argThat
+import org.mockito.kotlin.clearInvocations
+import org.mockito.kotlin.doReturn
 import org.mockito.kotlin.eq
+import org.mockito.kotlin.inOrder
 import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
 import org.mockito.kotlin.spy
 import org.mockito.kotlin.times
 import org.mockito.kotlin.verify
@@ -65,20 +78,24 @@
 @RunWithLooper
 @EnableFlags(NotificationRowContentBinderRefactor.FLAG_NAME)
 class NotificationRowContentBinderImplTest : SysuiTestCase() {
-    private lateinit var mNotificationInflater: NotificationRowContentBinderImpl
-    private lateinit var mBuilder: Notification.Builder
-    private lateinit var mRow: ExpandableNotificationRow
-    private lateinit var mHelper: NotificationTestHelper
+    private lateinit var notificationInflater: NotificationRowContentBinderImpl
+    private lateinit var builder: Notification.Builder
+    private lateinit var row: ExpandableNotificationRow
+    private lateinit var testHelper: NotificationTestHelper
 
-    private var mCache: NotifRemoteViewCache = mock()
-    private var mConversationNotificationProcessor: ConversationNotificationProcessor = mock()
-    private var mInflatedSmartReplyState: InflatedSmartReplyState = mock()
-    private var mInflatedSmartReplies: InflatedSmartReplyViewHolder = mock()
-    private var mNotifLayoutInflaterFactoryProvider: NotifLayoutInflaterFactory.Provider = mock()
-    private var mHeadsUpStyleProvider: HeadsUpStyleProvider = mock()
-    private var mNotifLayoutInflaterFactory: NotifLayoutInflaterFactory = mock()
-    private val mSmartReplyStateInflater: SmartReplyStateInflater =
+    private val cache: NotifRemoteViewCache = mock()
+    private val layoutInflaterFactoryProvider =
+        object : NotifLayoutInflaterFactory.Provider {
+            override fun provide(
+                row: ExpandableNotificationRow,
+                layoutType: Int
+            ): NotifLayoutInflaterFactory = mock()
+        }
+    private val smartReplyStateInflater: SmartReplyStateInflater =
         object : SmartReplyStateInflater {
+            private val inflatedSmartReplyState: InflatedSmartReplyState = mock()
+            private val inflatedSmartReplies: InflatedSmartReplyViewHolder = mock()
+
             override fun inflateSmartReplyViewHolder(
                 sysuiContext: Context,
                 notifPackageContext: Context,
@@ -86,37 +103,61 @@
                 existingSmartReplyState: InflatedSmartReplyState?,
                 newSmartReplyState: InflatedSmartReplyState
             ): InflatedSmartReplyViewHolder {
-                return mInflatedSmartReplies
+                return inflatedSmartReplies
             }
 
             override fun inflateSmartReplyState(entry: NotificationEntry): InflatedSmartReplyState {
-                return mInflatedSmartReplyState
+                return inflatedSmartReplyState
             }
         }
 
+    private var fakeRonContentModel: RichOngoingContentModel? = null
+    private val fakeRonExtractor =
+        object : RichOngoingNotificationContentExtractor {
+            override fun extractContentModel(
+                entry: NotificationEntry,
+                builder: Notification.Builder,
+                systemUIContext: Context,
+                packageContext: Context
+            ): RichOngoingContentModel? = fakeRonContentModel
+        }
+
+    private var fakeRonViewHolder: InflatedContentViewHolder? = null
+    private val fakeRonViewInflater =
+        spy(
+            object : RichOngoingNotificationViewInflater {
+                override fun inflateView(
+                    contentModel: RichOngoingContentModel,
+                    existingView: View?,
+                    entry: NotificationEntry,
+                    systemUiContext: Context,
+                    parentView: ViewGroup
+                ): InflatedContentViewHolder? = fakeRonViewHolder
+            }
+        )
+
     @Before
     fun setUp() {
         allowTestableLooperAsMainThread()
-        mBuilder =
+        builder =
             Notification.Builder(mContext, "no-id")
                 .setSmallIcon(R.drawable.ic_person)
                 .setContentTitle("Title")
                 .setContentText("Text")
                 .setStyle(Notification.BigTextStyle().bigText("big text"))
-        mHelper = NotificationTestHelper(mContext, mDependency)
-        val row = mHelper.createRow(mBuilder.build())
-        mRow = spy(row)
-        whenever(mNotifLayoutInflaterFactoryProvider.provide(any(), any()))
-            .thenReturn(mNotifLayoutInflaterFactory)
-        mNotificationInflater =
+        testHelper = NotificationTestHelper(mContext, mDependency)
+        row = spy(testHelper.createRow(builder.build()))
+        notificationInflater =
             NotificationRowContentBinderImpl(
-                mCache,
+                cache,
                 mock(),
-                mConversationNotificationProcessor,
+                mock<ConversationNotificationProcessor>(),
+                fakeRonExtractor,
+                fakeRonViewInflater,
                 mock(),
-                mSmartReplyStateInflater,
-                mNotifLayoutInflaterFactoryProvider,
-                mHeadsUpStyleProvider,
+                smartReplyStateInflater,
+                layoutInflaterFactoryProvider,
+                mock<HeadsUpStyleProvider>(),
                 mock()
             )
     }
@@ -125,16 +166,16 @@
     fun testIncreasedHeadsUpBeingUsed() {
         val params = BindParams()
         params.usesIncreasedHeadsUpHeight = true
-        val builder = spy(mBuilder)
-        mNotificationInflater.inflateNotificationViews(
-            mRow.entry,
-            mRow,
+        val builder = spy(builder)
+        notificationInflater.inflateNotificationViews(
+            row.entry,
+            row,
             params,
             true /* inflateSynchronously */,
-            NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL,
+            FLAG_CONTENT_VIEW_ALL,
             builder,
             mContext,
-            mSmartReplyStateInflater
+            smartReplyStateInflater
         )
         verify(builder).createHeadsUpContentView(true)
     }
@@ -143,80 +184,68 @@
     fun testIncreasedHeightBeingUsed() {
         val params = BindParams()
         params.usesIncreasedHeight = true
-        val builder = spy(mBuilder)
-        mNotificationInflater.inflateNotificationViews(
-            mRow.entry,
-            mRow,
+        val builder = spy(builder)
+        notificationInflater.inflateNotificationViews(
+            row.entry,
+            row,
             params,
             true /* inflateSynchronously */,
-            NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL,
+            FLAG_CONTENT_VIEW_ALL,
             builder,
             mContext,
-            mSmartReplyStateInflater
+            smartReplyStateInflater
         )
         verify(builder).createContentView(true)
     }
 
     @Test
     fun testInflationCallsUpdated() {
-        inflateAndWait(
-            mNotificationInflater,
-            NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL,
-            mRow
-        )
-        verify(mRow).onNotificationUpdated()
+        inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row)
+        verify(row).onNotificationUpdated()
     }
 
     @Test
     fun testInflationOnlyInflatesSetFlags() {
-        inflateAndWait(
-            mNotificationInflater,
-            NotificationRowContentBinder.FLAG_CONTENT_VIEW_HEADS_UP,
-            mRow
-        )
-        Assert.assertNotNull(mRow.privateLayout.headsUpChild)
-        verify(mRow).onNotificationUpdated()
+        inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_HEADS_UP, row)
+        Assert.assertNotNull(row.privateLayout.headsUpChild)
+        verify(row).onNotificationUpdated()
     }
 
     @Test
     fun testInflationThrowsErrorDoesntCallUpdated() {
-        mRow.privateLayout.removeAllViews()
-        mRow.entry.sbn.notification.contentView =
+        row.privateLayout.removeAllViews()
+        row.entry.sbn.notification.contentView =
             RemoteViews(mContext.packageName, R.layout.status_bar)
         inflateAndWait(
             true /* expectingException */,
-            mNotificationInflater,
-            NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL,
-            mRow
+            notificationInflater,
+            FLAG_CONTENT_VIEW_ALL,
+            row
         )
-        Assert.assertTrue(mRow.privateLayout.childCount == 0)
-        verify(mRow, times(0)).onNotificationUpdated()
+        Assert.assertTrue(row.privateLayout.childCount == 0)
+        verify(row, times(0)).onNotificationUpdated()
     }
 
     @Test
     fun testAsyncTaskRemoved() {
-        mRow.entry.abortTask()
-        inflateAndWait(
-            mNotificationInflater,
-            NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL,
-            mRow
-        )
-        verify(mRow).onNotificationUpdated()
+        row.entry.abortTask()
+        inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row)
+        verify(row).onNotificationUpdated()
     }
 
     @Test
     fun testRemovedNotInflated() {
-        mRow.setRemoved()
-        mNotificationInflater.setInflateSynchronously(true)
-        mNotificationInflater.bindContent(
-            mRow.entry,
-            mRow,
-            NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL,
+        row.setRemoved()
+        notificationInflater.setInflateSynchronously(true)
+        notificationInflater.bindContent(
+            row.entry,
+            row,
+            FLAG_CONTENT_VIEW_ALL,
             BindParams(),
             false /* forceInflate */,
             null /* callback */
         )
-        Assert.assertNull(mRow.entry.runningTask)
+        Assert.assertNull(row.entry.runningTask)
     }
 
     @Test
@@ -235,11 +264,11 @@
             inflateSynchronously = false,
             isMinimized = false,
             result = result,
-            reInflateFlags = NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED,
+            reInflateFlags = FLAG_CONTENT_VIEW_EXPANDED,
             inflationId = 0,
             remoteViewCache = mock(),
-            entry = mRow.entry,
-            row = mRow,
+            entry = row.entry,
+            row = row,
             isNewView = true, /* isNewView */
             remoteViewClickHandler = { _, _, _ -> true },
             callback =
@@ -253,7 +282,7 @@
                         countDownLatch.countDown()
                     }
                 },
-            parentLayout = mRow.privateLayout,
+            parentLayout = row.privateLayout,
             existingView = null,
             existingWrapper = null,
             runningInflations = HashMap(),
@@ -275,13 +304,13 @@
 
     @Test
     fun doesntReapplyDisallowedRemoteView() {
-        mBuilder.setStyle(Notification.MediaStyle())
-        val mediaView = mBuilder.createContentView()
-        mBuilder.setStyle(Notification.DecoratedCustomViewStyle())
-        mBuilder.setCustomContentView(
+        builder.setStyle(Notification.MediaStyle())
+        val mediaView = builder.createContentView()
+        builder.setStyle(Notification.DecoratedCustomViewStyle())
+        builder.setCustomContentView(
             RemoteViews(context.packageName, com.android.systemui.tests.R.layout.custom_view_dark)
         )
-        val decoratedMediaView = mBuilder.createContentView()
+        val decoratedMediaView = builder.createContentView()
         Assert.assertFalse(
             "The decorated media style doesn't allow a view to be reapplied!",
             NotificationRowContentBinderImpl.canReapplyRemoteView(mediaView, decoratedMediaView)
@@ -292,112 +321,167 @@
     @Ignore("b/345418902")
     fun testUsesSameViewWhenCachedPossibleToReuse() {
         // GIVEN a cached view.
-        val contractedRemoteView = mBuilder.createContentView()
-        whenever(
-                mCache.hasCachedView(
-                    mRow.entry,
-                    NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED
-                )
-            )
-            .thenReturn(true)
-        whenever(
-                mCache.getCachedView(
-                    mRow.entry,
-                    NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED
-                )
-            )
+        val contractedRemoteView = builder.createContentView()
+        whenever(cache.hasCachedView(row.entry, FLAG_CONTENT_VIEW_CONTRACTED)).thenReturn(true)
+        whenever(cache.getCachedView(row.entry, FLAG_CONTENT_VIEW_CONTRACTED))
             .thenReturn(contractedRemoteView)
 
         // GIVEN existing bound view with same layout id.
         val view = contractedRemoteView.apply(mContext, null /* parent */)
-        mRow.privateLayout.setContractedChild(view)
+        row.privateLayout.setContractedChild(view)
 
         // WHEN inflater inflates
-        inflateAndWait(
-            mNotificationInflater,
-            NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED,
-            mRow
-        )
+        inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row)
 
         // THEN the view should be re-used
         Assert.assertEquals(
             "Binder inflated a new view even though the old one was cached and usable.",
             view,
-            mRow.privateLayout.contractedChild
+            row.privateLayout.contractedChild
         )
     }
 
     @Test
     fun testInflatesNewViewWhenCachedNotPossibleToReuse() {
         // GIVEN a cached remote view.
-        val contractedRemoteView = mBuilder.createHeadsUpContentView()
-        whenever(
-                mCache.hasCachedView(
-                    mRow.entry,
-                    NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED
-                )
-            )
-            .thenReturn(true)
-        whenever(
-                mCache.getCachedView(
-                    mRow.entry,
-                    NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED
-                )
-            )
+        val contractedRemoteView = builder.createHeadsUpContentView()
+        whenever(cache.hasCachedView(row.entry, FLAG_CONTENT_VIEW_CONTRACTED)).thenReturn(true)
+        whenever(cache.getCachedView(row.entry, FLAG_CONTENT_VIEW_CONTRACTED))
             .thenReturn(contractedRemoteView)
 
         // GIVEN existing bound view with different layout id.
         val view: View = TextView(mContext)
-        mRow.privateLayout.setContractedChild(view)
+        row.privateLayout.setContractedChild(view)
 
         // WHEN inflater inflates
-        inflateAndWait(
-            mNotificationInflater,
-            NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED,
-            mRow
-        )
+        inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row)
 
         // THEN the view should be a new view
         Assert.assertNotEquals(
             "Binder (somehow) used the same view when inflating.",
             view,
-            mRow.privateLayout.contractedChild
+            row.privateLayout.contractedChild
         )
     }
 
     @Test
     fun testInflationCachesCreatedRemoteView() {
         // WHEN inflater inflates
-        inflateAndWait(
-            mNotificationInflater,
-            NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED,
-            mRow
-        )
+        inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row)
 
         // THEN inflater informs cache of the new remote view
-        verify(mCache)
-            .putCachedView(
-                eq(mRow.entry),
-                eq(NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED),
-                any()
-            )
+        verify(cache).putCachedView(eq(row.entry), eq(FLAG_CONTENT_VIEW_CONTRACTED), any())
     }
 
     @Test
     fun testUnbindRemovesCachedRemoteView() {
         // WHEN inflated unbinds content
-        mNotificationInflater.unbindContent(
-            mRow.entry,
-            mRow,
-            NotificationRowContentBinder.FLAG_CONTENT_VIEW_HEADS_UP
-        )
+        notificationInflater.unbindContent(row.entry, row, FLAG_CONTENT_VIEW_HEADS_UP)
 
         // THEN inflated informs cache to remove remote view
-        verify(mCache)
-            .removeCachedView(
-                eq(mRow.entry),
-                eq(NotificationRowContentBinder.FLAG_CONTENT_VIEW_HEADS_UP)
-            )
+        verify(cache).removeCachedView(eq(row.entry), eq(FLAG_CONTENT_VIEW_HEADS_UP))
+    }
+
+    @Test
+    fun testRonModelRequiredForRonView() {
+        fakeRonContentModel = null
+        val ronView = View(context)
+        fakeRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mock())
+        // WHEN inflater inflates
+        inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row)
+        verify(fakeRonViewInflater, never()).inflateView(any(), any(), any(), any(), any())
+    }
+
+    @Test
+    fun testRonModelTriggersInflationOfRonView() {
+        val mockRonModel = mock<TimerContentModel>()
+        val ronView = View(context)
+        val mockBinder = mock<DeferredContentViewBinder>()
+
+        val entry = row.entry
+        val privateLayout = row.privateLayout
+
+        fakeRonContentModel = mockRonModel
+        fakeRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mockBinder)
+        // WHEN inflater inflates
+        inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row)
+        // VERIFY that the inflater is invoked
+        verify(fakeRonViewInflater)
+            .inflateView(eq(mockRonModel), any(), eq(entry), any(), eq(privateLayout))
+        assertThat(row.privateLayout.contractedChild).isSameInstanceAs(ronView)
+        verify(mockBinder).setupContentViewBinder()
+    }
+
+    @Test
+    fun ronViewAppliesElementsInOrder() {
+        val oldHandle = mock<DisposableHandle>()
+        val mockRonModel = mock<TimerContentModel>()
+        val ronView = View(context)
+        val mockBinder = mock<DeferredContentViewBinder>()
+
+        row.privateLayout.mContractedBinderHandle = oldHandle
+        val entry = spy(row.entry)
+        row.entry = entry
+        val privateLayout = spy(row.privateLayout)
+        row.privateLayout = privateLayout
+
+        fakeRonContentModel = mockRonModel
+        fakeRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mockBinder)
+        // WHEN inflater inflates
+        inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row)
+
+        // Validate that these 4 steps happen in this precise order
+        inOrder(oldHandle, entry, privateLayout, mockBinder) {
+            verify(oldHandle).dispose()
+            verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel })
+            verify(privateLayout).setContractedChild(eq(ronView))
+            verify(mockBinder).setupContentViewBinder()
+        }
+    }
+
+    @Test
+    fun testRonNotReinflating() {
+        val handle0 = mock<DisposableHandle>()
+        val handle1 = mock<DisposableHandle>()
+        val ronView = View(context)
+        val mockRonModel1 = mock<TimerContentModel>()
+        val mockRonModel2 = mock<TimerContentModel>()
+        val mockBinder1 = mock<DeferredContentViewBinder>()
+        doReturn(handle1).whenever(mockBinder1).setupContentViewBinder()
+
+        row.privateLayout.mContractedBinderHandle = handle0
+        val entry = spy(row.entry)
+        row.entry = entry
+        val privateLayout = spy(row.privateLayout)
+        row.privateLayout = privateLayout
+
+        // WHEN inflater inflates both a model and a view
+        fakeRonContentModel = mockRonModel1
+        fakeRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mockBinder1)
+        inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row)
+
+        // Validate that these 4 steps happen in this precise order
+        inOrder(handle0, entry, privateLayout, mockBinder1, handle1) {
+            verify(handle0).dispose()
+            verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel1 })
+            verify(privateLayout).setContractedChild(eq(ronView))
+            verify(mockBinder1).setupContentViewBinder()
+            verify(handle1, never()).dispose()
+        }
+
+        clearInvocations(handle0, entry, privateLayout, mockBinder1, handle1)
+
+        // THEN when the inflater inflates just a model
+        fakeRonContentModel = mockRonModel2
+        fakeRonViewHolder = null
+        inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row)
+
+        // Validate that for reinflation, the only thing we do us update the model
+        verify(handle1, never()).dispose()
+        verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel2 })
+        verify(privateLayout, never()).setContractedChild(any())
+        verify(mockBinder1, never()).setupContentViewBinder()
+        verify(handle1, never()).dispose()
     }
 
     @Test
@@ -453,46 +537,36 @@
         whenever(view.measuredHeight)
             .thenReturn(
                 TypedValue.applyDimension(
-                        TypedValue.COMPLEX_UNIT_SP,
+                        COMPLEX_UNIT_SP,
                         measuredHeightDp,
                         mContext.resources.displayMetrics
                     )
                     .toInt()
             )
-        mRow.entry.targetSdk = targetSdk
-        mRow.entry.sbn.notification.contentView = contentView
-        return NotificationRowContentBinderImpl.isValidView(view, mRow.entry, mContext.resources)
+        row.entry.targetSdk = targetSdk
+        row.entry.sbn.notification.contentView = contentView
+        return NotificationRowContentBinderImpl.isValidView(view, row.entry, mContext.resources)
     }
 
     @Test
     fun testInvalidNotificationDoesNotInvokeCallback() {
-        mRow.privateLayout.removeAllViews()
-        mRow.entry.sbn.notification.contentView =
+        row.privateLayout.removeAllViews()
+        row.entry.sbn.notification.contentView =
             RemoteViews(
                 mContext.packageName,
                 com.android.systemui.tests.R.layout.invalid_notification_height
             )
-        inflateAndWait(
-            true,
-            mNotificationInflater,
-            NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL,
-            mRow
-        )
-        Assert.assertEquals(0, mRow.privateLayout.childCount.toLong())
-        verify(mRow, times(0)).onNotificationUpdated()
+        inflateAndWait(true, notificationInflater, FLAG_CONTENT_VIEW_ALL, row)
+        Assert.assertEquals(0, row.privateLayout.childCount.toLong())
+        verify(row, times(0)).onNotificationUpdated()
     }
 
     private class ExceptionHolder {
-        var mException: Exception? = null
-
-        fun setException(exception: Exception?) {
-            mException = exception
-        }
+        var exception: Exception? = null
     }
 
     private class AsyncFailRemoteView(packageName: String?, layoutId: Int) :
         RemoteViews(packageName, layoutId) {
-        var mHandler = mockExecutorHandler { p0 -> p0.run() }
 
         override fun apply(context: Context, parent: ViewGroup): View {
             return super.apply(context, parent)
@@ -505,7 +579,7 @@
             listener: OnViewAppliedListener,
             handler: InteractionHandler?
         ): CancellationSignal {
-            mHandler.post { listener.onError(RuntimeException("Failed to inflate async")) }
+            executor.execute { listener.onError(RuntimeException("Failed to inflate async")) }
             return CancellationSignal()
         }
 
@@ -541,18 +615,17 @@
                 object : InflationCallback {
                     override fun handleInflationException(entry: NotificationEntry, e: Exception) {
                         if (!expectingException) {
-                            exceptionHolder.setException(e)
+                            exceptionHolder.exception = e
                         }
                         countDownLatch.countDown()
                     }
 
                     override fun onAsyncInflationFinished(entry: NotificationEntry) {
                         if (expectingException) {
-                            exceptionHolder.setException(
+                            exceptionHolder.exception =
                                 RuntimeException(
                                     "Inflation finished even though there should be an error"
                                 )
-                            )
                         }
                         countDownLatch.countDown()
                     }
@@ -566,7 +639,7 @@
                 callback /* callback */
             )
             Assert.assertTrue(countDownLatch.await(500, TimeUnit.MILLISECONDS))
-            exceptionHolder.mException?.let { throw it }
+            exceptionHolder.exception?.let { throw it }
         }
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
index 21d586b..c74a04f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
@@ -199,6 +199,8 @@
                                 mock(NotifRemoteViewCache.class),
                                 mock(NotificationRemoteInputManager.class),
                                 mock(ConversationNotificationProcessor.class),
+                                mock(RichOngoingNotificationContentExtractor.class),
+                                mock(RichOngoingNotificationViewInflater.class),
                                 mock(Executor.class),
                                 new MockSmartReplyInflater(),
                                 mock(NotifLayoutInflaterFactory.Provider.class),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
index 9b0fd96..c1f2cb77 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
@@ -367,14 +367,14 @@
     @EnableSceneContainer
     fun updateState_withViewInShelf_showShelf() {
         // GIVEN a view is scrolled into the shelf
-        val stackCutoff = 200f
-        val scrimPadding =
-            context.resources.getDimensionPixelSize(R.dimen.notification_side_paddings)
-        val shelfTop = stackCutoff - scrimPadding - shelf.height
+        val stackTop = 200f
+        val stackHeight = 800f
+        whenever(ambientState.stackTop).thenReturn(stackTop)
+        whenever(ambientState.stackHeight).thenReturn(stackHeight)
+        val shelfTop = stackTop + stackHeight - shelf.height
         val stackScrollAlgorithmState = StackScrollAlgorithmState()
         val viewInShelf = mock(ExpandableView::class.java)
 
-        whenever(ambientState.stackCutoff).thenReturn(stackCutoff)
         whenever(ambientState.isShadeExpanded).thenReturn(true)
         whenever(ambientState.lastVisibleBackgroundChild).thenReturn(viewInShelf)
         whenever(viewInShelf.viewState).thenReturn(ExpandableViewState())
@@ -401,57 +401,14 @@
 
     @Test
     @EnableSceneContainer
-    fun updateState_withViewInShelfDuringExpansion_showShelf() {
-        // GIVEN a view is scrolled into the shelf
-        val stackCutoff = 200f
-        val scrimPadding =
-            context.resources.getDimensionPixelSize(R.dimen.notification_side_paddings)
-        val stackBottom = stackCutoff - scrimPadding
-        val shelfTop = stackBottom - shelf.height
-        val stackScrollAlgorithmState = StackScrollAlgorithmState()
-        val viewInShelf = mock(ExpandableView::class.java)
-
-        // AND a shade expansion is in progress
-        val shadeExpansionFraction = 0.5f
-
-        whenever(ambientState.stackCutoff).thenReturn(stackCutoff)
-        whenever(ambientState.isShadeExpanded).thenReturn(true)
-        whenever(ambientState.lastVisibleBackgroundChild).thenReturn(viewInShelf)
-        whenever(ambientState.isExpansionChanging).thenReturn(true)
-        whenever(ambientState.expansionFraction).thenReturn(shadeExpansionFraction)
-        whenever(viewInShelf.viewState).thenReturn(ExpandableViewState())
-        whenever(viewInShelf.shelfIcon).thenReturn(mock(StatusBarIconView::class.java))
-        whenever(viewInShelf.translationY).thenReturn(shelfTop)
-        whenever(viewInShelf.actualHeight).thenReturn(10)
-        whenever(viewInShelf.isInShelf).thenReturn(true)
-        whenever(viewInShelf.minHeight).thenReturn(10)
-        whenever(viewInShelf.shelfTransformationTarget).thenReturn(null) // use translationY
-        whenever(viewInShelf.isInShelf).thenReturn(true)
-
-        stackScrollAlgorithmState.visibleChildren.add(viewInShelf)
-        stackScrollAlgorithmState.firstViewInShelf = viewInShelf
-
-        // WHEN Shelf's ViewState is updated
-        shelf.updateState(stackScrollAlgorithmState, ambientState)
-
-        // THEN the shelf is visible
-        val shelfState = shelf.viewState as NotificationShelf.ShelfState
-        assertEquals(false, shelfState.hidden)
-        assertEquals(shelf.height, shelfState.height)
-        // AND its translation is scaled by the shade expansion
-        assertEquals((stackBottom * 0.75f) - shelf.height, shelfState.yTranslation)
-    }
-
-    @Test
-    @EnableSceneContainer
     fun updateState_withNullLastVisibleBackgroundChild_hideShelf_withSceneContainer() {
         // GIVEN
-        val stackCutoff = 200f
-        val scrimPadding =
-            context.resources.getDimensionPixelSize(R.dimen.notification_side_paddings)
+        val stackTop = 200f
+        val stackHeight = 800f
+        whenever(ambientState.stackTop).thenReturn(stackTop)
+        whenever(ambientState.stackHeight).thenReturn(stackHeight)
         val paddingBetweenElements =
             context.resources.getDimensionPixelSize(R.dimen.notification_divider_height)
-        whenever(ambientState.stackCutoff).thenReturn(stackCutoff)
         whenever(ambientState.isShadeExpanded).thenReturn(true)
         val lastVisibleBackgroundChild = mock<ExpandableView>()
         val expandableViewState = ExpandableViewState()
@@ -467,7 +424,7 @@
         // THEN
         val shelfState = shelf.viewState as NotificationShelf.ShelfState
         assertEquals(true, shelfState.hidden)
-        assertEquals(stackCutoff - scrimPadding + paddingBetweenElements, shelfState.yTranslation)
+        assertEquals(stackTop + stackHeight + paddingBetweenElements, shelfState.yTranslation)
     }
 
     @Test
@@ -501,12 +458,12 @@
     @EnableSceneContainer
     fun updateState_withNullFirstViewInShelf_hideShelf_withSceneContainer() {
         // GIVEN
-        val stackCutoff = 200f
-        val scrimPadding =
-            context.resources.getDimensionPixelSize(R.dimen.notification_side_paddings)
+        val stackTop = 200f
+        val stackHeight = 800f
+        whenever(ambientState.stackTop).thenReturn(stackTop)
+        whenever(ambientState.stackHeight).thenReturn(stackHeight)
         val paddingBetweenElements =
             context.resources.getDimensionPixelSize(R.dimen.notification_divider_height)
-        whenever(ambientState.stackCutoff).thenReturn(stackCutoff)
         whenever(ambientState.isShadeExpanded).thenReturn(true)
         val lastVisibleBackgroundChild = mock<ExpandableView>()
         val expandableViewState = ExpandableViewState()
@@ -522,7 +479,7 @@
         // THEN
         val shelfState = shelf.viewState as NotificationShelf.ShelfState
         assertEquals(true, shelfState.hidden)
-        assertEquals(stackCutoff - scrimPadding + paddingBetweenElements, shelfState.yTranslation)
+        assertEquals(stackTop + stackHeight + paddingBetweenElements, shelfState.yTranslation)
     }
 
     @Test
@@ -556,12 +513,12 @@
     @EnableSceneContainer
     fun updateState_withCollapsedShade_hideShelf_withSceneContainer() {
         // GIVEN
-        val stackCutoff = 200f
-        val scrimPadding =
-            context.resources.getDimensionPixelSize(R.dimen.notification_side_paddings)
+        val stackTop = 200f
+        val stackHeight = 800f
+        whenever(ambientState.stackTop).thenReturn(stackTop)
+        whenever(ambientState.stackHeight).thenReturn(stackHeight)
         val paddingBetweenElements =
             context.resources.getDimensionPixelSize(R.dimen.notification_divider_height)
-        whenever(ambientState.stackCutoff).thenReturn(stackCutoff)
         val lastVisibleBackgroundChild = mock<ExpandableView>()
         val expandableViewState = ExpandableViewState()
         whenever(lastVisibleBackgroundChild.viewState).thenReturn(expandableViewState)
@@ -577,7 +534,7 @@
         // THEN
         val shelfState = shelf.viewState as NotificationShelf.ShelfState
         assertEquals(true, shelfState.hidden)
-        assertEquals(stackCutoff - scrimPadding + paddingBetweenElements, shelfState.yTranslation)
+        assertEquals(stackTop + stackHeight + paddingBetweenElements, shelfState.yTranslation)
     }
 
     @Test
@@ -609,12 +566,12 @@
 
     @Test
     @EnableSceneContainer
-    fun updateState_withHiddenSectionBeforeShelf_hideShelf_withSceneContianer() {
+    fun updateState_withHiddenSectionBeforeShelf_hideShelf_withSceneContainer() {
         // GIVEN
-        val stackCutoff = 200f
-        whenever(ambientState.stackCutoff).thenReturn(stackCutoff)
-        val scrimPadding =
-            context.resources.getDimensionPixelSize(R.dimen.notification_side_paddings)
+        val stackTop = 200f
+        val stackHeight = 800f
+        whenever(ambientState.stackTop).thenReturn(stackTop)
+        whenever(ambientState.stackHeight).thenReturn(stackHeight)
         val paddingBetweenElements =
             context.resources.getDimensionPixelSize(R.dimen.notification_divider_height)
         whenever(ambientState.isShadeExpanded).thenReturn(true)
@@ -646,7 +603,7 @@
         // THEN
         val shelfState = shelf.viewState as NotificationShelf.ShelfState
         assertEquals(true, shelfState.hidden)
-        assertEquals(stackCutoff - scrimPadding + paddingBetweenElements, shelfState.yTranslation)
+        assertEquals(stackTop + stackHeight + paddingBetweenElements, shelfState.yTranslation)
     }
 
     @Test
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 12f3ef3..770c424 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
@@ -214,6 +214,7 @@
     }
 
     @Test
+    @DisableSceneContainer // TODO(b/332574413) cover stack bounds integration with tests
     public void testUpdateStackHeight_qsExpansionGreaterThanZero() {
         final float expansionFraction = 0.2f;
         final float overExpansion = 50f;
@@ -261,15 +262,62 @@
     }
 
     @Test
-    public void updateStackEndHeightAndStackHeight_normallyUpdatesBoth() {
-        final float expansionFraction = 0.5f;
+    @EnableSceneContainer
+    public void updateStackEndHeightAndStackHeight_shadeFullyExpanded_withSceneContainer() {
+        final float stackTop = 200f;
+        final float stackCutoff = 1000f;
+        final float stackEndHeight = stackCutoff - stackTop;
+        mAmbientState.setStackTop(stackTop);
+        mAmbientState.setStackCutoff(stackCutoff);
         mAmbientState.setStatusBarState(StatusBarState.KEYGUARD);
-
-        // Validate that by default we update everything
         clearInvocations(mAmbientState);
+
+        // WHEN shade is fully expanded
+        mStackScroller.updateStackEndHeightAndStackHeight(/* fraction = */ 1.0f);
+
+        // THEN stackHeight and stackEndHeight are the same
+        verify(mAmbientState).setStackEndHeight(stackEndHeight);
+        verify(mAmbientState).setStackHeight(stackEndHeight);
+    }
+
+    @Test
+    @EnableSceneContainer
+    public void updateStackEndHeightAndStackHeight_shadeExpanding_withSceneContainer() {
+        final float stackTop = 200f;
+        final float stackCutoff = 1000f;
+        final float stackEndHeight = stackCutoff - stackTop;
+        mAmbientState.setStackTop(stackTop);
+        mAmbientState.setStackCutoff(stackCutoff);
+        mAmbientState.setStatusBarState(StatusBarState.KEYGUARD);
+        clearInvocations(mAmbientState);
+
+        // WHEN shade is expanding
+        final float expansionFraction = 0.5f;
         mStackScroller.updateStackEndHeightAndStackHeight(expansionFraction);
-        verify(mAmbientState).setStackEndHeight(anyFloat());
-        verify(mAmbientState).setStackHeight(anyFloat());
+
+        // THEN stackHeight is changed by the expansion frac
+        verify(mAmbientState).setStackEndHeight(stackEndHeight);
+        verify(mAmbientState).setStackHeight(stackEndHeight * 0.75f);
+    }
+
+    @Test
+    @EnableSceneContainer
+    public void updateStackEndHeightAndStackHeight_shadeOverscrolledToTop_withSceneContainer() {
+        // GIVEN stack scrolled over the top, stack top is negative
+        final float stackTop = -2000f;
+        final float stackCutoff = 1000f;
+        final float stackEndHeight = stackCutoff - stackTop;
+        mAmbientState.setStackTop(stackTop);
+        mAmbientState.setStackCutoff(stackCutoff);
+        mAmbientState.setStatusBarState(StatusBarState.KEYGUARD);
+        clearInvocations(mAmbientState);
+
+        // WHEN stack is updated
+        mStackScroller.updateStackEndHeightAndStackHeight(/* fraction = */ 1.0f);
+
+        // THEN stackHeight is measured from the stack top
+        verify(mAmbientState).setStackEndHeight(stackEndHeight);
+        verify(mAmbientState).setStackHeight(stackEndHeight);
     }
 
     @Test
@@ -891,6 +939,7 @@
     }
 
     @Test
+    @DisableSceneContainer // NSSL has no more scroll logic when SceneContainer is on
     public void testNormalShade_hasNoTopOverscroll() {
         mTestableResources
                 .addOverride(R.bool.config_use_split_notification_shade, /* value= */ false);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
index d28e0c1..b12c098 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
@@ -8,9 +8,12 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerProgress
+import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.ShadeInterpolation.getContentAlpha
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.flags.DisableSceneContainer
+import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.FeatureFlagsClassic
 import com.android.systemui.res.R
@@ -66,7 +69,7 @@
         EmptyShadeView(context, /* attrs= */ null).apply {
             layout(/* l= */ 0, /* t= */ 0, /* r= */ 100, /* b= */ 100)
         }
-    private val footerView = FooterView(context, /*attrs=*/ null)
+    private val footerView = FooterView(context, /* attrs= */ null)
     @OptIn(ExperimentalCoroutinesApi::class)
     private val ambientState =
         AmbientState(
@@ -126,6 +129,7 @@
     }
 
     @Test
+    @DisableSceneContainer // TODO(b/332574413) cover hun bounds integration with tests
     fun resetViewStates_defaultHunWhenShadeIsOpening_yTranslationIsInset() {
         whenever(notificationRow.isPinned).thenReturn(true)
         whenever(notificationRow.isHeadsUp).thenReturn(true)
@@ -168,6 +172,7 @@
     }
 
     @Test
+    @DisableSceneContainer // TODO(b/332574413) cover hun bounds integration with tests
     @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
     fun resetViewStates_defaultHun_showingQS_newHeadsUpAnim_hunTranslatedToMax() {
         // Given: the shade is open and scrolled to the bottom to show the QuickSettings
@@ -184,6 +189,7 @@
     }
 
     @Test
+    @DisableSceneContainer // TODO(b/332574413) cover hun bounds integration with tests
     @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
     fun resetViewStates_hunAnimatingAway_showingQS_newHeadsUpAnim_hunTranslatedToBottomOfScreen() {
         // Given: the shade is open and scrolled to the bottom to show the QuickSettings
@@ -272,6 +278,27 @@
     }
 
     @Test
+    @EnableSceneContainer
+    fun resetViewStates_emptyShadeView_isCenteredVertically_withSceneContainer() {
+        stackScrollAlgorithm.initView(context)
+        hostView.removeAllViews()
+        hostView.addView(emptyShadeView)
+        ambientState.layoutMaxHeight = maxPanelHeight.toInt()
+
+        val stackTop = 200f
+        val stackBottom = 2000f
+        val stackHeight = stackBottom - stackTop
+        ambientState.stackTop = stackTop
+        ambientState.stackCutoff = stackBottom
+
+        stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0)
+
+        val centeredY = stackTop + stackHeight / 2f - emptyShadeView.height / 2f
+        assertThat(emptyShadeView.viewState.yTranslation).isEqualTo(centeredY)
+    }
+
+    @Test
+    @DisableSceneContainer
     fun resetViewStates_emptyShadeView_isCenteredVertically() {
         stackScrollAlgorithm.initView(context)
         hostView.removeAllViews()
@@ -523,6 +550,7 @@
         assertThat((footerView.viewState as FooterViewState).hideContent).isTrue()
     }
 
+    @DisableFlags(Flags.FLAG_NOTIFICATIONS_FOOTER_VIEW_REFACTOR)
     @Test
     fun resetViewStates_clearAllInProgress_allRowsRemoved_emptyShade_footerHidden() {
         ambientState.isClearAllInProgress = true
@@ -1157,6 +1185,7 @@
 
         assertFalse(stackScrollAlgorithm.shouldHunAppearFromBottom(ambientState, viewState))
     }
+
     // endregion
 
     private fun createHunViewMock(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt
index f2f336c..dfee2ed 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt
@@ -21,11 +21,14 @@
 import android.app.admin.DevicePolicyResourcesManager
 import android.content.SharedPreferences
 import android.os.UserManager
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import android.telecom.TelecomManager
 import android.testing.TestableLooper
 import android.testing.TestableLooper.RunWithLooper
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor
@@ -39,6 +42,7 @@
 import com.android.systemui.statusbar.phone.ui.StatusBarIconController
 import com.android.systemui.statusbar.policy.BluetoothController
 import com.android.systemui.statusbar.policy.CastController
+import com.android.systemui.statusbar.policy.CastDevice
 import com.android.systemui.statusbar.policy.DataSaverController
 import com.android.systemui.statusbar.policy.DeviceProvisionedController
 import com.android.systemui.statusbar.policy.HotspotController
@@ -54,6 +58,7 @@
 import com.android.systemui.util.kotlin.JavaAdapter
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.time.DateFormatUtil
 import com.android.systemui.util.time.FakeSystemClock
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -78,6 +83,7 @@
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.argumentCaptor
 
 @RunWith(AndroidJUnit4::class)
 @RunWithLooper
@@ -87,6 +93,8 @@
 
     companion object {
         private const val ALARM_SLOT = "alarm"
+        private const val CAST_SLOT = "cast"
+        private const val SCREEN_RECORD_SLOT = "screen_record"
         private const val CONNECTED_DISPLAY_SLOT = "connected_display"
         private const val MANAGED_PROFILE_SLOT = "managed_profile"
     }
@@ -271,6 +279,101 @@
             verify(iconController).setIconVisibility(CONNECTED_DISPLAY_SLOT, true)
         }
 
+    @Test
+    @DisableFlags(Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
+    fun cast_chipsFlagOff_iconShown() {
+        statusBarPolicy.init()
+        clearInvocations(iconController)
+
+        val callbackCaptor = argumentCaptor<CastController.Callback>()
+        verify(castController).addCallback(callbackCaptor.capture())
+
+        whenever(castController.castDevices)
+            .thenReturn(
+                listOf(
+                    CastDevice(
+                        "id",
+                        "name",
+                        "description",
+                        CastDevice.CastState.Connected,
+                        CastDevice.CastOrigin.MediaProjection,
+                    )
+                )
+            )
+        callbackCaptor.firstValue.onCastDevicesChanged()
+
+        verify(iconController).setIconVisibility(CAST_SLOT, true)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
+    fun cast_chipsFlagOn_noCallbackRegistered() {
+        statusBarPolicy.init()
+
+        verify(castController, never()).addCallback(any())
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
+    fun screenRecord_chipsFlagOff_iconShown_forAllStates() {
+        statusBarPolicy.init()
+        clearInvocations(iconController)
+
+        val callbackCaptor = argumentCaptor<RecordingController.RecordingStateChangeCallback>()
+        verify(recordingController).addCallback(callbackCaptor.capture())
+
+        callbackCaptor.firstValue.onCountdown(3000)
+        testableLooper.processAllMessages()
+        verify(iconController).setIconVisibility(SCREEN_RECORD_SLOT, true)
+        clearInvocations(iconController)
+
+        callbackCaptor.firstValue.onCountdownEnd()
+        testableLooper.processAllMessages()
+        verify(iconController).setIconVisibility(SCREEN_RECORD_SLOT, false)
+        clearInvocations(iconController)
+
+        callbackCaptor.firstValue.onRecordingStart()
+        testableLooper.processAllMessages()
+        verify(iconController).setIconVisibility(SCREEN_RECORD_SLOT, true)
+        clearInvocations(iconController)
+
+        callbackCaptor.firstValue.onRecordingEnd()
+        testableLooper.processAllMessages()
+        verify(iconController).setIconVisibility(SCREEN_RECORD_SLOT, false)
+        clearInvocations(iconController)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
+    fun screenRecord_chipsFlagOn_noCallbackRegistered() {
+        statusBarPolicy.init()
+
+        verify(recordingController, never()).addCallback(any())
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
+    fun screenRecord_chipsFlagOn_methodsDoNothing() {
+        statusBarPolicy.init()
+        clearInvocations(iconController)
+
+        statusBarPolicy.onCountdown(3000)
+        testableLooper.processAllMessages()
+        verify(iconController, never()).setIconVisibility(eq(SCREEN_RECORD_SLOT), any())
+
+        statusBarPolicy.onCountdownEnd()
+        testableLooper.processAllMessages()
+        verify(iconController, never()).setIconVisibility(eq(SCREEN_RECORD_SLOT), any())
+
+        statusBarPolicy.onRecordingStart()
+        testableLooper.processAllMessages()
+        verify(iconController, never()).setIconVisibility(eq(SCREEN_RECORD_SLOT), any())
+
+        statusBarPolicy.onRecordingEnd()
+        testableLooper.processAllMessages()
+        verify(iconController, never()).setIconVisibility(eq(SCREEN_RECORD_SLOT), any())
+    }
+
     private fun createAlarmInfo(): AlarmManager.AlarmClockInfo {
         return AlarmManager.AlarmClockInfo(10L, null)
     }
@@ -315,13 +418,18 @@
 
     private class FakeConnectedDisplayStateProvider : ConnectedDisplayInteractor {
         private val flow = MutableSharedFlow<State>()
+
         suspend fun emit(value: State) = flow.emit(value)
+
         override val connectedDisplayState: Flow<State>
             get() = flow
+
         override val connectedDisplayAddition: Flow<Unit>
             get() = TODO("Not yet implemented")
+
         override val pendingDisplay: Flow<PendingDisplay?>
             get() = TODO("Not yet implemented")
+
         override val concurrentDisplaysInProgress: Flow<Boolean>
             get() = TODO("Not yet implemented")
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepositoryImplTest.kt
deleted file mode 100644
index 004f679..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepositoryImplTest.kt
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file:OptIn(ExperimentalCoroutinesApi::class)
-
-package com.android.systemui.statusbar.policy.data.repository
-
-import android.app.NotificationManager
-import android.provider.Settings
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.statusbar.policy.ZenModeController
-import com.android.systemui.util.mockito.whenever
-import com.android.systemui.util.mockito.withArgCaptor
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.Mockito
-import org.mockito.MockitoAnnotations
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class ZenModeRepositoryImplTest : SysuiTestCase() {
-    @Mock lateinit var zenModeController: ZenModeController
-
-    lateinit var underTest: ZenModeRepositoryImpl
-
-    private val testPolicy = NotificationManager.Policy(0, 1, 0)
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-        underTest = ZenModeRepositoryImpl(zenModeController)
-    }
-
-    @Test
-    fun zenMode_reflectsCurrentControllerState() = runTest {
-        whenever(zenModeController.zen).thenReturn(Settings.Global.ZEN_MODE_NO_INTERRUPTIONS)
-        val zenMode by collectLastValue(underTest.zenMode)
-        assertThat(zenMode).isEqualTo(Settings.Global.ZEN_MODE_NO_INTERRUPTIONS)
-    }
-
-    @Test
-    fun zenMode_updatesWhenControllerStateChanges() = runTest {
-        val zenMode by collectLastValue(underTest.zenMode)
-        runCurrent()
-        whenever(zenModeController.zen).thenReturn(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
-        withArgCaptor { Mockito.verify(zenModeController).addCallback(capture()) }
-            .onZenChanged(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
-        assertThat(zenMode).isEqualTo(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
-    }
-
-    @Test
-    fun policy_reflectsCurrentControllerState() {
-        runTest {
-            whenever(zenModeController.consolidatedPolicy).thenReturn(testPolicy)
-            val policy by collectLastValue(underTest.consolidatedNotificationPolicy)
-            assertThat(policy).isEqualTo(testPolicy)
-        }
-    }
-
-    @Test
-    fun policy_updatesWhenControllerStateChanges() = runTest {
-        val policy by collectLastValue(underTest.consolidatedNotificationPolicy)
-        runCurrent()
-        whenever(zenModeController.consolidatedPolicy).thenReturn(testPolicy)
-        withArgCaptor { Mockito.verify(zenModeController).addCallback(capture()) }
-            .onConsolidatedPolicyChanged(testPolicy)
-        assertThat(policy).isEqualTo(testPolicy)
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
deleted file mode 100644
index 8981009..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
+++ /dev/null
@@ -1,164 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.policy.domain.interactor
-
-import android.app.NotificationManager.Policy
-import android.provider.Settings
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysUITestComponent
-import com.android.systemui.SysUITestModule
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.collectLastValue
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.runCurrent
-import com.android.systemui.runTest
-import com.android.systemui.statusbar.policy.data.repository.FakeZenModeRepository
-import com.android.systemui.user.domain.UserDomainLayerModule
-import com.google.common.truth.Truth.assertThat
-import dagger.BindsInstance
-import dagger.Component
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class ZenModeInteractorTest : SysuiTestCase() {
-    @SysUISingleton
-    @Component(
-        modules =
-            [
-                SysUITestModule::class,
-                UserDomainLayerModule::class,
-            ]
-    )
-    interface TestComponent : SysUITestComponent<ZenModeInteractor> {
-
-        val repository: FakeZenModeRepository
-
-        @Component.Factory
-        interface Factory {
-            fun create(@BindsInstance test: SysuiTestCase): TestComponent
-        }
-    }
-
-    private val testComponent: TestComponent =
-        DaggerZenModeInteractorTest_TestComponent.factory().create(test = this)
-
-    @Test
-    fun testIsZenModeEnabled_off() =
-        testComponent.runTest {
-            val enabled by collectLastValue(underTest.isZenModeEnabled)
-
-            repository.zenMode.value = Settings.Global.ZEN_MODE_OFF
-            runCurrent()
-
-            assertThat(enabled).isFalse()
-        }
-
-    @Test
-    fun testIsZenModeEnabled_alarms() =
-        testComponent.runTest {
-            val enabled by collectLastValue(underTest.isZenModeEnabled)
-
-            repository.zenMode.value = Settings.Global.ZEN_MODE_ALARMS
-            runCurrent()
-
-            assertThat(enabled).isTrue()
-        }
-
-    @Test
-    fun testIsZenModeEnabled_importantInterruptions() =
-        testComponent.runTest {
-            val enabled by collectLastValue(underTest.isZenModeEnabled)
-
-            repository.zenMode.value = Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS
-            runCurrent()
-
-            assertThat(enabled).isTrue()
-        }
-
-    @Test
-    fun testIsZenModeEnabled_noInterruptions() =
-        testComponent.runTest {
-            val enabled by collectLastValue(underTest.isZenModeEnabled)
-
-            repository.zenMode.value = Settings.Global.ZEN_MODE_NO_INTERRUPTIONS
-            runCurrent()
-
-            assertThat(enabled).isTrue()
-        }
-
-    @Test
-    fun testIsZenModeEnabled_unknown() =
-        testComponent.runTest {
-            val enabled by collectLastValue(underTest.isZenModeEnabled)
-
-            repository.zenMode.value = 4 // this should fail if we ever add another zen mode type
-            runCurrent()
-
-            assertThat(enabled).isFalse()
-        }
-
-    @Test
-    fun testAreNotificationsHiddenInShade_noPolicy() =
-        testComponent.runTest {
-            val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
-
-            repository.consolidatedNotificationPolicy.value = null
-            repository.zenMode.value = Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS
-            runCurrent()
-
-            assertThat(hidden).isFalse()
-        }
-
-    @Test
-    fun testAreNotificationsHiddenInShade_zenOffShadeSuppressed() =
-        testComponent.runTest {
-            val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
-
-            repository.setSuppressedVisualEffects(Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST)
-            repository.zenMode.value = Settings.Global.ZEN_MODE_OFF
-            runCurrent()
-
-            assertThat(hidden).isFalse()
-        }
-
-    @Test
-    fun testAreNotificationsHiddenInShade_zenOnShadeNotSuppressed() =
-        testComponent.runTest {
-            val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
-
-            repository.setSuppressedVisualEffects(Policy.SUPPRESSED_EFFECT_STATUS_BAR)
-            repository.zenMode.value = Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS
-            runCurrent()
-
-            assertThat(hidden).isFalse()
-        }
-
-    @Test
-    fun testAreNotificationsHiddenInShade_zenOnShadeSuppressed() =
-        testComponent.runTest {
-            val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
-
-            repository.setSuppressedVisualEffects(Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST)
-            repository.zenMode.value = Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS
-            runCurrent()
-
-            assertThat(hidden).isTrue()
-        }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index fabb9b7..c5fbc39 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -25,6 +25,8 @@
 import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL;
 import static android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED;
 
+import static androidx.test.ext.truth.content.IntentSubject.assertThat;
+
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.server.notification.Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING;
 import static com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_BAR;
@@ -2017,6 +2019,31 @@
     }
 
     @Test
+    public void testShowOrHideAppBubble_updateExistedBubbleInOverflow_updateIntentInBubble() {
+        String appBubbleKey = Bubble.getAppBubbleKeyForApp(mAppBubbleIntent.getPackage(), mUser0);
+        mBubbleController.showOrHideAppBubble(mAppBubbleIntent, mUser0, mAppBubbleIcon);
+        // Collapse the stack so we don't need to wait for the dismiss animation in the test
+        mBubbleController.collapseStack();
+        // Dismiss the app bubble so it's in the overflow
+        mBubbleController.dismissBubble(appBubbleKey, Bubbles.DISMISS_USER_GESTURE);
+        assertThat(mBubbleData.getOverflowBubbleWithKey(appBubbleKey)).isNotNull();
+
+        // Modify the intent to include new extras.
+        Intent newAppBubbleIntent = new Intent(mContext, BubblesTestActivity.class)
+                .setPackage(mContext.getPackageName())
+                .putExtra("hello", "world");
+
+        // Calling this while collapsed will re-add and expand the app bubble
+        mBubbleController.showOrHideAppBubble(newAppBubbleIntent, mUser0, mAppBubbleIcon);
+        assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(appBubbleKey);
+        assertThat(mBubbleController.isStackExpanded()).isTrue();
+        assertThat(mBubbleData.getBubbles().size()).isEqualTo(1);
+        assertThat(mBubbleData.getBubbles().get(0).getAppBubbleIntent()).extras().string(
+                "hello").isEqualTo("world");
+        assertThat(mBubbleData.getOverflowBubbleWithKey(appBubbleKey)).isNull();
+    }
+
+    @Test
     public void testCreateBubbleFromOngoingNotification() {
         NotificationEntry notif = new NotificationEntryBuilder()
                 .setFlag(mContext, Notification.FLAG_ONGOING_EVENT, true)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt
index 9dae44d..5db9d31 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt
@@ -16,6 +16,7 @@
 package com.android.systemui
 
 import android.app.ActivityManager
+import android.app.DreamManager
 import android.app.admin.DevicePolicyManager
 import android.app.trust.TrustManager
 import android.os.UserManager
@@ -32,6 +33,7 @@
 import com.android.systemui.biometrics.AuthController
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
 import com.android.systemui.demomode.DemoModeController
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.keyguard.ScreenLifecycle
@@ -93,6 +95,7 @@
     @get:Provides val demoModeController: DemoModeController = mock(),
     @get:Provides val deviceProvisionedController: DeviceProvisionedController = mock(),
     @get:Provides val dozeParameters: DozeParameters = mock(),
+    @get:Provides val dreamManager: DreamManager = mock(),
     @get:Provides val dumpManager: DumpManager = mock(),
     @get:Provides val headsUpManager: HeadsUpManager = mock(),
     @get:Provides val guestResumeSessionReceiver: GuestResumeSessionReceiver = mock(),
@@ -130,6 +133,7 @@
     @get:Provides val systemUIDialogManager: SystemUIDialogManager = mock(),
     @get:Provides val deviceEntryIconTransitions: Set<DeviceEntryIconTransition> = emptySet(),
     @get:Provides val communalInteractor: CommunalInteractor = mock(),
+    @get:Provides val communalSceneInteractor: CommunalSceneInteractor = mock(),
     @get:Provides val sceneLogger: SceneLogger = mock(),
     @get:Provides val trustManager: TrustManager = mock(),
     @get:Provides val primaryBouncerInteractor: PrimaryBouncerInteractor = mock(),
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeTrustRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeTrustRepository.kt
index cd83c2f..39a1a63 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeTrustRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeTrustRepository.kt
@@ -33,9 +33,10 @@
     private val _isTrustUsuallyManaged = MutableStateFlow(false)
     override val isCurrentUserTrustUsuallyManaged: StateFlow<Boolean>
         get() = _isTrustUsuallyManaged
+
     private val _isCurrentUserTrusted = MutableStateFlow(false)
-    override val isCurrentUserTrusted: Flow<Boolean>
-        get() = _isCurrentUserTrusted
+    override val isCurrentUserTrusted: StateFlow<Boolean>
+        get() = _isCurrentUserTrusted.asStateFlow()
 
     private val _isCurrentUserActiveUnlockAvailable = MutableStateFlow(false)
     override val isCurrentUserActiveUnlockRunning: StateFlow<Boolean> =
@@ -48,6 +49,13 @@
     private val _requestDismissKeyguard = MutableStateFlow(TrustModel(false, 0, TrustGrantFlags(0)))
     override val trustAgentRequestingToDismissKeyguard: Flow<TrustModel> = _requestDismissKeyguard
 
+    var keyguardShowingChangeEventCount: Int = 0
+        private set
+
+    override suspend fun reportKeyguardShowingChanged() {
+        keyguardShowingChangeEventCount++
+    }
+
     fun setCurrentUserTrusted(trust: Boolean) {
         _isCurrentUserTrusted.value = trust
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt
index edf77a0..744b127 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt
@@ -16,7 +16,9 @@
 
 package com.android.systemui.keyguard.domain.interactor
 
+import android.service.dream.dreamManager
 import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.communal.domain.interactor.communalSceneInteractor
 import com.android.systemui.deviceentry.data.repository.deviceEntryRepository
 import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
 import com.android.systemui.kosmos.Kosmos
@@ -35,8 +37,10 @@
             mainDispatcher = testDispatcher,
             keyguardInteractor = keyguardInteractor,
             communalInteractor = communalInteractor,
+            communalSceneInteractor = communalSceneInteractor,
             powerInteractor = powerInteractor,
             keyguardOcclusionInteractor = keyguardOcclusionInteractor,
             deviceEntryRepository = deviceEntryRepository,
+            dreamManager = dreamManager
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/TrustInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/TrustInteractorKosmos.kt
index 0ebf164..d60326c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/TrustInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/TrustInteractorKosmos.kt
@@ -19,5 +19,11 @@
 import com.android.systemui.keyguard.data.repository.trustRepository
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.applicationCoroutineScope
 
-val Kosmos.trustInteractor by Fixture { TrustInteractor(repository = trustRepository) }
+val Kosmos.trustInteractor by Fixture {
+    TrustInteractor(
+        applicationScope = applicationCoroutineScope,
+        repository = trustRepository,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/FixedColumnsRepositoryKosmos.kt
similarity index 88%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepositoryKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/FixedColumnsRepositoryKosmos.kt
index d8af3fa..2f5daaa 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/FixedColumnsRepositoryKosmos.kt
@@ -18,4 +18,4 @@
 
 import com.android.systemui.kosmos.Kosmos
 
-val Kosmos.infiniteGridSizeRepository by Kosmos.Fixture { InfiniteGridSizeRepository() }
+val Kosmos.fixedColumnsRepository by Kosmos.Fixture { FixedColumnsRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/PaginatedGridRepositoryKosmos.kt
similarity index 69%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepositoryKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/PaginatedGridRepositoryKosmos.kt
index d8af3fa..696c4bf 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/PaginatedGridRepositoryKosmos.kt
@@ -16,6 +16,12 @@
 
 package com.android.systemui.qs.panels.data.repository
 
+import com.android.systemui.common.ui.data.repository.configurationRepository
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
 
-val Kosmos.infiniteGridSizeRepository by Kosmos.Fixture { InfiniteGridSizeRepository() }
+val Kosmos.paginatedGridRepository by
+    Kosmos.Fixture {
+        testCase.context.orCreateTestableResources
+        PaginatedGridRepository(testCase.context.resources, configurationRepository)
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridSizeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/FixedColumnsSizeInteractorKosmos.kt
similarity index 78%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridSizeInteractorKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/FixedColumnsSizeInteractorKosmos.kt
index 6e11977..f4d281d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridSizeInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/FixedColumnsSizeInteractorKosmos.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.qs.panels.domain.interactor
 
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.qs.panels.data.repository.infiniteGridSizeRepository
+import com.android.systemui.qs.panels.data.repository.fixedColumnsRepository
 
-val Kosmos.infiniteGridSizeInteractor by
-    Kosmos.Fixture { InfiniteGridSizeInteractor(infiniteGridSizeRepository) }
+val Kosmos.fixedColumnsSizeInteractor by
+    Kosmos.Fixture { FixedColumnsSizeInteractor(fixedColumnsRepository) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractorKosmos.kt
index 7f387d7..320c2ec 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractorKosmos.kt
@@ -20,5 +20,5 @@
 
 val Kosmos.infiniteGridConsistencyInteractor by
     Kosmos.Fixture {
-        InfiniteGridConsistencyInteractor(iconTilesInteractor, infiniteGridSizeInteractor)
+        InfiniteGridConsistencyInteractor(iconTilesInteractor, fixedColumnsSizeInteractor)
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt
index 82cfaf5..be00152 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt
@@ -18,8 +18,8 @@
 
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.qs.panels.ui.compose.InfiniteGridLayout
+import com.android.systemui.qs.panels.ui.viewmodel.fixedColumnsSizeViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.iconTilesViewModel
-import com.android.systemui.qs.panels.ui.viewmodel.infiniteGridSizeViewModel
 
 val Kosmos.infiniteGridLayout by
-    Kosmos.Fixture { InfiniteGridLayout(iconTilesViewModel, infiniteGridSizeViewModel) }
+    Kosmos.Fixture { InfiniteGridLayout(iconTilesViewModel, fixedColumnsSizeViewModel) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridSizeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/PaginatedGridInteractorKosmos.kt
similarity index 78%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridSizeInteractorKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/PaginatedGridInteractorKosmos.kt
index 6e11977..a922e5d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridSizeInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/PaginatedGridInteractorKosmos.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.qs.panels.domain.interactor
 
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.qs.panels.data.repository.infiniteGridSizeRepository
+import com.android.systemui.qs.panels.data.repository.paginatedGridRepository
 
-val Kosmos.infiniteGridSizeInteractor by
-    Kosmos.Fixture { InfiniteGridSizeInteractor(infiniteGridSizeRepository) }
+val Kosmos.paginatedGridInteractor by
+    Kosmos.Fixture { PaginatedGridInteractor(paginatedGridRepository) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridSizeViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/FixedColumnsSizeViewModelKosmos.kt
similarity index 79%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridSizeViewModelKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/FixedColumnsSizeViewModelKosmos.kt
index f6dfb8b..feadc91 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridSizeViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/FixedColumnsSizeViewModelKosmos.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.qs.panels.ui.viewmodel
 
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.qs.panels.domain.interactor.infiniteGridSizeInteractor
+import com.android.systemui.qs.panels.domain.interactor.fixedColumnsSizeInteractor
 
-val Kosmos.infiniteGridSizeViewModel by
-    Kosmos.Fixture { InfiniteGridSizeViewModelImpl(infiniteGridSizeInteractor) }
+val Kosmos.fixedColumnsSizeViewModel by
+    Kosmos.Fixture { FixedColumnsSizeViewModelImpl(fixedColumnsSizeInteractor) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/MockTileViewModel.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/MockTileViewModel.kt
new file mode 100644
index 0000000..5386ece
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/MockTileViewModel.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.ui.viewmodel
+
+import com.android.systemui.plugins.qs.QSTile
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+fun MockTileViewModel(
+    spec: TileSpec,
+    state: StateFlow<QSTile.State> = MutableStateFlow(QSTile.State())
+): TileViewModel = mock {
+    whenever(this.spec).thenReturn(spec)
+    whenever(this.state).thenReturn(state)
+    whenever(this.currentState).thenReturn(state.value)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridSizeViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModelKosmos.kt
similarity index 62%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridSizeViewModelKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModelKosmos.kt
index f6dfb8b..85e9265 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridSizeViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModelKosmos.kt
@@ -17,7 +17,16 @@
 package com.android.systemui.qs.panels.ui.viewmodel
 
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.qs.panels.domain.interactor.infiniteGridSizeInteractor
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.qs.panels.domain.interactor.paginatedGridInteractor
 
-val Kosmos.infiniteGridSizeViewModel by
-    Kosmos.Fixture { InfiniteGridSizeViewModelImpl(infiniteGridSizeInteractor) }
+val Kosmos.paginatedGridViewModel by
+    Kosmos.Fixture {
+        PaginatedGridViewModel(
+            iconTilesViewModel,
+            fixedColumnsSizeViewModel,
+            iconLabelVisibilityViewModel,
+            paginatedGridInteractor,
+            applicationCoroutineScope,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PartitionedGridViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PartitionedGridViewModelKosmos.kt
index b07cc7d..fde174d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PartitionedGridViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PartitionedGridViewModelKosmos.kt
@@ -22,7 +22,7 @@
     Kosmos.Fixture {
         PartitionedGridViewModel(
             iconTilesViewModel,
-            infiniteGridSizeViewModel,
+            fixedColumnsSizeViewModel,
             iconLabelVisibilityViewModel,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneInteractorKosmos.kt
index 066736c..0921eb9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneInteractorKosmos.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.scene.domain.interactor
 
 import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.scene.data.repository.sceneContainerRepository
@@ -31,5 +32,6 @@
             logger = sceneLogger,
             sceneFamilyResolvers = { sceneFamilyResolvers },
             deviceUnlockedInteractor = deviceUnlockedInteractor,
+            keyguardEnabledInteractor = keyguardEnabledInteractor,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/KeyguardStateCallbackStartableKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/KeyguardStateCallbackStartableKosmos.kt
new file mode 100644
index 0000000..f9111bf
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/KeyguardStateCallbackStartableKosmos.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene.domain.startable
+
+import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
+import com.android.systemui.keyguard.domain.interactor.trustInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.user.domain.interactor.selectedUserInteractor
+
+val Kosmos.keyguardStateCallbackStartable by Fixture {
+    KeyguardStateCallbackStartable(
+        applicationScope = applicationCoroutineScope,
+        backgroundDispatcher = testDispatcher,
+        sceneInteractor = sceneInteractor,
+        selectedUserInteractor = selectedUserInteractor,
+        deviceEntryInteractor = deviceEntryInteractor,
+        simBouncerInteractor = simBouncerInteractor,
+        trustInteractor = trustInteractor,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneContainerStartableKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt
similarity index 92%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneContainerStartableKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt
index cf18c0e..8b887d3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneContainerStartableKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.scene.domain.interactor
+package com.android.systemui.scene.domain.startable
 
 import com.android.internal.logging.uiEventLogger
 import com.android.systemui.authentication.domain.interactor.authenticationInteractor
@@ -32,7 +32,9 @@
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.model.sysUiState
 import com.android.systemui.power.domain.interactor.powerInteractor
-import com.android.systemui.scene.domain.startable.SceneContainerStartable
+import com.android.systemui.scene.domain.interactor.sceneBackInteractor
+import com.android.systemui.scene.domain.interactor.sceneContainerOcclusionInteractor
+import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.session.shared.shadeSessionStorage
 import com.android.systemui.scene.shared.logger.sceneLogger
 import com.android.systemui.settings.displayTracker
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/NotificationsDataKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/NotificationsDataKosmos.kt
deleted file mode 100644
index a61f7ec..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/NotificationsDataKosmos.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *       http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-
-package com.android.systemui.statusbar.notification.data
-
-import com.android.settingslib.statusbar.notification.data.repository.FakeNotificationsSoundPolicyRepository
-import com.android.systemui.kosmos.Kosmos
-
-val Kosmos.notificationsSoundPolicyRepository by
-    Kosmos.Fixture { FakeNotificationsSoundPolicyRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsSoundPolicyInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsSoundPolicyInteractorKosmos.kt
index 0614309..e6ca458 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsSoundPolicyInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsSoundPolicyInteractorKosmos.kt
@@ -16,12 +16,9 @@
 
 package com.android.systemui.statusbar.notification.domain.interactor
 
-import com.android.settingslib.statusbar.notification.data.repository.FakeNotificationsSoundPolicyRepository
 import com.android.settingslib.statusbar.notification.domain.interactor.NotificationsSoundPolicyInteractor
 import com.android.systemui.kosmos.Kosmos
-
-var Kosmos.notificationsSoundPolicyRepository by
-    Kosmos.Fixture { FakeNotificationsSoundPolicyRepository() }
+import com.android.systemui.statusbar.policy.data.repository.zenModeRepository
 
 val Kosmos.notificationsSoundPolicyInteractor: NotificationsSoundPolicyInteractor by
-    Kosmos.Fixture { NotificationsSoundPolicyInteractor(notificationsSoundPolicyRepository) }
+    Kosmos.Fixture { NotificationsSoundPolicyInteractor(zenModeRepository) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt
new file mode 100644
index 0000000..16dc50f
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt
@@ -0,0 +1,390 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row
+
+import android.app.ActivityManager
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.Context
+import android.content.pm.LauncherApps
+import android.os.UserHandle
+import android.provider.DeviceConfig
+import androidx.core.os.bundleOf
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags
+import com.android.internal.logging.MetricsLogger
+import com.android.internal.statusbar.IStatusBarService
+import com.android.systemui.TestableDependency
+import com.android.systemui.classifier.FalsingManagerFake
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.flags.FakeFeatureFlagsClassic
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.media.dialog.MediaOutputDialogManager
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shared.system.ActivityManagerWrapper
+import com.android.systemui.shared.system.DevicePolicyManagerWrapper
+import com.android.systemui.shared.system.PackageManagerWrapper
+import com.android.systemui.statusbar.NotificationMediaManager
+import com.android.systemui.statusbar.NotificationRemoteInputManager
+import com.android.systemui.statusbar.NotificationShadeWindowController
+import com.android.systemui.statusbar.RankingBuilder
+import com.android.systemui.statusbar.SmartReplyController
+import com.android.systemui.statusbar.notification.ColorUpdateLogger
+import com.android.systemui.statusbar.notification.ConversationNotificationProcessor
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
+import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
+import com.android.systemui.statusbar.notification.collection.provider.NotificationDismissibilityProvider
+import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager
+import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManagerImpl
+import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager
+import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManagerImpl
+import com.android.systemui.statusbar.notification.icon.IconBuilder
+import com.android.systemui.statusbar.notification.icon.IconManager
+import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow.CoordinateOnClickListener
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow.ExpandableNotificationRowLogger
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow.OnExpandClickListener
+import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL
+import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag
+import com.android.systemui.statusbar.notification.row.shared.NotificationRowContentBinderRefactor
+import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainerLogger
+import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.statusbar.policy.HeadsUpManager
+import com.android.systemui.statusbar.policy.SmartActionInflaterImpl
+import com.android.systemui.statusbar.policy.SmartReplyConstants
+import com.android.systemui.statusbar.policy.SmartReplyInflaterImpl
+import com.android.systemui.statusbar.policy.SmartReplyStateInflaterImpl
+import com.android.systemui.statusbar.policy.dagger.RemoteInputViewSubcomponent
+import com.android.systemui.util.Assert.runWithCurrentThreadAsMainThread
+import com.android.systemui.util.DeviceConfigProxyFake
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.time.FakeSystemClock
+import com.android.systemui.wmshell.BubblesManager
+import com.google.common.util.concurrent.MoreExecutors
+import com.google.common.util.concurrent.SettableFuture
+import java.util.Optional
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.test.TestScope
+import org.junit.Assert.assertTrue
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito
+
+class ExpandableNotificationRowBuilder(
+    private val context: Context,
+    dependency: TestableDependency,
+    private val featureFlags: FakeFeatureFlagsClassic = FakeFeatureFlagsClassic()
+) {
+
+    private val mMockLogger: ExpandableNotificationRowLogger
+    private val mStatusBarStateController: StatusBarStateController
+    private val mKeyguardBypassController: KeyguardBypassController
+    private val mGroupMembershipManager: GroupMembershipManager
+    private val mGroupExpansionManager: GroupExpansionManager
+    private val mHeadsUpManager: HeadsUpManager
+    private val mIconManager: IconManager
+    private val mContentBinder: NotificationRowContentBinder
+    private val mBindStage: RowContentBindStage
+    private val mBindPipeline: NotifBindPipeline
+    private val mBindPipelineEntryListener: NotifCollectionListener
+    private val mPeopleNotificationIdentifier: PeopleNotificationIdentifier
+    private val mOnUserInteractionCallback: OnUserInteractionCallback
+    private val mDismissibilityProvider: NotificationDismissibilityProvider
+    private val mSmartReplyController: SmartReplyController
+    private val mSmartReplyConstants: SmartReplyConstants
+    private val mTestScope: TestScope = TestScope()
+    private val mBgCoroutineContext = mTestScope.backgroundScope.coroutineContext
+    private val mMainCoroutineContext = mTestScope.coroutineContext
+    private val mFakeSystemClock = FakeSystemClock()
+    private val mMainExecutor = FakeExecutor(mFakeSystemClock)
+
+    init {
+        featureFlags.setDefault(Flags.ENABLE_NOTIFICATIONS_SIMULATE_SLOW_MEASURE)
+        featureFlags.setDefault(Flags.BIGPICTURE_NOTIFICATION_LAZY_LOADING)
+
+        dependency.injectTestDependency(FeatureFlags::class.java, featureFlags)
+        dependency.injectMockDependency(NotificationMediaManager::class.java)
+        dependency.injectMockDependency(NotificationShadeWindowController::class.java)
+        dependency.injectMockDependency(MediaOutputDialogManager::class.java)
+
+        mMockLogger = Mockito.mock(ExpandableNotificationRowLogger::class.java)
+        mStatusBarStateController = Mockito.mock(StatusBarStateController::class.java)
+        mKeyguardBypassController = Mockito.mock(KeyguardBypassController::class.java)
+        mGroupMembershipManager = GroupMembershipManagerImpl()
+        mSmartReplyController = Mockito.mock(SmartReplyController::class.java)
+
+        val dumpManager = DumpManager()
+        mGroupExpansionManager = GroupExpansionManagerImpl(dumpManager, mGroupMembershipManager)
+        mHeadsUpManager = Mockito.mock(HeadsUpManager::class.java)
+        mIconManager =
+            IconManager(
+                Mockito.mock(CommonNotifCollection::class.java),
+                Mockito.mock(LauncherApps::class.java),
+                IconBuilder(context),
+                mTestScope,
+                mBgCoroutineContext,
+                mMainCoroutineContext,
+            )
+
+        mSmartReplyConstants =
+            SmartReplyConstants(
+                /* mainExecutor = */ mMainExecutor,
+                /* context = */ context,
+                /* deviceConfig = */ DeviceConfigProxyFake().apply {
+                    setProperty(
+                        DeviceConfig.NAMESPACE_SYSTEMUI,
+                        SystemUiDeviceConfigFlags.SSIN_SHOW_IN_HEADS_UP,
+                        "true",
+                        true
+                    )
+                    setProperty(
+                        DeviceConfig.NAMESPACE_SYSTEMUI,
+                        SystemUiDeviceConfigFlags.SSIN_ENABLED,
+                        "true",
+                        true
+                    )
+                    setProperty(
+                        DeviceConfig.NAMESPACE_SYSTEMUI,
+                        SystemUiDeviceConfigFlags.SSIN_REQUIRES_TARGETING_P,
+                        "false",
+                        true
+                    )
+                }
+            )
+        val remoteViewsFactories = getNotifRemoteViewsFactoryContainer(featureFlags)
+        val remoteInputManager = Mockito.mock(NotificationRemoteInputManager::class.java)
+        val smartReplyStateInflater =
+            SmartReplyStateInflaterImpl(
+                constants = mSmartReplyConstants,
+                activityManagerWrapper = ActivityManagerWrapper.getInstance(),
+                packageManagerWrapper = PackageManagerWrapper.getInstance(),
+                devicePolicyManagerWrapper = DevicePolicyManagerWrapper.getInstance(),
+                smartRepliesInflater =
+                    SmartReplyInflaterImpl(
+                        constants = mSmartReplyConstants,
+                        keyguardDismissUtil = mock(),
+                        remoteInputManager = remoteInputManager,
+                        smartReplyController = mSmartReplyController,
+                        context = context
+                    ),
+                smartActionsInflater =
+                    SmartActionInflaterImpl(
+                        constants = mSmartReplyConstants,
+                        activityStarter = mock(),
+                        smartReplyController = mSmartReplyController,
+                        headsUpManager = mHeadsUpManager
+                    )
+            )
+        val notifLayoutInflaterFactoryProvider =
+            object : NotifLayoutInflaterFactory.Provider {
+                override fun provide(
+                    row: ExpandableNotificationRow,
+                    layoutType: Int
+                ): NotifLayoutInflaterFactory =
+                    NotifLayoutInflaterFactory(row, layoutType, remoteViewsFactories)
+            }
+        val conversationProcessor =
+            ConversationNotificationProcessor(
+                mock(),
+                mock(),
+            )
+        mContentBinder =
+            if (NotificationRowContentBinderRefactor.isEnabled)
+                NotificationRowContentBinderImpl(
+                    mock(),
+                    remoteInputManager,
+                    conversationProcessor,
+                    mock(),
+                    mock(),
+                    mock(),
+                    smartReplyStateInflater,
+                    notifLayoutInflaterFactoryProvider,
+                    mock(),
+                    mock(),
+                )
+            else
+                NotificationContentInflater(
+                    mock(),
+                    remoteInputManager,
+                    conversationProcessor,
+                    mock(),
+                    mock(),
+                    smartReplyStateInflater,
+                    notifLayoutInflaterFactoryProvider,
+                    mock(),
+                    mock(),
+                )
+        mContentBinder.setInflateSynchronously(true)
+        mBindStage =
+            RowContentBindStage(
+                mContentBinder,
+                mock(),
+                mock(),
+            )
+
+        val collection = Mockito.mock(CommonNotifCollection::class.java)
+
+        mBindPipeline =
+            NotifBindPipeline(
+                collection,
+                Mockito.mock(NotifBindPipelineLogger::class.java),
+                NotificationEntryProcessorFactoryExecutorImpl(mMainExecutor),
+            )
+        mBindPipeline.setStage(mBindStage)
+
+        val collectionListenerCaptor = ArgumentCaptor.forClass(NotifCollectionListener::class.java)
+        Mockito.verify(collection).addCollectionListener(collectionListenerCaptor.capture())
+        mBindPipelineEntryListener = collectionListenerCaptor.value
+        mPeopleNotificationIdentifier = Mockito.mock(PeopleNotificationIdentifier::class.java)
+        mOnUserInteractionCallback = Mockito.mock(OnUserInteractionCallback::class.java)
+        mDismissibilityProvider = Mockito.mock(NotificationDismissibilityProvider::class.java)
+        val mFutureDismissalRunnable = Mockito.mock(Runnable::class.java)
+        whenever(
+                mOnUserInteractionCallback.registerFutureDismissal(
+                    ArgumentMatchers.any(),
+                    ArgumentMatchers.anyInt()
+                )
+            )
+            .thenReturn(mFutureDismissalRunnable)
+    }
+
+    private fun getNotifRemoteViewsFactoryContainer(
+        featureFlags: FeatureFlags,
+    ): NotifRemoteViewsFactoryContainer {
+        return NotifRemoteViewsFactoryContainerImpl(
+            featureFlags,
+            PrecomputedTextViewFactory(),
+            BigPictureLayoutInflaterFactory(),
+            NotificationOptimizedLinearLayoutFactory(),
+            { Mockito.mock(NotificationViewFlipperFactory::class.java) },
+        )
+    }
+
+    fun createRow(notification: Notification): ExpandableNotificationRow {
+        val channel =
+            NotificationChannel(
+                notification.channelId,
+                notification.channelId,
+                NotificationManager.IMPORTANCE_DEFAULT
+            )
+        channel.isBlockable = true
+        val entry =
+            NotificationEntryBuilder()
+                .setPkg(PKG)
+                .setOpPkg(PKG)
+                .setId(123321)
+                .setUid(UID)
+                .setInitialPid(2000)
+                .setNotification(notification)
+                .setUser(USER_HANDLE)
+                .setPostTime(System.currentTimeMillis())
+                .setChannel(channel)
+                .build()
+
+        // it is for mitigating Rank building process.
+        if (notification.isConversationStyleNotification) {
+            val rb = RankingBuilder(entry.ranking)
+            rb.setIsConversation(true)
+            entry.ranking = rb.build()
+        }
+
+        return generateRow(entry, FLAG_CONTENT_VIEW_ALL)
+    }
+
+    private fun generateRow(
+        entry: NotificationEntry,
+        @InflationFlag extraInflationFlags: Int
+    ): ExpandableNotificationRow {
+        // NOTE: This flag is read when the ExpandableNotificationRow is inflated, so it needs to be
+        //  set, but we do not want to override an existing value that is needed by a specific test.
+
+        val rowFuture: SettableFuture<ExpandableNotificationRow> = SettableFuture.create()
+        val rowInflaterTask =
+            RowInflaterTask(mFakeSystemClock, Mockito.mock(RowInflaterTaskLogger::class.java))
+        rowInflaterTask.inflate(context, null, entry, MoreExecutors.directExecutor()) { inflatedRow
+            ->
+            rowFuture.set(inflatedRow)
+        }
+        val row = rowFuture.get(1, TimeUnit.SECONDS)
+
+        entry.row = row
+        mIconManager.createIcons(entry)
+        mBindPipelineEntryListener.onEntryInit(entry)
+        mBindPipeline.manageRow(entry, row)
+        row.initialize(
+            entry,
+            Mockito.mock(RemoteInputViewSubcomponent.Factory::class.java),
+            APP_NAME,
+            entry.key,
+            mMockLogger,
+            mKeyguardBypassController,
+            mGroupMembershipManager,
+            mGroupExpansionManager,
+            mHeadsUpManager,
+            mBindStage,
+            Mockito.mock(OnExpandClickListener::class.java),
+            Mockito.mock(CoordinateOnClickListener::class.java),
+            FalsingManagerFake(),
+            mStatusBarStateController,
+            mPeopleNotificationIdentifier,
+            mOnUserInteractionCallback,
+            Optional.of(Mockito.mock(BubblesManager::class.java)),
+            Mockito.mock(NotificationGutsManager::class.java),
+            mDismissibilityProvider,
+            Mockito.mock(MetricsLogger::class.java),
+            Mockito.mock(NotificationChildrenContainerLogger::class.java),
+            Mockito.mock(ColorUpdateLogger::class.java),
+            mSmartReplyConstants,
+            mSmartReplyController,
+            featureFlags,
+            Mockito.mock(IStatusBarService::class.java)
+        )
+        row.setAboveShelfChangedListener { aboveShelf: Boolean -> }
+        mBindStage.getStageParams(entry).requireContentViews(extraInflationFlags)
+        inflateAndWait(entry)
+        return row
+    }
+
+    private fun inflateAndWait(entry: NotificationEntry) {
+        val countDownLatch = CountDownLatch(1)
+        mBindStage.requestRebind(entry) { en: NotificationEntry? -> countDownLatch.countDown() }
+        runWithCurrentThreadAsMainThread(mMainExecutor::runAllReady)
+        assertTrue(countDownLatch.await(500, TimeUnit.MILLISECONDS))
+    }
+
+    companion object {
+        private const val APP_NAME = "appName"
+        private const val PKG = "com.android.systemui"
+        private const val UID = 1000
+        private val USER_HANDLE = UserHandle.of(ActivityManager.getCurrentUser())
+
+        private const val IS_CONVERSATION_FLAG = "test.isConversation"
+
+        private val Notification.isConversationStyleNotification
+            get() = extras.getBoolean(IS_CONVERSATION_FLAG, false)
+
+        fun markAsConversation(builder: Notification.Builder) {
+            builder.addExtras(bundleOf(IS_CONVERSATION_FLAG to true))
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/data/repository/NotificationRowRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/data/repository/NotificationRowRepositoryKosmos.kt
new file mode 100644
index 0000000..84ef4b5
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/data/repository/NotificationRowRepositoryKosmos.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.statusbar.notification.row.shared.RichOngoingContentModel
+import kotlinx.coroutines.flow.MutableStateFlow
+
+val Kosmos.fakeNotificationRowRepository by Fixture { FakeNotificationRowRepository() }
+
+class FakeNotificationRowRepository : NotificationRowRepository {
+    override val richOngoingContentModel = MutableStateFlow<RichOngoingContentModel?>(null)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridSizeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractorKosmos.kt
similarity index 68%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridSizeInteractorKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractorKosmos.kt
index 6e11977..3a7d7ba 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridSizeInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractorKosmos.kt
@@ -14,10 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.systemui.qs.panels.domain.interactor
+package com.android.systemui.statusbar.notification.row.domain.interactor
 
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.qs.panels.data.repository.infiniteGridSizeRepository
+import com.android.systemui.statusbar.notification.row.data.repository.NotificationRowRepository
 
-val Kosmos.infiniteGridSizeInteractor by
-    Kosmos.Fixture { InfiniteGridSizeInteractor(infiniteGridSizeRepository) }
+fun Kosmos.getNotificationRowInteractor(repository: NotificationRowRepository) =
+    NotificationRowInteractor(repository = repository)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelKosmos.kt
new file mode 100644
index 0000000..00f45b2
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelKosmos.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row.ui.viewmodel
+
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.notification.row.data.repository.NotificationRowRepository
+import com.android.systemui.statusbar.notification.row.domain.interactor.getNotificationRowInteractor
+
+fun Kosmos.getTimerViewModel(repository: NotificationRowRepository) =
+    TimerViewModel(
+        dumpManager = dumpManager,
+        rowInteractor = getNotificationRowInteractor(repository),
+    )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryKosmos.kt
index 1851c89..6574946 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryKosmos.kt
@@ -36,3 +36,14 @@
 
 val Kosmos.mockSystemUIDialogFactory: SystemUIDialog.Factory by
     Kosmos.Fixture { mock<SystemUIDialog.Factory>() }
+
+val Kosmos.systemUIDialogDotFactory by
+    Kosmos.Fixture {
+        SystemUIDialog.Factory(
+            applicationContext,
+            systemUIDialogManager,
+            sysUiState,
+            broadcastDispatcher,
+            dialogTransitionAnimator,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/FakeStatusBarPolicyDataLayerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/FakeStatusBarPolicyDataLayerModule.kt
index 16dab40..5aece1b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/FakeStatusBarPolicyDataLayerModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/FakeStatusBarPolicyDataLayerModule.kt
@@ -16,14 +16,7 @@
 package com.android.systemui.statusbar.policy.data
 
 import com.android.systemui.statusbar.policy.data.repository.FakeDeviceProvisioningRepositoryModule
-import com.android.systemui.statusbar.policy.data.repository.FakeZenModeRepositoryModule
 import dagger.Module
 
-@Module(
-    includes =
-        [
-            FakeDeviceProvisioningRepositoryModule::class,
-            FakeZenModeRepositoryModule::class,
-        ]
-)
+@Module(includes = [FakeDeviceProvisioningRepositoryModule::class])
 object FakeStatusBarPolicyDataLayerModule
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeZenModeRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeZenModeRepository.kt
deleted file mode 100644
index c4d7867..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeZenModeRepository.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.policy.data.repository
-
-import android.app.NotificationManager.Policy
-import android.provider.Settings
-import com.android.systemui.dagger.SysUISingleton
-import dagger.Binds
-import dagger.Module
-import javax.inject.Inject
-import kotlinx.coroutines.flow.MutableStateFlow
-
-@SysUISingleton
-class FakeZenModeRepository @Inject constructor() : ZenModeRepository {
-    override val zenMode: MutableStateFlow<Int> = MutableStateFlow(Settings.Global.ZEN_MODE_OFF)
-    override val consolidatedNotificationPolicy: MutableStateFlow<Policy?> =
-        MutableStateFlow(
-            Policy(
-                /* priorityCategories = */ 0,
-                /* priorityCallSenders = */ 0,
-                /* priorityMessageSenders = */ 0,
-            )
-        )
-
-    fun setSuppressedVisualEffects(suppressedVisualEffects: Int) {
-        consolidatedNotificationPolicy.value =
-            Policy(
-                /* priorityCategories = */ 0,
-                /* priorityCallSenders = */ 0,
-                /* priorityMessageSenders = */ 0,
-                /* suppressedVisualEffects = */ suppressedVisualEffects,
-            )
-    }
-}
-
-@Module
-interface FakeZenModeRepositoryModule {
-    @Binds fun bindFake(fake: FakeZenModeRepository): ZenModeRepository
-}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepositoryKosmos.kt
index 1ec7511..c7fda54 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepositoryKosmos.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.policy.data.repository
 
+import com.android.settingslib.statusbar.notification.data.repository.FakeZenModeRepository
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/TestMediaDevicesFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/TestMediaDevicesFactory.kt
index 141f242..83adc79 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/TestMediaDevicesFactory.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/TestMediaDevicesFactory.kt
@@ -18,9 +18,11 @@
 
 import android.annotation.SuppressLint
 import android.bluetooth.BluetoothDevice
+import android.bluetooth.BluetoothProfile
 import android.graphics.drawable.Drawable
 import android.graphics.drawable.TestStubDrawable
 import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.LeAudioProfile
 import com.android.settingslib.media.BluetoothMediaDevice
 import com.android.settingslib.media.MediaDevice
 import com.android.settingslib.media.PhoneMediaDevice
@@ -59,11 +61,17 @@
                 whenever(name).thenReturn(deviceName)
                 whenever(address).thenReturn(deviceAddress)
             }
+        val leAudioProfile =
+            mock<LeAudioProfile> {
+                whenever(profileId).thenReturn(BluetoothProfile.LE_AUDIO)
+                whenever(isEnabled(bluetoothDevice)).thenReturn(true)
+            }
         val cachedBluetoothDevice: CachedBluetoothDevice = mock {
             whenever(isHearingAidDevice).thenReturn(true)
             whenever(address).thenReturn(deviceAddress)
             whenever(device).thenReturn(bluetoothDevice)
             whenever(name).thenReturn(deviceName)
+            whenever(profiles).thenReturn(listOf(leAudioProfile))
         }
         return mock<BluetoothMediaDevice> {
             whenever(name).thenReturn(deviceName)
diff --git a/proto/src/system_messages.proto b/proto/src/system_messages.proto
index e8db80a..994bdb5 100644
--- a/proto/src/system_messages.proto
+++ b/proto/src/system_messages.proto
@@ -314,6 +314,10 @@
     // Package: com.android.systemui
     NOTE_ADAPTIVE_NOTIFICATIONS = 76;
 
+    // Warn the user that the device's Headless System User Mode status doesn't match the build's.
+    // Package: android
+    NOTE_WRONG_HSUM_STATUS = 77;
+
     // ADD_NEW_IDS_ABOVE_THIS_LINE
     // Legacy IDs with arbitrary values appear below
     // Legacy IDs existed as stable non-conflicting constants prior to the O release
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index 48bc803..ad216b5 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -276,3 +276,77 @@
         ":services.core.ravenwood.keep_all",
     ],
 }
+
+java_library {
+    name: "services.fakes.ravenwood-jarjar",
+    installable: false,
+    srcs: [":services.fakes-sources"],
+    libs: [
+        "ravenwood-framework",
+        "services.core.ravenwood",
+    ],
+    jarjar_rules: ":ravenwood-services-jarjar-rules",
+    visibility: ["//visibility:private"],
+}
+
+java_library {
+    name: "mockito-ravenwood-prebuilt",
+    installable: false,
+    static_libs: [
+        "mockito-robolectric-prebuilt",
+    ],
+}
+
+java_library {
+    name: "inline-mockito-ravenwood-prebuilt",
+    installable: false,
+    static_libs: [
+        "inline-mockito-robolectric-prebuilt",
+    ],
+}
+
+android_ravenwood_libgroup {
+    name: "ravenwood-runtime",
+    libs: [
+        "100-framework-minus-apex.ravenwood",
+        "200-kxml2-android",
+
+        "ravenwood-runtime-common-ravenwood",
+
+        "android.test.mock.ravenwood",
+        "ravenwood-helper-runtime",
+        "hoststubgen-helper-runtime.ravenwood",
+        "services.core.ravenwood-jarjar",
+        "services.fakes.ravenwood-jarjar",
+
+        // Provide runtime versions of utils linked in below
+        "junit",
+        "truth",
+        "flag-junit",
+        "ravenwood-framework",
+        "ravenwood-junit-impl",
+        "ravenwood-junit-impl-flag",
+        "mockito-ravenwood-prebuilt",
+        "inline-mockito-ravenwood-prebuilt",
+
+        // It's a stub, so it should be towards the end.
+        "z00-all-updatable-modules-system-stubs",
+    ],
+    jni_libs: [
+        "libandroid_runtime",
+        "libravenwood_runtime",
+    ],
+}
+
+android_ravenwood_libgroup {
+    name: "ravenwood-utils",
+    libs: [
+        "junit",
+        "truth",
+        "flag-junit",
+        "ravenwood-framework",
+        "ravenwood-junit",
+        "mockito-ravenwood-prebuilt",
+        "inline-mockito-ravenwood-prebuilt",
+    ],
+}
diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig
index 9353150..b4efae3 100644
--- a/services/accessibility/accessibility.aconfig
+++ b/services/accessibility/accessibility.aconfig
@@ -11,6 +11,16 @@
 }
 
 flag {
+    name: "always_allow_observing_touch_events"
+    namespace: "accessibility"
+    description: "Always allows InputFilter observing SOURCE_TOUCHSCREEN events, even if touch exploration is enabled."
+    bug: "344604959"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
     name: "resettable_dynamic_properties"
     namespace: "accessibility"
     description: "Maintains initial copies of a11yServiceInfo dynamic properties so they can reset on disconnect."
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
index 5fb60e7..f9196f3 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
@@ -1087,21 +1087,15 @@
         }
     }
 
-    private boolean anyServiceWantsToObserveMotionEvent(MotionEvent event) {
-        // Disable SOURCE_TOUCHSCREEN generic event interception if any service is performing
-        // touch exploration.
-        if (event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)
-                && (mEnabledFeatures & FLAG_FEATURE_TOUCH_EXPLORATION) != 0) {
-            return false;
-        }
-        final int eventSourceWithoutClass = event.getSource() & ~InputDevice.SOURCE_CLASS_MASK;
-        return (mCombinedGenericMotionEventSources
-                        & mCombinedMotionEventObservedSources
-                        & eventSourceWithoutClass)
-                != 0;
-    }
-
     private boolean anyServiceWantsGenericMotionEvent(MotionEvent event) {
+        if (Flags.alwaysAllowObservingTouchEvents()) {
+            final boolean isTouchEvent = event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN);
+            if (isTouchEvent && !canShareGenericTouchEvent()) {
+                return false;
+            }
+            final int eventSourceWithoutClass = event.getSource() & ~InputDevice.SOURCE_CLASS_MASK;
+            return (mCombinedGenericMotionEventSources & eventSourceWithoutClass) != 0;
+        }
         // Disable SOURCE_TOUCHSCREEN generic event interception if any service is performing
         // touch exploration.
         if (event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)
@@ -1112,6 +1106,36 @@
         return (mCombinedGenericMotionEventSources & eventSourceWithoutClass) != 0;
     }
 
+    private boolean anyServiceWantsToObserveMotionEvent(MotionEvent event) {
+        if (Flags.alwaysAllowObservingTouchEvents()) {
+            final int eventSourceWithoutClass = event.getSource() & ~InputDevice.SOURCE_CLASS_MASK;
+            return (mCombinedMotionEventObservedSources & eventSourceWithoutClass) != 0;
+        }
+        // Disable SOURCE_TOUCHSCREEN generic event interception if any service is performing
+        // touch exploration.
+        if (event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)
+                && (mEnabledFeatures & FLAG_FEATURE_TOUCH_EXPLORATION) != 0) {
+            return false;
+        }
+        final int eventSourceWithoutClass = event.getSource() & ~InputDevice.SOURCE_CLASS_MASK;
+        return (mCombinedGenericMotionEventSources
+                & mCombinedMotionEventObservedSources
+                & eventSourceWithoutClass)
+                != 0;
+    }
+
+    private boolean canShareGenericTouchEvent() {
+        if ((mCombinedMotionEventObservedSources & InputDevice.SOURCE_TOUCHSCREEN) != 0) {
+            // Share touch events if a MotionEvent-observing service wants them.
+            return true;
+        }
+        if ((mEnabledFeatures & FLAG_FEATURE_TOUCH_EXPLORATION) == 0) {
+            // Share touch events if touch exploration is not enabled.
+            return true;
+        }
+        return false;
+    }
+
     public void setCombinedGenericMotionEventSources(int sources) {
         mCombinedGenericMotionEventSources = sources;
     }
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 4f9db8b..acd80ee 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -47,6 +47,11 @@
 import static com.android.internal.accessibility.common.ShortcutConstants.CHOOSER_PACKAGE_NAME;
 import static com.android.internal.accessibility.common.ShortcutConstants.USER_SHORTCUT_TYPES;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType;
+import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE;
+import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS;
+import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE;
+import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.TRIPLETAP;
+import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.TWOFINGER_DOUBLETAP;
 import static com.android.internal.accessibility.util.AccessibilityStatsLogUtils.logAccessibilityShortcutActivated;
 import static com.android.internal.util.FunctionalUtils.ignoreRemoteException;
 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
@@ -923,25 +928,11 @@
                                         newValue, restoredFromSdk);
                             }
                         }
-                        case Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS -> {
-                            synchronized (mLock) {
-                                restoreAccessibilityButtonTargetsLocked(
-                                        previousValue, newValue);
-                            }
-                        }
-                        case Settings.Secure.ACCESSIBILITY_QS_TARGETS -> {
-                            if (!android.view.accessibility.Flags.a11yQsShortcut()) {
-                                return;
-                            }
-                            restoreAccessibilityQsTargets(newValue);
-                        }
-                        case Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE -> {
-                            if (!android.view.accessibility.Flags
-                                    .restoreA11yShortcutTargetService()) {
-                                return;
-                            }
-                            restoreAccessibilityShortcutTargetService(previousValue, newValue);
-                        }
+                        case Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS,
+                                Settings.Secure.ACCESSIBILITY_QS_TARGETS,
+                                Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE ->
+                                restoreShortcutTargets(newValue,
+                                        ShortcutUtils.convertToType(which));
                     }
                 }
             }
@@ -1040,7 +1031,7 @@
         }
         persistColonDelimitedSetToSettingLocked(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS,
                 userState.mUserId, targetsFromSetting, str -> str);
-        readAccessibilityButtonTargetsLocked(userState);
+        readAccessibilityShortcutTargetsLocked(userState, SOFTWARE);
         onUserStateChangedLocked(userState);
     }
 
@@ -1720,12 +1711,12 @@
         }
         // Turn on/off a11y qs shortcut for the a11y features based on the change in QS Panel
         if (!a11yFeaturesToEnable.isEmpty()) {
-            enableShortcutForTargets(/* enable= */ true, UserShortcutType.QUICK_SETTINGS,
+            enableShortcutForTargets(/* enable= */ true, QUICK_SETTINGS,
                     a11yFeaturesToEnable, userId);
         }
 
         if (!a11yFeaturesToRemove.isEmpty()) {
-            enableShortcutForTargets(/* enable= */ false, UserShortcutType.QUICK_SETTINGS,
+            enableShortcutForTargets(/* enable= */ false, QUICK_SETTINGS,
                     a11yFeaturesToRemove, userId);
         }
     }
@@ -2057,100 +2048,78 @@
     }
 
     /**
-     * User could enable accessibility services and configure accessibility button during the SUW.
-     * Merges current value of accessibility button settings into the restored one to make sure
-     * user's preferences of accessibility button updated in SUW are not lost.
-     *
-     * Called only during settings restore; currently supports only the owner user
-     * TODO: http://b/22388012
-     */
-    void restoreAccessibilityButtonTargetsLocked(String oldSetting, String newSetting) {
-        final Set<String> targetsFromSetting = new ArraySet<>();
-        readColonDelimitedStringToSet(oldSetting, str -> str, targetsFromSetting,
-                /* doMerge = */false);
-        readColonDelimitedStringToSet(newSetting, str -> str, targetsFromSetting,
-                /* doMerge = */true);
-
-        final AccessibilityUserState userState = getUserStateLocked(UserHandle.USER_SYSTEM);
-        userState.mAccessibilityButtonTargets.clear();
-        userState.mAccessibilityButtonTargets.addAll(targetsFromSetting);
-        persistColonDelimitedSetToSettingLocked(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS,
-                UserHandle.USER_SYSTEM, userState.mAccessibilityButtonTargets, str -> str);
-
-        scheduleNotifyClientsOfServicesStateChangeLocked(userState);
-        onUserStateChangedLocked(userState);
-    }
-
-    /**
      * User could configure accessibility shortcut during the SUW before restoring user data.
      * Merges the current value and the new value to make sure we don't lost the setting the user's
-     * preferences of accessibility qs shortcut updated in SUW are not lost.
-     *
-     * Called only during settings restore; currently supports only the owner user
+     * preferences of accessibility shortcut updated in SUW are not lost.
+     * Called only during settings restore; currently supports only the owner user.
+     * <P>
+     * Throws an exception if used with {@code TRIPLETAP} or {@code TWOFINGER_DOUBLETAP}.
+     * </P>
      * TODO: http://b/22388012
      */
-    private void restoreAccessibilityQsTargets(String newValue) {
+    private void restoreShortcutTargets(String newValue,
+            @UserShortcutType int shortcutType) {
+        assertNoTapShortcut(shortcutType);
+        if (shortcutType == QUICK_SETTINGS && !android.view.accessibility.Flags.a11yQsShortcut()) {
+            return;
+        }
+        if (shortcutType == HARDWARE
+                && !android.view.accessibility.Flags.restoreA11yShortcutTargetService()) {
+            return;
+        }
+
         synchronized (mLock) {
             final AccessibilityUserState userState = getUserStateLocked(UserHandle.USER_SYSTEM);
-            final Set<String> mergedTargets = userState.getA11yQsTargets();
-            readColonDelimitedStringToSet(newValue, str -> str, mergedTargets,
-                    /* doMerge = */ true);
+            final Set<String> mergedTargets = (shortcutType == HARDWARE)
+                    ? new ArraySet<>(ShortcutUtils.getShortcutTargetsFromSettings(
+                            mContext, shortcutType, userState.mUserId))
+                    : userState.getShortcutTargetsLocked(shortcutType);
 
-            userState.updateA11yQsTargetLocked(mergedTargets);
-            persistColonDelimitedSetToSettingLocked(Settings.Secure.ACCESSIBILITY_QS_TARGETS,
+            // If dealing with the hardware shortcut,
+            // remove the default service if it wasn't present before restore,
+            // but only if the raw shortcut setting is not null (edge case during SUW).
+            // Otherwise, merge the old and new targets normally.
+            if (Flags.clearDefaultFromA11yShortcutTargetServiceRestore()
+                    && shortcutType == HARDWARE) {
+                final String defaultService =
+                        mContext.getString(R.string.config_defaultAccessibilityService);
+                final ComponentName defaultServiceComponent = TextUtils.isEmpty(defaultService)
+                        ? null : ComponentName.unflattenFromString(defaultService);
+                boolean shouldClearDefaultService = defaultServiceComponent != null
+                        && !stringSetContainsComponentName(mergedTargets, defaultServiceComponent);
+                readColonDelimitedStringToSet(newValue, str -> str,
+                        mergedTargets, /*doMerge=*/true);
+
+                if (shouldClearDefaultService && stringSetContainsComponentName(
+                        mergedTargets, defaultServiceComponent)) {
+                    Slog.i(LOG_TAG, "Removing default service " + defaultService
+                            + " from restore of "
+                            + Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE);
+                    mergedTargets.removeIf(str ->
+                            defaultServiceComponent.equals(ComponentName.unflattenFromString(str)));
+                }
+                if (mergedTargets.isEmpty()) {
+                    return;
+                }
+            } else {
+                readColonDelimitedStringToSet(newValue, str -> str, mergedTargets,
+                        /* doMerge = */ true);
+            }
+
+            userState.updateShortcutTargetsLocked(mergedTargets, shortcutType);
+            persistColonDelimitedSetToSettingLocked(ShortcutUtils.convertToKey(shortcutType),
                     UserHandle.USER_SYSTEM, mergedTargets, str -> str);
             scheduleNotifyClientsOfServicesStateChangeLocked(userState);
             onUserStateChangedLocked(userState);
         }
     }
 
-    /**
-     * Merges the old and restored value of
-     * {@link Settings.Secure#ACCESSIBILITY_SHORTCUT_TARGET_SERVICE}.
-     *
-     * <p>Also clears out {@link R.string#config_defaultAccessibilityService} from
-     * the merged set if it was not present before restoring.
-     */
-    private void restoreAccessibilityShortcutTargetService(
-            String oldValue, String restoredValue) {
-        final Set<String> targetsFromSetting = new ArraySet<>();
-        readColonDelimitedStringToSet(oldValue, str -> str,
-                targetsFromSetting, /*doMerge=*/false);
-        final String defaultService =
-                mContext.getString(R.string.config_defaultAccessibilityService);
-        final ComponentName defaultServiceComponent = TextUtils.isEmpty(defaultService)
-                ? null : ComponentName.unflattenFromString(defaultService);
-        boolean shouldClearDefaultService = defaultServiceComponent != null
-                && !stringSetContainsComponentName(targetsFromSetting, defaultServiceComponent);
-        readColonDelimitedStringToSet(restoredValue, str -> str,
-                targetsFromSetting, /*doMerge=*/true);
-        if (Flags.clearDefaultFromA11yShortcutTargetServiceRestore()) {
-            if (shouldClearDefaultService && stringSetContainsComponentName(
-                    targetsFromSetting, defaultServiceComponent)) {
-                Slog.i(LOG_TAG, "Removing default service " + defaultService
-                        + " from restore of "
-                        + Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE);
-                targetsFromSetting.removeIf(str ->
-                        defaultServiceComponent.equals(ComponentName.unflattenFromString(str)));
-            }
-            if (targetsFromSetting.isEmpty()) {
-                return;
-            }
-        }
-        synchronized (mLock) {
-            final AccessibilityUserState userState = getUserStateLocked(UserHandle.USER_SYSTEM);
-            final Set<String> shortcutTargets =
-                    userState.getShortcutTargetsLocked(UserShortcutType.HARDWARE);
-            shortcutTargets.clear();
-            shortcutTargets.addAll(targetsFromSetting);
-            persistColonDelimitedSetToSettingLocked(
-                    Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE,
-                    UserHandle.USER_SYSTEM, targetsFromSetting, str -> str);
-            scheduleNotifyClientsOfServicesStateChangeLocked(userState);
-            onUserStateChangedLocked(userState);
-        }
+    private String getRawShortcutSetting(int userId, @UserShortcutType int shortcutType) {
+        return Settings.Secure.getStringForUser(mContext.getContentResolver(),
+                ShortcutUtils.convertToKey(shortcutType), userId);
     }
 
+
     /**
      * Returns {@code true} if the set contains the provided non-null {@link ComponentName}.
      *
@@ -2263,7 +2232,7 @@
     private void showAccessibilityTargetsSelection(int displayId,
             @UserShortcutType int shortcutType) {
         final Intent intent = new Intent(AccessibilityManager.ACTION_CHOOSE_ACCESSIBILITY_BUTTON);
-        final String chooserClassName = (shortcutType == UserShortcutType.HARDWARE)
+        final String chooserClassName = (shortcutType == HARDWARE)
                 ? AccessibilityShortcutChooserActivity.class.getName()
                 : AccessibilityButtonChooserActivity.class.getName();
         intent.setClassName(CHOOSER_PACKAGE_NAME, chooserClassName);
@@ -3236,9 +3205,9 @@
         somethingChanged |= readAudioDescriptionEnabledSettingLocked(userState);
         somethingChanged |= readMagnificationEnabledSettingsLocked(userState);
         somethingChanged |= readAutoclickEnabledSettingLocked(userState);
-        somethingChanged |= readAccessibilityShortcutKeySettingLocked(userState);
-        somethingChanged |= readAccessibilityQsTargetsLocked(userState);
-        somethingChanged |= readAccessibilityButtonTargetsLocked(userState);
+        somethingChanged |= readAccessibilityShortcutTargetsLocked(userState, HARDWARE);
+        somethingChanged |= readAccessibilityShortcutTargetsLocked(userState, QUICK_SETTINGS);
+        somethingChanged |= readAccessibilityShortcutTargetsLocked(userState, SOFTWARE);
         somethingChanged |= readAccessibilityButtonTargetComponentLocked(userState);
         somethingChanged |= readUserRecommendedUiTimeoutSettingsLocked(userState);
         somethingChanged |= readMagnificationModeForDefaultDisplayLocked(userState);
@@ -3386,60 +3355,34 @@
         userState.setSendMotionEventsEnabled(sendMotionEvents);
     }
 
-    private boolean readAccessibilityShortcutKeySettingLocked(AccessibilityUserState userState) {
-        final String settingValue = Settings.Secure.getStringForUser(mContext.getContentResolver(),
-                Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, userState.mUserId);
+    /**
+     * Throws an exception for {@code TRIPLETAP} or {@code TWOFINGER_DOUBLETAP} types.
+     */
+    private boolean readAccessibilityShortcutTargetsLocked(AccessibilityUserState userState,
+            @UserShortcutType int shortcutType) {
+        assertNoTapShortcut(shortcutType);
+        final String settingValue = getRawShortcutSetting(userState.mUserId, shortcutType);
         final Set<String> targetsFromSetting = new ArraySet<>();
-        readColonDelimitedStringToSet(settingValue, str -> str, targetsFromSetting, false);
-        // Fall back to device's default a11y service, only when setting is never updated.
-        if (settingValue == null) {
+        // If dealing with an empty hardware shortcut, fall back to the default value.
+        if (shortcutType == HARDWARE && settingValue == null) {
             final String defaultService = mContext.getString(
                     R.string.config_defaultAccessibilityService);
             if (!TextUtils.isEmpty(defaultService)) {
-                targetsFromSetting.add(defaultService);
+                // Convert to component name to reformat the target if it has a relative path.
+                ComponentName name = ComponentName.unflattenFromString(defaultService);
+                if (name != null) {
+                    targetsFromSetting.add(name.flattenToString());
+                }
             }
+        } else {
+            readColonDelimitedStringToSet(settingValue, str -> str, targetsFromSetting, false);
         }
 
-        final Set<String> currentTargets =
-                userState.getShortcutTargetsLocked(UserShortcutType.HARDWARE);
-        if (targetsFromSetting.equals(currentTargets)) {
-            return false;
+        if (userState.updateShortcutTargetsLocked(targetsFromSetting, shortcutType)) {
+            scheduleNotifyClientsOfServicesStateChangeLocked(userState);
+            return true;
         }
-        currentTargets.clear();
-        currentTargets.addAll(targetsFromSetting);
-        scheduleNotifyClientsOfServicesStateChangeLocked(userState);
-        return true;
-    }
-
-    private boolean readAccessibilityQsTargetsLocked(AccessibilityUserState userState) {
-        final Set<String> targetsFromSetting = new ArraySet<>();
-        readColonDelimitedSettingToSet(Settings.Secure.ACCESSIBILITY_QS_TARGETS,
-                userState.mUserId, str -> str, targetsFromSetting);
-
-        final Set<String> currentTargets =
-                userState.getShortcutTargetsLocked(UserShortcutType.QUICK_SETTINGS);
-        if (targetsFromSetting.equals(currentTargets)) {
-            return false;
-        }
-        userState.updateA11yQsTargetLocked(targetsFromSetting);
-        scheduleNotifyClientsOfServicesStateChangeLocked(userState);
-        return true;
-    }
-
-    private boolean readAccessibilityButtonTargetsLocked(AccessibilityUserState userState) {
-        final Set<String> targetsFromSetting = new ArraySet<>();
-        readColonDelimitedSettingToSet(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS,
-                userState.mUserId, str -> str, targetsFromSetting);
-
-        final Set<String> currentTargets =
-                userState.getShortcutTargetsLocked(UserShortcutType.SOFTWARE);
-        if (targetsFromSetting.equals(currentTargets)) {
-            return false;
-        }
-        currentTargets.clear();
-        currentTargets.addAll(targetsFromSetting);
-        scheduleNotifyClientsOfServicesStateChangeLocked(userState);
-        return true;
+        return false;
     }
 
     private boolean readAccessibilityButtonTargetComponentLocked(AccessibilityUserState userState) {
@@ -3487,14 +3430,10 @@
      */
     private void updateAccessibilityShortcutKeyTargetsLocked(AccessibilityUserState userState) {
         final Set<String> currentTargets =
-                userState.getShortcutTargetsLocked(UserShortcutType.HARDWARE);
-        final int lastSize = currentTargets.size();
-        if (lastSize == 0) {
-            return;
-        }
+                userState.getShortcutTargetsLocked(HARDWARE);
         currentTargets.removeIf(
                 name -> !userState.isShortcutTargetInstalledLocked(name));
-        if (lastSize == currentTargets.size()) {
+        if (!userState.updateShortcutTargetsLocked(currentTargets, HARDWARE)) {
             return;
         }
 
@@ -3680,13 +3619,9 @@
 
         final Set<String> currentTargets =
                 userState.getShortcutTargetsLocked(UserShortcutType.SOFTWARE);
-        final int lastSize = currentTargets.size();
-        if (lastSize == 0) {
-            return;
-        }
         currentTargets.removeIf(
                 name -> !userState.isShortcutTargetInstalledLocked(name));
-        if (lastSize == currentTargets.size()) {
+        if (!userState.updateShortcutTargetsLocked(currentTargets, SOFTWARE)) {
             return;
         }
 
@@ -3719,8 +3654,7 @@
             return;
         }
         final Set<String> buttonTargets =
-                userState.getShortcutTargetsLocked(UserShortcutType.SOFTWARE);
-        int lastSize = buttonTargets.size();
+                userState.getShortcutTargetsLocked(SOFTWARE);
         buttonTargets.removeIf(name -> {
             if (packageName != null && name != null && !name.contains(packageName)) {
                 return false;
@@ -3752,13 +3686,11 @@
             }
             return false;
         });
-        boolean changed = (lastSize != buttonTargets.size());
-        lastSize = buttonTargets.size();
 
         final Set<String> shortcutKeyTargets =
-                userState.getShortcutTargetsLocked(UserShortcutType.HARDWARE);
+                userState.getShortcutTargetsLocked(HARDWARE);
         final Set<String> qsShortcutTargets =
-                userState.getShortcutTargetsLocked(UserShortcutType.QUICK_SETTINGS);
+                userState.getShortcutTargetsLocked(QUICK_SETTINGS);
         userState.mEnabledServices.forEach(componentName -> {
             if (packageName != null && componentName != null
                     && !packageName.equals(componentName.getPackageName())) {
@@ -3790,8 +3722,7 @@
                     + " should be assign to the button or shortcut.");
             buttonTargets.add(serviceName);
         });
-        changed |= (lastSize != buttonTargets.size());
-        if (!changed) {
+        if (!userState.updateShortcutTargetsLocked(buttonTargets, SOFTWARE)) {
             return;
         }
 
@@ -3815,10 +3746,10 @@
         }
 
         final Set<String> targets =
-                userState.getShortcutTargetsLocked(UserShortcutType.QUICK_SETTINGS);
+                userState.getShortcutTargetsLocked(QUICK_SETTINGS);
 
         // Removes the targets that are no longer installed on the device.
-        boolean somethingChanged = targets.removeIf(
+        targets.removeIf(
                 name -> !userState.isShortcutTargetInstalledLocked(name));
         // Add the target if the a11y service is enabled and the tile exist in QS panel
         Set<ComponentName> enabledServices = userState.getEnabledServicesLocked();
@@ -3829,14 +3760,13 @@
             ComponentName tileService =
                     a11yFeatureToTileService.getOrDefault(enabledService, null);
             if (tileService != null && currentA11yTilesInQsPanel.contains(tileService)) {
-                somethingChanged |= targets.add(enabledService.flattenToString());
+                targets.add(enabledService.flattenToString());
             }
         }
 
-        if (!somethingChanged) {
+        if (!userState.updateShortcutTargetsLocked(targets, QUICK_SETTINGS)) {
             return;
         }
-        userState.updateA11yQsTargetLocked(targets);
 
         // Update setting key with new value.
         persistColonDelimitedSetToSettingLocked(
@@ -3862,14 +3792,14 @@
 
         final List<Pair<Integer, String>> shortcutTypeAndShortcutSetting = new ArrayList<>(3);
         shortcutTypeAndShortcutSetting.add(
-                new Pair<>(UserShortcutType.HARDWARE,
+                new Pair<>(HARDWARE,
                         Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE));
         shortcutTypeAndShortcutSetting.add(
                 new Pair<>(UserShortcutType.SOFTWARE,
                         Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS));
         if (android.view.accessibility.Flags.a11yQsShortcut()) {
             shortcutTypeAndShortcutSetting.add(
-                    new Pair<>(UserShortcutType.QUICK_SETTINGS,
+                    new Pair<>(QUICK_SETTINGS,
                             Settings.Secure.ACCESSIBILITY_QS_TARGETS));
         }
 
@@ -3883,7 +3813,7 @@
                         shortcutSettingName,
                         userState.mUserId, currentTargets, str -> str);
 
-                if (shortcutType != UserShortcutType.QUICK_SETTINGS) {
+                if (shortcutType != QUICK_SETTINGS) {
                     continue;
                 }
 
@@ -3968,7 +3898,7 @@
 
         mMainHandler.sendMessage(obtainMessage(
                 AccessibilityManagerService::performAccessibilityShortcutInternal, this,
-                Display.DEFAULT_DISPLAY, UserShortcutType.HARDWARE, targetName));
+                Display.DEFAULT_DISPLAY, HARDWARE, targetName));
     }
 
     /**
@@ -4115,7 +4045,7 @@
             final boolean requestA11yButton = (installedServiceInfo.flags
                     & FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0;
             // Turns on / off the accessibility service
-            if ((targetSdk <= Build.VERSION_CODES.Q && shortcutType == UserShortcutType.HARDWARE)
+            if ((targetSdk <= Build.VERSION_CODES.Q && shortcutType == HARDWARE)
                     || (targetSdk > Build.VERSION_CODES.Q && !requestA11yButton)) {
                 if (serviceConnection == null) {
                     logAccessibilityShortcutActivated(mContext, assignedTarget, shortcutType,
@@ -4129,7 +4059,7 @@
                 }
                 return true;
             }
-            if (shortcutType == UserShortcutType.HARDWARE && targetSdk > Build.VERSION_CODES.Q
+            if (shortcutType == HARDWARE && targetSdk > Build.VERSION_CODES.Q
                     && requestA11yButton) {
                 if (!userState.getEnabledServicesLocked().contains(assignedTarget)) {
                     enableAccessibilityServiceLocked(assignedTarget, mCurrentUserId);
@@ -4222,7 +4152,7 @@
             validNewTargets = newTargets;
 
             // filter out targets that doesn't have qs shortcut
-            if (shortcutType == UserShortcutType.QUICK_SETTINGS) {
+            if (shortcutType == QUICK_SETTINGS) {
                 validNewTargets = newTargets.stream().filter(target -> {
                     ComponentName targetComponent = ComponentName.unflattenFromString(target);
                     return featureToTileMap.containsKey(targetComponent);
@@ -4240,10 +4170,10 @@
                     /* defaultEmptyString= */ ""
             );
 
-            if (shortcutType == UserShortcutType.QUICK_SETTINGS) {
+            if (shortcutType == QUICK_SETTINGS) {
                 int numOfFeatureChanged = Math.abs(currentTargets.size() - validNewTargets.size());
                 logMetricForQsShortcutConfiguration(enable, numOfFeatureChanged);
-                userState.updateA11yQsTargetLocked(validNewTargets);
+                userState.updateShortcutTargetsLocked(validNewTargets, QUICK_SETTINGS);
                 scheduleNotifyClientsOfServicesStateChangeLocked(userState);
                 onUserStateChangedLocked(userState);
             }
@@ -4257,7 +4187,7 @@
         }
 
         // Add or Remove tile in QS Panel
-        if (shortcutType == UserShortcutType.QUICK_SETTINGS) {
+        if (shortcutType == QUICK_SETTINGS) {
             mMainHandler.sendMessage(obtainMessage(
                     AccessibilityManagerService::updateA11yTileServicesInQuickSettingsPanel,
                     this, validNewTargets, currentTargets, userId));
@@ -4266,7 +4196,7 @@
         if (!enable) {
             return;
         }
-        if (shortcutType == UserShortcutType.HARDWARE) {
+        if (shortcutType == HARDWARE) {
             skipVolumeShortcutDialogTimeoutRestriction(userId);
             if (com.android.server.accessibility.Flags.enableHardwareShortcutDisablesWarning()) {
                 persistIntToSetting(
@@ -4461,6 +4391,7 @@
                     shortcutTargets.add(serviceName);
                 }
             }
+            userState.updateShortcutTargetsLocked(Set.copyOf(shortcutTargets), shortcutType);
             return shortcutTargets;
         }
     }
@@ -5672,7 +5603,7 @@
                         || mShowImeWithHardKeyboardUri.equals(uri)) {
                     userState.reconcileSoftKeyboardModeWithSettingsLocked();
                 } else if (mAccessibilityShortcutServiceIdUri.equals(uri)) {
-                    if (readAccessibilityShortcutKeySettingLocked(userState)) {
+                    if (readAccessibilityShortcutTargetsLocked(userState, HARDWARE)) {
                         onUserStateChangedLocked(userState);
                     }
                 } else if (mAccessibilityButtonComponentIdUri.equals(uri)) {
@@ -5680,7 +5611,7 @@
                         onUserStateChangedLocked(userState);
                     }
                 } else if (mAccessibilityButtonTargetsUri.equals(uri)) {
-                    if (readAccessibilityButtonTargetsLocked(userState)) {
+                    if (readAccessibilityShortcutTargetsLocked(userState, SOFTWARE)) {
                         onUserStateChangedLocked(userState);
                     }
                 } else if (mUserNonInteractiveUiTimeoutUri.equals(uri)
@@ -6505,4 +6436,10 @@
         String metricId = enable ? METRIC_ID_QS_SHORTCUT_ADD : METRIC_ID_QS_SHORTCUT_REMOVE;
         Counter.logIncrementWithUid(metricId, Binder.getCallingUid(), numOfFeatures);
     }
+
+    private void assertNoTapShortcut(@UserShortcutType int shortcutType) {
+        if ((shortcutType & (TRIPLETAP | TWOFINGER_DOUBLETAP)) != 0) {
+            throw new IllegalArgumentException("Tap shortcuts are not supported.");
+        }
+    }
 }
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
index a37a184..de1c86a 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
@@ -777,12 +777,15 @@
      * @return The array set of the strings
      */
     public ArraySet<String> getShortcutTargetsLocked(@UserShortcutType int shortcutType) {
+        return new ArraySet<>(getShortcutTargetsInternalLocked(shortcutType));
+    }
+    private ArraySet<String> getShortcutTargetsInternalLocked(@UserShortcutType int shortcutType) {
         if (shortcutType == UserShortcutType.HARDWARE) {
             return mAccessibilityShortcutKeyTargets;
         } else if (shortcutType == UserShortcutType.SOFTWARE) {
             return mAccessibilityButtonTargets;
         } else if (shortcutType == UserShortcutType.QUICK_SETTINGS) {
-            return getA11yQsTargets();
+            return mAccessibilityQsTargets;
         } else if ((shortcutType == UserShortcutType.TRIPLETAP
                 && isMagnificationSingleFingerTripleTapEnabledLocked()) || (
                 shortcutType == UserShortcutType.TWOFINGER_DOUBLETAP
@@ -795,6 +798,32 @@
     }
 
     /**
+     * Updates the corresponding shortcut targets with the provided set.
+     * Tap shortcuts don't operate using sets of targets,
+     * so trying to update {@code TRIPLETAP} or {@code TWOFINGER_DOUBLETAP}
+     * will instead throw an {@code IllegalArgumentException}
+     * @param newTargets set of targets to replace the existing set.
+     * @param shortcutType type to be replaced.
+     * @return {@code true} if the set was changed, or {@code false} if the elements are the same.
+     * @throws IllegalArgumentException if {@code TRIPLETAP} or {@code TWOFINGER_DOUBLETAP} is used.
+     */
+    boolean updateShortcutTargetsLocked(
+            Set<String> newTargets, @UserShortcutType int shortcutType) {
+        final int mask = UserShortcutType.TRIPLETAP | UserShortcutType.TWOFINGER_DOUBLETAP;
+        if ((shortcutType & mask) != 0) {
+            throw new IllegalArgumentException("Tap shortcuts cannot be updated with target sets.");
+        }
+
+        final Set<String> currentTargets = getShortcutTargetsInternalLocked(shortcutType);
+        if (newTargets.equals(currentTargets)) {
+            return false;
+        }
+        currentTargets.clear();
+        currentTargets.addAll(newTargets);
+        return true;
+    }
+
+    /**
      * Whether or not the given shortcut target is installed in device.
      *
      * @param name The shortcut target name
@@ -844,8 +873,9 @@
             );
         }
 
-        Set<String> targets = getShortcutTargetsLocked(shortcutType);
-        boolean result = targets.removeIf(name -> {
+        // getting internal set lets us directly modify targets, as it's not a copy.
+        Set<String> targets = getShortcutTargetsInternalLocked(shortcutType);
+        return targets.removeIf(name -> {
             ComponentName componentName;
             if (name == null
                     || (componentName = ComponentName.unflattenFromString(name)) == null) {
@@ -853,11 +883,6 @@
             }
             return componentName.equals(target);
         });
-        if (shortcutType == UserShortcutType.QUICK_SETTINGS) {
-            updateA11yQsTargetLocked(targets);
-        }
-
-        return result;
     }
 
     /**
@@ -1114,11 +1139,6 @@
         );
     }
 
-    public void updateA11yQsTargetLocked(Set<String> targets) {
-        mAccessibilityQsTargets.clear();
-        mAccessibilityQsTargets.addAll(targets);
-    }
-
     /**
      * Returns a copy of the targets which has qs shortcut turned on
      */
diff --git a/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java b/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java
index b64aa8a..ea6351b 100644
--- a/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java
+++ b/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java
@@ -364,7 +364,7 @@
     }
 
     @RequiresPermission(android.Manifest.permission.START_TASKS_FROM_RECENTS)
-    private int invokeContextualSearchIntent(Intent launchIntent) {
+    private int invokeContextualSearchIntent(Intent launchIntent, final int userId) {
         // Contextual search starts with a frozen screen - so we launch without
         // any system animations or starting window.
         final ActivityOptions opts = ActivityOptions.makeCustomTaskAnimation(mContext,
@@ -372,7 +372,7 @@
         opts.setDisableStartingWindow(true);
         return mAtmInternal.startActivityWithScreenshot(launchIntent,
                 mContext.getPackageName(), Binder.getCallingUid(), Binder.getCallingPid(), null,
-                opts.toBundle(), Binder.getCallingUserHandle().getIdentifier());
+                opts.toBundle(), userId);
     }
 
     private void enforcePermission(@NonNull final String func) {
@@ -446,6 +446,8 @@
             synchronized (this) {
                 if (DEBUG_USER) Log.d(TAG, "startContextualSearch");
                 enforcePermission("startContextualSearch");
+                final int callingUserId = Binder.getCallingUserHandle().getIdentifier();
+
                 mAssistDataRequester.cancel();
                 // Creates a new CallbackToken at mToken and an expiration handler.
                 issueToken();
@@ -455,7 +457,7 @@
                 Binder.withCleanCallingIdentity(() -> {
                     Intent launchIntent = getContextualSearchIntent(entrypoint, mToken);
                     if (launchIntent != null) {
-                        int result = invokeContextualSearchIntent(launchIntent);
+                        int result = invokeContextualSearchIntent(launchIntent, callingUserId);
                         if (DEBUG_USER) Log.d(TAG, "Launch result: " + result);
                     }
                 });
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index b35959f..19279a8 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -104,6 +104,7 @@
 import android.os.ServiceSpecificException;
 import android.os.SystemClock;
 import android.os.SystemProperties;
+import android.os.Trace;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.os.storage.DiskInfo;
@@ -1180,6 +1181,7 @@
 
     private void onUserUnlocking(int userId) {
         Slog.d(TAG, "onUserUnlocking " + userId);
+        Trace.instant(Trace.TRACE_TAG_SYSTEM_SERVER, "SMS.onUserUnlocking: " + userId);
 
         if (userId != UserHandle.USER_SYSTEM) {
             // Check if this user shares media with another user
@@ -1466,6 +1468,8 @@
         @Override
         public void onVolumeCreated(String volId, int type, String diskId, String partGuid,
                 int userId) {
+            Trace.instant(Trace.TRACE_TAG_SYSTEM_SERVER,
+                    "SMS.onVolumeCreated: " + volId + ", " + userId);
             synchronized (mLock) {
                 final DiskInfo disk = mDisks.get(diskId);
                 final VolumeInfo vol = new VolumeInfo(volId, type, disk, partGuid);
@@ -2352,6 +2356,7 @@
 
     private void mount(VolumeInfo vol) {
         try {
+            Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "SMS.mount: " + vol.id);
             // TODO(b/135341433): Remove cautious logging when FUSE is stable
             Slog.i(TAG, "Mounting volume " + vol);
             extendWatchdogTimeout("#mount might be slow");
@@ -2363,6 +2368,8 @@
                     vol.internalPath = internalPath;
                     ParcelFileDescriptor pfd = new ParcelFileDescriptor(fd);
                     try {
+                        Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER,
+                                "SMS.startFuseFileSystem: " + vol.id);
                         mStorageSessionController.onVolumeMount(pfd, vol);
                         return true;
                     } catch (ExternalStorageServiceException e) {
@@ -2375,6 +2382,7 @@
                                 TimeUnit.SECONDS.toMillis(nextResetSeconds));
                         return false;
                     } finally {
+                        Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
                         try {
                             pfd.close();
                         } catch (Exception e) {
@@ -2386,6 +2394,8 @@
             Slog.i(TAG, "Mounted volume " + vol);
         } catch (Exception e) {
             Slog.wtf(TAG, e);
+        } finally {
+            Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
         }
     }
 
diff --git a/services/core/java/com/android/server/SystemConfig.java b/services/core/java/com/android/server/SystemConfig.java
index 44aea15..e2ab0d9 100644
--- a/services/core/java/com/android/server/SystemConfig.java
+++ b/services/core/java/com/android/server/SystemConfig.java
@@ -1603,7 +1603,7 @@
                         } else {
                             mPackageToSharedUidAllowList.put(pkgName, sharedUid);
                         }
-                    }
+                    } break;
                     case "asl-file": {
                         String packageName = parser.getAttributeValue(null, "package");
                         String path = parser.getAttributeValue(null, "path");
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 195e94b..df35ff3 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -730,7 +730,7 @@
     /** Whether some specified important processes are allowed to use FIFO priority. */
     boolean mAllowSpecifiedFifoScheduling = true;
 
-    @GuardedBy("this")
+    @GuardedBy("mStrictModeCallbacks")
     private final SparseArray<IUnsafeIntentStrictModeCallback>
             mStrictModeCallbacks = new SparseArray<>();
 
@@ -9535,18 +9535,20 @@
      * @param callback The binder used to communicate the violations.
      */
     @Override
-    public synchronized void registerStrictModeCallback(IBinder callback) {
-        int callingPid = Binder.getCallingPid();
-        mStrictModeCallbacks.put(callingPid,
-                IUnsafeIntentStrictModeCallback.Stub.asInterface(callback));
-        try {
-            callback.linkToDeath(() -> {
-                synchronized (ActivityManagerService.this) {
-                    mStrictModeCallbacks.remove(callingPid);
-                }
-            }, 0);
-        } catch (RemoteException e) {
-            mStrictModeCallbacks.remove(callingPid);
+    public void registerStrictModeCallback(IBinder callback) {
+        final int callingPid = Binder.getCallingPid();
+        synchronized (mStrictModeCallbacks) {
+            mStrictModeCallbacks.put(callingPid,
+                    IUnsafeIntentStrictModeCallback.Stub.asInterface(callback));
+            try {
+                callback.linkToDeath(() -> {
+                    synchronized (mStrictModeCallbacks) {
+                        mStrictModeCallbacks.remove(callingPid);
+                    }
+                }, 0);
+            } catch (RemoteException e) {
+                mStrictModeCallbacks.remove(callingPid);
+            }
         }
     }
 
@@ -10013,8 +10015,9 @@
                 if (crashInfo != null && crashInfo.stackTrace != null) {
                     sb.append(crashInfo.stackTrace);
                 }
-                boolean shouldAddLogs = logcatLines > 0 || kernelLogLines > 0;
-                if (!runSynchronously && shouldAddLogs) {
+                boolean shouldAddLogs = (logcatLines > 0 || kernelLogLines > 0)
+                        && (Flags.collectLogcatOnRunSynchronously() || !runSynchronously);
+                if (shouldAddLogs) {
                     sb.append("\n");
                     if (logcatLines > 0) {
                         fetchLogcatBuffers(sb, logcatLines, LOGCAT_TIMEOUT_SEC,
@@ -19907,7 +19910,7 @@
         public void triggerUnsafeIntentStrictMode(int callingPid, int type, Intent intent) {
             final IUnsafeIntentStrictModeCallback callback;
             final Intent i = intent.cloneFilter();
-            synchronized (ActivityManagerService.this) {
+            synchronized (mStrictModeCallbacks) {
                 callback = mStrictModeCallbacks.get(callingPid);
             }
             if (callback != null) {
@@ -19915,7 +19918,7 @@
                     try {
                         callback.onUnsafeIntent(type, i);
                     } catch (RemoteException e) {
-                        synchronized (ActivityManagerService.this) {
+                        synchronized (mStrictModeCallbacks) {
                             mStrictModeCallbacks.remove(callingPid);
                         }
                     }
diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig
index bb52857..a30590f 100644
--- a/services/core/java/com/android/server/am/flags.aconfig
+++ b/services/core/java/com/android/server/am/flags.aconfig
@@ -152,3 +152,11 @@
         purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    name: "collect_logcat_on_run_synchronously"
+    namespace: "dropbox"
+    description: "Allow logcat collection on synchronous dropbox collection"
+    bug: "324222683"
+    is_fixed_read_only: true
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index bce1830..27fda15 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -414,7 +414,8 @@
         }
         if (!mScoManagedByAudio) {
             boolean isBtScoRequested = isBluetoothScoRequested();
-            if (isBtScoRequested && (!wasBtScoRequested || !isBluetoothScoActive())) {
+            if (isBtScoRequested && (!wasBtScoRequested || !isBluetoothScoActive()
+                    || !mBtHelper.isBluetoothScoRequestedInternally())) {
                 if (!mBtHelper.startBluetoothSco(scoAudioMode, eventSource)) {
                     Log.w(TAG, "setCommunicationRouteForClient: failure to start BT SCO for uid: "
                             + uid);
@@ -1148,13 +1149,14 @@
     }
 
     /*package*/ void setBluetoothScoOn(boolean on, String eventSource) {
-        if (AudioService.DEBUG_COMM_RTE) {
-            Log.v(TAG, "setBluetoothScoOn: " + on + " " + eventSource);
-        }
         synchronized (mBluetoothAudioStateLock) {
+            boolean isBtScoRequested = isBluetoothScoRequested();
+            Log.i(TAG, "setBluetoothScoOn: " + on + ", mBluetoothScoOn: "
+                    + mBluetoothScoOn + ", isBtScoRequested: " + isBtScoRequested
+                    + ", from: " + eventSource);
             mBluetoothScoOn = on;
             updateAudioHalBluetoothState();
-            postUpdateCommunicationRouteClient(isBluetoothScoRequested(), eventSource);
+            postUpdateCommunicationRouteClient(isBtScoRequested, eventSource);
         }
     }
 
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index 991f94b..8008717 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -378,7 +378,6 @@
     /*package*/ synchronized void onReceiveBtEvent(Intent intent) {
         final String action = intent.getAction();
 
-        Log.i(TAG, "onReceiveBtEvent action: " + action + " mScoAudioState: " + mScoAudioState);
         if (action.equals(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED)) {
             BluetoothDevice btDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE,
                     android.bluetooth.BluetoothDevice.class);
@@ -405,6 +404,7 @@
     private void onScoAudioStateChanged(int state) {
         boolean broadcast = false;
         int scoAudioState = AudioManager.SCO_AUDIO_STATE_ERROR;
+        Log.i(TAG, "onScoAudioStateChanged state: " + state + " mScoAudioState: " + mScoAudioState);
         if (mDeviceBroker.isScoManagedByAudio()) {
             switch (state) {
                 case BluetoothHeadset.STATE_AUDIO_CONNECTED:
@@ -488,6 +488,11 @@
                 == BluetoothHeadset.STATE_AUDIO_CONNECTED;
     }
 
+    /*package*/ synchronized boolean isBluetoothScoRequestedInternally() {
+        return mScoAudioState == SCO_STATE_ACTIVE_INTERNAL
+              || mScoAudioState == SCO_STATE_ACTIVATE_REQ;
+    }
+
     // @GuardedBy("mDeviceBroker.mSetModeLock")
     @GuardedBy("AudioDeviceBroker.this.mDeviceStateLock")
     /*package*/ synchronized boolean startBluetoothSco(int scoAudioMode,
diff --git a/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java b/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java
index 0fef55d..a188e79 100644
--- a/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java
+++ b/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java
@@ -84,6 +84,14 @@
     }
 
     @Override
+    public void cancelBlockScreenOn() {
+        if (mDisplayOffloader == null) {
+            return;
+        }
+        mDisplayOffloader.cancelBlockScreenOn();
+    }
+
+    @Override
     public float[] getAutoBrightnessLevels(int mode) {
         if (mode < 0 || mode > AUTO_BRIGHTNESS_MODE_MAX) {
             throw new IllegalArgumentException("Unknown auto-brightness mode: " + mode);
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index d14f2a0..c298bbf 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -2110,6 +2110,17 @@
                 Trace.TRACE_TAG_POWER, SCREEN_ON_BLOCKED_BY_DISPLAYOFFLOAD_TRACE_NAME, 0);
     }
 
+    private void cancelUnblockScreenOnByDisplayOffload() {
+        if (mDisplayOffloadSession == null) {
+            return;
+        }
+        if (mPendingScreenOnUnblockerByDisplayOffload != null) {
+            // Already unblocked.
+            return;
+        }
+        mDisplayOffloadSession.cancelBlockScreenOn();
+    }
+
     private boolean setScreenState(int state, @Display.StateReason int reason) {
         return setScreenState(state, reason, false /*reportOnly*/);
     }
@@ -2126,6 +2137,9 @@
             blockScreenOnByDisplayOffload(mDisplayOffloadSession);
         } else if (!isOn && mScreenTurningOnWasBlockedByDisplayOffload) {
             // No longer turning screen on, so unblock previous screen on blocking immediately.
+            if (mFlags.isOffloadSessionCancelBlockScreenOnEnabled()) {
+                cancelUnblockScreenOnByDisplayOffload();
+            }
             unblockScreenOnByDisplayOffload();
             mScreenTurningOnWasBlockedByDisplayOffload = false;
         }
diff --git a/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java b/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java
index b43b35b..ddb091d 100644
--- a/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java
+++ b/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java
@@ -15,6 +15,8 @@
  */
 package com.android.server.display.brightness.strategy;
 
+import static android.hardware.display.DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE;
+
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DEFAULT;
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DOZE;
 
@@ -133,14 +135,20 @@
         // We are still in the process of updating the power state, so there's no need to trigger
         // an update again
         switchMode(targetDisplayState, /* sendUpdate= */ false);
-        final boolean autoBrightnessEnabledInDoze =
-                allowAutoBrightnessWhileDozingConfig && Display.isDozeState(targetDisplayState);
+
+        // If the policy is POLICY_DOZE and the display state is STATE_ON, auto-brightness should
+        // only be enabled if the config allows it
+        final boolean autoBrightnessEnabledInDoze = allowAutoBrightnessWhileDozingConfig
+                && policy == POLICY_DOZE && targetDisplayState != Display.STATE_OFF;
+
         mIsAutoBrightnessEnabled = shouldUseAutoBrightness()
-                && (targetDisplayState == Display.STATE_ON || autoBrightnessEnabledInDoze)
+                && ((targetDisplayState == Display.STATE_ON && policy != POLICY_DOZE)
+                || autoBrightnessEnabledInDoze)
                 && brightnessReason != BrightnessReason.REASON_OVERRIDE
                 && mAutomaticBrightnessController != null;
         mAutoBrightnessDisabledDueToDisplayOff = shouldUseAutoBrightness()
-                && !(targetDisplayState == Display.STATE_ON || autoBrightnessEnabledInDoze);
+                && !((targetDisplayState == Display.STATE_ON && policy != POLICY_DOZE)
+                || autoBrightnessEnabledInDoze);
         final int autoBrightnessState = mIsAutoBrightnessEnabled
                 && brightnessReason != BrightnessReason.REASON_FOLLOWER
                 ? AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED
diff --git a/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy2.java b/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy2.java
index 4d9c18a..c87872c 100644
--- a/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy2.java
+++ b/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy2.java
@@ -15,6 +15,8 @@
  */
 package com.android.server.display.brightness.strategy;
 
+import static android.hardware.display.DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE;
+
 import android.annotation.Nullable;
 import android.content.Context;
 import android.hardware.display.BrightnessConfiguration;
@@ -107,14 +109,19 @@
     public void setAutoBrightnessState(int targetDisplayState,
             boolean allowAutoBrightnessWhileDozingConfig, int brightnessReason, int policy,
             float lastUserSetScreenBrightness, boolean userSetBrightnessChanged) {
-        final boolean autoBrightnessEnabledInDoze =
-                allowAutoBrightnessWhileDozingConfig && Display.isDozeState(targetDisplayState);
+        // If the policy is POLICY_DOZE and the display state is STATE_ON, auto-brightness should
+        // only be enabled if the config allows it
+        final boolean autoBrightnessEnabledInDoze = allowAutoBrightnessWhileDozingConfig
+                && policy == POLICY_DOZE && targetDisplayState != Display.STATE_OFF;
+
         mIsAutoBrightnessEnabled = shouldUseAutoBrightness()
-                && (targetDisplayState == Display.STATE_ON || autoBrightnessEnabledInDoze)
+                && ((targetDisplayState == Display.STATE_ON && policy != POLICY_DOZE)
+                || autoBrightnessEnabledInDoze)
                 && brightnessReason != BrightnessReason.REASON_OVERRIDE
                 && mAutomaticBrightnessController != null;
         mAutoBrightnessDisabledDueToDisplayOff = shouldUseAutoBrightness()
-                && !(targetDisplayState == Display.STATE_ON || autoBrightnessEnabledInDoze);
+                && !((targetDisplayState == Display.STATE_ON  && policy != POLICY_DOZE)
+                || autoBrightnessEnabledInDoze);
         final int autoBrightnessState = mIsAutoBrightnessEnabled
                 && brightnessReason != BrightnessReason.REASON_FOLLOWER
                 ? AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED
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 f56d803..41d18cd 100644
--- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
+++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
@@ -179,6 +179,11 @@
             Flags::offloadDozeOverrideHoldsWakelock
     );
 
+    private final FlagState mOffloadSessionCancelBlockScreenOn =
+            new FlagState(
+                    Flags.FLAG_OFFLOAD_SESSION_CANCEL_BLOCK_SCREEN_ON,
+                    Flags::offloadSessionCancelBlockScreenOn);
+
     /**
      * @return {@code true} if 'port' is allowed in display layout configuration file.
      */
@@ -352,6 +357,10 @@
         return mOffloadDozeOverrideHoldsWakelock.isEnabled();
     }
 
+    public boolean isOffloadSessionCancelBlockScreenOnEnabled() {
+        return mOffloadSessionCancelBlockScreenOn.isEnabled();
+    }
+
     /**
      * @return Whether to ignore preferredRefreshRate app request conversion to display mode or not
      */
@@ -399,6 +408,7 @@
         pw.println(" " + mIgnoreAppPreferredRefreshRate);
         pw.println(" " + mSynthetic60hzModes);
         pw.println(" " + mOffloadDozeOverrideHoldsWakelock);
+        pw.println(" " + mOffloadSessionCancelBlockScreenOn);
     }
 
     private static class FlagState {
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 95d0ca3..1ea5c0b 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
@@ -299,3 +299,11 @@
       purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    name: "offload_session_cancel_block_screen_on"
+    namespace: "wear_frameworks"
+    description: "Flag for DisplayPowerController to start notifying DisplayOffloadSession about cancelling screen on blocker."
+    bug: "331725519"
+    is_fixed_read_only: true
+}
diff --git a/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java b/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java
index 7d48527..b77f47d 100644
--- a/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java
+++ b/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java
@@ -69,7 +69,6 @@
     @NonNull
     private final ImeTargetVisibilityPolicy mImeTargetVisibilityPolicy;
 
-
     DefaultImeVisibilityApplier(InputMethodManagerService service) {
         mService = service;
         mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class);
@@ -80,8 +79,9 @@
     @Override
     public void performShowIme(IBinder showInputToken, @NonNull ImeTracker.Token statsToken,
             @InputMethod.ShowFlags int showFlags, ResultReceiver resultReceiver,
-            @SoftInputShowHideReason int reason) {
-        final IInputMethodInvoker curMethod = mService.getCurMethodLocked();
+            @SoftInputShowHideReason int reason, @UserIdInt int userId) {
+        final var bindingController = mService.getInputMethodBindingController(userId);
+        final IInputMethodInvoker curMethod = bindingController.getCurMethod();
         if (curMethod != null) {
             if (DEBUG) {
                 Slog.v(TAG, "Calling " + curMethod + ".showSoftInput(" + showInputToken
@@ -99,7 +99,7 @@
                                     mService.mImeBindingState.mFocusedWindowSoftInputMode));
                 }
                 mService.onShowHideSoftInputRequested(true /* show */, showInputToken, reason,
-                        statsToken);
+                        statsToken, userId);
             }
         }
     }
@@ -107,8 +107,10 @@
     @GuardedBy("ImfLock.class")
     @Override
     public void performHideIme(IBinder hideInputToken, @NonNull ImeTracker.Token statsToken,
-            ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
-        final IInputMethodInvoker curMethod = mService.getCurMethodLocked();
+            ResultReceiver resultReceiver, @SoftInputShowHideReason int reason,
+            @UserIdInt int userId) {
+        final var bindingController = mService.getInputMethodBindingController(userId);
+        final IInputMethodInvoker curMethod = bindingController.getCurMethod();
         if (curMethod != null) {
             // The IME will report its visible state again after the following message finally
             // delivered to the IME process as an IPC.  Hence the inconsistency between
@@ -130,7 +132,7 @@
                                     mService.mImeBindingState.mFocusedWindowSoftInputMode));
                 }
                 mService.onShowHideSoftInputRequested(false /* show */, hideInputToken, reason,
-                        statsToken);
+                        statsToken, userId);
             }
         }
     }
@@ -180,7 +182,7 @@
                     setImeVisibilityOnFocusedWindowClient(false);
                 } else {
                     mService.hideCurrentInputLocked(windowToken, statsToken,
-                        0 /* flags */, null /* resultReceiver */, reason);
+                            0 /* flags */, null /* resultReceiver */, reason);
                 }
                 break;
             case STATE_HIDE_IME_NOT_ALWAYS:
@@ -199,14 +201,14 @@
                 } else {
                     mService.showCurrentInputLocked(windowToken, statsToken,
                             InputMethodManager.SHOW_IMPLICIT, MotionEvent.TOOL_TYPE_UNKNOWN,
-                        null /* resultReceiver */, reason);
+                            null /* resultReceiver */, reason);
                 }
                 break;
             case STATE_SHOW_IME_SNAPSHOT:
-                showImeScreenshot(windowToken, displayIdToShowIme);
+                showImeScreenshot(windowToken, displayIdToShowIme, userId);
                 break;
             case STATE_REMOVE_IME_SNAPSHOT:
-                removeImeScreenshot(displayIdToShowIme);
+                removeImeScreenshot(displayIdToShowIme, userId);
                 break;
             default:
                 throw new IllegalArgumentException("Invalid IME visibility state: " + state);
@@ -215,10 +217,11 @@
 
     @GuardedBy("ImfLock.class")
     @Override
-    public boolean showImeScreenshot(@NonNull IBinder imeTarget, int displayId) {
+    public boolean showImeScreenshot(@NonNull IBinder imeTarget, int displayId,
+            @UserIdInt int userId) {
         if (mImeTargetVisibilityPolicy.showImeScreenshot(imeTarget, displayId)) {
             mService.onShowHideSoftInputRequested(false /* show */, imeTarget,
-                    SHOW_IME_SCREENSHOT_FROM_IMMS, null /* statsToken */);
+                    SHOW_IME_SCREENSHOT_FROM_IMMS, null /* statsToken */, userId);
             return true;
         }
         return false;
@@ -226,11 +229,11 @@
 
     @GuardedBy("ImfLock.class")
     @Override
-    public boolean removeImeScreenshot(int displayId) {
+    public boolean removeImeScreenshot(int displayId, @UserIdInt int userId) {
         if (mImeTargetVisibilityPolicy.removeImeScreenshot(displayId)) {
             mService.onShowHideSoftInputRequested(false /* show */,
                     mService.mImeBindingState.mFocusedWindow,
-                    REMOVE_IME_SCREENSHOT_FROM_IMMS, null /* statsToken */);
+                    REMOVE_IME_SCREENSHOT_FROM_IMMS, null /* statsToken */, userId);
             return true;
         }
         return false;
diff --git a/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java b/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java
index 62adb25..a6b07de 100644
--- a/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java
+++ b/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java
@@ -19,7 +19,6 @@
 import android.annotation.AnyThread;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.annotation.UserIdInt;
 import android.view.inputmethod.InputMethodInfo;
 import android.view.inputmethod.InputMethodSubtype;
 
@@ -34,24 +33,13 @@
     @GuardedBy("ImfLock.class")
     private final ArrayList<InputMethodSubtypeHandle> mSubtypeHandles = new ArrayList<>();
 
-    @UserIdInt
-    private final int mUserId;
-
-    @AnyThread
-    @UserIdInt
-    int getUserId() {
-        return mUserId;
-    }
-
-    HardwareKeyboardShortcutController(@NonNull InputMethodMap methodMap, @UserIdInt int userId) {
-        mUserId = userId;
-        reset(methodMap);
+    HardwareKeyboardShortcutController(@NonNull InputMethodSettings settings) {
+        update(settings);
     }
 
     @GuardedBy("ImfLock.class")
-    void reset(@NonNull InputMethodMap methodMap) {
+    void update(@NonNull InputMethodSettings settings) {
         mSubtypeHandles.clear();
-        final InputMethodSettings settings = InputMethodSettings.create(methodMap, mUserId);
         final List<InputMethodInfo> inputMethods = settings.getEnabledInputMethodList();
         for (int i = 0; i < inputMethods.size(); ++i) {
             final InputMethodInfo imi = inputMethods.get(i);
diff --git a/services/core/java/com/android/server/inputmethod/ImeVisibilityApplier.java b/services/core/java/com/android/server/inputmethod/ImeVisibilityApplier.java
index a5f9b7a..c1069f2 100644
--- a/services/core/java/com/android/server/inputmethod/ImeVisibilityApplier.java
+++ b/services/core/java/com/android/server/inputmethod/ImeVisibilityApplier.java
@@ -33,38 +33,45 @@
     /**
      * Performs showing IME on top of the given window.
      *
-     * @param showInputToken A token that represents the requester to show IME.
-     * @param statsToken     The token tracking the current IME request.
-     * @param resultReceiver If non-null, this will be called back to the caller when
-     *                       it has processed request to tell what it has done.
-     * @param reason         The reason for requesting to show IME.
+     * @param showInputToken a token that represents the requester to show IME
+     * @param statsToken     the token tracking the current IME request
+     * @param resultReceiver if non-null, this will be called back to the caller when
+     *                       it has processed request to tell what it has done
+     * @param reason         yhe reason for requesting to show IME
+     * @param userId         the target user when performing show IME
      */
     default void performShowIme(IBinder showInputToken, @NonNull ImeTracker.Token statsToken,
             @InputMethod.ShowFlags int showFlags, ResultReceiver resultReceiver,
-            @SoftInputShowHideReason int reason) {}
+            @SoftInputShowHideReason int reason, @UserIdInt int userId) {
+    }
 
     /**
      * Performs hiding IME to the given window
      *
-     * @param hideInputToken A token that represents the requester to hide IME.
-     * @param statsToken     The token tracking the current IME request.
-     * @param resultReceiver If non-null, this will be called back to the caller when
-     *                       it has processed request to tell what it has done.
-     * @param reason         The reason for requesting to hide IME.
+     * @param hideInputToken a token that represents the requester to hide IME
+     * @param statsToken     the token tracking the current IME request
+     * @param resultReceiver if non-null, this will be called back to the caller when
+     *                       it has processed request to tell what it has done
+     * @param reason         the reason for requesting to hide IME
+     * @param userId         the target user when performing hide IME
      */
     default void performHideIme(IBinder hideInputToken, @NonNull ImeTracker.Token statsToken,
-            ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {}
+            ResultReceiver resultReceiver, @SoftInputShowHideReason int reason,
+            @UserIdInt int userId) {
+    }
 
     /**
      * Applies the IME visibility from {@link android.inputmethodservice.InputMethodService} with
      * according to the given visibility state.
      *
-     * @param windowToken The token of a window for applying the IME visibility
-     * @param statsToken  The token tracking the current IME request.
-     * @param state       The new IME visibility state for the applier to handle
+     * @param windowToken the token of a window for applying the IME visibility
+     * @param statsToken  the token tracking the current IME request
+     * @param state       the new IME visibility state for the applier to handle
+     * @param userId      the target user when applying the IME visibility state
      */
     default void applyImeVisibility(IBinder windowToken, @NonNull ImeTracker.Token statsToken,
-            @ImeVisibilityStateComputer.VisibilityState int state, @UserIdInt int userId) {}
+            @ImeVisibilityStateComputer.VisibilityState int state, @UserIdInt int userId) {
+    }
 
     /**
      * Updates the IME Z-ordering relative to the given window.
@@ -72,7 +79,7 @@
      * This used to adjust the IME relative layer of the window during
      * {@link InputMethodManagerService} is in switching IME clients.
      *
-     * @param windowToken The token of a window to update the Z-ordering relative to the IME.
+     * @param windowToken the token of a window to update the Z-ordering relative to the IME
      */
     default void updateImeLayeringByTarget(IBinder windowToken) {
         // TODO: add a method in WindowManagerInternal to call DC#updateImeInputAndControlTarget
@@ -82,21 +89,24 @@
     /**
      * Shows the IME screenshot and attach it to the given IME target window.
      *
-     * @param windowToken The token of a window to show the IME screenshot.
-     * @param displayId The unique id to identify the display
-     * @return {@code true} if success, {@code false} otherwise.
+     * @param windowToken the token of a window to show the IME screenshot
+     * @param displayId   the unique id to identify the display
+     * @param userId      the target user when when showing the IME screenshot
+     * @return {@code true} if success, {@code false} otherwise
      */
-    default boolean showImeScreenshot(@NonNull IBinder windowToken, int displayId) {
+    default boolean showImeScreenshot(@NonNull IBinder windowToken, int displayId,
+            @UserIdInt int userId) {
         return false;
     }
 
     /**
      * Removes the IME screenshot on the given display.
      *
-     * @param displayId The target display of showing IME screenshot.
-     * @return {@code true} if success, {@code false} otherwise.
+     * @param displayId the target display of showing IME screenshot
+     * @param userId    the target user of showing IME screenshot
+     * @return {@code true} if success, {@code false} otherwise
      */
-    default boolean removeImeScreenshot(int displayId) {
+    default boolean removeImeScreenshot(int displayId, @UserIdInt int userId) {
         return false;
     }
 }
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 39262c5..440343d 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -381,10 +381,6 @@
     @NonNull
     @MultiUserUnawareField
     private InputMethodSubtypeSwitchingController mSwitchingController;
-    // TODO: Instantiate mHardwareKeyboardShortcutController for each user.
-    @NonNull
-    @MultiUserUnawareField
-    private HardwareKeyboardShortcutController mHardwareKeyboardShortcutController;
 
     @Nullable
     private StatusBarManagerInternal mStatusBarManagerInternal;
@@ -1300,11 +1296,8 @@
 
             final InputMethodSettings settings = InputMethodSettingsRepository.get(mCurrentUserId);
 
-            mSwitchingController = new InputMethodSubtypeSwitchingController(context,
-                    settings.getMethodMap(), settings.getUserId());
-            mHardwareKeyboardShortcutController =
-                    new HardwareKeyboardShortcutController(settings.getMethodMap(),
-                            settings.getUserId());
+            mSwitchingController = new InputMethodSubtypeSwitchingController(context, settings);
+            getUserData(mCurrentUserId).mHardwareKeyboardShortcutController.update(settings);
             mMenuController = new InputMethodMenuController(this);
             mVisibilityStateComputer = new ImeVisibilityStateComputer(this);
             mVisibilityApplier = new DefaultImeVisibilityApplier(this);
@@ -2936,7 +2929,6 @@
      *     </li>
      *     <li>{@link InputMethodBindingController#getDeviceIdToShowIme()} is ignored.</li>
      *     <li>{@link #mSwitchingController} is ignored.</li>
-     *     <li>{@link #mHardwareKeyboardShortcutController} is ignored.</li>
      *     <li>{@link #mPreventImeStartupUnlessTextEditor} is ignored.</li>
      *     <li>and so on.</li>
      * </ul>
@@ -2969,6 +2961,9 @@
             id = imi.getId();
             settings.putSelectedInputMethod(id);
         }
+
+        final var userData = getUserData(userId);
+        userData.mHardwareKeyboardShortcutController.update(settings);
     }
 
     @GuardedBy("ImfLock.class")
@@ -3046,18 +3041,11 @@
 
         // TODO: Instantiate mSwitchingController for each user.
         if (userId == mSwitchingController.getUserId()) {
-            mSwitchingController.resetCircularListLocked(settings.getMethodMap());
+            mSwitchingController.resetCircularListLocked(settings);
         } else {
-            mSwitchingController = new InputMethodSubtypeSwitchingController(mContext,
-                    settings.getMethodMap(), userId);
+            mSwitchingController = new InputMethodSubtypeSwitchingController(mContext, settings);
         }
-        // TODO: Instantiate mHardwareKeyboardShortcutController for each user.
-        if (userId == mHardwareKeyboardShortcutController.getUserId()) {
-            mHardwareKeyboardShortcutController.reset(settings.getMethodMap());
-        } else {
-            mHardwareKeyboardShortcutController = new HardwareKeyboardShortcutController(
-                    settings.getMethodMap(), userId);
-        }
+        getUserData(userId).mHardwareKeyboardShortcutController.update(settings);
         sendOnNavButtonFlagsChangedLocked();
     }
 
@@ -3519,10 +3507,11 @@
 
         mVisibilityStateComputer.requestImeVisibility(windowToken, true);
 
+        final int userId = mCurrentUserId;
         // Ensure binding the connection when IME is going to show.
-        final var bindingController = getInputMethodBindingController(mCurrentUserId);
+        final var bindingController = getInputMethodBindingController(userId);
         bindingController.setCurrentMethodVisible();
-        final IInputMethodInvoker curMethod = getCurMethodLocked();
+        final IInputMethodInvoker curMethod = bindingController.getCurMethod();
         ImeTracker.forLogging().onCancelled(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME);
         final boolean readyToDispatchToIme;
         if (Flags.deferShowSoftInputUntilSessionCreation()) {
@@ -3542,7 +3531,7 @@
             }
             mVisibilityApplier.performShowIme(windowToken, statsToken,
                     mVisibilityStateComputer.getShowFlagsForInputMethodServiceOnly(),
-                    resultReceiver, reason);
+                    resultReceiver, reason, userId);
             mVisibilityStateComputer.setInputShown(true);
             return true;
         } else {
@@ -3654,7 +3643,9 @@
         // since Android Eclair.  That's why we need to accept IMM#hideSoftInput() even when only
         // IMMS#InputShown indicates that the software keyboard is shown.
         // TODO(b/246309664): Clean up IMMS#mImeWindowVis
-        IInputMethodInvoker curMethod = getCurMethodLocked();
+        final int userId = mCurrentUserId;
+        final var bindingController = getInputMethodBindingController(userId);
+        IInputMethodInvoker curMethod = bindingController.getCurMethod();
         final boolean shouldHideSoftInput = curMethod != null
                 && (isInputShownLocked() || (mImeWindowVis & InputMethodService.IME_ACTIVE) != 0);
 
@@ -3665,11 +3656,11 @@
             // IMMS#mInputShown and IMMS#mImeWindowVis should be resolved spontaneously in
             // the final state.
             ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_SERVER_SHOULD_HIDE);
-            mVisibilityApplier.performHideIme(windowToken, statsToken, resultReceiver, reason);
+            mVisibilityApplier.performHideIme(windowToken, statsToken, resultReceiver, reason,
+                    userId);
         } else {
             ImeTracker.forLogging().onCancelled(statsToken, ImeTracker.PHASE_SERVER_SHOULD_HIDE);
         }
-        final var bindingController = getInputMethodBindingController(mCurrentUserId);
         bindingController.setCurrentMethodNotVisible();
         mVisibilityStateComputer.clearImeShowFlags();
         // Cancel existing statsToken for show IME as we got a hide request.
@@ -4735,12 +4726,14 @@
      */
     @GuardedBy("ImfLock.class")
     void onShowHideSoftInputRequested(boolean show, IBinder requestImeToken,
-            @SoftInputShowHideReason int reason, @Nullable ImeTracker.Token statsToken) {
+            @SoftInputShowHideReason int reason, @Nullable ImeTracker.Token statsToken,
+            @UserIdInt int userId) {
         final IBinder requestToken = mVisibilityStateComputer.getWindowTokenFrom(requestImeToken);
+        final var bindingController = getInputMethodBindingController(userId);
         final WindowManagerInternal.ImeTargetInfo info =
                 mWindowManagerInternal.onToggleImeRequested(
                         show, mImeBindingState.mFocusedWindow, requestToken,
-                        getCurTokenDisplayIdLocked());
+                        bindingController.getCurTokenDisplayId());
         mSoftInputShowHideHistory.addEntry(new SoftInputShowHideHistory.Entry(
                 mImeBindingState.mFocusedWindowClient, mImeBindingState.mFocusedWindowEditorInfo,
                 info.focusedWindowName, mImeBindingState.mFocusedWindowSoftInputMode, reason,
@@ -4927,7 +4920,7 @@
                     final List<ImeSubtypeListItem> imList = InputMethodSubtypeSwitchingController
                             .getSortedInputMethodAndSubtypeList(
                                     showAuxSubtypes, isScreenLocked, true /* forImeMenu */,
-                                    mContext, settings.getMethodMap(), settings.getUserId());
+                                    mContext, settings);
                     if (imList.isEmpty()) {
                         Slog.w(TAG, "Show switching menu failed, imList is empty,"
                                 + " showAuxSubtypes: " + showAuxSubtypes
@@ -5317,18 +5310,11 @@
 
         // TODO: Instantiate mSwitchingController for each user.
         if (userId == mSwitchingController.getUserId()) {
-            mSwitchingController.resetCircularListLocked(settings.getMethodMap());
+            mSwitchingController.resetCircularListLocked(settings);
         } else {
-            mSwitchingController = new InputMethodSubtypeSwitchingController(mContext,
-                    settings.getMethodMap(), mCurrentUserId);
+            mSwitchingController = new InputMethodSubtypeSwitchingController(mContext, settings);
         }
-        // TODO: Instantiate mHardwareKeyboardShortcutController for each user.
-        if (userId == mHardwareKeyboardShortcutController.getUserId()) {
-            mHardwareKeyboardShortcutController.reset(settings.getMethodMap());
-        } else {
-            mHardwareKeyboardShortcutController = new HardwareKeyboardShortcutController(
-                    settings.getMethodMap(), userId);
-        }
+        getUserData(userId).mHardwareKeyboardShortcutController.update(settings);
 
         sendOnNavButtonFlagsChangedLocked();
 
@@ -5639,8 +5625,8 @@
         final InputMethodSubtypeHandle currentSubtypeHandle =
                 InputMethodSubtypeHandle.of(currentImi, bindingController.getCurrentSubtype());
         final InputMethodSubtypeHandle nextSubtypeHandle =
-                mHardwareKeyboardShortcutController.onSubtypeSwitch(currentSubtypeHandle,
-                        direction > 0);
+                getUserData(userId).mHardwareKeyboardShortcutController.onSubtypeSwitch(
+                        currentSubtypeHandle, direction > 0);
         if (nextSubtypeHandle == null) {
             return;
         }
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java
index bf9621f..f97a516 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java
@@ -163,13 +163,12 @@
     @NonNull
     static List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeList(
             boolean includeAuxiliarySubtypes, boolean isScreenLocked, boolean forImeMenu,
-            @NonNull Context context, @NonNull InputMethodMap methodMap,
-            @UserIdInt int userId) {
+            @NonNull Context context, @NonNull InputMethodSettings settings) {
+        final int userId = settings.getUserId();
         final Context userAwareContext = context.getUserId() == userId
                 ? context
                 : context.createContextAsUser(UserHandle.of(userId), 0 /* flags */);
         final String mSystemLocaleStr = SystemLocaleWrapper.get(userId).get(0).toLanguageTag();
-        final InputMethodSettings settings = InputMethodSettings.create(methodMap, userId);
 
         final ArrayList<InputMethodInfo> imis = settings.getEnabledInputMethodList();
         if (imis.isEmpty()) {
@@ -487,13 +486,13 @@
     private ControllerImpl mController;
 
     InputMethodSubtypeSwitchingController(@NonNull Context context,
-            @NonNull InputMethodMap methodMap, @UserIdInt int userId) {
+            @NonNull InputMethodSettings settings) {
         mContext = context;
-        mUserId = userId;
+        mUserId = settings.getUserId();
         mController = ControllerImpl.createFrom(null,
                 getSortedInputMethodAndSubtypeList(
                         false /* includeAuxiliarySubtypes */, false /* isScreenLocked */,
-                        false /* forImeMenu */, context, methodMap, userId));
+                        false /* forImeMenu */, context, settings));
     }
 
     @AnyThread
@@ -507,11 +506,11 @@
         mController.onUserActionLocked(imi, subtype);
     }
 
-    public void resetCircularListLocked(@NonNull InputMethodMap methodMap) {
+    public void resetCircularListLocked(@NonNull InputMethodSettings settings) {
         mController = ControllerImpl.createFrom(mController,
                 getSortedInputMethodAndSubtypeList(
                         false /* includeAuxiliarySubtypes */, false /* isScreenLocked */,
-                        false /* forImeMenu */, mContext, methodMap, mUserId));
+                        false /* forImeMenu */, mContext, settings));
     }
 
     @Nullable
diff --git a/services/core/java/com/android/server/inputmethod/UserDataRepository.java b/services/core/java/com/android/server/inputmethod/UserDataRepository.java
index 2b19d3e..5da4e89 100644
--- a/services/core/java/com/android/server/inputmethod/UserDataRepository.java
+++ b/services/core/java/com/android/server/inputmethod/UserDataRepository.java
@@ -88,6 +88,9 @@
         @NonNull
         final InputMethodBindingController mBindingController;
 
+        @NonNull
+        final HardwareKeyboardShortcutController mHardwareKeyboardShortcutController;
+
         /**
          * Intended to be instantiated only from this file.
          */
@@ -95,6 +98,8 @@
                 @NonNull InputMethodBindingController bindingController) {
             mUserId = userId;
             mBindingController = bindingController;
+            mHardwareKeyboardShortcutController = new HardwareKeyboardShortcutController(
+                    InputMethodSettings.createEmptyMap(userId));
         }
 
         @Override
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubClientBroker.java b/services/core/java/com/android/server/location/contexthub/ContextHubClientBroker.java
index 363a4a7..91a4d6f 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubClientBroker.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubClientBroker.java
@@ -1126,7 +1126,7 @@
         }
     }
 
-    private void sendHostEndpointConnectedEvent() {
+    void sendHostEndpointConnectedEvent() {
         HostEndpointInfo info = new HostEndpointInfo();
         info.hostEndpointId = (char) mHostEndPointId;
         info.packageName = mPackage;
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubService.java b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
index b3fb147..7a722bc 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubService.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
@@ -74,6 +74,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -158,10 +159,8 @@
 
     // A queue of reliable message records for duplicate detection
     private final PriorityQueue<ReliableMessageRecord> mReliableMessageRecordQueue =
-            new PriorityQueue<ReliableMessageRecord>(
-                    (ReliableMessageRecord left, ReliableMessageRecord right) -> {
-                        return Long.compare(left.getTimestamp(), right.getTimestamp());
-                    });
+            new PriorityQueue<>(
+                    Comparator.comparingLong(ReliableMessageRecord::getTimestamp));
 
     // The test mode manager that manages behaviors during test mode.
     private final TestModeManager mTestModeManager = new TestModeManager();
@@ -179,10 +178,10 @@
     private boolean mIsBtMainEnabled = false;
 
     // True if test mode is enabled for the Context Hub
-    private AtomicBoolean mIsTestModeEnabled = new AtomicBoolean(false);
+    private final AtomicBoolean mIsTestModeEnabled = new AtomicBoolean(false);
 
     // A hashmap used to record if a contexthub is waiting for daily query
-    private Set<Integer> mMetricQueryPendingContextHubIds =
+    private final Set<Integer> mMetricQueryPendingContextHubIds =
             Collections.newSetFromMap(new ConcurrentHashMap<Integer, Boolean>());
 
     // Lock object for sendWifiSettingUpdate()
@@ -242,10 +241,14 @@
 
         @Override
         public void handleServiceRestart() {
-            Log.i(TAG, "Starting Context Hub Service restart");
+            Log.i(TAG, "Recovering from Context Hub HAL restart...");
             initExistingCallbacks();
             resetSettings();
-            Log.i(TAG, "Finished Context Hub Service restart");
+            if (Flags.reconnectHostEndpointsAfterHalRestart()) {
+                mClientManager.forEachClientOfHub(mContextHubId,
+                        ContextHubClientBroker::sendHostEndpointConnectedEvent);
+            }
+            Log.i(TAG, "Finished recovering from Context Hub HAL restart");
         }
 
         @Override
@@ -317,11 +320,11 @@
          */
         private static final int MAX_PROBABILITY_PERCENT = 100;
 
-        private Random mRandom = new Random();
+        private final Random mRandom = new Random();
 
         /**
-         * @see ContextHubServiceCallback.handleNanoappMessage
          * @return whether the message was handled
+         * @see ContextHubServiceCallback#handleNanoappMessage
          */
         public boolean handleNanoappMessage(int contextHubId,
                 short hostEndpointId, NanoAppMessage message,
@@ -331,7 +334,8 @@
             }
 
             if (Flags.reliableMessageDuplicateDetectionService()
-                && didEventHappen(MESSAGE_DUPLICATION_PROBABILITY_PERCENT)) {
+                    && mRandom.nextInt(MAX_PROBABILITY_PERCENT)
+                    < MESSAGE_DUPLICATION_PROBABILITY_PERCENT) {
                 Log.i(TAG, "[TEST MODE] Duplicating message ("
                         + NUM_MESSAGES_TO_DUPLICATE
                         + " sends) with message sequence number: "
@@ -344,16 +348,6 @@
             }
             return false;
         }
-
-        /**
-         * Returns true if the event with percentPercent did happen.
-         *
-         * @param probabilityPercent the percent probability of the event.
-         * @return true if the event happened, false otherwise.
-         */
-        private boolean didEventHappen(int probabilityPercent) {
-            return mRandom.nextInt(MAX_PROBABILITY_PERCENT) < probabilityPercent;
-        }
     }
 
     public ContextHubService(Context context, IContextHubWrapper contextHubWrapper) {
@@ -476,7 +470,7 @@
             hubInfo = mContextHubWrapper.getHubs();
         } catch (RemoteException e) {
             Log.e(TAG, "RemoteException while getting Context Hub info", e);
-            hubInfo = new Pair(Collections.emptyList(), Collections.emptyList());
+            hubInfo = new Pair<>(Collections.emptyList(), Collections.emptyList());
         }
 
         long bootTimeNs = SystemClock.elapsedRealtimeNanos() - startTimeNs;
@@ -536,6 +530,7 @@
         for (int contextHubId : mContextHubIdToInfoMap.keySet()) {
             try {
                 mContextHubWrapper.registerExistingCallback(contextHubId);
+                Log.i(TAG, "Re-registered callback to context hub " + contextHubId);
             } catch (RemoteException e) {
                 Log.e(TAG, "RemoteException while registering existing service callback for hub "
                         + "(ID = " + contextHubId + ")", e);
@@ -647,7 +642,7 @@
         mSensorPrivacyManagerInternal.addSensorPrivacyListenerForAllUsers(
                 SensorPrivacyManager.Sensors.MICROPHONE, (userId, enabled) -> {
                     // If we are in HSUM mode, any user can change the microphone setting
-                    if (mUserManager.isHeadlessSystemUserMode() || userId == getCurrentUserId()) {
+                    if (UserManager.isHeadlessSystemUserMode() || userId == getCurrentUserId()) {
                         Log.d(TAG, "User: " + userId + " mic privacy: " + enabled);
                         sendMicrophoneDisableSettingUpdate(enabled);
                     }
@@ -720,33 +715,30 @@
 
     @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     @Override
-    public int[] getContextHubHandles() throws RemoteException {
+    public int[] getContextHubHandles() {
         super.getContextHubHandles_enforcePermission();
-
         return ContextHubServiceUtil.createPrimitiveIntArray(mContextHubIdToInfoMap.keySet());
     }
 
     @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     @Override
-    public ContextHubInfo getContextHubInfo(int contextHubHandle) throws RemoteException {
+    public ContextHubInfo getContextHubInfo(int contextHubHandle) {
         super.getContextHubInfo_enforcePermission();
-
         if (!mContextHubIdToInfoMap.containsKey(contextHubHandle)) {
             Log.e(TAG, "Invalid Context Hub handle " + contextHubHandle + " in getContextHubInfo");
             return null;
         }
-
         return mContextHubIdToInfoMap.get(contextHubHandle);
     }
 
-    @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     /**
      * Returns a List of ContextHubInfo object describing the available hubs.
      *
      * @return the List of ContextHubInfo objects
      */
+    @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     @Override
-    public List<ContextHubInfo> getContextHubs() throws RemoteException {
+    public List<ContextHubInfo> getContextHubs() {
         super.getContextHubs_enforcePermission();
 
         return mContextHubInfoList;
@@ -814,7 +806,7 @@
 
     @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     @Override
-    public int loadNanoApp(int contextHubHandle, NanoApp nanoApp) throws RemoteException {
+    public int loadNanoApp(int contextHubHandle, NanoApp nanoApp) {
         super.loadNanoApp_enforcePermission();
 
         if (mContextHubWrapper == null) {
@@ -843,7 +835,7 @@
 
     @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     @Override
-    public int unloadNanoApp(int nanoAppHandle) throws RemoteException {
+    public int unloadNanoApp(int nanoAppHandle) {
         super.unloadNanoApp_enforcePermission();
 
         if (mContextHubWrapper == null) {
@@ -870,7 +862,7 @@
 
     @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     @Override
-    public NanoAppInstanceInfo getNanoAppInstanceInfo(int nanoAppHandle) throws RemoteException {
+    public NanoAppInstanceInfo getNanoAppInstanceInfo(int nanoAppHandle) {
 
         super.getNanoAppInstanceInfo_enforcePermission();
 
@@ -880,7 +872,7 @@
     @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     @Override
     public int[] findNanoAppOnHub(
-            int contextHubHandle, NanoAppFilter filter) throws RemoteException {
+            int contextHubHandle, NanoAppFilter filter) {
 
         super.findNanoAppOnHub_enforcePermission();
 
@@ -895,20 +887,19 @@
 
         int[] retArray = new int[foundInstances.size()];
         for (int i = 0; i < foundInstances.size(); i++) {
-            retArray[i] = foundInstances.get(i).intValue();
+            retArray[i] = foundInstances.get(i);
         }
         return retArray;
     }
 
     /**
      * Performs a query at the specified hub.
-     * <p>
-     * This method should only be invoked internally by the service, either to update the service
+     *
+     * <p>This method should only be invoked internally by the service, either to update the service
      * cache or as a result of an explicit query requested by a client through the sendMessage API.
      *
      * @param contextHubId the ID of the hub to do the query
      * @return true if the query succeeded
-     * @throws IllegalStateException if the transaction queue is full
      */
     private boolean queryNanoAppsInternal(int contextHubId) {
         if (mContextHubWrapper == null) {
@@ -1003,7 +994,7 @@
             return;
         }
 
-        byte errorCode = ErrorCode.OK;
+        byte errorCode;
         synchronized (mReliableMessageRecordQueue) {
             Optional<ReliableMessageRecord> record =
                     findReliableMessageRecord(contextHubId,
@@ -1219,7 +1210,6 @@
         return mContextHubIdToInfoMap.containsKey(contextHubId);
     }
 
-    @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     /**
      * Creates and registers a client at the service for the specified Context Hub.
      *
@@ -1232,10 +1222,11 @@
      * @throws IllegalStateException    if max number of clients have already registered
      * @throws NullPointerException     if clientCallback is null
      */
+    @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     @Override
     public IContextHubClient createClient(
             int contextHubId, IContextHubClientCallback clientCallback,
-            @Nullable String attributionTag, String packageName) throws RemoteException {
+            @Nullable String attributionTag, String packageName) {
         super.createClient_enforcePermission();
 
         if (!isValidContextHubId(contextHubId)) {
@@ -1250,7 +1241,6 @@
                 contextHubInfo, clientCallback, attributionTag, mTransactionManager, packageName);
     }
 
-    @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     /**
      * Creates and registers a PendingIntent client at the service for the specified Context Hub.
      *
@@ -1262,10 +1252,11 @@
      * @throws IllegalArgumentException if hubInfo does not represent a valid hub
      * @throws IllegalStateException    if there were too many registered clients at the service
      */
+    @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     @Override
     public IContextHubClient createPendingIntentClient(
             int contextHubId, PendingIntent pendingIntent, long nanoAppId,
-            @Nullable String attributionTag) throws RemoteException {
+            @Nullable String attributionTag) {
         super.createPendingIntentClient_enforcePermission();
 
         if (!isValidContextHubId(contextHubId)) {
@@ -1277,15 +1268,14 @@
                 contextHubInfo, pendingIntent, nanoAppId, attributionTag, mTransactionManager);
     }
 
-    @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     /**
      * Loads a nanoapp binary at the specified Context hub.
      *
      * @param contextHubId        the ID of the hub to load the binary
      * @param transactionCallback the client-facing transaction callback interface
      * @param nanoAppBinary       the binary to load
-     * @throws IllegalStateException if the transaction queue is full
      */
+    @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     @Override
     public void loadNanoAppOnHub(
             int contextHubId, IContextHubTransactionCallback transactionCallback,
@@ -1308,15 +1298,14 @@
         mTransactionManager.addTransaction(transaction);
     }
 
-    @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     /**
      * Unloads a nanoapp from the specified Context Hub.
      *
      * @param contextHubId        the ID of the hub to unload the nanoapp
      * @param transactionCallback the client-facing transaction callback interface
      * @param nanoAppId           the ID of the nanoapp to unload
-     * @throws IllegalStateException if the transaction queue is full
      */
+    @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     @Override
     public void unloadNanoAppFromHub(
             int contextHubId, IContextHubTransactionCallback transactionCallback, long nanoAppId)
@@ -1333,19 +1322,17 @@
         mTransactionManager.addTransaction(transaction);
     }
 
-    @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     /**
      * Enables a nanoapp at the specified Context Hub.
      *
      * @param contextHubId        the ID of the hub to enable the nanoapp
      * @param transactionCallback the client-facing transaction callback interface
      * @param nanoAppId           the ID of the nanoapp to enable
-     * @throws IllegalStateException if the transaction queue is full
      */
+    @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     @Override
     public void enableNanoApp(
-            int contextHubId, IContextHubTransactionCallback transactionCallback, long nanoAppId)
-            throws RemoteException {
+            int contextHubId, IContextHubTransactionCallback transactionCallback, long nanoAppId) {
         super.enableNanoApp_enforcePermission();
 
         if (!checkHalProxyAndContextHubId(
@@ -1358,19 +1345,17 @@
         mTransactionManager.addTransaction(transaction);
     }
 
-    @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     /**
      * Disables a nanoapp at the specified Context Hub.
      *
      * @param contextHubId        the ID of the hub to disable the nanoapp
      * @param transactionCallback the client-facing transaction callback interface
      * @param nanoAppId           the ID of the nanoapp to disable
-     * @throws IllegalStateException if the transaction queue is full
      */
+    @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     @Override
     public void disableNanoApp(
-            int contextHubId, IContextHubTransactionCallback transactionCallback, long nanoAppId)
-            throws RemoteException {
+            int contextHubId, IContextHubTransactionCallback transactionCallback, long nanoAppId) {
         super.disableNanoApp_enforcePermission();
 
         if (!checkHalProxyAndContextHubId(
@@ -1383,17 +1368,16 @@
         mTransactionManager.addTransaction(transaction);
     }
 
-    @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     /**
      * Queries for a list of nanoapps from the specified Context hub.
      *
      * @param contextHubId        the ID of the hub to query
      * @param transactionCallback the client-facing transaction callback interface
-     * @throws IllegalStateException if the transaction queue is full
      */
+    @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     @Override
-    public void queryNanoApps(int contextHubId, IContextHubTransactionCallback transactionCallback)
-            throws RemoteException {
+    public void queryNanoApps(int contextHubId,
+            IContextHubTransactionCallback transactionCallback) {
         super.queryNanoApps_enforcePermission();
 
         if (!checkHalProxyAndContextHubId(
@@ -1406,16 +1390,15 @@
         mTransactionManager.addTransaction(transaction);
     }
 
-    @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     /**
      * Queries for a list of preloaded nanoapp IDs from the specified Context Hub.
      *
      * @param hubInfo The Context Hub to query a list of nanoapps from.
      * @return The list of 64-bit IDs of the preloaded nanoapps.
-     * @throws NullPointerException if hubInfo is null
      */
+    @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     @Override
-    public long[] getPreloadedNanoAppIds(ContextHubInfo hubInfo) throws RemoteException {
+    public long[] getPreloadedNanoAppIds(ContextHubInfo hubInfo) {
         super.getPreloadedNanoAppIds_enforcePermission();
         Objects.requireNonNull(hubInfo, "hubInfo cannot be null");
 
@@ -1426,7 +1409,6 @@
         return nanoappIds;
     }
 
-    @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     /**
      * Puts the context hub in and out of test mode. Test mode is a clean state
      * where tests can be executed in the same environment. If enable is true,
@@ -1442,6 +1424,7 @@
      *               test mode.
      * @return       If true, the operation was successful; false otherwise.
      */
+    @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     @Override
     public boolean setTestMode(boolean enable) {
         super.setTestMode_enforcePermission();
@@ -1551,10 +1534,6 @@
         }
     }
 
-    private void checkPermissions() {
-        ContextHubServiceUtil.checkPermissions(mContext);
-    }
-
     private int onMessageReceiptOldApi(
             int msgType, int contextHubHandle, int appInstance, byte[] data) {
         if (data == null) {
@@ -1586,7 +1565,6 @@
                     callback.onMessageReceipt(contextHubHandle, appInstance, msg);
                 } catch (RemoteException e) {
                     Log.i(TAG, "Exception (" + e + ") calling remote callback (" + callback + ").");
-                    continue;
                 }
             }
             mCallbacksList.finishBroadcast();
@@ -1729,8 +1707,8 @@
      * Hub.
      */
     private void sendMicrophoneDisableSettingUpdateForCurrentUser() {
-        boolean isEnabled = mSensorPrivacyManagerInternal == null ? false :
-                mSensorPrivacyManagerInternal.isSensorPrivacyEnabled(
+        boolean isEnabled = mSensorPrivacyManagerInternal != null
+                && mSensorPrivacyManagerInternal.isSensorPrivacyEnabled(
                 getCurrentUserId(), SensorPrivacyManager.Sensors.MICROPHONE);
         sendMicrophoneDisableSettingUpdate(isEnabled);
     }
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index f297708..a4f534e 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -8565,6 +8565,13 @@
          */
         private boolean enqueueNotification() {
             synchronized (mNotificationLock) {
+                if (android.app.Flags.secureAllowlistToken()) {
+                    // allowlistToken is populated by unparceling, so it will be absent if the
+                    // EnqueueNotificationRunnable is created directly by NMS (as we do for group
+                    // summaries) instead of via notify(). Fix that.
+                    r.getNotification().overrideAllowlistToken(ALLOWLIST_TOKEN);
+                }
+
                 final long snoozeAt =
                         mSnoozeHelper.getSnoozeTimeForUnpostedNotification(
                                 r.getUser().getIdentifier(),
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 2e7295e..c078409 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -34,6 +34,7 @@
 import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_INIT_USER;
 import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_RESTORE_BACKUP;
 import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI;
+import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_UNKNOWN;
 import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_USER;
 
 import static com.android.internal.util.FrameworkStatsLog.DND_MODE_RULE;
@@ -1143,6 +1144,17 @@
                 modified = true;
             }
 
+            if (Flags.modesUi()) {
+                if (!azr.isEnabled() && (isNew || rule.enabled)) {
+                    // Creating a rule as disabled, or disabling a previously enabled rule.
+                    // Record whodunit.
+                    rule.disabledOrigin = origin;
+                } else if (azr.isEnabled()) {
+                    // Enabling or previously enabled. Clear disabler.
+                    rule.disabledOrigin = UPDATE_ORIGIN_UNKNOWN;
+                }
+            }
+
             if (!Objects.equals(rule.conditionId, azr.getConditionId())) {
                 rule.conditionId = azr.getConditionId();
                 modified = true;
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index d1d8993..7f4a5cb 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -62,6 +62,8 @@
 import android.app.IActivityManager;
 import android.app.IStopUserCallback;
 import android.app.KeyguardManager;
+import android.app.Notification;
+import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.app.StatsManager;
 import android.app.admin.DevicePolicyEventLogger;
@@ -147,6 +149,8 @@
 import com.android.internal.app.IAppOpsService;
 import com.android.internal.app.SetScreenLockDialogActivity;
 import com.android.internal.logging.MetricsLogger;
+import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
+import com.android.internal.notification.SystemNotificationChannels;
 import com.android.internal.os.BackgroundThread;
 import com.android.internal.os.RoSystemProperties;
 import com.android.internal.util.DumpUtils;
@@ -1070,6 +1074,8 @@
         if (isAutoLockingPrivateSpaceOnRestartsEnabled()) {
             autoLockPrivateSpace();
         }
+
+        showHsumNotificationIfNeeded();
     }
 
     private boolean isAutoLockingPrivateSpaceOnRestartsEnabled() {
@@ -4163,6 +4169,48 @@
         mUpdatingSystemUserMode = true;
     }
 
+    /**
+     * If the device's actual HSUM status differs from that which is defined by its build
+     * configuration, warn the user. Ignores HSUM emulated status, since that isn't relevant.
+     *
+     * The goal is to inform dogfooders that they need to factory reset the device to align their
+     * device with its build configuration.
+     */
+    private void showHsumNotificationIfNeeded() {
+        if (RoSystemProperties.MULTIUSER_HEADLESS_SYSTEM_USER == isHeadlessSystemUserMode()) {
+            // Actual state does match the configuration. Great!
+            return;
+        }
+        if (Build.isDebuggable()
+                && !TextUtils.isEmpty(SystemProperties.get(SYSTEM_USER_MODE_EMULATION_PROPERTY))) {
+            // Ignore any device that has been playing around with HSUM emulation.
+            return;
+        }
+        Slogf.w(LOG_TAG, "Posting warning that device's HSUM status doesn't match the build's.");
+
+        final String title = mContext
+                .getString(R.string.wrong_hsum_configuration_notification_title);
+        final String message = mContext
+                .getString(R.string.wrong_hsum_configuration_notification_message);
+
+        final Notification notification =
+                new Notification.Builder(mContext, SystemNotificationChannels.DEVELOPER)
+                        .setSmallIcon(R.drawable.stat_sys_adb)
+                        .setWhen(0)
+                        .setOngoing(true)
+                        .setTicker(title)
+                        .setDefaults(0)
+                        .setColor(mContext.getColor(R.color.system_notification_accent_color))
+                        .setContentTitle(title)
+                        .setContentText(message)
+                        .setVisibility(Notification.VISIBILITY_PUBLIC)
+                        .build();
+
+        final NotificationManager notificationManager =
+                mContext.getSystemService(NotificationManager.class);
+        notificationManager.notifyAsUser(
+                null, SystemMessage.NOTE_WRONG_HSUM_STATUS, notification, UserHandle.ALL);
+    }
 
     private ResilientAtomicFile getUserListFile() {
         File tempBackup = new File(mUserListFile.getParent(), mUserListFile.getName() + ".backup");
diff --git a/services/core/java/com/android/server/power/OWNERS b/services/core/java/com/android/server/power/OWNERS
index 94340ec..c1fad33 100644
--- a/services/core/java/com/android/server/power/OWNERS
+++ b/services/core/java/com/android/server/power/OWNERS
@@ -1,6 +1,7 @@
 michaelwr@google.com
 santoscordon@google.com
-philipjunker@google.com
+petsjonkin@google.com
+brup@google.com
 
 per-file ThermalManagerService.java=file:/THERMAL_OWNERS
 per-file LowPowerStandbyController.java=qingxun@google.com
diff --git a/services/core/java/com/android/server/timezonedetector/OWNERS b/services/core/java/com/android/server/timezonedetector/OWNERS
index dfa07d8..4220d14 100644
--- a/services/core/java/com/android/server/timezonedetector/OWNERS
+++ b/services/core/java/com/android/server/timezonedetector/OWNERS
@@ -1,7 +1,6 @@
 # Bug component: 847766
 # This is the main list for platform time / time zone detection maintainers, for this dir and
 # ultimately referenced by other OWNERS files for components maintained by the same team.
-nfuller@google.com
 boullanger@google.com
 jmorace@google.com
 kanyinsola@google.com
diff --git a/services/core/java/com/android/server/vibrator/HapticFeedbackCustomization.java b/services/core/java/com/android/server/vibrator/HapticFeedbackCustomization.java
index 9756094..503a726 100644
--- a/services/core/java/com/android/server/vibrator/HapticFeedbackCustomization.java
+++ b/services/core/java/com/android/server/vibrator/HapticFeedbackCustomization.java
@@ -108,9 +108,9 @@
             throws CustomizationParserException, IOException {
         try {
             return loadVibrationsInternal(res, vibratorInfo);
-        } catch (VibrationXmlParser.VibrationXmlParserException
-                | XmlParserException
-                | XmlPullParserException e) {
+        } catch (VibrationXmlParser.ParseFailedException
+                 | XmlParserException
+                 | XmlPullParserException e) {
             throw new CustomizationParserException(
                     "Error parsing haptic feedback customization file.", e);
         }
@@ -121,7 +121,6 @@
             Resources res, VibratorInfo vibratorInfo) throws
                     CustomizationParserException,
                     IOException,
-                    VibrationXmlParser.VibrationXmlParserException,
                     XmlParserException,
                     XmlPullParserException {
         if (!Flags.hapticFeedbackVibrationOemCustomizationEnabled()) {
@@ -172,10 +171,6 @@
 
             ParsedVibration parsedVibration = VibrationXmlParser.parseElement(
                     parser, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS);
-            if (parsedVibration == null) {
-                throw new CustomizationParserException(
-                        "Unable to parse vibration element for effect " + effectId);
-            }
             VibrationEffect effect = parsedVibration.resolve(vibratorInfo);
             if (effect != null) {
                 if (effect.getDuration() == Long.MAX_VALUE) {
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index 7f60dc44..5c15ccb 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -2494,9 +2494,6 @@
             try {
                 ParsedVibration parsedVibration =
                         VibrationXmlParser.parseDocument(new StringReader(xml));
-                if (parsedVibration == null) {
-                    throw new IllegalArgumentException("Error parsing vibration XML " + xml);
-                }
                 VibratorInfo combinedVibratorInfo = getCombinedVibratorInfo();
                 if (combinedVibratorInfo == null) {
                     throw new IllegalStateException(
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index b89120b..b846947 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -650,9 +650,12 @@
             synchronized (mLock) {
                 if (mLastWallpaper != null) {
                     WallpaperData targetWallpaper = null;
-                    if (mLastWallpaper.connection.containsDisplay(displayId)) {
+                    if (mLastWallpaper.connection != null &&
+                            mLastWallpaper.connection.containsDisplay(displayId)) {
                         targetWallpaper = mLastWallpaper;
-                    } else if (mFallbackWallpaper.connection.containsDisplay(displayId)) {
+                    } else if (mFallbackWallpaper != null &&
+                            mFallbackWallpaper.connection != null &&
+                            mFallbackWallpaper.connection.containsDisplay(displayId)) {
                         targetWallpaper = mFallbackWallpaper;
                     }
                     if (targetWallpaper == null) return;
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 9bc4389..21155bb 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -6975,14 +6975,11 @@
         updateReportedVisibilityLocked();
     }
 
-    /**
-     * Sets whether something has been visible in the task and returns {@code true} if the state
-     * is changed from invisible to visible.
-     */
-    private boolean setTaskHasBeenVisible() {
+    /** Sets whether something has been visible in the task. */
+    private void setTaskHasBeenVisible() {
         final boolean wasTaskVisible = task.getHasBeenVisible();
         if (wasTaskVisible) {
-            return false;
+            return;
         }
         if (inTransition()) {
             // The deferring will be canceled until transition is ready so it won't dispatch
@@ -6990,20 +6987,22 @@
             task.setDeferTaskAppear(true);
         }
         task.setHasBeenVisible(true);
-        return true;
     }
 
     void onStartingWindowDrawn() {
-        boolean wasTaskVisible = false;
         if (task != null) {
             mSplashScreenStyleSolidColor = true;
-            wasTaskVisible = !setTaskHasBeenVisible();
+            setTaskHasBeenVisible();
         }
+        if (mStartingData == null || mStartingData.mIsDisplayed) {
+            return;
+        }
+        mStartingData.mIsDisplayed = true;
 
         // The transition may not be executed if the starting process hasn't attached. But if the
         // starting window is drawn, the transition can start earlier. Exclude finishing and bubble
         // because it may be a trampoline.
-        if (!wasTaskVisible && mStartingData != null && !finishing && !mLaunchedFromBubble
+        if (app == null && !finishing && !mLaunchedFromBubble
                 && mVisibleRequested && !mDisplayContent.mAppTransition.isReady()
                 && !mDisplayContent.mAppTransition.isRunning()
                 && mDisplayContent.isNextTransitionForward()) {
@@ -7240,9 +7239,6 @@
                         isInterestingAndDrawn = true;
                     }
                 }
-            } else if (mStartingData != null && w.isDrawn()) {
-                // The starting window for this container is drawn.
-                mStartingData.mIsDisplayed = true;
             }
         }
 
@@ -7526,7 +7522,8 @@
      *               use an icon or solid color splash screen will be made by WmShell.
      */
     private boolean shouldUseSolidColorSplashScreen(ActivityRecord sourceRecord,
-            boolean startActivity, ActivityOptions options, int resolvedTheme) {
+            boolean startActivity, ActivityOptions options, int resolvedTheme,
+            boolean newTask) {
         if (sourceRecord == null && !startActivity) {
             // Use simple style if this activity is not top activity. This could happen when adding
             // a splash screen window to the warm start activity which is re-create because top is
@@ -7549,21 +7546,19 @@
 
         // Choose the default behavior when neither the ActivityRecord nor the activity theme have
         // specified a splash screen style.
-
-        if (mLaunchSourceType == LAUNCH_SOURCE_TYPE_HOME || launchedFromUid == Process.SHELL_UID) {
-            return false;
-        } else if (mLaunchSourceType == LAUNCH_SOURCE_TYPE_SYSTEMUI) {
+        if (mLaunchSourceType == LAUNCH_SOURCE_TYPE_SYSTEMUI) {
             return true;
         } else {
             // Need to check sourceRecord in case this activity is launched from a service.
             if (sourceRecord == null) {
                 sourceRecord = searchCandidateLaunchingActivity();
             }
-
             if (sourceRecord != null) {
-                return sourceRecord.mSplashScreenStyleSolidColor;
+                return sourceRecord.mSplashScreenStyleSolidColor; // follow previous activity
+            } else if (mLaunchSourceType == LAUNCH_SOURCE_TYPE_HOME
+                    || launchedFromUid == Process.SHELL_UID) {
+                return !newTask; // only show icon for new task
             }
-
             // Use an icon if the activity was launched from System for the first start.
             // Otherwise, must use solid color splash screen.
             return mLaunchSourceType != LAUNCH_SOURCE_TYPE_SYSTEM || !startActivity;
@@ -7631,7 +7626,7 @@
                 splashScreenTheme);
 
         mSplashScreenStyleSolidColor = shouldUseSolidColorSplashScreen(sourceRecord, startActivity,
-                startOptions, resolvedTheme);
+                startOptions, resolvedTheme, newTask);
 
         final boolean activityCreated =
                 mState.ordinal() >= STARTED.ordinal() && mState.ordinal() <= STOPPED.ordinal();
@@ -8293,7 +8288,8 @@
      */
     @Override
     protected int getOverrideOrientation() {
-        return mLetterboxUiController.overrideOrientationIfNeeded(super.getOverrideOrientation());
+        return mAppCompatController.getOrientationPolicy()
+                .overrideOrientationIfNeeded(super.getOverrideOrientation());
     }
 
     /**
@@ -10825,7 +10821,8 @@
         proto.write(SHOULD_OVERRIDE_MIN_ASPECT_RATIO,
                 mLetterboxUiController.shouldOverrideMinAspectRatio());
         proto.write(SHOULD_IGNORE_ORIENTATION_REQUEST_LOOP,
-                mLetterboxUiController.shouldIgnoreOrientationRequestLoop());
+                mAppCompatController.getAppCompatCapability().getAppCompatOrientationCapability()
+                        .shouldIgnoreOrientationRequestLoop());
         proto.write(SHOULD_OVERRIDE_FORCE_RESIZE_APP,
                 mLetterboxUiController.shouldOverrideForceResizeApp());
         proto.write(SHOULD_ENABLE_USER_ASPECT_RATIO_SETTINGS,
@@ -11157,8 +11154,7 @@
             boolean cancel) {
         // This override is just for getting metrics. allFinished needs to be checked before
         // finish because finish resets all the states.
-        final BLASTSyncEngine.SyncGroup syncGroup = getSyncGroup();
-        if (syncGroup != null && group != getSyncGroup()) return;
+        if (isDifferentSyncGroup(group)) return;
         mLastAllReadyAtSync = allSyncFinished();
         super.finishSync(outMergedTransaction, group, cancel);
     }
diff --git a/services/core/java/com/android/server/wm/AppCompatOrientationCapability.java b/services/core/java/com/android/server/wm/AppCompatOrientationCapability.java
index fbe90a2..10f3e83 100644
--- a/services/core/java/com/android/server/wm/AppCompatOrientationCapability.java
+++ b/services/core/java/com/android/server/wm/AppCompatOrientationCapability.java
@@ -38,6 +38,7 @@
 import com.android.server.wm.utils.OptPropFactory;
 
 import java.util.function.BooleanSupplier;
+import java.util.function.LongSupplier;
 
 class AppCompatOrientationCapability {
 
@@ -58,7 +59,8 @@
                                    @NonNull LetterboxConfiguration letterboxConfiguration,
                                    @NonNull ActivityRecord activityRecord) {
         mActivityRecord = activityRecord;
-        mOrientationCapabilityState = new OrientationCapabilityState(mActivityRecord);
+        mOrientationCapabilityState = new OrientationCapabilityState(mActivityRecord,
+                System::currentTimeMillis);
         final BooleanSupplier isPolicyForIgnoringRequestedOrientationEnabled = asLazy(
                 letterboxConfiguration::isPolicyForIgnoringRequestedOrientationEnabled);
         mIgnoreRequestedOrientationOptProp = optPropBuilder.create(
@@ -214,8 +216,12 @@
         private long mTimeMsLastSetOrientationRequest = 0;
         // Counter for ActivityRecord#setRequestedOrientation
         private int mSetOrientationRequestCounter = 0;
+        @VisibleForTesting
+        LongSupplier mCurrentTimeMillisSupplier;
 
-        OrientationCapabilityState(@NonNull ActivityRecord activityRecord) {
+        OrientationCapabilityState(@NonNull ActivityRecord activityRecord,
+                @NonNull LongSupplier currentTimeMillisSupplier) {
+            mCurrentTimeMillisSupplier = currentTimeMillisSupplier;
             mIsOverrideToNosensorOrientationEnabled =
                     activityRecord.info.isChangeEnabled(OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR);
             mIsOverrideToPortraitOrientationEnabled =
@@ -238,7 +244,7 @@
          * Updates the orientation request counter using a specific timeout.
          */
         void updateOrientationRequestLoopState() {
-            final long currTimeMs = System.currentTimeMillis();
+            final long currTimeMs = mCurrentTimeMillisSupplier.getAsLong();
             final long elapsedTime = currTimeMs - mTimeMsLastSetOrientationRequest;
             if (elapsedTime < SET_ORIENTATION_REQUEST_COUNTER_TIMEOUT_MS) {
                 mSetOrientationRequestCounter++;
diff --git a/services/core/java/com/android/server/wm/BLASTSyncEngine.java b/services/core/java/com/android/server/wm/BLASTSyncEngine.java
index e8faff6..a8cc2ae 100644
--- a/services/core/java/com/android/server/wm/BLASTSyncEngine.java
+++ b/services/core/java/com/android/server/wm/BLASTSyncEngine.java
@@ -348,6 +348,11 @@
                 wc.setSyncGroup(this);
             }
             wc.prepareSync();
+            if (wc.mSyncState == WindowContainer.SYNC_STATE_NONE && wc.mSyncGroup != null) {
+                Slog.w(TAG, "addToSync: unset SyncGroup " + wc.mSyncGroup.mSyncId
+                        + " for non-sync " + wc);
+                wc.mSyncGroup = null;
+            }
             if (mReady) {
                 mWm.mWindowPlacerLocked.requestTraversal();
             }
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index 17547f5..e0d2035 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -66,7 +66,6 @@
 import android.annotation.Nullable;
 import android.app.ActivityManager.TaskDescription;
 import android.app.CameraCompatTaskInfo.FreeformCameraCompatMode;
-import android.content.pm.ActivityInfo.ScreenOrientation;
 import android.content.pm.PackageManager;
 import android.content.res.Configuration;
 import android.content.res.Resources;
@@ -151,27 +150,6 @@
         }
     }
 
-    /**
-     * Whether an app is calling {@link android.app.Activity#setRequestedOrientation}
-     * in a loop and orientation request should be ignored.
-     *
-     * <p>This should only be called once in response to
-     * {@link android.app.Activity#setRequestedOrientation}. See
-     * {@link #shouldIgnoreRequestedOrientation} for more details.
-     *
-     * <p>This treatment is enabled when the following conditions are met:
-     * <ul>
-     *     <li>Flag gating the treatment is enabled
-     *     <li>Opt-out component property isn't enabled
-     *     <li>Per-app override is enabled
-     *     <li>App has requested orientation more than 2 times within 1-second
-     *     timer and activity is not letterboxed for fixed orientation
-     * </ul>
-     */
-    boolean shouldIgnoreOrientationRequestLoop() {
-        return getAppCompatCapability().getAppCompatOrientationCapability()
-                .shouldIgnoreOrientationRequestLoop();
-    }
 
     @VisibleForTesting
     int getSetOrientationRequestCounter() {
@@ -299,12 +277,6 @@
         return getAppCompatCapability().shouldUseDisplayLandscapeNaturalOrientation();
     }
 
-    @ScreenOrientation
-    int overrideOrientationIfNeeded(@ScreenOrientation int candidate) {
-        return mActivityRecord.mAppCompatController.getOrientationPolicy()
-                .overrideOrientationIfNeeded(candidate);
-    }
-
     boolean isOverrideOrientationOnlyForCameraEnabled() {
         return getAppCompatCapability().isOverrideOrientationOnlyForCameraEnabled();
     }
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index c72087b..9b8c038 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -47,8 +47,6 @@
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.INVALID_DISPLAY;
 import static android.view.SurfaceControl.METADATA_TASK_ID;
-import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
-import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
 import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
 import static android.view.WindowManager.TRANSIT_CHANGE;
@@ -3586,15 +3584,29 @@
                 ? null : new PictureInPictureParams(top.pictureInPictureArgs);
     }
 
-    Rect getDisplayCutoutInsets() {
-        if (mDisplayContent == null || getDisplayInfo().displayCutout == null) return null;
+    /** @return The display cutout insets where the main window is not allowed to extend to. */
+    @NonNull Rect getDisplayCutoutInsets() {
+        final Rect displayCutoutInsets = new Rect();
+        if (mDisplayContent == null || getDisplayInfo().displayCutout == null) {
+            return displayCutoutInsets;
+        }
         final WindowState w = getTopVisibleAppMainWindow();
-        final int displayCutoutMode = w == null
-                ? WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
-                : w.getAttrs().layoutInDisplayCutoutMode;
-        return (displayCutoutMode == LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
-                || displayCutoutMode == LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES)
-                ? null : getDisplayInfo().displayCutout.getSafeInsets();
+        final Rect displayFrame;
+        if (w != null && w.mHaveFrame) {
+            displayFrame = w.getDisplayFrame();
+        } else {
+            displayFrame = mDisplayContent.getBounds();
+            displayFrame.inset(getDisplayInfo().displayCutout.getSafeInsets());
+        }
+        final Rect taskBounds = getBounds();
+        if (displayCutoutInsets.setIntersect(taskBounds, displayFrame)) {
+            displayCutoutInsets.set(
+                    displayCutoutInsets.left - taskBounds.left,
+                    displayCutoutInsets.top - taskBounds.top,
+                    taskBounds.right - displayCutoutInsets.right,
+                    taskBounds.bottom - displayCutoutInsets.bottom);
+        }
+        return displayCutoutInsets;
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 7f6499f..acdb66a 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -29,6 +29,7 @@
 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.isFloating;
 import static android.content.pm.ActivityInfo.FLAG_ALLOW_UNTRUSTED_ACTIVITY_EMBEDDING;
 import static android.content.pm.ActivityInfo.FLAG_RESUME_WHILE_PAUSING;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET;
@@ -2248,8 +2249,10 @@
         void resolveTmpOverrides(DisplayContent dc, Configuration parentConfig,
                 boolean isFixedRotationTransforming) {
             mParentAppBoundsOverride = new Rect(parentConfig.windowConfiguration.getAppBounds());
+            mTmpOverrideConfigOrientation = parentConfig.orientation;
             final Insets insets;
-            if (mUseOverrideInsetsForConfig && dc != null) {
+            if (mUseOverrideInsetsForConfig && dc != null
+                    && !isFloating(parentConfig.windowConfiguration.getWindowingMode())) {
                 // Insets are decoupled from configuration by default from V+, use legacy
                 // compatibility behaviour for apps targeting SDK earlier than 35
                 // (see applySizeOverrideIfNeeded).
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 7e61023..2572128 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -2544,9 +2544,9 @@
             if (wc.asWindowState() != null) continue;
 
             final ChangeInfo changeInfo = changes.get(wc);
-
-            // Reject no-ops
-            if (!changeInfo.hasChanged()) {
+            // Reject no-ops, unless wallpaper
+            if (!changeInfo.hasChanged()
+                    && (!Flags.ensureWallpaperInTransitions() || wc.asWallpaperToken() == null)) {
                 ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
                         "  Rejecting as no-op: %s", wc);
                 continue;
diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java
index 3e43f5a..86440ac 100644
--- a/services/core/java/com/android/server/wm/WallpaperController.java
+++ b/services/core/java/com/android/server/wm/WallpaperController.java
@@ -60,6 +60,7 @@
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.internal.util.ToBooleanFunction;
 import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils;
+import com.android.window.flags.Flags;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -764,10 +765,19 @@
 
     void collectTopWallpapers(Transition transition) {
         if (mFindResults.hasTopShowWhenLockedWallpaper()) {
-            transition.collect(mFindResults.mTopWallpaper.mTopShowWhenLockedWallpaper);
+            if (Flags.ensureWallpaperInTransitions()) {
+                transition.collect(mFindResults.mTopWallpaper.mTopShowWhenLockedWallpaper.mToken);
+            } else {
+                transition.collect(mFindResults.mTopWallpaper.mTopShowWhenLockedWallpaper);
+            }
+
         }
         if (mFindResults.hasTopHideWhenLockedWallpaper()) {
-            transition.collect(mFindResults.mTopWallpaper.mTopHideWhenLockedWallpaper);
+            if (Flags.ensureWallpaperInTransitions()) {
+                transition.collect(mFindResults.mTopWallpaper.mTopHideWhenLockedWallpaper.mToken);
+            } else {
+                transition.collect(mFindResults.mTopWallpaper.mTopHideWhenLockedWallpaper);
+            }
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 6dbd259..1f31af6 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -3986,6 +3986,19 @@
     }
 
     /**
+     * Returns {@code true} if this window container belongs to a different sync group than the
+     * given group.
+     */
+    boolean isDifferentSyncGroup(@Nullable BLASTSyncEngine.SyncGroup group) {
+        if (group == null) return false;
+        final BLASTSyncEngine.SyncGroup thisGroup = getSyncGroup();
+        if (thisGroup == null || group == thisGroup) return false;
+        Slog.d(TAG, this + " uses a different SyncGroup, current=" + thisGroup.mSyncId
+                + " given=" + group.mSyncId);
+        return true;
+    }
+
+    /**
      * Recursively finishes/cleans-up sync state of this subtree and collects all the sync
      * transactions into `outMergedTransaction`.
      * @param outMergedTransaction A transaction to merge all the recorded sync operations into.
@@ -3994,10 +4007,14 @@
      */
     void finishSync(Transaction outMergedTransaction, @Nullable BLASTSyncEngine.SyncGroup group,
             boolean cancel) {
-        if (mSyncState == SYNC_STATE_NONE) return;
-        final BLASTSyncEngine.SyncGroup syncGroup = getSyncGroup();
-        // If it's null, then we need to clean-up anyways.
-        if (syncGroup != null && group != syncGroup) return;
+        if (mSyncState == SYNC_STATE_NONE) {
+            if (mSyncGroup != null) {
+                Slog.e(TAG, "finishSync: stale group " + mSyncGroup.mSyncId + " of " + this);
+                mSyncGroup = null;
+            }
+            return;
+        }
+        if (isDifferentSyncGroup(group)) return;
         ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "finishSync cancel=%b for %s", cancel, this);
         outMergedTransaction.merge(mSyncTransaction);
         for (int i = mChildren.size() - 1; i >= 0; --i) {
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index dcd4bd6..9d4a3b8 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -96,6 +96,7 @@
 import static android.view.WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME;
 import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_MULTIPLIER;
 import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_OFFSET;
+import static android.util.SequenceUtils.getNextSeq;
 
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ADD_REMOVE;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ANIM;
@@ -3652,6 +3653,7 @@
             }
         }
         outFrames.compatScale = getCompatScaleForClient();
+        outFrames.seq = getNextSeq(mLastReportedFrames.seq);
         if (mLastReportedFrames != outFrames) {
             mLastReportedFrames.setTo(outFrames);
         }
@@ -3682,7 +3684,9 @@
     }
 
     void fillInsetsState(@NonNull InsetsState outInsetsState, boolean copySources) {
+        final int lastSeq = mLastReportedInsetsState.getSeq();
         outInsetsState.set(getCompatInsetsState(), copySources);
+        outInsetsState.setSeq(getNextSeq(lastSeq));
         if (outInsetsState != mLastReportedInsetsState) {
             // No need to copy for the recorded.
             mLastReportedInsetsState.set(outInsetsState, false /* copySources */);
@@ -3691,9 +3695,11 @@
 
     void fillInsetsSourceControls(@NonNull InsetsSourceControl.Array outArray,
             boolean copyControls) {
+        final int lastSeq = mLastReportedInsetsState.getSeq();
         final InsetsSourceControl[] controls =
                 getDisplayContent().getInsetsStateController().getControlsForDispatch(this);
         outArray.set(controls, copyControls);
+        outArray.setSeq(getNextSeq(lastSeq));
         if (outArray != mLastReportedActiveControls) {
             // No need to copy for the recorded.
             mLastReportedActiveControls.setTo(outArray, false /* copyControls */);
@@ -5791,8 +5797,7 @@
     @Override
     void finishSync(Transaction outMergedTransaction, BLASTSyncEngine.SyncGroup group,
             boolean cancel) {
-        final BLASTSyncEngine.SyncGroup syncGroup = getSyncGroup();
-        if (syncGroup != null && group != syncGroup) return;
+        if (isDifferentSyncGroup(group)) return;
         mPrepareSyncSeqId = 0;
         if (cancel) {
             // This is leaving sync so any buffers left in the sync have a chance of
diff --git a/services/fakes/Android.bp b/services/fakes/Android.bp
index 148054b..d44bb5a 100644
--- a/services/fakes/Android.bp
+++ b/services/fakes/Android.bp
@@ -16,5 +16,5 @@
         "java/**/*.java",
     ],
     path: "java",
-    visibility: ["//frameworks/base"],
+    visibility: ["//frameworks/base/ravenwood:__subpackages__"],
 }
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
index a4ca317..d4adba2 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
@@ -87,7 +87,7 @@
         synchronized (ImfLock.class) {
             mVisibilityApplier.performShowIme(new Binder() /* showInputToken */,
                     ImeTracker.Token.empty(), 0 /* showFlags */, null /* resultReceiver */,
-                    SHOW_SOFT_INPUT);
+                    SHOW_SOFT_INPUT, mUserId);
         }
         verifyShowSoftInput(false, true, 0 /* showFlags */);
     }
@@ -96,7 +96,7 @@
     public void testPerformHideIme() throws Exception {
         synchronized (ImfLock.class) {
             mVisibilityApplier.performHideIme(new Binder() /* hideInputToken */,
-                    ImeTracker.Token.empty(), null /* resultReceiver */, HIDE_SOFT_INPUT);
+                    ImeTracker.Token.empty(), null /* resultReceiver */, HIDE_SOFT_INPUT, mUserId);
         }
         verifyHideSoftInput(false, true);
     }
@@ -186,7 +186,7 @@
     @Test
     public void testShowImeScreenshot() {
         synchronized (ImfLock.class) {
-            mVisibilityApplier.showImeScreenshot(mWindowToken, Display.DEFAULT_DISPLAY);
+            mVisibilityApplier.showImeScreenshot(mWindowToken, Display.DEFAULT_DISPLAY, mUserId);
         }
 
         verify(mMockImeTargetVisibilityPolicy).showImeScreenshot(eq(mWindowToken),
@@ -196,7 +196,7 @@
     @Test
     public void testRemoveImeScreenshot() {
         synchronized (ImfLock.class) {
-            mVisibilityApplier.removeImeScreenshot(Display.DEFAULT_DISPLAY);
+            mVisibilityApplier.removeImeScreenshot(Display.DEFAULT_DISPLAY, mUserId);
         }
 
         verify(mMockImeTargetVisibilityPolicy).removeImeScreenshot(eq(Display.DEFAULT_DISPLAY));
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
index 02b7291..e81cf9d 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
@@ -38,7 +38,6 @@
 import org.junit.Test;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.List;
 
 public final class InputMethodSubtypeSwitchingControllerTest {
@@ -65,17 +64,17 @@
     private static void addTestImeSubtypeListItems(@NonNull List<ImeSubtypeListItem> items,
             @NonNull String imeName, @NonNull String imeLabel,
             @Nullable List<String> subtypeLocales, boolean supportsSwitchingToNextInputMethod) {
-        final ResolveInfo ri = new ResolveInfo();
-        final ServiceInfo si = new ServiceInfo();
         final ApplicationInfo ai = new ApplicationInfo();
         ai.packageName = TEST_PACKAGE_NAME;
         ai.enabled = true;
+        final ServiceInfo si = new ServiceInfo();
         si.applicationInfo = ai;
         si.enabled = true;
         si.packageName = TEST_PACKAGE_NAME;
         si.name = imeName;
         si.exported = true;
         si.nonLocalizedLabel = imeLabel;
+        final ResolveInfo ri = new ResolveInfo();
         ri.serviceInfo = si;
         List<InputMethodSubtype> subtypes = null;
         if (subtypeLocales != null) {
@@ -102,8 +101,7 @@
     @NonNull
     private static ImeSubtypeListItem createTestItem(@NonNull ComponentName imeComponentName,
             @NonNull String imeName, @NonNull String subtypeName,
-            @NonNull String subtypeLocale, int subtypeIndex,
-            @NonNull String systemLocale) {
+            @NonNull String subtypeLocale, int subtypeIndex) {
         final var ai = new ApplicationInfo();
         ai.packageName = imeComponentName.getPackageName();
         ai.enabled = true;
@@ -125,26 +123,26 @@
                 .build());
         final InputMethodInfo imi = new InputMethodInfo(ri, TEST_IS_AUX_IME,
                 TEST_SETTING_ACTIVITY_NAME, subtypes, TEST_IS_DEFAULT_RES_ID,
-                TEST_FORCE_DEFAULT, true /* supportsSwitchingToNextInputMethod */,
-                TEST_IS_VR_IME);
+                TEST_FORCE_DEFAULT, true /* supportsSwitchingToNextInputMethod */, TEST_IS_VR_IME);
         return new ImeSubtypeListItem(imeName, subtypeName, imi, subtypeIndex, subtypeLocale,
-                systemLocale);
+                SYSTEM_LOCALE);
     }
 
     @NonNull
     private static List<ImeSubtypeListItem> createEnabledImeSubtypes() {
         final var items = new ArrayList<ImeSubtypeListItem>();
-        addTestImeSubtypeListItems(items, "LatinIme", "LatinIme", Arrays.asList("en_US", "fr"),
+        addTestImeSubtypeListItems(items, "LatinIme", "LatinIme", List.of("en_US", "fr"),
                 true /* supportsSwitchingToNextInputMethod*/);
         addTestImeSubtypeListItems(items, "switchUnawareLatinIme", "switchUnawareLatinIme",
-                Arrays.asList("en_UK", "hi"),
-                false /* supportsSwitchingToNextInputMethod*/);
+                List.of("en_UK", "hi"), false /* supportsSwitchingToNextInputMethod*/);
+        addTestImeSubtypeListItems(items, "subtypeAwareIme", "subtypeAwareIme", null,
+                true /* supportsSwitchingToNextInputMethod */);
         addTestImeSubtypeListItems(items, "subtypeUnawareIme", "subtypeUnawareIme", null,
                 false /* supportsSwitchingToNextInputMethod*/);
-        addTestImeSubtypeListItems(items, "JapaneseIme", "JapaneseIme", Arrays.asList("ja_JP"),
+        addTestImeSubtypeListItems(items, "JapaneseIme", "JapaneseIme", List.of("ja_JP"),
                 true /* supportsSwitchingToNextInputMethod*/);
         addTestImeSubtypeListItems(items, "switchUnawareJapaneseIme", "switchUnawareJapaneseIme",
-                Arrays.asList("ja_JP"), false /* supportsSwitchingToNextInputMethod*/);
+                List.of("ja_JP"), false /* supportsSwitchingToNextInputMethod*/);
         return items;
     }
 
@@ -153,11 +151,11 @@
         final var items = new ArrayList<ImeSubtypeListItem>();
         addTestImeSubtypeListItems(items,
                 "UnknownIme", "UnknownIme",
-                Arrays.asList("en_US", "hi"),
+                List.of("en_US", "hi"),
                 true /* supportsSwitchingToNextInputMethod*/);
         addTestImeSubtypeListItems(items,
                 "UnknownSwitchingUnawareIme", "UnknownSwitchingUnawareIme",
-                Arrays.asList("en_US"),
+                List.of("en_US"),
                 false /* supportsSwitchingToNextInputMethod*/);
         addTestImeSubtypeListItems(items, "UnknownSubtypeUnawareIme",
                 "UnknownSubtypeUnawareIme", null,
@@ -209,16 +207,17 @@
         final ImeSubtypeListItem latinIme_fr = enabledItems.get(1);
         final ImeSubtypeListItem switchingUnawareLatinIme_en_uk = enabledItems.get(2);
         final ImeSubtypeListItem switchingUnawareLatinIme_hi = enabledItems.get(3);
-        final ImeSubtypeListItem subtypeUnawareIme = enabledItems.get(4);
-        final ImeSubtypeListItem japaneseIme_ja_jp = enabledItems.get(5);
-        final ImeSubtypeListItem switchUnawareJapaneseIme_ja_jp = enabledItems.get(6);
+        final ImeSubtypeListItem subtypeAwareIme = enabledItems.get(4);
+        final ImeSubtypeListItem subtypeUnawareIme = enabledItems.get(5);
+        final ImeSubtypeListItem japaneseIme_ja_jp = enabledItems.get(6);
+        final ImeSubtypeListItem switchUnawareJapaneseIme_ja_jp = enabledItems.get(7);
 
         final ControllerImpl controller = ControllerImpl.createFrom(
                 null /* currentInstance */, enabledItems);
 
         // switching-aware loop
         assertRotationOrder(controller, false /* onlyCurrentIme */,
-                latinIme_en_us, latinIme_fr, japaneseIme_ja_jp);
+                latinIme_en_us, latinIme_fr, subtypeAwareIme, japaneseIme_ja_jp);
 
         // switching-unaware loop
         assertRotationOrder(controller, false /* onlyCurrentIme */,
@@ -231,6 +230,8 @@
         assertRotationOrder(controller, true /* onlyCurrentIme */,
                 switchingUnawareLatinIme_en_uk, switchingUnawareLatinIme_hi);
         assertNextInputMethod(controller, true /* onlyCurrentIme */,
+                subtypeAwareIme, null);
+        assertNextInputMethod(controller, true /* onlyCurrentIme */,
                 subtypeUnawareIme, null);
         assertNextInputMethod(controller, true /* onlyCurrentIme */,
                 japaneseIme_ja_jp, null);
@@ -261,55 +262,56 @@
         final List<ImeSubtypeListItem> enabledItems = createEnabledImeSubtypes();
         final ImeSubtypeListItem latinIme_en_us = enabledItems.get(0);
         final ImeSubtypeListItem latinIme_fr = enabledItems.get(1);
-        final ImeSubtypeListItem switchingUnawarelatinIme_en_uk = enabledItems.get(2);
-        final ImeSubtypeListItem switchingUnawarelatinIme_hi = enabledItems.get(3);
-        final ImeSubtypeListItem subtypeUnawareIme = enabledItems.get(4);
-        final ImeSubtypeListItem japaneseIme_ja_jp = enabledItems.get(5);
-        final ImeSubtypeListItem switchUnawareJapaneseIme_ja_jp = enabledItems.get(6);
+        final ImeSubtypeListItem switchingUnawareLatinIme_en_uk = enabledItems.get(2);
+        final ImeSubtypeListItem switchingUnawareLatinIme_hi = enabledItems.get(3);
+        final ImeSubtypeListItem subtypeAwareIme = enabledItems.get(4);
+        final ImeSubtypeListItem subtypeUnawareIme = enabledItems.get(5);
+        final ImeSubtypeListItem japaneseIme_ja_jp = enabledItems.get(6);
+        final ImeSubtypeListItem switchUnawareJapaneseIme_ja_jp = enabledItems.get(7);
 
         final ControllerImpl controller = ControllerImpl.createFrom(
                 null /* currentInstance */, enabledItems);
 
         // === switching-aware loop ===
         assertRotationOrder(controller, false /* onlyCurrentIme */,
-                latinIme_en_us, latinIme_fr, japaneseIme_ja_jp);
+                latinIme_en_us, latinIme_fr, subtypeAwareIme, japaneseIme_ja_jp);
         // Then notify that a user did something for latinIme_fr.
         onUserAction(controller, latinIme_fr);
         assertRotationOrder(controller, false /* onlyCurrentIme */,
-                latinIme_fr, latinIme_en_us, japaneseIme_ja_jp);
+                latinIme_fr, latinIme_en_us, subtypeAwareIme, japaneseIme_ja_jp);
         // Then notify that a user did something for latinIme_fr again.
         onUserAction(controller, latinIme_fr);
         assertRotationOrder(controller, false /* onlyCurrentIme */,
-                latinIme_fr, latinIme_en_us, japaneseIme_ja_jp);
-        // Then notify that a user did something for japaneseIme_ja_JP.
-        onUserAction(controller, latinIme_fr);
+                latinIme_fr, latinIme_en_us, subtypeAwareIme, japaneseIme_ja_jp);
+        // Then notify that a user did something for subtypeAwareIme.
+        onUserAction(controller, subtypeAwareIme);
         assertRotationOrder(controller, false /* onlyCurrentIme */,
-                japaneseIme_ja_jp, latinIme_fr, latinIme_en_us);
+                subtypeAwareIme, latinIme_fr, latinIme_en_us, japaneseIme_ja_jp);
         // Check onlyCurrentIme == true.
-        assertNextInputMethod(controller, true /* onlyCurrentIme */,
-                japaneseIme_ja_jp, null);
         assertRotationOrder(controller, true /* onlyCurrentIme */,
                 latinIme_fr, latinIme_en_us);
-        assertRotationOrder(controller, true /* onlyCurrentIme */,
-                latinIme_en_us, latinIme_fr);
+        assertNextInputMethod(controller, true /* onlyCurrentIme */,
+                subtypeAwareIme, null);
+        assertNextInputMethod(controller, true /* onlyCurrentIme */,
+                japaneseIme_ja_jp, null);
 
         // === switching-unaware loop ===
         assertRotationOrder(controller, false /* onlyCurrentIme */,
-                switchingUnawarelatinIme_en_uk, switchingUnawarelatinIme_hi, subtypeUnawareIme,
+                switchingUnawareLatinIme_en_uk, switchingUnawareLatinIme_hi, subtypeUnawareIme,
                 switchUnawareJapaneseIme_ja_jp);
         // User action should be ignored for switching unaware IMEs.
-        onUserAction(controller, switchingUnawarelatinIme_hi);
+        onUserAction(controller, switchingUnawareLatinIme_hi);
         assertRotationOrder(controller, false /* onlyCurrentIme */,
-                switchingUnawarelatinIme_en_uk, switchingUnawarelatinIme_hi, subtypeUnawareIme,
+                switchingUnawareLatinIme_en_uk, switchingUnawareLatinIme_hi, subtypeUnawareIme,
                 switchUnawareJapaneseIme_ja_jp);
         // User action should be ignored for switching unaware IMEs.
-        onUserAction(controller, switchUnawareJapaneseIme_ja_jp);
+        onUserAction(controller, subtypeUnawareIme);
         assertRotationOrder(controller, false /* onlyCurrentIme */,
-                switchingUnawarelatinIme_en_uk, switchingUnawarelatinIme_hi, subtypeUnawareIme,
+                switchingUnawareLatinIme_en_uk, switchingUnawareLatinIme_hi, subtypeUnawareIme,
                 switchUnawareJapaneseIme_ja_jp);
         // Check onlyCurrentIme == true.
         assertRotationOrder(controller, true /* onlyCurrentIme */,
-                switchingUnawarelatinIme_en_uk, switchingUnawarelatinIme_hi);
+                switchingUnawareLatinIme_en_uk, switchingUnawareLatinIme_hi);
         assertNextInputMethod(controller, true /* onlyCurrentIme */,
                 subtypeUnawareIme, null);
         assertNextInputMethod(controller, true /* onlyCurrentIme */,
@@ -320,28 +322,28 @@
         final ControllerImpl newController = ControllerImpl.createFrom(controller,
                 sameEnabledItems);
         assertRotationOrder(newController, false /* onlyCurrentIme */,
-                japaneseIme_ja_jp, latinIme_fr, latinIme_en_us);
+                subtypeAwareIme, latinIme_fr, latinIme_en_us, japaneseIme_ja_jp);
         assertRotationOrder(newController, false /* onlyCurrentIme */,
-                switchingUnawarelatinIme_en_uk, switchingUnawarelatinIme_hi, subtypeUnawareIme,
+                switchingUnawareLatinIme_en_uk, switchingUnawareLatinIme_hi, subtypeUnawareIme,
                 switchUnawareJapaneseIme_ja_jp);
 
         // Rotation order should be initialized when created with a different subtype list.
-        final List<ImeSubtypeListItem> differentEnabledItems = Arrays.asList(
-                latinIme_en_us, latinIme_fr, switchingUnawarelatinIme_en_uk,
-                switchUnawareJapaneseIme_ja_jp);
+        final List<ImeSubtypeListItem> differentEnabledItems = List.of(
+                latinIme_en_us, latinIme_fr, subtypeAwareIme, switchingUnawareLatinIme_en_uk,
+                switchUnawareJapaneseIme_ja_jp, subtypeUnawareIme);
         final ControllerImpl anotherController = ControllerImpl.createFrom(controller,
                 differentEnabledItems);
         assertRotationOrder(anotherController, false /* onlyCurrentIme */,
-                latinIme_en_us, latinIme_fr);
+                latinIme_en_us, latinIme_fr, subtypeAwareIme);
         assertRotationOrder(anotherController, false /* onlyCurrentIme */,
-                switchingUnawarelatinIme_en_uk, switchUnawareJapaneseIme_ja_jp);
+                switchingUnawareLatinIme_en_uk, switchUnawareJapaneseIme_ja_jp, subtypeUnawareIme);
     }
 
     @Test
     public void testImeSubtypeListItem() {
         final var items = new ArrayList<ImeSubtypeListItem>();
         addTestImeSubtypeListItems(items, "LatinIme", "LatinIme",
-                Arrays.asList("en_US", "fr", "en", "en_uk", "enn", "e", "EN_US"),
+                List.of("en_US", "fr", "en", "en_uk", "enn", "e", "EN_US"),
                 true /* supportsSwitchingToNextInputMethod*/);
         final ImeSubtypeListItem item_en_us = items.get(0);
         final ImeSubtypeListItem item_fr = items.get(1);
@@ -376,61 +378,61 @@
         final ComponentName imeY1 = new ComponentName("com.example.imeY", "Ime1");
         final ComponentName imeZ1 = new ComponentName("com.example.imeZ", "Ime1");
         {
-            final List<ImeSubtypeListItem> items = Arrays.asList(
+            final List<ImeSubtypeListItem> items = List.of(
                     // Subtypes of two IMEs that have the same display name "X".
                     // Subtypes that has the same locale of the system's.
-                    createTestItem(imeX1, "X", "E", "en_US", 0, "en_US"),
-                    createTestItem(imeX2, "X", "E", "en_US", 0, "en_US"),
-                    createTestItem(imeX1, "X", "Z", "en_US", 3, "en_US"),
-                    createTestItem(imeX2, "X", "Z", "en_US", 3, "en_US"),
-                    createTestItem(imeX1, "X", "", "en_US", 6, "en_US"),
-                    createTestItem(imeX2, "X", "", "en_US", 6, "en_US"),
+                    createTestItem(imeX1, "X", "E", "en_US", 0),
+                    createTestItem(imeX2, "X", "E", "en_US", 0),
+                    createTestItem(imeX1, "X", "Z", "en_US", 3),
+                    createTestItem(imeX2, "X", "Z", "en_US", 3),
+                    createTestItem(imeX1, "X", "", "en_US", 6),
+                    createTestItem(imeX2, "X", "", "en_US", 6),
                     // Subtypes that has the same language of the system's.
-                    createTestItem(imeX1, "X", "E", "en", 1, "en_US"),
-                    createTestItem(imeX2, "X", "E", "en", 1, "en_US"),
-                    createTestItem(imeX1, "X", "Z", "en", 4, "en_US"),
-                    createTestItem(imeX2, "X", "Z", "en", 4, "en_US"),
-                    createTestItem(imeX1, "X", "", "en", 7, "en_US"),
-                    createTestItem(imeX2, "X", "", "en", 7, "en_US"),
+                    createTestItem(imeX1, "X", "E", "en", 1),
+                    createTestItem(imeX2, "X", "E", "en", 1),
+                    createTestItem(imeX1, "X", "Z", "en", 4),
+                    createTestItem(imeX2, "X", "Z", "en", 4),
+                    createTestItem(imeX1, "X", "", "en", 7),
+                    createTestItem(imeX2, "X", "", "en", 7),
                     // Subtypes that has different language than the system's.
-                    createTestItem(imeX1, "X", "A", "hi_IN", 27, "en_US"),
-                    createTestItem(imeX2, "X", "A", "hi_IN", 27, "en_US"),
-                    createTestItem(imeX1, "X", "E", "ja", 2, "en_US"),
-                    createTestItem(imeX2, "X", "E", "ja", 2, "en_US"),
-                    createTestItem(imeX1, "X", "Z", "ja", 5, "en_US"),
-                    createTestItem(imeX2, "X", "Z", "ja", 5, "en_US"),
-                    createTestItem(imeX1, "X", "", "ja", 8, "en_US"),
-                    createTestItem(imeX2, "X", "", "ja", 8, "en_US"),
+                    createTestItem(imeX1, "X", "A", "hi_IN", 27),
+                    createTestItem(imeX2, "X", "A", "hi_IN", 27),
+                    createTestItem(imeX1, "X", "E", "ja", 2),
+                    createTestItem(imeX2, "X", "E", "ja", 2),
+                    createTestItem(imeX1, "X", "Z", "ja", 5),
+                    createTestItem(imeX2, "X", "Z", "ja", 5),
+                    createTestItem(imeX1, "X", "", "ja", 8),
+                    createTestItem(imeX2, "X", "", "ja", 8),
 
                     // Subtypes of IME "Y".
                     // Subtypes that has the same locale of the system's.
-                    createTestItem(imeY1, "Y", "E", "en_US", 9, "en_US"),
-                    createTestItem(imeY1, "Y", "Z", "en_US", 12, "en_US"),
-                    createTestItem(imeY1, "Y", "", "en_US", 15, "en_US"),
+                    createTestItem(imeY1, "Y", "E", "en_US", 9),
+                    createTestItem(imeY1, "Y", "Z", "en_US", 12),
+                    createTestItem(imeY1, "Y", "", "en_US", 15),
                     // Subtypes that has the same language of the system's.
-                    createTestItem(imeY1, "Y", "E", "en", 10, "en_US"),
-                    createTestItem(imeY1, "Y", "Z", "en", 13, "en_US"),
-                    createTestItem(imeY1, "Y", "", "en", 16, "en_US"),
+                    createTestItem(imeY1, "Y", "E", "en", 10),
+                    createTestItem(imeY1, "Y", "Z", "en", 13),
+                    createTestItem(imeY1, "Y", "", "en", 16),
                     // Subtypes that has different language than the system's.
-                    createTestItem(imeY1, "Y", "A", "hi_IN", 28, "en_US"),
-                    createTestItem(imeY1, "Y", "E", "ja", 11, "en_US"),
-                    createTestItem(imeY1, "Y", "Z", "ja", 14, "en_US"),
-                    createTestItem(imeY1, "Y", "", "ja", 17, "en_US"),
+                    createTestItem(imeY1, "Y", "A", "hi_IN", 28),
+                    createTestItem(imeY1, "Y", "E", "ja", 11),
+                    createTestItem(imeY1, "Y", "Z", "ja", 14),
+                    createTestItem(imeY1, "Y", "", "ja", 17),
 
                     // Subtypes of IME Z.
                     // Subtypes that has the same locale of the system's.
-                    createTestItem(imeZ1, "", "E", "en_US", 18, "en_US"),
-                    createTestItem(imeZ1, "", "Z", "en_US", 21, "en_US"),
-                    createTestItem(imeZ1, "", "", "en_US", 24, "en_US"),
+                    createTestItem(imeZ1, "", "E", "en_US", 18),
+                    createTestItem(imeZ1, "", "Z", "en_US", 21),
+                    createTestItem(imeZ1, "", "", "en_US", 24),
                     // Subtypes that has the same language of the system's.
-                    createTestItem(imeZ1, "", "E", "en", 19, "en_US"),
-                    createTestItem(imeZ1, "", "Z", "en", 22, "en_US"),
-                    createTestItem(imeZ1, "", "", "en", 25, "en_US"),
+                    createTestItem(imeZ1, "", "E", "en", 19),
+                    createTestItem(imeZ1, "", "Z", "en", 22),
+                    createTestItem(imeZ1, "", "", "en", 25),
                     // Subtypes that has different language than the system's.
-                    createTestItem(imeZ1, "", "A", "hi_IN", 29, "en_US"),
-                    createTestItem(imeZ1, "", "E", "ja", 20, "en_US"),
-                    createTestItem(imeZ1, "", "Z", "ja", 23, "en_US"),
-                    createTestItem(imeZ1, "", "", "ja", 26, "en_US"));
+                    createTestItem(imeZ1, "", "A", "hi_IN", 29),
+                    createTestItem(imeZ1, "", "E", "ja", 20),
+                    createTestItem(imeZ1, "", "Z", "ja", 23),
+                    createTestItem(imeZ1, "", "", "ja", 26));
 
             // Ensure {@link java.lang.Comparable#compareTo} contracts are satisfied.
             for (int i = 0; i < items.size(); ++i) {
@@ -449,10 +451,8 @@
 
         {
             // Following two items have the same priority.
-            final ImeSubtypeListItem nonSystemLocale1 =
-                    createTestItem(imeX1, "X", "A", "ja_JP", 0, "en_US");
-            final ImeSubtypeListItem nonSystemLocale2 =
-                    createTestItem(imeX1, "X", "A", "hi_IN", 1, "en_US");
+            final ImeSubtypeListItem nonSystemLocale1 = createTestItem(imeX1, "X", "A", "ja_JP", 0);
+            final ImeSubtypeListItem nonSystemLocale2 = createTestItem(imeX1, "X", "A", "hi_IN", 1);
             assertEquals(0, nonSystemLocale1.compareTo(nonSystemLocale2));
             assertEquals(0, nonSystemLocale2.compareTo(nonSystemLocale1));
             // But those aren't equal to each other.
@@ -462,8 +462,8 @@
 
         {
             // Check if ComponentName is also taken into account when comparing two items.
-            final ImeSubtypeListItem ime1 = createTestItem(imeX1, "X", "A", "ja_JP", 0, "en_US");
-            final ImeSubtypeListItem ime2 = createTestItem(imeX2, "X", "A", "ja_JP", 0, "en_US");
+            final ImeSubtypeListItem ime1 = createTestItem(imeX1, "X", "A", "ja_JP", 0);
+            final ImeSubtypeListItem ime2 = createTestItem(imeX2, "X", "A", "ja_JP", 0);
             assertTrue(ime1.compareTo(ime2) < 0);
             assertTrue(ime2.compareTo(ime1) > 0);
             // But those aren't equal to each other.
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java
index 4409051..30c384a 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java
@@ -94,4 +94,11 @@
 
         verify(mDisplayOffloader).onBlockingScreenOn(eq(unblocker));
     }
+
+    @Test
+    public void testUnblockScreenOn() {
+        mSession.cancelBlockScreenOn();
+
+        verify(mDisplayOffloader).cancelBlockScreenOn();
+    }
 }
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
index 95f0b65..bb774ee 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -105,7 +105,6 @@
 
 import java.util.List;
 
-
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public final class DisplayPowerControllerTest {
@@ -1660,6 +1659,8 @@
         int initState = Display.STATE_OFF;
         mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
         mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession);
+        when(mDisplayOffloadSession.blockScreenOn(any())).thenReturn(true);
+
         // start with OFF.
         when(mHolder.displayPowerState.getScreenState()).thenReturn(initState);
         DisplayPowerRequest dpr = new DisplayPowerRequest();
@@ -1673,6 +1674,7 @@
         advanceTime(1); // Run updatePowerState
 
         verify(mDisplayOffloadSession).blockScreenOn(any(Runnable.class));
+        verify(mDisplayOffloadSession, never()).cancelBlockScreenOn();
     }
 
     @Test
@@ -1680,6 +1682,8 @@
         // set up.
         int initState = Display.STATE_ON;
         mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession);
+        when(mDisplayOffloadSession.blockScreenOn(any())).thenReturn(true);
+
         // start with ON.
         when(mHolder.displayPowerState.getScreenState()).thenReturn(initState);
         DisplayPowerRequest dpr = new DisplayPowerRequest();
@@ -1692,7 +1696,78 @@
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
         advanceTime(1); // Run updatePowerState
 
-        verify(mDisplayOffloadSession, never()).blockScreenOn(any(Runnable.class));
+        // No cancelBlockScreenOn call because we didn't block.
+        verify(mDisplayOffloadSession, never()).cancelBlockScreenOn();
+    }
+
+    @RequiresFlagsEnabled(Flags.FLAG_OFFLOAD_SESSION_CANCEL_BLOCK_SCREEN_ON)
+    @Test
+    public void testOffloadBlocker_turnON_thenOFF_cancelBlockScreenOnNotCalledIfUnblocked() {
+        // Set up.
+        int initState = Display.STATE_OFF;
+        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
+        mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession);
+        when(mDisplayOffloadSession.blockScreenOn(any())).thenReturn(true);
+
+        // Start with OFF.
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(initState);
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        dpr.policy = DisplayPowerRequest.POLICY_OFF;
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        // Go to ON.
+        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        ArgumentCaptor<Runnable> argumentCaptor = ArgumentCaptor.forClass(Runnable.class);
+        verify(mDisplayOffloadSession).blockScreenOn(argumentCaptor.capture());
+
+        // Unblocked
+        argumentCaptor.getValue().run();
+        advanceTime(1); // Run updatePowerState
+
+        // Go to OFF immediately
+        dpr.policy = DisplayPowerRequest.POLICY_OFF;
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        // No cancelBlockScreenOn call because we already unblocked
+        verify(mDisplayOffloadSession, never()).cancelBlockScreenOn();
+    }
+
+    @RequiresFlagsEnabled(Flags.FLAG_OFFLOAD_SESSION_CANCEL_BLOCK_SCREEN_ON)
+    @Test
+    public void testOffloadBlocker_turnON_thenOFF_cancelBlockScreenOn() {
+        // Set up.
+        int initState = Display.STATE_OFF;
+        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
+        mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession);
+        when(mDisplayOffloadSession.blockScreenOn(any())).thenReturn(true);
+
+        // Start with OFF.
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(initState);
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        dpr.policy = DisplayPowerRequest.POLICY_OFF;
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        // Go to ON.
+        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        // We should call blockScreenOn
+        verify(mDisplayOffloadSession).blockScreenOn(any(Runnable.class));
+
+        // Go to OFF immediately
+        dpr.policy = DisplayPowerRequest.POLICY_OFF;
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        // We should call cancelBlockScreenOn
+        verify(mDisplayOffloadSession).cancelBlockScreenOn();
     }
 
     @Test
diff --git a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
index 01ff35f..a7e0ebd 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
@@ -33,7 +33,6 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.when;
 
@@ -83,7 +82,6 @@
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
-
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class LocalDisplayAdapterTest {
@@ -126,7 +124,7 @@
 
     private DisplayOffloadSessionImpl mDisplayOffloadSession;
 
-    private DisplayOffloader mDisplayOffloader;
+    @Mock DisplayOffloader mDisplayOffloader;
 
     private TestListener mListener = new TestListener();
 
@@ -1249,24 +1247,8 @@
     }
 
     private void initDisplayOffloadSession() {
-        mDisplayOffloader = spy(new DisplayOffloader() {
-            @Override
-            public boolean startOffload() {
-                return true;
-            }
-
-            @Override
-            public void stopOffload() {}
-
-            @Override
-            public void onBlockingScreenOn(Runnable unblocker) {}
-
-            @Override
-            public boolean allowAutoBrightnessInDoze() {
-                return true;
-            }
-        });
-
+        when(mDisplayOffloader.startOffload()).thenReturn(true);
+        when(mDisplayOffloader.allowAutoBrightnessInDoze()).thenReturn(true);
         mDisplayOffloadSession = new DisplayOffloadSessionImpl(mDisplayOffloader,
                 mMockedDisplayPowerController);
     }
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy2Test.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy2Test.java
index 498bffd..4e10b98 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy2Test.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy2Test.java
@@ -192,7 +192,7 @@
     }
 
     @Test
-    public void testAutoBrightnessState_DisplayIsInDoze_ConfigDoesAllow() {
+    public void testAutoBrightnessState_DeviceIsInDoze_ConfigDoesAllow() {
         mAutomaticBrightnessStrategy.setUseAutoBrightness(true);
         int targetDisplayState = Display.STATE_DOZE;
         boolean allowAutoBrightnessWhileDozing = true;
@@ -218,6 +218,32 @@
     }
 
     @Test
+    public void testAutoBrightnessState_DeviceIsInDoze_ConfigDoesAllow_ScreenOff() {
+        mAutomaticBrightnessStrategy.setUseAutoBrightness(true);
+        int targetDisplayState = Display.STATE_OFF;
+        boolean allowAutoBrightnessWhileDozing = true;
+        int brightnessReason = BrightnessReason.REASON_UNKNOWN;
+        int policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE;
+        float lastUserSetBrightness = 0.2f;
+        boolean userSetBrightnessChanged = true;
+        Settings.System.putFloat(mContext.getContentResolver(),
+                Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0.4f);
+        mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments();
+        mAutomaticBrightnessStrategy.setAutoBrightnessState(targetDisplayState,
+                allowAutoBrightnessWhileDozing, brightnessReason, policy, lastUserSetBrightness,
+                userSetBrightnessChanged);
+        verify(mAutomaticBrightnessController)
+                .configure(AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE,
+                        mBrightnessConfiguration,
+                        lastUserSetBrightness,
+                        userSetBrightnessChanged, /* adjustment */ 0.4f,
+                        /* userChangedAutoBrightnessAdjustment= */ true, policy,
+                        targetDisplayState, /* shouldResetShortTermModel */ true);
+        assertFalse(mAutomaticBrightnessStrategy.isAutoBrightnessEnabled());
+        assertTrue(mAutomaticBrightnessStrategy.isAutoBrightnessDisabledDueToDisplayOff());
+    }
+
+    @Test
     public void testAutoBrightnessState_DisplayIsOn() {
         mAutomaticBrightnessStrategy.setUseAutoBrightness(true);
         int targetDisplayState = Display.STATE_ON;
@@ -245,6 +271,33 @@
     }
 
     @Test
+    public void testAutoBrightnessState_DisplayIsOn_PolicyIsDoze() {
+        mAutomaticBrightnessStrategy.setUseAutoBrightness(true);
+        int targetDisplayState = Display.STATE_ON;
+        boolean allowAutoBrightnessWhileDozing = false;
+        int brightnessReason = BrightnessReason.REASON_UNKNOWN;
+        float lastUserSetBrightness = 0.2f;
+        boolean userSetBrightnessChanged = true;
+        int policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE;
+        float pendingBrightnessAdjustment = 0.1f;
+        Settings.System.putFloat(mContext.getContentResolver(),
+                Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, pendingBrightnessAdjustment);
+        mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments();
+        mAutomaticBrightnessStrategy.setAutoBrightnessState(targetDisplayState,
+                allowAutoBrightnessWhileDozing, brightnessReason, policy, lastUserSetBrightness,
+                userSetBrightnessChanged);
+        verify(mAutomaticBrightnessController)
+                .configure(AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE,
+                        mBrightnessConfiguration,
+                        lastUserSetBrightness,
+                        userSetBrightnessChanged, pendingBrightnessAdjustment,
+                        /* userChangedAutoBrightnessAdjustment= */ true, policy, targetDisplayState,
+                        /* shouldResetShortTermModel */ true);
+        assertFalse(mAutomaticBrightnessStrategy.isAutoBrightnessEnabled());
+        assertTrue(mAutomaticBrightnessStrategy.isAutoBrightnessDisabledDueToDisplayOff());
+    }
+
+    @Test
     public void accommodateUserBrightnessChangesWorksAsExpected() {
         // Verify the state if automaticBrightnessController is configured.
         assertFalse(mAutomaticBrightnessStrategy.isShortTermModelActive());
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java
index 1d04baa..e16377e 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java
@@ -202,7 +202,7 @@
     }
 
     @Test
-    public void testAutoBrightnessState_DisplayIsInDoze_ConfigDoesAllow() {
+    public void testAutoBrightnessState_DeviceIsInDoze_ConfigDoesAllow() {
         mAutomaticBrightnessStrategy.setUseAutoBrightness(true);
         int targetDisplayState = Display.STATE_DOZE;
         boolean allowAutoBrightnessWhileDozing = true;
@@ -228,6 +228,32 @@
     }
 
     @Test
+    public void testAutoBrightnessState_DeviceIsInDoze_ConfigDoesAllow_ScreenOff() {
+        mAutomaticBrightnessStrategy.setUseAutoBrightness(true);
+        int targetDisplayState = Display.STATE_OFF;
+        boolean allowAutoBrightnessWhileDozing = true;
+        int brightnessReason = BrightnessReason.REASON_UNKNOWN;
+        int policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE;
+        float lastUserSetBrightness = 0.2f;
+        boolean userSetBrightnessChanged = true;
+        Settings.System.putFloat(mContext.getContentResolver(),
+                Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0.4f);
+        mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments();
+        mAutomaticBrightnessStrategy.setAutoBrightnessState(targetDisplayState,
+                allowAutoBrightnessWhileDozing, brightnessReason, policy, lastUserSetBrightness,
+                userSetBrightnessChanged);
+        verify(mAutomaticBrightnessController)
+                .configure(AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE,
+                        mBrightnessConfiguration,
+                        lastUserSetBrightness,
+                        userSetBrightnessChanged, /* adjustment */ 0.4f,
+                        /* userChangedAutoBrightnessAdjustment= */ true, policy,
+                        targetDisplayState, /* shouldResetShortTermModel */ true);
+        assertFalse(mAutomaticBrightnessStrategy.isAutoBrightnessEnabled());
+        assertTrue(mAutomaticBrightnessStrategy.isAutoBrightnessDisabledDueToDisplayOff());
+    }
+
+    @Test
     public void testAutoBrightnessState_DisplayIsOn() {
         mAutomaticBrightnessStrategy.setUseAutoBrightness(true);
         int targetDisplayState = Display.STATE_ON;
@@ -255,6 +281,33 @@
     }
 
     @Test
+    public void testAutoBrightnessState_DisplayIsOn_PolicyIsDoze() {
+        mAutomaticBrightnessStrategy.setUseAutoBrightness(true);
+        int targetDisplayState = Display.STATE_ON;
+        boolean allowAutoBrightnessWhileDozing = false;
+        int brightnessReason = BrightnessReason.REASON_UNKNOWN;
+        float lastUserSetBrightness = 0.2f;
+        boolean userSetBrightnessChanged = true;
+        int policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE;
+        float pendingBrightnessAdjustment = 0.1f;
+        Settings.System.putFloat(mContext.getContentResolver(),
+                Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, pendingBrightnessAdjustment);
+        mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments();
+        mAutomaticBrightnessStrategy.setAutoBrightnessState(targetDisplayState,
+                allowAutoBrightnessWhileDozing, brightnessReason, policy, lastUserSetBrightness,
+                userSetBrightnessChanged);
+        verify(mAutomaticBrightnessController)
+                .configure(AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE,
+                        mBrightnessConfiguration,
+                        lastUserSetBrightness,
+                        userSetBrightnessChanged, pendingBrightnessAdjustment,
+                        /* userChangedAutoBrightnessAdjustment= */ true, policy, targetDisplayState,
+                        /* shouldResetShortTermModel */ true);
+        assertFalse(mAutomaticBrightnessStrategy.isAutoBrightnessEnabled());
+        assertTrue(mAutomaticBrightnessStrategy.isAutoBrightnessDisabledDueToDisplayOff());
+    }
+
+    @Test
     public void testAutoBrightnessState_modeSwitch() {
         // Setup the test
         when(mDisplayManagerFlags.areAutoBrightnessModesEnabled()).thenReturn(true);
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
index 4e8c755..9884085 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
@@ -25,6 +25,8 @@
 
 import static com.android.internal.accessibility.AccessibilityShortcutController.ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME;
 import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME;
+import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE;
+import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS;
 import static com.android.server.accessibility.AccessibilityManagerService.ACTION_LAUNCH_HEARING_DEVICES_DIALOG;
 import static com.android.window.flags.Flags.FLAG_ALWAYS_DRAW_MAGNIFICATION_FULLSCREEN_BORDER;
 
@@ -1082,7 +1084,7 @@
 
         mA11yms.enableShortcutsForTargets(
                 /* enable= */ true,
-                UserShortcutType.HARDWARE,
+                HARDWARE,
                 List.of(target),
                 mA11yms.getCurrentUserIdLocked());
         mTestableLooper.processAllMessages();
@@ -1346,14 +1348,14 @@
 
         mA11yms.enableShortcutsForTargets(
                 /* enable= */ true,
-                UserShortcutType.HARDWARE,
+                HARDWARE,
                 List.of(TARGET_STANDARD_A11Y_SERVICE.flattenToString()),
                 mA11yms.getCurrentUserIdLocked());
         mTestableLooper.processAllMessages();
 
         assertThat(
                 ShortcutUtils.isComponentIdExistingInSettings(
-                        mTestableContext, ShortcutConstants.UserShortcutType.HARDWARE,
+                        mTestableContext, HARDWARE,
                         TARGET_STANDARD_A11Y_SERVICE.flattenToString())
         ).isTrue();
     }
@@ -1367,7 +1369,7 @@
 
         mA11yms.enableShortcutsForTargets(
                 /* enable= */ false,
-                UserShortcutType.HARDWARE,
+                HARDWARE,
                 List.of(TARGET_STANDARD_A11Y_SERVICE.flattenToString()),
                 mA11yms.getCurrentUserIdLocked());
         mTestableLooper.processAllMessages();
@@ -1375,7 +1377,7 @@
         assertThat(
                         ShortcutUtils.isComponentIdExistingInSettings(
                                 mTestableContext,
-                                ShortcutConstants.UserShortcutType.HARDWARE,
+                                HARDWARE,
                                 TARGET_STANDARD_A11Y_SERVICE.flattenToString()))
                 .isFalse();
     }
@@ -1390,14 +1392,14 @@
 
         mA11yms.enableShortcutsForTargets(
                 /* enable= */ true,
-                UserShortcutType.QUICK_SETTINGS,
+                QUICK_SETTINGS,
                 List.of(TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString()),
                 mA11yms.getCurrentUserIdLocked());
         mTestableLooper.processAllMessages();
 
         assertThat(
                 ShortcutUtils.isComponentIdExistingInSettings(
-                        mTestableContext, UserShortcutType.QUICK_SETTINGS,
+                        mTestableContext, QUICK_SETTINGS,
                         TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString())
         ).isTrue();
         verify(mStatusBarManagerInternal)
@@ -1417,14 +1419,14 @@
 
         mA11yms.enableShortcutsForTargets(
                 /* enable= */ false,
-                UserShortcutType.QUICK_SETTINGS,
+                QUICK_SETTINGS,
                 List.of(TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString()),
                 mA11yms.getCurrentUserIdLocked());
         mTestableLooper.processAllMessages();
 
         assertThat(
                 ShortcutUtils.isComponentIdExistingInSettings(
-                        mTestableContext, UserShortcutType.QUICK_SETTINGS,
+                        mTestableContext, QUICK_SETTINGS,
                         TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString())
         ).isFalse();
         verify(mStatusBarManagerInternal)
@@ -1614,44 +1616,49 @@
 
     @Test
     @EnableFlags(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT)
-    public void restoreAccessibilityQsTargets_a11yQsTargetsRestored() {
+    public void restoreShortcutTargets_qs_a11yQsTargetsRestored() {
         String daltonizerTile =
                 AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME.flattenToString();
         String colorInversionTile =
                 AccessibilityShortcutController.COLOR_INVERSION_COMPONENT_NAME.flattenToString();
         final AccessibilityUserState userState = new AccessibilityUserState(
                 UserHandle.USER_SYSTEM, mTestableContext, mA11yms);
-        userState.updateA11yQsTargetLocked(Set.of(daltonizerTile));
+        userState.updateShortcutTargetsLocked(Set.of(daltonizerTile), QUICK_SETTINGS);
         mA11yms.mUserStates.put(UserHandle.USER_SYSTEM, userState);
 
         broadcastSettingRestored(
-                Settings.Secure.ACCESSIBILITY_QS_TARGETS,
-                /*previousValue=*/null,
+                ShortcutUtils.convertToKey(QUICK_SETTINGS),
                 /*newValue=*/colorInversionTile);
 
-        assertThat(mA11yms.mUserStates.get(UserHandle.USER_SYSTEM).getA11yQsTargets())
-                .containsExactlyElementsIn(Set.of(daltonizerTile, colorInversionTile));
+        Set<String> expected = Set.of(daltonizerTile, colorInversionTile);
+        assertThat(readStringsFromSetting(ShortcutUtils.convertToKey(QUICK_SETTINGS)))
+                .containsExactlyElementsIn(expected);
+        assertThat(userState.getShortcutTargetsLocked(QUICK_SETTINGS))
+                .containsExactlyElementsIn(expected);
     }
 
     @Test
     @DisableFlags(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT)
-    public void restoreAccessibilityQsTargets_a11yQsTargetsNotRestored() {
+    public void restoreShortcutTargets_qs_a11yQsTargetsNotRestored() {
         String daltonizerTile =
                 AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME.flattenToString();
         String colorInversionTile =
                 AccessibilityShortcutController.COLOR_INVERSION_COMPONENT_NAME.flattenToString();
         final AccessibilityUserState userState = new AccessibilityUserState(
                 UserHandle.USER_SYSTEM, mTestableContext, mA11yms);
-        userState.updateA11yQsTargetLocked(Set.of(daltonizerTile));
+        userState.updateShortcutTargetsLocked(Set.of(daltonizerTile), QUICK_SETTINGS);
+        putShortcutSettingForUser(QUICK_SETTINGS, daltonizerTile, userState.mUserId);
         mA11yms.mUserStates.put(UserHandle.USER_SYSTEM, userState);
 
         broadcastSettingRestored(
-                Settings.Secure.ACCESSIBILITY_QS_TARGETS,
-                /*previousValue=*/null,
+                ShortcutUtils.convertToKey(QUICK_SETTINGS),
                 /*newValue=*/colorInversionTile);
 
-        assertThat(userState.getA11yQsTargets())
-                .containsExactlyElementsIn(Set.of(daltonizerTile));
+        Set<String> expected = Set.of(daltonizerTile);
+        assertThat(readStringsFromSetting(ShortcutUtils.convertToKey(QUICK_SETTINGS)))
+                .containsExactlyElementsIn(expected);
+        assertThat(userState.getShortcutTargetsLocked(QUICK_SETTINGS))
+                .containsExactlyElementsIn(expected);
     }
 
     @Test
@@ -1717,27 +1724,26 @@
 
     @Test
     @EnableFlags(android.view.accessibility.Flags.FLAG_RESTORE_A11Y_SHORTCUT_TARGET_SERVICE)
-    public void restoreA11yShortcutTargetService_targetsMerged() {
+    public void restoreShortcutTargets_hardware_targetsMerged() {
+        mFakePermissionEnforcer.grant(Manifest.permission.MANAGE_ACCESSIBILITY);
         final String servicePrevious = TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString();
         final String otherPrevious = TARGET_MAGNIFICATION;
-        final String combinedPrevious = String.join(":", servicePrevious, otherPrevious);
         final String serviceRestored = TARGET_STANDARD_A11Y_SERVICE.flattenToString();
         final AccessibilityUserState userState = new AccessibilityUserState(
                 UserHandle.USER_SYSTEM, mTestableContext, mA11yms);
         mA11yms.mUserStates.put(UserHandle.USER_SYSTEM, userState);
         setupShortcutTargetServices(userState);
+        mA11yms.enableShortcutsForTargets(
+                true, HARDWARE, List.of(servicePrevious, otherPrevious), userState.mUserId);
 
         broadcastSettingRestored(
-                Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE,
-                /*previousValue=*/combinedPrevious,
+                ShortcutUtils.convertToKey(HARDWARE),
                 /*newValue=*/serviceRestored);
 
         final Set<String> expected = Set.of(servicePrevious, otherPrevious, serviceRestored);
-        assertThat(readStringsFromSetting(
-                Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE))
+        assertThat(readStringsFromSetting(ShortcutUtils.convertToKey(HARDWARE)))
                 .containsExactlyElementsIn(expected);
-        assertThat(mA11yms.mUserStates.get(UserHandle.USER_SYSTEM)
-                .getShortcutTargetsLocked(UserShortcutType.HARDWARE))
+        assertThat(userState.getShortcutTargetsLocked(HARDWARE))
                 .containsExactlyElementsIn(expected);
     }
 
@@ -1745,7 +1751,7 @@
     @EnableFlags({
             android.view.accessibility.Flags.FLAG_RESTORE_A11Y_SHORTCUT_TARGET_SERVICE,
             Flags.FLAG_CLEAR_DEFAULT_FROM_A11Y_SHORTCUT_TARGET_SERVICE_RESTORE})
-    public void restoreA11yShortcutTargetService_alreadyHadDefaultService_doesNotClear() {
+    public void restoreShortcutTargets_hardware_alreadyHadDefaultService_doesNotClear() {
         final String serviceDefault = TARGET_STANDARD_A11Y_SERVICE.flattenToString();
         mTestableContext.getOrCreateTestableResources().addOverride(
                 R.string.config_defaultAccessibilityService, serviceDefault);
@@ -1754,17 +1760,18 @@
         mA11yms.mUserStates.put(UserHandle.USER_SYSTEM, userState);
         setupShortcutTargetServices(userState);
 
+        // default is present in userState & setting, so it's not cleared
+        putShortcutSettingForUser(HARDWARE, serviceDefault, UserHandle.USER_SYSTEM);
+        userState.updateShortcutTargetsLocked(Set.of(serviceDefault), HARDWARE);
+
         broadcastSettingRestored(
                 Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE,
-                /*previousValue=*/serviceDefault,
                 /*newValue=*/serviceDefault);
 
         final Set<String> expected = Set.of(serviceDefault);
-        assertThat(readStringsFromSetting(
-                Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE))
+        assertThat(readStringsFromSetting(ShortcutUtils.convertToKey(HARDWARE)))
                 .containsExactlyElementsIn(expected);
-        assertThat(mA11yms.mUserStates.get(UserHandle.USER_SYSTEM)
-                .getShortcutTargetsLocked(UserShortcutType.HARDWARE))
+        assertThat(userState.getShortcutTargetsLocked(HARDWARE))
                 .containsExactlyElementsIn(expected);
     }
 
@@ -1772,7 +1779,7 @@
     @EnableFlags({
             android.view.accessibility.Flags.FLAG_RESTORE_A11Y_SHORTCUT_TARGET_SERVICE,
             Flags.FLAG_CLEAR_DEFAULT_FROM_A11Y_SHORTCUT_TARGET_SERVICE_RESTORE})
-    public void restoreA11yShortcutTargetService_didNotHaveDefaultService_clearsDefaultService() {
+    public void restoreShortcutTargets_hardware_didNotHaveDefaultService_clearsDefaultService() {
         final String serviceDefault = TARGET_STANDARD_A11Y_SERVICE.flattenToString();
         final String serviceRestored = TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString();
         // Restored value from the broadcast contains both default and non-default service.
@@ -1784,18 +1791,45 @@
         mA11yms.mUserStates.put(UserHandle.USER_SYSTEM, userState);
         setupShortcutTargetServices(userState);
 
-        broadcastSettingRestored(
-                Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE,
-                /*previousValue=*/null,
+        broadcastSettingRestored(ShortcutUtils.convertToKey(HARDWARE),
                 /*newValue=*/combinedRestored);
 
         // The default service is cleared from the final restored value.
         final Set<String> expected = Set.of(serviceRestored);
-        assertThat(readStringsFromSetting(
-                Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE))
+        assertThat(readStringsFromSetting(ShortcutUtils.convertToKey(HARDWARE)))
                 .containsExactlyElementsIn(expected);
-        assertThat(mA11yms.mUserStates.get(UserHandle.USER_SYSTEM)
-                .getShortcutTargetsLocked(UserShortcutType.HARDWARE))
+        assertThat(userState.getShortcutTargetsLocked(HARDWARE))
+                .containsExactlyElementsIn(expected);
+    }
+
+    @Test
+    @EnableFlags({
+            android.view.accessibility.Flags.FLAG_RESTORE_A11Y_SHORTCUT_TARGET_SERVICE,
+            Flags.FLAG_CLEAR_DEFAULT_FROM_A11Y_SHORTCUT_TARGET_SERVICE_RESTORE})
+    public void restoreShortcutTargets_hardware_nullSetting_clearsDefaultService() {
+        final String serviceDefault = TARGET_STANDARD_A11Y_SERVICE.flattenToString();
+        final String serviceRestored = TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString();
+        // Restored value from the broadcast contains both default and non-default service.
+        final String combinedRestored = String.join(":", serviceDefault, serviceRestored);
+        mTestableContext.getOrCreateTestableResources().addOverride(
+                R.string.config_defaultAccessibilityService, serviceDefault);
+        final AccessibilityUserState userState = new AccessibilityUserState(
+                UserHandle.USER_SYSTEM, mTestableContext, mA11yms);
+        mA11yms.mUserStates.put(UserHandle.USER_SYSTEM, userState);
+        setupShortcutTargetServices(userState);
+
+        // UserState has default, but setting is null (this emulates a typical scenario in SUW).
+        userState.updateShortcutTargetsLocked(Set.of(serviceDefault), HARDWARE);
+        putShortcutSettingForUser(HARDWARE, null, UserHandle.USER_SYSTEM);
+
+        broadcastSettingRestored(ShortcutUtils.convertToKey(HARDWARE),
+                /*newValue=*/combinedRestored);
+
+        // The default service is cleared from the final restored value.
+        final Set<String> expected = Set.of(serviceRestored);
+        assertThat(readStringsFromSetting(ShortcutUtils.convertToKey(HARDWARE)))
+                .containsExactlyElementsIn(expected);
+        assertThat(userState.getShortcutTargetsLocked(HARDWARE))
                 .containsExactlyElementsIn(expected);
     }
 
@@ -1806,11 +1840,10 @@
         return result;
     }
 
-    private void broadcastSettingRestored(String setting, String previousValue, String newValue) {
+    private void broadcastSettingRestored(String setting, String newValue) {
         Intent intent = new Intent(Intent.ACTION_SETTING_RESTORED)
                 .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY)
                 .putExtra(Intent.EXTRA_SETTING_NAME, setting)
-                .putExtra(Intent.EXTRA_SETTING_PREVIOUS_VALUE, previousValue)
                 .putExtra(Intent.EXTRA_SETTING_NEW_VALUE, newValue);
         sendBroadcastToAccessibilityManagerService(intent);
         mTestableLooper.processAllMessages();
@@ -1952,4 +1985,13 @@
     private static boolean isSameCurrentUser(AccessibilityManagerService service, Context context) {
         return service.getCurrentUserIdLocked() == context.getUserId();
     }
+
+    private void putShortcutSettingForUser(@UserShortcutType int shortcutType,
+            String shortcutValue, int userId) {
+        Settings.Secure.putStringForUser(
+                mTestableContext.getContentResolver(),
+                ShortcutUtils.convertToKey(shortcutType),
+                shortcutValue,
+                userId);
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java
index b269beb9..9fad14d 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java
@@ -28,6 +28,7 @@
 import static android.view.accessibility.AccessibilityManager.STATE_FLAG_HIGH_TEXT_CONTRAST_ENABLED;
 import static android.view.accessibility.AccessibilityManager.STATE_FLAG_TOUCH_EXPLORATION_ENABLED;
 
+import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS;
 import static com.android.server.accessibility.AccessibilityUserState.doesShortcutTargetsStringContain;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -429,20 +430,20 @@
     }
 
     @Test
-    public void updateA11yQsTargetLocked_valueUpdated() {
+    public void updateShortcutTargetsLocked_quickSettings_valueUpdated() {
         Set<String> newTargets = Set.of(
                 AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME.flattenToString(),
                 AccessibilityShortcutController.COLOR_INVERSION_COMPONENT_NAME.flattenToString()
         );
 
-        mUserState.updateA11yQsTargetLocked(newTargets);
+        mUserState.updateShortcutTargetsLocked(newTargets, QUICK_SETTINGS);
 
         assertThat(mUserState.getA11yQsTargets()).isEqualTo(newTargets);
     }
 
     @Test
     public void getA11yQsTargets_returnsCopiedData() {
-        updateA11yQsTargetLocked_valueUpdated();
+        updateShortcutTargetsLocked_quickSettings_valueUpdated();
 
         Set<String> targets = mUserState.getA11yQsTargets();
         targets.clear();
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index c1d7afb..c48d745 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -14335,6 +14335,29 @@
 
     @Test
     @EnableFlags(android.app.Flags.FLAG_SECURE_ALLOWLIST_TOKEN)
+    public void enqueueNotification_directlyThroughRunnable_populatesAllowlistToken() {
+        Notification receivedWithoutParceling = new Notification.Builder(mContext, TEST_CHANNEL_ID)
+                .setContentIntent(createPendingIntent("content"))
+                .build();
+        NotificationRecord record = new NotificationRecord(
+                mContext,
+                new StatusBarNotification(mPkg, mPkg, 1, "tag", mUid, 44, receivedWithoutParceling,
+                        mUser, "groupKey", 0),
+                mTestNotificationChannel);
+        assertThat(record.getNotification().getAllowlistToken()).isNull();
+
+        mWorkerHandler.post(
+                mService.new EnqueueNotificationRunnable(mUserId, record, false, false,
+                mPostNotificationTrackerFactory.newTracker(null)));
+        waitForIdle();
+
+        assertThat(mService.mNotificationList).hasSize(1);
+        assertThat(mService.mNotificationList.get(0).getNotification().getAllowlistToken())
+                .isEqualTo(NotificationManagerService.ALLOWLIST_TOKEN);
+    }
+
+    @Test
+    @EnableFlags(android.app.Flags.FLAG_SECURE_ALLOWLIST_TOKEN)
     public void enqueueNotification_rejectsOtherToken() throws RemoteException {
         Notification sent = new Notification.Builder(mContext, TEST_CHANNEL_ID)
                 .setContentIntent(createPendingIntent("content"))
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
index 9352c12..f5ab95c 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
@@ -511,6 +511,9 @@
         rule.iconResName = ICON_RES_NAME;
         rule.triggerDescription = TRIGGER_DESC;
         rule.deletionInstant = Instant.ofEpochMilli(1701790147000L);
+        if (Flags.modesUi()) {
+            rule.disabledOrigin = ZenModeConfig.UPDATE_ORIGIN_USER;
+        }
 
         Parcel parcel = Parcel.obtain();
         rule.writeToParcel(parcel, 0);
@@ -540,6 +543,9 @@
         assertEquals(rule.triggerDescription, parceled.triggerDescription);
         assertEquals(rule.zenPolicy, parceled.zenPolicy);
         assertEquals(rule.deletionInstant, parceled.deletionInstant);
+        if (Flags.modesUi()) {
+            assertEquals(rule.disabledOrigin, parceled.disabledOrigin);
+        }
 
         assertEquals(rule, parceled);
         assertEquals(rule.hashCode(), parceled.hashCode());
@@ -620,6 +626,9 @@
         rule.iconResName = ICON_RES_NAME;
         rule.triggerDescription = TRIGGER_DESC;
         rule.deletionInstant = Instant.ofEpochMilli(1701790147000L);
+        if (Flags.modesUi()) {
+            rule.disabledOrigin = ZenModeConfig.UPDATE_ORIGIN_APP;
+        }
 
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
         writeRuleXml(rule, baos);
@@ -653,6 +662,9 @@
         assertEquals(rule.triggerDescription, fromXml.triggerDescription);
         assertEquals(rule.iconResName, fromXml.iconResName);
         assertEquals(rule.deletionInstant, fromXml.deletionInstant);
+        if (Flags.modesUi()) {
+            assertEquals(rule.disabledOrigin, fromXml.disabledOrigin);
+        }
     }
 
     @Test
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
index 26a13cb..57587f7 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.notification;
 
+import static android.app.Flags.FLAG_MODES_API;
 import static android.app.Flags.FLAG_MODES_UI;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -32,6 +33,7 @@
 import android.app.NotificationManager;
 import android.content.ComponentName;
 import android.net.Uri;
+import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.FlagsParameterization;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.provider.Settings;
@@ -79,16 +81,17 @@
                     : Set.of("version", "manualRule", "automaticRules");
 
     // Differences for flagged fields are only generated if the flag is enabled.
-    // "Metadata" fields (userModifiedFields & co, deletionInstant) are not compared.
+    // "Metadata" fields (userModifiedFields, deletionInstant, disabledOrigin) are not compared.
     private static final Set<String> ZEN_RULE_EXEMPT_FIELDS =
             android.app.Flags.modesApi()
                     ? Set.of("userModifiedFields", "zenPolicyUserModifiedFields",
-                            "zenDeviceEffectsUserModifiedFields", "deletionInstant")
+                            "zenDeviceEffectsUserModifiedFields", "deletionInstant",
+                            "disabledOrigin")
                     : Set.of(RuleDiff.FIELD_TYPE, RuleDiff.FIELD_TRIGGER_DESCRIPTION,
                             RuleDiff.FIELD_ICON_RES, RuleDiff.FIELD_ALLOW_MANUAL,
                             RuleDiff.FIELD_ZEN_DEVICE_EFFECTS, "userModifiedFields",
                             "zenPolicyUserModifiedFields", "zenDeviceEffectsUserModifiedFields",
-                            "deletionInstant");
+                            "deletionInstant", "disabledOrigin");
 
     // allowPriorityChannels is flagged by android.app.modes_api
     public static final Set<String> ZEN_MODE_CONFIG_FLAGGED_FIELDS =
@@ -201,8 +204,8 @@
     }
 
     @Test
+    @EnableFlags(FLAG_MODES_API)
     public void testConfigDiff_fieldDiffs_flagOn() throws Exception {
-        mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
         // these two start the same
         ZenModeConfig c1 = new ZenModeConfig();
         ZenModeConfig c2 = new ZenModeConfig();
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index 40b0d78..7bb633e 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -6226,6 +6226,101 @@
         assertThat(mZenModeHelper.getConfig().manualRule.zenDeviceEffects).isEqualTo(effects);
     }
 
+    @Test
+    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    public void addAutomaticZenRule_startsDisabled_recordsDisabledOrigin() {
+        AutomaticZenRule startsDisabled = new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
+                .setOwner(new ComponentName(mPkg, "SomeProvider"))
+                .setEnabled(false)
+                .build();
+
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, startsDisabled, UPDATE_ORIGIN_APP,
+                "new", CUSTOM_PKG_UID);
+
+        assertThat(mZenModeHelper.mConfig.automaticRules.get(ruleId).disabledOrigin).isEqualTo(
+                UPDATE_ORIGIN_APP);
+    }
+
+    @Test
+    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    public void updateAutomaticZenRule_disabling_recordsDisabledOrigin() {
+        AutomaticZenRule startsEnabled = new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
+                .setOwner(new ComponentName(mPkg, "SomeProvider"))
+                .setEnabled(true)
+                .build();
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, startsEnabled, UPDATE_ORIGIN_APP,
+                "new", CUSTOM_PKG_UID);
+        assertThat(mZenModeHelper.mConfig.automaticRules.get(ruleId).disabledOrigin).isEqualTo(
+                UPDATE_ORIGIN_UNKNOWN);
+
+        AutomaticZenRule nowDisabled = new AutomaticZenRule.Builder(startsEnabled)
+                .setEnabled(false)
+                .build();
+        mZenModeHelper.updateAutomaticZenRule(ruleId, nowDisabled, UPDATE_ORIGIN_USER, "off",
+                Process.SYSTEM_UID);
+
+        assertThat(mZenModeHelper.mConfig.automaticRules.get(ruleId).disabledOrigin).isEqualTo(
+                UPDATE_ORIGIN_USER);
+    }
+
+    @Test
+    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    public void updateAutomaticZenRule_keepingDisabled_preservesPreviousDisabledOrigin() {
+        AutomaticZenRule startsEnabled = new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
+                .setOwner(new ComponentName(mPkg, "SomeProvider"))
+                .setEnabled(true)
+                .build();
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, startsEnabled, UPDATE_ORIGIN_APP,
+                "new", CUSTOM_PKG_UID);
+        AutomaticZenRule nowDisabled = new AutomaticZenRule.Builder(startsEnabled)
+                .setEnabled(false)
+                .build();
+        mZenModeHelper.updateAutomaticZenRule(ruleId, nowDisabled, UPDATE_ORIGIN_USER, "off",
+                Process.SYSTEM_UID);
+        assertThat(mZenModeHelper.mConfig.automaticRules.get(ruleId).disabledOrigin).isEqualTo(
+                UPDATE_ORIGIN_USER);
+
+        // Now update it again, for an unrelated reason with a different origin.
+        AutomaticZenRule nowRenamed = new AutomaticZenRule.Builder(nowDisabled)
+                .setName("Fancy pants rule")
+                .build();
+        mZenModeHelper.updateAutomaticZenRule(ruleId, nowRenamed, UPDATE_ORIGIN_APP, "update",
+                CUSTOM_PKG_UID);
+
+        // Identity of the disabler is preserved.
+        assertThat(mZenModeHelper.mConfig.automaticRules.get(ruleId).disabledOrigin).isEqualTo(
+                UPDATE_ORIGIN_USER);
+    }
+
+    @Test
+    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    public void updateAutomaticZenRule_enabling_clearsDisabledOrigin() {
+        AutomaticZenRule startsEnabled = new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
+                .setOwner(new ComponentName(mPkg, "SomeProvider"))
+                .setEnabled(true)
+                .build();
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, startsEnabled, UPDATE_ORIGIN_APP,
+                "new", CUSTOM_PKG_UID);
+        AutomaticZenRule nowDisabled = new AutomaticZenRule.Builder(startsEnabled)
+                .setEnabled(false)
+                .build();
+        mZenModeHelper.updateAutomaticZenRule(ruleId, nowDisabled, UPDATE_ORIGIN_USER, "off",
+                Process.SYSTEM_UID);
+        assertThat(mZenModeHelper.mConfig.automaticRules.get(ruleId).disabledOrigin).isEqualTo(
+                UPDATE_ORIGIN_USER);
+
+        // Now enable it again
+        AutomaticZenRule nowEnabled = new AutomaticZenRule.Builder(nowDisabled)
+                .setEnabled(true)
+                .build();
+        mZenModeHelper.updateAutomaticZenRule(ruleId, nowEnabled, UPDATE_ORIGIN_APP, "on",
+                CUSTOM_PKG_UID);
+
+        // Identity of the disabler was cleared.
+        assertThat(mZenModeHelper.mConfig.automaticRules.get(ruleId).disabledOrigin).isEqualTo(
+                UPDATE_ORIGIN_UNKNOWN);
+    }
+
     private static void addZenRule(ZenModeConfig config, String id, String ownerPkg, int zenMode,
             @Nullable ZenPolicy zenPolicy) {
         ZenRule rule = new ZenRule();
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index d143297..13d52ea 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -3098,6 +3098,30 @@
     }
 
     @Test
+    public void testOnStartingWindowDrawn() {
+        final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build();
+        // The task-has-been-visible should not affect the decision of making transition ready.
+        activity.getTask().setHasBeenVisible(true);
+        activity.detachFromProcess();
+        activity.mStartingData = mock(StartingData.class);
+        registerTestTransitionPlayer();
+        final Transition transition = activity.mTransitionController.requestTransitionIfNeeded(
+                WindowManager.TRANSIT_OPEN, 0 /* flags */, null /* trigger */, mDisplayContent);
+        activity.onStartingWindowDrawn();
+        assertTrue(activity.mStartingData.mIsDisplayed);
+        // The transition can be ready by the starting window of a visible-requested activity
+        // without a running process.
+        assertTrue(transition.allReady());
+
+        // If other event makes the transition unready, the reentrant of onStartingWindowDrawn
+        // should not replace the readiness again.
+        transition.setReady(mDisplayContent, false);
+        activity.onStartingWindowDrawn();
+        assertFalse(transition.allReady());
+    }
+
+
+    @Test
     public void testCloseToSquareFixedOrientation() {
         if (Flags.insetsDecoupledConfiguration()) {
             // No test needed as decor insets no longer affects orientation.
@@ -3333,14 +3357,8 @@
         // to client if the app didn't request IME visible.
         assertFalse(app2.mActivityRecord.mImeInsetsFrozenUntilStartInput);
 
-        if (Flags.bundleClientTransactionFlag()) {
-            verify(app2.getProcess(), atLeastOnce()).scheduleClientTransactionItem(
-                    isA(WindowStateResizeItem.class));
-        } else {
-            verify(app2.mClient, atLeastOnce()).resized(any(), anyBoolean(), any(),
-                    insetsStateCaptor.capture(), anyBoolean(), anyBoolean(), anyInt(), anyInt(),
-                    anyBoolean(), any());
-        }
+        verify(app2.getProcess(), atLeastOnce()).scheduleClientTransactionItem(
+                isA(WindowStateResizeItem.class));
         assertFalse(app2.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime()));
     }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationCapabilityTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationCapabilityTest.java
new file mode 100644
index 0000000..f1cf866
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationCapabilityTest.java
@@ -0,0 +1,433 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.wm;
+
+import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH;
+import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED;
+import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED;
+import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.server.wm.AppCompatOrientationCapability.OrientationCapabilityState.MIN_COUNT_TO_IGNORE_REQUEST_IN_LOOP;
+import static com.android.server.wm.AppCompatOrientationCapability.OrientationCapabilityState.SET_ORIENTATION_REQUEST_COUNTER_TIMEOUT_MS;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+
+import android.compat.testing.PlatformCompatChangeRule;
+import android.content.ComponentName;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.annotation.NonNull;
+
+import com.android.server.wm.utils.TestComponentStack;
+
+import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.RunWith;
+
+import java.util.function.Consumer;
+import java.util.function.IntConsumer;
+import java.util.function.LongSupplier;
+
+/**
+ * Test class for {@link AppCompatOrientationCapability}.
+ * <p>
+ * Build/Install/Run:
+ * atest WmTests:AppCompatOrientationCapabilityTest
+ */
+@Presubmit
+@RunWith(WindowTestRunner.class)
+public class AppCompatOrientationCapabilityTest extends WindowTestsBase {
+
+    @Rule
+    public TestRule compatChangeRule = new PlatformCompatChangeRule();
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION})
+    public void testShouldIgnoreRequestedOrientation_activityRelaunching_returnsTrue() {
+        runTestScenario((robot) -> {
+            robot.prepareIsPolicyForIgnoringRequestedOrientationEnabled(true);
+            robot.createActivityWithComponent();
+            robot.prepareRelaunchingAfterRequestedOrientationChanged(true);
+
+            robot.checkShouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED);
+        });
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION})
+    public void testShouldIgnoreRequestedOrientation_cameraCompatTreatment_returnsTrue() {
+        runTestScenario((robot) -> {
+            robot.prepareIsCameraCompatTreatmentEnabled(true);
+            robot.prepareIsCameraCompatTreatmentEnabledAtBuildTime(true);
+            robot.prepareIsPolicyForIgnoringRequestedOrientationEnabled(true);
+
+            robot.createActivityWithComponentInNewTask();
+            robot.prepareRelaunchingAfterRequestedOrientationChanged(false);
+            robot.prepareIsTreatmentEnabledForTopActivity(true);
+
+            robot.checkShouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED);
+        });
+    }
+
+    @Test
+    public void testShouldIgnoreRequestedOrientation_overrideDisabled_returnsFalse() {
+        runTestScenario((robot) -> {
+            robot.prepareIsPolicyForIgnoringRequestedOrientationEnabled(true);
+
+            robot.createActivityWithComponent();
+            robot.prepareRelaunchingAfterRequestedOrientationChanged(true);
+
+            robot.checkShouldNotIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED);
+        });
+    }
+
+    @Test
+    public void testShouldIgnoreRequestedOrientation_propertyIsTrue_returnsTrue() {
+        runTestScenario((robot) -> {
+            robot.prepareIsPolicyForIgnoringRequestedOrientationEnabled(true);
+            robot.enableProperty(PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION);
+
+            robot.createActivityWithComponent();
+            robot.prepareRelaunchingAfterRequestedOrientationChanged(true);
+
+            robot.checkShouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED);
+        });
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION})
+    public void testShouldIgnoreRequestedOrientation_propertyIsFalseAndOverride_returnsFalse()
+            throws Exception {
+        runTestScenario((robot) -> {
+            robot.prepareIsPolicyForIgnoringRequestedOrientationEnabled(true);
+            robot.disableProperty(PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION);
+
+            robot.createActivityWithComponent();
+            robot.prepareRelaunchingAfterRequestedOrientationChanged(true);
+
+            robot.checkShouldNotIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED);
+        });
+    }
+
+    @Test
+    public void testShouldIgnoreOrientationRequestLoop_overrideDisabled_returnsFalse() {
+        runTestScenario((robot) -> {
+            robot.prepareIsPolicyForIgnoringRequestedOrientationEnabled(true);
+            robot.createActivityWithComponent();
+            robot.prepareIsLetterboxedForFixedOrientationAndAspectRatio(false);
+
+            robot.checkRequestLoopExtended((i) -> {
+                robot.checkShouldNotIgnoreOrientationLoop();
+                robot.checkExpectedLoopCount(/* expectedCount */ 0);
+            });
+        });
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED})
+    public void testShouldIgnoreOrientationRequestLoop_propertyIsFalseAndOverride_returnsFalse() {
+        runTestScenario((robot) -> {
+            robot.prepareIsPolicyForIgnoringRequestedOrientationEnabled(true);
+            robot.disableProperty(
+                    PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED);
+            robot.createActivityWithComponent();
+            robot.prepareIsLetterboxedForFixedOrientationAndAspectRatio(false);
+
+            robot.checkRequestLoopExtended((i) -> {
+                robot.checkShouldNotIgnoreOrientationLoop();
+                robot.checkExpectedLoopCount(/* expectedCount */ 0);
+            });
+        });
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED})
+    public void testShouldIgnoreOrientationRequestLoop_isLetterboxed_returnsFalse() {
+        runTestScenario((robot) -> {
+            robot.prepareIsPolicyForIgnoringRequestedOrientationEnabled(true);
+            robot.createActivityWithComponent();
+            robot.prepareIsLetterboxedForFixedOrientationAndAspectRatio(true);
+
+            robot.checkRequestLoopExtended((i) -> {
+                robot.checkShouldNotIgnoreOrientationLoop();
+                robot.checkExpectedLoopCount(/* expectedCount */ i);
+            });
+        });
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED})
+    public void testShouldIgnoreOrientationRequestLoop_noLoop_returnsFalse() {
+        runTestScenario((robot) -> {
+            robot.prepareIsPolicyForIgnoringRequestedOrientationEnabled(true);
+            robot.createActivityWithComponent();
+            robot.prepareIsLetterboxedForFixedOrientationAndAspectRatio(false);
+
+            robot.checkShouldNotIgnoreOrientationLoop();
+            robot.checkExpectedLoopCount(/* expectedCount */ 0);
+        });
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED})
+    public void testShouldIgnoreOrientationRequestLoop_timeout_returnsFalse() {
+        runTestScenario((robot) -> {
+            robot.prepareIsPolicyForIgnoringRequestedOrientationEnabled(true);
+            robot.createActivityWithComponent();
+            robot.prepareIsLetterboxedForFixedOrientationAndAspectRatio(false);
+
+            robot.prepareMockedTime();
+            robot.checkRequestLoopExtended((i) -> {
+                robot.checkShouldNotIgnoreOrientationLoop();
+                robot.checkExpectedLoopCount(/* expectedCount */ 0);
+                robot.delay();
+            });
+        });
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED})
+    public void testShouldIgnoreOrientationRequestLoop_returnsTrue() {
+        runTestScenario((robot) -> {
+            robot.prepareIsPolicyForIgnoringRequestedOrientationEnabled(true);
+            robot.createActivityWithComponent();
+            robot.prepareIsLetterboxedForFixedOrientationAndAspectRatio(false);
+
+            robot.checkRequestLoop((i) -> {
+                robot.checkShouldNotIgnoreOrientationLoop();
+                robot.checkExpectedLoopCount(/* expectedCount */ i);
+            });
+            robot.checkShouldIgnoreOrientationLoop();
+            robot.checkExpectedLoopCount(/* expectedCount */ MIN_COUNT_TO_IGNORE_REQUEST_IN_LOOP);
+        });
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH})
+    public void testShouldIgnoreRequestedOrientation_flagIsDisabled_returnsFalse() {
+        runTestScenario((robot) -> {
+            robot.prepareIsPolicyForIgnoringRequestedOrientationEnabled(true);
+            robot.createActivityWithComponent();
+            robot.prepareIsLetterboxedForFixedOrientationAndAspectRatio(false);
+
+            robot.checkShouldNotIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED);
+        });
+    }
+
+    /**
+     * Runs a test scenario providing a Robot.
+     */
+    void runTestScenario(@NonNull Consumer<OrientationCapabilityRobotTest> consumer) {
+        spyOn(mWm.mLetterboxConfiguration);
+        final OrientationCapabilityRobotTest robot =
+                new OrientationCapabilityRobotTest(mWm, mAtm, mSupervisor);
+        consumer.accept(robot);
+    }
+
+    private static class OrientationCapabilityRobotTest {
+
+        @NonNull
+        private final ActivityTaskManagerService mAtm;
+        @NonNull
+        private final WindowManagerService mWm;
+        @NonNull
+        private final ActivityTaskSupervisor mSupervisor;
+        @NonNull
+        private final LetterboxConfiguration mLetterboxConfiguration;
+        @NonNull
+        private final TestComponentStack<ActivityRecord> mActivityStack;
+        @NonNull
+        private final TestComponentStack<Task> mTaskStack;
+        @NonNull
+        private final CurrentTimeMillisSupplierTest mTestCurrentTimeMillisSupplier;
+
+
+        OrientationCapabilityRobotTest(@NonNull WindowManagerService wm,
+                @NonNull ActivityTaskManagerService atm,
+                @NonNull ActivityTaskSupervisor supervisor) {
+            mAtm = atm;
+            mWm = wm;
+            mSupervisor = supervisor;
+            mActivityStack = new TestComponentStack<>();
+            mTaskStack = new TestComponentStack<>();
+            mLetterboxConfiguration = mWm.mLetterboxConfiguration;
+            mTestCurrentTimeMillisSupplier = new CurrentTimeMillisSupplierTest();
+        }
+
+        void prepareRelaunchingAfterRequestedOrientationChanged(boolean enabled) {
+            getTopOrientationCapability().setRelaunchingAfterRequestedOrientationChanged(enabled);
+        }
+
+        void prepareIsPolicyForIgnoringRequestedOrientationEnabled(boolean enabled) {
+            doReturn(enabled).when(mLetterboxConfiguration)
+                    .isPolicyForIgnoringRequestedOrientationEnabled();
+        }
+
+        void prepareIsCameraCompatTreatmentEnabled(boolean enabled) {
+            doReturn(enabled).when(mLetterboxConfiguration).isCameraCompatTreatmentEnabled();
+        }
+
+        void prepareIsCameraCompatTreatmentEnabledAtBuildTime(boolean enabled) {
+            doReturn(enabled).when(mLetterboxConfiguration)
+                    .isCameraCompatTreatmentEnabledAtBuildTime();
+        }
+
+        void prepareIsTreatmentEnabledForTopActivity(boolean enabled) {
+            final DisplayRotationCompatPolicy displayPolicy = mActivityStack.top()
+                    .mDisplayContent.mDisplayRotationCompatPolicy;
+            spyOn(displayPolicy);
+            doReturn(enabled).when(displayPolicy)
+                    .isTreatmentEnabledForActivity(eq(mActivityStack.top()));
+        }
+
+        // Useful to reduce timeout during tests
+        void prepareMockedTime() {
+            getTopOrientationCapability().mOrientationCapabilityState.mCurrentTimeMillisSupplier =
+                    mTestCurrentTimeMillisSupplier;
+        }
+
+        void delay() {
+            mTestCurrentTimeMillisSupplier.delay(SET_ORIENTATION_REQUEST_COUNTER_TIMEOUT_MS);
+        }
+
+        void enableProperty(@NonNull String propertyName) {
+            setPropertyValue(propertyName, /* enabled */ true);
+        }
+
+        void disableProperty(@NonNull String propertyName) {
+            setPropertyValue(propertyName, /* enabled */ false);
+        }
+
+        void prepareIsLetterboxedForFixedOrientationAndAspectRatio(boolean enabled) {
+            spyOn(mActivityStack.top());
+            doReturn(enabled).when(mActivityStack.top())
+                    .isLetterboxedForFixedOrientationAndAspectRatio();
+        }
+
+        void createActivityWithComponent() {
+            createActivityWithComponentInNewTask(/* inNewTask */ mTaskStack.isEmpty());
+        }
+
+        void createActivityWithComponentInNewTask() {
+            createActivityWithComponentInNewTask(/* inNewTask */ true);
+        }
+
+        private void createActivityWithComponentInNewTask(boolean inNewTask) {
+            if (inNewTask) {
+                createNewTask();
+            }
+            final ActivityRecord activity = new ActivityBuilder(mAtm)
+                    .setOnTop(true)
+                    .setTask(mTaskStack.top())
+                    // Set the component to be that of the test class in order
+                    // to enable compat changes
+                    .setComponent(ComponentName.createRelative(mAtm.mContext,
+                            com.android.server.wm.LetterboxUiControllerTest.class.getName()))
+                    .build();
+            mActivityStack.push(activity);
+        }
+
+        void checkShouldIgnoreRequestedOrientation(
+                @Configuration.Orientation int expectedOrientation) {
+            assertTrue(getTopOrientationCapability()
+                    .shouldIgnoreRequestedOrientation(expectedOrientation));
+        }
+
+        void checkShouldNotIgnoreRequestedOrientation(
+                @Configuration.Orientation int expectedOrientation) {
+            assertFalse(getTopOrientationCapability()
+                    .shouldIgnoreRequestedOrientation(expectedOrientation));
+        }
+
+        void checkExpectedLoopCount(int expectedCount) {
+            assertEquals(expectedCount, getTopOrientationCapability()
+                    .getSetOrientationRequestCounter());
+        }
+
+        void checkShouldNotIgnoreOrientationLoop() {
+            assertFalse(getTopOrientationCapability().shouldIgnoreOrientationRequestLoop());
+        }
+
+        void checkShouldIgnoreOrientationLoop() {
+            assertTrue(getTopOrientationCapability().shouldIgnoreOrientationRequestLoop());
+        }
+
+        void checkRequestLoop(IntConsumer consumer) {
+            for (int i = 0; i < MIN_COUNT_TO_IGNORE_REQUEST_IN_LOOP; i++) {
+                consumer.accept(i);
+            }
+        }
+
+        void checkRequestLoopExtended(IntConsumer consumer) {
+            for (int i = 0; i <= MIN_COUNT_TO_IGNORE_REQUEST_IN_LOOP; i++) {
+                consumer.accept(i);
+            }
+        }
+
+        private AppCompatOrientationCapability getTopOrientationCapability() {
+            return mActivityStack.top().mAppCompatController.getAppCompatCapability()
+                    .getAppCompatOrientationCapability();
+        }
+
+        private void createNewTask() {
+            final DisplayContent displayContent = new TestDisplayContent
+                    .Builder(mAtm, /* dw */ 1000, /* dh */ 2000).build();
+            final Task newTask = new TaskBuilder(mSupervisor).setDisplay(displayContent).build();
+            mTaskStack.push(newTask);
+        }
+
+        private void setPropertyValue(@NonNull String propertyName, boolean enabled) {
+            PackageManager.Property property = new PackageManager.Property(propertyName,
+                    /* value */ enabled, /* packageName */ "",
+                    /* className */ "");
+            PackageManager pm = mWm.mContext.getPackageManager();
+            spyOn(pm);
+            try {
+                doReturn(property).when(pm).getProperty(eq(propertyName), anyString());
+            } catch (PackageManager.NameNotFoundException e) {
+                fail(e.getLocalizedMessage());
+            }
+        }
+
+        private static class CurrentTimeMillisSupplierTest implements LongSupplier {
+
+            private long mCurrenTimeMillis = System.currentTimeMillis();
+
+            @Override
+            public long getAsLong() {
+                return mCurrenTimeMillis;
+            }
+
+            public void delay(long delay) {
+                mCurrenTimeMillis += delay;
+            }
+        }
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java
new file mode 100644
index 0000000..2260999
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java
@@ -0,0 +1,570 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static android.content.pm.ActivityInfo.OVERRIDE_ANY_ORIENTATION;
+import static android.content.pm.ActivityInfo.OVERRIDE_ANY_ORIENTATION_TO_USER;
+import static android.content.pm.ActivityInfo.OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE;
+import static android.content.pm.ActivityInfo.OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA;
+import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR;
+import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LOCKED;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_3_2;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_FULLSCREEN;
+import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE;
+import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+
+import android.compat.testing.PlatformCompatChangeRule;
+import android.content.ComponentName;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.annotation.NonNull;
+
+import com.android.server.wm.utils.TestComponentStack;
+
+import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
+
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.RunWith;
+
+import java.util.function.Consumer;
+
+/**
+ * Test class for {@link AppCompatOrientationPolicy}.
+ * <p>
+ * Build/Install/Run:
+ * atest WmTests:AppCompatOrientationPolicyTest
+ */
+@Presubmit
+@RunWith(WindowTestRunner.class)
+public class AppCompatOrientationPolicyTest extends WindowTestsBase {
+
+    @Rule
+    public TestRule compatChangeRule = new PlatformCompatChangeRule();
+
+    @Test
+    public void testOverrideOrientationIfNeeded_mapInvokedOnRequest() {
+        runTestScenarioWithActivity((robot) -> {
+            robot.overrideOrientationIfNeeded(SCREEN_ORIENTATION_PORTRAIT);
+            robot.checkOrientationRequestMapped();
+        });
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
+    public void testOverrideOrientationIfNeeded_fullscreenOverrideEnabled_returnsUser() {
+        runTestScenarioWithActivity((robot) -> {
+            robot.configureSetIgnoreOrientationRequest(true);
+            robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+                    /* expected */ SCREEN_ORIENTATION_USER);
+        });
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
+    public void testOverrideOrientationIfNeeded_fullscreenOverrideEnabled_optOut_isUnchanged() {
+        runTestScenario((robot) -> {
+            robot.disableProperty(PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE);
+            robot.createActivityWithComponent();
+            robot.configureSetIgnoreOrientationRequest(true);
+
+            robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+                    /* expected */ SCREEN_ORIENTATION_PORTRAIT);
+        });
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
+    public void testOverrideOrientationIfNeeded_fullscreenOverrides_optOutSystem_returnsUser() {
+        runTestScenario((robot) -> {
+            robot.disableProperty(PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE);
+            robot.configureIsUserAppAspectRatioFullscreenEnabled(true);
+
+            robot.createActivityWithComponent();
+            robot.configureSetIgnoreOrientationRequest(true);
+            robot.prepareGetUserMinAspectRatioOverrideCode(USER_MIN_ASPECT_RATIO_FULLSCREEN);
+
+            robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+                    /* expected */ SCREEN_ORIENTATION_USER);
+        });
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
+    public void testOverrideOrientationIfNeeded_fullscreenOverrides_optOutUser_returnsUser() {
+        runTestScenario((robot) -> {
+            robot.disableProperty(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE);
+            robot.configureIsUserAppAspectRatioFullscreenEnabled(true);
+
+            robot.createActivityWithComponent();
+            robot.configureSetIgnoreOrientationRequest(true);
+            robot.prepareGetUserMinAspectRatioOverrideCode(USER_MIN_ASPECT_RATIO_FULLSCREEN);
+
+            robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+                    /* expected */ SCREEN_ORIENTATION_USER);
+        });
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
+    public void testOverrideOrientationIfNeeded_fullscreenOverrideEnabled_returnsUnchanged()
+            throws Exception {
+        runTestScenarioWithActivity((robot) -> {
+            robot.configureSetIgnoreOrientationRequest(false);
+
+            robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+                    /* expected */ SCREEN_ORIENTATION_PORTRAIT);
+        });
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
+    public void testOverrideOrientationIfNeeded_fullscreenAndUserOverrideEnabled_isUnchanged() {
+        runTestScenario((robot) -> {
+            robot.prepareIsUserAppAspectRatioSettingsEnabled(true);
+
+            robot.createActivityWithComponent();
+            robot.configureSetIgnoreOrientationRequest(true);
+            robot.prepareGetUserMinAspectRatioOverrideCode(USER_MIN_ASPECT_RATIO_3_2);
+
+            robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+                    /* expected */ SCREEN_ORIENTATION_PORTRAIT);
+        });
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT})
+    public void testOverrideOrientationIfNeeded_portraitOverrideEnabled_returnsPortrait() {
+        runTestScenarioWithActivity((robot) -> {
+            robot.checkOverrideOrientation(
+                    /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED,
+                    /* expected */ SCREEN_ORIENTATION_PORTRAIT);
+        });
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR})
+    public void testOverrideOrientationIfNeeded_portraitOverrideEnabled_returnsNosensor() {
+        runTestScenarioWithActivity((robot) -> {
+            robot.checkOverrideOrientation(
+                    /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED,
+                    /* expected */ SCREEN_ORIENTATION_NOSENSOR);
+        });
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR})
+    public void testOverrideOrientationIfNeeded_nosensorOverride_orientationFixed_isUnchanged() {
+        runTestScenarioWithActivity((robot) -> {
+            robot.checkOverrideOrientation(
+                    /* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+                    /* expected */ SCREEN_ORIENTATION_PORTRAIT);
+        });
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE})
+    public void testOverrideOrientationIfNeeded_reverseLandscape_portraitOrUndefined_isUnchanged() {
+        runTestScenarioWithActivity((robot) -> {
+            robot.checkOverrideOrientation(
+                    /* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+                    /* expected */ SCREEN_ORIENTATION_PORTRAIT);
+            robot.checkOverrideOrientation(
+                    /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED,
+                    /* expected */ SCREEN_ORIENTATION_UNSPECIFIED);
+        });
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE})
+    public void testOverrideOrientationIfNeeded_reverseLandscape_Landscape_getsReverseLandscape() {
+        runTestScenarioWithActivity((robot) -> {
+            robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_LANDSCAPE,
+                    /* expected */ SCREEN_ORIENTATION_REVERSE_LANDSCAPE);
+        });
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT})
+    public void testOverrideOrientationIfNeeded_portraitOverride_orientationFixed_IsUnchanged() {
+        runTestScenarioWithActivity((robot) -> {
+            robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_NOSENSOR,
+                    /* expected */ SCREEN_ORIENTATION_NOSENSOR);
+        });
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT, OVERRIDE_ANY_ORIENTATION})
+    public void testOverrideOrientationIfNeeded_portraitAndIgnoreFixedOverrides_returnsPortrait() {
+        runTestScenarioWithActivity((robot) -> {
+            robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_NOSENSOR,
+                    /* expected */ SCREEN_ORIENTATION_PORTRAIT);
+        });
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR, OVERRIDE_ANY_ORIENTATION})
+    public void testOverrideOrientationIfNeeded_noSensorAndIgnoreFixedOverrides_returnsNosensor() {
+        runTestScenarioWithActivity((robot) -> {
+            robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+                    /* expected */ SCREEN_ORIENTATION_NOSENSOR);
+        });
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT})
+    public void testOverrideOrientationIfNeeded_propertyIsFalse_isUnchanged()
+            throws Exception {
+        runTestScenario((robot) -> {
+            robot.disableProperty(PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE);
+
+            robot.createActivityWithComponent();
+
+            robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_UNSPECIFIED,
+                    /* expected */ SCREEN_ORIENTATION_UNSPECIFIED);
+        });
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT,
+            OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA})
+    public void testOverrideOrientationIfNeeded_whenCameraNotActive_isUnchanged() {
+        runTestScenario((robot) -> {
+            robot.configureIsCameraCompatTreatmentEnabled(true);
+            robot.configureIsCameraCompatTreatmentEnabledAtBuildTime(true);
+
+            robot.createActivityWithComponentInNewTask();
+            robot.prepareIsTopActivityEligibleForOrientationOverride(false);
+
+            robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_UNSPECIFIED,
+                    /* expected */ SCREEN_ORIENTATION_UNSPECIFIED);
+        });
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT,
+            OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA})
+    public void testOverrideOrientationIfNeeded_whenCameraActive_returnsPortrait() {
+        runTestScenario((robot) -> {
+            robot.configureIsCameraCompatTreatmentEnabled(true);
+            robot.configureIsCameraCompatTreatmentEnabledAtBuildTime(true);
+
+            robot.createActivityWithComponentInNewTask();
+            robot.prepareIsTopActivityEligibleForOrientationOverride(true);
+
+            robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_UNSPECIFIED,
+                    /* expected */ SCREEN_ORIENTATION_PORTRAIT);
+        });
+    }
+
+    @Test
+    public void testOverrideOrientationIfNeeded_userFullscreenOverride_returnsUser() {
+        runTestScenarioWithActivity((robot) -> {
+            robot.prepareShouldApplyUserFullscreenOverride(true);
+            robot.configureSetIgnoreOrientationRequest(true);
+
+            robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_UNSPECIFIED,
+                    /* expected */ SCREEN_ORIENTATION_USER);
+        });
+    }
+
+    @Test
+    public void testOverrideOrientationIfNeeded_fullscreenOverride_cameraActivity_unchanged() {
+        runTestScenario((robot) -> {
+            robot.configureIsCameraCompatTreatmentEnabled(true);
+            robot.configureIsCameraCompatTreatmentEnabledAtBuildTime(true);
+
+            robot.createActivityWithComponentInNewTask();
+            robot.configureIsTopActivityCameraActive(false);
+
+            robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+                    /* expected */ SCREEN_ORIENTATION_PORTRAIT);
+        });
+    }
+
+    @Test
+    public void testOverrideOrientationIfNeeded_respectOrientationRequestOverUserFullScreen() {
+        runTestScenarioWithActivity((robot) -> {
+            robot.prepareShouldApplyUserFullscreenOverride(true);
+            robot.configureSetIgnoreOrientationRequest(false);
+
+            robot.checkOverrideOrientationIsNot(/* candidate */ SCREEN_ORIENTATION_UNSPECIFIED,
+                    /* notExpected */ SCREEN_ORIENTATION_USER);
+        });
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT, OVERRIDE_ANY_ORIENTATION})
+    public void testOverrideOrientationIfNeeded_userFullScreenOverrideOverSystem_returnsUser() {
+        runTestScenarioWithActivity((robot) -> {
+            robot.prepareShouldApplyUserFullscreenOverride(true);
+            robot.configureSetIgnoreOrientationRequest(true);
+
+            robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+                    /* expected */ SCREEN_ORIENTATION_USER);
+        });
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT, OVERRIDE_ANY_ORIENTATION})
+    public void testOverrideOrientationIfNeeded_respectOrientationReqOverUserFullScreenAndSystem() {
+        runTestScenarioWithActivity((robot) -> {
+            robot.prepareShouldApplyUserFullscreenOverride(true);
+            robot.configureSetIgnoreOrientationRequest(false);
+
+            robot.checkOverrideOrientationIsNot(/* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+                    /* notExpected */ SCREEN_ORIENTATION_USER);
+        });
+    }
+
+    @Test
+    public void testOverrideOrientationIfNeeded_userFullScreenOverrideDisabled_returnsUnchanged() {
+        runTestScenarioWithActivity((robot) -> {
+            robot.prepareShouldApplyUserFullscreenOverride(false);
+
+            robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+                    /* expected */ SCREEN_ORIENTATION_PORTRAIT);
+        });
+    }
+
+    @Test
+    public void testOverrideOrientationIfNeeded_userAspectRatioApplied_unspecifiedOverridden() {
+        runTestScenarioWithActivity((robot) -> {
+            robot.prepareShouldApplyUserMinAspectRatioOverride(true);
+
+            robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_UNSPECIFIED,
+                    /* expected */ SCREEN_ORIENTATION_PORTRAIT);
+            robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_LOCKED,
+                    /* expected */ SCREEN_ORIENTATION_PORTRAIT);
+            robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_LANDSCAPE,
+                    /* expected */ SCREEN_ORIENTATION_LANDSCAPE);
+        });
+    }
+
+    @Test
+    public void testOverrideOrientationIfNeeded_userAspectRatioNotApplied_isUnchanged() {
+        runTestScenarioWithActivity((robot) -> {
+            robot.prepareShouldApplyUserFullscreenOverride(false);
+
+            robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_UNSPECIFIED,
+                    /* expected */ SCREEN_ORIENTATION_UNSPECIFIED);
+        });
+    }
+
+
+    /**
+     * Runs a test scenario with an existing activity providing a Robot.
+     */
+    void runTestScenarioWithActivity(@NonNull Consumer<OrientationPolicyRobotTest> consumer) {
+        runTestScenario(/* withActivity */ true, consumer);
+    }
+
+    /**
+     * Runs a test scenario without an existing activity providing a Robot.
+     */
+    void runTestScenario(@NonNull Consumer<OrientationPolicyRobotTest> consumer) {
+        runTestScenario(/* withActivity */ false, consumer);
+    }
+
+    /**
+     * Runs a test scenario providing a Robot.
+     */
+    void runTestScenario(boolean withActivity,
+                         @NonNull Consumer<OrientationPolicyRobotTest> consumer) {
+        spyOn(mWm.mLetterboxConfiguration);
+        final OrientationPolicyRobotTest robot =
+                new OrientationPolicyRobotTest(mWm, mAtm, mSupervisor, withActivity);
+        consumer.accept(robot);
+    }
+
+    private static class OrientationPolicyRobotTest {
+
+        @NonNull
+        private final ActivityTaskManagerService mAtm;
+        @NonNull
+        private final WindowManagerService mWm;
+        @NonNull
+        private final LetterboxConfiguration mLetterboxConfiguration;
+        @NonNull
+        private final TestComponentStack<ActivityRecord> mActivityStack;
+        @NonNull
+        private final TestComponentStack<Task> mTaskStack;
+
+        @NonNull
+        private final ActivityTaskSupervisor mSupervisor;
+
+        OrientationPolicyRobotTest(@NonNull WindowManagerService wm,
+                                   @NonNull ActivityTaskManagerService atm,
+                                   @NonNull ActivityTaskSupervisor supervisor,
+                                   boolean withActivity) {
+            mAtm = atm;
+            mWm = wm;
+            spyOn(mWm);
+            mSupervisor = supervisor;
+            mActivityStack = new TestComponentStack<>();
+            mTaskStack = new TestComponentStack<>();
+            mLetterboxConfiguration = mWm.mLetterboxConfiguration;
+            if (withActivity) {
+                createActivityWithComponent();
+            }
+        }
+
+        void configureSetIgnoreOrientationRequest(boolean enabled) {
+            mActivityStack.top().mDisplayContent.setIgnoreOrientationRequest(enabled);
+        }
+
+        void configureIsUserAppAspectRatioFullscreenEnabled(boolean enabled) {
+            doReturn(enabled).when(mLetterboxConfiguration).isUserAppAspectRatioFullscreenEnabled();
+        }
+
+        void configureIsCameraCompatTreatmentEnabled(boolean enabled) {
+            doReturn(enabled).when(mLetterboxConfiguration).isCameraCompatTreatmentEnabled();
+        }
+
+        void configureIsCameraCompatTreatmentEnabledAtBuildTime(boolean enabled) {
+            doReturn(enabled).when(mLetterboxConfiguration)
+                    .isCameraCompatTreatmentEnabledAtBuildTime();
+        }
+
+        void prepareGetUserMinAspectRatioOverrideCode(int orientation) {
+            spyOn(mActivityStack.top().mLetterboxUiController);
+            doReturn(orientation).when(mActivityStack.top()
+                    .mLetterboxUiController).getUserMinAspectRatioOverrideCode();
+        }
+
+        void prepareShouldApplyUserFullscreenOverride(boolean enabled) {
+            spyOn(mActivityStack.top().mLetterboxUiController);
+            doReturn(enabled).when(mActivityStack.top()
+                    .mLetterboxUiController).shouldApplyUserFullscreenOverride();
+        }
+
+        void prepareShouldApplyUserMinAspectRatioOverride(boolean enabled) {
+            spyOn(mActivityStack.top().mLetterboxUiController);
+            doReturn(enabled).when(mActivityStack.top()
+                    .mLetterboxUiController).shouldApplyUserMinAspectRatioOverride();
+        }
+
+        void prepareIsUserAppAspectRatioSettingsEnabled(boolean enabled) {
+            doReturn(enabled).when(mLetterboxConfiguration).isUserAppAspectRatioSettingsEnabled();
+        }
+
+        void prepareIsTopActivityEligibleForOrientationOverride(boolean enabled) {
+            final DisplayRotationCompatPolicy displayPolicy =
+                    mActivityStack.top().mDisplayContent.mDisplayRotationCompatPolicy;
+            spyOn(displayPolicy);
+            doReturn(enabled).when(displayPolicy)
+                    .isActivityEligibleForOrientationOverride(eq(mActivityStack.top()));
+        }
+
+        void configureIsTopActivityCameraActive(boolean enabled) {
+            final DisplayRotationCompatPolicy displayPolicy =
+                    mActivityStack.top().mDisplayContent.mDisplayRotationCompatPolicy;
+            spyOn(displayPolicy);
+            doReturn(enabled).when(displayPolicy)
+                    .isCameraActive(eq(mActivityStack.top()), /* mustBeFullscreen= */ eq(true));
+        }
+
+        void disableProperty(@NonNull String propertyName) {
+            setPropertyValue(propertyName, /* enabled */ false);
+        }
+
+        int overrideOrientationIfNeeded(@ActivityInfo.ScreenOrientation int candidate) {
+            return mActivityStack.top().mAppCompatController.getOrientationPolicy()
+                    .overrideOrientationIfNeeded(candidate);
+        }
+
+        void checkOrientationRequestMapped() {
+            verify(mWm).mapOrientationRequest(SCREEN_ORIENTATION_PORTRAIT);
+        }
+
+        void checkOverrideOrientation(@ActivityInfo.ScreenOrientation int candidate,
+                                      @ActivityInfo.ScreenOrientation int expected) {
+            Assert.assertEquals(expected, overrideOrientationIfNeeded(candidate));
+        }
+
+        void checkOverrideOrientationIsNot(@ActivityInfo.ScreenOrientation int candidate,
+                                           @ActivityInfo.ScreenOrientation int notExpected) {
+            Assert.assertNotEquals(notExpected, overrideOrientationIfNeeded(candidate));
+        }
+
+        private void createActivityWithComponent() {
+            if (mTaskStack.isEmpty()) {
+                final DisplayContent displayContent = new TestDisplayContent
+                        .Builder(mAtm, /* dw */ 1000, /* dh */ 2000).build();
+                final Task task = new TaskBuilder(mSupervisor).setDisplay(displayContent).build();
+                mTaskStack.push(task);
+            }
+            final ActivityRecord activity = new ActivityBuilder(mAtm)
+                    .setOnTop(true)
+                    .setTask(mTaskStack.top())
+                    // Set the component to be that of the test class in order
+                    // to enable compat changes
+                    .setComponent(ComponentName.createRelative(mAtm.mContext,
+                            com.android.server.wm.LetterboxUiControllerTest.class.getName()))
+                    .build();
+            mActivityStack.push(activity);
+        }
+
+        private void createActivityWithComponentInNewTask() {
+            final DisplayContent displayContent = new TestDisplayContent
+                    .Builder(mAtm, /* dw */ 1000, /* dh */ 2000).build();
+            final Task task = new TaskBuilder(mSupervisor).setDisplay(displayContent).build();
+            final ActivityRecord activity = new ActivityBuilder(mAtm)
+                    .setOnTop(true)
+                    .setTask(task)
+                    // Set the component to be that of the test class in order
+                    // to enable compat changes
+                    .setComponent(ComponentName.createRelative(mAtm.mContext,
+                            com.android.server.wm.LetterboxUiControllerTest.class.getName()))
+                    .build();
+            mTaskStack.push(task);
+            mActivityStack.push(activity);
+        }
+
+        private void setPropertyValue(@NonNull String propertyName, boolean enabled) {
+            PackageManager.Property property = new PackageManager.Property(propertyName,
+                    /* value */ enabled, /* packageName */ "",
+                    /* className */ "");
+            PackageManager pm = mWm.mContext.getPackageManager();
+            spyOn(pm);
+            try {
+                doReturn(property).when(pm).getProperty(eq(propertyName), anyString());
+            } catch (PackageManager.NameNotFoundException e) {
+                fail(e.getLocalizedMessage());
+            }
+        }
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/ClientLifecycleManagerTests.java b/services/tests/wmtests/src/com/android/server/wm/ClientLifecycleManagerTests.java
index b21eca7..2bda950 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ClientLifecycleManagerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ClientLifecycleManagerTests.java
@@ -19,7 +19,6 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
-import static com.android.window.flags.Flags.FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
@@ -106,31 +105,7 @@
     }
 
     @Test
-    public void testScheduleTransactionItem_notBundle() throws RemoteException {
-        mSetFlagsRule.disableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
-        // Use non binder client to get non-recycled ClientTransaction.
-        mLifecycleManager.scheduleTransactionItem(mNonBinderClient, mTransactionItem);
-
-        verify(mLifecycleManager).scheduleTransaction(mTransactionCaptor.capture());
-        ClientTransaction transaction = mTransactionCaptor.getValue();
-        assertEquals(1, transaction.getCallbacks().size());
-        assertEquals(mTransactionItem, transaction.getCallbacks().get(0));
-        assertNull(transaction.getLifecycleStateRequest());
-        assertNull(transaction.getTransactionItems());
-
-        clearInvocations(mLifecycleManager);
-        mLifecycleManager.scheduleTransactionItem(mNonBinderClient, mLifecycleItem);
-
-        verify(mLifecycleManager).scheduleTransaction(mTransactionCaptor.capture());
-        transaction = mTransactionCaptor.getValue();
-        assertNull(transaction.getCallbacks());
-        assertEquals(mLifecycleItem, transaction.getLifecycleStateRequest());
-    }
-
-    @Test
     public void testScheduleTransactionItem() throws RemoteException {
-        mSetFlagsRule.enableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
         spyOn(mWms.mWindowPlacerLocked);
         doReturn(true).when(mWms.mWindowPlacerLocked).isTraversalScheduled();
 
@@ -176,23 +151,7 @@
     }
 
     @Test
-    public void testScheduleTransactionAndLifecycleItems_notBundle() throws RemoteException {
-        mSetFlagsRule.disableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
-        // Use non binder client to get non-recycled ClientTransaction.
-        mLifecycleManager.scheduleTransactionAndLifecycleItems(mNonBinderClient, mTransactionItem,
-                mLifecycleItem);
-
-        verify(mLifecycleManager).scheduleTransaction(mTransactionCaptor.capture());
-        final ClientTransaction transaction = mTransactionCaptor.getValue();
-        assertEquals(1, transaction.getCallbacks().size());
-        assertEquals(mTransactionItem, transaction.getCallbacks().get(0));
-        assertEquals(mLifecycleItem, transaction.getLifecycleStateRequest());
-    }
-
-    @Test
     public void testScheduleTransactionAndLifecycleItems() throws RemoteException {
-        mSetFlagsRule.enableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
         spyOn(mWms.mWindowPlacerLocked);
         doReturn(true).when(mWms.mWindowPlacerLocked).isTraversalScheduled();
 
@@ -216,7 +175,6 @@
     @Test
     public void testScheduleTransactionAndLifecycleItems_shouldDispatchImmediately()
             throws RemoteException {
-        mSetFlagsRule.enableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
         spyOn(mWms.mWindowPlacerLocked);
         doReturn(true).when(mWms.mWindowPlacerLocked).isTraversalScheduled();
 
@@ -230,8 +188,6 @@
 
     @Test
     public void testDispatchPendingTransactions() throws RemoteException {
-        mSetFlagsRule.enableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
         mLifecycleManager.mPendingTransactions.put(mClientBinder, mTransaction);
 
         mLifecycleManager.dispatchPendingTransactions();
@@ -243,7 +199,6 @@
 
     @Test
     public void testLayoutDeferred() throws RemoteException {
-        mSetFlagsRule.enableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
         spyOn(mWms.mWindowPlacerLocked);
         doReturn(false).when(mWms.mWindowPlacerLocked).isInLayout();
         doReturn(false).when(mWms.mWindowPlacerLocked).isTraversalScheduled();
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
index 0787052..bdd45c6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
@@ -18,29 +18,14 @@
 
 import static android.content.pm.ActivityInfo.FORCE_NON_RESIZE_APP;
 import static android.content.pm.ActivityInfo.FORCE_RESIZE_APP;
-import static android.content.pm.ActivityInfo.OVERRIDE_ANY_ORIENTATION;
-import static android.content.pm.ActivityInfo.OVERRIDE_ANY_ORIENTATION_TO_USER;
 import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION;
 import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT;
 import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH;
 import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
 import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS;
-import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED;
-import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION;
-import static android.content.pm.ActivityInfo.OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE;
 import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO;
 import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA;
-import static android.content.pm.ActivityInfo.OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA;
-import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR;
-import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT;
 import static android.content.pm.ActivityInfo.OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION;
-import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
-import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LOCKED;
-import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
-import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
-import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
-import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
-import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER;
 import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_3_2;
 import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_FULLSCREEN;
 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
@@ -50,21 +35,16 @@
 import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH;
 import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
 import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE;
-import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED;
 import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE;
-import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE;
 import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES;
 import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE;
 import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE;
 import static android.view.WindowManager.PROPERTY_COMPAT_ENABLE_FAKE_FOCUS;
-import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
-import static com.android.server.wm.AppCompatOrientationCapability.OrientationCapabilityState.MIN_COUNT_TO_IGNORE_REQUEST_IN_LOOP;
-import static com.android.server.wm.AppCompatOrientationCapability.OrientationCapabilityState.SET_ORIENTATION_REQUEST_COUNTER_TIMEOUT_MS;
 import static com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM;
 
 import static org.junit.Assert.assertEquals;
@@ -149,187 +129,6 @@
         mController = new LetterboxUiController(mWm, mActivity);
     }
 
-    // shouldIgnoreRequestedOrientation
-
-    @Test
-    @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION})
-    public void testShouldIgnoreRequestedOrientation_activityRelaunching_returnsTrue() {
-        prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch();
-
-        assertTrue(mActivity.mAppCompatController.getAppCompatCapability()
-                .getAppCompatOrientationCapability()
-                .shouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED));
-    }
-
-    @Test
-    @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION})
-    public void testShouldIgnoreRequestedOrientation_cameraCompatTreatment_returnsTrue() {
-        doReturn(true).when(mLetterboxConfiguration).isCameraCompatTreatmentEnabled();
-        doReturn(true).when(mLetterboxConfiguration)
-                .isCameraCompatTreatmentEnabledAtBuildTime();
-
-        // Recreate DisplayContent with DisplayRotationCompatPolicy
-        mActivity = setUpActivityWithComponent();
-        mController = new LetterboxUiController(mWm, mActivity);
-        prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch();
-        mController.setRelaunchingAfterRequestedOrientationChanged(false);
-
-        spyOn(mDisplayContent.mDisplayRotationCompatPolicy);
-        doReturn(true).when(mDisplayContent.mDisplayRotationCompatPolicy)
-                .isTreatmentEnabledForActivity(eq(mActivity));
-
-        assertTrue(mActivity.mAppCompatController.getAppCompatCapability()
-                .getAppCompatOrientationCapability()
-                .shouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED));
-    }
-
-    @Test
-    public void testShouldIgnoreRequestedOrientation_overrideDisabled_returnsFalse() {
-        prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch();
-
-        assertFalse(mActivity.mAppCompatController.getAppCompatCapability()
-                .getAppCompatOrientationCapability()
-                .shouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED));
-    }
-
-    @Test
-    public void testShouldIgnoreRequestedOrientation_propertyIsTrue_returnsTrue()
-            throws Exception {
-        doReturn(true).when(mLetterboxConfiguration)
-                .isPolicyForIgnoringRequestedOrientationEnabled();
-        mockThatProperty(PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION, /* value */ true);
-        mController = new LetterboxUiController(mWm, mActivity);
-        prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch();
-
-        assertTrue(mActivity.mAppCompatController.getAppCompatCapability()
-                .getAppCompatOrientationCapability()
-                .shouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED));
-    }
-
-    @Test
-    @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION})
-    public void testShouldIgnoreRequestedOrientation_propertyIsFalseAndOverride_returnsFalse()
-            throws Exception {
-        doReturn(true).when(mLetterboxConfiguration)
-                .isPolicyForIgnoringRequestedOrientationEnabled();
-        mockThatProperty(PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION, /* value */ false);
-
-        mController = new LetterboxUiController(mWm, mActivity);
-        prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch();
-
-        assertFalse(mActivity.mAppCompatController.getAppCompatCapability()
-                .getAppCompatOrientationCapability()
-                .shouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED));
-    }
-
-    @Test
-    public void testShouldIgnoreOrientationRequestLoop_overrideDisabled_returnsFalse() {
-        doReturn(true).when(mLetterboxConfiguration)
-                .isPolicyForIgnoringRequestedOrientationEnabled();
-        doReturn(false).when(mActivity).isLetterboxedForFixedOrientationAndAspectRatio();
-        // Request 3 times to simulate orientation request loop
-        for (int i = 0; i <= MIN_COUNT_TO_IGNORE_REQUEST_IN_LOOP; i++) {
-            assertShouldIgnoreOrientationRequestLoop(/* shouldIgnore */ false,
-                    /* expectedCount */ 0);
-        }
-    }
-
-    @Test
-    @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED})
-    public void testShouldIgnoreOrientationRequestLoop_propertyIsFalseAndOverride_returnsFalse()
-            throws Exception {
-        doReturn(true).when(mLetterboxConfiguration)
-                .isPolicyForIgnoringRequestedOrientationEnabled();
-        mockThatProperty(PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED,
-                /* value */ false);
-        doReturn(false).when(mActivity).isLetterboxedForFixedOrientationAndAspectRatio();
-
-        mController = new LetterboxUiController(mWm, mActivity);
-
-        // Request 3 times to simulate orientation request loop
-        for (int i = 0; i <= MIN_COUNT_TO_IGNORE_REQUEST_IN_LOOP; i++) {
-            assertShouldIgnoreOrientationRequestLoop(/* shouldIgnore */ false,
-                    /* expectedCount */ 0);
-        }
-    }
-
-    @Test
-    @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED})
-    public void testShouldIgnoreOrientationRequestLoop_isLetterboxed_returnsFalse() {
-        doReturn(true).when(mLetterboxConfiguration)
-                .isPolicyForIgnoringRequestedOrientationEnabled();
-        doReturn(true).when(mActivity).isLetterboxedForFixedOrientationAndAspectRatio();
-
-        // Request 3 times to simulate orientation request loop
-        for (int i = 0; i <= MIN_COUNT_TO_IGNORE_REQUEST_IN_LOOP; i++) {
-            assertShouldIgnoreOrientationRequestLoop(/* shouldIgnore */ false,
-                    /* expectedCount */ i);
-        }
-    }
-
-    @Test
-    @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED})
-    public void testShouldIgnoreOrientationRequestLoop_noLoop_returnsFalse() {
-        doReturn(true).when(mLetterboxConfiguration)
-                .isPolicyForIgnoringRequestedOrientationEnabled();
-        doReturn(false).when(mActivity).isLetterboxedForFixedOrientationAndAspectRatio();
-
-        // No orientation request loop
-        assertShouldIgnoreOrientationRequestLoop(/* shouldIgnore */ false,
-                /* expectedCount */ 0);
-    }
-
-    @Test
-    @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED})
-    public void testShouldIgnoreOrientationRequestLoop_timeout_returnsFalse()
-            throws InterruptedException {
-        doReturn(true).when(mLetterboxConfiguration)
-                .isPolicyForIgnoringRequestedOrientationEnabled();
-        doReturn(false).when(mActivity).isLetterboxedForFixedOrientationAndAspectRatio();
-
-        for (int i = MIN_COUNT_TO_IGNORE_REQUEST_IN_LOOP; i > 0; i--) {
-            assertShouldIgnoreOrientationRequestLoop(/* shouldIgnore */ false,
-                    /* expectedCount */ 0);
-            Thread.sleep(SET_ORIENTATION_REQUEST_COUNTER_TIMEOUT_MS);
-        }
-    }
-
-    @Test
-    @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED})
-    public void testShouldIgnoreOrientationRequestLoop_returnsTrue() {
-        doReturn(true).when(mLetterboxConfiguration)
-                .isPolicyForIgnoringRequestedOrientationEnabled();
-        doReturn(false).when(mActivity).isLetterboxedForFixedOrientationAndAspectRatio();
-
-        for (int i = 0; i < MIN_COUNT_TO_IGNORE_REQUEST_IN_LOOP; i++) {
-            assertShouldIgnoreOrientationRequestLoop(/* shouldIgnore */ false,
-                    /* expectedCount */ i);
-        }
-        assertShouldIgnoreOrientationRequestLoop(/* shouldIgnore */ true,
-                /* expectedCount */ MIN_COUNT_TO_IGNORE_REQUEST_IN_LOOP);
-    }
-
-    private void assertShouldIgnoreOrientationRequestLoop(boolean shouldIgnore, int expectedCount) {
-        if (shouldIgnore) {
-            assertTrue(mController.shouldIgnoreOrientationRequestLoop());
-        } else {
-            assertFalse(mController.shouldIgnoreOrientationRequestLoop());
-        }
-        assertEquals(expectedCount, mController.getSetOrientationRequestCounter());
-    }
-
-    @Test
-    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH})
-    public void testShouldIgnoreRequestedOrientation_flagIsDisabled_returnsFalse() {
-        prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch();
-        doReturn(false).when(mLetterboxConfiguration)
-                .isPolicyForIgnoringRequestedOrientationEnabled();
-
-        assertFalse(mActivity.mAppCompatController.getAppCompatCapability()
-                .getAppCompatOrientationCapability()
-                .shouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED));
-    }
-
     // shouldRefreshActivityForCameraCompat
 
     @Test
@@ -722,322 +521,6 @@
         return mainWindow;
     }
 
-    // overrideOrientationIfNeeded
-
-    @Test
-    public void testOverrideOrientationIfNeeded_mapInvokedOnRequest() throws Exception {
-        mController = new LetterboxUiController(mWm, mActivity);
-        spyOn(mWm);
-
-        mController.overrideOrientationIfNeeded(SCREEN_ORIENTATION_PORTRAIT);
-
-        verify(mWm).mapOrientationRequest(SCREEN_ORIENTATION_PORTRAIT);
-    }
-
-    @Test
-    @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
-    public void testOverrideOrientationIfNeeded_fullscreenOverrideEnabled_returnsUser()
-            throws Exception {
-        mDisplayContent.setIgnoreOrientationRequest(true);
-        assertEquals(SCREEN_ORIENTATION_USER, mController.overrideOrientationIfNeeded(
-                /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
-    }
-
-    @Test
-    @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
-    public void testOverrideOrientationIfNeeded_fullscreenOverrideEnabled_optOut_returnsUnchanged()
-            throws Exception {
-        mockThatProperty(PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE, /* value */ false);
-
-        mActivity = setUpActivityWithComponent();
-        mController = new LetterboxUiController(mWm, mActivity);
-        mDisplayContent.setIgnoreOrientationRequest(true);
-
-        assertEquals(SCREEN_ORIENTATION_PORTRAIT, mController.overrideOrientationIfNeeded(
-                /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
-    }
-
-    @Test
-    @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
-    public void testOverrideOrientationIfNeeded_fullscreenOverrides_optOutSystem_returnsUser()
-            throws Exception {
-        mockThatProperty(PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE, /* value */ false);
-        prepareActivityThatShouldApplyUserFullscreenOverride();
-
-        // fullscreen override still applied
-        assertEquals(SCREEN_ORIENTATION_USER, mController.overrideOrientationIfNeeded(
-                /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
-    }
-
-    @Test
-    @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
-    public void testOverrideOrientationIfNeeded_fullscreenOverrides_optOutUser_returnsUser()
-            throws Exception {
-        mockThatProperty(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE,
-                /* value */ false);
-        prepareActivityThatShouldApplyUserFullscreenOverride();
-
-        // fullscreen override still applied
-        assertEquals(SCREEN_ORIENTATION_USER, mController.overrideOrientationIfNeeded(
-                /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
-    }
-
-    @Test
-    @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
-    public void testOverrideOrientationIfNeeded_fullscreenOverrideEnabled_returnsUnchanged()
-            throws Exception {
-        mDisplayContent.setIgnoreOrientationRequest(false);
-        assertEquals(SCREEN_ORIENTATION_PORTRAIT, mController.overrideOrientationIfNeeded(
-                /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
-    }
-
-    @Test
-    @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
-    public void testOverrideOrientationIfNeeded_fullscreenAndUserOverrideEnabled_returnsUnchanged()
-            throws Exception {
-        doReturn(true).when(mLetterboxConfiguration).isUserAppAspectRatioSettingsEnabled();
-        mActivity = setUpActivityWithComponent();
-        spyOn(mActivity.mLetterboxUiController);
-        doReturn(USER_MIN_ASPECT_RATIO_3_2).when(mActivity.mLetterboxUiController)
-                .getUserMinAspectRatioOverrideCode();
-
-        assertEquals(SCREEN_ORIENTATION_PORTRAIT, mActivity.mLetterboxUiController
-                .overrideOrientationIfNeeded(/* candidate */ SCREEN_ORIENTATION_PORTRAIT));
-    }
-
-    @Test
-    @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT})
-    public void testOverrideOrientationIfNeeded_portraitOverrideEnabled_returnsPortrait()
-            throws Exception {
-        assertEquals(SCREEN_ORIENTATION_PORTRAIT, mController.overrideOrientationIfNeeded(
-                /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED));
-    }
-
-    @Test
-    @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR})
-    public void testOverrideOrientationIfNeeded_portraitOverrideEnabled_returnsNosensor() {
-        assertEquals(SCREEN_ORIENTATION_NOSENSOR, mController.overrideOrientationIfNeeded(
-                /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED));
-    }
-
-    @Test
-    @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR})
-    public void testOverrideOrientationIfNeeded_nosensorOverride_orientationFixed_returnsUnchanged() {
-        assertEquals(SCREEN_ORIENTATION_PORTRAIT, mController.overrideOrientationIfNeeded(
-                /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
-    }
-
-    @Test
-    @EnableCompatChanges({OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE})
-    public void testOverrideOrientationIfNeeded_reverseLandscapeOverride_orientationPortraitOrUndefined_returnsUnchanged() {
-        assertEquals(SCREEN_ORIENTATION_PORTRAIT, mController.overrideOrientationIfNeeded(
-                /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
-        assertEquals(SCREEN_ORIENTATION_UNSPECIFIED, mController.overrideOrientationIfNeeded(
-                /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED));
-    }
-
-    @Test
-    @EnableCompatChanges({OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE})
-    public void testOverrideOrientationIfNeeded_reverseLandscapeOverride_orientationLandscape_returnsReverseLandscape() {
-        assertEquals(SCREEN_ORIENTATION_REVERSE_LANDSCAPE, mController.overrideOrientationIfNeeded(
-                /* candidate */ SCREEN_ORIENTATION_LANDSCAPE));
-    }
-
-    @Test
-    @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT})
-    public void testOverrideOrientationIfNeeded_portraitOverride_orientationFixed_returnsUnchanged() {
-        assertEquals(SCREEN_ORIENTATION_NOSENSOR, mController.overrideOrientationIfNeeded(
-                /* candidate */ SCREEN_ORIENTATION_NOSENSOR));
-    }
-
-    @Test
-    @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT, OVERRIDE_ANY_ORIENTATION})
-    public void testOverrideOrientationIfNeeded_portraitAndIgnoreFixedOverrides_returnsPortrait() {
-        assertEquals(SCREEN_ORIENTATION_PORTRAIT, mController.overrideOrientationIfNeeded(
-                /* candidate */ SCREEN_ORIENTATION_NOSENSOR));
-    }
-
-    @Test
-    @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR, OVERRIDE_ANY_ORIENTATION})
-    public void testOverrideOrientationIfNeeded_noSensorAndIgnoreFixedOverrides_returnsNosensor() {
-        assertEquals(SCREEN_ORIENTATION_NOSENSOR, mController.overrideOrientationIfNeeded(
-                /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
-    }
-
-    @Test
-    @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT})
-    public void testOverrideOrientationIfNeeded_propertyIsFalse_returnsUnchanged()
-            throws Exception {
-        mockThatProperty(PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE, /* value */ false);
-
-        mActivity = setUpActivityWithComponent();
-        mController = new LetterboxUiController(mWm, mActivity);
-
-        assertEquals(SCREEN_ORIENTATION_UNSPECIFIED, mController.overrideOrientationIfNeeded(
-                /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED));
-    }
-
-    @Test
-    @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT,
-            OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA})
-    public void testOverrideOrientationIfNeeded_whenCameraNotActive_returnsUnchanged() {
-        doReturn(true).when(mLetterboxConfiguration).isCameraCompatTreatmentEnabled();
-        doReturn(true).when(mLetterboxConfiguration)
-                .isCameraCompatTreatmentEnabledAtBuildTime();
-
-        // Recreate DisplayContent with DisplayRotationCompatPolicy
-        mActivity = setUpActivityWithComponent();
-        mController = new LetterboxUiController(mWm, mActivity);
-
-        spyOn(mDisplayContent.mDisplayRotationCompatPolicy);
-        doReturn(false).when(mDisplayContent.mDisplayRotationCompatPolicy)
-                .isActivityEligibleForOrientationOverride(eq(mActivity));
-
-        assertEquals(SCREEN_ORIENTATION_UNSPECIFIED, mController.overrideOrientationIfNeeded(
-                /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED));
-    }
-
-    @Test
-    @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT,
-            OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA})
-    public void testOverrideOrientationIfNeeded_whenCameraActive_returnsPortrait() {
-        doReturn(true).when(mLetterboxConfiguration).isCameraCompatTreatmentEnabled();
-        doReturn(true).when(mLetterboxConfiguration)
-                .isCameraCompatTreatmentEnabledAtBuildTime();
-
-        // Recreate DisplayContent with DisplayRotationCompatPolicy
-        mActivity = setUpActivityWithComponent();
-        mController = new LetterboxUiController(mWm, mActivity);
-
-        spyOn(mDisplayContent.mDisplayRotationCompatPolicy);
-        doReturn(true).when(mDisplayContent.mDisplayRotationCompatPolicy)
-                .isActivityEligibleForOrientationOverride(eq(mActivity));
-
-        assertEquals(SCREEN_ORIENTATION_PORTRAIT, mController.overrideOrientationIfNeeded(
-                /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED));
-    }
-
-    @Test
-    public void testOverrideOrientationIfNeeded_userFullscreenOverride_returnsUser() {
-        spyOn(mActivity.mLetterboxUiController);
-        doReturn(true).when(mActivity.mLetterboxUiController)
-                .shouldApplyUserFullscreenOverride();
-        mDisplayContent.setIgnoreOrientationRequest(true);
-
-        assertEquals(SCREEN_ORIENTATION_USER, mActivity.mAppCompatController
-                .getOrientationPolicy()
-                .overrideOrientationIfNeeded(/* candidate */ SCREEN_ORIENTATION_UNSPECIFIED));
-    }
-
-    @Test
-    public void testOverrideOrientationIfNeeded_userFullscreenOverride_cameraActivity_noChange() {
-        doReturn(true).when(mLetterboxConfiguration).isCameraCompatTreatmentEnabled();
-        doReturn(true).when(mLetterboxConfiguration)
-                .isCameraCompatTreatmentEnabledAtBuildTime();
-
-        // Recreate DisplayContent with DisplayRotationCompatPolicy
-        mActivity = setUpActivityWithComponent();
-        mController = new LetterboxUiController(mWm, mActivity);
-        spyOn(mController);
-        doReturn(true).when(mController).shouldApplyUserFullscreenOverride();
-
-        spyOn(mDisplayContent.mDisplayRotationCompatPolicy);
-        doReturn(true).when(mDisplayContent.mDisplayRotationCompatPolicy)
-                .isCameraActive(mActivity, /* mustBeFullscreen= */ true);
-
-        assertEquals(SCREEN_ORIENTATION_PORTRAIT, mController.overrideOrientationIfNeeded(
-                /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
-    }
-
-    @Test
-    public void testOverrideOrientationIfNeeded_systemFullscreenOverride_cameraActivity_noChange() {
-        doReturn(true).when(mLetterboxConfiguration).isCameraCompatTreatmentEnabled();
-        doReturn(true).when(mLetterboxConfiguration)
-                .isCameraCompatTreatmentEnabledAtBuildTime();
-
-        // Recreate DisplayContent with DisplayRotationCompatPolicy
-        mActivity = setUpActivityWithComponent();
-        mController = new LetterboxUiController(mWm, mActivity);
-        spyOn(mController);
-        doReturn(true).when(mController).isSystemOverrideToFullscreenEnabled();
-
-        spyOn(mDisplayContent.mDisplayRotationCompatPolicy);
-        doReturn(true).when(mDisplayContent.mDisplayRotationCompatPolicy)
-                .isCameraActive(mActivity, /* mustBeFullscreen= */ true);
-
-        assertEquals(SCREEN_ORIENTATION_PORTRAIT, mController.overrideOrientationIfNeeded(
-                /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
-    }
-
-    @Test
-    public void testOverrideOrientationIfNeeded_respectOrientationRequestOverUserFullScreen() {
-        spyOn(mController);
-        doReturn(true).when(mController).shouldApplyUserFullscreenOverride();
-        mDisplayContent.setIgnoreOrientationRequest(false);
-
-        assertNotEquals(SCREEN_ORIENTATION_USER, mController.overrideOrientationIfNeeded(
-                /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED));
-    }
-
-    @Test
-    @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT, OVERRIDE_ANY_ORIENTATION})
-    public void testOverrideOrientationIfNeeded_userFullScreenOverrideOverSystem_returnsUser() {
-        spyOn(mActivity.mLetterboxUiController);
-        doReturn(true).when(mActivity.mLetterboxUiController)
-                .shouldApplyUserFullscreenOverride();
-        mDisplayContent.setIgnoreOrientationRequest(true);
-
-        assertEquals(SCREEN_ORIENTATION_USER, mActivity.mAppCompatController
-                .getOrientationPolicy()
-                .overrideOrientationIfNeeded(/* candidate */ SCREEN_ORIENTATION_PORTRAIT));
-    }
-
-    @Test
-    @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT, OVERRIDE_ANY_ORIENTATION})
-    public void testOverrideOrientationIfNeeded_respectOrientationReqOverUserFullScreenAndSystem() {
-        spyOn(mController);
-        doReturn(true).when(mController).shouldApplyUserFullscreenOverride();
-        mDisplayContent.setIgnoreOrientationRequest(false);
-
-        assertNotEquals(SCREEN_ORIENTATION_USER, mController.overrideOrientationIfNeeded(
-                /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
-    }
-
-    @Test
-    public void testOverrideOrientationIfNeeded_userFullScreenOverrideDisabled_returnsUnchanged() {
-        spyOn(mController);
-        doReturn(false).when(mController).shouldApplyUserFullscreenOverride();
-
-        assertEquals(SCREEN_ORIENTATION_PORTRAIT, mController.overrideOrientationIfNeeded(
-                /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
-    }
-
-    @Test
-    public void testOverrideOrientationIfNeeded_userAspectRatioApplied_unspecifiedOverridden() {
-        spyOn(mActivity.mLetterboxUiController);
-        doReturn(true).when(mActivity.mLetterboxUiController)
-                .shouldApplyUserMinAspectRatioOverride();
-
-        assertEquals(SCREEN_ORIENTATION_PORTRAIT, mActivity.mLetterboxUiController
-                .overrideOrientationIfNeeded(/* candidate */ SCREEN_ORIENTATION_UNSPECIFIED));
-
-        assertEquals(SCREEN_ORIENTATION_PORTRAIT, mActivity.mLetterboxUiController
-                .overrideOrientationIfNeeded(/* candidate */ SCREEN_ORIENTATION_LOCKED));
-
-        // unchanged if orientation is specified
-        assertEquals(SCREEN_ORIENTATION_LANDSCAPE, mActivity.mLetterboxUiController
-                .overrideOrientationIfNeeded(/* candidate */ SCREEN_ORIENTATION_LANDSCAPE));
-    }
-
-    @Test
-    public void testOverrideOrientationIfNeeded_userAspectRatioNotApplied_returnsUnchanged() {
-        spyOn(mController);
-        doReturn(false).when(mController).shouldApplyUserMinAspectRatioOverride();
-
-        assertEquals(SCREEN_ORIENTATION_UNSPECIFIED, mController.overrideOrientationIfNeeded(
-                /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED));
-    }
-
     // shouldApplyUser...Override
     @Test
     public void testShouldApplyUserFullscreenOverride_trueProperty_returnsFalse() throws Exception {
@@ -1715,12 +1198,6 @@
         mDisplayContent.setIgnoreOrientationRequest(true);
     }
 
-    private void prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch() {
-        doReturn(true).when(mLetterboxConfiguration)
-                .isPolicyForIgnoringRequestedOrientationEnabled();
-        mController.setRelaunchingAfterRequestedOrientationChanged(true);
-    }
-
     private ActivityRecord setUpActivityWithComponent() {
         mDisplayContent = new TestDisplayContent
                 .Builder(mAtm, /* dw */ 1000, /* dh */ 2000).build();
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index ac1aa20..3a85451 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -4271,6 +4271,27 @@
 
     }
 
+    @Test
+    public void testInsetOverrideNotAppliedInFreeform() {
+        final int notchHeight = 100;
+        final DisplayContent display = new TestDisplayContent.Builder(mAtm, 1000, 2800)
+                .setNotch(notchHeight)
+                .build();
+        setUpApp(display);
+
+        // Simulate inset override for legacy app bound behaviour
+        mActivity.mResolveConfigHint.mUseOverrideInsetsForConfig = true;
+        // Set task as freeform
+        mTask.setWindowingMode(WindowConfiguration.WINDOWING_MODE_FREEFORM);
+        prepareUnresizable(mActivity,  SCREEN_ORIENTATION_PORTRAIT);
+
+        Rect bounds = new Rect(mActivity.getWindowConfiguration().getBounds());
+        Rect appBounds = new Rect(mActivity.getWindowConfiguration().getAppBounds());
+        // App bounds should not include insets and should match bounds when in freeform.
+        assertEquals(new Rect(0, 0, 1000, 2800), appBounds);
+        assertEquals(new Rect(0, 0, 1000, 2800), bounds);
+    }
+
     private void assertVerticalPositionForDifferentDisplayConfigsForLandscapeActivity(
             float letterboxVerticalPositionMultiplier, Rect fixedOrientationLetterbox,
             Rect sizeCompatUnscaled, Rect sizeCompatScaled) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java b/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java
index 6c5f975..1c32980 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java
@@ -33,6 +33,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -417,6 +418,22 @@
     }
 
     @Test
+    public void testSkipPrepareSync() {
+        final TestWindowContainer wc = new TestWindowContainer(mWm, true /* waiter */);
+        wc.mSkipPrepareSync = true;
+        final BLASTSyncEngine bse = createTestBLASTSyncEngine();
+        final BLASTSyncEngine.SyncGroup syncGroup = bse.prepareSyncSet(
+                mock(BLASTSyncEngine.TransactionReadyListener.class), "test");
+        bse.startSyncSet(syncGroup);
+        bse.addToSyncSet(syncGroup.mSyncId, wc);
+        assertEquals(SYNC_STATE_NONE, wc.mSyncState);
+        // If the implementation of prepareSync doesn't set sync state, the sync group should also
+        // be empty.
+        assertNull(wc.mSyncGroup);
+        assertTrue(wc.isSyncFinished(syncGroup));
+    }
+
+    @Test
     public void testNonBlastMethod() {
         mAppWindow = createWindow(null, TYPE_BASE_APPLICATION, "mAppWindow");
 
@@ -694,6 +711,7 @@
         final boolean mWaiter;
         boolean mVisibleRequested = true;
         boolean mFillsParent = false;
+        boolean mSkipPrepareSync = false;
 
         TestWindowContainer(WindowManagerService wms, boolean waiter) {
             super(wms);
@@ -703,6 +721,9 @@
 
         @Override
         boolean prepareSync() {
+            if (mSkipPrepareSync) {
+                return false;
+            }
             if (!super.prepareSync()) {
                 return false;
             }
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
index 9670a9a..a71b81e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -26,8 +26,8 @@
 import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_NONE;
 import static android.view.WindowManager.TRANSIT_OPEN;
-import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_TASK_FRAGMENT;
 import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE;
+import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_TASK_FRAGMENT;
 import static android.window.TaskFragmentOperation.OP_TYPE_DELETE_TASK_FRAGMENT;
 import static android.window.TaskFragmentOperation.OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE;
 import static android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_BOTTOM_OF_TASK;
@@ -220,30 +220,7 @@
     }
 
     @Test
-    public void testOnTaskFragmentAppeared_throughTaskFragmentOrganizer() throws RemoteException {
-        mSetFlagsRule.disableFlags(Flags.FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
-        // No-op when the TaskFragment is not attached.
-        mController.onTaskFragmentAppeared(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
-        mController.dispatchPendingEvents();
-
-        verify(mOrganizer, never()).onTransactionReady(any());
-        verify(mAppThread, never()).scheduleTaskFragmentTransaction(any(), any());
-
-        // Send callback when the TaskFragment is attached.
-        setupMockParent(mTaskFragment, mTask);
-
-        mController.onTaskFragmentAppeared(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
-        mController.dispatchPendingEvents();
-
-        assertTaskFragmentParentInfoChangedTransaction(mTask);
-        assertTaskFragmentAppearedTransaction(false /* hasSurfaceControl */);
-        verify(mAppThread, never()).scheduleTaskFragmentTransaction(any(), any());
-    }
-
-    @Test
     public void testOnTaskFragmentAppeared_throughApplicationThread() throws RemoteException  {
-        mSetFlagsRule.enableFlags(Flags.FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
         // Re-register the organizer in case the flag was disabled during setup.
         mController.unregisterOrganizer(mIOrganizer);
         registerTaskFragmentOrganizer(mIOrganizer);
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java
index 78cea95..628c65e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java
@@ -46,12 +46,14 @@
 import android.graphics.Rect;
 import android.platform.test.annotations.Presubmit;
 
+import androidx.annotation.NonNull;
+
+import com.android.server.wm.utils.TestComponentStack;
+
 import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import java.util.ArrayList;
-import java.util.List;
 import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Predicate;
@@ -318,20 +320,22 @@
      */
     private static class TransparentPolicyRobotTest {
 
+        @NonNull
         private final ActivityTaskManagerService mAtm;
-
+        @NonNull
         private final Task mTask;
-
-        private final ActivityStackTest mActivityStack;
-
+        @NonNull
+        private final TestComponentStack<ActivityRecord> mActivityStack;
+        @NonNull
         private WindowConfiguration mTopActivityWindowConfiguration;
 
-        private TransparentPolicyRobotTest(ActivityTaskManagerService atm, Task task,
-                                           ActivityRecord opaqueActivity) {
+        private TransparentPolicyRobotTest(@NonNull ActivityTaskManagerService atm,
+                                           @NonNull Task task,
+                                           @NonNull ActivityRecord opaqueActivity) {
             mAtm = atm;
             mTask = task;
-            mActivityStack = new ActivityStackTest();
-            mActivityStack.pushActivity(opaqueActivity);
+            mActivityStack = new TestComponentStack<>();
+            mActivityStack.push(opaqueActivity);
             spyOn(opaqueActivity.mAppCompatController.getTransparentPolicy());
         }
 
@@ -361,7 +365,7 @@
                 mTask.addChild(newActivity);
             }
             spyOn(newActivity.mAppCompatController.getTransparentPolicy());
-            mActivityStack.pushActivity(newActivity);
+            mActivityStack.push(newActivity);
         }
 
         void attachTopActivityToTask() {
@@ -596,45 +600,5 @@
             display.computeScreenConfiguration(c);
             display.onRequestedOverrideConfigurationChanged(c);
         }
-
-        /**
-         * Contains all the ActivityRecord launched in the test. This is different from what's in
-         * the Task because activities are added here even if not added to tasks.
-         */
-        private static class ActivityStackTest {
-            private final List<ActivityRecord> mActivities = new ArrayList<>();
-
-            void pushActivity(ActivityRecord activityRecord) {
-                mActivities.add(activityRecord);
-            }
-
-            void applyToTop(Consumer<ActivityRecord> consumer) {
-                consumer.accept(top());
-            }
-
-            ActivityRecord getFromTop(int fromTop) {
-                return mActivities.get(mActivities.size() - fromTop - 1);
-            }
-
-            ActivityRecord base() {
-                return mActivities.get(0);
-            }
-
-            ActivityRecord top() {
-                return mActivities.get(mActivities.size() - 1);
-            }
-
-            // Allows access to the activity at position beforeLast from the top.
-            // If fromTop = 0 the activity used is the top one.
-            void applyTo(int fromTop, Consumer<ActivityRecord> consumer) {
-                consumer.accept(getFromTop(fromTop));
-            }
-
-            void applyToAll(Consumer<ActivityRecord> consumer) {
-                for (int i = mActivities.size() - 1; i >= 0; i--) {
-                    consumer.accept(mActivities.get(i));
-                }
-            }
-        }
     }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/utils/TestComponentStack.java b/services/tests/wmtests/src/com/android/server/wm/utils/TestComponentStack.java
new file mode 100644
index 0000000..a06c0a2
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/utils/TestComponentStack.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.utils;
+
+import androidx.annotation.NonNull;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * Contains all the component created in the test.
+ */
+public class TestComponentStack<T> {
+
+    @NonNull
+    private final List<T> mItems = new ArrayList<>();
+
+    /**
+     * Adds an item to the stack.
+     *
+     * @param item The item to add.
+     */
+    public void push(@NonNull T item) {
+        mItems.add(item);
+    }
+
+    /**
+     * Consumes the top element of the stack.
+     *
+     * @param consumer Consumer for the optional top element.
+     * @throws IndexOutOfBoundsException In case that stack is empty.
+     */
+    public void applyToTop(@NonNull Consumer<T> consumer) {
+        consumer.accept(top());
+    }
+
+    /**
+     * Returns the item at fromTop position from the top one if present or it throws an
+     * exception if not present.
+     *
+     * @param fromTop The position from the top of the item to return.
+     * @return The returned item.
+     * @throws IndexOutOfBoundsException In case that position doesn't exist.
+     */
+    @NonNull
+    public T getFromTop(int fromTop) {
+        return mItems.get(mItems.size() - fromTop - 1);
+    }
+
+    /**
+     * @return The item at the base of the stack if present.
+     * @throws IndexOutOfBoundsException In case that stack is empty.
+     */
+    @NonNull
+    public T base() {
+        return mItems.get(0);
+    }
+
+    /**
+     * @return The item at the top of the stack if present.
+     * @throws IndexOutOfBoundsException In case that stack is empty.
+     */
+    @NonNull
+    public T top() {
+        return mItems.get(mItems.size() - 1);
+    }
+
+    /**
+     * @return {@code true} if the stack is empty.
+     */
+    public boolean isEmpty() {
+        return mItems.isEmpty();
+    }
+
+    /**
+     * Allows access to the item at position beforeLast from the top.
+     *
+     * @param fromTop  The position from the top of the item to return.
+     * @param consumer Consumer for the optional returned element.
+     */
+    public void applyTo(int fromTop, @NonNull Consumer<T> consumer) {
+        consumer.accept(getFromTop(fromTop));
+    }
+
+    /**
+     * Invoked the consumer iterating over all the elements in the stack.
+     *
+     * @param consumer Consumer for the elements.
+     */
+    public void applyToAll(@NonNull Consumer<T> consumer) {
+        for (int i = mItems.size() - 1; i >= 0; i--) {
+            consumer.accept(mItems.get(i));
+        }
+    }
+}
diff --git a/services/usage/java/com/android/server/usage/UsageStatsDatabase.java b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
index a8a9017..ba33eab 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
@@ -33,6 +33,7 @@
 import android.util.TimeUtils;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.IndentingPrintWriter;
 
@@ -136,6 +137,7 @@
     // The obfuscated packages to tokens mappings file
     private final File mPackageMappingsFile;
     // Holds all of the data related to the obfuscated packages and their token mappings.
+    @GuardedBy("mLock")
     final PackagesTokenData mPackagesTokenData = new PackagesTokenData();
 
     /**
@@ -771,27 +773,30 @@
      * all of the stats at once has an amortized cost for future calls.
      */
     void filterStats(IntervalStats stats) {
-        if (mPackagesTokenData.removedPackagesMap.isEmpty()) {
-            return;
-        }
-        final ArrayMap<String, Long> removedPackagesMap = mPackagesTokenData.removedPackagesMap;
-
-        // filter out package usage stats
-        final int removedPackagesSize = removedPackagesMap.size();
-        for (int i = 0; i < removedPackagesSize; i++) {
-            final String removedPackage = removedPackagesMap.keyAt(i);
-            final UsageStats usageStats = stats.packageStats.get(removedPackage);
-            if (usageStats != null && usageStats.mEndTimeStamp < removedPackagesMap.valueAt(i)) {
-                stats.packageStats.remove(removedPackage);
+        synchronized (mLock) {
+            if (mPackagesTokenData.removedPackagesMap.isEmpty()) {
+                return;
             }
-        }
+            final ArrayMap<String, Long> removedPackagesMap = mPackagesTokenData.removedPackagesMap;
 
-        // filter out events
-        for (int i = stats.events.size() - 1; i >= 0; i--) {
-            final UsageEvents.Event event = stats.events.get(i);
-            final Long timeRemoved = removedPackagesMap.get(event.mPackage);
-            if (timeRemoved != null && timeRemoved > event.mTimeStamp) {
-                stats.events.remove(i);
+            // filter out package usage stats
+            final int removedPackagesSize = removedPackagesMap.size();
+            for (int i = 0; i < removedPackagesSize; i++) {
+                final String removedPackage = removedPackagesMap.keyAt(i);
+                final UsageStats usageStats = stats.packageStats.get(removedPackage);
+                if (usageStats != null &&
+                        usageStats.mEndTimeStamp < removedPackagesMap.valueAt(i)) {
+                    stats.packageStats.remove(removedPackage);
+                }
+            }
+
+            // filter out events
+            for (int i = stats.events.size() - 1; i >= 0; i--) {
+                final UsageEvents.Event event = stats.events.get(i);
+                final Long timeRemoved = removedPackagesMap.get(event.mPackage);
+                if (timeRemoved != null && timeRemoved > event.mTimeStamp) {
+                    stats.events.remove(i);
+                }
             }
         }
     }
@@ -1226,12 +1231,14 @@
     }
 
     void obfuscateCurrentStats(IntervalStats[] currentStats) {
-        if (mCurrentVersion < 5) {
-            return;
-        }
-        for (int i = 0; i < currentStats.length; i++) {
-            final IntervalStats stats = currentStats[i];
-            stats.obfuscateData(mPackagesTokenData);
+        synchronized (mLock) {
+            if (mCurrentVersion < 5) {
+                return;
+            }
+            for (int i = 0; i < currentStats.length; i++) {
+                final IntervalStats stats = currentStats[i];
+                stats.obfuscateData(mPackagesTokenData);
+            }
         }
     }
 
diff --git a/telephony/java/android/telephony/PhoneNumberUtils.java b/telephony/java/android/telephony/PhoneNumberUtils.java
index 0ecafc7..019fb7b 100644
--- a/telephony/java/android/telephony/PhoneNumberUtils.java
+++ b/telephony/java/android/telephony/PhoneNumberUtils.java
@@ -49,6 +49,7 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
 import java.util.Locale;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -1285,6 +1286,13 @@
 
     private static final String SINGAPORE_ISO_COUNTRY_CODE = "SG";
 
+    private static final String[] COUNTRY_CODES_TO_FORMAT_NATIONALLY = new String[] {
+            "KR", // Korea
+            "JP", // Japan
+            "SG", // Singapore
+            "TW", // Taiwan
+    };
+
     /**
      * Breaks the given number down and formats it according to the rules
      * for the country the number is from.
@@ -1647,45 +1655,58 @@
             defaultCountryIso = defaultCountryIso.toUpperCase(Locale.ROOT);
         }
 
+        Rlog.v(LOG_TAG, "formatNumber: defaultCountryIso: " + defaultCountryIso);
+
         PhoneNumberUtil util = PhoneNumberUtil.getInstance();
         String result = null;
         try {
             PhoneNumber pn = util.parseAndKeepRawInput(phoneNumber, defaultCountryIso);
 
-            if (KOREA_ISO_COUNTRY_CODE.equalsIgnoreCase(defaultCountryIso) &&
-                    (pn.getCountryCode() == util.getCountryCodeForRegion(KOREA_ISO_COUNTRY_CODE)) &&
-                    (pn.getCountryCodeSource() ==
-                            PhoneNumber.CountryCodeSource.FROM_NUMBER_WITH_PLUS_SIGN)) {
-                /**
-                 * Need to reformat any local Korean phone numbers (when the user is in Korea) with
-                 * country code to corresponding national format which would replace the leading
-                 * +82 with 0.
-                 */
-                result = util.format(pn, PhoneNumberUtil.PhoneNumberFormat.NATIONAL);
-            } else if (JAPAN_ISO_COUNTRY_CODE.equalsIgnoreCase(defaultCountryIso) &&
-                    pn.getCountryCode() == util.getCountryCodeForRegion(JAPAN_ISO_COUNTRY_CODE) &&
-                    (pn.getCountryCodeSource() ==
-                            PhoneNumber.CountryCodeSource.FROM_NUMBER_WITH_PLUS_SIGN)) {
-                /**
-                 * Need to reformat Japanese phone numbers (when user is in Japan) with the national
-                 * dialing format.
-                 */
-                result = util.format(pn, PhoneNumberUtil.PhoneNumberFormat.NATIONAL);
-            } else if (Flags.removeCountryCodeFromLocalSingaporeCalls() &&
-                    (SINGAPORE_ISO_COUNTRY_CODE.equalsIgnoreCase(defaultCountryIso) &&
-                            pn.getCountryCode() ==
-                                    util.getCountryCodeForRegion(SINGAPORE_ISO_COUNTRY_CODE) &&
-                            (pn.getCountryCodeSource() ==
-                                    PhoneNumber.CountryCodeSource.FROM_NUMBER_WITH_PLUS_SIGN))) {
-                /*
-                 * Need to reformat Singaporean phone numbers (when the user is in Singapore)
-                 * with the country code (+65) removed to comply with Singaporean regulations.
-                 */
-                result = util.format(pn, PhoneNumberUtil.PhoneNumberFormat.NATIONAL);
+            if (Flags.nationalCountryCodeFormattingForLocalCalls()) {
+                if (Arrays.asList(COUNTRY_CODES_TO_FORMAT_NATIONALLY).contains(defaultCountryIso)
+                        && pn.getCountryCode() == util.getCountryCodeForRegion(defaultCountryIso)
+                        && pn.getCountryCodeSource()
+                        == PhoneNumber.CountryCodeSource.FROM_NUMBER_WITH_PLUS_SIGN) {
+                    return util.format(pn, PhoneNumberUtil.PhoneNumberFormat.NATIONAL);
+                } else {
+                    return util.formatInOriginalFormat(pn, defaultCountryIso);
+                }
             } else {
-                result = util.formatInOriginalFormat(pn, defaultCountryIso);
+                if (KOREA_ISO_COUNTRY_CODE.equalsIgnoreCase(defaultCountryIso) && (
+                        pn.getCountryCode() == util.getCountryCodeForRegion(KOREA_ISO_COUNTRY_CODE))
+                        && (pn.getCountryCodeSource()
+                        == PhoneNumber.CountryCodeSource.FROM_NUMBER_WITH_PLUS_SIGN)) {
+                    /**
+                     * Need to reformat any local Korean phone numbers (when the user is in
+                     * Korea) with country code to corresponding national format which would
+                     * replace the leading +82 with 0.
+                     */
+                    result = util.format(pn, PhoneNumberUtil.PhoneNumberFormat.NATIONAL);
+                } else if (JAPAN_ISO_COUNTRY_CODE.equalsIgnoreCase(defaultCountryIso)
+                        && pn.getCountryCode() == util.getCountryCodeForRegion(
+                        JAPAN_ISO_COUNTRY_CODE) && (pn.getCountryCodeSource()
+                        == PhoneNumber.CountryCodeSource.FROM_NUMBER_WITH_PLUS_SIGN)) {
+                    /**
+                     * Need to reformat Japanese phone numbers (when user is in Japan) with the
+                     * national dialing format.
+                     */
+                    result = util.format(pn, PhoneNumberUtil.PhoneNumberFormat.NATIONAL);
+                } else if (Flags.removeCountryCodeFromLocalSingaporeCalls() && (
+                        SINGAPORE_ISO_COUNTRY_CODE.equalsIgnoreCase(defaultCountryIso)
+                                && pn.getCountryCode() == util.getCountryCodeForRegion(
+                                SINGAPORE_ISO_COUNTRY_CODE) && (pn.getCountryCodeSource()
+                                == PhoneNumber.CountryCodeSource.FROM_NUMBER_WITH_PLUS_SIGN))) {
+                    /*
+                     * Need to reformat Singaporean phone numbers (when the user is in Singapore)
+                     * with the country code (+65) removed to comply with Singaporean regulations.
+                     */
+                    result = util.format(pn, PhoneNumberUtil.PhoneNumberFormat.NATIONAL);
+                } else {
+                    result = util.formatInOriginalFormat(pn, defaultCountryIso);
+                }
             }
         } catch (NumberParseException e) {
+            if (DBG) log("formatNumber: NumberParseException caught " + e);
         }
         return result;
     }
diff --git a/test-mock/Android.bp b/test-mock/Android.bp
index e29d321..5976657 100644
--- a/test-mock/Android.bp
+++ b/test-mock/Android.bp
@@ -51,10 +51,8 @@
 
 java_library {
     name: "android.test.mock.ravenwood",
+    defaults: ["ravenwood-internal-only-visibility-java"],
     srcs: [":android-test-mock-sources"],
-    visibility: [
-        "//frameworks/base",
-    ],
 }
 
 android_ravenwood_test {
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataLabels.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataLabels.java
index ba0e3db..3c93c88 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataLabels.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataLabels.java
@@ -24,6 +24,7 @@
 
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 
 /**
  * Data label representation with data shared and data collected maps containing zero or more {@link
@@ -138,7 +139,7 @@
                                 "|",
                                 dataType.getPurposes().stream()
                                         .map(DataType.Purpose::toString)
-                                        .toList()));
+                                        .collect(Collectors.toList())));
                 dataLabelsEle.appendChild(hrDataTypeEle);
             }
         }
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataType.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataType.java
index d2326d1..284a4b8 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataType.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataType.java
@@ -25,6 +25,7 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 /**
  * Data usage type representation. Types are specific to a {@link DataCategory} and contains
@@ -182,7 +183,7 @@
                             XmlUtils.OD_NAME_PURPOSES,
                             this.getPurposes().stream()
                                     .map(p -> String.valueOf(p.getValue()))
-                                    .toList()));
+                                    .collect(Collectors.toList())));
         }
 
         maybeAddBoolToOdElement(
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/XmlUtils.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/XmlUtils.java
index 26b5639..2c1517b 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/XmlUtils.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/XmlUtils.java
@@ -25,6 +25,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.stream.Collectors;
 
 public class XmlUtils {
     public static final String DATA_TYPE_SEPARATOR = "_data_type_";
@@ -131,7 +132,9 @@
     /** Gets the top-level children with the tag name.. */
     public static List<Element> getChildrenByTagName(Node parentEle, String tagName) {
         var elements = XmlUtils.asElementList(parentEle.getChildNodes());
-        return elements.stream().filter(e -> e.getTagName().equals(tagName)).toList();
+        return elements.stream()
+                .filter(e -> e.getTagName().equals(tagName))
+                .collect(Collectors.toList());
     }
 
     /**
@@ -286,7 +289,8 @@
     /** Gets a pipeline-split attribute. */
     public static List<String> getPipelineSplitAttr(Element ele, String attrName, boolean required)
             throws MalformedXmlException {
-        List<String> list = Arrays.stream(ele.getAttribute(attrName).split("\\|")).toList();
+        List<String> list =
+                Arrays.stream(ele.getAttribute(attrName).split("\\|")).collect(Collectors.toList());
         if ((list.isEmpty() || list.get(0).isEmpty()) && required) {
             throw new MalformedXmlException(
                     String.format(
@@ -315,7 +319,7 @@
         List<Element> boolEles =
                 XmlUtils.getChildrenByTagName(ele, XmlUtils.OD_TAG_BOOLEAN).stream()
                         .filter(e -> e.getAttribute(XmlUtils.OD_ATTR_NAME).equals(nameName))
-                        .toList();
+                        .collect(Collectors.toList());
         if (boolEles.size() > 1) {
             throw new MalformedXmlException(
                     String.format(
@@ -346,7 +350,7 @@
         List<Element> longEles =
                 XmlUtils.getChildrenByTagName(ele, XmlUtils.OD_TAG_LONG).stream()
                         .filter(e -> e.getAttribute(XmlUtils.OD_ATTR_NAME).equals(nameName))
-                        .toList();
+                        .collect(Collectors.toList());
         if (longEles.size() > 1) {
             throw new MalformedXmlException(
                     String.format(
@@ -377,7 +381,7 @@
         List<Element> eles =
                 XmlUtils.getChildrenByTagName(ele, XmlUtils.OD_TAG_STRING).stream()
                         .filter(e -> e.getAttribute(XmlUtils.OD_ATTR_NAME).equals(nameName))
-                        .toList();
+                        .collect(Collectors.toList());
         if (eles.size() > 1) {
             throw new MalformedXmlException(
                     String.format(
@@ -405,7 +409,7 @@
         List<Element> eles =
                 XmlUtils.getChildrenByTagName(ele, XmlUtils.OD_TAG_PBUNDLE_AS_MAP).stream()
                         .filter(e -> e.getAttribute(XmlUtils.OD_ATTR_NAME).equals(nameName))
-                        .toList();
+                        .collect(Collectors.toList());
         if (eles.size() > 1) {
             throw new MalformedXmlException(
                     String.format(
@@ -449,7 +453,7 @@
         List<Element> intArrayEles =
                 XmlUtils.getChildrenByTagName(ele, XmlUtils.OD_TAG_INT_ARRAY).stream()
                         .filter(e -> e.getAttribute(XmlUtils.OD_ATTR_NAME).equals(nameName))
-                        .toList();
+                        .collect(Collectors.toList());
         if (intArrayEles.size() > 1) {
             throw new MalformedXmlException(
                     String.format("Found more than one %s in %s.", nameName, ele.getTagName()));
@@ -502,7 +506,7 @@
         List<Element> arrayEles =
                 XmlUtils.getChildrenByTagName(ele, XmlUtils.OD_TAG_STRING_ARRAY).stream()
                         .filter(e -> e.getAttribute(XmlUtils.OD_ATTR_NAME).equals(nameName))
-                        .toList();
+                        .collect(Collectors.toList());
         if (arrayEles.size() > 1) {
             throw new MalformedXmlException(
                     String.format(