Merge "Update enforcement of wallpaper zooming for Wear to W." into main
diff --git a/Android.bp b/Android.bp
index 057b1d6..59e903e 100644
--- a/Android.bp
+++ b/Android.bp
@@ -389,7 +389,6 @@
         // TODO(b/120066492): remove gps_debug and protolog.conf.json when the build
         // system propagates "required" properly.
         "gps_debug.conf",
-        "protolog.conf.json.gz",
         "core.protolog.pb",
         "framework-res",
         // any install dependencies should go into framework-minus-apex-install-dependencies
diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig
index 441d521..3ec6fe7 100644
--- a/core/java/android/app/admin/flags/flags.aconfig
+++ b/core/java/android/app/admin/flags/flags.aconfig
@@ -1,3 +1,6 @@
+# proto-file: build/make/tools/aconfig/aconfig_protos/protos/aconfig.proto
+# proto-message: flag_declarations
+
 package: "android.app.admin.flags"
 
 flag {
@@ -180,3 +183,10 @@
   description: "Allow COPE admin to control screen brightness and timeout."
   bug: "323894620"
 }
+
+flag {
+  name: "is_recursive_required_app_merging_enabled"
+  namespace: "enterprise"
+  description: "Guards a new flow for recursive required enterprise app list merging"
+  bug: "319084618"
+}
diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig
index 24d6a5c..2904e7c 100644
--- a/core/java/android/companion/virtual/flags/flags.aconfig
+++ b/core/java/android/companion/virtual/flags/flags.aconfig
@@ -44,3 +44,11 @@
      description: "Enable device awareness in camera service"
      bug: "305170199"
 }
+
+flag {
+    namespace: "virtual_devices"
+    name: "device_aware_drm"
+    description: "Makes MediaDrm APIs device-aware"
+    bug: "303535376"
+    is_fixed_read_only: true
+}
\ No newline at end of file
diff --git a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
index 5b32f33..c00e610 100644
--- a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
@@ -1757,7 +1757,8 @@
                                 mCallbacks, result.getSequenceId());
                     }
                     if ((!mSingleCapture) && (mPreviewProcessorType ==
-                            IPreviewExtenderImpl.PROCESSOR_TYPE_REQUEST_UPDATE_ONLY)) {
+                            IPreviewExtenderImpl.PROCESSOR_TYPE_REQUEST_UPDATE_ONLY)
+                            && mInitialized) {
                         CaptureStageImpl captureStage = null;
                         try {
                             captureStage = mPreviewRequestUpdateProcessor.process(
@@ -1780,8 +1781,8 @@
                         } else {
                             mRequestUpdatedNeeded = false;
                         }
-                    } else if (mPreviewProcessorType ==
-                            IPreviewExtenderImpl.PROCESSOR_TYPE_IMAGE_PROCESSOR) {
+                    } else if ((mPreviewProcessorType ==
+                            IPreviewExtenderImpl.PROCESSOR_TYPE_IMAGE_PROCESSOR) && mInitialized) {
                         int idx = mPendingResultMap.indexOfKey(timestamp);
 
                         if ((idx >= 0) && (mPendingResultMap.get(timestamp).first == null)) {
@@ -1828,7 +1829,7 @@
                     } else {
                         // No special handling for PROCESSOR_TYPE_NONE
                     }
-                    if (notifyClient) {
+                    if (notifyClient && mInitialized) {
                         final long ident = Binder.clearCallingIdentity();
                         try {
                             if (processStatus) {
diff --git a/core/java/android/hardware/radio/Announcement.java b/core/java/android/hardware/radio/Announcement.java
index 3ba3ebc..faa103c 100644
--- a/core/java/android/hardware/radio/Announcement.java
+++ b/core/java/android/hardware/radio/Announcement.java
@@ -71,7 +71,7 @@
         /**
          * An event called whenever a list of active announcements change.
          *
-         * The entire list is sent each time a new announcement appears or any ends broadcasting.
+         * <p>The entire list is sent each time a new announcement appears or any ends broadcasting.
          *
          * @param activeAnnouncements a full list of active announcements
          */
diff --git a/core/java/android/hardware/radio/ProgramList.java b/core/java/android/hardware/radio/ProgramList.java
index c5167db..6146df8 100644
--- a/core/java/android/hardware/radio/ProgramList.java
+++ b/core/java/android/hardware/radio/ProgramList.java
@@ -357,7 +357,7 @@
         /**
          * Constructor of program list filter.
          *
-         * Arrays passed to this constructor become owned by this object, do not modify them later.
+         * <p>Arrays passed to this constructor will be owned by this object, do not modify them.
          *
          * @param identifierTypes see getIdentifierTypes()
          * @param identifiers see getIdentifiers()
@@ -438,12 +438,11 @@
         /**
          * Returns the list of identifier types that satisfy the filter.
          *
-         * If the program list entry contains at least one identifier of the type
-         * listed, it satisfies this condition.
+         * <p>If the program list entry contains at least one identifier of the type
+         * listed, it satisfies this condition. Empty list means no filtering on
+         * identifier type.
          *
-         * Empty list means no filtering on identifier type.
-         *
-         * @return the list of accepted identifier types, must not be modified
+         * @return the set of accepted identifier types, must not be modified
          */
         public @NonNull Set<Integer> getIdentifierTypes() {
             return mIdentifierTypes;
@@ -452,12 +451,10 @@
         /**
          * Returns the list of identifiers that satisfy the filter.
          *
-         * If the program list entry contains at least one listed identifier,
-         * it satisfies this condition.
+         * <p>If the program list entry contains at least one listed identifier,
+         * it satisfies this condition. Empty list means no filtering on identifier.
          *
-         * Empty list means no filtering on identifier.
-         *
-         * @return the list of accepted identifiers, must not be modified
+         * @return the set of accepted identifiers, must not be modified
          */
         public @NonNull Set<ProgramSelector.Identifier> getIdentifiers() {
             return mIdentifiers;
@@ -476,7 +473,7 @@
         /**
          * Checks, if updates on entry modifications should be disabled.
          *
-         * If true, 'modified' vector of ProgramListChunk must contain list
+         * <p>If true, 'modified' vector of ProgramListChunk must contain list
          * additions only. Once the program is added to the list, it's not
          * updated anymore.
          */
diff --git a/core/java/android/hardware/radio/ProgramSelector.java b/core/java/android/hardware/radio/ProgramSelector.java
index 0740374..42028f6 100644
--- a/core/java/android/hardware/radio/ProgramSelector.java
+++ b/core/java/android/hardware/radio/ProgramSelector.java
@@ -36,27 +36,31 @@
 /**
  * A set of identifiers necessary to tune to a given station.
  *
- * This can hold various identifiers, like
- * - AM/FM frequency
- * - HD Radio subchannel
- * - DAB channel info
+ * <p>This can hold various identifiers, like
+ * <ui>
+ *     <li>AM/FM frequency</li>
+ *     <li>HD Radio subchannel</li>
+ *     <li>DAB channel info</li>
+ * </ui>
  *
- * The primary ID uniquely identifies a station and can be used for equality
+ * <p>The primary ID uniquely identifies a station and can be used for equality
  * check. The secondary IDs are supplementary and can speed up tuning process,
  * but the primary ID is sufficient (ie. after a full band scan).
  *
- * Two selectors with different secondary IDs, but the same primary ID are
+ * <p>Two selectors with different secondary IDs, but the same primary ID are
  * considered equal. In particular, secondary IDs vector may get updated for
  * an entry on the program list (ie. when a better frequency for a given
  * station is found).
  *
- * The primaryId of a given programType MUST be of a specific type:
- * - AM, FM: RDS_PI if the station broadcasts RDS, AMFM_FREQUENCY otherwise;
- * - AM_HD, FM_HD: HD_STATION_ID_EXT;
- * - DAB: DAB_SIDECC;
- * - DRMO: DRMO_SERVICE_ID;
- * - SXM: SXM_SERVICE_ID;
- * - VENDOR: VENDOR_PRIMARY.
+ * <p>The primaryId of a given programType MUST be of a specific type:
+ * <ui>
+ *     <li>AM, FM: RDS_PI if the station broadcasts RDS, AMFM_FREQUENCY otherwise;</li>
+ *     <li>AM_HD, FM_HD: HD_STATION_ID_EXT;</li>
+ *     <li>DAB: DAB_SIDECC;</li>
+ *     <li>DRMO: DRMO_SERVICE_ID;</li>
+ *     <li>SXM: SXM_SERVICE_ID;</li>
+ *     <li>VENDOR: VENDOR_PRIMARY.</li>
+ * </ui>
  * @hide
  */
 @SystemApi
@@ -258,10 +262,10 @@
     /**
      * 64bit additional identifier for HD Radio.
      *
-     * <p>Due to Station ID abuse, some HD_STATION_ID_EXT identifiers may be not
-     * globally unique. To provide a best-effort solution, a short version of
-     * station name may be carried as additional identifier and may be used
-     * by the tuner hardware to double-check tuning.
+     * <p>Due to Station ID abuse, some {@link #IDENTIFIER_TYPE_HD_STATION_ID_EXT}
+     * identifiers may be not globally unique. To provide a best-effort solution, a
+     * short version of station name may be carried as additional identifier and
+     * may be used by the tuner hardware to double-check tuning.
      *
      * <p>The name is limited to the first 8 A-Z0-9 characters (lowercase
      * letters must be converted to uppercase). Encoded in little-endian
@@ -384,7 +388,7 @@
      * The value format is determined by a vendor.
      *
      * <p>It must not be used in any other programType than corresponding VENDOR
-     * type between VENDOR_START and VENDOR_END (eg. identifier type 1015 must
+     * type between VENDOR_START and VENDOR_END (e.g. identifier type 1015 must
      * not be used in any program type other than 1015).
      */
     public static final int IDENTIFIER_TYPE_VENDOR_START = PROGRAM_TYPE_VENDOR_START;
@@ -435,9 +439,10 @@
     /**
      * Constructor for ProgramSelector.
      *
-     * It's not desired to modify selector objects, so all its fields are initialized at creation.
+     * <p>It's not desired to modify selector objects, so all its fields are initialized at
+     * creation.
      *
-     * Identifier lists must not contain any nulls, but can itself be null to be interpreted
+     * <p>Identifier lists must not contain any nulls, but can itself be null to be interpreted
      * as empty list at object creation.
      *
      * @param programType type of a radio technology.
@@ -492,8 +497,8 @@
     /**
      * Looks up an identifier of a given type (either primary or secondary).
      *
-     * If there are multiple identifiers if a given type, then first in order (where primary id is
-     * before any secondary) is selected.
+     * <p>If there are multiple identifiers if a given type, then first in order (where primary id
+     * is before any secondary) is selected.
      *
      * @param type type of identifier.
      * @return identifier value, if found.
@@ -510,11 +515,11 @@
     /**
      * Looks up all identifier of a given type (either primary or secondary).
      *
-     * Some identifiers may be provided multiple times, for example
-     * IDENTIFIER_TYPE_AMFM_FREQUENCY for FM Alternate Frequencies.
+     * <p>Some identifiers may be provided multiple times, for example
+     * {@link #IDENTIFIER_TYPE_AMFM_FREQUENCY} for FM Alternate Frequencies.
      *
      * @param type type of identifier.
-     * @return a list of identifiers, generated on each call. May be modified.
+     * @return an array of identifiers, generated on each call. May be modified.
      */
     public @NonNull Identifier[] getAllIds(@IdentifierType int type) {
         List<Identifier> out = new ArrayList<>();
@@ -543,14 +548,14 @@
     /**
      * Creates an equivalent ProgramSelector with a given secondary identifier preferred.
      *
-     * Used to point to a specific physical identifier for technologies that may broadcast the same
-     * program on different channels. For example, with a DAB program broadcasted over multiple
+     * <p>Used to point to a specific physical identifier for technologies that may broadcast the
+     * same program on different channels. For example, with a DAB program broadcasted over multiple
      * ensembles, the radio hardware may select the one with the strongest signal. The UI may select
      * preferred ensemble though, so the radio hardware may try to use it in the first place.
      *
-     * This is a best-effort hint for the tuner, not a guaranteed behavior.
+     * <p>This is a best-effort hint for the tuner, not a guaranteed behavior.
      *
-     * Setting the given secondary identifier as preferred means filtering out other secondary
+     * <p>Setting the given secondary identifier as preferred means filtering out other secondary
      * identifiers of its type and adding it to the list.
      *
      * @param preferred preferred secondary identifier
@@ -577,7 +582,7 @@
      *
      * @param band the band.
      * @param frequencyKhz the frequency in kHz.
-     * @return new ProgramSelector object representing given frequency.
+     * @return new {@link ProgramSelector} object representing given frequency.
      * @throws IllegalArgumentException if provided frequency is out of bounds.
      */
     public static @NonNull ProgramSelector createAmFmSelector(
@@ -588,13 +593,13 @@
     /**
      * Checks, if a given AM/FM frequency is roughly valid and in correct unit.
      *
-     * It does not check the range precisely: it may provide false positives, but not false
+     * <p>It does not check the range precisely: it may provide false positives, but not false
      * negatives. In particular, it may be way off for certain regions.
-     * The main purpose is to avoid passing inproper units, ie. MHz instead of kHz.
+     * The main purpose is to avoid passing improper units, ie. MHz instead of kHz.
      *
      * @param isAm true, if AM, false if FM.
      * @param frequencyKhz the frequency in kHz.
-     * @return true, if the frequency is rougly valid.
+     * @return true, if the frequency is roughly valid.
      */
     private static boolean isValidAmFmFrequency(boolean isAm, int frequencyKhz) {
         if (isAm) {
@@ -607,7 +612,7 @@
     /**
      * Builds new ProgramSelector for AM/FM frequency.
      *
-     * This method variant supports HD Radio subchannels, but it's undesirable to
+     * <p>This method variant supports HD Radio subchannels, but it's undesirable to
      * select them manually. Instead, the value should be retrieved from program list.
      *
      * @param band the band.
@@ -741,9 +746,9 @@
     };
 
     /**
-     * A single program identifier component, eg. frequency or channel ID.
+     * A single program identifier component, e.g. frequency or channel ID.
      *
-     * The long value field holds the value in format described in comments for
+     * <p>The long value field holds the value in format described in comments for
      * IdentifierType constants.
      */
     public static final class Identifier implements Parcelable {
@@ -776,11 +781,11 @@
         }
 
         /**
-         * Returns whether this Identifier's type is considered a category when filtering
+         * Returns whether this identifier's type is considered a category when filtering
          * ProgramLists for category entries.
          *
          * @see ProgramList.Filter#areCategoriesIncluded
-         * @return False if this identifier's type is not tuneable (e.g. DAB ensemble or
+         * @return False if this identifier's type is not tunable (e.g. DAB ensemble or
          *         vendor-specified type). True otherwise.
          */
         public boolean isCategoryType() {
@@ -791,14 +796,14 @@
         /**
          * Value of an identifier.
          *
-         * Its meaning depends on identifier type, ie. for IDENTIFIER_TYPE_AMFM_FREQUENCY type,
-         * the value is a frequency in kHz.
+         * <p>Its meaning depends on identifier type, ie. for
+         * {@link #IDENTIFIER_TYPE_AMFM_FREQUENCY} type, the value is a frequency in kHz.
          *
-         * The range of a value depends on its type; it does not always require the whole long
+         * <p>The range of a value depends on its type; it does not always require the whole long
          * range. Casting to necessary type (ie. int) without range checking is correct in front-end
          * code - any range violations are either errors in the framework or in the
-         * HAL implementation. For example, IDENTIFIER_TYPE_AMFM_FREQUENCY always fits in int,
-         * as Integer.MAX_VALUE would mean 2.1THz.
+         * HAL implementation. For example, {@link #IDENTIFIER_TYPE_AMFM_FREQUENCY} always fits in
+         * int, as {@link Integer#MAX_VALUE} would mean 2.1THz.
          *
          * @return value of an identifier.
          */
diff --git a/core/java/android/hardware/radio/RadioManager.java b/core/java/android/hardware/radio/RadioManager.java
index da6c686..61854e4 100644
--- a/core/java/android/hardware/radio/RadioManager.java
+++ b/core/java/android/hardware/radio/RadioManager.java
@@ -102,7 +102,7 @@
     public @interface RadioStatusType{}
 
 
-    // keep in sync with radio_class_t in /system/core/incluse/system/radio.h
+    // keep in sync with radio_class_t in /system/core/include/system/radio.h
     /** Radio module class supporting FM (including HD radio) and AM */
     public static final int CLASS_AM_FM = 0;
     /** Radio module class supporting satellite radio */
@@ -154,7 +154,7 @@
     /**
      * Forces mono audio stream reception.
      *
-     * Analog broadcasts can recover poor reception conditions by jointing
+     * <p>Analog broadcasts can recover poor reception conditions by jointing
      * stereo channels into one. Mainly for, but not limited to AM/FM.
      */
     public static final int CONFIG_FORCE_MONO = 1;
@@ -176,7 +176,7 @@
     /**
      * Forces the digital playback for the supporting radio technology.
      *
-     * User may disable digital-analog handover that happens with poor
+     * <p>User may disable digital-analog handover that happens with poor
      * reception conditions. With digital forced, the radio will remain silent
      * instead of switching to analog channel if it's available. This is purely
      * user choice, it does not reflect the actual state of handover.
@@ -185,7 +185,7 @@
     /**
      * RDS Alternative Frequencies.
      *
-     * If set and the currently tuned RDS station broadcasts on multiple
+     * <p>If set and the currently tuned RDS station broadcasts on multiple
      * channels, radio tuner automatically switches to the best available
      * alternative.
      */
@@ -193,7 +193,7 @@
     /**
      * RDS region-specific program lock-down.
      *
-     * Allows user to lock to the current region as they move into the
+     * <p>Allows user to lock to the current region as they move into the
      * other region.
      */
     public static final int CONFIG_RDS_REG = 5;
@@ -247,11 +247,12 @@
     @Retention(RetentionPolicy.SOURCE)
     public @interface ConfigFlag {}
 
-    /*****************************************************************************
+    /**
      * Lists properties, options and radio bands supported by a given broadcast radio module.
-     * Each module has a unique ID used to address it when calling RadioManager APIs.
-     * Module properties are returned by {@link #listModules(List <ModuleProperties>)} method.
-     ****************************************************************************/
+     *
+     * <p>Each module has a unique ID used to address it when calling RadioManager APIs.
+     * Module properties are returned by {@link #listModules(List)} method.
+     */
     public static class ModuleProperties implements Parcelable {
 
         private final int mId;
@@ -315,8 +316,11 @@
             return set.stream().mapToInt(Integer::intValue).toArray();
         }
 
-        /** Unique module identifier provided by the native service.
-         * For use with {@link #openTuner(int, BandConfig, boolean, RadioTuner.Callback, Handler)}.
+        /**
+         * Unique module identifier provided by the native service.
+         *
+         * <p>or use with
+         * {@link #openTuner(int, BandConfig, boolean, RadioTuner.Callback, Handler)}.
          * @return the radio module unique identifier.
          */
         public int getId() {
@@ -324,22 +328,24 @@
         }
 
         /**
-         * Module service (driver) name as registered with HIDL.
+         * Module service (driver) name as registered with HIDL or AIDL HAL.
          * @return the module service name.
          */
         public @NonNull String getServiceName() {
             return mServiceName;
         }
 
-        /** Module class identifier: {@link #CLASS_AM_FM}, {@link #CLASS_SAT}, {@link #CLASS_DT}
+        /**
+         * Module class identifier: {@link #CLASS_AM_FM}, {@link #CLASS_SAT}, {@link #CLASS_DT}
          * @return the radio module class identifier.
          */
         public int getClassId() {
             return mClassId;
         }
 
-        /** Human readable broadcast radio module implementor
-         * @return the name of the radio module implementator.
+        /**
+         * Human readable broadcast radio module implementor
+         * @return the name of the radio module implementer.
          */
         public String getImplementor() {
             return mImplementor;
@@ -352,31 +358,38 @@
             return mProduct;
         }
 
-        /** Human readable broadcast radio module version number
+        /**
+         * Human readable broadcast radio module version number
          * @return the radio module version.
          */
         public String getVersion() {
             return mVersion;
         }
 
-        /** Radio module serial number.
-         * Can be used for subscription services.
+        /**
+         * Radio module serial number.
+         *
+         * <p>This can be used for subscription services.
          * @return the radio module serial number.
          */
         public String getSerial() {
             return mSerial;
         }
 
-        /** Number of tuners available.
-         * This is the number of tuners that can be open simultaneously.
+        /**
+         * Number of tuners available.
+         *
+         * <p>This is the number of tuners that can be open simultaneously.
          * @return the number of tuners supported.
          */
         public int getNumTuners() {
             return mNumTuners;
         }
 
-        /** Number tuner audio sources available. Must be less or equal to getNumTuners().
-         * When more than one tuner is supported, one is usually for playback and has one
+        /**
+         * Number tuner audio sources available. Must be less or equal to {@link #getNumTuners}.
+         *
+         * <p>When more than one tuner is supported, one is usually for playback and has one
          * associated audio source and the other is for pre scanning and building a
          * program list.
          * @return the number of audio sources available.
@@ -387,20 +400,24 @@
         }
 
         /**
-         * Checks, if BandConfig initialization (after {@link RadioManager#openTuner})
+         * Checks, if {@link BandConfig} initialization (after {@link RadioManager#openTuner})
          * is required to be done before other operations or not.
          *
-         * If it is, the client has to wait for {@link RadioTuner.Callback#onConfigurationChanged}
-         * callback before executing any other operations. Otherwise, such operation will fail
-         * returning {@link RadioManager#STATUS_INVALID_OPERATION} error code.
+         * <p>If it is, the client has to wait for
+         * {@link RadioTuner.Callback#onConfigurationChanged} callback before executing any other
+         * operations. Otherwise, such operation will fail returning
+         * {@link RadioManager#STATUS_INVALID_OPERATION} error code.
          */
         public boolean isInitializationRequired() {
             return mIsInitializationRequired;
         }
 
-        /** {@code true} if audio capture is possible from radio tuner output.
-         * This indicates if routing to audio devices not connected to the same HAL as the FM radio
-         * is possible (e.g. to USB) or DAR (Digital Audio Recorder) feature can be implemented.
+        /**
+         * {@code true} if audio capture is possible from radio tuner output.
+         *
+         * <p>This indicates if routing to audio devices not connected to the same HAL as the FM
+         * radio is possible (e.g. to USB) or DAR (Digital Audio Recorder) feature can be
+         * implemented.
          * @return {@code true} if audio capture is possible, {@code false} otherwise.
          */
         public boolean isCaptureSupported() {
@@ -421,8 +438,8 @@
         /**
          * Checks, if a given program type is supported by this tuner.
          *
-         * If a program type is supported by radio module, it means it can tune
-         * to ProgramSelector of a given type.
+         * <p>If a program type is supported by radio module, it means it can tune
+         * to {@link ProgramSelector} of a given type.
          *
          * @return {@code true} if a given program type is supported.
          */
@@ -433,8 +450,8 @@
         /**
          * Checks, if a given program identifier is supported by this tuner.
          *
-         * If an identifier is supported by radio module, it means it can use it for
-         * tuning to ProgramSelector with either primary or secondary Identifier of
+         * <p>If an identifier is supported by radio module, it means it can use it for
+         * tuning to {@link ProgramSelector} with either primary or secondary Identifier of
          * a given type.
          *
          * @return {@code true} if a given program type is supported.
@@ -446,9 +463,9 @@
         /**
          * A frequency table for Digital Audio Broadcasting (DAB).
          *
-         * The key is a channel name, i.e. 5A, 7B.
+         * <p>The key is a channel name, i.e. 5A, 7B.
          *
-         * The value is a frequency, in kHz.
+         * <p>The value is a frequency, in kHz.
          *
          * @return a frequency table, or {@code null} if the module doesn't support DAB
          */
@@ -460,17 +477,18 @@
          * A map of vendor-specific opaque strings, passed from HAL without changes.
          * Format of these strings can vary across vendors.
          *
-         * It may be used for extra features, that's not supported by a platform,
+         * <p>It may be used for extra features, that's not supported by a platform,
          * for example: preset-slots=6; ultra-hd-capable=false.
          *
-         * Keys must be prefixed with unique vendor Java-style namespace,
-         * eg. 'com.somecompany.parameter1'.
+         * <p>Keys must be prefixed with unique vendor Java-style namespace,
+         * e.g. 'com.somecompany.parameter1'.
          */
         public @NonNull Map<String, String> getVendorInfo() {
             return mVendorInfo;
         }
 
-        /** List of descriptors for all bands supported by this module.
+        /**
+         * List of descriptors for all bands supported by this module.
          * @return an array of {@link BandDescriptor}.
          */
         public BandDescriptor[] getBands() {
@@ -590,7 +608,9 @@
     }
 
     /** Radio band descriptor: an element in ModuleProperties bands array.
-     * It is either an instance of {@link FmBandDescriptor} or {@link AmBandDescriptor} */
+     *
+     * <p>It is either an instance of {@link FmBandDescriptor} or {@link AmBandDescriptor}
+     */
     public static class BandDescriptor implements Parcelable {
 
         private final int mRegion;
@@ -610,16 +630,18 @@
             mSpacing = spacing;
         }
 
-        /** Region this band applies to. E.g. {@link #REGION_ITU_1}
+        /**
+         * Region this band applies to. E.g. {@link #REGION_ITU_1}
          * @return the region this band is associated to.
          */
         public int getRegion() {
             return mRegion;
         }
-        /** Band type, e.g {@link #BAND_FM}. Defines the subclass this descriptor can be cast to:
+        /**
+         * Band type, e.g. {@link #BAND_FM}. Defines the subclass this descriptor can be cast to:
          * <ul>
-         *  <li>{@link #BAND_FM} or {@link #BAND_FM_HD} cast to {@link FmBandDescriptor}, </li>
-         *  <li>{@link #BAND_AM} cast to {@link AmBandDescriptor}, </li>
+         *     <li>{@link #BAND_FM} or {@link #BAND_FM_HD} cast to {@link FmBandDescriptor}</li>
+         *     <li>{@link #BAND_AM} cast to {@link AmBandDescriptor}</li>
          * </ul>
          * @return the band type.
          */
@@ -645,23 +667,29 @@
             return mType == BAND_FM || mType == BAND_FM_HD;
         }
 
-        /** Lower band limit expressed in units according to band type.
-         * Currently all defined band types express channels as frequency in kHz
+        /**
+         * Lower band limit expressed in units according to band type.
+         *
+         * <p>Currently all defined band types express channels as frequency in kHz.
          * @return the lower band limit.
          */
         public int getLowerLimit() {
             return mLowerLimit;
         }
-        /** Upper band limit expressed in units according to band type.
-         * Currently all defined band types express channels as frequency in kHz
+        /**
+         * Upper band limit expressed in units according to band type.
+         *
+         * <p>Currently all defined band types express channels as frequency in kHz.
          * @return the upper band limit.
          */
         public int getUpperLimit() {
             return mUpperLimit;
         }
-        /** Channel spacing in units according to band type.
-         * Currently all defined band types express channels as frequency in kHz
-         * @return the channel spacing.
+        /**
+         * Channel spacing in units according to band type.
+         *
+         * <p>Currently all defined band types express channels as frequency in kHz
+         * @return the channel spacing.</p>
          */
         public int getSpacing() {
             return mSpacing;
@@ -758,9 +786,11 @@
         }
     }
 
-    /** FM band descriptor
+    /**
+     * FM band descriptor
      * @see #BAND_FM
-     * @see #BAND_FM_HD */
+     * @see #BAND_FM_HD
+     */
     public static class FmBandDescriptor extends BandDescriptor {
         private final boolean mStereo;
         private final boolean mRds;
@@ -779,19 +809,22 @@
             mEa = ea;
         }
 
-        /** Stereo is supported
+        /**
+         * Stereo is supported
          * @return {@code true} if stereo is supported, {@code false} otherwise.
          */
         public boolean isStereoSupported() {
             return mStereo;
         }
-        /** RDS or RBDS(if region is ITU2) is supported
+        /**
+         * RDS or RBDS(if region is ITU2) is supported
          * @return {@code true} if RDS or RBDS is supported, {@code false} otherwise.
          */
         public boolean isRdsSupported() {
             return mRds;
         }
-        /** Traffic announcement is supported
+        /**
+         * Traffic announcement is supported
          * @return {@code true} if TA is supported, {@code false} otherwise.
          */
         public boolean isTaSupported() {
@@ -804,8 +837,9 @@
             return mAf;
         }
 
-        /** Emergency Announcement is supported
-         * @return {@code true} if Emergency annoucement is supported, {@code false} otherwise.
+        /**
+         * Emergency Announcement is supported
+         * @return {@code true} if Emergency announcement is supported, {@code false} otherwise.
          */
         public boolean isEaSupported() {
             return mEa;
@@ -890,8 +924,10 @@
         }
     }
 
-    /** AM band descriptor.
-     * @see #BAND_AM */
+    /**
+     * AM band descriptor.
+     * @see #BAND_AM
+     */
     public static class AmBandDescriptor extends BandDescriptor {
 
         private final boolean mStereo;
@@ -903,8 +939,9 @@
             mStereo = stereo;
         }
 
-        /** Stereo is supported
-         *  @return {@code true} if stereo is supported, {@code false} otherwise.
+        /**
+         * Stereo is supported
+         * @return {@code true} if stereo is supported, {@code false} otherwise.
          */
         public boolean isStereoSupported() {
             return mStereo;
@@ -991,39 +1028,47 @@
             return mDescriptor;
         }
 
-        /** Region this band applies to. E.g. {@link #REGION_ITU_1}
-         *  @return the region associated with this band.
+        /**
+         * Region this band applies to. E.g. {@link #REGION_ITU_1}
+         * @return the region associated with this band.
          */
         public int getRegion() {
             return mDescriptor.getRegion();
         }
-        /** Band type, e.g {@link #BAND_FM}. Defines the subclass this descriptor can be cast to:
+        /**
+         * Band type, e.g. {@link #BAND_FM}. Defines the subclass this descriptor can be cast to:
          * <ul>
-         *  <li>{@link #BAND_FM} or {@link #BAND_FM_HD} cast to {@link FmBandDescriptor}, </li>
-         *  <li>{@link #BAND_AM} cast to {@link AmBandDescriptor}, </li>
+         *     <li>{@link #BAND_FM} or {@link #BAND_FM_HD} cast to {@link FmBandDescriptor}</li>
+         *     <li>{@link #BAND_AM} cast to {@link AmBandDescriptor}</li>
          * </ul>
          *  @return the band type.
          */
         public int getType() {
             return mDescriptor.getType();
         }
-        /** Lower band limit expressed in units according to band type.
-         * Currently all defined band types express channels as frequency in kHz
-         *  @return the lower band limit.
+        /**
+         * Lower band limit expressed in units according to band type.
+         *
+         * <p>Currently all defined band types express channels as frequency in kHz.
+         * @return the lower band limit.
          */
         public int getLowerLimit() {
             return mDescriptor.getLowerLimit();
         }
-        /** Upper band limit expressed in units according to band type.
-         * Currently all defined band types express channels as frequency in kHz
-         *  @return the upper band limit.
+        /**
+         * Upper band limit expressed in units according to band type.
+         *
+         * <p>Currently all defined band types express channels as frequency in kHz.
+         * @return the upper band limit.
          */
         public int getUpperLimit() {
             return mDescriptor.getUpperLimit();
         }
-        /** Channel spacing in units according to band type.
-         * Currently all defined band types express channels as frequency in kHz
-         *  @return the channel spacing.
+        /**
+         * Channel spacing in units according to band type.
+         *
+         * <p>Currently all defined band types express channels as frequency in kHz.
+         * @return the channel spacing.
          */
         public int getSpacing() {
             return mDescriptor.getSpacing();
@@ -1089,9 +1134,11 @@
         }
     }
 
-    /** FM band configuration.
+    /**
+     * FM band configuration.
      * @see #BAND_FM
-     * @see #BAND_FM_HD */
+     * @see #BAND_FM_HD
+     */
     public static class FmBandConfig extends BandConfig {
         private final boolean mStereo;
         private final boolean mRds;
@@ -1119,28 +1166,32 @@
             mEa = ea;
         }
 
-        /** Get stereo enable state
+        /**
+         * Get stereo enable state
          * @return the enable state.
          */
         public boolean getStereo() {
             return mStereo;
         }
 
-        /** Get RDS or RBDS(if region is ITU2) enable state
+        /**
+         * Get RDS or RBDS(if region is ITU2) enable state
          * @return the enable state.
          */
         public boolean getRds() {
             return mRds;
         }
 
-        /** Get Traffic announcement enable state
+        /**
+         * Get Traffic announcement enable state
          * @return the enable state.
          */
         public boolean getTa() {
             return mTa;
         }
 
-        /** Get Alternate Frequency Switching enable state
+        /**
+         * Get Alternate Frequency Switching enable state
          * @return the enable state.
          */
         public boolean getAf() {
@@ -1285,7 +1336,8 @@
                 return config;
             }
 
-            /** Set stereo enable state
+            /**
+             * Set stereo enable state
              * @param state The new enable state.
              * @return the same Builder instance.
              */
@@ -1294,7 +1346,8 @@
                 return this;
             }
 
-            /** Set RDS or RBDS(if region is ITU2) enable state
+            /**
+             * Set RDS or RBDS(if region is ITU2) enable state
              * @param state The new enable state.
              * @return the same Builder instance.
              */
@@ -1303,7 +1356,8 @@
                 return this;
             }
 
-            /** Set Traffic announcement enable state
+            /**
+             * Set Traffic announcement enable state
              * @param state The new enable state.
              * @return the same Builder instance.
              */
@@ -1312,7 +1366,8 @@
                 return this;
             }
 
-            /** Set Alternate Frequency Switching enable state
+            /**
+             * Set Alternate Frequency Switching enable state
              * @param state The new enable state.
              * @return the same Builder instance.
              */
@@ -1321,7 +1376,8 @@
                 return this;
             }
 
-            /** Set Emergency Announcement enable state
+            /**
+             * Set Emergency Announcement enable state
              * @param state The new enable state.
              * @return the same Builder instance.
              */
@@ -1332,8 +1388,10 @@
         };
     }
 
-    /** AM band configuration.
-     * @see #BAND_AM */
+    /**
+     * AM band configuration.
+     * @see #BAND_AM
+     */
     public static class AmBandConfig extends BandConfig {
         private final boolean mStereo;
 
@@ -1349,7 +1407,8 @@
             mStereo = stereo;
         }
 
-        /** Get stereo enable state
+        /**
+         * Get stereo enable state
          * @return the enable state.
          */
         public boolean getStereo() {
@@ -1453,7 +1512,8 @@
                 return config;
             }
 
-            /** Set stereo enable state
+            /**
+             * Set stereo enable state
              * @param state The new enable state.
              * @return the same Builder instance.
              */
@@ -1467,7 +1527,8 @@
     /** Radio program information. */
     public static class ProgramInfo implements Parcelable {
 
-        // sourced from hardware/interfaces/broadcastradio/2.0/types.hal
+        // sourced from
+        // hardware/interfaces/broadcastradio/aidl/android/hardware/broadcastradio/ProgramInfo.aidl
         private static final int FLAG_LIVE = 1 << 0;
         private static final int FLAG_MUTED = 1 << 1;
         private static final int FLAG_TRAFFIC_PROGRAM = 1 << 2;
@@ -1521,10 +1582,10 @@
         /**
          * Identifier currently used for program selection.
          *
-         * This identifier can be used to determine which technology is
+         * <p>This identifier can be used to determine which technology is
          * currently being used for reception.
          *
-         * Some program selectors contain tuning information for different radio
+         * <p>Some program selectors contain tuning information for different radio
          * technologies (i.e. FM RDS and DAB). For example, user may tune using
          * a ProgramSelector with RDS_PI primary identifier, but the tuner hardware
          * may choose to use DAB technology to make actual tuning. This identifier
@@ -1537,7 +1598,7 @@
         /**
          * Identifier currently used by hardware to physically tune to a channel.
          *
-         * Some radio technologies broadcast the same program on multiple channels,
+         * <p>Some radio technologies broadcast the same program on multiple channels,
          * i.e. with RDS AF the same program may be broadcasted on multiple
          * alternative frequencies; the same DAB program may be broadcast on
          * multiple ensembles. This identifier points to the channel to which the
@@ -1550,11 +1611,11 @@
         /**
          * Primary identifiers of related contents.
          *
-         * Some radio technologies provide pointers to other programs that carry
+         * <p>Some radio technologies provide pointers to other programs that carry
          * related content (i.e. DAB soft-links). This field is a list of pointers
          * to other programs on the program list.
          *
-         * Please note, that these identifiers does not have to exist on the program
+         * <p>Please note, that these identifiers does not have to exist on the program
          * list - i.e. DAB tuner may provide information on FM RDS alternatives
          * despite not supporting FM RDS. If the system has multiple tuners, another
          * one may have it on its list.
@@ -1563,7 +1624,8 @@
             return mRelatedContent;
         }
 
-        /** Main channel expressed in units according to band type.
+        /**
+         * Main channel expressed in units according to band type.
          * Currently all defined band types express channels as frequency in kHz
          * @return the program channel
          * @deprecated Use {@link ProgramInfo#getSelector} instead.
@@ -1578,7 +1640,8 @@
             }
         }
 
-        /** Sub channel ID. E.g 1 for HD radio HD1
+        /**
+         * Sub channel ID. E.g. 1 for HD radio HD1
          * @return the program sub channel
          * @deprecated Use {@link ProgramInfo#getSelector} instead.
          */
@@ -1600,14 +1663,16 @@
             return (mInfoFlags & FLAG_TUNED) != 0;
         }
 
-        /** {@code true} if the received program is stereo
+        /**
+         * {@code true} if the received program is stereo
          * @return {@code true} if stereo, {@code false} otherwise.
          */
         public boolean isStereo() {
             return (mInfoFlags & FLAG_STEREO) != 0;
         }
 
-        /** {@code true} if the received program is digital (e.g HD radio)
+        /**
+         * {@code true} if the received program is digital (e.g. HD radio)
          * @return {@code true} if digital, {@code false} otherwise.
          * @deprecated Use {@link ProgramInfo#getLogicallyTunedTo()} instead.
          */
@@ -1623,8 +1688,9 @@
 
         /**
          * {@code true} if the program is currently playing live stream.
-         * This may result in a slightly altered reception parameters,
-         * usually targetted at reduced latency.
+         *
+         * <p>This may result in a slightly altered reception parameters,
+         * usually targeted at reduced latency.
          */
         public boolean isLive() {
             return (mInfoFlags & FLAG_LIVE) != 0;
@@ -1634,7 +1700,8 @@
          * {@code true} if radio stream is not playing, i.e. due to bad reception
          * conditions or buffering. In this state volume knob MAY be disabled to
          * prevent user increasing volume too much.
-         * It does NOT mean the user has muted audio.
+         *
+         * <p>It does NOT mean the user has muted audio.
          */
         public boolean isMuted() {
             return (mInfoFlags & FLAG_MUTED) != 0;
@@ -1688,8 +1755,9 @@
         }
 
         /** Metadata currently received from this station.
-         * null if no metadata have been received
-         * @return current meta data received from this program.
+         *
+         * @return current meta data received from this program, {@code null} if no metadata have
+         * been received
          */
         public RadioMetadata getMetadata() {
             return mMetadata;
@@ -1699,11 +1767,11 @@
          * A map of vendor-specific opaque strings, passed from HAL without changes.
          * Format of these strings can vary across vendors.
          *
-         * It may be used for extra features, that's not supported by a platform,
+         * <p>It may be used for extra features, that's not supported by a platform,
          * for example: paid-service=true; bitrate=320kbps.
          *
-         * Keys must be prefixed with unique vendor Java-style namespace,
-         * eg. 'com.somecompany.parameter1'.
+         * <p>Keys must be prefixed with unique vendor Java-style namespace,
+         * e.g. 'com.somecompany.parameter1'.
          */
         public @NonNull Map<String, String> getVendorInfo() {
             return mVendorInfo;
@@ -1830,13 +1898,14 @@
 
     /**
      * Open an interface to control a tuner on a given broadcast radio module.
-     * Optionally selects and applies the configuration passed as "config" argument.
+     *
+     * <p>Optionally selects and applies the configuration passed as "config" argument.
      * @param moduleId radio module identifier {@link ModuleProperties#getId()}. Mandatory.
      * @param config desired band and configuration to apply when enabling the hardware module.
      * optional, can be null.
      * @param withAudio {@code true} to request a tuner with an audio source.
      * This tuner is intended for live listening or recording or a radio program.
-     * If {@code false}, the tuner can only be used to retrieve program informations.
+     * If {@code false}, the tuner can only be used to retrieve program information.
      * @param callback {@link RadioTuner.Callback} interface. Mandatory.
      * @param handler the Handler on which the callbacks will be received.
      * Can be null if default handler is OK.
diff --git a/core/java/android/hardware/radio/RadioMetadata.java b/core/java/android/hardware/radio/RadioMetadata.java
index 67381ec..31880fd 100644
--- a/core/java/android/hardware/radio/RadioMetadata.java
+++ b/core/java/android/hardware/radio/RadioMetadata.java
@@ -291,7 +291,7 @@
     /**
      * Provides a Clock that can be used to describe time as provided by the Radio.
      *
-     * The clock is defined by the seconds since epoch at the UTC + 0 timezone
+     * <p>The clock time is defined by the seconds since epoch at the UTC + 0 timezone
      * and timezone offset from UTC + 0 represented in number of minutes.
      *
      * @hide
@@ -493,16 +493,16 @@
     /**
      * Retrieves an identifier for a bitmap.
      *
-     * The format of an identifier is opaque to the application,
+     * <p>The format of an identifier is opaque to the application,
      * with a special case of value 0 being invalid.
      * An identifier for a given image-tuner pair is unique, so an application
      * may cache images and determine if there is a necessity to fetch them
      * again - if identifier changes, it means the image has changed.
      *
-     * Only bitmap keys may be used with this method:
+     * <p>Only bitmap keys may be used with this method:
      * <ul>
-     * <li>{@link #METADATA_KEY_ICON}</li>
-     * <li>{@link #METADATA_KEY_ART}</li>
+     *     <li>{@link #METADATA_KEY_ICON}</li>
+     *     <li>{@link #METADATA_KEY_ART}</li>
      * </ul>
      *
      * @param key The key the value is stored under.
@@ -537,7 +537,7 @@
      *
      * <p>Only string array keys may be used with this method:
      * <ul>
-     * <li>{@link #METADATA_KEY_UFIDS}</li>
+     *     <li>{@link #METADATA_KEY_UFIDS}</li>
      * </ul>
      *
      * @param key The key the value is stored under
@@ -667,17 +667,17 @@
          * the METADATA_KEYs defined in this class are used they may only be one
          * of the following:
          * <ul>
-         * <li>{@link #METADATA_KEY_RDS_PS}</li>
-         * <li>{@link #METADATA_KEY_RDS_RT}</li>
-         * <li>{@link #METADATA_KEY_TITLE}</li>
-         * <li>{@link #METADATA_KEY_ARTIST}</li>
-         * <li>{@link #METADATA_KEY_ALBUM}</li>
-         * <li>{@link #METADATA_KEY_GENRE}</li>
-         * <li>{@link #METADATA_KEY_COMMENT_SHORT_DESCRIPTION}</li>
-         * <li>{@link #METADATA_KEY_COMMENT_ACTUAL_TEXT}</li>
-         * <li>{@link #METADATA_KEY_COMMERCIAL}</li>
-         * <li>{@link #METADATA_KEY_HD_STATION_NAME_SHORT}</li>
-         * <li>{@link #METADATA_KEY_HD_STATION_NAME_LONG}</li>
+         *     <li>{@link #METADATA_KEY_RDS_PS}</li>
+         *     <li>{@link #METADATA_KEY_RDS_RT}</li>
+         *     <li>{@link #METADATA_KEY_TITLE}</li>
+         *     <li>{@link #METADATA_KEY_ARTIST}</li>
+         *     <li>{@link #METADATA_KEY_ALBUM}</li>
+         *     <li>{@link #METADATA_KEY_GENRE}</li>
+         *     <li>{@link #METADATA_KEY_COMMENT_SHORT_DESCRIPTION}</li>
+         *     <li>{@link #METADATA_KEY_COMMENT_ACTUAL_TEXT}</li>
+         *     <li>{@link #METADATA_KEY_COMMERCIAL}</li>
+         *     <li>{@link #METADATA_KEY_HD_STATION_NAME_SHORT}</li>
+         *     <li>{@link #METADATA_KEY_HD_STATION_NAME_LONG}</li>
          * </ul>
          *
          * @param key The key for referencing this value
@@ -699,10 +699,10 @@
          * the METADATA_KEYs defined in this class are used they may only be one
          * of the following:
          * <ul>
-         * <li>{@link #METADATA_KEY_RDS_PI}</li>
-         * <li>{@link #METADATA_KEY_RDS_PTY}</li>
-         * <li>{@link #METADATA_KEY_RBDS_PTY}</li>
-         * <li>{@link #METADATA_KEY_HD_SUBCHANNELS_AVAILABLE}</li>
+         *     <li>{@link #METADATA_KEY_RDS_PI}</li>
+         *     <li>{@link #METADATA_KEY_RDS_PTY}</li>
+         *     <li>{@link #METADATA_KEY_RBDS_PTY}</li>
+         *     <li>{@link #METADATA_KEY_HD_SUBCHANNELS_AVAILABLE}</li>
          * </ul>
          * or any bitmap represented by its identifier.
          *
@@ -720,8 +720,8 @@
          * if the METADATA_KEYs defined in this class are used they may only be
          * one of the following:
          * <ul>
-         * <li>{@link #METADATA_KEY_ICON}</li>
-         * <li>{@link #METADATA_KEY_ART}</li>
+         *     <li>{@link #METADATA_KEY_ICON}</li>
+         *     <li>{@link #METADATA_KEY_ART}</li>
          * </ul>
          * <p>
          *
@@ -765,7 +765,7 @@
          * the METADATA_KEYs defined in this class are used they may only be one
          * of the following:
          * <ul>
-         * <li>{@link #METADATA_KEY_UFIDS}</li>
+         *     <li>{@link #METADATA_KEY_UFIDS}</li>
          * </ul>
          *
          * @param key The key for referencing this value
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index 86fc6f4..5666739 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -1476,15 +1476,26 @@
      */
     @TestApi
     static boolean hasWindowExtensionsEnabled() {
-        return HAS_WINDOW_EXTENSIONS_ON_DEVICE
-                && ActivityTaskManager.supportsMultiWindow(ActivityThread.currentApplication())
-                // Since enableWmExtensionsForAllFlag, HAS_WINDOW_EXTENSIONS_ON_DEVICE is now true
-                // on all devices by default as a build file property.
-                // Until finishing flag ramp up, only return true when
-                // ACTIVITY_EMBEDDING_GUARD_WITH_ANDROID_15 is false, which is set per device by
-                // OEMs.
-                && (Flags.enableWmExtensionsForAllFlag()
-                || !ACTIVITY_EMBEDDING_GUARD_WITH_ANDROID_15);
+        if (!Flags.enableWmExtensionsForAllFlag() && ACTIVITY_EMBEDDING_GUARD_WITH_ANDROID_15) {
+            // Since enableWmExtensionsForAllFlag, HAS_WINDOW_EXTENSIONS_ON_DEVICE is now true
+            // on all devices by default as a build file property.
+            // Until finishing flag ramp up, only return true when
+            // ACTIVITY_EMBEDDING_GUARD_WITH_ANDROID_15 is false, which is set per device by
+            // OEMs.
+            return false;
+        }
+
+        if (!HAS_WINDOW_EXTENSIONS_ON_DEVICE) {
+            return false;
+        }
+
+        try {
+            return ActivityTaskManager.supportsMultiWindow(ActivityThread.currentApplication());
+        } catch (Exception e) {
+            // In case the PackageManager is not set up correctly in test.
+            Log.e("WindowManager", "Unable to read if the device supports multi window", e);
+            return false;
+        }
     }
 
     /**
diff --git a/core/java/android/window/InputTransferToken.java b/core/java/android/window/InputTransferToken.java
index 5fab48f..d2cefa8 100644
--- a/core/java/android/window/InputTransferToken.java
+++ b/core/java/android/window/InputTransferToken.java
@@ -57,6 +57,7 @@
     private static native void nativeWriteToParcel(long nativeObject, Parcel out);
     private static native long nativeReadFromParcel(Parcel in);
     private static native IBinder nativeGetBinderToken(long nativeObject);
+    private static native long nativeGetBinderTokenRef(long nativeObject);
     private static native long nativeGetNativeInputTransferTokenFinalizer();
     private static native boolean nativeEquals(long nativeObject1, long nativeObject2);
 
@@ -130,7 +131,7 @@
      */
     @Override
     public int hashCode() {
-        return Objects.hash(getToken());
+        return Objects.hash(nativeGetBinderTokenRef(mNativeObject));
     }
 
     /**
diff --git a/core/java/android/window/flags/accessibility.aconfig b/core/java/android/window/flags/accessibility.aconfig
index 814c620..90b54bd 100644
--- a/core/java/android/window/flags/accessibility.aconfig
+++ b/core/java/android/window/flags/accessibility.aconfig
@@ -12,4 +12,7 @@
   namespace: "accessibility"
   description: "Always draw fullscreen orange border in fullscreen magnification"
   bug: "291891390"
+  metadata {
+        purpose: PURPOSE_BUGFIX
+  }
 }
\ No newline at end of file
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index 78f06b6..84715aa 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -217,6 +217,12 @@
     public static final String EXTRA_IS_AUDIO_CAPTURE_DEVICE = "is_audio_capture_device";
 
     /**
+     * Boolean extra to indicate if Resolver Sheet needs to be started in single user mode.
+     */
+    protected static final String EXTRA_RESTRICT_TO_SINGLE_USER =
+            "com.android.internal.app.ResolverActivity.EXTRA_RESTRICT_TO_SINGLE_USER";
+
+    /**
      * Integer extra to indicate which profile should be automatically selected.
      * <p>Can only be used if there is a work profile.
      * <p>Possible values can be either {@link #PROFILE_PERSONAL} or {@link #PROFILE_WORK}.
@@ -750,8 +756,10 @@
     }
 
     protected UserHandle getPersonalProfileUserHandle() {
-        if (privateSpaceEnabled() && isLaunchedAsPrivateProfile()){
-            return mPrivateProfileUserHandle;
+        // When launched in single user mode, only personal tab is populated, so we use
+        // tabOwnerUserHandleForLaunch as personal tab's user handle.
+        if (privateSpaceEnabled() && isLaunchedInSingleUserMode()) {
+            return getTabOwnerUserHandleForLaunch();
         }
         return mPersonalProfileUserHandle;
     }
@@ -822,11 +830,11 @@
         // If we are in work or private profile's process, return WorkProfile/PrivateProfile user
         // as owner, otherwise we always return PersonalProfile user as owner
         if (UserHandle.of(UserHandle.myUserId()).equals(getWorkProfileUserHandle())) {
-            return getWorkProfileUserHandle();
+            return mWorkProfileUserHandle;
         } else if (privateSpaceEnabled() && isLaunchedAsPrivateProfile()) {
-            return getPrivateProfileUserHandle();
+            return mPrivateProfileUserHandle;
         }
-        return getPersonalProfileUserHandle();
+        return mPersonalProfileUserHandle;
     }
 
     private boolean hasWorkProfile() {
@@ -847,8 +855,18 @@
                 && (UserHandle.myUserId() == getPrivateProfileUserHandle().getIdentifier());
     }
 
+    protected final boolean isLaunchedInSingleUserMode() {
+        // When launched from Private Profile, return true
+        if (isLaunchedAsPrivateProfile()) {
+            return true;
+        }
+        return getIntent()
+                .getBooleanExtra(EXTRA_RESTRICT_TO_SINGLE_USER, /* defaultValue = */ false);
+    }
+
     protected boolean shouldShowTabs() {
-        if (privateSpaceEnabled() && isLaunchedAsPrivateProfile()) {
+        // No Tabs are shown when launched in single user mode.
+        if (privateSpaceEnabled() && isLaunchedInSingleUserMode()) {
             return false;
         }
         return hasWorkProfile() && ENABLE_TABBED_VIEW;
diff --git a/core/java/com/android/internal/compat/compat_logging_flags.aconfig b/core/java/com/android/internal/compat/compat_logging_flags.aconfig
index fab3856..a5c31ed 100644
--- a/core/java/com/android/internal/compat/compat_logging_flags.aconfig
+++ b/core/java/com/android/internal/compat/compat_logging_flags.aconfig
@@ -2,7 +2,7 @@
 
 flag {
     name: "skip_old_and_disabled_compat_logging"
-    namespace: "platform_compat"
+    namespace: "app_compat"
     description: "Feature flag for skipping debug logging for changes that do not target the latest sdk or are disabled"
     bug: "323949942"
     is_fixed_read_only: true
diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java
index 0f1f7e9..a65a1bb 100644
--- a/core/java/com/android/internal/policy/DecorView.java
+++ b/core/java/com/android/internal/policy/DecorView.java
@@ -137,7 +137,7 @@
 
     private static final int SCRIM_LIGHT = 0xe6ffffff; // 90% white
 
-    private static final int SCRIM_ALPHA = 0xcc0000; // 80% alpha
+    private static final int SCRIM_ALPHA = 0xcc000000; // 80% alpha
 
     public static final ColorViewAttributes STATUS_BAR_COLOR_VIEW_ATTRIBUTES =
             new ColorViewAttributes(FLAG_TRANSLUCENT_STATUS,
diff --git a/core/java/com/android/internal/widget/ImageFloatingTextView.java b/core/java/com/android/internal/widget/ImageFloatingTextView.java
index 5da6435..352e6d8 100644
--- a/core/java/com/android/internal/widget/ImageFloatingTextView.java
+++ b/core/java/com/android/internal/widget/ImageFloatingTextView.java
@@ -31,6 +31,8 @@
 import android.widget.RemoteViews;
 import android.widget.TextView;
 
+import com.android.internal.R;
+
 /**
  * A TextView that can float around an image on the end.
  *
@@ -49,6 +51,7 @@
     private int mMaxLinesForHeight = -1;
     private int mLayoutMaxLines = -1;
     private int mImageEndMargin;
+    private final int mMaxLineUpperLimit;
 
     private int mStaticLayoutCreationCountInOnMeasure = 0;
 
@@ -71,6 +74,8 @@
         super(context, attrs, defStyleAttr, defStyleRes);
         setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL_FAST);
         setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY);
+        mMaxLineUpperLimit =
+                getResources().getInteger(R.integer.config_notificationLongTextMaxLineCount);
     }
 
     @Override
@@ -102,6 +107,11 @@
         } else {
             maxLines = getMaxLines() >= 0 ? getMaxLines() : Integer.MAX_VALUE;
         }
+
+        if (mMaxLineUpperLimit > 0) {
+            maxLines = Math.min(maxLines, mMaxLineUpperLimit);
+        }
+
         builder.setMaxLines(maxLines);
         mLayoutMaxLines = maxLines;
         if (shouldEllipsize) {
diff --git a/core/jni/OWNERS b/core/jni/OWNERS
index 3aca751..2a4f062 100644
--- a/core/jni/OWNERS
+++ b/core/jni/OWNERS
@@ -27,6 +27,7 @@
 # WindowManager
 per-file android_graphics_BLASTBufferQueue.cpp = file:/services/core/java/com/android/server/wm/OWNERS
 per-file android_view_Surface* = file:/services/core/java/com/android/server/wm/OWNERS
+per-file android_view_WindowManagerGlobal.cpp = file:/services/core/java/com/android/server/wm/OWNERS
 per-file android_window_* = file:/services/core/java/com/android/server/wm/OWNERS
 
 # Resources
diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp
index d48cdc4..5223798 100644
--- a/core/jni/android_media_AudioSystem.cpp
+++ b/core/jni/android_media_AudioSystem.cpp
@@ -161,7 +161,6 @@
     jfieldID mMixType;
     jfieldID mCallbackFlags;
     jfieldID mToken;
-    jfieldID mVirtualDeviceId;
 } gAudioMixFields;
 
 static jclass gAudioFormatClass;
@@ -2313,7 +2312,7 @@
     jstring deviceAddress = env->NewStringUTF(nAudioMix.mDeviceAddress.c_str());
     *jAudioMix = env->NewObject(gAudioMixClass, gAudioMixCstor, jAudioMixingRule, jAudioFormat,
                                 nAudioMix.mRouteFlags, nAudioMix.mCbFlags, nAudioMix.mDeviceType,
-                                deviceAddress, jBinderToken, nAudioMix.mVirtualDeviceId);
+                                deviceAddress, jBinderToken);
     return AUDIO_JAVA_SUCCESS;
 }
 
@@ -2348,7 +2347,6 @@
             aiBinder(AIBinder_fromJavaBinder(env, jToken), &AIBinder_decStrong);
     nAudioMix->mToken = AIBinder_toPlatformBinder(aiBinder.get());
 
-    nAudioMix->mVirtualDeviceId = env->GetIntField(jAudioMix, gAudioMixFields.mVirtualDeviceId);
     jint status = convertAudioMixingRuleToNative(env, jRule, &(nAudioMix->mCriteria));
 
     env->DeleteLocalRef(jRule);
@@ -3678,7 +3676,7 @@
         gAudioMixCstor =
                 GetMethodIDOrDie(env, audioMixClass, "<init>",
                                  "(Landroid/media/audiopolicy/AudioMixingRule;Landroid/"
-                                 "media/AudioFormat;IIILjava/lang/String;Landroid/os/IBinder;I)V");
+                                 "media/AudioFormat;IIILjava/lang/String;Landroid/os/IBinder;)V");
     }
     gAudioMixFields.mRule = GetFieldIDOrDie(env, audioMixClass, "mRule",
                                                 "Landroid/media/audiopolicy/AudioMixingRule;");
@@ -3691,7 +3689,6 @@
     gAudioMixFields.mMixType = GetFieldIDOrDie(env, audioMixClass, "mMixType", "I");
     gAudioMixFields.mCallbackFlags = GetFieldIDOrDie(env, audioMixClass, "mCallbackFlags", "I");
     gAudioMixFields.mToken = GetFieldIDOrDie(env, audioMixClass, "mToken", "Landroid/os/IBinder;");
-    gAudioMixFields.mVirtualDeviceId = GetFieldIDOrDie(env, audioMixClass, "mVirtualDeviceId", "I");
 
     jclass audioFormatClass = FindClassOrDie(env, "android/media/AudioFormat");
     gAudioFormatClass = MakeGlobalRefOrDie(env, audioFormatClass);
diff --git a/core/jni/android_view_WindowManagerGlobal.cpp b/core/jni/android_view_WindowManagerGlobal.cpp
index b03ac88..abc621d 100644
--- a/core/jni/android_view_WindowManagerGlobal.cpp
+++ b/core/jni/android_view_WindowManagerGlobal.cpp
@@ -48,7 +48,7 @@
             surfaceControlObj(env,
                               android_view_SurfaceControl_getJavaSurfaceControl(env,
                                                                                 surfaceControl));
-    jobject clientTokenObj = javaObjectForIBinder(env, clientToken);
+    ScopedLocalRef<jobject> clientTokenObj(env, javaObjectForIBinder(env, clientToken));
     ScopedLocalRef<jobject> clientInputTransferTokenObj(
             env,
             android_window_InputTransferToken_getJavaInputTransferToken(env,
@@ -57,7 +57,7 @@
             inputChannelObj(env,
                             env->CallStaticObjectMethod(gWindowManagerGlobal.clazz,
                                                         gWindowManagerGlobal.createInputChannel,
-                                                        clientTokenObj,
+                                                        clientTokenObj.get(),
                                                         hostInputTransferTokenObj.get(),
                                                         surfaceControlObj.get(),
                                                         clientInputTransferTokenObj.get()));
@@ -68,9 +68,9 @@
 void removeInputChannel(const sp<IBinder>& clientToken) {
     JNIEnv* env = AndroidRuntime::getJNIEnv();
 
-    jobject clientTokenObj(javaObjectForIBinder(env, clientToken));
+    ScopedLocalRef<jobject> clientTokenObj(env, javaObjectForIBinder(env, clientToken));
     env->CallStaticObjectMethod(gWindowManagerGlobal.clazz, gWindowManagerGlobal.removeInputChannel,
-                                clientTokenObj);
+                                clientTokenObj.get());
 }
 
 int register_android_view_WindowManagerGlobal(JNIEnv* env) {
diff --git a/core/jni/android_window_InputTransferToken.cpp b/core/jni/android_window_InputTransferToken.cpp
index 8fb668d..5bcea9b 100644
--- a/core/jni/android_window_InputTransferToken.cpp
+++ b/core/jni/android_window_InputTransferToken.cpp
@@ -70,6 +70,11 @@
     return javaObjectForIBinder(env, inputTransferToken->mToken);
 }
 
+static jlong nativeGetBinderTokenRef(JNIEnv*, jclass, jlong nativeObj) {
+    sp<InputTransferToken> inputTransferToken = reinterpret_cast<InputTransferToken*>(nativeObj);
+    return reinterpret_cast<jlong>(inputTransferToken->mToken.get());
+}
+
 InputTransferToken* android_window_InputTransferToken_getNativeInputTransferToken(
         JNIEnv* env, jobject inputTransferTokenObj) {
     if (inputTransferTokenObj != nullptr &&
@@ -114,6 +119,7 @@
     {"nativeWriteToParcel", "(JLandroid/os/Parcel;)V", (void*)nativeWriteToParcel},
     {"nativeReadFromParcel", "(Landroid/os/Parcel;)J", (void*)nativeReadFromParcel},
     {"nativeGetBinderToken", "(J)Landroid/os/IBinder;", (void*)nativeGetBinderToken},
+    {"nativeGetBinderTokenRef", "(J)J", (void*)nativeGetBinderTokenRef},
     {"nativeGetNativeInputTransferTokenFinalizer", "()J", (void*)nativeGetNativeInputTransferTokenFinalizer},
         {"nativeEquals", "(JJ)Z", (void*) nativeEquals},
         // clang-format on
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index e3f1cb6..efba709 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -1483,6 +1483,11 @@
     <!-- Number of notifications to keep in the notification service historical archive -->
     <integer name="config_notificationServiceArchiveSize">100</integer>
 
+    <!-- Upper limit imposed for long text content for BigTextStyle, MessagingStyle and
+    ConversationStyle notifications for performance reasons, and that line count is also
+    capped by vertical space available. It is only enabled when the value is positive int.-->
+    <integer name="config_notificationLongTextMaxLineCount">10</integer>
+
     <!-- Allow the menu hard key to be disabled in LockScreen on some devices -->
     <bool name="config_disableMenuKeyInLockScreen">false</bool>
 
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index f915f03..5639a58 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -231,8 +231,10 @@
     <string name="NetworkPreferenceSwitchSummary">Try changing preferred network. Tap to change.</string>
     <!-- Displayed to tell the user that emergency calls might not be available. -->
     <string name="EmergencyCallWarningTitle">Emergency calling unavailable</string>
-    <!-- Displayed to tell the user that emergency calls might not be available. -->
-    <string name="EmergencyCallWarningSummary">Can\u2019t make emergency calls over Wi\u2011Fi</string>
+    <!-- Displayed to tell the user that emergency calls might not be available; this is shown to
+         the user when only WiFi calling is available and the carrier does not support emergency
+         calls over WiFi calling. -->
+    <string name="EmergencyCallWarningSummary">Emergency calls require a mobile network</string>
 
     <!-- Telephony notification channel name for a channel containing network alert notifications. -->
     <string name="notification_channel_network_alert">Alerts</string>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index f4b42f6..668a88c 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2078,6 +2078,7 @@
   <java-symbol type="integer" name="config_notificationsBatteryMediumARGB" />
   <java-symbol type="integer" name="config_notificationsBatteryNearlyFullLevel" />
   <java-symbol type="integer" name="config_notificationServiceArchiveSize" />
+  <java-symbol type="integer" name="config_notificationLongTextMaxLineCount" />
   <java-symbol type="dimen" name="config_rotaryEncoderAxisScrollTickInterval" />
   <java-symbol type="integer" name="config_recentVibrationsDumpSizeLimit" />
   <java-symbol type="integer" name="config_previousVibrationsDumpSizeLimit" />
diff --git a/core/tests/bugreports/OWNERS b/core/tests/bugreports/OWNERS
new file mode 100644
index 0000000..dbd767c
--- /dev/null
+++ b/core/tests/bugreports/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 153446
+file:/platform/frameworks/native:/cmds/dumpstate/OWNERS
diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java b/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java
index cb8754a..488f017 100644
--- a/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java
+++ b/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java
@@ -27,6 +27,7 @@
 import static androidx.test.espresso.matcher.ViewMatchers.withText;
 
 import static com.android.internal.app.MatcherUtils.first;
+import static com.android.internal.app.ResolverActivity.EXTRA_RESTRICT_TO_SINGLE_USER;
 import static com.android.internal.app.ResolverDataProvider.createPackageManagerMockedInfo;
 import static com.android.internal.app.ResolverWrapperActivity.sOverrides;
 
@@ -1254,6 +1255,51 @@
         }
     }
 
+    @Test
+    public void testTriggerFromMainProfile_inSingleUserMode_withWorkProfilePresent() {
+        mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
+                android.multiuser.Flags.FLAG_ALLOW_RESOLVER_SHEET_FOR_PRIVATE_SPACE);
+        markWorkProfileUserAvailable();
+        setTabOwnerUserHandleForLaunch(PERSONAL_USER_HANDLE);
+        Intent sendIntent = createSendImageIntent();
+        sendIntent.putExtra(EXTRA_RESTRICT_TO_SINGLE_USER, true);
+        List<ResolvedComponentInfo> personalResolvedComponentInfos =
+                createResolvedComponentsForTestWithOtherProfile(3, PERSONAL_USER_HANDLE);
+        List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4,
+                sOverrides.workProfileUserHandle);
+        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+        waitForIdle();
+        assertThat(activity.getPersonalListAdapter().getCount(), is(2));
+        onView(withId(R.id.tabs)).check(matches(not(isDisplayed())));
+        assertEquals(activity.getMultiProfilePagerAdapterCount(), 1);
+        for (ResolvedComponentInfo resolvedInfo : personalResolvedComponentInfos) {
+            assertEquals(resolvedInfo.getResolveInfoAt(0).userHandle, PERSONAL_USER_HANDLE);
+        }
+    }
+
+    @Test
+    public void testTriggerFromWorkProfile_inSingleUserMode() {
+        mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
+                android.multiuser.Flags.FLAG_ALLOW_RESOLVER_SHEET_FOR_PRIVATE_SPACE);
+        markWorkProfileUserAvailable();
+        setTabOwnerUserHandleForLaunch(sOverrides.workProfileUserHandle);
+        Intent sendIntent = createSendImageIntent();
+        sendIntent.putExtra(EXTRA_RESTRICT_TO_SINGLE_USER, true);
+        List<ResolvedComponentInfo> personalResolvedComponentInfos =
+                createResolvedComponentsForTest(3, sOverrides.workProfileUserHandle);
+        setupResolverControllers(personalResolvedComponentInfos);
+        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+        waitForIdle();
+        assertThat(activity.getPersonalListAdapter().getCount(), is(3));
+        onView(withId(R.id.tabs)).check(matches(not(isDisplayed())));
+        assertEquals(activity.getMultiProfilePagerAdapterCount(), 1);
+        for (ResolvedComponentInfo resolvedInfo : personalResolvedComponentInfos) {
+            assertEquals(resolvedInfo.getResolveInfoAt(0).userHandle,
+                    sOverrides.workProfileUserHandle);
+        }
+    }
+
     private Intent createSendImageIntent() {
         Intent sendIntent = new Intent();
         sendIntent.setAction(Intent.ACTION_SEND);
@@ -1339,6 +1385,10 @@
         ResolverWrapperActivity.sOverrides.privateProfileUserHandle = UserHandle.of(12);
     }
 
+    private void setTabOwnerUserHandleForLaunch(UserHandle tabOwnerUserHandleForLaunch) {
+        sOverrides.tabOwnerUserHandleForLaunch = tabOwnerUserHandleForLaunch;
+    }
+
     private void setupResolverControllers(
             List<ResolvedComponentInfo> personalResolvedComponentInfos,
             List<ResolvedComponentInfo> workResolvedComponentInfos) {
diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java
index 862cbd5..4604b01 100644
--- a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java
+++ b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java
@@ -116,6 +116,10 @@
             when(sOverrides.resolverListController.getUserHandle()).thenReturn(UserHandle.SYSTEM);
             return sOverrides.resolverListController;
         }
+        if (isLaunchedInSingleUserMode()) {
+            when(sOverrides.resolverListController.getUserHandle()).thenReturn(userHandle);
+            return sOverrides.resolverListController;
+        }
         when(sOverrides.workResolverListController.getUserHandle()).thenReturn(userHandle);
         return sOverrides.workResolverListController;
     }
diff --git a/data/keyboards/Vendor_054c_Product_05c4.idc b/data/keyboards/Vendor_054c_Product_05c4.idc
index 2cb3f7b..9576e8d 100644
--- a/data/keyboards/Vendor_054c_Product_05c4.idc
+++ b/data/keyboards/Vendor_054c_Product_05c4.idc
@@ -13,9 +13,11 @@
 # limitations under the License.
 
 #
-# Sony DS4 motion sensor configuration file.
+# Sony Playstation(R) DualShock 4 Controller
 #
 
+## Motion sensor ##
+
 # reporting mode 0 - continuous
 sensor.accelerometer.reportingMode = 0
 # The delay between sensor events corresponding to the lowest frequency in microsecond
@@ -33,3 +35,27 @@
 sensor.gyroscope.minDelay = 5000
 # The power in mA used by this sensor while in use
 sensor.gyroscope.power = 0.8
+
+## Touchpad ##
+
+# After the DualShock 4 has been connected over Bluetooth for a minute or so,
+# its reports start bunching up in time, meaning that we receive 2–4 reports
+# within a millisecond followed by a >10ms wait until the next batch.
+#
+# This uneven timing causes the apparent speed of a finger (calculated using
+# time deltas between received reports) to vary dramatically even if it's
+# actually moving smoothly across the touchpad, triggering the touchpad stack's
+# drumroll detection logic, which causes the finger's single smooth movement to
+# be treated as many small movements of consecutive touches, which are then
+# inhibited by the click wiggle filter.
+#
+# Since this touchpad does not seem vulnerable to click wiggle, we can safely
+# disable drumroll detection due to speed changes (by setting the speed change
+# threshold very high, since there's no boolean control property).
+gestureProp.Drumroll_Max_Speed_Change_Factor = 1000000000
+
+# Because of the way this touchpad is positioned, touches around the edges are
+# no more likely to be palms than ones in the middle, so remove the edge zones
+# from the palm classifier to increase the usable area of the pad.
+gestureProp.Palm_Edge_Zone_Width = 0
+gestureProp.Tap_Exclusion_Border_Width = 0
diff --git a/data/keyboards/Vendor_054c_Product_09cc.idc b/data/keyboards/Vendor_054c_Product_09cc.idc
index 2cb3f7b..9576e8d 100644
--- a/data/keyboards/Vendor_054c_Product_09cc.idc
+++ b/data/keyboards/Vendor_054c_Product_09cc.idc
@@ -13,9 +13,11 @@
 # limitations under the License.
 
 #
-# Sony DS4 motion sensor configuration file.
+# Sony Playstation(R) DualShock 4 Controller
 #
 
+## Motion sensor ##
+
 # reporting mode 0 - continuous
 sensor.accelerometer.reportingMode = 0
 # The delay between sensor events corresponding to the lowest frequency in microsecond
@@ -33,3 +35,27 @@
 sensor.gyroscope.minDelay = 5000
 # The power in mA used by this sensor while in use
 sensor.gyroscope.power = 0.8
+
+## Touchpad ##
+
+# After the DualShock 4 has been connected over Bluetooth for a minute or so,
+# its reports start bunching up in time, meaning that we receive 2–4 reports
+# within a millisecond followed by a >10ms wait until the next batch.
+#
+# This uneven timing causes the apparent speed of a finger (calculated using
+# time deltas between received reports) to vary dramatically even if it's
+# actually moving smoothly across the touchpad, triggering the touchpad stack's
+# drumroll detection logic, which causes the finger's single smooth movement to
+# be treated as many small movements of consecutive touches, which are then
+# inhibited by the click wiggle filter.
+#
+# Since this touchpad does not seem vulnerable to click wiggle, we can safely
+# disable drumroll detection due to speed changes (by setting the speed change
+# threshold very high, since there's no boolean control property).
+gestureProp.Drumroll_Max_Speed_Change_Factor = 1000000000
+
+# Because of the way this touchpad is positioned, touches around the edges are
+# no more likely to be palms than ones in the middle, so remove the edge zones
+# from the palm classifier to increase the usable area of the pad.
+gestureProp.Palm_Edge_Zone_Width = 0
+gestureProp.Tap_Exclusion_Border_Width = 0
diff --git a/keystore/java/android/security/AndroidKeyStoreMaintenance.java b/keystore/java/android/security/AndroidKeyStoreMaintenance.java
index 2430e8d..efbbfc2 100644
--- a/keystore/java/android/security/AndroidKeyStoreMaintenance.java
+++ b/keystore/java/android/security/AndroidKeyStoreMaintenance.java
@@ -175,20 +175,6 @@
     }
 
     /**
-     * Informs Keystore 2.0 that an off body event was detected.
-     */
-    public static void onDeviceOffBody() {
-        StrictMode.noteDiskWrite();
-        try {
-            getService().onDeviceOffBody();
-        } catch (Exception e) {
-            // TODO This fails open. This is not a regression with respect to keystore1 but it
-            //      should get fixed.
-            Log.e(TAG, "Error while reporting device off body event.", e);
-        }
-    }
-
-    /**
      * Migrates a key given by the source descriptor to the location designated by the destination
      * descriptor.
      *
diff --git a/keystore/java/android/security/KeyStore.java b/keystore/java/android/security/KeyStore.java
index bd9abec..f105072 100644
--- a/keystore/java/android/security/KeyStore.java
+++ b/keystore/java/android/security/KeyStore.java
@@ -56,11 +56,4 @@
 
         return Authorization.addAuthToken(authToken);
     }
-
-    /**
-     * Notify keystore that the device went off-body.
-     */
-    public void onDeviceOffBody() {
-        AndroidKeyStoreMaintenance.onDeviceOffBody();
-    }
 }
diff --git a/keystore/java/android/security/keystore/KeyGenParameterSpec.java b/keystore/java/android/security/keystore/KeyGenParameterSpec.java
index 7aecfd8..d359a90 100644
--- a/keystore/java/android/security/keystore/KeyGenParameterSpec.java
+++ b/keystore/java/android/security/keystore/KeyGenParameterSpec.java
@@ -880,9 +880,7 @@
     }
 
     /**
-     * Returns {@code true} if the screen must be unlocked for this key to be used for decryption or
-     * signing. Encryption and signature verification will still be available when the screen is
-     * locked.
+     * Returns {@code true} if the key is authorized to be used only while the device is unlocked.
      *
      * @see Builder#setUnlockedDeviceRequired(boolean)
      */
@@ -1672,16 +1670,16 @@
          * {@link #setUserAuthenticationValidityDurationSeconds} and
          * {@link #setUserAuthenticationRequired}). Once the device has been removed from the
          * user's body, the key will be considered unauthorized and the user will need to
-         * re-authenticate to use it. For keys without an authentication validity period this
-         * parameter has no effect.
+         * re-authenticate to use it. If the device does not have an on-body sensor or the key does
+         * not have an authentication validity period, this parameter has no effect.
+         * <p>
+         * Since Android 12 (API level 31), this parameter has no effect even on devices that have
+         * an on-body sensor. A future version of Android may restore enforcement of this parameter.
+         * Meanwhile, it is recommended to not use it.
          *
-         * <p>Similarly, on devices that do not have an on-body sensor, this parameter will have no
-         * effect; the device will always be considered to be "on-body" and the key will therefore
-         * remain authorized until the validity period ends.
-         *
-         * @param remainsValid if {@code true}, and if the device supports on-body detection, key
-         * will be invalidated when the device is removed from the user's body or when the
-         * authentication validity expires, whichever occurs first.
+         * @param remainsValid if {@code true}, and if the device supports enforcement of this
+         * parameter, the key will be invalidated when the device is removed from the user's body or
+         * when the authentication validity expires, whichever occurs first.
          */
         @NonNull
         public Builder setUserAuthenticationValidWhileOnBody(boolean remainsValid) {
@@ -1723,11 +1721,49 @@
         }
 
         /**
-         * Sets whether the keystore requires the screen to be unlocked before allowing decryption
-         * using this key. If this is set to {@code true}, any attempt to decrypt or sign using this
-         * key while the screen is locked will fail. A locked device requires a PIN, password,
-         * biometric, or other trusted factor to access. While the screen is locked, any associated
-         * public key can still be used (e.g for signature verification).
+         * Sets whether this key is authorized to be used only while the device is unlocked.
+         * <p>
+         * The device is considered to be locked for a user when the user's apps are currently
+         * inaccessible and some form of lock screen authentication is required to regain access to
+         * them. For the full definition, see {@link KeyguardManager#isDeviceLocked()}.
+         * <p>
+         * Public key operations aren't restricted by {@code setUnlockedDeviceRequired(true)} and
+         * may be performed even while the device is locked. In Android 11 (API level 30) and lower,
+         * encryption and verification operations with symmetric keys weren't restricted either.
+         * <p>
+         * Keys that use {@code setUnlockedDeviceRequired(true)} can be imported and generated even
+         * while the device is locked, as long as the device has been unlocked at least once since
+         * the last reboot. However, such keys cannot be used (except for the unrestricted
+         * operations mentioned above) until the device is unlocked. Apps that need to encrypt data
+         * while the device is locked such that it can only be decrypted while the device is
+         * unlocked can generate a key and encrypt the data in software, import the key into
+         * Keystore using {@code setUnlockedDeviceRequired(true)}, and zeroize the original key.
+         * <p>
+         * {@code setUnlockedDeviceRequired(true)} is related to but distinct from
+         * {@link #setUserAuthenticationRequired(boolean) setUserAuthenticationRequired(true)}.
+         * {@code setUnlockedDeviceRequired(true)} requires that the device be unlocked, whereas
+         * {@code setUserAuthenticationRequired(true)} requires that a specific type of strong
+         * authentication has happened within a specific time period. They may be used together or
+         * separately; there are cases in which one requirement can be satisfied but not the other.
+         * <p>
+         * <b>Warning:</b> Be careful using {@code setUnlockedDeviceRequired(true)} on Android 14
+         * (API level 34) and lower, since the following bugs existed in Android 12 through 14:
+         * <ul>
+         *   <li>When the user didn't have a secure lock screen, unlocked-device-required keys
+         *   couldn't be generated, imported, or used.</li>
+         *   <li>When the user's secure lock screen was removed, all of that user's
+         *   unlocked-device-required keys were automatically deleted.</li>
+         *   <li>Unlocking the device with a non-strong biometric, such as face on many devices,
+         *   didn't re-authorize the use of unlocked-device-required keys.</li>
+         *   <li>Unlocking the device with a biometric didn't re-authorize the use of
+         *   unlocked-device-required keys in profiles that share their parent user's lock.</li>
+         * </ul>
+         * These issues are fixed in Android 15, so apps can avoid them by using
+         * {@code setUnlockedDeviceRequired(true)} only on Android 15 and higher.
+         * Apps that use both {@code setUnlockedDeviceRequired(true)} and
+         * {@link #setUserAuthenticationRequired(boolean) setUserAuthenticationRequired(true)}
+         * are unaffected by the first two issues, since the first two issues describe expected
+         * behavior for {@code setUserAuthenticationRequired(true)}.
          */
         @NonNull
         public Builder setUnlockedDeviceRequired(boolean unlockedDeviceRequired) {
diff --git a/keystore/java/android/security/keystore/KeyInfo.java b/keystore/java/android/security/keystore/KeyInfo.java
index 5cffe46..2163ca2 100644
--- a/keystore/java/android/security/keystore/KeyInfo.java
+++ b/keystore/java/android/security/keystore/KeyInfo.java
@@ -279,7 +279,7 @@
     }
 
     /**
-     * Returns {@code true} if the key is authorized to be used only when the device is unlocked.
+     * Returns {@code true} if the key is authorized to be used only while the device is unlocked.
      *
      * <p>This authorization applies only to secret key and private key operations. Public key
      * operations are not restricted.
diff --git a/keystore/java/android/security/keystore/KeyProtection.java b/keystore/java/android/security/keystore/KeyProtection.java
index 31b4a5e..8e5ac45 100644
--- a/keystore/java/android/security/keystore/KeyProtection.java
+++ b/keystore/java/android/security/keystore/KeyProtection.java
@@ -577,9 +577,7 @@
     }
 
     /**
-     * Returns {@code true} if the screen must be unlocked for this key to be used for decryption or
-     * signing. Encryption and signature verification will still be available when the screen is
-     * locked.
+     * Returns {@code true} if the key is authorized to be used only while the device is unlocked.
      *
      * @see Builder#setUnlockedDeviceRequired(boolean)
      */
@@ -1039,16 +1037,16 @@
          * {@link #setUserAuthenticationValidityDurationSeconds} and
          * {@link #setUserAuthenticationRequired}). Once the device has been removed from the
          * user's body, the key will be considered unauthorized and the user will need to
-         * re-authenticate to use it. For keys without an authentication validity period this
-         * parameter has no effect.
+         * re-authenticate to use it. If the device does not have an on-body sensor or the key does
+         * not have an authentication validity period, this parameter has no effect.
+         * <p>
+         * Since Android 12 (API level 31), this parameter has no effect even on devices that have
+         * an on-body sensor. A future version of Android may restore enforcement of this parameter.
+         * Meanwhile, it is recommended to not use it.
          *
-         * <p>Similarly, on devices that do not have an on-body sensor, this parameter will have no
-         * effect; the device will always be considered to be "on-body" and the key will therefore
-         * remain authorized until the validity period ends.
-         *
-         * @param remainsValid if {@code true}, and if the device supports on-body detection, key
-         * will be invalidated when the device is removed from the user's body or when the
-         * authentication validity expires, whichever occurs first.
+         * @param remainsValid if {@code true}, and if the device supports enforcement of this
+         * parameter, the key will be invalidated when the device is removed from the user's body or
+         * when the authentication validity expires, whichever occurs first.
          */
         @NonNull
         public Builder setUserAuthenticationValidWhileOnBody(boolean remainsValid) {
@@ -1117,11 +1115,49 @@
         }
 
         /**
-         * Sets whether the keystore requires the screen to be unlocked before allowing decryption
-         * using this key. If this is set to {@code true}, any attempt to decrypt or sign using this
-         * key while the screen is locked will fail. A locked device requires a PIN, password,
-         * biometric, or other trusted factor to access. While the screen is locked, the key can
-         * still be used for encryption or signature verification.
+         * Sets whether this key is authorized to be used only while the device is unlocked.
+         * <p>
+         * The device is considered to be locked for a user when the user's apps are currently
+         * inaccessible and some form of lock screen authentication is required to regain access to
+         * them. For the full definition, see {@link KeyguardManager#isDeviceLocked()}.
+         * <p>
+         * Public key operations aren't restricted by {@code setUnlockedDeviceRequired(true)} and
+         * may be performed even while the device is locked. In Android 11 (API level 30) and lower,
+         * encryption and verification operations with symmetric keys weren't restricted either.
+         * <p>
+         * Keys that use {@code setUnlockedDeviceRequired(true)} can be imported and generated even
+         * while the device is locked, as long as the device has been unlocked at least once since
+         * the last reboot. However, such keys cannot be used (except for the unrestricted
+         * operations mentioned above) until the device is unlocked. Apps that need to encrypt data
+         * while the device is locked such that it can only be decrypted while the device is
+         * unlocked can generate a key and encrypt the data in software, import the key into
+         * Keystore using {@code setUnlockedDeviceRequired(true)}, and zeroize the original key.
+         * <p>
+         * {@code setUnlockedDeviceRequired(true)} is related to but distinct from
+         * {@link #setUserAuthenticationRequired(boolean) setUserAuthenticationRequired(true)}.
+         * {@code setUnlockedDeviceRequired(true)} requires that the device be unlocked, whereas
+         * {@code setUserAuthenticationRequired(true)} requires that a specific type of strong
+         * authentication has happened within a specific time period. They may be used together or
+         * separately; there are cases in which one requirement can be satisfied but not the other.
+         * <p>
+         * <b>Warning:</b> Be careful using {@code setUnlockedDeviceRequired(true)} on Android 14
+         * (API level 34) and lower, since the following bugs existed in Android 12 through 14:
+         * <ul>
+         *   <li>When the user didn't have a secure lock screen, unlocked-device-required keys
+         *   couldn't be generated, imported, or used.</li>
+         *   <li>When the user's secure lock screen was removed, all of that user's
+         *   unlocked-device-required keys were automatically deleted.</li>
+         *   <li>Unlocking the device with a non-strong biometric, such as face on many devices,
+         *   didn't re-authorize the use of unlocked-device-required keys.</li>
+         *   <li>Unlocking the device with a biometric didn't re-authorize the use of
+         *   unlocked-device-required keys in profiles that share their parent user's lock.</li>
+         * </ul>
+         * These issues are fixed in Android 15, so apps can avoid them by using
+         * {@code setUnlockedDeviceRequired(true)} only on Android 15 and higher.
+         * Apps that use both {@code setUnlockedDeviceRequired(true)} and
+         * {@link #setUserAuthenticationRequired(boolean) setUserAuthenticationRequired(true)}
+         * are unaffected by the first two issues, since the first two issues describe expected
+         * behavior for {@code setUserAuthenticationRequired(true)}.
          */
         @NonNull
         public Builder setUnlockedDeviceRequired(boolean unlockedDeviceRequired) {
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt
index e422198..e73d880 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt
@@ -26,6 +26,7 @@
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.internal.protolog.common.ProtoLog
 import com.android.wm.shell.R
 import com.android.wm.shell.bubbles.BubblePositioner.MAX_HEIGHT
 import com.android.wm.shell.common.bubbles.BubbleBarLocation
@@ -54,6 +55,7 @@
 
     @Before
     fun setUp() {
+        ProtoLog.REQUIRE_PROTOLOGTOOL = false
         val windowManager = context.getSystemService(WindowManager::class.java)
         positioner = BubblePositioner(context, windowManager)
     }
@@ -167,8 +169,9 @@
 
     @Test
     fun testGetRestingPosition_afterBoundsChange() {
-        positioner.update(defaultDeviceConfig.copy(isLargeScreen = true,
-                windowBounds = Rect(0, 0, 2000, 1600)))
+        positioner.update(
+            defaultDeviceConfig.copy(isLargeScreen = true, windowBounds = Rect(0, 0, 2000, 1600))
+        )
 
         // Set the resting position to the right side
         var allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
@@ -176,8 +179,9 @@
         positioner.restingPosition = restingPosition
 
         // Now make the device smaller
-        positioner.update(defaultDeviceConfig.copy(isLargeScreen = false,
-                windowBounds = Rect(0, 0, 1000, 1600)))
+        positioner.update(
+            defaultDeviceConfig.copy(isLargeScreen = false, windowBounds = Rect(0, 0, 1000, 1600))
+        )
 
         // Check the resting position is on the correct side
         allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
@@ -236,7 +240,8 @@
                 0 /* taskId */,
                 null /* locus */,
                 true /* isDismissable */,
-                directExecutor()) {}
+                directExecutor()
+            ) {}
 
         // Ensure the height is the same as the desired value
         assertThat(positioner.getExpandedViewHeight(bubble))
@@ -263,7 +268,8 @@
                 0 /* taskId */,
                 null /* locus */,
                 true /* isDismissable */,
-                directExecutor()) {}
+                directExecutor()
+            ) {}
 
         // Ensure the height is the same as the desired value
         val minHeight =
@@ -471,20 +477,20 @@
     fun testGetTaskViewContentWidth_onLeft() {
         positioner.update(defaultDeviceConfig.copy(insets = Insets.of(100, 0, 200, 0)))
         val taskViewWidth = positioner.getTaskViewContentWidth(true /* onLeft */)
-        val paddings = positioner.getExpandedViewContainerPadding(true /* onLeft */,
-                false /* isOverflow */)
-        assertThat(taskViewWidth).isEqualTo(
-                positioner.screenRect.width() - paddings[0] - paddings[2])
+        val paddings =
+            positioner.getExpandedViewContainerPadding(true /* onLeft */, false /* isOverflow */)
+        assertThat(taskViewWidth)
+            .isEqualTo(positioner.screenRect.width() - paddings[0] - paddings[2])
     }
 
     @Test
     fun testGetTaskViewContentWidth_onRight() {
         positioner.update(defaultDeviceConfig.copy(insets = Insets.of(100, 0, 200, 0)))
         val taskViewWidth = positioner.getTaskViewContentWidth(false /* onLeft */)
-        val paddings = positioner.getExpandedViewContainerPadding(false /* onLeft */,
-                false /* isOverflow */)
-        assertThat(taskViewWidth).isEqualTo(
-                positioner.screenRect.width() - paddings[0] - paddings[2])
+        val paddings =
+            positioner.getExpandedViewContainerPadding(false /* onLeft */, false /* isOverflow */)
+        assertThat(taskViewWidth)
+            .isEqualTo(positioner.screenRect.width() - paddings[0] - paddings[2])
     }
 
     @Test
@@ -513,6 +519,66 @@
         assertThat(positioner.isBubbleBarOnLeft).isFalse()
     }
 
+    @Test
+    fun testGetBubbleBarExpandedViewBounds_onLeft() {
+        testGetBubbleBarExpandedViewBounds(onLeft = true, isOverflow = false)
+    }
+
+    @Test
+    fun testGetBubbleBarExpandedViewBounds_onRight() {
+        testGetBubbleBarExpandedViewBounds(onLeft = false, isOverflow = false)
+    }
+
+    @Test
+    fun testGetBubbleBarExpandedViewBounds_isOverflow_onLeft() {
+        testGetBubbleBarExpandedViewBounds(onLeft = true, isOverflow = true)
+    }
+
+    @Test
+    fun testGetBubbleBarExpandedViewBounds_isOverflow_onRight() {
+        testGetBubbleBarExpandedViewBounds(onLeft = false, isOverflow = true)
+    }
+
+    private fun testGetBubbleBarExpandedViewBounds(onLeft: Boolean, isOverflow: Boolean) {
+        positioner.setShowingInBubbleBar(true)
+        val deviceConfig =
+            defaultDeviceConfig.copy(
+                isLargeScreen = true,
+                isLandscape = true,
+                insets = Insets.of(10, 20, 5, 15),
+                windowBounds = Rect(0, 0, 2000, 2600)
+            )
+        positioner.update(deviceConfig)
+
+        positioner.bubbleBarBounds = getBubbleBarBounds(onLeft, deviceConfig)
+
+        val expandedViewPadding =
+            context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding)
+
+        val left: Int
+        val right: Int
+        if (onLeft) {
+            // Pin to the left, calculate right
+            left = deviceConfig.insets.left + expandedViewPadding
+            right = left + positioner.getExpandedViewWidthForBubbleBar(isOverflow)
+        } else {
+            // Pin to the right, calculate left
+            right =
+                deviceConfig.windowBounds.right - deviceConfig.insets.right - expandedViewPadding
+            left = right - positioner.getExpandedViewWidthForBubbleBar(isOverflow)
+        }
+        // Above the bubble bar
+        val bottom = positioner.bubbleBarBounds.top - expandedViewPadding
+        // Calculate right and top based on size
+        val top = bottom - positioner.getExpandedViewHeightForBubbleBar(isOverflow)
+        val expectedBounds = Rect(left, top, right, bottom)
+
+        val bounds = Rect()
+        positioner.getBubbleBarExpandedViewBounds(onLeft, isOverflow, bounds)
+
+        assertThat(bounds).isEqualTo(expectedBounds)
+    }
+
     private val defaultYPosition: Float
         /**
          * Calculates the Y position bubbles should be placed based on the config. Based on the
@@ -544,4 +610,21 @@
                 positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
             return allowableStackRegion.top + allowableStackRegion.height() * offsetPercent
         }
+
+    private fun getBubbleBarBounds(onLeft: Boolean, deviceConfig: DeviceConfig): Rect {
+        val width = 200
+        val height = 100
+        val bottom = deviceConfig.windowBounds.bottom - deviceConfig.insets.bottom
+        val top = bottom - height
+        val left: Int
+        val right: Int
+        if (onLeft) {
+            left = deviceConfig.insets.left
+            right = left + width
+        } else {
+            right = deviceConfig.windowBounds.right - deviceConfig.insets.right
+            left = right - width
+        }
+        return Rect(left, top, right, bottom)
+    }
 }
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 00fb298..43ce166 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -535,5 +535,7 @@
     <!-- The vertical margin that needs to be preserved between the scaled window bounds and the
     original window bounds (once the surface is scaled enough to do so) -->
     <dimen name="cross_task_back_vertical_margin">8dp</dimen>
+    <!-- The offset from the left edge of the entering page for the cross-activity animation -->
+    <dimen name="cross_activity_back_entering_start_offset">96dp</dimen>
 
 </resources>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java
deleted file mode 100644
index d6f7c36..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java
+++ /dev/null
@@ -1,455 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.back;
-
-import static android.view.RemoteAnimationTarget.MODE_CLOSING;
-import static android.view.RemoteAnimationTarget.MODE_OPENING;
-import static android.window.BackEvent.EDGE_RIGHT;
-
-import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY;
-import static com.android.wm.shell.back.BackAnimationConstants.UPDATE_SYSUI_FLAGS_THRESHOLD;
-import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
-import android.annotation.NonNull;
-import android.content.Context;
-import android.graphics.Matrix;
-import android.graphics.PointF;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.os.RemoteException;
-import android.util.FloatProperty;
-import android.util.TypedValue;
-import android.view.IRemoteAnimationFinishedCallback;
-import android.view.IRemoteAnimationRunner;
-import android.view.RemoteAnimationTarget;
-import android.view.SurfaceControl;
-import android.view.animation.Interpolator;
-import android.window.BackEvent;
-import android.window.BackMotionEvent;
-import android.window.BackProgressAnimator;
-import android.window.IOnBackInvokedCallback;
-
-import com.android.internal.dynamicanimation.animation.SpringAnimation;
-import com.android.internal.dynamicanimation.animation.SpringForce;
-import com.android.internal.policy.ScreenDecorationsUtils;
-import com.android.internal.protolog.common.ProtoLog;
-import com.android.wm.shell.animation.Interpolators;
-import com.android.wm.shell.common.annotations.ShellMainThread;
-
-import javax.inject.Inject;
-
-/** Class that defines cross-activity animation. */
-@ShellMainThread
-public class CrossActivityBackAnimation extends ShellBackAnimation {
-    /**
-     * Minimum scale of the entering/closing window.
-     */
-    private static final float MIN_WINDOW_SCALE = 0.9f;
-
-    /** Duration of post animation after gesture committed. */
-    private static final int POST_ANIMATION_DURATION = 350;
-    private static final Interpolator INTERPOLATOR = Interpolators.STANDARD_DECELERATE;
-    private static final FloatProperty<CrossActivityBackAnimation> ENTER_PROGRESS_PROP =
-            new FloatProperty<>("enter-alpha") {
-                @Override
-                public void setValue(CrossActivityBackAnimation anim, float value) {
-                    anim.setEnteringProgress(value);
-                }
-
-                @Override
-                public Float get(CrossActivityBackAnimation object) {
-                    return object.getEnteringProgress();
-                }
-            };
-    private static final FloatProperty<CrossActivityBackAnimation> LEAVE_PROGRESS_PROP =
-            new FloatProperty<>("leave-alpha") {
-                @Override
-                public void setValue(CrossActivityBackAnimation anim, float value) {
-                    anim.setLeavingProgress(value);
-                }
-
-                @Override
-                public Float get(CrossActivityBackAnimation object) {
-                    return object.getLeavingProgress();
-                }
-            };
-    private static final float MIN_WINDOW_ALPHA = 0.01f;
-    private static final float WINDOW_X_SHIFT_DP = 48;
-    private static final int SCALE_FACTOR = 100;
-    // TODO(b/264710590): Use the progress commit threshold from ViewConfiguration once it exists.
-    private static final float TARGET_COMMIT_PROGRESS = 0.5f;
-    private static final float ENTER_ALPHA_THRESHOLD = 0.22f;
-
-    private final Rect mStartTaskRect = new Rect();
-    private final float mCornerRadius;
-
-    // The closing window properties.
-    private final RectF mClosingRect = new RectF();
-
-    // The entering window properties.
-    private final Rect mEnteringStartRect = new Rect();
-    private final RectF mEnteringRect = new RectF();
-    private final SpringAnimation mEnteringProgressSpring;
-    private final SpringAnimation mLeavingProgressSpring;
-    // Max window x-shift in pixels.
-    private final float mWindowXShift;
-    private final BackAnimationRunner mBackAnimationRunner;
-
-    private float mEnteringProgress = 0f;
-    private float mLeavingProgress = 0f;
-
-    private final PointF mInitialTouchPos = new PointF();
-
-    private final Matrix mTransformMatrix = new Matrix();
-
-    private final float[] mTmpFloat9 = new float[9];
-
-    private RemoteAnimationTarget mEnteringTarget;
-    private RemoteAnimationTarget mClosingTarget;
-    private SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
-
-    private boolean mBackInProgress = false;
-    private boolean mIsRightEdge;
-    private boolean mTriggerBack = false;
-
-    private PointF mTouchPos = new PointF();
-    private IRemoteAnimationFinishedCallback mFinishCallback;
-
-    private final BackProgressAnimator mProgressAnimator = new BackProgressAnimator();
-
-    private final BackAnimationBackground mBackground;
-
-    @Inject
-    public CrossActivityBackAnimation(Context context, BackAnimationBackground background) {
-        mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context);
-        mBackAnimationRunner = new BackAnimationRunner(
-                new Callback(), new Runner(), context, CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY);
-        mBackground = background;
-        mEnteringProgressSpring = new SpringAnimation(this, ENTER_PROGRESS_PROP);
-        mEnteringProgressSpring.setSpring(new SpringForce()
-                .setStiffness(SpringForce.STIFFNESS_MEDIUM)
-                .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY));
-        mLeavingProgressSpring = new SpringAnimation(this, LEAVE_PROGRESS_PROP);
-        mLeavingProgressSpring.setSpring(new SpringForce()
-                .setStiffness(SpringForce.STIFFNESS_MEDIUM)
-                .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY));
-        mWindowXShift = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, WINDOW_X_SHIFT_DP,
-                context.getResources().getDisplayMetrics());
-    }
-
-    /**
-     * Returns 1 if x >= edge1, 0 if x <= edge0, and a smoothed value between the two.
-     * From https://en.wikipedia.org/wiki/Smoothstep
-     */
-    private static float smoothstep(float edge0, float edge1, float x) {
-        if (x < edge0) return 0;
-        if (x >= edge1) return 1;
-
-        x = (x - edge0) / (edge1 - edge0);
-        return x * x * (3 - 2 * x);
-    }
-
-    /**
-     * Linearly map x from range (a1, a2) to range (b1, b2).
-     */
-    private static float mapLinear(float x, float a1, float a2, float b1, float b2) {
-        return b1 + (x - a1) * (b2 - b1) / (a2 - a1);
-    }
-
-    /**
-     * Linearly map a normalized value from (0, 1) to (min, max).
-     */
-    private static float mapRange(float value, float min, float max) {
-        return min + (value * (max - min));
-    }
-
-    private void startBackAnimation() {
-        if (mEnteringTarget == null || mClosingTarget == null) {
-            ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Entering target or closing target is null.");
-            return;
-        }
-        mTransaction.setAnimationTransaction();
-
-        // Offset start rectangle to align task bounds.
-        mStartTaskRect.set(mClosingTarget.windowConfiguration.getBounds());
-        mStartTaskRect.offsetTo(0, 0);
-
-        // Draw background with task background color.
-        mBackground.ensureBackground(mClosingTarget.windowConfiguration.getBounds(),
-                mEnteringTarget.taskInfo.taskDescription.getBackgroundColor(), mTransaction);
-        setEnteringProgress(0);
-        setLeavingProgress(0);
-    }
-
-    private void applyTransform(SurfaceControl leash, RectF targetRect, float targetAlpha) {
-        if (leash == null || !leash.isValid()) {
-            return;
-        }
-
-        final float scale = targetRect.width() / mStartTaskRect.width();
-        mTransformMatrix.reset();
-        mTransformMatrix.setScale(scale, scale);
-        mTransformMatrix.postTranslate(targetRect.left, targetRect.top);
-        mTransaction.setAlpha(leash, targetAlpha)
-                .setMatrix(leash, mTransformMatrix, mTmpFloat9)
-                .setWindowCrop(leash, mStartTaskRect)
-                .setCornerRadius(leash, mCornerRadius);
-    }
-
-    private void finishAnimation() {
-        if (mEnteringTarget != null) {
-            if (mEnteringTarget.leash != null && mEnteringTarget.leash.isValid()) {
-                mTransaction.setCornerRadius(mEnteringTarget.leash, 0);
-                mEnteringTarget.leash.release();
-            }
-            mEnteringTarget = null;
-        }
-        if (mClosingTarget != null) {
-            if (mClosingTarget.leash != null) {
-                mClosingTarget.leash.release();
-            }
-            mClosingTarget = null;
-        }
-        if (mBackground != null) {
-            mBackground.removeBackground(mTransaction);
-        }
-
-        mTransaction.apply();
-        mBackInProgress = false;
-        mTransformMatrix.reset();
-        mInitialTouchPos.set(0, 0);
-
-        if (mFinishCallback != null) {
-            try {
-                mFinishCallback.onAnimationFinished();
-            } catch (RemoteException e) {
-                e.printStackTrace();
-            }
-            mFinishCallback = null;
-        }
-        mEnteringProgressSpring.animateToFinalPosition(0);
-        mEnteringProgressSpring.skipToEnd();
-        mLeavingProgressSpring.animateToFinalPosition(0);
-        mLeavingProgressSpring.skipToEnd();
-    }
-
-    private void onGestureProgress(@NonNull BackEvent backEvent) {
-        if (!mBackInProgress) {
-            mIsRightEdge = backEvent.getSwipeEdge() == EDGE_RIGHT;
-            mInitialTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY());
-            mBackInProgress = true;
-        }
-        mTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY());
-
-        float progress = backEvent.getProgress();
-        float springProgress = (mTriggerBack
-                ? mapLinear(progress, 0f, 1, TARGET_COMMIT_PROGRESS, 1)
-                : mapLinear(progress, 0, 1f, 0, TARGET_COMMIT_PROGRESS)) * SCALE_FACTOR;
-        mLeavingProgressSpring.animateToFinalPosition(springProgress);
-        mEnteringProgressSpring.animateToFinalPosition(springProgress);
-        mBackground.onBackProgressed(progress);
-    }
-
-    private void onGestureCommitted() {
-        if (mEnteringTarget == null || mClosingTarget == null || mClosingTarget.leash == null
-                || mEnteringTarget.leash == null || !mEnteringTarget.leash.isValid()
-                || !mClosingTarget.leash.isValid()) {
-            finishAnimation();
-            return;
-        }
-        // End the fade animations
-        mLeavingProgressSpring.cancel();
-        mEnteringProgressSpring.cancel();
-
-        // We enter phase 2 of the animation, the starting coordinates for phase 2 are the current
-        // coordinate of the gesture driven phase.
-        mEnteringRect.round(mEnteringStartRect);
-        mTransaction.hide(mClosingTarget.leash);
-
-        ValueAnimator valueAnimator =
-                ValueAnimator.ofFloat(1f, 0f).setDuration(POST_ANIMATION_DURATION);
-        valueAnimator.setInterpolator(INTERPOLATOR);
-        valueAnimator.addUpdateListener(animation -> {
-            float progress = animation.getAnimatedFraction();
-            updatePostCommitEnteringAnimation(progress);
-            if (progress > 1 - UPDATE_SYSUI_FLAGS_THRESHOLD) {
-                mBackground.resetStatusBarCustomization();
-            }
-            mTransaction.apply();
-        });
-
-        valueAnimator.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                mBackground.resetStatusBarCustomization();
-                finishAnimation();
-            }
-        });
-        valueAnimator.start();
-    }
-
-    private void updatePostCommitEnteringAnimation(float progress) {
-        float left = mapRange(progress, mEnteringStartRect.left, mStartTaskRect.left);
-        float top = mapRange(progress, mEnteringStartRect.top, mStartTaskRect.top);
-        float width = mapRange(progress, mEnteringStartRect.width(), mStartTaskRect.width());
-        float height = mapRange(progress, mEnteringStartRect.height(), mStartTaskRect.height());
-        float alpha = mapRange(progress, getPreCommitEnteringAlpha(), 1.0f);
-        mEnteringRect.set(left, top, left + width, top + height);
-        applyTransform(mEnteringTarget.leash, mEnteringRect, alpha);
-    }
-
-    private float getPreCommitEnteringAlpha() {
-        return Math.max(smoothstep(ENTER_ALPHA_THRESHOLD, 0.7f, mEnteringProgress),
-                MIN_WINDOW_ALPHA);
-    }
-
-    private float getEnteringProgress() {
-        return mEnteringProgress * SCALE_FACTOR;
-    }
-
-    private void setEnteringProgress(float value) {
-        mEnteringProgress = value / SCALE_FACTOR;
-        if (mEnteringTarget != null && mEnteringTarget.leash != null) {
-            transformWithProgress(
-                    mEnteringProgress,
-                    getPreCommitEnteringAlpha(),
-                    mEnteringTarget.leash,
-                    mEnteringRect,
-                    -mWindowXShift,
-                    0
-            );
-        }
-    }
-
-    private float getPreCommitLeavingAlpha() {
-        return Math.max(1 - smoothstep(0, ENTER_ALPHA_THRESHOLD, mLeavingProgress),
-                MIN_WINDOW_ALPHA);
-    }
-
-    private float getLeavingProgress() {
-        return mLeavingProgress * SCALE_FACTOR;
-    }
-
-    private void setLeavingProgress(float value) {
-        mLeavingProgress = value / SCALE_FACTOR;
-        if (mClosingTarget != null && mClosingTarget.leash != null) {
-            transformWithProgress(
-                    mLeavingProgress,
-                    getPreCommitLeavingAlpha(),
-                    mClosingTarget.leash,
-                    mClosingRect,
-                    0,
-                    mIsRightEdge ? 0 : mWindowXShift
-            );
-        }
-    }
-
-    private void transformWithProgress(float progress, float alpha, SurfaceControl surface,
-            RectF targetRect, float deltaXMin, float deltaXMax) {
-
-        final int width = mStartTaskRect.width();
-        final int height = mStartTaskRect.height();
-
-        final float interpolatedProgress = INTERPOLATOR.getInterpolation(progress);
-        final float closingScale = MIN_WINDOW_SCALE
-                + (1 - interpolatedProgress) * (1 - MIN_WINDOW_SCALE);
-        final float closingWidth = closingScale * width;
-        final float closingHeight = (float) height / width * closingWidth;
-
-        // Move the window along the X axis.
-        float closingLeft = mStartTaskRect.left + (width - closingWidth) / 2;
-        closingLeft += mapRange(interpolatedProgress, deltaXMin, deltaXMax);
-
-        // Move the window along the Y axis.
-        final float closingTop = (height - closingHeight) * 0.5f;
-        targetRect.set(
-                closingLeft, closingTop, closingLeft + closingWidth, closingTop + closingHeight);
-
-        applyTransform(surface, targetRect, Math.max(alpha, MIN_WINDOW_ALPHA));
-        mTransaction.apply();
-    }
-
-    @Override
-    public BackAnimationRunner getRunner() {
-        return mBackAnimationRunner;
-    }
-
-    private final class Callback extends IOnBackInvokedCallback.Default {
-        @Override
-        public void onBackStarted(BackMotionEvent backEvent) {
-            mTriggerBack = backEvent.getTriggerBack();
-            mProgressAnimator.onBackStarted(backEvent,
-                    CrossActivityBackAnimation.this::onGestureProgress);
-        }
-
-        @Override
-        public void onBackProgressed(@NonNull BackMotionEvent backEvent) {
-            mTriggerBack = backEvent.getTriggerBack();
-            mProgressAnimator.onBackProgressed(backEvent);
-        }
-
-        @Override
-        public void onBackCancelled() {
-            mProgressAnimator.onBackCancelled(() -> {
-                // mProgressAnimator can reach finish stage earlier than mLeavingProgressSpring,
-                // and if we release all animation leash first, the leavingProgressSpring won't
-                // able to update the animation anymore, which cause flicker.
-                // Here should force update the closing animation target to the final stage before
-                // release it.
-                setLeavingProgress(0);
-                finishAnimation();
-            });
-        }
-
-        @Override
-        public void onBackInvoked() {
-            mProgressAnimator.reset();
-            onGestureCommitted();
-        }
-    }
-
-    private final class Runner extends IRemoteAnimationRunner.Default {
-        @Override
-        public void onAnimationStart(
-                int transit,
-                RemoteAnimationTarget[] apps,
-                RemoteAnimationTarget[] wallpapers,
-                RemoteAnimationTarget[] nonApps,
-                IRemoteAnimationFinishedCallback finishedCallback) {
-            ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Start back to activity animation.");
-            for (RemoteAnimationTarget a : apps) {
-                if (a.mode == MODE_CLOSING) {
-                    mClosingTarget = a;
-                }
-                if (a.mode == MODE_OPENING) {
-                    mEnteringTarget = a;
-                }
-            }
-
-            startBackAnimation();
-            mFinishCallback = finishedCallback;
-        }
-
-        @Override
-        public void onAnimationCancelled() {
-            finishAnimation();
-        }
-    }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt
new file mode 100644
index 0000000..edf29dd
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt
@@ -0,0 +1,367 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.back
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.content.Context
+import android.content.res.Configuration
+import android.graphics.Matrix
+import android.graphics.PointF
+import android.graphics.Rect
+import android.graphics.RectF
+import android.os.RemoteException
+import android.view.Display
+import android.view.IRemoteAnimationFinishedCallback
+import android.view.IRemoteAnimationRunner
+import android.view.RemoteAnimationTarget
+import android.view.SurfaceControl
+import android.view.animation.DecelerateInterpolator
+import android.view.animation.Interpolator
+import android.window.BackEvent
+import android.window.BackMotionEvent
+import android.window.BackProgressAnimator
+import android.window.IOnBackInvokedCallback
+import com.android.internal.jank.Cuj
+import com.android.internal.policy.ScreenDecorationsUtils
+import com.android.internal.protolog.common.ProtoLog
+import com.android.wm.shell.R
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
+import com.android.wm.shell.animation.Interpolators
+import com.android.wm.shell.common.annotations.ShellMainThread
+import com.android.wm.shell.protolog.ShellProtoLogGroup
+import javax.inject.Inject
+import kotlin.math.abs
+import kotlin.math.max
+import kotlin.math.min
+
+/** Class that defines cross-activity animation.  */
+@ShellMainThread
+class CrossActivityBackAnimation @Inject constructor(
+    private val context: Context,
+    private val background: BackAnimationBackground,
+    private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
+) : ShellBackAnimation() {
+
+    private val startClosingRect = RectF()
+    private val targetClosingRect = RectF()
+    private val currentClosingRect = RectF()
+
+    private val startEnteringRect = RectF()
+    private val targetEnteringRect = RectF()
+    private val currentEnteringRect = RectF()
+
+    private val taskBoundsRect = Rect()
+
+    private val cornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context)
+
+    private val backAnimationRunner = BackAnimationRunner(
+        Callback(), Runner(), context, Cuj.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY
+    )
+    private val initialTouchPos = PointF()
+    private val transformMatrix = Matrix()
+    private val tmpFloat9 = FloatArray(9)
+    private var enteringTarget: RemoteAnimationTarget? = null
+    private var closingTarget: RemoteAnimationTarget? = null
+    private val transaction = SurfaceControl.Transaction()
+    private var triggerBack = false
+    private var finishCallback: IRemoteAnimationFinishedCallback? = null
+    private val progressAnimator = BackProgressAnimator()
+    private val displayBoundsMargin =
+        context.resources.getDimension(R.dimen.cross_task_back_vertical_margin)
+    private val enteringStartOffset =
+        context.resources.getDimension(R.dimen.cross_activity_back_entering_start_offset)
+
+    private val gestureInterpolator = Interpolators.STANDARD_DECELERATE
+    private val postCommitInterpolator = Interpolators.FAST_OUT_SLOW_IN
+    private val verticalMoveInterpolator: Interpolator = DecelerateInterpolator()
+
+    private var scrimLayer: SurfaceControl? = null
+    private var maxScrimAlpha: Float = 0f
+
+    override fun getRunner() = backAnimationRunner
+
+    private fun startBackAnimation(backMotionEvent: BackMotionEvent) {
+        if (enteringTarget == null || closingTarget == null) {
+            ProtoLog.d(
+                ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW,
+                "Entering target or closing target is null."
+            )
+            return
+        }
+        triggerBack = backMotionEvent.triggerBack
+        initialTouchPos.set(backMotionEvent.touchX, backMotionEvent.touchY)
+
+        transaction.setAnimationTransaction()
+
+        // Offset start rectangle to align task bounds.
+        taskBoundsRect.set(closingTarget!!.windowConfiguration.bounds)
+        taskBoundsRect.offsetTo(0, 0)
+
+        startClosingRect.set(taskBoundsRect)
+
+        // scale closing target into the middle for rhs and to the right for lhs
+        targetClosingRect.set(startClosingRect)
+        targetClosingRect.scaleCentered(MAX_SCALE)
+        if (backMotionEvent.swipeEdge != BackEvent.EDGE_RIGHT) {
+            targetClosingRect.offset(
+                startClosingRect.right - targetClosingRect.right - displayBoundsMargin, 0f
+            )
+        }
+
+        // the entering target starts 96dp to the left of the screen edge...
+        startEnteringRect.set(startClosingRect)
+        startEnteringRect.offset(-enteringStartOffset, 0f)
+
+        // ...and gets scaled in sync with the closing target
+        targetEnteringRect.set(startEnteringRect)
+        targetEnteringRect.scaleCentered(MAX_SCALE)
+
+        // Draw background with task background color.
+        background.ensureBackground(
+            closingTarget!!.windowConfiguration.bounds,
+            enteringTarget!!.taskInfo.taskDescription!!.backgroundColor, transaction
+        )
+        ensureScrimLayer()
+        transaction.apply()
+    }
+
+    private fun onGestureProgress(backEvent: BackEvent) {
+        val progress = gestureInterpolator.getInterpolation(backEvent.progress)
+        background.onBackProgressed(progress)
+        currentClosingRect.setInterpolatedRectF(startClosingRect, targetClosingRect, progress)
+        val yOffset = getYOffset(currentClosingRect, backEvent.touchY)
+        currentClosingRect.offset(0f, yOffset)
+        applyTransform(closingTarget?.leash, currentClosingRect, 1f)
+        currentEnteringRect.setInterpolatedRectF(startEnteringRect, targetEnteringRect, progress)
+        currentEnteringRect.offset(0f, yOffset)
+        applyTransform(enteringTarget?.leash, currentEnteringRect, 1f)
+        transaction.apply()
+    }
+
+    private fun getYOffset(centeredRect: RectF, touchY: Float): Float {
+        val screenHeight = taskBoundsRect.height()
+        // Base the window movement in the Y axis on the touch movement in the Y axis.
+        val rawYDelta = touchY - initialTouchPos.y
+        val yDirection = (if (rawYDelta < 0) -1 else 1)
+        // limit yDelta interpretation to 1/2 of screen height in either direction
+        val deltaYRatio = min(screenHeight / 2f, abs(rawYDelta)) / (screenHeight / 2f)
+        val interpolatedYRatio: Float = verticalMoveInterpolator.getInterpolation(deltaYRatio)
+        // limit y-shift so surface never passes 8dp screen margin
+        val deltaY = yDirection * interpolatedYRatio * max(
+            0f, (screenHeight - centeredRect.height()) / 2f - displayBoundsMargin
+        )
+        return deltaY
+    }
+
+    private fun onGestureCommitted() {
+        if (closingTarget?.leash == null || enteringTarget?.leash == null ||
+                !enteringTarget!!.leash.isValid || !closingTarget!!.leash.isValid
+        ) {
+            finishAnimation()
+            return
+        }
+
+        // We enter phase 2 of the animation, the starting coordinates for phase 2 are the current
+        // coordinate of the gesture driven phase. Let's update the start and target rects and kick
+        // off the animator
+        startClosingRect.set(currentClosingRect)
+        startEnteringRect.set(currentEnteringRect)
+        targetEnteringRect.set(taskBoundsRect)
+        targetClosingRect.set(taskBoundsRect)
+        targetClosingRect.offset(currentClosingRect.left + enteringStartOffset, 0f)
+
+        val valueAnimator = ValueAnimator.ofFloat(1f, 0f).setDuration(POST_ANIMATION_DURATION)
+        valueAnimator.addUpdateListener { animation: ValueAnimator ->
+            val progress = animation.animatedFraction
+            onPostCommitProgress(progress)
+            if (progress > 1 - BackAnimationConstants.UPDATE_SYSUI_FLAGS_THRESHOLD) {
+                background.resetStatusBarCustomization()
+            }
+        }
+        valueAnimator.addListener(object : AnimatorListenerAdapter() {
+            override fun onAnimationEnd(animation: Animator) {
+                background.resetStatusBarCustomization()
+                finishAnimation()
+            }
+        })
+        valueAnimator.start()
+    }
+
+    private fun onPostCommitProgress(linearProgress: Float) {
+        val closingAlpha = max(1f - linearProgress * 2, 0f)
+        val progress = postCommitInterpolator.getInterpolation(linearProgress)
+        scrimLayer?.let { transaction.setAlpha(it, maxScrimAlpha * (1f - linearProgress)) }
+        currentClosingRect.setInterpolatedRectF(startClosingRect, targetClosingRect, progress)
+        applyTransform(closingTarget?.leash, currentClosingRect, closingAlpha)
+        currentEnteringRect.setInterpolatedRectF(startEnteringRect, targetEnteringRect, progress)
+        applyTransform(enteringTarget?.leash, currentEnteringRect, 1f)
+        transaction.apply()
+    }
+
+    private fun finishAnimation() {
+        enteringTarget?.let {
+            if (it.leash != null && it.leash.isValid) {
+                transaction.setCornerRadius(it.leash, 0f)
+                it.leash.release()
+            }
+            enteringTarget = null
+        }
+
+        closingTarget?.leash?.release()
+        closingTarget = null
+
+        background.removeBackground(transaction)
+        transaction.apply()
+        transformMatrix.reset()
+        initialTouchPos.set(0f, 0f)
+        try {
+            finishCallback?.onAnimationFinished()
+        } catch (e: RemoteException) {
+            e.printStackTrace()
+        }
+        finishCallback = null
+        removeScrimLayer()
+    }
+
+    private fun applyTransform(leash: SurfaceControl?, rect: RectF, alpha: Float) {
+        if (leash == null || !leash.isValid) return
+        val scale = rect.width() / taskBoundsRect.width()
+        transformMatrix.reset()
+        transformMatrix.setScale(scale, scale)
+        transformMatrix.postTranslate(rect.left, rect.top)
+        transaction.setAlpha(leash, alpha)
+            .setMatrix(leash, transformMatrix, tmpFloat9)
+            .setCrop(leash, taskBoundsRect)
+            .setCornerRadius(leash, cornerRadius)
+    }
+
+    private fun ensureScrimLayer() {
+        if (scrimLayer != null) return
+        val isDarkTheme: Boolean = isDarkMode(context)
+        val scrimBuilder = SurfaceControl.Builder()
+            .setName("Cross-Activity back animation scrim")
+            .setCallsite("CrossActivityBackAnimation")
+            .setColorLayer()
+            .setOpaque(false)
+            .setHidden(false)
+
+        rootTaskDisplayAreaOrganizer.attachToDisplayArea(Display.DEFAULT_DISPLAY, scrimBuilder)
+        scrimLayer = scrimBuilder.build()
+        val colorComponents = floatArrayOf(0f, 0f, 0f)
+        maxScrimAlpha = if (isDarkTheme) MAX_SCRIM_ALPHA_DARK else MAX_SCRIM_ALPHA_LIGHT
+        transaction
+            .setColor(scrimLayer, colorComponents)
+            .setAlpha(scrimLayer!!, maxScrimAlpha)
+            .setRelativeLayer(scrimLayer!!, closingTarget!!.leash, -1)
+            .show(scrimLayer)
+    }
+
+    private fun removeScrimLayer() {
+        scrimLayer?.let {
+            if (it.isValid) {
+                transaction.remove(it).apply()
+            }
+        }
+        scrimLayer = null
+    }
+
+
+    private inner class Callback : IOnBackInvokedCallback.Default() {
+        override fun onBackStarted(backMotionEvent: BackMotionEvent) {
+            startBackAnimation(backMotionEvent)
+            progressAnimator.onBackStarted(backMotionEvent) { backEvent: BackEvent ->
+                onGestureProgress(backEvent)
+            }
+        }
+
+        override fun onBackProgressed(backEvent: BackMotionEvent) {
+            triggerBack = backEvent.triggerBack
+            progressAnimator.onBackProgressed(backEvent)
+        }
+
+        override fun onBackCancelled() {
+            progressAnimator.onBackCancelled {
+                finishAnimation()
+            }
+        }
+
+        override fun onBackInvoked() {
+            progressAnimator.reset()
+            onGestureCommitted()
+        }
+    }
+
+    private inner class Runner : IRemoteAnimationRunner.Default() {
+        override fun onAnimationStart(
+            transit: Int,
+            apps: Array<RemoteAnimationTarget>,
+            wallpapers: Array<RemoteAnimationTarget>?,
+            nonApps: Array<RemoteAnimationTarget>?,
+            finishedCallback: IRemoteAnimationFinishedCallback
+        ) {
+            ProtoLog.d(
+                ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW, "Start back to activity animation."
+            )
+            for (a in apps) {
+                when (a.mode) {
+                    RemoteAnimationTarget.MODE_CLOSING -> closingTarget = a
+                    RemoteAnimationTarget.MODE_OPENING -> enteringTarget = a
+                }
+            }
+            finishCallback = finishedCallback
+        }
+
+        override fun onAnimationCancelled() {
+            finishAnimation()
+        }
+    }
+
+    companion object {
+        /** Max scale of the entering/closing window.*/
+        private const val MAX_SCALE = 0.9f
+
+        /** Duration of post animation after gesture committed.  */
+        private const val POST_ANIMATION_DURATION = 300L
+
+        private const val MAX_SCRIM_ALPHA_DARK = 0.8f
+        private const val MAX_SCRIM_ALPHA_LIGHT = 0.2f
+    }
+}
+
+private fun isDarkMode(context: Context): Boolean {
+    return context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
+            Configuration.UI_MODE_NIGHT_YES
+}
+
+private fun RectF.setInterpolatedRectF(start: RectF, target: RectF, progress: Float) {
+    require(!(progress < 0 || progress > 1)) { "Progress value must be between 0 and 1" }
+    left = start.left + (target.left - start.left) * progress
+    top = start.top + (target.top - start.top) * progress
+    right = start.right + (target.right - start.right) * progress
+    bottom = start.bottom + (target.bottom - start.bottom) * progress
+}
+
+private fun RectF.scaleCentered(
+    scale: Float,
+    pivotX: Float = left + width() / 2,
+    pivotY: Float = top + height() / 2
+) {
+    offset(-pivotX, -pivotY) // move pivot to origin
+    scale(scale)
+    offset(pivotX, pivotY) // Move back to the original position
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
index f4a401c..4d5e516 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
@@ -870,7 +870,7 @@
         if (onLeft) {
             left = getInsets().left + padding;
         } else {
-            left = getAvailableRect().width() - width - padding;
+            left = getAvailableRect().right - width - padding;
         }
         int top = getExpandedViewBottomForBubbleBar() - height;
         out.offsetTo(left, top);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
index 9ded6ea..703eb19 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
@@ -61,6 +61,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.TestShellExecutor;
 import com.android.wm.shell.sysui.ShellCommandHandler;
@@ -113,6 +114,8 @@
     private InputManager mInputManager;
     @Mock
     private ShellCommandHandler mShellCommandHandler;
+    @Mock
+    private RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
 
     private BackAnimationController mController;
     private TestableContentResolver mContentResolver;
@@ -133,7 +136,8 @@
         mShellInit = spy(new ShellInit(mShellExecutor));
         mShellBackAnimationRegistry =
                 new ShellBackAnimationRegistry(
-                        new CrossActivityBackAnimation(mContext, mAnimationBackground),
+                        new CrossActivityBackAnimation(
+                                mContext, mAnimationBackground, mRootTaskDisplayAreaOrganizer),
                         new CrossTaskBackAnimation(mContext, mAnimationBackground),
                         /* dialogCloseAnimation= */ null,
                         new CustomizeActivityAnimation(mContext, mAnimationBackground),
@@ -528,8 +532,8 @@
 
     @Test
     public void testBackToActivity() throws RemoteException {
-        final CrossActivityBackAnimation animation = new CrossActivityBackAnimation(mContext,
-                mAnimationBackground);
+        final CrossActivityBackAnimation animation = new CrossActivityBackAnimation(
+                mContext, mAnimationBackground, mRootTaskDisplayAreaOrganizer);
         verifySystemBackBehavior(BackNavigationInfo.TYPE_CROSS_ACTIVITY, animation.getRunner());
     }
 
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 1fe3c2e..6f7024a 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -5453,8 +5453,7 @@
             String regId = service.registerAudioPolicy(policy.getConfig(), policy.cb(),
                     policy.hasFocusListener(), policy.isFocusPolicy(), policy.isTestFocusPolicy(),
                     policy.isVolumeController(),
-                    projection == null ? null : projection.getProjection(),
-                    policy.getAttributionSource());
+                    projection == null ? null : projection.getProjection());
             if (regId == null) {
                 return ERROR;
             } else {
diff --git a/media/java/android/media/AudioRecord.java b/media/java/android/media/AudioRecord.java
index 80e5719..447d3bb 100644
--- a/media/java/android/media/AudioRecord.java
+++ b/media/java/android/media/AudioRecord.java
@@ -789,7 +789,7 @@
         private @NonNull AudioRecord buildAudioPlaybackCaptureRecord() {
             AudioMix audioMix = mAudioPlaybackCaptureConfiguration.createAudioMix(mFormat);
             MediaProjection projection = mAudioPlaybackCaptureConfiguration.getMediaProjection();
-            AudioPolicy audioPolicy = new AudioPolicy.Builder(/*context=*/ mContext)
+            AudioPolicy audioPolicy = new AudioPolicy.Builder(/*context=*/ null)
                     .setMediaProjection(projection)
                     .addMix(audioMix).build();
 
@@ -853,7 +853,7 @@
                     .setFormat(mFormat)
                     .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK)
                     .build();
-            AudioPolicy audioPolicy = new AudioPolicy.Builder(mContext).addMix(audioMix).build();
+            AudioPolicy audioPolicy = new AudioPolicy.Builder(null).addMix(audioMix).build();
             if (AudioManager.registerAudioPolicyStatic(audioPolicy) != 0) {
                 throw new UnsupportedOperationException("Error: could not register audio policy");
             }
diff --git a/media/java/android/media/AudioTrack.java b/media/java/android/media/AudioTrack.java
index 73deb17..194da21 100644
--- a/media/java/android/media/AudioTrack.java
+++ b/media/java/android/media/AudioTrack.java
@@ -1353,8 +1353,7 @@
                     .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK)
                     .build();
             AudioPolicy audioPolicy =
-                    new AudioPolicy.Builder(/*context=*/ mContext).addMix(audioMix).build();
-
+                    new AudioPolicy.Builder(/*context=*/ null).addMix(audioMix).build();
             if (AudioManager.registerAudioPolicyStatic(audioPolicy) != 0) {
                 throw new UnsupportedOperationException("Error: could not register audio policy");
             }
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index e612645..98bd3ca 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -18,7 +18,6 @@
 
 import android.bluetooth.BluetoothDevice;
 import android.content.ComponentName;
-import android.content.AttributionSource;
 import android.media.AudioAttributes;
 import android.media.AudioDeviceAttributes;
 import android.media.AudioFormat;
@@ -362,8 +361,7 @@
     String registerAudioPolicy(in AudioPolicyConfig policyConfig,
             in IAudioPolicyCallback pcb, boolean hasFocusListener, boolean isFocusPolicy,
             boolean isTestFocusPolicy,
-            boolean isVolumeController, in IMediaProjection projection,
-            in AttributionSource attributionSource);
+            boolean isVolumeController, in IMediaProjection projection);
 
     oneway void unregisterAudioPolicyAsync(in IAudioPolicyCallback pcb);
 
diff --git a/media/java/android/media/audiopolicy/AudioMix.java b/media/java/android/media/audiopolicy/AudioMix.java
index e4eaaa3..a53a8ce 100644
--- a/media/java/android/media/audiopolicy/AudioMix.java
+++ b/media/java/android/media/audiopolicy/AudioMix.java
@@ -24,7 +24,6 @@
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
 import android.compat.annotation.UnsupportedAppUsage;
-import android.content.Context;
 import android.media.AudioDeviceInfo;
 import android.media.AudioFormat;
 import android.media.AudioSystem;
@@ -68,19 +67,12 @@
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     final int mDeviceSystemType; // an AudioSystem.DEVICE_* value, not AudioDeviceInfo.TYPE_*
 
-    // The (virtual) device ID that this AudioMix was registered for. This value is overwritten
-    // when registering this AudioMix with an AudioPolicy or attaching this AudioMix to an
-    // AudioPolicy to match the AudioPolicy attribution. Does not imply that it only modifies
-    // audio routing for this device ID.
-    private int mVirtualDeviceId;
-
     /**
      * All parameters are guaranteed valid through the Builder.
      */
     private AudioMix(@NonNull AudioMixingRule rule, @NonNull AudioFormat format,
             int routeFlags, int callbackFlags,
-            int deviceType, @Nullable String deviceAddress, IBinder token,
-            int virtualDeviceId) {
+            int deviceType, @Nullable String deviceAddress, IBinder token) {
         mRule = Objects.requireNonNull(rule);
         mFormat = Objects.requireNonNull(format);
         mRouteFlags = routeFlags;
@@ -89,7 +81,6 @@
         mDeviceSystemType = deviceType;
         mDeviceAddress = (deviceAddress == null) ? new String("") : deviceAddress;
         mToken = token;
-        mVirtualDeviceId = virtualDeviceId;
     }
 
     // CALLBACK_FLAG_* values: keep in sync with AudioMix::kCbFlag* values defined
@@ -278,11 +269,6 @@
     }
 
     /** @hide */
-    public boolean matchesVirtualDeviceId(int deviceId) {
-        return mVirtualDeviceId == deviceId;
-    }
-
-    /** @hide */
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;
@@ -325,7 +311,6 @@
         mFormat.writeToParcel(dest, flags);
         mRule.writeToParcel(dest, flags);
         dest.writeStrongBinder(mToken);
-        dest.writeInt(mVirtualDeviceId);
     }
 
     public static final @NonNull Parcelable.Creator<AudioMix> CREATOR = new Parcelable.Creator<>() {
@@ -346,7 +331,6 @@
             mixBuilder.setFormat(AudioFormat.CREATOR.createFromParcel(p));
             mixBuilder.setMixingRule(AudioMixingRule.CREATOR.createFromParcel(p));
             mixBuilder.setToken(p.readStrongBinder());
-            mixBuilder.setVirtualDeviceId(p.readInt());
             return mixBuilder.build();
         }
 
@@ -355,15 +339,6 @@
         }
     };
 
-    /**
-     * Updates the deviceId of the AudioMix to match with the AudioPolicy the mix is registered
-     * through.
-     * @hide
-     */
-    public void setVirtualDeviceId(int virtualDeviceId) {
-        mVirtualDeviceId = virtualDeviceId;
-    }
-
     /** @hide */
     @IntDef(flag = true,
             value = { ROUTE_FLAG_RENDER, ROUTE_FLAG_LOOP_BACK } )
@@ -379,7 +354,6 @@
         private int mRouteFlags = 0;
         private int mCallbackFlags = 0;
         private IBinder mToken = null;
-        private int mVirtualDeviceId = Context.DEVICE_ID_DEFAULT;
         // an AudioSystem.DEVICE_* value, not AudioDeviceInfo.TYPE_*
         private int mDeviceSystemType = AudioSystem.DEVICE_NONE;
         private String mDeviceAddress = null;
@@ -430,15 +404,6 @@
 
         /**
          * @hide
-         * Only used by AudioMix internally.
-         */
-        Builder setVirtualDeviceId(int virtualDeviceId) {
-            mVirtualDeviceId = virtualDeviceId;
-            return this;
-        }
-
-        /**
-         * @hide
          * Only used by AudioPolicyConfig, not a public API.
          * @param callbackFlags which callbacks are called from native
          * @return the same Builder instance.
@@ -605,7 +570,7 @@
             }
 
             return new AudioMix(mRule, mFormat, mRouteFlags, mCallbackFlags, mDeviceSystemType,
-                    mDeviceAddress, mToken, mVirtualDeviceId);
+                    mDeviceAddress, mToken);
         }
 
         private int getLoopbackDeviceSystemTypeForAudioMixingRule(AudioMixingRule rule) {
diff --git a/media/java/android/media/audiopolicy/AudioPolicy.java b/media/java/android/media/audiopolicy/AudioPolicy.java
index 293a8f8..508c0a2b 100644
--- a/media/java/android/media/audiopolicy/AudioPolicy.java
+++ b/media/java/android/media/audiopolicy/AudioPolicy.java
@@ -27,7 +27,6 @@
 import android.annotation.TestApi;
 import android.annotation.UserIdInt;
 import android.app.ActivityManager;
-import android.content.AttributionSource;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.media.AudioAttributes;
@@ -147,16 +146,6 @@
         return mProjection;
     }
 
-    /** @hide */
-    public AttributionSource getAttributionSource() {
-        return getAttributionSource(mContext);
-    }
-
-    private static AttributionSource getAttributionSource(Context context) {
-        return context == null
-                ? AttributionSource.myAttributionSource() : context.getAttributionSource();
-    }
-
     /**
      * The parameters are guaranteed non-null through the Builder
      */
@@ -219,9 +208,6 @@
             if (mix == null) {
                 throw new IllegalArgumentException("Illegal null AudioMix argument");
             }
-            if (android.permission.flags.Flags.deviceAwarePermissionApisEnabled()) {
-                mix.setVirtualDeviceId(getAttributionSource(mContext).getDeviceId());
-            }
             mMixes.add(mix);
             return this;
         }
@@ -372,9 +358,6 @@
                 if (mix == null) {
                     throw new IllegalArgumentException("Illegal null AudioMix in attachMixes");
                 } else {
-                    if (android.permission.flags.Flags.deviceAwarePermissionApisEnabled()) {
-                        mix.setVirtualDeviceId(getAttributionSource(mContext).getDeviceId());
-                    }
                     zeMixes.add(mix);
                 }
             }
@@ -417,9 +400,6 @@
                 if (mix == null) {
                     throw new IllegalArgumentException("Illegal null AudioMix in detachMixes");
                 } else {
-                    if (android.permission.flags.Flags.deviceAwarePermissionApisEnabled()) {
-                        mix.setVirtualDeviceId(getAttributionSource(mContext).getDeviceId());
-                    }
                     zeMixes.add(mix);
                 }
             }
diff --git a/native/android/surface_control_input_receiver.cpp b/native/android/surface_control_input_receiver.cpp
index da0defd..d178abc 100644
--- a/native/android/surface_control_input_receiver.cpp
+++ b/native/android/surface_control_input_receiver.cpp
@@ -45,6 +45,8 @@
             mClientToken(clientToken),
             mInputTransferToken(inputTransferToken) {}
 
+    // The InputConsumer does not keep the InputReceiver alive so the receiver is cleared once the
+    // owner releases it.
     ~InputReceiver() {
         remove();
     }
diff --git a/nfc/api/current.txt b/nfc/api/current.txt
index da292a81..80b2be2 100644
--- a/nfc/api/current.txt
+++ b/nfc/api/current.txt
@@ -268,10 +268,9 @@
   }
 
   @FlaggedApi("android.nfc.nfc_read_polling_loop") public final class PollingFrame implements android.os.Parcelable {
-    ctor public PollingFrame(int, @Nullable byte[], int, int, boolean);
     method public int describeContents();
     method @NonNull public byte[] getData();
-    method public int getTimestamp();
+    method public long getTimestamp();
     method public boolean getTriggeredAutoTransact();
     method public int getType();
     method public int getVendorSpecificGain();
diff --git a/nfc/java/android/nfc/cardemulation/PollingFrame.java b/nfc/java/android/nfc/cardemulation/PollingFrame.java
index af63a6e..654e8cc 100644
--- a/nfc/java/android/nfc/cardemulation/PollingFrame.java
+++ b/nfc/java/android/nfc/cardemulation/PollingFrame.java
@@ -16,6 +16,7 @@
 
 package android.nfc.cardemulation;
 
+import android.annotation.DurationMillisLong;
 import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
@@ -148,7 +149,8 @@
     private final int mType;
     private final byte[] mData;
     private final int mGain;
-    private final int mTimestamp;
+    @DurationMillisLong
+    private final long mTimestamp;
     private final boolean mTriggeredAutoTransact;
 
     public static final @NonNull Parcelable.Creator<PollingFrame> CREATOR =
@@ -180,16 +182,18 @@
      * @param type the type of the frame
      * @param data a byte array of the data contained in the frame
      * @param gain the vendor-specific gain of the field
-     * @param timestamp the timestamp in millisecones
+     * @param timestampMillis the timestamp in millisecones
      * @param triggeredAutoTransact whether or not this frame triggered the device to start a
      * transaction automatically
+     *
+     * @hide
      */
     public PollingFrame(@PollingFrameType int type, @Nullable byte[] data,
-            int gain, int timestamp, boolean triggeredAutoTransact) {
+            int gain, @DurationMillisLong long timestampMillis, boolean triggeredAutoTransact) {
         mType = type;
         mData = data == null ? new byte[0] : data;
         mGain = gain;
-        mTimestamp = timestamp;
+        mTimestamp = timestampMillis;
         mTriggeredAutoTransact = triggeredAutoTransact;
     }
 
@@ -230,7 +234,7 @@
      * frames relative to each other.
      * @return the timestamp in milliseconds
      */
-    public int getTimestamp() {
+    public @DurationMillisLong long getTimestamp() {
         return mTimestamp;
     }
 
@@ -264,7 +268,7 @@
             frame.putInt(KEY_POLLING_LOOP_GAIN, (byte) getVendorSpecificGain());
         }
         frame.putByteArray(KEY_POLLING_LOOP_DATA, getData());
-        frame.putInt(KEY_POLLING_LOOP_TIMESTAMP, getTimestamp());
+        frame.putLong(KEY_POLLING_LOOP_TIMESTAMP, getTimestamp());
         frame.putBoolean(KEY_POLLING_LOOP_TRIGGERED_AUTOTRANSACT, getTriggeredAutoTransact());
         return frame;
     }
@@ -273,7 +277,7 @@
     public String toString() {
         return "PollingFrame { Type: " + (char) getType()
                 + ", gain: " + getVendorSpecificGain()
-                + ", timestamp: " + Integer.toUnsignedString(getTimestamp())
+                + ", timestamp: " + Long.toUnsignedString(getTimestamp())
                 + ", data: [" + HexFormat.ofDelimiter(" ").formatHex(getData()) + "] }";
     }
 }
diff --git a/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml b/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml
index 7f09dd5..914987a 100644
--- a/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml
+++ b/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml
@@ -33,10 +33,9 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_gravity="center"
-            android:paddingLeft="@dimen/autofill_view_left_padding"
+            android:paddingStart="@dimen/autofill_view_left_padding"
             android:src="@drawable/more_horiz_24px"
             android:tint="?androidprv:attr/materialColorOnSurface"
-            android:layout_alignParentStart="true"
             android:contentDescription="@string/more_options_content_description"
             android:background="@null"/>
 
@@ -44,8 +43,8 @@
         android:id="@+id/text_container"
         android:layout_width="@dimen/autofill_dropdown_textview_max_width"
         android:layout_height="wrap_content"
-        android:paddingLeft="@dimen/autofill_view_left_padding"
-        android:paddingRight="@dimen/autofill_view_right_padding"
+        android:paddingStart="@dimen/autofill_view_left_padding"
+        android:paddingEnd="@dimen/autofill_view_right_padding"
         android:paddingTop="@dimen/more_options_item_vertical_padding"
         android:paddingBottom="@dimen/more_options_item_vertical_padding"
         android:orientation="vertical">
@@ -54,9 +53,7 @@
             android:id="@android:id/text1"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:layout_alignParentTop="true"
             android:textColor="?androidprv:attr/materialColorOnSurface"
-            android:layout_toEndOf="@android:id/icon1"
             style="@style/autofill.TextTitle"/>
     </LinearLayout>
 
diff --git a/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml b/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml
index 08948d7..e998fe8 100644
--- a/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml
+++ b/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml
@@ -42,8 +42,8 @@
                     android:id="@+id/text_container"
                     android:layout_width="@dimen/autofill_dropdown_textview_max_width"
                     android:layout_height="wrap_content"
-                    android:paddingLeft="@dimen/autofill_view_left_padding"
-                    android:paddingRight="@dimen/autofill_view_right_padding"
+                    android:paddingStart="@dimen/autofill_view_left_padding"
+                    android:paddingEnd="@dimen/autofill_view_right_padding"
                     android:paddingTop="@dimen/autofill_view_top_padding"
                     android:paddingBottom="@dimen/autofill_view_bottom_padding"
                     android:orientation="vertical">
@@ -52,8 +52,6 @@
                             android:id="@android:id/text1"
                             android:layout_width="wrap_content"
                             android:layout_height="wrap_content"
-                            android:layout_alignParentTop="true"
-                            android:layout_toEndOf="@android:id/icon1"
                             android:textColor="?androidprv:attr/materialColorOnSurface"
                             style="@style/autofill.TextTitle"/>
 
@@ -61,8 +59,6 @@
                             android:id="@android:id/text2"
                             android:layout_width="wrap_content"
                             android:layout_height="wrap_content"
-                            android:layout_below="@android:id/text1"
-                            android:layout_toEndOf="@android:id/icon1"
                             android:textColor="?androidprv:attr/materialColorOnSurfaceVariant"
                             style="@style/autofill.TextSubtitle"/>
 
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallSuccess.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallSuccess.java
index 215ead3..167d5061 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallSuccess.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallSuccess.java
@@ -108,18 +108,19 @@
         mDialog = builder.create();
         mDialog.show();
         mDialog.requireViewById(R.id.install_success).setVisibility(View.VISIBLE);
-        // Enable or disable "launch" button
-        boolean enabled = false;
+        // Show or hide "launch" button
+        boolean visible = false;
         if (mLaunchIntent != null) {
             List<ResolveInfo> list = getPackageManager().queryIntentActivities(mLaunchIntent,
                     0);
             if (list != null && list.size() > 0) {
-                enabled = true;
+                visible = true;
             }
         }
+        visible = visible && isLauncherActivityEnabled(mLaunchIntent);
 
         Button launchButton = mDialog.getButton(DialogInterface.BUTTON_POSITIVE);
-        if (enabled) {
+        if (visible) {
             launchButton.setOnClickListener(view -> {
                 try {
                     startActivity(mLaunchIntent.addFlags(
@@ -130,7 +131,15 @@
                 finish();
             });
         } else {
-            launchButton.setEnabled(false);
+            launchButton.setVisibility(View.GONE);
         }
     }
+
+    private boolean isLauncherActivityEnabled(Intent intent) {
+        if (intent == null || intent.getComponent() == null) {
+            return false;
+        }
+        return getPackageManager().getComponentEnabledSetting(intent.getComponent())
+            != PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
+    }
 }
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
index aeabbd5..32795e4 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
@@ -30,6 +30,7 @@
 import android.content.pm.PackageInstaller.SessionInfo
 import android.content.pm.PackageInstaller.SessionParams
 import android.content.pm.PackageManager
+import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED
 import android.net.Uri
 import android.os.ParcelFileDescriptor
 import android.os.Process
@@ -830,7 +831,8 @@
             val resultIntent = if (shouldReturnResult) {
                 Intent().putExtra(Intent.EXTRA_INSTALL_RESULT, PackageManager.INSTALL_SUCCEEDED)
             } else {
-                packageManager.getLaunchIntentForPackage(newPackageInfo!!.packageName)
+                val intent = packageManager.getLaunchIntentForPackage(newPackageInfo!!.packageName)
+                if (isLauncherActivityEnabled(intent)) intent else null
             }
             _installResult.setValue(InstallSuccess(appSnippet, shouldReturnResult, resultIntent))
         } else {
@@ -838,6 +840,14 @@
         }
     }
 
+    private fun isLauncherActivityEnabled(intent: Intent?): Boolean {
+        if (intent == null || intent.component == null) {
+            return false
+        }
+        return (intent.component?.let { packageManager.getComponentEnabledSetting(it) }
+            != COMPONENT_ENABLED_STATE_DISABLED)
+    }
+
     /**
      * Cleanup the staged session. Also signal the packageinstaller that an install session is to
      * be aborted
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallSuccessFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallSuccessFragment.java
index b2a65faa..e491f9c 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallSuccessFragment.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallSuccessFragment.java
@@ -23,13 +23,13 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.os.Bundle;
+import android.util.Log;
 import android.view.View;
 import android.widget.Button;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.fragment.app.DialogFragment;
 import com.android.packageinstaller.R;
-import com.android.packageinstaller.v2.model.InstallStage;
 import com.android.packageinstaller.v2.model.InstallSuccess;
 import com.android.packageinstaller.v2.ui.InstallActionListener;
 import java.util.List;
@@ -40,6 +40,7 @@
  */
 public class InstallSuccessFragment extends DialogFragment {
 
+    private static final String LOG_TAG = InstallSuccessFragment.class.getSimpleName();
     private final InstallSuccess mDialogData;
     private AlertDialog mDialog;
     private InstallActionListener mInstallActionListener;
@@ -60,12 +61,15 @@
     @Override
     public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
         View dialogView = getLayoutInflater().inflate(R.layout.install_content_view, null);
-        mDialog = new AlertDialog.Builder(requireContext()).setTitle(mDialogData.getAppLabel())
-            .setIcon(mDialogData.getAppIcon()).setView(dialogView).setNegativeButton(R.string.done,
+        mDialog = new AlertDialog.Builder(requireContext())
+            .setTitle(mDialogData.getAppLabel())
+            .setIcon(mDialogData.getAppIcon())
+            .setView(dialogView)
+            .setNegativeButton(R.string.done,
                 (dialog, which) -> mInstallActionListener.onNegativeResponse(
-                    InstallStage.STAGE_SUCCESS))
-            .setPositiveButton(R.string.launch, (dialog, which) -> {
-            }).create();
+                    mDialogData.getStageCode()))
+            .setPositiveButton(R.string.launch, (dialog, which) -> {})
+            .create();
 
         dialogView.requireViewById(R.id.install_success).setVisibility(View.VISIBLE);
 
@@ -76,25 +80,28 @@
     public void onStart() {
         super.onStart();
         Button launchButton = mDialog.getButton(DialogInterface.BUTTON_POSITIVE);
-        boolean enabled = false;
+        boolean visible = false;
         if (mDialogData.getResultIntent() != null) {
             List<ResolveInfo> list = mPm.queryIntentActivities(mDialogData.getResultIntent(), 0);
             if (list.size() > 0) {
-                enabled = true;
+                visible = true;
             }
         }
-        if (enabled) {
+        if (visible) {
             launchButton.setOnClickListener(view -> {
+                Log.i(LOG_TAG, "Finished installing and launching " +
+                    mDialogData.getAppLabel());
                 mInstallActionListener.openInstalledApp(mDialogData.getResultIntent());
             });
         } else {
-            launchButton.setEnabled(false);
+            launchButton.setVisibility(View.GONE);
         }
     }
 
     @Override
     public void onCancel(@NonNull DialogInterface dialog) {
         super.onCancel(dialog);
+        Log.i(LOG_TAG, "Finished installing " + mDialogData.getAppLabel());
         mInstallActionListener.onNegativeResponse(mDialogData.getStageCode());
     }
 }
diff --git a/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/KeyedObserverTest.kt b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/KeyedObserverTest.kt
new file mode 100644
index 0000000..b52586c
--- /dev/null
+++ b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/KeyedObserverTest.kt
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.datastore
+
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors.directExecutor
+import java.util.concurrent.Executor
+import java.util.concurrent.atomic.AtomicInteger
+import org.junit.Assert
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
+import org.mockito.kotlin.never
+import org.mockito.kotlin.reset
+import org.mockito.kotlin.verify
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class KeyedObserverTest {
+    @get:Rule
+    val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+    @Mock
+    private lateinit var observer1: KeyedObserver<Any?>
+
+    @Mock
+    private lateinit var observer2: KeyedObserver<Any?>
+
+    @Mock
+    private lateinit var keyedObserver1: KeyedObserver<Any>
+
+    @Mock
+    private lateinit var keyedObserver2: KeyedObserver<Any>
+
+    @Mock
+    private lateinit var key1: Any
+
+    @Mock
+    private lateinit var key2: Any
+
+    @Mock
+    private lateinit var executor: Executor
+
+    private val keyedObservable = KeyedDataObservable<Any>()
+
+    @Test
+    fun addObserver_sameExecutor() {
+        keyedObservable.addObserver(observer1, executor)
+        keyedObservable.addObserver(observer1, executor)
+    }
+
+    @Test
+    fun addObserver_keyedObserver_sameExecutor() {
+        keyedObservable.addObserver(key1, keyedObserver1, executor)
+        keyedObservable.addObserver(key1, keyedObserver1, executor)
+    }
+
+    @Test
+    fun addObserver_differentExecutor() {
+        keyedObservable.addObserver(observer1, executor)
+        Assert.assertThrows(IllegalStateException::class.java) {
+            keyedObservable.addObserver(observer1, directExecutor())
+        }
+    }
+
+    @Test
+    fun addObserver_keyedObserver_differentExecutor() {
+        keyedObservable.addObserver(key1, keyedObserver1, executor)
+        Assert.assertThrows(IllegalStateException::class.java) {
+            keyedObservable.addObserver(key1, keyedObserver1, directExecutor())
+        }
+    }
+
+    @Test
+    fun addObserver_weaklyReferenced() {
+        val counter = AtomicInteger()
+        var observer: KeyedObserver<Any?>? = KeyedObserver { _, _ -> counter.incrementAndGet() }
+        keyedObservable.addObserver(observer!!, directExecutor())
+
+        keyedObservable.notifyChange(ChangeReason.UPDATE)
+        assertThat(counter.get()).isEqualTo(1)
+
+        // trigger GC, the observer callback should not be invoked
+        null.also { observer = it }
+        System.gc()
+        System.runFinalization()
+
+        keyedObservable.notifyChange(ChangeReason.UPDATE)
+        assertThat(counter.get()).isEqualTo(1)
+    }
+
+    @Test
+    fun addObserver_keyedObserver_weaklyReferenced() {
+        val counter = AtomicInteger()
+        var keyObserver: KeyedObserver<Any>? = KeyedObserver { _, _ -> counter.incrementAndGet() }
+        keyedObservable.addObserver(key1, keyObserver!!, directExecutor())
+
+        keyedObservable.notifyChange(key1, ChangeReason.UPDATE)
+        assertThat(counter.get()).isEqualTo(1)
+
+        // trigger GC, the observer callback should not be invoked
+        null.also { keyObserver = it }
+        System.gc()
+        System.runFinalization()
+
+        keyedObservable.notifyChange(key1, ChangeReason.UPDATE)
+        assertThat(counter.get()).isEqualTo(1)
+    }
+
+    @Test
+    fun addObserver_notifyObservers_removeObserver() {
+        keyedObservable.addObserver(observer1, directExecutor())
+        keyedObservable.addObserver(observer2, executor)
+
+        keyedObservable.notifyChange(ChangeReason.UPDATE)
+        verify(observer1).onKeyChanged(null, ChangeReason.UPDATE)
+        verify(observer2, never()).onKeyChanged(any(), any())
+        verify(executor).execute(any())
+
+        reset(observer1, executor)
+        keyedObservable.removeObserver(observer2)
+
+        keyedObservable.notifyChange(ChangeReason.DELETE)
+        verify(observer1).onKeyChanged(null, ChangeReason.DELETE)
+        verify(executor, never()).execute(any())
+    }
+
+    @Test
+    fun addObserver_keyedObserver_notifyObservers_removeObserver() {
+        keyedObservable.addObserver(key1, keyedObserver1, directExecutor())
+        keyedObservable.addObserver(key2, keyedObserver2, executor)
+
+        keyedObservable.notifyChange(key1, ChangeReason.UPDATE)
+        verify(keyedObserver1).onKeyChanged(key1, ChangeReason.UPDATE)
+        verify(keyedObserver2, never()).onKeyChanged(any(), any())
+        verify(executor, never()).execute(any())
+
+        reset(keyedObserver1, executor)
+        keyedObservable.removeObserver(key2, keyedObserver2)
+
+        keyedObservable.notifyChange(key1, ChangeReason.DELETE)
+        verify(keyedObserver1).onKeyChanged(key1, ChangeReason.DELETE)
+        verify(executor, never()).execute(any())
+    }
+
+    @Test
+    fun notifyChange_addMoreTypeObservers_checkOnKeyChanged() {
+        keyedObservable.addObserver(observer1, directExecutor())
+        keyedObservable.addObserver(key1, keyedObserver1, directExecutor())
+        keyedObservable.addObserver(key2, keyedObserver2, directExecutor())
+
+        keyedObservable.notifyChange(ChangeReason.UPDATE)
+        verify(observer1).onKeyChanged(null, ChangeReason.UPDATE)
+        verify(keyedObserver1).onKeyChanged(key1, ChangeReason.UPDATE)
+        verify(keyedObserver2).onKeyChanged(key2, ChangeReason.UPDATE)
+
+        reset(observer1, keyedObserver1, keyedObserver2)
+        keyedObservable.notifyChange(key1, ChangeReason.UPDATE)
+
+        verify(observer1).onKeyChanged(key1, ChangeReason.UPDATE)
+        verify(keyedObserver1).onKeyChanged(key1, ChangeReason.UPDATE)
+        verify(keyedObserver2, never()).onKeyChanged(key1, ChangeReason.UPDATE)
+
+        reset(observer1, keyedObserver1, keyedObserver2)
+        keyedObservable.notifyChange(key2, ChangeReason.UPDATE)
+
+        verify(observer1).onKeyChanged(key2, ChangeReason.UPDATE)
+        verify(keyedObserver1, never()).onKeyChanged(key2, ChangeReason.UPDATE)
+        verify(keyedObserver2).onKeyChanged(key2, ChangeReason.UPDATE)
+    }
+
+    @Test
+    fun notifyChange_addObserverWithinCallback() {
+        // ConcurrentModificationException is raised if it is not implemented correctly
+        val observer: KeyedObserver<Any?> = KeyedObserver { _, _ ->
+            keyedObservable.addObserver(observer1, executor)
+        }
+
+        keyedObservable.addObserver(observer, directExecutor())
+
+        keyedObservable.notifyChange(ChangeReason.UPDATE)
+        keyedObservable.removeObserver(observer)
+    }
+
+    @Test
+    fun notifyChange_KeyedObserver_addObserverWithinCallback() {
+        // ConcurrentModificationException is raised if it is not implemented correctly
+        val keyObserver: KeyedObserver<Any?> = KeyedObserver { _, _ ->
+            keyedObservable.addObserver(key1, keyedObserver1, executor)
+        }
+
+        keyedObservable.addObserver(key1, keyObserver, directExecutor())
+
+        keyedObservable.notifyChange(key1, ChangeReason.UPDATE)
+        keyedObservable.removeObserver(key1, keyObserver)
+    }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/ObserverTest.kt b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/ObserverTest.kt
index bb791dc..f065829 100644
--- a/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/ObserverTest.kt
+++ b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/ObserverTest.kt
@@ -69,8 +69,7 @@
         assertThat(counter.get()).isEqualTo(1)
 
         // trigger GC, the observer callback should not be invoked
-        @Suppress("unused")
-        observer = null
+        null.also { observer = it }
         System.gc()
         System.runFinalization()
 
@@ -100,10 +99,12 @@
     @Test
     fun notifyChange_addObserverWithinCallback() {
         // ConcurrentModificationException is raised if it is not implemented correctly
+        val observer = Observer { observable.addObserver(observer1, executor) }
         observable.addObserver(
-            { observable.addObserver(observer1, executor) },
+            observer,
             MoreExecutors.directExecutor()
         )
         observable.notifyChange(ChangeReason.UPDATE)
+        observable.removeObserver(observer)
     }
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index b8624fd..4777b0d 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -1315,8 +1315,7 @@
                 boolean isActiveAshaHearingAid = mIsActiveDeviceHearingAid;
                 boolean isActiveLeAudioHearingAid = mIsActiveDeviceLeAudio
                         && isConnectedHapClientDevice();
-                if ((isActiveAshaHearingAid || isActiveLeAudioHearingAid)
-                        && stringRes == R.string.bluetooth_active_no_battery_level) {
+                if (isActiveAshaHearingAid || isActiveLeAudioHearingAid) {
                     final Set<CachedBluetoothDevice> memberDevices = getMemberDevice();
                     final CachedBluetoothDevice subDevice = getSubDevice();
                     if (memberDevices.stream().anyMatch(m -> m.isConnected())) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt b/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt
index cda6b8b..68f471d 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt
@@ -17,6 +17,7 @@
 package com.android.settingslib.media.session
 
 import android.media.session.MediaController
+import android.media.session.MediaSession
 import android.media.session.MediaSessionManager
 import android.os.UserHandle
 import androidx.concurrent.futures.DirectExecutor
@@ -28,7 +29,7 @@
 import kotlinx.coroutines.launch
 
 /** [Flow] for [MediaSessionManager.OnActiveSessionsChangedListener]. */
-val MediaSessionManager.activeMediaChanges: Flow<Collection<MediaController>?>
+val MediaSessionManager.activeMediaChanges: Flow<List<MediaController>?>
     get() =
         callbackFlow {
                 val listener =
@@ -42,3 +43,24 @@
                 awaitClose { removeOnActiveSessionsChangedListener(listener) }
             }
             .buffer(capacity = Channel.CONFLATED)
+
+/** [Flow] for [MediaSessionManager.RemoteSessionCallback]. */
+val MediaSessionManager.remoteSessionChanges: Flow<MediaSession.Token?>
+    get() =
+        callbackFlow {
+                val callback =
+                    object : MediaSessionManager.RemoteSessionCallback {
+                        override fun onVolumeChanged(sessionToken: MediaSession.Token, flags: Int) {
+                            launch { send(sessionToken) }
+                        }
+
+                        override fun onDefaultRemoteSessionChanged(
+                            sessionToken: MediaSession.Token?
+                        ) {
+                            launch { send(sessionToken) }
+                        }
+                    }
+                registerRemoteSessionCallback(DirectExecutor.INSTANCE, callback)
+                awaitClose { unregisterRemoteSessionCallback(callback) }
+            }
+            .buffer(capacity = Channel.CONFLATED)
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt
index 298dd71e..724dd51 100644
--- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt
@@ -15,14 +15,10 @@
  */
 package com.android.settingslib.volume.data.repository
 
-import android.media.MediaRouter2Manager
-import android.media.RoutingSessionInfo
 import com.android.settingslib.media.LocalMediaManager
 import com.android.settingslib.media.MediaDevice
-import com.android.settingslib.volume.data.model.RoutingSession
 import com.android.settingslib.volume.shared.AudioManagerEventsReceiver
 import com.android.settingslib.volume.shared.model.AudioManagerEvent
-import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
@@ -30,35 +26,23 @@
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.callbackFlow
 import kotlinx.coroutines.flow.filterIsInstance
-import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.mapNotNull
 import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.shareIn
 import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.withContext
 
 /** Repository providing data about connected media devices. */
 interface LocalMediaRepository {
 
-    /** Available devices list */
-    val mediaDevices: StateFlow<Collection<MediaDevice>>
-
     /** Currently connected media device */
     val currentConnectedDevice: StateFlow<MediaDevice?>
-
-    val remoteRoutingSessions: StateFlow<Collection<RoutingSession>>
-
-    suspend fun adjustSessionVolume(sessionId: String?, volume: Int)
 }
 
 class LocalMediaRepositoryImpl(
     audioManagerEventsReceiver: AudioManagerEventsReceiver,
     private val localMediaManager: LocalMediaManager,
-    private val mediaRouter2Manager: MediaRouter2Manager,
     coroutineScope: CoroutineScope,
-    private val backgroundContext: CoroutineContext,
 ) : LocalMediaRepository {
 
     private val devicesChanges =
@@ -94,18 +78,6 @@
             }
             .shareIn(coroutineScope, SharingStarted.WhileSubscribed(), replay = 0)
 
-    override val mediaDevices: StateFlow<Collection<MediaDevice>> =
-        mediaDevicesUpdates
-            .mapNotNull {
-                if (it is DevicesUpdate.DeviceListUpdate) {
-                    it.newDevices ?: emptyList()
-                } else {
-                    null
-                }
-            }
-            .flowOn(backgroundContext)
-            .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList())
-
     override val currentConnectedDevice: StateFlow<MediaDevice?> =
         merge(devicesChanges, mediaDevicesUpdates)
             .map { localMediaManager.currentConnectedDevice }
@@ -116,30 +88,6 @@
                 localMediaManager.currentConnectedDevice
             )
 
-    override val remoteRoutingSessions: StateFlow<Collection<RoutingSession>> =
-        merge(devicesChanges, mediaDevicesUpdates)
-            .onStart { emit(Unit) }
-            .map { localMediaManager.remoteRoutingSessions.map(::toRoutingSession) }
-            .flowOn(backgroundContext)
-            .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList())
-
-    override suspend fun adjustSessionVolume(sessionId: String?, volume: Int) {
-        withContext(backgroundContext) {
-            if (sessionId == null) {
-                localMediaManager.adjustSessionVolume(volume)
-            } else {
-                localMediaManager.adjustSessionVolume(sessionId, volume)
-            }
-        }
-    }
-
-    private fun toRoutingSession(info: RoutingSessionInfo): RoutingSession =
-        RoutingSession(
-            info,
-            isMediaOutputDisabled = mediaRouter2Manager.getTransferableRoutes(info).isEmpty(),
-            isVolumeSeekBarEnabled = localMediaManager.shouldEnableVolumeSeekBar(info)
-        )
-
     private sealed interface DevicesUpdate {
 
         data class DeviceListUpdate(val newDevices: List<MediaDevice>?) : DevicesUpdate
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt
index 7c231d1..e4ac9fe 100644
--- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt
@@ -27,18 +27,26 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.flow.filterIsInstance
-import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.stateIn
 
 /** Provides controllers for currently active device media sessions. */
 interface MediaControllerRepository {
 
-    /** Current [MediaController]. Null is emitted when there is no active [MediaController]. */
-    val activeLocalMediaController: StateFlow<MediaController?>
+    /**
+     * Get a list of controllers for all ongoing sessions. The controllers will be provided in
+     * priority order with the most important controller at index 0.
+     *
+     * This requires the [android.Manifest.permission.MEDIA_CONTENT_CONTROL] permission be held by
+     * the calling app.
+     */
+    val activeSessions: StateFlow<List<MediaController>>
 }
 
 class MediaControllerRepositoryImpl(
@@ -49,51 +57,17 @@
     backgroundContext: CoroutineContext,
 ) : MediaControllerRepository {
 
-    private val devicesChanges =
-        audioManagerEventsReceiver.events.filterIsInstance(
-            AudioManagerEvent.StreamDevicesChanged::class
-        )
-
-    override val activeLocalMediaController: StateFlow<MediaController?> =
-        combine(
-                mediaSessionManager.activeMediaChanges.onStart {
-                    emit(mediaSessionManager.getActiveSessions(null))
-                },
-                localBluetoothManager?.headsetAudioModeChanges?.onStart { emit(Unit) }
-                    ?: flowOf(null),
-                devicesChanges.onStart { emit(AudioManagerEvent.StreamDevicesChanged) },
-            ) { controllers, _, _ ->
-                controllers?.let(::findLocalMediaController)
-            }
+    override val activeSessions: StateFlow<List<MediaController>> =
+        merge(
+                mediaSessionManager.activeMediaChanges.filterNotNull(),
+                localBluetoothManager?.headsetAudioModeChanges?.map {
+                    mediaSessionManager.getActiveSessions(null)
+                } ?: emptyFlow(),
+                audioManagerEventsReceiver.events
+                    .filterIsInstance(AudioManagerEvent.StreamDevicesChanged::class)
+                    .map { mediaSessionManager.getActiveSessions(null) },
+            )
+            .onStart { emit(mediaSessionManager.getActiveSessions(null)) }
             .flowOn(backgroundContext)
-            .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), null)
-
-    private fun findLocalMediaController(
-        controllers: Collection<MediaController>,
-    ): MediaController? {
-        var localController: MediaController? = null
-        val remoteMediaSessionLists: MutableList<String> = ArrayList()
-        for (controller in controllers) {
-            val playbackInfo: MediaController.PlaybackInfo = controller.playbackInfo ?: continue
-            when (playbackInfo.playbackType) {
-                MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE -> {
-                    if (localController?.packageName.equals(controller.packageName)) {
-                        localController = null
-                    }
-                    if (!remoteMediaSessionLists.contains(controller.packageName)) {
-                        remoteMediaSessionLists.add(controller.packageName)
-                    }
-                }
-                MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL -> {
-                    if (
-                        localController == null &&
-                            !remoteMediaSessionLists.contains(controller.packageName)
-                    ) {
-                        localController = controller
-                    }
-                }
-            }
-        }
-        return localController
-    }
+            .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList())
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/LocalMediaInteractor.kt b/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/LocalMediaInteractor.kt
deleted file mode 100644
index f621335..0000000
--- a/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/LocalMediaInteractor.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *       http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-
-package com.android.settingslib.volume.domain.interactor
-
-import com.android.settingslib.media.MediaDevice
-import com.android.settingslib.volume.data.repository.LocalMediaRepository
-import com.android.settingslib.volume.domain.model.RoutingSession
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
-
-class LocalMediaInteractor(
-    private val repository: LocalMediaRepository,
-    coroutineScope: CoroutineScope,
-) {
-
-    /** Available devices list */
-    val mediaDevices: StateFlow<Collection<MediaDevice>>
-        get() = repository.mediaDevices
-
-    /** Currently connected media device */
-    val currentConnectedDevice: StateFlow<MediaDevice?>
-        get() = repository.currentConnectedDevice
-
-    val remoteRoutingSessions: StateFlow<List<RoutingSession>> =
-        repository.remoteRoutingSessions
-            .map { sessions ->
-                sessions.map {
-                    RoutingSession(
-                        routingSessionInfo = it.routingSessionInfo,
-                        isMediaOutputDisabled = it.isMediaOutputDisabled,
-                        isVolumeSeekBarEnabled =
-                            it.isVolumeSeekBarEnabled && it.routingSessionInfo.volumeMax > 0
-                    )
-                }
-            }
-            .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList())
-
-    suspend fun adjustSessionVolume(sessionId: String?, volume: Int) =
-        repository.adjustSessionVolume(sessionId, volume)
-}
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/LocalMediaRepositoryImplTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/LocalMediaRepositoryImplTest.kt
index 2d12dae..caf41f2 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/LocalMediaRepositoryImplTest.kt
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/LocalMediaRepositoryImplTest.kt
@@ -15,17 +15,12 @@
  */
 package com.android.settingslib.volume.data.repository
 
-import android.media.MediaRoute2Info
-import android.media.MediaRouter2Manager
-import android.media.RoutingSessionInfo
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.settingslib.media.LocalMediaManager
 import com.android.settingslib.media.MediaDevice
-import com.android.settingslib.volume.data.model.RoutingSession
 import com.android.settingslib.volume.shared.FakeAudioManagerEventsReceiver
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.test.TestScope
@@ -37,15 +32,10 @@
 import org.mockito.ArgumentCaptor
 import org.mockito.Captor
 import org.mockito.Mock
-import org.mockito.Mockito.any
-import org.mockito.Mockito.anyInt
-import org.mockito.Mockito.anyString
-import org.mockito.Mockito.mock
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
 
-@OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(AndroidJUnit4::class)
 @SmallTest
 class LocalMediaRepositoryImplTest {
@@ -53,7 +43,6 @@
     @Mock private lateinit var localMediaManager: LocalMediaManager
     @Mock private lateinit var mediaDevice1: MediaDevice
     @Mock private lateinit var mediaDevice2: MediaDevice
-    @Mock private lateinit var mediaRouter2Manager: MediaRouter2Manager
 
     @Captor
     private lateinit var deviceCallbackCaptor: ArgumentCaptor<LocalMediaManager.DeviceCallback>
@@ -71,29 +60,11 @@
             LocalMediaRepositoryImpl(
                 eventsReceiver,
                 localMediaManager,
-                mediaRouter2Manager,
                 testScope.backgroundScope,
-                testScope.testScheduler,
             )
     }
 
     @Test
-    fun mediaDevices_areUpdated() {
-        testScope.runTest {
-            var mediaDevices: Collection<MediaDevice>? = null
-            underTest.mediaDevices.onEach { mediaDevices = it }.launchIn(backgroundScope)
-            runCurrent()
-            verify(localMediaManager).registerCallback(deviceCallbackCaptor.capture())
-            deviceCallbackCaptor.value.onDeviceListUpdate(listOf(mediaDevice1, mediaDevice2))
-            runCurrent()
-
-            assertThat(mediaDevices).hasSize(2)
-            assertThat(mediaDevices).contains(mediaDevice1)
-            assertThat(mediaDevices).contains(mediaDevice2)
-        }
-    }
-
-    @Test
     fun deviceListUpdated_currentConnectedDeviceUpdated() {
         testScope.runTest {
             var currentConnectedDevice: MediaDevice? = null
@@ -110,78 +81,4 @@
             assertThat(currentConnectedDevice).isEqualTo(mediaDevice1)
         }
     }
-
-    @Test
-    fun kek() {
-        testScope.runTest {
-            `when`(localMediaManager.remoteRoutingSessions)
-                .thenReturn(
-                    listOf(
-                        testRoutingSessionInfo1,
-                        testRoutingSessionInfo2,
-                        testRoutingSessionInfo3,
-                    )
-                )
-            `when`(localMediaManager.shouldEnableVolumeSeekBar(any())).then {
-                (it.arguments[0] as RoutingSessionInfo) == testRoutingSessionInfo1
-            }
-            `when`(mediaRouter2Manager.getTransferableRoutes(any<RoutingSessionInfo>())).then {
-                if ((it.arguments[0] as RoutingSessionInfo) == testRoutingSessionInfo2) {
-                    return@then listOf(mock(MediaRoute2Info::class.java))
-                }
-                emptyList<MediaRoute2Info>()
-            }
-            var remoteRoutingSessions: Collection<RoutingSession>? = null
-            underTest.remoteRoutingSessions
-                .onEach { remoteRoutingSessions = it }
-                .launchIn(backgroundScope)
-
-            runCurrent()
-
-            assertThat(remoteRoutingSessions)
-                .containsExactlyElementsIn(
-                    listOf(
-                        RoutingSession(
-                            routingSessionInfo = testRoutingSessionInfo1,
-                            isVolumeSeekBarEnabled = true,
-                            isMediaOutputDisabled = true,
-                        ),
-                        RoutingSession(
-                            routingSessionInfo = testRoutingSessionInfo2,
-                            isVolumeSeekBarEnabled = false,
-                            isMediaOutputDisabled = false,
-                        ),
-                        RoutingSession(
-                            routingSessionInfo = testRoutingSessionInfo3,
-                            isVolumeSeekBarEnabled = false,
-                            isMediaOutputDisabled = true,
-                        )
-                    )
-                )
-        }
-    }
-
-    @Test
-    fun adjustSessionVolume_adjusts() {
-        testScope.runTest {
-            var volume = 0
-            `when`(localMediaManager.adjustSessionVolume(anyString(), anyInt())).then {
-                volume = it.arguments[1] as Int
-                Unit
-            }
-
-            underTest.adjustSessionVolume("test_session", 10)
-
-            assertThat(volume).isEqualTo(10)
-        }
-    }
-
-    private companion object {
-        val testRoutingSessionInfo1 =
-            RoutingSessionInfo.Builder("id_1", "test.pkg.1").addSelectedRoute("route_1").build()
-        val testRoutingSessionInfo2 =
-            RoutingSessionInfo.Builder("id_2", "test.pkg.2").addSelectedRoute("route_2").build()
-        val testRoutingSessionInfo3 =
-            RoutingSessionInfo.Builder("id_3", "test.pkg.3").addSelectedRoute("route_3").build()
-    }
 }
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt
index f3d1714..964c3f7 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt
@@ -22,13 +22,10 @@
 import android.media.session.PlaybackState
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.settingslib.bluetooth.BluetoothCallback
 import com.android.settingslib.bluetooth.BluetoothEventManager
 import com.android.settingslib.bluetooth.LocalBluetoothManager
 import com.android.settingslib.volume.shared.FakeAudioManagerEventsReceiver
-import com.android.settingslib.volume.shared.model.AudioManagerEvent
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.test.TestScope
@@ -37,21 +34,15 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.Captor
 import org.mockito.Mock
 import org.mockito.Mockito.any
-import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
 
-@OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(AndroidJUnit4::class)
 @SmallTest
 class MediaControllerRepositoryImplTest {
 
-    @Captor private lateinit var callbackCaptor: ArgumentCaptor<BluetoothCallback>
-
     @Mock private lateinit var mediaSessionManager: MediaSessionManager
     @Mock private lateinit var localBluetoothManager: LocalBluetoothManager
     @Mock private lateinit var eventManager: BluetoothEventManager
@@ -103,7 +94,7 @@
     }
 
     @Test
-    fun playingMediaDevicesAvailable_sessionIsActive() {
+    fun mediaDevicesAvailable_returnsAllActiveOnes() {
         testScope.runTest {
             `when`(mediaSessionManager.getActiveSessions(any()))
                 .thenReturn(
@@ -112,53 +103,25 @@
                         statelessMediaController,
                         errorMediaController,
                         remoteMediaController,
-                        localMediaController
+                        localMediaController,
                     )
                 )
-            var mediaController: MediaController? = null
-            underTest.activeLocalMediaController
-                .onEach { mediaController = it }
-                .launchIn(backgroundScope)
+
+            var mediaControllers: Collection<MediaController>? = null
+            underTest.activeSessions.onEach { mediaControllers = it }.launchIn(backgroundScope)
             runCurrent()
 
-            eventsReceiver.triggerEvent(AudioManagerEvent.StreamDevicesChanged)
-            triggerOnAudioModeChanged()
-            runCurrent()
-
-            assertThat(mediaController).isSameInstanceAs(localMediaController)
-        }
-    }
-
-    @Test
-    fun noPlayingMediaDevicesAvailable_sessionIsInactive() {
-        testScope.runTest {
-            `when`(mediaSessionManager.getActiveSessions(any()))
-                .thenReturn(
-                    listOf(
-                        stoppedMediaController,
-                        statelessMediaController,
-                        errorMediaController,
-                    )
+            assertThat(mediaControllers)
+                .containsExactly(
+                    stoppedMediaController,
+                    statelessMediaController,
+                    errorMediaController,
+                    remoteMediaController,
+                    localMediaController,
                 )
-            var mediaController: MediaController? = null
-            underTest.activeLocalMediaController
-                .onEach { mediaController = it }
-                .launchIn(backgroundScope)
-            runCurrent()
-
-            eventsReceiver.triggerEvent(AudioManagerEvent.StreamDevicesChanged)
-            triggerOnAudioModeChanged()
-            runCurrent()
-
-            assertThat(mediaController).isNull()
         }
     }
 
-    private fun triggerOnAudioModeChanged() {
-        verify(eventManager).registerCallback(callbackCaptor.capture())
-        callbackCaptor.value.onAudioModeChanged()
-    }
-
     private companion object {
         val statePlaying: PlaybackState =
             PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0, 0f).build()
diff --git a/packages/SettingsProvider/Android.bp b/packages/SettingsProvider/Android.bp
index 7ec3d24..bf4f60d 100644
--- a/packages/SettingsProvider/Android.bp
+++ b/packages/SettingsProvider/Android.bp
@@ -60,6 +60,7 @@
         // because this test is not an instrumentation test. (because the target runs in the system process.)
         "SettingsProviderLib",
         "androidx.test.rules",
+        "frameworks-base-testutils",
         "device_config_service_flags_java",
         "flag-junit",
         "junit",
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
index 3e0d05c..1eb04ac 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
@@ -98,6 +98,7 @@
         sBroadcastOnRestore.add(Settings.Secure.DARK_THEME_CUSTOM_END_TIME);
         sBroadcastOnRestore.add(Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED);
         sBroadcastOnRestore.add(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS);
+        sBroadcastOnRestore.add(Settings.Secure.ACCESSIBILITY_QS_TARGETS);
         sBroadcastOnRestoreSystemUI = new ArraySet<String>(2);
         sBroadcastOnRestoreSystemUI.add(Settings.Secure.QS_TILES);
         sBroadcastOnRestoreSystemUI.add(Settings.Secure.QS_AUTO_ADDED_TILES);
@@ -229,6 +230,10 @@
             } else if (Settings.System.ACCELEROMETER_ROTATION.equals(name)
                     && shouldSkipAutoRotateRestore()) {
                 return;
+            } else if (Settings.Secure.ACCESSIBILITY_QS_TARGETS.equals(name)) {
+                // Don't write it to setting. Let the broadcast receiver in
+                // AccessibilityManagerService handle restore/merging logic.
+                return;
             }
 
             // Default case: write the restored value to settings
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperRestoreTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperRestoreTest.java
index 197788e..2f8cf4b 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperRestoreTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperRestoreTest.java
@@ -16,23 +16,31 @@
 
 package com.android.providers.settings;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static junit.framework.Assert.assertEquals;
 
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
+import android.content.Intent;
 import android.net.Uri;
 import android.os.Build;
 import android.provider.Settings;
+import android.provider.SettingsStringUtil;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.internal.util.test.BroadcastInterceptingContext;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mockito;
 
+import java.util.concurrent.ExecutionException;
+
 /**
  * Tests for {@link SettingsHelper#restoreValue(Context, ContentResolver, ContentValues, Uri,
  * String, String, int)}. Specifically verifies that we restore critical accessibility settings only
@@ -165,4 +173,33 @@
 
         assertEquals(restoreSettingValue, Settings.Secure.getInt(mContentResolver, settingName));
     }
+
+    @Test
+    public void restoreAccessibilityQsTargets_broadcastSent()
+            throws ExecutionException, InterruptedException {
+        BroadcastInterceptingContext interceptingContext = new BroadcastInterceptingContext(
+                mContext);
+        final String settingName = Settings.Secure.ACCESSIBILITY_QS_TARGETS;
+        final String restoreSettingValue = "com.android.server.accessibility/ColorInversion"
+                + SettingsStringUtil.DELIMITER
+                + "com.android.server.accessibility/ColorCorrectionTile";
+        BroadcastInterceptingContext.FutureIntent futureIntent =
+                interceptingContext.nextBroadcastIntent(Intent.ACTION_SETTING_RESTORED);
+
+        mSettingsHelper.restoreValue(
+                interceptingContext,
+                mContentResolver,
+                new ContentValues(2),
+                Settings.Secure.getUriFor(settingName),
+                settingName,
+                restoreSettingValue,
+                Build.VERSION.SDK_INT);
+
+        Intent intentReceived = futureIntent.get();
+        assertThat(intentReceived.getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE))
+                .isEqualTo(restoreSettingValue);
+        assertThat(intentReceived.getIntExtra(
+                Intent.EXTRA_SETTING_RESTORED_FROM_SDK_INT, /* defaultValue= */ 0))
+                .isEqualTo(Build.VERSION.SDK_INT);
+    }
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
index 621ddf7..1da6c1e 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
@@ -53,6 +53,7 @@
 import androidx.compose.material3.Text
 import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
@@ -71,6 +72,7 @@
 import androidx.compose.ui.unit.DpOffset
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
 import androidx.compose.ui.unit.times
 import com.android.compose.PlatformButton
 import com.android.compose.animation.scene.ElementKey
@@ -84,7 +86,9 @@
 import com.android.systemui.bouncer.ui.BouncerDialogFactory
 import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout
 import com.android.systemui.bouncer.ui.viewmodel.AuthMethodBouncerViewModel
+import com.android.systemui.bouncer.ui.viewmodel.BouncerMessageViewModel
 import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
+import com.android.systemui.bouncer.ui.viewmodel.MessageViewModel
 import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel
 import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel
 import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel
@@ -166,7 +170,7 @@
                 modifier = Modifier.fillMaxWidth(),
             ) {
                 StatusMessage(
-                    viewModel = viewModel,
+                    viewModel = viewModel.message,
                     modifier = Modifier,
                 )
 
@@ -228,7 +232,7 @@
             when (authMethod) {
                 is PinBouncerViewModel -> {
                     StatusMessage(
-                        viewModel = viewModel,
+                        viewModel = viewModel.message,
                         modifier = Modifier.align(Alignment.TopCenter),
                     )
 
@@ -241,7 +245,7 @@
                 }
                 is PatternBouncerViewModel -> {
                     StatusMessage(
-                        viewModel = viewModel,
+                        viewModel = viewModel.message,
                         modifier = Modifier.align(Alignment.TopCenter),
                     )
 
@@ -280,7 +284,7 @@
                         modifier = Modifier.fillMaxWidth().align(Alignment.Center),
                     ) {
                         StatusMessage(
-                            viewModel = viewModel,
+                            viewModel = viewModel.message,
                         )
 
                         OutputArea(viewModel = viewModel, modifier = Modifier.padding(top = 24.dp))
@@ -376,7 +380,7 @@
                     modifier = Modifier.fillMaxWidth()
                 ) {
                     StatusMessage(
-                        viewModel = viewModel,
+                        viewModel = viewModel.message,
                     )
 
                     OutputArea(viewModel = viewModel, modifier = Modifier.padding(top = 24.dp))
@@ -441,7 +445,7 @@
                 modifier = Modifier.fillMaxWidth(),
             ) {
                 StatusMessage(
-                    viewModel = viewModel,
+                    viewModel = viewModel.message,
                 )
 
                 OutputArea(viewModel = viewModel, modifier = Modifier.padding(top = 24.dp))
@@ -548,26 +552,44 @@
 
 @Composable
 private fun StatusMessage(
-    viewModel: BouncerViewModel,
+    viewModel: BouncerMessageViewModel,
     modifier: Modifier = Modifier,
 ) {
-    val message: BouncerViewModel.MessageViewModel by viewModel.message.collectAsState()
+    val message: MessageViewModel? by viewModel.message.collectAsState()
+
+    DisposableEffect(Unit) {
+        viewModel.onShown()
+        onDispose {}
+    }
 
     Crossfade(
         targetState = message,
         label = "Bouncer message",
-        animationSpec = if (message.isUpdateAnimated) tween() else snap(),
+        animationSpec = if (message?.isUpdateAnimated == true) tween() else snap(),
         modifier = modifier.fillMaxWidth(),
-    ) {
-        Box(
-            contentAlignment = Alignment.Center,
+    ) { msg ->
+        Column(
+            horizontalAlignment = Alignment.CenterHorizontally,
             modifier = Modifier.fillMaxWidth(),
         ) {
-            Text(
-                text = it.text,
-                color = MaterialTheme.colorScheme.onSurface,
-                style = MaterialTheme.typography.bodyLarge,
-            )
+            msg?.let {
+                Text(
+                    text = it.text,
+                    color = MaterialTheme.colorScheme.onSurface,
+                    fontSize = 18.sp,
+                    lineHeight = 24.sp,
+                    overflow = TextOverflow.Ellipsis,
+                )
+                Spacer(modifier = Modifier.size(10.dp))
+                Text(
+                    text = it.secondaryText ?: "",
+                    color = MaterialTheme.colorScheme.onSurface,
+                    fontSize = 14.sp,
+                    lineHeight = 20.sp,
+                    overflow = TextOverflow.Ellipsis,
+                    maxLines = 2
+                )
+            }
         }
     }
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt
index 2a13d49..c34f2fd 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt
@@ -74,10 +74,7 @@
     val isImeSwitcherButtonVisible by viewModel.isImeSwitcherButtonVisible.collectAsState()
     val selectedUserId by viewModel.selectedUserId.collectAsState()
 
-    DisposableEffect(Unit) {
-        viewModel.onShown()
-        onDispose { viewModel.onHidden() }
-    }
+    DisposableEffect(Unit) { onDispose { viewModel.onHidden() } }
 
     LaunchedEffect(animateFailure) {
         if (animateFailure) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
index 0a5f5d2..a78c2c0 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
@@ -72,10 +72,7 @@
     centerDotsVertically: Boolean,
     modifier: Modifier = Modifier,
 ) {
-    DisposableEffect(Unit) {
-        viewModel.onShown()
-        onDispose { viewModel.onHidden() }
-    }
+    DisposableEffect(Unit) { onDispose { viewModel.onHidden() } }
 
     val colCount = viewModel.columnCount
     val rowCount = viewModel.rowCount
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
index f505b90..5651a46 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
@@ -72,10 +72,7 @@
     verticalSpacing: Dp,
     modifier: Modifier = Modifier,
 ) {
-    DisposableEffect(Unit) {
-        viewModel.onShown()
-        onDispose { viewModel.onHidden() }
-    }
+    DisposableEffect(Unit) { onDispose { viewModel.onHidden() } }
 
     val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState()
     val backspaceButtonAppearance by viewModel.backspaceButtonAppearance.collectAsState()
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
index d780978..8ad2bb7 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
@@ -72,6 +72,7 @@
 import com.android.systemui.notifications.ui.composable.Notifications.TransitionThresholds.EXPANSION_FOR_MAX_SCRIM_ALPHA
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shade.ui.composable.ShadeHeader
+import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding
 import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackAppearanceViewBinder.SCRIM_CORNER_RADIUS
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
 import kotlin.math.roundToInt
@@ -156,6 +157,8 @@
 
     val contentHeight = viewModel.intrinsicContentHeight.collectAsState()
 
+    val stackRounding = viewModel.stackRounding.collectAsState()
+
     // the offset for the notifications scrim. Its upper bound is 0, and its lower bound is
     // calculated in minScrimOffset. The scrim is the same height as the screen minus the
     // height of the Shade Header, and at rest (scrimOffset = 0) its top bound is at maxScrimStartY.
@@ -226,12 +229,7 @@
                                 { expansionFraction },
                                 layoutState.isTransitioningBetween(Scenes.Gone, Scenes.Shade)
                             )
-                            .let {
-                                RoundedCornerShape(
-                                    topStart = it,
-                                    topEnd = it,
-                                )
-                            }
+                            .let { stackRounding.value.toRoundedCornerShape(it) }
                     clip = true
                 }
     ) {
@@ -367,7 +365,7 @@
         lerp(
                 start = screenCornerRadius.value,
                 stop = SCRIM_CORNER_RADIUS,
-                fraction = (expansionFraction() / EXPANSION_FOR_MAX_CORNER_RADIUS).coerceAtMost(1f),
+                fraction = (expansionFraction() / EXPANSION_FOR_MAX_CORNER_RADIUS).coerceIn(0f, 1f),
             )
             .dp
     } else {
@@ -394,5 +392,16 @@
         this
     }
 
+fun StackRounding.toRoundedCornerShape(radius: Dp): RoundedCornerShape {
+    val topRadius = if (roundTop) radius else 0.dp
+    val bottomRadius = if (roundBottom) radius else 0.dp
+    return RoundedCornerShape(
+        topStart = topRadius,
+        topEnd = topRadius,
+        bottomStart = bottomRadius,
+        bottomEnd = bottomRadius,
+    )
+}
+
 private const val TAG = "FlexiNotifs"
 private val DEBUG_COLOR = Color(1f, 0f, 0f, 0.2f)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
index bc48dd1..244861c 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
@@ -20,6 +20,7 @@
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
@@ -36,7 +37,8 @@
 import com.android.systemui.qs.ui.adapter.QSSceneAdapter
 import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.Companion.Collapsing
 import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.Expanding
-import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.Unsquishing
+import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.UnsquishingQQS
+import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.UnsquishingQS
 import com.android.systemui.scene.shared.model.Scenes
 
 object QuickSettings {
@@ -49,6 +51,8 @@
     object Elements {
         val Content =
             ElementKey("QuickSettingsContent", scenePicker = MovableElementScenePicker(SCENES))
+        val QuickQuickSettings = ElementKey("QuickQuickSettings")
+        val SplitShadeQuickSettings = ElementKey("SplitShadeQuickSettings")
         val FooterActions = ElementKey("QuickSettingsFooterActions")
     }
 
@@ -78,12 +82,16 @@
         is TransitionState.Transition ->
             with(transitionState) {
                 when {
-                    isSplitShade -> QSSceneAdapter.State.QS
-                    fromScene == Scenes.Shade && toScene == Scenes.QuickSettings ->
+                    isSplitShade -> UnsquishingQS(squishiness)
+                    fromScene == Scenes.Shade && toScene == Scenes.QuickSettings -> {
                         Expanding(progress)
-                    fromScene == Scenes.QuickSettings && toScene == Scenes.Shade ->
+                    }
+                    fromScene == Scenes.QuickSettings && toScene == Scenes.Shade -> {
                         Collapsing(progress)
-                    fromScene == Scenes.Shade || toScene == Scenes.Shade -> Unsquishing(squishiness)
+                    }
+                    fromScene == Scenes.Shade || toScene == Scenes.Shade -> {
+                        UnsquishingQQS(squishiness)
+                    }
                     fromScene == Scenes.QuickSettings || toScene == Scenes.QuickSettings -> {
                         QSSceneAdapter.State.QS
                     }
@@ -119,6 +127,18 @@
     squishiness: Float = QuickSettings.SharedValues.SquishinessValues.Default,
 ) {
     val contentState = stateForQuickSettingsContent(isSplitShade, squishiness)
+    val transitionState = layoutState.transitionState
+    val isClosing =
+        transitionState is TransitionState.Transition &&
+            transitionState.progress >= 0.9f && // almost done closing
+            !(layoutState.isTransitioning(to = Scenes.Shade) ||
+                layoutState.isTransitioning(to = Scenes.QuickSettings))
+
+    if (isClosing) {
+        DisposableEffect(Unit) {
+            onDispose { qsSceneAdapter.setState(QSSceneAdapter.State.CLOSED) }
+        }
+    }
 
     MovableElement(
         key = QuickSettings.Elements.Content,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt
index 5c6e1c8..9b59708 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt
@@ -13,11 +13,18 @@
 ) {
     spec = tween(durationMillis = DefaultDuration.times(durationScale).inWholeMilliseconds.toInt())
 
-    fractionRange(start = .58f) { fade(ShadeHeader.Elements.Clock) }
-    fractionRange(start = .58f) { fade(ShadeHeader.Elements.CollapsedContentStart) }
-    fractionRange(start = .58f) { fade(ShadeHeader.Elements.CollapsedContentEnd) }
-    fractionRange(start = .58f) { fade(ShadeHeader.Elements.PrivacyChip) }
-    translate(QuickSettings.Elements.Content, y = -ShadeHeader.Dimensions.CollapsedHeight * .66f)
+    fractionRange(start = .58f) {
+        fade(ShadeHeader.Elements.Clock)
+        fade(ShadeHeader.Elements.CollapsedContentStart)
+        fade(ShadeHeader.Elements.CollapsedContentEnd)
+        fade(ShadeHeader.Elements.PrivacyChip)
+        fade(QuickSettings.Elements.SplitShadeQuickSettings)
+        fade(QuickSettings.Elements.FooterActions)
+    }
+    translate(
+        QuickSettings.Elements.QuickQuickSettings,
+        y = -ShadeHeader.Dimensions.CollapsedHeight * .66f
+    )
     translate(Notifications.Elements.NotificationScrim, Edge.Top, false)
 }
 
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index 15e7b51..2c31f9b 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -55,6 +55,7 @@
 import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.LowestZIndexScenePicker
 import com.android.compose.animation.scene.SceneScope
+import com.android.compose.animation.scene.TransitionState
 import com.android.compose.animation.scene.UserAction
 import com.android.compose.animation.scene.UserActionResult
 import com.android.compose.animation.scene.animateSceneFloatAsState
@@ -222,15 +223,17 @@
                                         horizontal = Shade.Dimensions.HorizontalPadding
                                     )
                             )
-                            QuickSettings(
-                                viewModel.qsSceneAdapter,
-                                {
-                                    (viewModel.qsSceneAdapter.qqsHeight * tileSquishiness)
-                                        .roundToInt()
-                                },
-                                isSplitShade = false,
-                                squishiness = tileSquishiness,
-                            )
+                            Box(Modifier.element(QuickSettings.Elements.QuickQuickSettings)) {
+                                QuickSettings(
+                                    viewModel.qsSceneAdapter,
+                                    {
+                                        (viewModel.qsSceneAdapter.qqsHeight * tileSquishiness)
+                                            .roundToInt()
+                                    },
+                                    isSplitShade = false,
+                                    squishiness = tileSquishiness,
+                                )
+                            }
 
                             MediaIfVisible(
                                 viewModel = viewModel,
@@ -280,6 +283,8 @@
     val lifecycleOwner = LocalLifecycleOwner.current
     val footerActionsViewModel =
         remember(lifecycleOwner, viewModel) { viewModel.getFooterActionsViewModel(lifecycleOwner) }
+    val tileSquishiness by
+        animateSceneFloatAsState(value = 1f, key = QuickSettings.SharedValues.TilesSquishiness)
 
     val navBarBottomHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
     val density = LocalDensity.current
@@ -290,6 +295,7 @@
     }
 
     val quickSettingsScrollState = rememberScrollState()
+    val isScrollable = layoutState.transitionState is TransitionState.Idle
     LaunchedEffect(isCustomizing, quickSettingsScrollState) {
         if (isCustomizing) {
             quickSettingsScrollState.scrollTo(0)
@@ -318,31 +324,41 @@
                 Column(
                     verticalArrangement = Arrangement.Top,
                     modifier =
-                        Modifier.weight(1f).fillMaxHeight().thenIf(!isCustomizing) {
-                            Modifier.verticalNestedScrollToScene()
-                                .verticalScroll(quickSettingsScrollState)
-                                .clipScrollableContainer(Orientation.Horizontal)
-                                .padding(bottom = navBarBottomHeight)
-                        }
+                        Modifier.weight(1f).fillMaxSize().thenIf(!isCustomizing) {
+                            Modifier.padding(bottom = navBarBottomHeight)
+                        },
                 ) {
-                    QuickSettings(
-                        qsSceneAdapter = viewModel.qsSceneAdapter,
-                        heightProvider = { viewModel.qsSceneAdapter.qsHeight },
-                        isSplitShade = true,
-                        modifier = Modifier.fillMaxWidth(),
-                    )
+                    Column(
+                        modifier =
+                            Modifier.fillMaxSize().weight(1f).thenIf(!isCustomizing) {
+                                Modifier.verticalNestedScrollToScene()
+                                    .verticalScroll(
+                                        quickSettingsScrollState,
+                                        enabled = isScrollable
+                                    )
+                                    .clipScrollableContainer(Orientation.Horizontal)
+                            }
+                    ) {
+                        Box(
+                            modifier =
+                                Modifier.element(QuickSettings.Elements.SplitShadeQuickSettings)
+                        ) {
+                            QuickSettings(
+                                qsSceneAdapter = viewModel.qsSceneAdapter,
+                                heightProvider = { viewModel.qsSceneAdapter.qsHeight },
+                                isSplitShade = true,
+                                modifier = Modifier.fillMaxWidth(),
+                                squishiness = tileSquishiness,
+                            )
+                        }
 
-                    MediaIfVisible(
-                        viewModel = viewModel,
-                        mediaCarouselController = mediaCarouselController,
-                        mediaHost = mediaHost,
-                        modifier = Modifier.fillMaxWidth(),
-                    )
-
-                    Spacer(
-                        modifier = Modifier.weight(1f),
-                    )
-
+                        MediaIfVisible(
+                            viewModel = viewModel,
+                            mediaCarouselController = mediaCarouselController,
+                            mediaHost = mediaHost,
+                            modifier = Modifier.fillMaxWidth(),
+                        )
+                    }
                     FooterActionsWithAnimatedVisibility(
                         viewModel = footerActionsViewModel,
                         isCustomizing = isCustomizing,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
index af51cee..dc3b612 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
@@ -73,7 +73,7 @@
 internal class SceneScopeImpl(
     private val layoutImpl: SceneTransitionLayoutImpl,
     private val scene: Scene,
-) : SceneScope {
+) : SceneScope, ElementStateScope by layoutImpl.elementStateScope {
     override val layoutState: SceneTransitionLayoutState = layoutImpl.state
 
     override fun Modifier.element(key: ElementKey): Modifier {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index b7e2dd1..ebc9099 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -131,9 +131,30 @@
  */
 @DslMarker annotation class ElementDsl
 
+/** A scope that can be used to query the target state of an element or scene. */
+interface ElementStateScope {
+    /**
+     * Return the *target* size of [this] element in the given [scene], i.e. the size of the element
+     * when idle, or `null` if the element is not composed and measured in that scene (yet).
+     */
+    fun ElementKey.targetSize(scene: SceneKey): IntSize?
+
+    /**
+     * Return the *target* offset of [this] element in the given [scene], i.e. the size of the
+     * element when idle, or `null` if the element is not composed and placed in that scene (yet).
+     */
+    fun ElementKey.targetOffset(scene: SceneKey): Offset?
+
+    /**
+     * Return the *target* size of [this] scene, i.e. the size of the scene when idle, or `null` if
+     * the scene was never composed.
+     */
+    fun SceneKey.targetSize(): IntSize?
+}
+
 @Stable
 @ElementDsl
-interface BaseSceneScope {
+interface BaseSceneScope : ElementStateScope {
     /** The state of the [SceneTransitionLayout] in which this scene is contained. */
     val layoutState: SceneTransitionLayoutState
 
@@ -415,25 +436,7 @@
     ): Float
 }
 
-interface UserActionDistanceScope : Density {
-    /**
-     * Return the *target* size of [this] element in the given [scene], i.e. the size of the element
-     * when idle, or `null` if the element is not composed and measured in that scene (yet).
-     */
-    fun ElementKey.targetSize(scene: SceneKey): IntSize?
-
-    /**
-     * Return the *target* offset of [this] element in the given [scene], i.e. the size of the
-     * element when idle, or `null` if the element is not composed and placed in that scene (yet).
-     */
-    fun ElementKey.targetOffset(scene: SceneKey): Offset?
-
-    /**
-     * Return the *target* size of [this] scene, i.e. the size of the scene when idle, or `null` if
-     * the scene was never composed.
-     */
-    fun SceneKey.targetSize(): IntSize?
-}
+interface UserActionDistanceScope : Density, ElementStateScope
 
 /** The user action has a fixed [absoluteDistance]. */
 class FixedDistance(private val distance: Dp) : UserActionDistance {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index 25b0895..b1cfdcf 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -98,6 +98,7 @@
     private val horizontalDraggableHandler: DraggableHandlerImpl
     private val verticalDraggableHandler: DraggableHandlerImpl
 
+    internal val elementStateScope = ElementStateScopeImpl(this)
     private var _userActionDistanceScope: UserActionDistanceScope? = null
     internal val userActionDistanceScope: UserActionDistanceScope
         get() =
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt
index 228d19f..b7abb33 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt
@@ -19,15 +19,9 @@
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.unit.IntSize
 
-internal class UserActionDistanceScopeImpl(
+internal class ElementStateScopeImpl(
     private val layoutImpl: SceneTransitionLayoutImpl,
-) : UserActionDistanceScope {
-    override val density: Float
-        get() = layoutImpl.density.density
-
-    override val fontScale: Float
-        get() = layoutImpl.density.fontScale
-
+) : ElementStateScope {
     override fun ElementKey.targetSize(scene: SceneKey): IntSize? {
         return layoutImpl.elements[this]?.sceneStates?.get(scene)?.targetSize.takeIf {
             it != Element.SizeUnspecified
@@ -44,3 +38,13 @@
         return layoutImpl.scenes[this]?.targetSize.takeIf { it != IntSize.Zero }
     }
 }
+
+internal class UserActionDistanceScopeImpl(
+    private val layoutImpl: SceneTransitionLayoutImpl,
+) : UserActionDistanceScope, ElementStateScope by layoutImpl.elementStateScope {
+    override val density: Float
+        get() = layoutImpl.density.density
+
+    override val fontScale: Float
+        get() = layoutImpl.density.fontScale
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
index 707777b..b0d03b1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
@@ -71,34 +71,6 @@
     }
 
     @Test
-    fun pinAuthMethod() =
-        testScope.runTest {
-            val message by collectLastValue(underTest.message)
-
-            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
-                AuthenticationMethodModel.Pin
-            )
-            runCurrent()
-            underTest.clearMessage()
-            assertThat(message).isNull()
-
-            underTest.resetMessage()
-            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
-
-            // Wrong input.
-            assertThat(underTest.authenticate(listOf(9, 8, 7)))
-                .isEqualTo(AuthenticationResult.FAILED)
-            assertThat(message).isEqualTo(MESSAGE_WRONG_PIN)
-
-            underTest.resetMessage()
-            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
-
-            // Correct input.
-            assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
-                .isEqualTo(AuthenticationResult.SUCCEEDED)
-        }
-
-    @Test
     fun pinAuthMethod_sim_skipsAuthentication() =
         testScope.runTest {
             kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
@@ -146,8 +118,6 @@
     @Test
     fun pinAuthMethod_tryAutoConfirm_withoutAutoConfirmPin() =
         testScope.runTest {
-            val message by collectLastValue(underTest.message)
-
             kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
                 AuthenticationMethodModel.Pin
             )
@@ -156,7 +126,6 @@
             // Incomplete input.
             assertThat(underTest.authenticate(listOf(1, 2), tryAutoConfirm = true))
                 .isEqualTo(AuthenticationResult.SKIPPED)
-            assertThat(message).isNull()
 
             // Correct input.
             assertThat(
@@ -166,28 +135,19 @@
                     )
                 )
                 .isEqualTo(AuthenticationResult.SKIPPED)
-            assertThat(message).isNull()
         }
 
     @Test
     fun passwordAuthMethod() =
         testScope.runTest {
-            val message by collectLastValue(underTest.message)
             kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
                 AuthenticationMethodModel.Password
             )
             runCurrent()
 
-            underTest.resetMessage()
-            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
-
             // Wrong input.
             assertThat(underTest.authenticate("alohamora".toList()))
                 .isEqualTo(AuthenticationResult.FAILED)
-            assertThat(message).isEqualTo(MESSAGE_WRONG_PASSWORD)
-
-            underTest.resetMessage()
-            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
 
             // Too short input.
             assertThat(
@@ -201,7 +161,6 @@
                     )
                 )
                 .isEqualTo(AuthenticationResult.SKIPPED)
-            assertThat(message).isEqualTo(MESSAGE_WRONG_PASSWORD)
 
             // Correct input.
             assertThat(underTest.authenticate("password".toList()))
@@ -211,13 +170,10 @@
     @Test
     fun patternAuthMethod() =
         testScope.runTest {
-            val message by collectLastValue(underTest.message)
             kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
                 AuthenticationMethodModel.Pattern
             )
             runCurrent()
-            underTest.resetMessage()
-            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
 
             // Wrong input.
             val wrongPattern =
@@ -231,10 +187,6 @@
             assertThat(wrongPattern.size)
                 .isAtLeast(kosmos.fakeAuthenticationRepository.minPatternLength)
             assertThat(underTest.authenticate(wrongPattern)).isEqualTo(AuthenticationResult.FAILED)
-            assertThat(message).isEqualTo(MESSAGE_WRONG_PATTERN)
-
-            underTest.resetMessage()
-            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
 
             // Too short input.
             val tooShortPattern =
@@ -244,10 +196,6 @@
                 )
             assertThat(underTest.authenticate(tooShortPattern))
                 .isEqualTo(AuthenticationResult.SKIPPED)
-            assertThat(message).isEqualTo(MESSAGE_WRONG_PATTERN)
-
-            underTest.resetMessage()
-            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
 
             // Correct input.
             assertThat(underTest.authenticate(FakeAuthenticationRepository.PATTERN))
@@ -258,7 +206,6 @@
     fun lockoutStarted() =
         testScope.runTest {
             val lockoutStartedEvents by collectValues(underTest.onLockoutStarted)
-            val message by collectLastValue(underTest.message)
 
             kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
                 AuthenticationMethodModel.Pin
@@ -272,17 +219,14 @@
                     .isEqualTo(AuthenticationResult.FAILED)
                 if (times < FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) {
                     assertThat(lockoutStartedEvents).isEmpty()
-                    assertThat(message).isNotEmpty()
                 }
             }
             assertThat(authenticationInteractor.lockoutEndTimestamp).isNotNull()
             assertThat(lockoutStartedEvents.size).isEqualTo(1)
-            assertThat(message).isNull()
 
             // Advance the time to finish the lockout:
             advanceTimeBy(FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS.seconds)
             assertThat(authenticationInteractor.lockoutEndTimestamp).isNull()
-            assertThat(message).isNull()
             assertThat(lockoutStartedEvents.size).isEqualTo(1)
 
             // Trigger lockout again:
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt
similarity index 83%
rename from packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt
index 701b703..c878e0b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.bouncer.domain.interactor
 
 import android.content.pm.UserInfo
-import android.os.Handler
 import android.testing.TestableLooper
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
@@ -28,27 +27,25 @@
 import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.biometrics.data.repository.FaceSensorInfo
-import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository
+import com.android.systemui.biometrics.data.repository.fakeFacePropertyRepository
+import com.android.systemui.biometrics.data.repository.fakeFingerprintPropertyRepository
 import com.android.systemui.biometrics.shared.model.SensorStrength
 import com.android.systemui.bouncer.data.repository.BouncerMessageRepositoryImpl
-import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
+import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
 import com.android.systemui.bouncer.shared.model.BouncerMessageModel
-import com.android.systemui.bouncer.ui.BouncerView
-import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryFingerprintAuthInteractor
 import com.android.systemui.flags.SystemPropertiesHelper
-import com.android.systemui.keyguard.DismissCallbackRegistry
-import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
-import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository
-import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
-import com.android.systemui.keyguard.data.repository.FakeTrustRepository
+import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeTrustRepository
 import com.android.systemui.keyguard.shared.model.AuthenticationFlags
+import com.android.systemui.kosmos.testScope
 import com.android.systemui.res.R.string.kg_too_many_failed_attempts_countdown
 import com.android.systemui.res.R.string.kg_trust_agent_disabled
-import com.android.systemui.statusbar.policy.KeyguardStateController
-import com.android.systemui.user.data.repository.FakeUserRepository
-import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.fakeUserRepository
 import com.android.systemui.util.mockito.KotlinArgumentCaptor
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
@@ -61,7 +58,6 @@
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.eq
 import org.mockito.Mock
-import org.mockito.Mockito.mock
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
@@ -70,34 +66,22 @@
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 @RunWith(AndroidJUnit4::class)
 class BouncerMessageInteractorTest : SysuiTestCase() {
-
+    private val kosmos = testKosmos()
     private val countDownTimerCallback = KotlinArgumentCaptor(CountDownTimerCallback::class.java)
     private val repository = BouncerMessageRepositoryImpl()
-    private val userRepository = FakeUserRepository()
-    private val fakeTrustRepository = FakeTrustRepository()
-    private val fakeFacePropertyRepository = FakeFacePropertyRepository()
-    private val bouncerRepository = FakeKeyguardBouncerRepository()
-    private val fakeDeviceEntryFingerprintAuthRepository =
-        FakeDeviceEntryFingerprintAuthRepository()
-    private val fakeDeviceEntryFaceAuthRepository = FakeDeviceEntryFaceAuthRepository()
-    private val biometricSettingsRepository: FakeBiometricSettingsRepository =
-        FakeBiometricSettingsRepository()
+    private val biometricSettingsRepository = kosmos.fakeBiometricSettingsRepository
+    private val testScope = kosmos.testScope
     @Mock private lateinit var updateMonitor: KeyguardUpdateMonitor
     @Mock private lateinit var securityModel: KeyguardSecurityModel
     @Mock private lateinit var countDownTimerUtil: CountDownTimerUtil
     @Mock private lateinit var systemPropertiesHelper: SystemPropertiesHelper
-    @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
-    @Mock private lateinit var mSelectedUserInteractor: SelectedUserInteractor
 
-    private lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor
-    private lateinit var testScope: TestScope
     private lateinit var underTest: BouncerMessageInteractor
 
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
-        userRepository.setUserInfos(listOf(PRIMARY_USER))
-        testScope = TestScope()
+        kosmos.fakeUserRepository.setUserInfos(listOf(PRIMARY_USER))
         allowTestableLooperAsMainThread()
         whenever(securityModel.getSecurityMode(PRIMARY_USER_ID)).thenReturn(PIN)
         biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
@@ -105,44 +89,28 @@
     }
 
     suspend fun TestScope.init() {
-        userRepository.setSelectedUserInfo(PRIMARY_USER)
+        kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
         mSetFlagsRule.enableFlags(Flags.FLAG_REVAMPED_BOUNCER_MESSAGES)
-        primaryBouncerInteractor =
-            PrimaryBouncerInteractor(
-                bouncerRepository,
-                mock(BouncerView::class.java),
-                mock(Handler::class.java),
-                mock(KeyguardStateController::class.java),
-                mock(KeyguardSecurityModel::class.java),
-                mock(PrimaryBouncerCallbackInteractor::class.java),
-                mock(FalsingCollector::class.java),
-                mock(DismissCallbackRegistry::class.java),
-                context,
-                keyguardUpdateMonitor,
-                fakeTrustRepository,
-                testScope.backgroundScope,
-                mSelectedUserInteractor,
-                mock(DeviceEntryFaceAuthInteractor::class.java),
-            )
         underTest =
             BouncerMessageInteractor(
                 repository = repository,
-                userRepository = userRepository,
+                userRepository = kosmos.fakeUserRepository,
                 countDownTimerUtil = countDownTimerUtil,
                 updateMonitor = updateMonitor,
                 biometricSettingsRepository = biometricSettingsRepository,
-                applicationScope = this.backgroundScope,
-                trustRepository = fakeTrustRepository,
+                applicationScope = testScope.backgroundScope,
+                trustRepository = kosmos.fakeTrustRepository,
                 systemPropertiesHelper = systemPropertiesHelper,
-                primaryBouncerInteractor = primaryBouncerInteractor,
-                facePropertyRepository = fakeFacePropertyRepository,
-                deviceEntryFingerprintAuthRepository = fakeDeviceEntryFingerprintAuthRepository,
-                faceAuthRepository = fakeDeviceEntryFaceAuthRepository,
+                primaryBouncerInteractor = kosmos.primaryBouncerInteractor,
+                facePropertyRepository = kosmos.fakeFacePropertyRepository,
+                deviceEntryFingerprintAuthInteractor = kosmos.deviceEntryFingerprintAuthInteractor,
+                faceAuthRepository = kosmos.fakeDeviceEntryFaceAuthRepository,
                 securityModel = securityModel
             )
         biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
-        fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false)
-        bouncerRepository.setPrimaryShow(true)
+        kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false)
+        kosmos.fakeFingerprintPropertyRepository.supportsSideFps()
+        kosmos.fakeKeyguardBouncerRepository.setPrimaryShow(true)
         runCurrent()
     }
 
@@ -268,7 +236,7 @@
             init()
             val lockoutMessage by collectLastValue(underTest.bouncerMessage)
 
-            fakeDeviceEntryFaceAuthRepository.setLockedOut(true)
+            kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(true)
             runCurrent()
 
             assertThat(primaryResMessage(lockoutMessage))
@@ -276,7 +244,7 @@
             assertThat(secondaryResMessage(lockoutMessage))
                 .isEqualTo("Can’t unlock with face. Too many attempts.")
 
-            fakeDeviceEntryFaceAuthRepository.setLockedOut(false)
+            kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(false)
             runCurrent()
 
             assertThat(primaryResMessage(lockoutMessage))
@@ -289,15 +257,17 @@
         testScope.runTest {
             init()
             val lockoutMessage by collectLastValue(underTest.bouncerMessage)
-            fakeFacePropertyRepository.setSensorInfo(FaceSensorInfo(1, SensorStrength.STRONG))
-            fakeDeviceEntryFaceAuthRepository.setLockedOut(true)
+            kosmos.fakeFacePropertyRepository.setSensorInfo(
+                FaceSensorInfo(1, SensorStrength.STRONG)
+            )
+            kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(true)
             runCurrent()
 
             assertThat(primaryResMessage(lockoutMessage)).isEqualTo("Enter PIN")
             assertThat(secondaryResMessage(lockoutMessage))
                 .isEqualTo("PIN is required after too many attempts")
 
-            fakeDeviceEntryFaceAuthRepository.setLockedOut(false)
+            kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(false)
             runCurrent()
 
             assertThat(primaryResMessage(lockoutMessage))
@@ -311,14 +281,14 @@
             init()
             val lockedOutMessage by collectLastValue(underTest.bouncerMessage)
 
-            fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true)
+            kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true)
             runCurrent()
 
             assertThat(primaryResMessage(lockedOutMessage)).isEqualTo("Enter PIN")
             assertThat(secondaryResMessage(lockedOutMessage))
                 .isEqualTo("PIN is required after too many attempts")
 
-            fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false)
+            kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false)
             runCurrent()
 
             assertThat(primaryResMessage(lockedOutMessage))
@@ -327,6 +297,19 @@
         }
 
     @Test
+    fun onUdfpsFingerprint_DoesNotShowFingerprintMessage() =
+        testScope.runTest {
+            init()
+            kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false)
+            kosmos.fakeFingerprintPropertyRepository.supportsUdfps()
+            val lockedOutMessage by collectLastValue(underTest.bouncerMessage)
+
+            runCurrent()
+
+            assertThat(primaryResMessage(lockedOutMessage)).isEqualTo("Enter PIN")
+        }
+
+    @Test
     fun onRestartForMainlineUpdate_shouldProvideRelevantMessage() =
         testScope.runTest {
             init()
@@ -344,9 +327,10 @@
     fun onAuthFlagsChanged_withTrustNotManagedAndNoBiometrics_isANoop() =
         testScope.runTest {
             init()
-            fakeTrustRepository.setTrustUsuallyManaged(false)
+            kosmos.fakeTrustRepository.setTrustUsuallyManaged(false)
             biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false)
             biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
+            runCurrent()
 
             val defaultMessage = Pair("Enter PIN", null)
 
@@ -377,12 +361,13 @@
         testScope.runTest {
             init()
 
-            userRepository.setSelectedUserInfo(PRIMARY_USER)
+            kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
             biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false)
             biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
+            runCurrent()
 
-            fakeTrustRepository.setCurrentUserTrustManaged(true)
-            fakeTrustRepository.setTrustUsuallyManaged(true)
+            kosmos.fakeTrustRepository.setCurrentUserTrustManaged(true)
+            kosmos.fakeTrustRepository.setTrustUsuallyManaged(true)
 
             val defaultMessage = Pair("Enter PIN", null)
 
@@ -415,8 +400,8 @@
     fun authFlagsChanges_withFaceEnrolled_providesDifferentMessages() =
         testScope.runTest {
             init()
-            userRepository.setSelectedUserInfo(PRIMARY_USER)
-            fakeTrustRepository.setTrustUsuallyManaged(false)
+            kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+            kosmos.fakeTrustRepository.setTrustUsuallyManaged(false)
             biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
 
             biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
@@ -453,12 +438,13 @@
     fun authFlagsChanges_withFingerprintEnrolled_providesDifferentMessages() =
         testScope.runTest {
             init()
-            userRepository.setSelectedUserInfo(PRIMARY_USER)
-            fakeTrustRepository.setCurrentUserTrustManaged(false)
+            kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+            kosmos.fakeTrustRepository.setCurrentUserTrustManaged(false)
             biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false)
 
             biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
             biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
+            runCurrent()
 
             verifyMessagesForAuthFlag(
                 LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED to
@@ -466,6 +452,7 @@
             )
 
             biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(false)
+            runCurrent()
 
             verifyMessagesForAuthFlag(
                 LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
index d30e333..c9fa671 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
@@ -48,6 +48,7 @@
             isInputEnabled = MutableStateFlow(true),
             simBouncerInteractor = kosmos.simBouncerInteractor,
             authenticationMethod = AuthenticationMethodModel.Pin,
+            onIntentionalUserInput = {},
         )
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt
new file mode 100644
index 0000000..16ec9aa
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt
@@ -0,0 +1,455 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.ui.viewmodel
+
+import android.content.pm.UserInfo
+import android.hardware.biometrics.BiometricFaceConstants
+import android.hardware.fingerprint.FingerprintManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.widget.LockPatternUtils
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
+import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
+import com.android.systemui.authentication.domain.interactor.authenticationInteractor
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pattern
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pin
+import com.android.systemui.biometrics.data.repository.FaceSensorInfo
+import com.android.systemui.biometrics.data.repository.fakeFacePropertyRepository
+import com.android.systemui.biometrics.data.repository.fakeFingerprintPropertyRepository
+import com.android.systemui.biometrics.shared.model.SensorStrength
+import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
+import com.android.systemui.bouncer.shared.flag.fakeComposeBouncerFlags
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
+import com.android.systemui.deviceentry.shared.model.ErrorFaceAuthenticationStatus
+import com.android.systemui.deviceentry.shared.model.FailedFaceAuthenticationStatus
+import com.android.systemui.deviceentry.shared.model.HelpFaceAuthenticationStatus
+import com.android.systemui.flags.fakeSystemPropertiesHelper
+import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeTrustRepository
+import com.android.systemui.keyguard.shared.model.AuthenticationFlags
+import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.HelpFingerprintAuthenticationStatus
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.res.R
+import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.google.common.truth.Truth.assertThat
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.currentTime
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class BouncerMessageViewModelTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val authenticationInteractor by lazy { kosmos.authenticationInteractor }
+    private val bouncerInteractor by lazy { kosmos.bouncerInteractor }
+    private lateinit var underTest: BouncerMessageViewModel
+
+    @Before
+    fun setUp() {
+        kosmos.fakeUserRepository.setUserInfos(listOf(PRIMARY_USER))
+        kosmos.fakeComposeBouncerFlags.composeBouncerEnabled = true
+        underTest = kosmos.bouncerMessageViewModel
+        overrideResource(R.string.kg_trust_agent_disabled, "Trust agent is unavailable")
+        kosmos.fakeSystemPropertiesHelper.set(
+            DeviceEntryInteractor.SYS_BOOT_REASON_PROP,
+            "not mainline reboot"
+        )
+    }
+
+    @Test
+    fun message_defaultMessage_basedOnAuthMethod() =
+        testScope.runTest {
+            val message by collectLastValue(underTest.message)
+
+            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin)
+            kosmos.fakeFingerprintPropertyRepository.supportsSideFps()
+            kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+            kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
+            runCurrent()
+
+            assertThat(message!!.text).isEqualTo("Unlock with PIN or fingerprint")
+
+            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pattern)
+            runCurrent()
+            assertThat(message!!.text).isEqualTo("Unlock with pattern or fingerprint")
+
+            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Password
+            )
+            runCurrent()
+            assertThat(message!!.text).isEqualTo("Unlock with password or fingerprint")
+        }
+
+    @Test
+    fun message() =
+        testScope.runTest {
+            val message by collectLastValue(underTest.message)
+            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin)
+            assertThat(message?.isUpdateAnimated).isTrue()
+
+            repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) {
+                bouncerInteractor.authenticate(WRONG_PIN)
+            }
+            assertThat(message?.isUpdateAnimated).isFalse()
+
+            val lockoutEndMs = authenticationInteractor.lockoutEndTimestamp ?: 0
+            advanceTimeBy(lockoutEndMs - testScope.currentTime)
+            assertThat(message?.isUpdateAnimated).isTrue()
+        }
+
+    @Test
+    fun lockoutMessage() =
+        testScope.runTest {
+            val message by collectLastValue(underTest.message)
+            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin)
+            assertThat(kosmos.fakeAuthenticationRepository.lockoutEndTimestamp).isNull()
+            runCurrent()
+
+            repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) { times ->
+                bouncerInteractor.authenticate(WRONG_PIN)
+                runCurrent()
+                if (times < FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) {
+                    assertThat(message?.text).isEqualTo("Wrong PIN. Try again.")
+                    assertThat(message?.isUpdateAnimated).isTrue()
+                }
+            }
+            val lockoutSeconds = FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS
+            assertTryAgainMessage(message?.text, lockoutSeconds)
+            assertThat(message?.isUpdateAnimated).isFalse()
+
+            repeat(FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS) { time ->
+                advanceTimeBy(1.seconds)
+                val remainingSeconds = lockoutSeconds - time - 1
+                if (remainingSeconds > 0) {
+                    assertTryAgainMessage(message?.text, remainingSeconds)
+                }
+            }
+            assertThat(message?.text).isEqualTo("Enter PIN")
+            assertThat(message?.isUpdateAnimated).isTrue()
+        }
+
+    @Test
+    fun defaultMessage_mapsToDeviceEntryRestrictionReason_whenTrustAgentIsEnabled() =
+        testScope.runTest {
+            kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+            kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false)
+            kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
+            kosmos.fakeTrustRepository.setTrustUsuallyManaged(true)
+            kosmos.fakeTrustRepository.setCurrentUserTrustManaged(false)
+            runCurrent()
+
+            val defaultMessage = Pair("Enter PIN", null)
+
+            verifyMessagesForAuthFlags(
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED to defaultMessage,
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to
+                    Pair("Enter PIN", "PIN is required after device restarts"),
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to
+                    Pair("Enter PIN", "Added security required. PIN not used for a while."),
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to
+                    Pair("Enter PIN", "For added security, device was locked by work policy"),
+                LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to
+                    Pair("Enter PIN", "Trust agent is unavailable"),
+                LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to
+                    Pair("Enter PIN", "Trust agent is unavailable"),
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to
+                    Pair("Enter PIN", "PIN is required after lockdown"),
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to
+                    Pair("Enter PIN", "PIN required for additional security"),
+                LockPatternUtils.StrongAuthTracker
+                    .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to
+                    Pair(
+                        "Enter PIN",
+                        "Added security required. Device wasn’t unlocked for a while."
+                    ),
+            )
+        }
+
+    @Test
+    fun defaultMessage_mapsToDeviceEntryRestrictionReason_whenFingerprintIsAvailable() =
+        testScope.runTest {
+            kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+            kosmos.fakeFingerprintPropertyRepository.supportsSideFps()
+            kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+            kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
+            kosmos.fakeTrustRepository.setCurrentUserTrustManaged(false)
+            kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false)
+            runCurrent()
+
+            verifyMessagesForAuthFlags(
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED to
+                    Pair("Unlock with PIN or fingerprint", null),
+                LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to
+                    Pair("Unlock with PIN or fingerprint", null),
+                LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to
+                    Pair("Unlock with PIN or fingerprint", null),
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to
+                    Pair("Enter PIN", "PIN is required after device restarts"),
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to
+                    Pair("Enter PIN", "Added security required. PIN not used for a while."),
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to
+                    Pair("Enter PIN", "For added security, device was locked by work policy"),
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to
+                    Pair("Enter PIN", "PIN is required after lockdown"),
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to
+                    Pair("Enter PIN", "PIN required for additional security"),
+                LockPatternUtils.StrongAuthTracker
+                    .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to
+                    Pair(
+                        "Unlock with PIN or fingerprint",
+                        "Added security required. Device wasn’t unlocked for a while."
+                    ),
+            )
+        }
+
+    @Test
+    fun onFingerprintLockout_messageUpdated() =
+        testScope.runTest {
+            kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+            kosmos.fakeFingerprintPropertyRepository.supportsSideFps()
+            kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+            kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
+
+            val lockedOutMessage by collectLastValue(underTest.message)
+
+            kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true)
+            runCurrent()
+
+            assertThat(lockedOutMessage?.text).isEqualTo("Enter PIN")
+            assertThat(lockedOutMessage?.secondaryText)
+                .isEqualTo("PIN is required after too many attempts")
+
+            kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false)
+            runCurrent()
+
+            assertThat(lockedOutMessage?.text).isEqualTo("Unlock with PIN or fingerprint")
+            assertThat(lockedOutMessage?.secondaryText.isNullOrBlank()).isTrue()
+        }
+
+    @Test
+    fun onUdfpsFingerprint_DoesNotShowFingerprintMessage() =
+        testScope.runTest {
+            kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+            kosmos.fakeFingerprintPropertyRepository.supportsUdfps()
+            kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+            kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
+            kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false)
+            val message by collectLastValue(underTest.message)
+
+            runCurrent()
+
+            assertThat(message?.text).isEqualTo("Enter PIN")
+        }
+
+    @Test
+    fun onRestartForMainlineUpdate_shouldProvideRelevantMessage() =
+        testScope.runTest {
+            kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+            kosmos.fakeSystemPropertiesHelper.set("sys.boot.reason.last", "reboot,mainline_update")
+            kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+            runCurrent()
+
+            verifyMessagesForAuthFlags(
+                LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to
+                    Pair("Enter PIN", "Device updated. Enter PIN to continue.")
+            )
+        }
+
+    @Test
+    fun onFaceLockout_whenItIsClass3_shouldProvideRelevantMessage() =
+        testScope.runTest {
+            kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+            kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
+            val lockoutMessage by collectLastValue(underTest.message)
+            kosmos.fakeFacePropertyRepository.setSensorInfo(
+                FaceSensorInfo(1, SensorStrength.STRONG)
+            )
+            kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(true)
+            runCurrent()
+
+            assertThat(lockoutMessage?.text).isEqualTo("Enter PIN")
+            assertThat(lockoutMessage?.secondaryText)
+                .isEqualTo("PIN is required after too many attempts")
+
+            kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(false)
+            runCurrent()
+
+            assertThat(lockoutMessage?.text).isEqualTo("Enter PIN")
+            assertThat(lockoutMessage?.secondaryText.isNullOrBlank()).isTrue()
+        }
+
+    @Test
+    fun onFaceLockout_whenItIsNotStrong_shouldProvideRelevantMessage() =
+        testScope.runTest {
+            kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+            kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
+            val lockoutMessage by collectLastValue(underTest.message)
+            kosmos.fakeFacePropertyRepository.setSensorInfo(FaceSensorInfo(1, SensorStrength.WEAK))
+            kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(true)
+            runCurrent()
+
+            assertThat(lockoutMessage?.text).isEqualTo("Enter PIN")
+            assertThat(lockoutMessage?.secondaryText)
+                .isEqualTo("Can’t unlock with face. Too many attempts.")
+
+            kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(false)
+            runCurrent()
+
+            assertThat(lockoutMessage?.text).isEqualTo("Enter PIN")
+            assertThat(lockoutMessage?.secondaryText.isNullOrBlank()).isTrue()
+        }
+
+    @Test
+    fun setFingerprintMessage_propagateValue() =
+        testScope.runTest {
+            val bouncerMessage by collectLastValue(underTest.message)
+
+            kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+            kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+            kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
+            kosmos.fakeFingerprintPropertyRepository.supportsSideFps()
+            runCurrent()
+
+            kosmos.deviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+                HelpFingerprintAuthenticationStatus(1, "some helpful message")
+            )
+            runCurrent()
+            assertThat(bouncerMessage?.text).isEqualTo("Unlock with PIN or fingerprint")
+            assertThat(bouncerMessage?.secondaryText).isEqualTo("some helpful message")
+
+            kosmos.deviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+                FailFingerprintAuthenticationStatus
+            )
+            runCurrent()
+            assertThat(bouncerMessage?.text).isEqualTo("Fingerprint not recognized")
+            assertThat(bouncerMessage?.secondaryText).isEqualTo("Try again or enter PIN")
+
+            kosmos.deviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+                ErrorFingerprintAuthenticationStatus(
+                    FingerprintManager.FINGERPRINT_ERROR_LOCKOUT,
+                    "locked out"
+                )
+            )
+            runCurrent()
+            assertThat(bouncerMessage?.text).isEqualTo("Enter PIN")
+            assertThat(bouncerMessage?.secondaryText)
+                .isEqualTo("PIN is required after too many attempts")
+        }
+
+    @Test
+    fun setFaceMessage_propagateValue() =
+        testScope.runTest {
+            val bouncerMessage by collectLastValue(underTest.message)
+
+            kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+            kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
+            kosmos.fakeBiometricSettingsRepository.setIsFaceAuthCurrentlyAllowed(true)
+            runCurrent()
+
+            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(
+                HelpFaceAuthenticationStatus(1, "some helpful message")
+            )
+            runCurrent()
+            assertThat(bouncerMessage?.text).isEqualTo("Enter PIN")
+            assertThat(bouncerMessage?.secondaryText).isEqualTo("some helpful message")
+
+            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(
+                ErrorFaceAuthenticationStatus(
+                    BiometricFaceConstants.FACE_ERROR_TIMEOUT,
+                    "Try again"
+                )
+            )
+            runCurrent()
+            assertThat(bouncerMessage?.text).isEqualTo("Enter PIN")
+            assertThat(bouncerMessage?.secondaryText).isEqualTo("Try again")
+
+            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(
+                FailedFaceAuthenticationStatus()
+            )
+            runCurrent()
+            assertThat(bouncerMessage?.text).isEqualTo("Face not recognized")
+            assertThat(bouncerMessage?.secondaryText).isEqualTo("Try again or enter PIN")
+
+            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(
+                ErrorFaceAuthenticationStatus(
+                    BiometricFaceConstants.FACE_ERROR_LOCKOUT,
+                    "locked out"
+                )
+            )
+            runCurrent()
+            assertThat(bouncerMessage?.text).isEqualTo("Enter PIN")
+            assertThat(bouncerMessage?.secondaryText)
+                .isEqualTo("Can’t unlock with face. Too many attempts.")
+        }
+
+    private fun TestScope.verifyMessagesForAuthFlags(
+        vararg authFlagToMessagePair: Pair<Int, Pair<String, String?>>
+    ) {
+        val actualMessage by collectLastValue(underTest.message)
+
+        authFlagToMessagePair.forEach { (flag, expectedMessagePair) ->
+            kosmos.fakeBiometricSettingsRepository.setAuthenticationFlags(
+                AuthenticationFlags(userId = PRIMARY_USER_ID, flag = flag)
+            )
+            runCurrent()
+
+            assertThat(actualMessage?.text).isEqualTo(expectedMessagePair.first)
+
+            if (expectedMessagePair.second == null) {
+                assertThat(actualMessage?.secondaryText.isNullOrBlank()).isTrue()
+            } else {
+                assertThat(actualMessage?.secondaryText).isEqualTo(expectedMessagePair.second)
+            }
+        }
+    }
+
+    private fun assertTryAgainMessage(
+        message: String?,
+        time: Int,
+    ) {
+        assertThat(message).contains("Try again in $time second")
+    }
+
+    companion object {
+        private val WRONG_PIN = FakeAuthenticationRepository.DEFAULT_PIN.map { it + 1 }
+        private const val PRIMARY_USER_ID = 0
+        private val PRIMARY_USER =
+            UserInfo(
+                /* id= */ PRIMARY_USER_ID,
+                /* name= */ "primary user",
+                /* flags= */ UserInfo.FLAG_PRIMARY
+            )
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
index 73db175..3afca96 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
@@ -37,7 +37,6 @@
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
-import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.flow.flatMapLatest
@@ -142,54 +141,6 @@
     }
 
     @Test
-    fun message() =
-        testScope.runTest {
-            val message by collectLastValue(underTest.message)
-            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin)
-            assertThat(message?.isUpdateAnimated).isTrue()
-
-            repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) {
-                bouncerInteractor.authenticate(WRONG_PIN)
-            }
-            assertThat(message?.isUpdateAnimated).isFalse()
-
-            val lockoutEndMs = authenticationInteractor.lockoutEndTimestamp ?: 0
-            advanceTimeBy(lockoutEndMs - testScope.currentTime)
-            assertThat(message?.isUpdateAnimated).isTrue()
-        }
-
-    @Test
-    fun lockoutMessage() =
-        testScope.runTest {
-            val authMethodViewModel by collectLastValue(underTest.authMethodViewModel)
-            val message by collectLastValue(underTest.message)
-            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin)
-            assertThat(kosmos.fakeAuthenticationRepository.lockoutEndTimestamp).isNull()
-            assertThat(authMethodViewModel?.lockoutMessageId).isNotNull()
-
-            repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) { times ->
-                bouncerInteractor.authenticate(WRONG_PIN)
-                if (times < FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) {
-                    assertThat(message?.text).isEqualTo(bouncerInteractor.message.value)
-                    assertThat(message?.isUpdateAnimated).isTrue()
-                }
-            }
-            val lockoutSeconds = FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS
-            assertTryAgainMessage(message?.text, lockoutSeconds)
-            assertThat(message?.isUpdateAnimated).isFalse()
-
-            repeat(FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS) { time ->
-                advanceTimeBy(1.seconds)
-                val remainingSeconds = lockoutSeconds - time - 1
-                if (remainingSeconds > 0) {
-                    assertTryAgainMessage(message?.text, remainingSeconds)
-                }
-            }
-            assertThat(message?.text).isEmpty()
-            assertThat(message?.isUpdateAnimated).isTrue()
-        }
-
-    @Test
     fun isInputEnabled() =
         testScope.runTest {
             val isInputEnabled by
@@ -212,25 +163,6 @@
         }
 
     @Test
-    fun dialogViewModel() =
-        testScope.runTest {
-            val authMethodViewModel by collectLastValue(underTest.authMethodViewModel)
-            val dialogViewModel by collectLastValue(underTest.dialogViewModel)
-            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin)
-            assertThat(authMethodViewModel?.lockoutMessageId).isNotNull()
-
-            repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) {
-                assertThat(dialogViewModel).isNull()
-                bouncerInteractor.authenticate(WRONG_PIN)
-            }
-            assertThat(dialogViewModel).isNotNull()
-            assertThat(dialogViewModel?.text).isNotEmpty()
-
-            dialogViewModel?.onDismiss?.invoke()
-            assertThat(dialogViewModel).isNull()
-        }
-
-    @Test
     fun isSideBySideSupported() =
         testScope.runTest {
             val isSideBySideSupported by collectLastValue(underTest.isSideBySideSupported)
@@ -265,13 +197,6 @@
         return listOf(None, Pin, Password, Pattern, Sim)
     }
 
-    private fun assertTryAgainMessage(
-        message: String?,
-        time: Int,
-    ) {
-        assertThat(message).isEqualTo("Try again in $time seconds.")
-    }
-
     companion object {
         private val WRONG_PIN = FakeAuthenticationRepository.DEFAULT_PIN.map { it + 1 }
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
index df50eb6..71c5785 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
@@ -66,7 +66,6 @@
     private val bouncerInteractor by lazy { kosmos.bouncerInteractor }
     private val selectedUserInteractor by lazy { kosmos.selectedUserInteractor }
     private val inputMethodInteractor by lazy { kosmos.inputMethodInteractor }
-    private val bouncerViewModel by lazy { kosmos.bouncerViewModel }
     private val isInputEnabled = MutableStateFlow(true)
 
     private val underTest =
@@ -76,6 +75,7 @@
             interactor = bouncerInteractor,
             inputMethodInteractor = inputMethodInteractor,
             selectedUserInteractor = selectedUserInteractor,
+            onIntentionalUserInput = {},
         )
 
     @Before
@@ -88,11 +88,9 @@
     fun onShown() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.currentScene)
-            val message by collectLastValue(bouncerViewModel.message)
             val password by collectLastValue(underTest.password)
             lockDeviceAndOpenPasswordBouncer()
 
-            assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD)
             assertThat(password).isEmpty()
             assertThat(currentScene).isEqualTo(Scenes.Bouncer)
             assertThat(underTest.authenticationMethod).isEqualTo(AuthenticationMethodModel.Password)
@@ -101,16 +99,13 @@
     @Test
     fun onHidden_resetsPasswordInputAndMessage() =
         testScope.runTest {
-            val message by collectLastValue(bouncerViewModel.message)
             val password by collectLastValue(underTest.password)
             lockDeviceAndOpenPasswordBouncer()
 
             underTest.onPasswordInputChanged("password")
-            assertThat(message?.text).isNotEqualTo(ENTER_YOUR_PASSWORD)
             assertThat(password).isNotEmpty()
 
             underTest.onHidden()
-            assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD)
             assertThat(password).isEmpty()
         }
 
@@ -118,13 +113,11 @@
     fun onPasswordInputChanged() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.currentScene)
-            val message by collectLastValue(bouncerViewModel.message)
             val password by collectLastValue(underTest.password)
             lockDeviceAndOpenPasswordBouncer()
 
             underTest.onPasswordInputChanged("password")
 
-            assertThat(message?.text).isEmpty()
             assertThat(password).isEqualTo("password")
             assertThat(currentScene).isEqualTo(Scenes.Bouncer)
         }
@@ -144,7 +137,6 @@
     @Test
     fun onAuthenticateKeyPressed_whenWrong() =
         testScope.runTest {
-            val message by collectLastValue(bouncerViewModel.message)
             val password by collectLastValue(underTest.password)
             lockDeviceAndOpenPasswordBouncer()
 
@@ -152,13 +144,11 @@
             underTest.onAuthenticateKeyPressed()
 
             assertThat(password).isEmpty()
-            assertThat(message?.text).isEqualTo(WRONG_PASSWORD)
         }
 
     @Test
     fun onAuthenticateKeyPressed_whenEmpty() =
         testScope.runTest {
-            val message by collectLastValue(bouncerViewModel.message)
             val password by collectLastValue(underTest.password)
             kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
                 AuthenticationMethodModel.Password
@@ -171,14 +161,12 @@
             underTest.onAuthenticateKeyPressed()
 
             assertThat(password).isEmpty()
-            assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD)
         }
 
     @Test
     fun onAuthenticateKeyPressed_correctAfterWrong() =
         testScope.runTest {
             val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult)
-            val message by collectLastValue(bouncerViewModel.message)
             val password by collectLastValue(underTest.password)
             lockDeviceAndOpenPasswordBouncer()
 
@@ -186,12 +174,10 @@
             underTest.onPasswordInputChanged("wrong")
             underTest.onAuthenticateKeyPressed()
             assertThat(password).isEqualTo("")
-            assertThat(message?.text).isEqualTo(WRONG_PASSWORD)
             assertThat(authResult).isFalse()
 
             // Enter the correct password:
             underTest.onPasswordInputChanged("password")
-            assertThat(message?.text).isEmpty()
 
             underTest.onAuthenticateKeyPressed()
 
@@ -331,10 +317,8 @@
 
     private fun TestScope.switchToScene(toScene: SceneKey) {
         val currentScene by collectLastValue(sceneInteractor.currentScene)
-        val bouncerShown = currentScene != Scenes.Bouncer && toScene == Scenes.Bouncer
         val bouncerHidden = currentScene == Scenes.Bouncer && toScene != Scenes.Bouncer
         sceneInteractor.changeScene(toScene, "reason")
-        if (bouncerShown) underTest.onShown()
         if (bouncerHidden) underTest.onHidden()
         runCurrent()
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
index 91a056d..51b73ee9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
@@ -63,6 +63,7 @@
             viewModelScope = testScope.backgroundScope,
             interactor = bouncerInteractor,
             isInputEnabled = MutableStateFlow(true).asStateFlow(),
+            onIntentionalUserInput = {},
         )
     }
 
@@ -79,12 +80,10 @@
     fun onShown() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.currentScene)
-            val message by collectLastValue(bouncerViewModel.message)
             val selectedDots by collectLastValue(underTest.selectedDots)
             val currentDot by collectLastValue(underTest.currentDot)
             lockDeviceAndOpenPatternBouncer()
 
-            assertThat(message?.text).isEqualTo(ENTER_YOUR_PATTERN)
             assertThat(selectedDots).isEmpty()
             assertThat(currentDot).isNull()
             assertThat(currentScene).isEqualTo(Scenes.Bouncer)
@@ -95,14 +94,12 @@
     fun onDragStart() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.currentScene)
-            val message by collectLastValue(bouncerViewModel.message)
             val selectedDots by collectLastValue(underTest.selectedDots)
             val currentDot by collectLastValue(underTest.currentDot)
             lockDeviceAndOpenPatternBouncer()
 
             underTest.onDragStart()
 
-            assertThat(message?.text).isEmpty()
             assertThat(selectedDots).isEmpty()
             assertThat(currentDot).isNull()
             assertThat(currentScene).isEqualTo(Scenes.Bouncer)
@@ -148,7 +145,6 @@
     fun onDragEnd_whenWrong() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.currentScene)
-            val message by collectLastValue(bouncerViewModel.message)
             val selectedDots by collectLastValue(underTest.selectedDots)
             val currentDot by collectLastValue(underTest.currentDot)
             lockDeviceAndOpenPatternBouncer()
@@ -159,7 +155,6 @@
 
             assertThat(selectedDots).isEmpty()
             assertThat(currentDot).isNull()
-            assertThat(message?.text).isEqualTo(WRONG_PATTERN)
             assertThat(currentScene).isEqualTo(Scenes.Bouncer)
         }
 
@@ -302,7 +297,6 @@
     @Test
     fun onDragEnd_whenPatternTooShort() =
         testScope.runTest {
-            val message by collectLastValue(bouncerViewModel.message)
             val dialogViewModel by collectLastValue(bouncerViewModel.dialogViewModel)
             lockDeviceAndOpenPatternBouncer()
 
@@ -325,7 +319,6 @@
 
                 underTest.onDragEnd()
 
-                assertWithMessage("Attempt #$attempt").that(message?.text).isEqualTo(WRONG_PATTERN)
                 assertWithMessage("Attempt #$attempt").that(dialogViewModel).isNull()
             }
         }
@@ -334,7 +327,6 @@
     fun onDragEnd_correctAfterWrong() =
         testScope.runTest {
             val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult)
-            val message by collectLastValue(bouncerViewModel.message)
             val selectedDots by collectLastValue(underTest.selectedDots)
             val currentDot by collectLastValue(underTest.currentDot)
             lockDeviceAndOpenPatternBouncer()
@@ -344,7 +336,6 @@
             underTest.onDragEnd()
             assertThat(selectedDots).isEmpty()
             assertThat(currentDot).isNull()
-            assertThat(message?.text).isEqualTo(WRONG_PATTERN)
             assertThat(authResult).isFalse()
 
             // Enter the correct pattern:
@@ -370,10 +361,8 @@
 
     private fun TestScope.switchToScene(toScene: SceneKey) {
         val currentScene by collectLastValue(sceneInteractor.currentScene)
-        val bouncerShown = currentScene != Scenes.Bouncer && toScene == Scenes.Bouncer
         val bouncerHidden = currentScene == Scenes.Bouncer && toScene != Scenes.Bouncer
         sceneInteractor.changeScene(toScene, "reason")
-        if (bouncerShown) underTest.onShown()
         if (bouncerHidden) underTest.onHidden()
         runCurrent()
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
index 7b75a37..5647954 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
@@ -56,7 +56,6 @@
     private val sceneInteractor by lazy { kosmos.sceneInteractor }
     private val authenticationInteractor by lazy { kosmos.authenticationInteractor }
     private val bouncerInteractor by lazy { kosmos.bouncerInteractor }
-    private val bouncerViewModel by lazy { kosmos.bouncerViewModel }
     private lateinit var underTest: PinBouncerViewModel
 
     @Before
@@ -69,6 +68,7 @@
                 isInputEnabled = MutableStateFlow(true).asStateFlow(),
                 simBouncerInteractor = kosmos.simBouncerInteractor,
                 authenticationMethod = AuthenticationMethodModel.Pin,
+                onIntentionalUserInput = {},
             )
 
         overrideResource(R.string.keyguard_enter_your_pin, ENTER_YOUR_PIN)
@@ -78,11 +78,9 @@
     @Test
     fun onShown() =
         testScope.runTest {
-            val message by collectLastValue(bouncerViewModel.message)
             val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
             lockDeviceAndOpenPinBouncer()
 
-            assertThat(message?.text).ignoringCase().isEqualTo(ENTER_YOUR_PIN)
             assertThat(pin).isEmpty()
             assertThat(underTest.authenticationMethod).isEqualTo(AuthenticationMethodModel.Pin)
         }
@@ -98,6 +96,7 @@
                     isInputEnabled = MutableStateFlow(true).asStateFlow(),
                     simBouncerInteractor = kosmos.simBouncerInteractor,
                     authenticationMethod = AuthenticationMethodModel.Sim,
+                    onIntentionalUserInput = {},
                 )
 
             assertThat(underTest.isSimAreaVisible).isTrue()
@@ -126,6 +125,7 @@
                     isInputEnabled = MutableStateFlow(true).asStateFlow(),
                     simBouncerInteractor = kosmos.simBouncerInteractor,
                     authenticationMethod = AuthenticationMethodModel.Sim,
+                    onIntentionalUserInput = {},
                 )
             kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true)
             val hintedPinLength by collectLastValue(underTest.hintedPinLength)
@@ -136,20 +136,17 @@
     @Test
     fun onPinButtonClicked() =
         testScope.runTest {
-            val message by collectLastValue(bouncerViewModel.message)
             val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
             lockDeviceAndOpenPinBouncer()
 
             underTest.onPinButtonClicked(1)
 
-            assertThat(message?.text).isEmpty()
             assertThat(pin).containsExactly(1)
         }
 
     @Test
     fun onBackspaceButtonClicked() =
         testScope.runTest {
-            val message by collectLastValue(bouncerViewModel.message)
             val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
             lockDeviceAndOpenPinBouncer()
 
@@ -158,7 +155,6 @@
 
             underTest.onBackspaceButtonClicked()
 
-            assertThat(message?.text).isEmpty()
             assertThat(pin).isEmpty()
         }
 
@@ -183,7 +179,6 @@
     fun onBackspaceButtonLongPressed() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.currentScene)
-            val message by collectLastValue(bouncerViewModel.message)
             val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
             lockDeviceAndOpenPinBouncer()
 
@@ -195,7 +190,6 @@
 
             underTest.onBackspaceButtonLongPressed()
 
-            assertThat(message?.text).isEmpty()
             assertThat(pin).isEmpty()
             assertThat(currentScene).isEqualTo(Scenes.Bouncer)
         }
@@ -217,7 +211,6 @@
     fun onAuthenticateButtonClicked_whenWrong() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.currentScene)
-            val message by collectLastValue(bouncerViewModel.message)
             val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
             lockDeviceAndOpenPinBouncer()
 
@@ -230,7 +223,6 @@
             underTest.onAuthenticateButtonClicked()
 
             assertThat(pin).isEmpty()
-            assertThat(message?.text).ignoringCase().isEqualTo(WRONG_PIN)
             assertThat(currentScene).isEqualTo(Scenes.Bouncer)
         }
 
@@ -238,7 +230,6 @@
     fun onAuthenticateButtonClicked_correctAfterWrong() =
         testScope.runTest {
             val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult)
-            val message by collectLastValue(bouncerViewModel.message)
             val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
             lockDeviceAndOpenPinBouncer()
 
@@ -248,13 +239,11 @@
             underTest.onPinButtonClicked(4)
             underTest.onPinButtonClicked(5) // PIN is now wrong!
             underTest.onAuthenticateButtonClicked()
-            assertThat(message?.text).ignoringCase().isEqualTo(WRONG_PIN)
             assertThat(pin).isEmpty()
             assertThat(authResult).isFalse()
 
             // Enter the correct PIN:
             FakeAuthenticationRepository.DEFAULT_PIN.forEach(underTest::onPinButtonClicked)
-            assertThat(message?.text).isEmpty()
 
             underTest.onAuthenticateButtonClicked()
 
@@ -277,7 +266,6 @@
     fun onAutoConfirm_whenWrong() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.currentScene)
-            val message by collectLastValue(bouncerViewModel.message)
             val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
             kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true)
             lockDeviceAndOpenPinBouncer()
@@ -290,7 +278,6 @@
             ) // PIN is now wrong!
 
             assertThat(pin).isEmpty()
-            assertThat(message?.text).ignoringCase().isEqualTo(WRONG_PIN)
             assertThat(currentScene).isEqualTo(Scenes.Bouncer)
         }
 
@@ -390,10 +377,8 @@
 
     private fun TestScope.switchToScene(toScene: SceneKey) {
         val currentScene by collectLastValue(sceneInteractor.currentScene)
-        val bouncerShown = currentScene != Scenes.Bouncer && toScene == Scenes.Bouncer
         val bouncerHidden = currentScene == Scenes.Bouncer && toScene != Scenes.Bouncer
         sceneInteractor.changeScene(toScene, "reason")
-        if (bouncerShown) underTest.onShown()
         if (bouncerHidden) underTest.onHidden()
         runCurrent()
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorTest.kt
similarity index 83%
rename from packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorTest.kt
index decbdaf..51f9957 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorTest.kt
@@ -26,12 +26,10 @@
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
-import kotlin.test.Test
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.runTest
+import org.junit.Test
 import org.junit.runner.RunWith
 
-@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class DeviceEntryFingerprintAuthInteractorTest : SysuiTestCase() {
@@ -59,17 +57,20 @@
         }
 
     @Test
-    fun isSensorUnderDisplay_trueForUdfpsSensorTypes() =
+    fun isFingerprintCurrentlyAllowedInBouncer_trueForNonUdfpsSensorTypes() =
         testScope.runTest {
-            val isSensorUnderDisplay by collectLastValue(underTest.isSensorUnderDisplay)
+            biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
+
+            val isFingerprintCurrentlyAllowedInBouncer by
+                collectLastValue(underTest.isFingerprintCurrentlyAllowedOnBouncer)
 
             fingerprintPropertyRepository.supportsUdfps()
-            assertThat(isSensorUnderDisplay).isTrue()
+            assertThat(isFingerprintCurrentlyAllowedInBouncer).isFalse()
 
             fingerprintPropertyRepository.supportsRearFps()
-            assertThat(isSensorUnderDisplay).isFalse()
+            assertThat(isFingerprintCurrentlyAllowedInBouncer).isTrue()
 
             fingerprintPropertyRepository.supportsSideFps()
-            assertThat(isSensorUnderDisplay).isFalse()
+            assertThat(isFingerprintCurrentlyAllowedInBouncer).isTrue()
         }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
index 3c0ab24..27c4ec1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
@@ -27,9 +27,17 @@
 import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
 import com.android.systemui.qs.QSImpl
 import com.android.systemui.qs.dagger.QSComponent
 import com.android.systemui.qs.dagger.QSSceneComponent
+import com.android.systemui.shade.data.repository.fakeShadeRepository
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.capture
@@ -41,8 +49,6 @@
 import java.util.Locale
 import javax.inject.Provider
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.StandardTestDispatcher
-import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
@@ -57,8 +63,9 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 class QSSceneAdapterImplTest : SysuiTestCase() {
 
-    private val testDispatcher = StandardTestDispatcher()
-    private val testScope = TestScope(testDispatcher)
+    private val kosmos = Kosmos().apply { testCase = this@QSSceneAdapterImplTest }
+    private val testDispatcher = kosmos.testDispatcher
+    private val testScope = kosmos.testScope
 
     private val qsImplProvider =
         object : Provider<QSImpl> {
@@ -107,10 +114,15 @@
             }
         }
 
+    private val shadeInteractor = kosmos.shadeInteractor
+    private val dumpManager = mock<DumpManager>()
+
     private val underTest =
         QSSceneAdapterImpl(
             qsSceneComponentFactory,
             qsImplProvider,
+            shadeInteractor,
+            dumpManager,
             testDispatcher,
             testScope.backgroundScope,
             configurationInteractor,
@@ -158,12 +170,6 @@
                     )
                 verify(this).setListening(false)
                 verify(this).setExpanded(false)
-                verify(this)
-                    .setTransitionToFullShadeProgress(
-                        /* isTransitioningToFullShade= */ false,
-                        /* qsTransitionFraction= */ 1f,
-                        /* qsSquishinessFraction = */ 1f,
-                    )
             }
         }
 
@@ -187,13 +193,7 @@
                         /* squishinessFraction= */ 1f,
                     )
                 verify(this).setListening(true)
-                verify(this).setExpanded(true)
-                verify(this)
-                    .setTransitionToFullShadeProgress(
-                        /* isTransitioningToFullShade= */ false,
-                        /* qsTransitionFraction= */ 1f,
-                        /* qsSquishinessFraction = */ 1f,
-                    )
+                verify(this).setExpanded(false)
             }
         }
 
@@ -218,12 +218,6 @@
                     )
                 verify(this).setListening(true)
                 verify(this).setExpanded(true)
-                verify(this)
-                    .setTransitionToFullShadeProgress(
-                        /* isTransitioningToFullShade= */ false,
-                        /* qsTransitionFraction= */ 1f,
-                        /* qsSquishinessFraction = */ 1f,
-                    )
             }
         }
 
@@ -249,12 +243,6 @@
                     )
                 verify(this).setListening(true)
                 verify(this).setExpanded(true)
-                verify(this)
-                    .setTransitionToFullShadeProgress(
-                        /* isTransitioningToFullShade= */ false,
-                        /* qsTransitionFraction= */ 1f,
-                        /* qsSquishinessFraction = */ 1f,
-                    )
             }
         }
 
@@ -268,7 +256,7 @@
             runCurrent()
             clearInvocations(qsImpl!!)
 
-            underTest.setState(QSSceneAdapter.State.Unsquishing(squishiness))
+            underTest.setState(QSSceneAdapter.State.UnsquishingQQS(squishiness))
             with(qsImpl!!) {
                 verify(this).setQsVisible(true)
                 verify(this)
@@ -279,13 +267,7 @@
                         /* squishinessFraction= */ squishiness,
                     )
                 verify(this).setListening(true)
-                verify(this).setExpanded(true)
-                verify(this)
-                    .setTransitionToFullShadeProgress(
-                        /* isTransitioningToFullShade= */ false,
-                        /* qsTransitionFraction= */ 1f,
-                        /* qsSquishinessFraction = */ squishiness,
-                    )
+                verify(this).setExpanded(false)
             }
         }
 
@@ -497,4 +479,21 @@
 
             verify(qsImpl!!).applyBottomNavBarToCustomizerPadding(navBarHeight)
         }
+
+    @Test
+    fun dispatchSplitShade() =
+        testScope.runTest {
+            val shadeRepository = kosmos.fakeShadeRepository
+            shadeRepository.setShadeMode(ShadeMode.Single)
+            val qsImpl by collectLastValue(underTest.qsImpl)
+
+            underTest.inflate(context)
+            runCurrent()
+
+            verify(qsImpl!!).setInSplitShade(false)
+
+            shadeRepository.setShadeMode(ShadeMode.Split)
+            runCurrent()
+            verify(qsImpl!!).setInSplitShade(true)
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterTest.kt
index e281383..ebd65fd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterTest.kt
@@ -49,9 +49,16 @@
     }
 
     @Test
-    fun unsquishing_expansionSameAsQQS() {
+    fun unsquishingQQS_expansionSameAsQQS() {
         val squishiness = 0.6f
-        assertThat(QSSceneAdapter.State.Unsquishing(squishiness).expansion)
+        assertThat(QSSceneAdapter.State.UnsquishingQQS(squishiness).expansion)
             .isEqualTo(QSSceneAdapter.State.QQS.expansion)
     }
+
+    @Test
+    fun unsquishingQS_expansionSameAsQS() {
+        val squishiness = 0.6f
+        assertThat(QSSceneAdapter.State.UnsquishingQS(squishiness).expansion)
+            .isEqualTo(QSSceneAdapter.State.QS.expansion)
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
index 1c54961..d1c4ec3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
@@ -95,7 +95,7 @@
             scope = testScope.backgroundScope,
         )
 
-    private val qsFlexiglassAdapter = FakeQSSceneAdapter({ mock() })
+    private val qsSceneAdapter = FakeQSSceneAdapter({ mock() })
 
     private lateinit var shadeHeaderViewModel: ShadeHeaderViewModel
 
@@ -122,7 +122,7 @@
                 applicationScope = testScope.backgroundScope,
                 deviceEntryInteractor = deviceEntryInteractor,
                 shadeHeaderViewModel = shadeHeaderViewModel,
-                qsSceneAdapter = qsFlexiglassAdapter,
+                qsSceneAdapter = qsSceneAdapter,
                 notifications = kosmos.notificationsPlaceholderViewModel,
                 mediaDataManager = mediaDataManager,
                 shadeInteractor = kosmos.shadeInteractor,
@@ -279,6 +279,20 @@
         }
 
     @Test
+    fun upTransitionSceneKey_customizing_noTransition() =
+            testScope.runTest {
+                val destinationScenes by collectLastValue(underTest.destinationScenes)
+
+                qsSceneAdapter.setCustomizing(true)
+                assertThat(
+                        destinationScenes!!
+                                .keys
+                                .filterIsInstance<Swipe>()
+                                .filter { it.direction == SwipeDirection.Up }
+                ).isEmpty()
+            }
+
+    @Test
     fun shadeMode() =
         testScope.runTest {
             val shadeMode by collectLastValue(underTest.shadeMode)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt
index 2689fc1..94539a3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt
@@ -22,7 +22,6 @@
 import androidx.test.filters.SmallTest
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.common.shared.model.NotificationContainerBounds
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.flags.Flags
 import com.android.systemui.flags.fakeFeatureFlagsClassic
@@ -31,6 +30,7 @@
 import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.shared.model.fakeSceneDataSource
+import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationStackAppearanceViewModel
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel
 import com.android.systemui.testKosmos
@@ -64,7 +64,7 @@
     @Test
     fun updateBounds() =
         testScope.runTest {
-            val bounds by collectLastValue(appearanceViewModel.stackBounds)
+            val clipping by collectLastValue(appearanceViewModel.stackClipping)
 
             val top = 200f
             val left = 0f
@@ -76,15 +76,8 @@
                 right = right,
                 bottom = bottom
             )
-            assertThat(bounds)
-                .isEqualTo(
-                    NotificationContainerBounds(
-                        left = left,
-                        top = top,
-                        right = right,
-                        bottom = bottom
-                    )
-                )
+            assertThat(clipping?.bounds)
+                .isEqualTo(StackBounds(left = left, top = top, right = right, bottom = bottom))
         }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt
index ffe6e6d..6dd425c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt
@@ -19,10 +19,10 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.common.shared.model.NotificationContainerBounds
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
@@ -43,19 +43,17 @@
             val stackBounds by collectLastValue(underTest.stackBounds)
 
             val bounds1 =
-                NotificationContainerBounds(
+                StackBounds(
                     top = 100f,
                     bottom = 200f,
-                    isAnimated = true,
                 )
             underTest.setStackBounds(bounds1)
             assertThat(stackBounds).isEqualTo(bounds1)
 
             val bounds2 =
-                NotificationContainerBounds(
+                StackBounds(
                     top = 200f,
                     bottom = 300f,
-                    isAnimated = false,
                 )
             underTest.setStackBounds(bounds2)
             assertThat(stackBounds).isEqualTo(bounds2)
@@ -65,7 +63,7 @@
     fun setStackBounds_withImproperBounds_throwsException() =
         testScope.runTest {
             underTest.setStackBounds(
-                NotificationContainerBounds(
+                StackBounds(
                     top = 100f,
                     bottom = 99f,
                 )
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt
index 693de55..2ccc8b4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.common.shared.model.NotificationContainerBounds
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor
+import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
@@ -36,9 +37,9 @@
     fun onBoundsChanged_setsNotificationContainerBounds() {
         underTest.onBoundsChanged(left = 5f, top = 5f, right = 5f, bottom = 5f)
         assertThat(kosmos.keyguardInteractor.notificationContainerBounds.value)
-            .isEqualTo(NotificationContainerBounds(left = 5f, top = 5f, right = 5f, bottom = 5f))
+            .isEqualTo(NotificationContainerBounds(top = 5f, bottom = 5f))
         assertThat(kosmos.notificationStackAppearanceInteractor.stackBounds.value)
-            .isEqualTo(NotificationContainerBounds(left = 5f, top = 5f, right = 5f, bottom = 5f))
+            .isEqualTo(StackBounds(left = 5f, top = 5f, right = 5f, bottom = 5f))
     }
     @Test
     fun onContentTopChanged_setsContentTop() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractorTest.kt
new file mode 100644
index 0000000..b5c5809
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractorTest.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.panel.component.mediaoutput.domain.interactor
+
+import android.os.Handler
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.volume.localMediaController
+import com.android.systemui.volume.mediaControllerRepository
+import com.android.systemui.volume.mediaOutputInteractor
+import com.android.systemui.volume.remoteMediaController
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class MediaDeviceSessionInteractorTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+
+    private lateinit var underTest: MediaDeviceSessionInteractor
+
+    @Before
+    fun setup() {
+        with(kosmos) {
+            mediaControllerRepository.setActiveSessions(
+                listOf(localMediaController, remoteMediaController)
+            )
+
+            underTest =
+                MediaDeviceSessionInteractor(
+                    testScope.testScheduler,
+                    Handler(TestableLooper.get(kosmos.testCase).looper),
+                    mediaControllerRepository,
+                )
+        }
+    }
+
+    @Test
+    fun playbackInfo_returnsPlaybackInfo() {
+        with(kosmos) {
+            testScope.runTest {
+                val session by collectLastValue(mediaOutputInteractor.defaultActiveMediaSession)
+                runCurrent()
+                val info by collectLastValue(underTest.playbackInfo(session!!))
+                runCurrent()
+
+                assertThat(info).isEqualTo(localMediaController.playbackInfo)
+            }
+        }
+    }
+
+    @Test
+    fun playbackState_returnsPlaybackState() {
+        with(kosmos) {
+            testScope.runTest {
+                val session by collectLastValue(mediaOutputInteractor.defaultActiveMediaSession)
+                runCurrent()
+                val state by collectLastValue(underTest.playbackState(session!!))
+                runCurrent()
+
+                assertThat(state).isEqualTo(localMediaController.playbackState)
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelTest.kt
index dcf635e..6f7f20b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelTest.kt
@@ -29,9 +29,10 @@
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
+import com.android.systemui.volume.localMediaController
 import com.android.systemui.volume.localMediaRepository
-import com.android.systemui.volume.mediaController
 import com.android.systemui.volume.mediaControllerRepository
+import com.android.systemui.volume.mediaDeviceSessionInteractor
 import com.android.systemui.volume.mediaOutputActionsInteractor
 import com.android.systemui.volume.mediaOutputInteractor
 import com.android.systemui.volume.panel.volumePanelViewModel
@@ -63,6 +64,7 @@
                     testScope.backgroundScope,
                     volumePanelViewModel,
                     mediaOutputActionsInteractor,
+                    mediaDeviceSessionInteractor,
                     mediaOutputInteractor,
                 )
 
@@ -74,11 +76,11 @@
                 )
             }
 
-            whenever(mediaController.packageName).thenReturn("test.pkg")
-            whenever(mediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {}))
-            whenever(mediaController.playbackState).then { playbackStateBuilder.build() }
+            whenever(localMediaController.packageName).thenReturn("test.pkg")
+            whenever(localMediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {}))
+            whenever(localMediaController.playbackState).then { playbackStateBuilder.build() }
 
-            mediaControllerRepository.setActiveLocalMediaController(mediaController)
+            mediaControllerRepository.setActiveSessions(listOf(localMediaController))
         }
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt
index 1ed7f5d..2f69942 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt
@@ -32,8 +32,8 @@
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
+import com.android.systemui.volume.localMediaController
 import com.android.systemui.volume.localMediaRepository
-import com.android.systemui.volume.mediaController
 import com.android.systemui.volume.mediaControllerRepository
 import com.android.systemui.volume.panel.component.spatial.spatialAudioComponentInteractor
 import com.google.common.truth.Truth.assertThat
@@ -66,11 +66,11 @@
                 }
             )
 
-            whenever(mediaController.packageName).thenReturn("test.pkg")
-            whenever(mediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {}))
-            whenever(mediaController.playbackState).thenReturn(PlaybackState.Builder().build())
+            whenever(localMediaController.packageName).thenReturn("test.pkg")
+            whenever(localMediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {}))
+            whenever(localMediaController.playbackState).thenReturn(PlaybackState.Builder().build())
 
-            mediaControllerRepository.setActiveLocalMediaController(mediaController)
+            mediaControllerRepository.setActiveSessions(listOf(localMediaController))
 
             underTest = SpatialAudioAvailabilityCriteria(spatialAudioComponentInteractor)
         }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt
index 281b03d..e36ae60 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt
@@ -34,8 +34,8 @@
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
+import com.android.systemui.volume.localMediaController
 import com.android.systemui.volume.localMediaRepository
-import com.android.systemui.volume.mediaController
 import com.android.systemui.volume.mediaControllerRepository
 import com.android.systemui.volume.mediaOutputInteractor
 import com.android.systemui.volume.panel.component.spatial.domain.model.SpatialAudioAvailabilityModel
@@ -70,11 +70,11 @@
                 }
             )
 
-            whenever(mediaController.packageName).thenReturn("test.pkg")
-            whenever(mediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {}))
-            whenever(mediaController.playbackState).thenReturn(PlaybackState.Builder().build())
+            whenever(localMediaController.packageName).thenReturn("test.pkg")
+            whenever(localMediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {}))
+            whenever(localMediaController.playbackState).thenReturn(PlaybackState.Builder().build())
 
-            mediaControllerRepository.setActiveLocalMediaController(mediaController)
+            mediaControllerRepository.setActiveSessions(listOf(localMediaController))
 
             underTest =
                 SpatialAudioComponentInteractor(
diff --git a/packages/SystemUI/res/layout/window_magnification_settings_view.xml b/packages/SystemUI/res/layout/window_magnification_settings_view.xml
index efdb0a3..704cf0b 100644
--- a/packages/SystemUI/res/layout/window_magnification_settings_view.xml
+++ b/packages/SystemUI/res/layout/window_magnification_settings_view.xml
@@ -29,9 +29,13 @@
         android:layout_height="wrap_content"
         android:orientation="horizontal">
         <TextView
+            android:id="@+id/magnifier_size_title"
             android:layout_width="0dp"
             android:layout_height="wrap_content"
             android:layout_weight="1"
+            android:singleLine="true"
+            android:scrollHorizontally="true"
+            android:ellipsize="marquee"
             android:text="@string/accessibility_magnifier_size"
             android:textAppearance="@style/TextAppearance.MagnificationSetting.Title"
             android:focusable="true"
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 774bbe5..bf5eeb9 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1437,8 +1437,11 @@
     <!-- Indication on the keyguard that appears when a trust agents unlocks the device. [CHAR LIMIT=40] -->
     <string name="keyguard_indication_trust_unlocked">Kept unlocked by TrustAgent</string>
 
-    <!-- Message asking the user to authenticate with primary authentication methods (PIN/pattern/password) or biometrics after the device is locked by adaptive auth. [CHAR LIMIT=60] -->
-    <string name="kg_prompt_after_adaptive_auth_lock">Theft protection\nDevice locked, too many unlock attempts</string>
+    <!-- Message asking the user to authenticate with primary authentication methods (PIN/pattern/password) or biometrics after the device is locked by adaptive auth. [CHAR LIMIT=70] -->
+    <string name="kg_prompt_after_adaptive_auth_lock">Device was locked, too many authentication attempts</string>
+
+    <!-- Indication on the keyguard that appears after the device is locked by adaptive auth. [CHAR LIMIT=60] -->
+    <string name="keyguard_indication_after_adaptive_auth_lock">Device locked\nFailed authentication</string>
 
     <!-- Accessibility string for current zen mode and selected exit condition. A template that simply concatenates existing mode string and the current condition description. [CHAR LIMIT=20] -->
     <string name="zen_mode_and_condition"><xliff:g id="zen_mode" example="Priority interruptions only">%1$s</xliff:g>. <xliff:g id="exit_condition" example="For one hour">%2$s</xliff:g></string>
@@ -1991,8 +1994,6 @@
     <string name="group_system_cycle_back">Cycle backward through recent apps</string>
     <!-- User visible title for the keyboard shortcut that accesses list of all apps and search. [CHAR LIMIT=70] -->
     <string name="group_system_access_all_apps_search">Open apps list</string>
-    <!-- User visible title for the keyboard shortcut that hides and (re)showes taskbar. [CHAR LIMIT=70] -->
-    <string name="group_system_hide_reshow_taskbar">Show taskbar</string>
     <!-- User visible title for the keyboard shortcut that accesses [system] settings. [CHAR LIMIT=70] -->
     <string name="group_system_access_system_settings">Open settings</string>
     <!-- User visible title for the keyboard shortcut that accesses Assistant app. [CHAR LIMIT=70] -->
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 59516be..0483a07 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -962,7 +962,7 @@
         <item name="android:windowBackground">@android:color/transparent</item>
         <item name="android:backgroundDimEnabled">true</item>
         <item name="android:windowCloseOnTouchOutside">true</item>
-        <item name="android:windowAnimationStyle">@android:style/Animation.Dialog</item>
+        <item name="android:windowAnimationStyle">@null</item>
     </style>
 
     <style name="Widget.SliceView.VolumePanel">
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
index a98990a..ca24ccb 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
@@ -98,6 +98,7 @@
     private ImageButton mMediumButton;
     private ImageButton mLargeButton;
     private Button mDoneButton;
+    private TextView mSizeTitle;
     private Button mEditButton;
     private ImageButton mFullScreenButton;
     private int mLastSelectedButtonIndex = MagnificationSize.NONE;
@@ -521,6 +522,7 @@
         mMediumButton = mSettingView.findViewById(R.id.magnifier_medium_button);
         mLargeButton = mSettingView.findViewById(R.id.magnifier_large_button);
         mDoneButton = mSettingView.findViewById(R.id.magnifier_done_button);
+        mSizeTitle = mSettingView.findViewById(R.id.magnifier_size_title);
         mEditButton = mSettingView.findViewById(R.id.magnifier_edit_button);
         mFullScreenButton = mSettingView.findViewById(R.id.magnifier_full_button);
         mAllowDiagonalScrollingTitle =
@@ -548,6 +550,7 @@
         mDoneButton.setOnClickListener(mButtonClickListener);
         mFullScreenButton.setOnClickListener(mButtonClickListener);
         mEditButton.setOnClickListener(mButtonClickListener);
+        mSizeTitle.setSelected(true);
         mAllowDiagonalScrollingTitle.setSelected(true);
 
         mSettingView.setOnApplyWindowInsetsListener((v, insets) -> {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
index ac99fc6..85f63e9 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
@@ -126,6 +126,7 @@
             field?.let { oldView ->
                 val lottie = oldView.requireViewById(R.id.sidefps_animation) as LottieAnimationView
                 lottie.pauseAnimation()
+                lottie.removeAllLottieOnCompositionLoadedListener()
                 windowManager.removeView(oldView)
                 orientationListener.disable()
             }
@@ -288,7 +289,7 @@
     }
 
     private fun onOrientationChanged(@BiometricRequestConstants.RequestReason reason: Int) {
-        if (overlayView != null) {
+        if (overlayView?.isAttachedToWindow == true) {
             createOverlayForDisplay(reason)
         }
     }
@@ -322,7 +323,7 @@
         )
         lottie.addLottieOnCompositionLoadedListener {
             // Check that view is not stale, and that overlayView has not been hidden/removed
-            if (overlayView != null && overlayView == view) {
+            if (overlayView?.isAttachedToWindow == true && overlayView == view) {
                 updateOverlayParams(display, it.bounds)
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractor.kt
index ed1557c..c4967ec 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractor.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.keyguard.shared.model.FingerprintAuthenticationStatus
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.map
 
 /** Encapsulates business logic for interacting with biometric authentication state. */
@@ -52,7 +53,7 @@
             } else {
                 AuthenticationReason.NotRunning
             }
-        }
+        }.distinctUntilChanged()
 
     override val fingerprintAcquiredStatus: Flow<FingerprintAuthenticationStatus> =
         biometricStatusRepository.fingerprintAcquiredStatus
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt
index d849b3a..94e0854 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt
@@ -20,7 +20,6 @@
 import com.android.systemui.flags.FeatureFlagsClassic
 import com.android.systemui.flags.Flags
 import javax.inject.Inject
-import kotlinx.coroutines.flow.MutableStateFlow
 
 /** Provides access to bouncer-related application state. */
 @SysUISingleton
@@ -29,9 +28,6 @@
 constructor(
     private val flags: FeatureFlagsClassic,
 ) {
-    /** The user-facing message to show in the bouncer. */
-    val message = MutableStateFlow<String?>(null)
-
     /** Whether the user switcher should be displayed within the bouncer UI on large screens. */
     val isUserSwitcherVisible: Boolean
         get() = flags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
index d8be1af..aeb564d5 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
@@ -16,13 +16,8 @@
 
 package com.android.systemui.bouncer.domain.interactor
 
-import android.content.Context
 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
 import com.android.systemui.authentication.domain.interactor.AuthenticationResult
-import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
-import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Password
-import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pattern
-import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pin
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Sim
 import com.android.systemui.bouncer.data.repository.BouncerRepository
 import com.android.systemui.classifier.FalsingClassifier
@@ -31,7 +26,6 @@
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
 import com.android.systemui.power.domain.interactor.PowerInteractor
-import com.android.systemui.res.R
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.async
@@ -41,7 +35,6 @@
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.launch
 
 /** Encapsulates business logic and application state accessing use-cases. */
 @SysUISingleton
@@ -49,16 +42,14 @@
 @Inject
 constructor(
     @Application private val applicationScope: CoroutineScope,
-    @Application private val applicationContext: Context,
     private val repository: BouncerRepository,
     private val authenticationInteractor: AuthenticationInteractor,
     private val deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor,
     private val falsingInteractor: FalsingInteractor,
     private val powerInteractor: PowerInteractor,
-    private val simBouncerInteractor: SimBouncerInteractor,
 ) {
-    /** The user-facing message to show in the bouncer when lockout is not active. */
-    val message: StateFlow<String?> = repository.message
+    private val _onIncorrectBouncerInput = MutableSharedFlow<Unit>()
+    val onIncorrectBouncerInput: SharedFlow<Unit> = _onIncorrectBouncerInput
 
     /** Whether the auto confirm feature is enabled for the currently-selected user. */
     val isAutoConfirmEnabled: StateFlow<Boolean> = authenticationInteractor.isAutoConfirmEnabled
@@ -119,25 +110,6 @@
         )
     }
 
-    fun setMessage(message: String?) {
-        repository.message.value = message
-    }
-
-    /**
-     * Resets the user-facing message back to the default according to the current authentication
-     * method.
-     */
-    fun resetMessage() {
-        applicationScope.launch {
-            setMessage(promptMessage(authenticationInteractor.getAuthenticationMethod()))
-        }
-    }
-
-    /** Removes the user-facing message. */
-    fun clearMessage() {
-        setMessage(null)
-    }
-
     /**
      * Attempts to authenticate based on the given user input.
      *
@@ -176,50 +148,17 @@
                 .async { authenticationInteractor.authenticate(input, tryAutoConfirm) }
                 .await()
 
-        if (authenticationInteractor.lockoutEndTimestamp != null) {
-            clearMessage()
-        } else if (
+        if (
             authResult == AuthenticationResult.FAILED ||
                 (authResult == AuthenticationResult.SKIPPED && !tryAutoConfirm)
         ) {
-            showWrongInputMessage()
+            _onIncorrectBouncerInput.emit(Unit)
         }
         return authResult
     }
 
-    /**
-     * Shows the a message notifying the user that their credentials input is wrong.
-     *
-     * Callers should use this instead of [authenticate] when they know ahead of time that an auth
-     * attempt will fail but aren't interested in the other side effects like triggering lockout.
-     * For example, if the user entered a pattern that's too short, the system can show the error
-     * message without having the attempt trigger lockout.
-     */
-    private suspend fun showWrongInputMessage() {
-        setMessage(wrongInputMessage(authenticationInteractor.getAuthenticationMethod()))
-    }
-
     /** Notifies that the input method editor (software keyboard) has been hidden by the user. */
     suspend fun onImeHiddenByUser() {
         _onImeHiddenByUser.emit(Unit)
     }
-
-    private fun promptMessage(authMethod: AuthenticationMethodModel): String {
-        return when (authMethod) {
-            is Sim -> simBouncerInteractor.getDefaultMessage()
-            is Pin -> applicationContext.getString(R.string.keyguard_enter_your_pin)
-            is Password -> applicationContext.getString(R.string.keyguard_enter_your_password)
-            is Pattern -> applicationContext.getString(R.string.keyguard_enter_your_pattern)
-            else -> ""
-        }
-    }
-
-    private fun wrongInputMessage(authMethod: AuthenticationMethodModel): String {
-        return when (authMethod) {
-            is Pin -> applicationContext.getString(R.string.kg_wrong_pin)
-            is Password -> applicationContext.getString(R.string.kg_wrong_password)
-            is Pattern -> applicationContext.getString(R.string.kg_wrong_pattern)
-            else -> ""
-        }
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt
index 7f6fc91..d20c607 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt
@@ -33,15 +33,17 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepository
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFingerprintAuthInteractor
 import com.android.systemui.flags.SystemPropertiesHelper
 import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
-import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.data.repository.TrustRepository
 import com.android.systemui.user.data.repository.UserRepository
-import com.android.systemui.util.kotlin.Quint
+import com.android.systemui.util.kotlin.Sextuple
+import com.android.systemui.util.kotlin.combine
 import javax.inject.Inject
 import kotlin.math.roundToInt
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.combine
@@ -56,6 +58,7 @@
 private const val TAG = "BouncerMessageInteractor"
 
 /** Handles business logic for the primary bouncer message area. */
+@OptIn(ExperimentalCoroutinesApi::class)
 @SysUISingleton
 class BouncerMessageInteractor
 @Inject
@@ -63,23 +66,24 @@
     private val repository: BouncerMessageRepository,
     private val userRepository: UserRepository,
     private val countDownTimerUtil: CountDownTimerUtil,
-    private val updateMonitor: KeyguardUpdateMonitor,
+    updateMonitor: KeyguardUpdateMonitor,
     trustRepository: TrustRepository,
     biometricSettingsRepository: BiometricSettingsRepository,
     private val systemPropertiesHelper: SystemPropertiesHelper,
     primaryBouncerInteractor: PrimaryBouncerInteractor,
     @Application private val applicationScope: CoroutineScope,
     private val facePropertyRepository: FacePropertyRepository,
-    deviceEntryFingerprintAuthRepository: DeviceEntryFingerprintAuthRepository,
+    private val deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor,
     faceAuthRepository: DeviceEntryFaceAuthRepository,
     private val securityModel: KeyguardSecurityModel,
 ) {
 
-    private val isFingerprintAuthCurrentlyAllowed =
-        deviceEntryFingerprintAuthRepository.isLockedOut
-            .isFalse()
-            .and(biometricSettingsRepository.isFingerprintAuthCurrentlyAllowed)
-            .stateIn(applicationScope, SharingStarted.Eagerly, false)
+    private val isFingerprintAuthCurrentlyAllowedOnBouncer =
+        deviceEntryFingerprintAuthInteractor.isFingerprintCurrentlyAllowedOnBouncer.stateIn(
+            applicationScope,
+            SharingStarted.Eagerly,
+            false
+        )
 
     private val currentSecurityMode
         get() = securityModel.getSecurityMode(currentUserId)
@@ -99,13 +103,13 @@
                         BiometricSourceType.FACE ->
                             BouncerMessageStrings.incorrectFaceInput(
                                     currentSecurityMode.toAuthModel(),
-                                    isFingerprintAuthCurrentlyAllowed.value
+                                    isFingerprintAuthCurrentlyAllowedOnBouncer.value
                                 )
                                 .toMessage()
                         else ->
                             BouncerMessageStrings.defaultMessage(
                                     currentSecurityMode.toAuthModel(),
-                                    isFingerprintAuthCurrentlyAllowed.value
+                                    isFingerprintAuthCurrentlyAllowedOnBouncer.value
                                 )
                                 .toMessage()
                     }
@@ -144,11 +148,12 @@
                 biometricSettingsRepository.authenticationFlags,
                 trustRepository.isCurrentUserTrustManaged,
                 isAnyBiometricsEnabledAndEnrolled,
-                deviceEntryFingerprintAuthRepository.isLockedOut,
+                deviceEntryFingerprintAuthInteractor.isLockedOut,
                 faceAuthRepository.isLockedOut,
-                ::Quint
+                isFingerprintAuthCurrentlyAllowedOnBouncer,
+                ::Sextuple
             )
-            .map { (flags, _, biometricsEnrolledAndEnabled, fpLockedOut, faceLockedOut) ->
+            .map { (flags, _, biometricsEnrolledAndEnabled, fpLockedOut, faceLockedOut, _) ->
                 val isTrustUsuallyManaged = trustRepository.isCurrentUserTrustUsuallyManaged.value
                 val trustOrBiometricsAvailable =
                     (isTrustUsuallyManaged || biometricsEnrolledAndEnabled)
@@ -193,14 +198,14 @@
                     } else {
                         BouncerMessageStrings.faceLockedOut(
                                 currentSecurityMode.toAuthModel(),
-                                isFingerprintAuthCurrentlyAllowed.value
+                                isFingerprintAuthCurrentlyAllowedOnBouncer.value
                             )
                             .toMessage()
                     }
                 } else if (flags.isSomeAuthRequiredAfterAdaptiveAuthRequest) {
                     BouncerMessageStrings.authRequiredAfterAdaptiveAuthRequest(
                             currentSecurityMode.toAuthModel(),
-                            isFingerprintAuthCurrentlyAllowed.value
+                            isFingerprintAuthCurrentlyAllowedOnBouncer.value
                         )
                         .toMessage()
                 } else if (
@@ -209,19 +214,19 @@
                 ) {
                     BouncerMessageStrings.nonStrongAuthTimeout(
                             currentSecurityMode.toAuthModel(),
-                            isFingerprintAuthCurrentlyAllowed.value
+                            isFingerprintAuthCurrentlyAllowedOnBouncer.value
                         )
                         .toMessage()
                 } else if (isTrustUsuallyManaged && flags.someAuthRequiredAfterUserRequest) {
                     BouncerMessageStrings.trustAgentDisabled(
                             currentSecurityMode.toAuthModel(),
-                            isFingerprintAuthCurrentlyAllowed.value
+                            isFingerprintAuthCurrentlyAllowedOnBouncer.value
                         )
                         .toMessage()
                 } else if (isTrustUsuallyManaged && flags.someAuthRequiredAfterTrustAgentExpired) {
                     BouncerMessageStrings.trustAgentDisabled(
                             currentSecurityMode.toAuthModel(),
-                            isFingerprintAuthCurrentlyAllowed.value
+                            isFingerprintAuthCurrentlyAllowedOnBouncer.value
                         )
                         .toMessage()
                 } else if (trustOrBiometricsAvailable && flags.isInUserLockdown) {
@@ -265,7 +270,7 @@
         repository.setMessage(
             BouncerMessageStrings.incorrectSecurityInput(
                     currentSecurityMode.toAuthModel(),
-                    isFingerprintAuthCurrentlyAllowed.value
+                    isFingerprintAuthCurrentlyAllowedOnBouncer.value
                 )
                 .toMessage()
         )
@@ -274,14 +279,22 @@
     fun setFingerprintAcquisitionMessage(value: String?) {
         if (!Flags.revampedBouncerMessages()) return
         repository.setMessage(
-            defaultMessage(currentSecurityMode, value, isFingerprintAuthCurrentlyAllowed.value)
+            defaultMessage(
+                currentSecurityMode,
+                value,
+                isFingerprintAuthCurrentlyAllowedOnBouncer.value
+            )
         )
     }
 
     fun setFaceAcquisitionMessage(value: String?) {
         if (!Flags.revampedBouncerMessages()) return
         repository.setMessage(
-            defaultMessage(currentSecurityMode, value, isFingerprintAuthCurrentlyAllowed.value)
+            defaultMessage(
+                currentSecurityMode,
+                value,
+                isFingerprintAuthCurrentlyAllowedOnBouncer.value
+            )
         )
     }
 
@@ -289,7 +302,11 @@
         if (!Flags.revampedBouncerMessages()) return
 
         repository.setMessage(
-            defaultMessage(currentSecurityMode, value, isFingerprintAuthCurrentlyAllowed.value)
+            defaultMessage(
+                currentSecurityMode,
+                value,
+                isFingerprintAuthCurrentlyAllowedOnBouncer.value
+            )
         )
     }
 
@@ -297,7 +314,7 @@
         get() =
             BouncerMessageStrings.defaultMessage(
                     currentSecurityMode.toAuthModel(),
-                    isFingerprintAuthCurrentlyAllowed.value
+                    isFingerprintAuthCurrentlyAllowedOnBouncer.value
                 )
                 .toMessage()
 
@@ -355,11 +372,6 @@
 private fun Flow<Boolean>.or(anotherFlow: Flow<Boolean>) =
     this.combine(anotherFlow) { a, b -> a || b }
 
-private fun Flow<Boolean>.and(anotherFlow: Flow<Boolean>) =
-    this.combine(anotherFlow) { a, b -> a && b }
-
-private fun Flow<Boolean>.isFalse() = this.map { !it }
-
 private fun defaultMessage(
     securityMode: SecurityMode,
     secondaryMessage: String?,
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt
index f3903de..aebc50f 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt
@@ -18,6 +18,7 @@
 
 import android.app.AlertDialog
 import android.content.Context
+import com.android.systemui.bouncer.ui.viewmodel.BouncerMessageViewModelModule
 import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModelModule
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
@@ -30,6 +31,7 @@
     includes =
         [
             BouncerViewModelModule::class,
+            BouncerMessageViewModelModule::class,
         ],
 )
 interface BouncerViewModule {
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
index 0d7f6dc..4fbf735 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
@@ -57,17 +57,11 @@
      */
     @get:StringRes abstract val lockoutMessageId: Int
 
-    /** Notifies that the UI has been shown to the user. */
-    fun onShown() {
-        interactor.resetMessage()
-    }
-
     /**
      * Notifies that the UI has been hidden from the user (after any transitions have completed).
      */
     open fun onHidden() {
         clearInput()
-        interactor.resetMessage()
     }
 
     /** Notifies that the user has placed down a pointer. */
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt
new file mode 100644
index 0000000..6cb9b16
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt
@@ -0,0 +1,436 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.ui.viewmodel
+
+import android.content.Context
+import android.util.PluralsMessageFormatter
+import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor
+import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags
+import com.android.systemui.bouncer.shared.model.BouncerMessagePair
+import com.android.systemui.bouncer.shared.model.BouncerMessageStrings
+import com.android.systemui.bouncer.shared.model.primaryMessage
+import com.android.systemui.bouncer.shared.model.secondaryMessage
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.deviceentry.domain.interactor.BiometricMessageInteractor
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFingerprintAuthInteractor
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
+import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason
+import com.android.systemui.deviceentry.shared.model.FaceFailureMessage
+import com.android.systemui.deviceentry.shared.model.FaceLockoutMessage
+import com.android.systemui.deviceentry.shared.model.FaceTimeoutMessage
+import com.android.systemui.deviceentry.shared.model.FingerprintFailureMessage
+import com.android.systemui.deviceentry.shared.model.FingerprintLockoutMessage
+import com.android.systemui.res.R.string.kg_too_many_failed_attempts_countdown
+import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
+import com.android.systemui.user.ui.viewmodel.UserViewModel
+import com.android.systemui.util.kotlin.Utils.Companion.sample
+import com.android.systemui.util.time.SystemClock
+import dagger.Module
+import dagger.Provides
+import kotlin.math.ceil
+import kotlin.math.max
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+
+/** Holds UI state for the 2-line status message shown on the bouncer. */
+@OptIn(ExperimentalCoroutinesApi::class)
+class BouncerMessageViewModel(
+    @Application private val applicationContext: Context,
+    @Application private val applicationScope: CoroutineScope,
+    private val bouncerInteractor: BouncerInteractor,
+    private val simBouncerInteractor: SimBouncerInteractor,
+    private val authenticationInteractor: AuthenticationInteractor,
+    selectedUser: Flow<UserViewModel>,
+    private val clock: SystemClock,
+    private val biometricMessageInteractor: BiometricMessageInteractor,
+    private val faceAuthInteractor: DeviceEntryFaceAuthInteractor,
+    private val deviceEntryInteractor: DeviceEntryInteractor,
+    private val fingerprintInteractor: DeviceEntryFingerprintAuthInteractor,
+    flags: ComposeBouncerFlags,
+) {
+    /**
+     * A message shown when the user has attempted the wrong credential too many times and now must
+     * wait a while before attempting to authenticate again.
+     *
+     * This is updated every second (countdown) during the lockout. When lockout is not active, this
+     * is `null` and no lockout message should be shown.
+     */
+    private val lockoutMessage: MutableStateFlow<MessageViewModel?> = MutableStateFlow(null)
+
+    /** Whether there is a lockout message that is available to be shown in the status message. */
+    val isLockoutMessagePresent: Flow<Boolean> = lockoutMessage.map { it != null }
+
+    /** The user-facing message to show in the bouncer. */
+    val message: MutableStateFlow<MessageViewModel?> = MutableStateFlow(null)
+
+    /** Initializes the bouncer message to default whenever it is shown. */
+    fun onShown() {
+        showDefaultMessage()
+    }
+
+    /** Reset the message shown on the bouncer to the default message. */
+    fun showDefaultMessage() {
+        resetToDefault.tryEmit(Unit)
+    }
+
+    private val resetToDefault = MutableSharedFlow<Unit>(replay = 1)
+
+    private var lockoutCountdownJob: Job? = null
+
+    private fun defaultBouncerMessageInitializer() {
+        applicationScope.launch {
+            resetToDefault.emit(Unit)
+            authenticationInteractor.authenticationMethod
+                .flatMapLatest { authMethod ->
+                    if (authMethod == AuthenticationMethodModel.Sim) {
+                        resetToDefault.map {
+                            MessageViewModel(simBouncerInteractor.getDefaultMessage())
+                        }
+                    } else if (authMethod.isSecure) {
+                        combine(
+                            deviceEntryInteractor.deviceEntryRestrictionReason,
+                            lockoutMessage,
+                            fingerprintInteractor.isFingerprintCurrentlyAllowedOnBouncer,
+                            resetToDefault,
+                        ) { deviceEntryRestrictedReason, lockoutMsg, isFpAllowedInBouncer, _ ->
+                            lockoutMsg
+                                ?: deviceEntryRestrictedReason.toMessage(
+                                    authMethod,
+                                    isFpAllowedInBouncer
+                                )
+                        }
+                    } else {
+                        emptyFlow()
+                    }
+                }
+                .collectLatest { messageViewModel -> message.value = messageViewModel }
+        }
+    }
+
+    private fun listenForSimBouncerEvents() {
+        // Listen for any events from the SIM bouncer and update the message shown on the bouncer.
+        applicationScope.launch {
+            authenticationInteractor.authenticationMethod
+                .flatMapLatest { authMethod ->
+                    if (authMethod == AuthenticationMethodModel.Sim) {
+                        simBouncerInteractor.bouncerMessageChanged.map { simMsg ->
+                            simMsg?.let { MessageViewModel(it) }
+                        }
+                    } else {
+                        emptyFlow()
+                    }
+                }
+                .collectLatest {
+                    if (it != null) {
+                        message.value = it
+                    } else {
+                        resetToDefault.emit(Unit)
+                    }
+                }
+        }
+    }
+
+    private fun listenForFaceMessages() {
+        // Listen for any events from face authentication and update the message shown on the
+        // bouncer.
+        applicationScope.launch {
+            biometricMessageInteractor.faceMessage
+                .sample(
+                    authenticationInteractor.authenticationMethod,
+                    fingerprintInteractor.isFingerprintCurrentlyAllowedOnBouncer,
+                )
+                .collectLatest { (faceMessage, authMethod, fingerprintAllowedOnBouncer) ->
+                    val isFaceAuthStrong = faceAuthInteractor.isFaceAuthStrong()
+                    val defaultPrimaryMessage =
+                        BouncerMessageStrings.defaultMessage(
+                                authMethod,
+                                fingerprintAllowedOnBouncer
+                            )
+                            .primaryMessage
+                            .toResString()
+                    message.value =
+                        when (faceMessage) {
+                            is FaceTimeoutMessage ->
+                                MessageViewModel(
+                                    text = defaultPrimaryMessage,
+                                    secondaryText = faceMessage.message,
+                                    isUpdateAnimated = true
+                                )
+                            is FaceLockoutMessage ->
+                                if (isFaceAuthStrong)
+                                    BouncerMessageStrings.class3AuthLockedOut(authMethod)
+                                        .toMessage()
+                                else
+                                    BouncerMessageStrings.faceLockedOut(
+                                            authMethod,
+                                            fingerprintAllowedOnBouncer
+                                        )
+                                        .toMessage()
+                            is FaceFailureMessage ->
+                                BouncerMessageStrings.incorrectFaceInput(
+                                        authMethod,
+                                        fingerprintAllowedOnBouncer
+                                    )
+                                    .toMessage()
+                            else ->
+                                MessageViewModel(
+                                    text = defaultPrimaryMessage,
+                                    secondaryText = faceMessage.message,
+                                    isUpdateAnimated = false
+                                )
+                        }
+                    delay(MESSAGE_DURATION)
+                    resetToDefault.emit(Unit)
+                }
+        }
+    }
+
+    private fun listenForFingerprintMessages() {
+        applicationScope.launch {
+            // Listen for any events from fingerprint authentication and update the message shown
+            // on the bouncer.
+            biometricMessageInteractor.fingerprintMessage
+                .sample(
+                    authenticationInteractor.authenticationMethod,
+                    fingerprintInteractor.isFingerprintCurrentlyAllowedOnBouncer
+                )
+                .collectLatest { (fingerprintMessage, authMethod, isFingerprintAllowed) ->
+                    val defaultPrimaryMessage =
+                        BouncerMessageStrings.defaultMessage(authMethod, isFingerprintAllowed)
+                            .primaryMessage
+                            .toResString()
+                    message.value =
+                        when (fingerprintMessage) {
+                            is FingerprintLockoutMessage ->
+                                BouncerMessageStrings.class3AuthLockedOut(authMethod).toMessage()
+                            is FingerprintFailureMessage ->
+                                BouncerMessageStrings.incorrectFingerprintInput(authMethod)
+                                    .toMessage()
+                            else ->
+                                MessageViewModel(
+                                    text = defaultPrimaryMessage,
+                                    secondaryText = fingerprintMessage.message,
+                                    isUpdateAnimated = false
+                                )
+                        }
+                    delay(MESSAGE_DURATION)
+                    resetToDefault.emit(Unit)
+                }
+        }
+    }
+
+    private fun listenForBouncerEvents() {
+        // Keeps the lockout message up-to-date.
+        applicationScope.launch {
+            bouncerInteractor.onLockoutStarted.collect { startLockoutCountdown() }
+        }
+
+        // Listens to relevant bouncer events
+        applicationScope.launch {
+            bouncerInteractor.onIncorrectBouncerInput
+                .sample(
+                    authenticationInteractor.authenticationMethod,
+                    fingerprintInteractor.isFingerprintCurrentlyAllowedOnBouncer
+                )
+                .collectLatest { (_, authMethod, isFingerprintAllowed) ->
+                    message.emit(
+                        BouncerMessageStrings.incorrectSecurityInput(
+                                authMethod,
+                                isFingerprintAllowed
+                            )
+                            .toMessage()
+                    )
+                    delay(MESSAGE_DURATION)
+                    resetToDefault.emit(Unit)
+                }
+        }
+    }
+
+    private fun DeviceEntryRestrictionReason?.toMessage(
+        authMethod: AuthenticationMethodModel,
+        isFingerprintAllowedOnBouncer: Boolean,
+    ): MessageViewModel {
+        return when (this) {
+            DeviceEntryRestrictionReason.UserLockdown ->
+                BouncerMessageStrings.authRequiredAfterUserLockdown(authMethod)
+            DeviceEntryRestrictionReason.DeviceNotUnlockedSinceReboot ->
+                BouncerMessageStrings.authRequiredAfterReboot(authMethod)
+            DeviceEntryRestrictionReason.PolicyLockdown ->
+                BouncerMessageStrings.authRequiredAfterAdminLockdown(authMethod)
+            DeviceEntryRestrictionReason.UnattendedUpdate ->
+                BouncerMessageStrings.authRequiredForUnattendedUpdate(authMethod)
+            DeviceEntryRestrictionReason.DeviceNotUnlockedSinceMainlineUpdate ->
+                BouncerMessageStrings.authRequiredForMainlineUpdate(authMethod)
+            DeviceEntryRestrictionReason.SecurityTimeout ->
+                BouncerMessageStrings.authRequiredAfterPrimaryAuthTimeout(authMethod)
+            DeviceEntryRestrictionReason.StrongBiometricsLockedOut ->
+                BouncerMessageStrings.class3AuthLockedOut(authMethod)
+            DeviceEntryRestrictionReason.NonStrongFaceLockedOut ->
+                BouncerMessageStrings.faceLockedOut(authMethod, isFingerprintAllowedOnBouncer)
+            DeviceEntryRestrictionReason.NonStrongBiometricsSecurityTimeout ->
+                BouncerMessageStrings.nonStrongAuthTimeout(
+                    authMethod,
+                    isFingerprintAllowedOnBouncer
+                )
+            DeviceEntryRestrictionReason.TrustAgentDisabled ->
+                BouncerMessageStrings.trustAgentDisabled(authMethod, isFingerprintAllowedOnBouncer)
+            DeviceEntryRestrictionReason.AdaptiveAuthRequest ->
+                BouncerMessageStrings.authRequiredAfterAdaptiveAuthRequest(
+                    authMethod,
+                    isFingerprintAllowedOnBouncer
+                )
+            else -> BouncerMessageStrings.defaultMessage(authMethod, isFingerprintAllowedOnBouncer)
+        }.toMessage()
+    }
+
+    private fun BouncerMessagePair.toMessage(): MessageViewModel {
+        val primaryMsg = this.primaryMessage.toResString()
+        val secondaryMsg =
+            if (this.secondaryMessage == 0) "" else this.secondaryMessage.toResString()
+        return MessageViewModel(primaryMsg, secondaryText = secondaryMsg, isUpdateAnimated = true)
+    }
+
+    /** Shows the countdown message and refreshes it every second. */
+    private fun startLockoutCountdown() {
+        lockoutCountdownJob?.cancel()
+        lockoutCountdownJob =
+            applicationScope.launch {
+                authenticationInteractor.authenticationMethod.collectLatest { authMethod ->
+                    do {
+                        val remainingSeconds = remainingLockoutSeconds()
+                        val authLockedOutMsg =
+                            BouncerMessageStrings.primaryAuthLockedOut(authMethod)
+                        lockoutMessage.value =
+                            if (remainingSeconds > 0) {
+                                MessageViewModel(
+                                    text =
+                                        kg_too_many_failed_attempts_countdown.toPluralString(
+                                            mutableMapOf<String, Any>(
+                                                Pair("count", remainingSeconds)
+                                            )
+                                        ),
+                                    secondaryText = authLockedOutMsg.secondaryMessage.toResString(),
+                                    isUpdateAnimated = false
+                                )
+                            } else {
+                                null
+                            }
+                        delay(1.seconds)
+                    } while (remainingSeconds > 0)
+                    lockoutCountdownJob = null
+                }
+            }
+    }
+
+    private fun remainingLockoutSeconds(): Int {
+        val endTimestampMs = authenticationInteractor.lockoutEndTimestamp ?: 0
+        val remainingMs = max(0, endTimestampMs - clock.elapsedRealtime())
+        return ceil(remainingMs / 1000f).toInt()
+    }
+
+    private fun Int.toPluralString(formatterArgs: Map<String, Any>): String =
+        PluralsMessageFormatter.format(applicationContext.resources, formatterArgs, this)
+
+    private fun Int.toResString(): String = applicationContext.getString(this)
+
+    init {
+        if (flags.isComposeBouncerOrSceneContainerEnabled()) {
+            applicationScope.launch {
+                // Update the lockout countdown whenever the selected user is switched.
+                selectedUser.collect { startLockoutCountdown() }
+            }
+
+            defaultBouncerMessageInitializer()
+
+            listenForSimBouncerEvents()
+            listenForBouncerEvents()
+            listenForFaceMessages()
+            listenForFingerprintMessages()
+        }
+    }
+
+    companion object {
+        private const val MESSAGE_DURATION = 2000L
+    }
+}
+
+/** Data class that represents the status message show on the bouncer. */
+data class MessageViewModel(
+    val text: String,
+    val secondaryText: String? = null,
+    /**
+     * Whether updates to the message should be cross-animated from one message to another.
+     *
+     * If `false`, no animation should be applied, the message text should just be replaced
+     * instantly.
+     */
+    val isUpdateAnimated: Boolean = true,
+)
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@Module
+object BouncerMessageViewModelModule {
+
+    @Provides
+    @SysUISingleton
+    fun viewModel(
+        @Application applicationContext: Context,
+        @Application applicationScope: CoroutineScope,
+        bouncerInteractor: BouncerInteractor,
+        simBouncerInteractor: SimBouncerInteractor,
+        authenticationInteractor: AuthenticationInteractor,
+        clock: SystemClock,
+        biometricMessageInteractor: BiometricMessageInteractor,
+        faceAuthInteractor: DeviceEntryFaceAuthInteractor,
+        deviceEntryInteractor: DeviceEntryInteractor,
+        fingerprintInteractor: DeviceEntryFingerprintAuthInteractor,
+        flags: ComposeBouncerFlags,
+        userSwitcherViewModel: UserSwitcherViewModel,
+    ): BouncerMessageViewModel {
+        return BouncerMessageViewModel(
+            applicationContext = applicationContext,
+            applicationScope = applicationScope,
+            bouncerInteractor = bouncerInteractor,
+            simBouncerInteractor = simBouncerInteractor,
+            authenticationInteractor = authenticationInteractor,
+            clock = clock,
+            biometricMessageInteractor = biometricMessageInteractor,
+            faceAuthInteractor = faceAuthInteractor,
+            deviceEntryInteractor = deviceEntryInteractor,
+            fingerprintInteractor = fingerprintInteractor,
+            flags = flags,
+            selectedUser = userSwitcherViewModel.selectedUser,
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
index 6287578..5c07cc5 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
@@ -21,7 +21,6 @@
 import android.content.Context
 import android.graphics.Bitmap
 import androidx.core.graphics.drawable.toBitmap
-import com.android.internal.R
 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.authentication.shared.model.AuthenticationWipeModel
@@ -40,18 +39,12 @@
 import com.android.systemui.user.ui.viewmodel.UserActionViewModel
 import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
 import com.android.systemui.user.ui.viewmodel.UserViewModel
-import com.android.systemui.util.time.SystemClock
 import dagger.Module
 import dagger.Provides
-import kotlin.math.ceil
-import kotlin.math.max
-import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Job
 import kotlinx.coroutines.SupervisorJob
 import kotlinx.coroutines.cancel
-import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
@@ -72,13 +65,13 @@
     private val simBouncerInteractor: SimBouncerInteractor,
     private val authenticationInteractor: AuthenticationInteractor,
     private val selectedUserInteractor: SelectedUserInteractor,
+    private val devicePolicyManager: DevicePolicyManager,
+    bouncerMessageViewModel: BouncerMessageViewModel,
     flags: ComposeBouncerFlags,
     selectedUser: Flow<UserViewModel>,
     users: Flow<List<UserViewModel>>,
     userSwitcherMenu: Flow<List<UserActionViewModel>>,
     actionButton: Flow<BouncerActionButtonModel?>,
-    private val clock: SystemClock,
-    private val devicePolicyManager: DevicePolicyManager,
 ) {
     val selectedUserImage: StateFlow<Bitmap?> =
         selectedUser
@@ -89,6 +82,8 @@
                 initialValue = null,
             )
 
+    val message: BouncerMessageViewModel = bouncerMessageViewModel
+
     val userSwitcherDropdown: StateFlow<List<UserSwitcherDropdownItemViewModel>> =
         combine(
                 users,
@@ -163,24 +158,6 @@
             )
 
     /**
-     * A message shown when the user has attempted the wrong credential too many times and now must
-     * wait a while before attempting to authenticate again.
-     *
-     * This is updated every second (countdown) during the lockout duration. When lockout is not
-     * active, this is `null` and no lockout message should be shown.
-     */
-    private val lockoutMessage = MutableStateFlow<String?>(null)
-
-    /** The user-facing message to show in the bouncer. */
-    val message: StateFlow<MessageViewModel> =
-        combine(bouncerInteractor.message, lockoutMessage) { _, _ -> createMessageViewModel() }
-            .stateIn(
-                scope = applicationScope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = createMessageViewModel(),
-            )
-
-    /**
      * The bouncer action button (Return to Call / Emergency Call). If `null`, the button should not
      * be shown.
      */
@@ -222,31 +199,16 @@
             )
 
     private val isInputEnabled: StateFlow<Boolean> =
-        lockoutMessage
-            .map { it == null }
+        bouncerMessageViewModel.isLockoutMessagePresent
+            .map { lockoutMessagePresent -> !lockoutMessagePresent }
             .stateIn(
                 scope = applicationScope,
                 started = SharingStarted.WhileSubscribed(),
                 initialValue = authenticationInteractor.lockoutEndTimestamp == null,
             )
 
-    private var lockoutCountdownJob: Job? = null
-
     init {
         if (flags.isComposeBouncerOrSceneContainerEnabled()) {
-            // Keeps the lockout dialog up-to-date.
-            applicationScope.launch {
-                bouncerInteractor.onLockoutStarted.collect {
-                    showLockoutDialog()
-                    startLockoutCountdown()
-                }
-            }
-
-            applicationScope.launch {
-                // Update the lockout countdown whenever the selected user is switched.
-                selectedUser.collect { startLockoutCountdown() }
-            }
-
             // Keeps the upcoming wipe dialog up-to-date.
             applicationScope.launch {
                 authenticationInteractor.upcomingWipe.collect { wipeModel ->
@@ -256,48 +218,6 @@
         }
     }
 
-    private fun showLockoutDialog() {
-        applicationScope.launch {
-            val failedAttempts = authenticationInteractor.failedAuthenticationAttempts.value
-            lockoutDialogMessage.value =
-                authMethodViewModel.value?.lockoutMessageId?.let { messageId ->
-                    applicationContext.getString(
-                        messageId,
-                        failedAttempts,
-                        remainingLockoutSeconds()
-                    )
-                }
-        }
-    }
-
-    /** Shows the countdown message and refreshes it every second. */
-    private fun startLockoutCountdown() {
-        lockoutCountdownJob?.cancel()
-        lockoutCountdownJob =
-            applicationScope.launch {
-                do {
-                    val remainingSeconds = remainingLockoutSeconds()
-                    lockoutMessage.value =
-                        if (remainingSeconds > 0) {
-                            applicationContext.getString(
-                                R.string.lockscreen_too_many_failed_attempts_countdown,
-                                remainingSeconds,
-                            )
-                        } else {
-                            null
-                        }
-                    delay(1.seconds)
-                } while (remainingSeconds > 0)
-                lockoutCountdownJob = null
-            }
-    }
-
-    private fun remainingLockoutSeconds(): Int {
-        val endTimestampMs = authenticationInteractor.lockoutEndTimestamp ?: 0
-        val remainingMs = max(0, endTimestampMs - clock.elapsedRealtime())
-        return ceil(remainingMs / 1000f).toInt()
-    }
-
     private fun isSideBySideSupported(authMethod: AuthMethodBouncerViewModel?): Boolean {
         return isUserSwitcherVisible || authMethod !is PasswordBouncerViewModel
     }
@@ -306,15 +226,6 @@
         return authMethod !is PasswordBouncerViewModel
     }
 
-    private fun createMessageViewModel(): MessageViewModel {
-        val isLockedOut = lockoutMessage.value != null
-        return MessageViewModel(
-            // A lockout message takes precedence over the non-lockout message.
-            text = lockoutMessage.value ?: bouncerInteractor.message.value ?: "",
-            isUpdateAnimated = !isLockedOut,
-        )
-    }
-
     private fun getChildViewModel(
         authenticationMethod: AuthenticationMethodModel,
     ): AuthMethodBouncerViewModel? {
@@ -336,7 +247,8 @@
                     interactor = bouncerInteractor,
                     isInputEnabled = isInputEnabled,
                     simBouncerInteractor = simBouncerInteractor,
-                    authenticationMethod = authenticationMethod
+                    authenticationMethod = authenticationMethod,
+                    onIntentionalUserInput = ::onIntentionalUserInput
                 )
             is AuthenticationMethodModel.Sim ->
                 PinBouncerViewModel(
@@ -346,6 +258,7 @@
                     isInputEnabled = isInputEnabled,
                     simBouncerInteractor = simBouncerInteractor,
                     authenticationMethod = authenticationMethod,
+                    onIntentionalUserInput = ::onIntentionalUserInput
                 )
             is AuthenticationMethodModel.Password ->
                 PasswordBouncerViewModel(
@@ -354,6 +267,7 @@
                     interactor = bouncerInteractor,
                     inputMethodInteractor = inputMethodInteractor,
                     selectedUserInteractor = selectedUserInteractor,
+                    onIntentionalUserInput = ::onIntentionalUserInput
                 )
             is AuthenticationMethodModel.Pattern ->
                 PatternBouncerViewModel(
@@ -361,11 +275,17 @@
                     viewModelScope = newViewModelScope,
                     interactor = bouncerInteractor,
                     isInputEnabled = isInputEnabled,
+                    onIntentionalUserInput = ::onIntentionalUserInput
                 )
             else -> null
         }
     }
 
+    private fun onIntentionalUserInput() {
+        message.showDefaultMessage()
+        bouncerInteractor.onIntentionalUserInput()
+    }
+
     private fun createChildCoroutineScope(parentScope: CoroutineScope): CoroutineScope {
         return CoroutineScope(
             SupervisorJob(parent = parentScope.coroutineContext.job) + mainDispatcher
@@ -437,18 +357,6 @@
         }
     }
 
-    data class MessageViewModel(
-        val text: String,
-
-        /**
-         * Whether updates to the message should be cross-animated from one message to another.
-         *
-         * If `false`, no animation should be applied, the message text should just be replaced
-         * instantly.
-         */
-        val isUpdateAnimated: Boolean,
-    )
-
     data class DialogViewModel(
         val text: String,
 
@@ -480,8 +388,8 @@
         selectedUserInteractor: SelectedUserInteractor,
         flags: ComposeBouncerFlags,
         userSwitcherViewModel: UserSwitcherViewModel,
-        clock: SystemClock,
         devicePolicyManager: DevicePolicyManager,
+        bouncerMessageViewModel: BouncerMessageViewModel,
     ): BouncerViewModel {
         return BouncerViewModel(
             applicationContext = applicationContext,
@@ -497,8 +405,8 @@
             users = userSwitcherViewModel.users,
             userSwitcherMenu = userSwitcherViewModel.menu,
             actionButton = actionButtonInteractor.actionButton,
-            clock = clock,
             devicePolicyManager = devicePolicyManager,
+            bouncerMessageViewModel = bouncerMessageViewModel,
         )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
index b42eda1..052fb6b 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
@@ -40,6 +40,7 @@
     viewModelScope: CoroutineScope,
     isInputEnabled: StateFlow<Boolean>,
     interactor: BouncerInteractor,
+    private val onIntentionalUserInput: () -> Unit,
     private val inputMethodInteractor: InputMethodInteractor,
     private val selectedUserInteractor: SelectedUserInteractor,
 ) :
@@ -96,12 +97,8 @@
 
     /** Notifies that the user has changed the password input. */
     fun onPasswordInputChanged(newPassword: String) {
-        if (this.password.value.isEmpty() && newPassword.isNotEmpty()) {
-            interactor.clearMessage()
-        }
-
         if (newPassword.isNotEmpty()) {
-            interactor.onIntentionalUserInput()
+            onIntentionalUserInput()
         }
 
         _password.value = newPassword
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
index 69f8032..a401600 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
@@ -40,6 +40,7 @@
     viewModelScope: CoroutineScope,
     interactor: BouncerInteractor,
     isInputEnabled: StateFlow<Boolean>,
+    private val onIntentionalUserInput: () -> Unit,
 ) :
     AuthMethodBouncerViewModel(
         viewModelScope = viewModelScope,
@@ -84,7 +85,7 @@
 
     /** Notifies that the user has started a drag gesture across the dot grid. */
     fun onDragStart() {
-        interactor.clearMessage()
+        onIntentionalUserInput()
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
index e910a92..62da5c0 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
@@ -41,6 +41,7 @@
     viewModelScope: CoroutineScope,
     interactor: BouncerInteractor,
     isInputEnabled: StateFlow<Boolean>,
+    private val onIntentionalUserInput: () -> Unit,
     private val simBouncerInteractor: SimBouncerInteractor,
     authenticationMethod: AuthenticationMethodModel,
 ) :
@@ -131,11 +132,8 @@
     /** Notifies that the user clicked on a PIN button with the given digit value. */
     fun onPinButtonClicked(input: Int) {
         val pinInput = mutablePinInput.value
-        if (pinInput.isEmpty()) {
-            interactor.clearMessage()
-        }
 
-        interactor.onIntentionalUserInput()
+        onIntentionalUserInput()
 
         mutablePinInput.value = pinInput.append(input)
         tryAuthenticate(useAutoConfirm = true)
@@ -149,7 +147,6 @@
     /** Notifies that the user long-pressed the backspace button. */
     fun onBackspaceButtonLongPressed() {
         clearInput()
-        interactor.clearMessage()
     }
 
     /** Notifies that the user clicked the "enter" button. */
@@ -173,7 +170,6 @@
     /** Resets the sim screen and shows a default message. */
     private fun onResetSimFlow() {
         simBouncerInteractor.resetSimPukUserInput()
-        interactor.resetMessage()
         clearInput()
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/NotificationContainerBounds.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/NotificationContainerBounds.kt
index 3063ebd..fdd98bec 100644
--- a/packages/SystemUI/src/com/android/systemui/common/shared/model/NotificationContainerBounds.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/NotificationContainerBounds.kt
@@ -18,12 +18,8 @@
 
 /** Models the bounds of the notification container. */
 data class NotificationContainerBounds(
-    /** The position of the left of the container in its window coordinate system, in pixels. */
-    val left: Float = 0f,
     /** The position of the top of the container in its window coordinate system, in pixels. */
     val top: Float = 0f,
-    /** The position of the right of the container in its window coordinate system, in pixels. */
-    val right: Float = 0f,
     /** The position of the bottom of the container in its window coordinate system, in pixels. */
     val bottom: Float = 0f,
     /** Whether any modifications to top/bottom should be smoothly animated. */
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt
index 8059993..c4e0ef7 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt
@@ -29,6 +29,8 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.filterIsInstance
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 
 @OptIn(ExperimentalCoroutinesApi::class)
@@ -72,4 +74,14 @@
      */
     val isSensorUnderDisplay =
         fingerprintPropertyRepository.sensorType.map(FingerprintSensorType::isUdfps)
+
+    /** Whether fingerprint authentication is currently allowed while on the bouncer. */
+    val isFingerprintCurrentlyAllowedOnBouncer =
+        isSensorUnderDisplay.flatMapLatest { sensorBelowDisplay ->
+            if (sensorBelowDisplay) {
+                flowOf(false)
+            } else {
+                isFingerprintAuthCurrentlyAllowed
+            }
+        }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
index 9a6088d..7f752b4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
@@ -231,5 +231,6 @@
         private val DEFAULT_DURATION = 500.milliseconds
         val TO_GLANCEABLE_HUB_DURATION = 1.seconds
         val TO_LOCKSCREEN_DURATION = 1167.milliseconds
+        val TO_GONE_DURATION = DEFAULT_DURATION
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
index b8ba098..5de1a61 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
@@ -17,10 +17,10 @@
 
 import android.view.animation.Interpolator
 import com.android.app.animation.Interpolators.LINEAR
-import com.android.app.tracing.coroutines.launch
 import com.android.keyguard.logging.KeyguardTransitionAnimationLogger
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.Edge
 import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -35,6 +35,7 @@
 import kotlin.math.min
 import kotlin.time.Duration
 import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.BufferOverflow
 import kotlinx.coroutines.flow.Flow
@@ -42,6 +43,7 @@
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.launch
 
 /**
  * Assists in creating sub-flows for a KeyguardTransition. Call [setup] once for a transition, and
@@ -52,13 +54,14 @@
 @Inject
 constructor(
     @Application private val scope: CoroutineScope,
+    @Main private val mainDispatcher: CoroutineDispatcher,
     private val transitionInteractor: KeyguardTransitionInteractor,
     private val logger: KeyguardTransitionAnimationLogger,
 ) {
     private val transitionMap = mutableMapOf<Edge, MutableSharedFlow<TransitionStep>>()
 
     init {
-        scope.launch("KeyguardTransitionAnimationFlow") {
+        scope.launch(mainDispatcher) {
             transitionInteractor.transitions.collect {
                 // FROM->TO
                 transitionMap[Edge(it.from, it.to)]?.emit(it)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGoneTransitionViewModel.kt
new file mode 100644
index 0000000..ec7b931
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGoneTransitionViewModel.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import kotlinx.coroutines.flow.Flow
+import javax.inject.Inject
+
+@SysUISingleton
+class DreamingToGoneTransitionViewModel
+@Inject
+constructor(
+    animationFlow: KeyguardTransitionAnimationFlow,
+) {
+
+    private val transitionAnimation =
+            animationFlow.setup(
+                duration = FromDreamingTransitionInteractor.TO_GONE_DURATION,
+                from = KeyguardState.DREAMING,
+                to = KeyguardState.GONE,
+            )
+
+    /** Lockscreen views alpha */
+    val lockscreenAlpha: Flow<Float> = transitionAnimation.immediatelyTransitionTo(0f)
+
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index 55a4025..301f00e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -85,10 +85,12 @@
     private val dozingToLockscreenTransitionViewModel: DozingToLockscreenTransitionViewModel,
     private val dozingToOccludedTransitionViewModel: DozingToOccludedTransitionViewModel,
     private val dreamingToLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel,
+    private val dreamingToGoneTransitionViewModel: DreamingToGoneTransitionViewModel,
     private val glanceableHubToLockscreenTransitionViewModel:
         GlanceableHubToLockscreenTransitionViewModel,
     private val goneToAodTransitionViewModel: GoneToAodTransitionViewModel,
     private val goneToDozingTransitionViewModel: GoneToDozingTransitionViewModel,
+    private val goneToDreamingTransitionViewModel: GoneToDreamingTransitionViewModel,
     private val lockscreenToAodTransitionViewModel: LockscreenToAodTransitionViewModel,
     private val lockscreenToDozingTransitionViewModel: LockscreenToDozingTransitionViewModel,
     private val lockscreenToDreamingTransitionViewModel: LockscreenToDreamingTransitionViewModel,
@@ -136,14 +138,20 @@
             }
             .distinctUntilChanged()
 
+    private val lockscreenToGoneTransitionRunning: Flow<Boolean> =
+        keyguardTransitionInteractor
+            .isInTransitionWhere { from, to -> from == LOCKSCREEN && to == GONE }
+            .onStart { emit(false) }
+
     private val alphaOnShadeExpansion: Flow<Float> =
         combineTransform(
+                lockscreenToGoneTransitionRunning,
                 isOnLockscreen,
                 shadeInteractor.qsExpansion,
                 shadeInteractor.shadeExpansion,
-            ) { isOnLockscreen, qsExpansion, shadeExpansion ->
+            ) { lockscreenToGoneTransitionRunning, isOnLockscreen, qsExpansion, shadeExpansion ->
                 // Fade out quickly as the shade expands
-                if (isOnLockscreen) {
+                if (isOnLockscreen && !lockscreenToGoneTransitionRunning) {
                     val alpha =
                         1f -
                             MathUtils.constrainedMap(
@@ -204,10 +212,12 @@
                         dozingToGoneTransitionViewModel.lockscreenAlpha(viewState),
                         dozingToLockscreenTransitionViewModel.lockscreenAlpha,
                         dozingToOccludedTransitionViewModel.lockscreenAlpha(viewState),
+                        dreamingToGoneTransitionViewModel.lockscreenAlpha,
                         dreamingToLockscreenTransitionViewModel.lockscreenAlpha,
                         glanceableHubToLockscreenTransitionViewModel.keyguardAlpha,
                         goneToAodTransitionViewModel.enterFromTopAnimationAlpha,
                         goneToDozingTransitionViewModel.lockscreenAlpha,
+                        goneToDreamingTransitionViewModel.lockscreenAlpha,
                         lockscreenToAodTransitionViewModel.lockscreenAlpha(viewState),
                         lockscreenToDozingTransitionViewModel.lockscreenAlpha,
                         lockscreenToDreamingTransitionViewModel.lockscreenAlpha,
diff --git a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java
index 1d820a1..0a880293 100644
--- a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java
+++ b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java
@@ -21,6 +21,9 @@
 import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL;
 import static android.app.NotificationManager.INTERRUPTION_FILTER_NONE;
 import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
+import static android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN;
+import static android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD;
+import static android.appwidget.flags.Flags.generatedPreviews;
 import static android.content.Intent.ACTION_BOOT_COMPLETED;
 import static android.content.Intent.ACTION_PACKAGE_ADDED;
 import static android.content.Intent.ACTION_PACKAGE_REMOVED;
@@ -56,6 +59,7 @@
 import android.app.people.PeopleManager;
 import android.app.people.PeopleSpaceTile;
 import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProviderInfo;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
@@ -80,12 +84,15 @@
 import android.service.notification.ZenModeConfig;
 import android.text.TextUtils;
 import android.util.Log;
+import android.util.SparseBooleanArray;
 import android.widget.RemoteViews;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.UiEventLogger;
 import com.android.internal.logging.UiEventLoggerImpl;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.keyguard.KeyguardUpdateMonitorCallback;
 import com.android.systemui.Dumpable;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.SysUISingleton;
@@ -96,6 +103,8 @@
 import com.android.systemui.people.PeopleSpaceUtils;
 import com.android.systemui.people.PeopleTileViewHelper;
 import com.android.systemui.people.SharedPreferencesHelper;
+import com.android.systemui.res.R;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.NotificationListener;
 import com.android.systemui.statusbar.NotificationListener.NotificationHandler;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -160,13 +169,27 @@
     @GuardedBy("mLock")
     public static Map<Integer, PeopleSpaceTile> mTiles = new HashMap<>();
 
+    @NonNull private final UserTracker mUserTracker;
+    @NonNull private final SparseBooleanArray mUpdatedPreviews = new SparseBooleanArray();
+    @NonNull private final KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback =
+            new KeyguardUpdateMonitorCallback() {
+                @Override
+                public void onUserUnlocked() {
+                    if (DEBUG) {
+                        Log.d(TAG, "onUserUnlocked " + mUserTracker.getUserId());
+                    }
+                    updateGeneratedPreviewForUser(mUserTracker.getUserHandle());
+                }
+            };
+
     @Inject
     public PeopleSpaceWidgetManager(Context context, LauncherApps launcherApps,
             CommonNotifCollection notifCollection,
             PackageManager packageManager, Optional<Bubbles> bubblesOptional,
             UserManager userManager, NotificationManager notificationManager,
             BroadcastDispatcher broadcastDispatcher, @Background Executor bgExecutor,
-            DumpManager dumpManager) {
+            DumpManager dumpManager, @NonNull UserTracker userTracker,
+            @NonNull KeyguardUpdateMonitor keyguardUpdateMonitor) {
         if (DEBUG) Log.d(TAG, "constructor");
         mContext = context;
         mAppWidgetManager = AppWidgetManager.getInstance(context);
@@ -187,6 +210,8 @@
         mBroadcastDispatcher = broadcastDispatcher;
         mBgExecutor = bgExecutor;
         dumpManager.registerNormalDumpable(TAG, this);
+        mUserTracker = userTracker;
+        keyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback);
     }
 
     /** Initializes {@PeopleSpaceWidgetManager}. */
@@ -246,7 +271,7 @@
             CommonNotifCollection notifCollection, PackageManager packageManager,
             Optional<Bubbles> bubblesOptional, UserManager userManager, BackupManager backupManager,
             INotificationManager iNotificationManager, NotificationManager notificationManager,
-            @Background Executor executor) {
+            @Background Executor executor, UserTracker userTracker) {
         mContext = context;
         mAppWidgetManager = appWidgetManager;
         mIPeopleManager = iPeopleManager;
@@ -262,6 +287,7 @@
         mManager = this;
         mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(context);
         mBgExecutor = executor;
+        mUserTracker = userTracker;
     }
 
     /**
@@ -1407,4 +1433,32 @@
 
         Trace.traceEnd(Trace.TRACE_TAG_APP);
     }
+
+    @VisibleForTesting
+    void updateGeneratedPreviewForUser(UserHandle user) {
+        if (!generatedPreviews() || mUpdatedPreviews.get(user.getIdentifier())
+                || !mUserManager.isUserUnlocked(user)) {
+            return;
+        }
+
+        // The widget provider may be disabled on SystemUI implementers, e.g. TvSystemUI.
+        ComponentName provider = new ComponentName(mContext, PeopleSpaceWidgetProvider.class);
+        List<AppWidgetProviderInfo> infos = mAppWidgetManager.getInstalledProvidersForPackage(
+                mContext.getPackageName(), user);
+        if (infos.stream().noneMatch(info -> info.provider.equals(provider))) {
+            return;
+        }
+
+        if (DEBUG) {
+            Log.d(TAG, "Updating People Space widget preview for user " + user.getIdentifier());
+        }
+        boolean success = mAppWidgetManager.setWidgetPreview(
+                provider, WIDGET_CATEGORY_HOME_SCREEN | WIDGET_CATEGORY_KEYGUARD,
+                new RemoteViews(mContext.getPackageName(),
+                        R.layout.people_space_placeholder_layout));
+        if (DEBUG && !success) {
+            Log.d(TAG, "Failed to update generated preview for user " + user.getIdentifier());
+        }
+        mUpdatedPreviews.put(user.getIdentifier(), success);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index 7a7ee59..00757b7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -127,8 +127,9 @@
 
     }
 
-    void initialize(QSLogger qsLogger) {
+    void initialize(QSLogger qsLogger, boolean usingMediaPlayer) {
         mQsLogger = qsLogger;
+        mUsingMediaPlayer = usingMediaPlayer;
         mTileLayout = getOrCreateTileLayout();
 
         if (mUsingMediaPlayer) {
@@ -163,22 +164,25 @@
     }
 
     protected void setHorizontalContentContainerClipping() {
-        mHorizontalContentContainer.setClipChildren(true);
-        mHorizontalContentContainer.setClipToPadding(false);
-        // Don't clip on the top, that way, secondary pages tiles can animate up
-        // Clipping coordinates should be relative to this view, not absolute (parent coordinates)
-        mHorizontalContentContainer.addOnLayoutChangeListener(
-                (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
-                    if ((right - left) != (oldRight - oldLeft)
-                            || ((bottom - top) != (oldBottom - oldTop))) {
-                        mClippingRect.right = right - left;
-                        mClippingRect.bottom = bottom - top;
-                        mHorizontalContentContainer.setClipBounds(mClippingRect);
-                    }
-                });
-        mClippingRect.left = 0;
-        mClippingRect.top = -1000;
-        mHorizontalContentContainer.setClipBounds(mClippingRect);
+        if (mHorizontalContentContainer != null) {
+            mHorizontalContentContainer.setClipChildren(true);
+            mHorizontalContentContainer.setClipToPadding(false);
+            // Don't clip on the top, that way, secondary pages tiles can animate up
+            // Clipping coordinates should be relative to this view, not absolute
+            // (parent coordinates)
+            mHorizontalContentContainer.addOnLayoutChangeListener(
+                    (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
+                        if ((right - left) != (oldRight - oldLeft)
+                                || ((bottom - top) != (oldBottom - oldTop))) {
+                            mClippingRect.right = right - left;
+                            mClippingRect.bottom = bottom - top;
+                            mHorizontalContentContainer.setClipBounds(mClippingRect);
+                        }
+                    });
+            mClippingRect.left = 0;
+            mClippingRect.top = -1000;
+            mHorizontalContentContainer.setClipBounds(mClippingRect);
+        }
     }
 
     /**
@@ -412,7 +416,7 @@
     }
 
     private void updateHorizontalLinearLayoutMargins() {
-        if (mHorizontalLinearLayout != null && !displayMediaMarginsOnMedia()) {
+        if (mUsingMediaPlayer && mHorizontalLinearLayout != null && !displayMediaMarginsOnMedia()) {
             LayoutParams lp = (LayoutParams) mHorizontalLinearLayout.getLayoutParams();
             lp.bottomMargin = Math.max(mMediaTotalBottomMargin - getPaddingBottom(), 0);
             mHorizontalLinearLayout.setLayoutParams(lp);
@@ -461,6 +465,11 @@
     /** Call when orientation has changed and MediaHost needs to be adjusted. */
     private void reAttachMediaHost(ViewGroup hostView, boolean horizontal) {
         if (!mUsingMediaPlayer) {
+            // If the host view was attached, detach it.
+            ViewGroup parent = (ViewGroup) hostView.getParent();
+            if (parent != null) {
+                parent.removeView(hostView);
+            }
             return;
         }
         mMediaHostView = hostView;
@@ -492,8 +501,10 @@
     public void setExpanded(boolean expanded) {
         if (mExpanded == expanded) return;
         mExpanded = expanded;
-        if (!mExpanded && mTileLayout instanceof PagedTileLayout) {
-            ((PagedTileLayout) mTileLayout).setCurrentItem(0, false);
+        if (!mExpanded && mTileLayout instanceof PagedTileLayout tilesLayout) {
+            // Use post, so it will wait until the view is attached. If the view is not attached,
+            // it will not populate corresponding views (and will not do it later when attached).
+            tilesLayout.post(() -> tilesLayout.setCurrentItem(0, false));
         }
     }
 
@@ -616,7 +627,10 @@
         if (horizontal != mUsingHorizontalLayout || force) {
             Log.d(getDumpableTag(), "setUsingHorizontalLayout: " + horizontal + ", " + force);
             mUsingHorizontalLayout = horizontal;
-            ViewGroup newParent = horizontal ? mHorizontalContentContainer : this;
+            // The tile layout should be reparented if horizontal and we are using media. If not
+            // using media, the parent should always be this.
+            ViewGroup newParent =
+                    horizontal && mUsingMediaPlayer ? mHorizontalContentContainer : this;
             switchAllContentToParent(newParent, mTileLayout);
             reAttachMediaHost(mediaHostView, horizontal);
             if (needsDynamicRowsAndColumns()) {
@@ -624,7 +638,9 @@
                 mTileLayout.setMaxColumns(horizontal ? 2 : 4);
             }
             updateMargins(mediaHostView);
-            mHorizontalLinearLayout.setVisibility(horizontal ? View.VISIBLE : View.GONE);
+            if (mHorizontalLinearLayout != null) {
+                mHorizontalLinearLayout.setVisibility(horizontal ? View.VISIBLE : View.GONE);
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
index 5e12b9d..d8e8187 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
@@ -167,7 +167,7 @@
 
     @Override
     protected void onInit() {
-        mView.initialize(mQSLogger);
+        mView.initialize(mQSLogger, mUsingMediaPlayer);
         mQSLogger.logAllTilesChangeListening(mView.isListening(), mView.getDumpableTag(), "");
         mHost.addCallback(mQSHostCallback);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
index c1b2037..6710504 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
@@ -23,16 +23,21 @@
 import androidx.annotation.VisibleForTesting
 import androidx.asynclayoutinflater.view.AsyncLayoutInflater
 import com.android.settingslib.applications.InterestingConfigChanges
+import com.android.systemui.Dumpable
 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.qs.QSContainerController
 import com.android.systemui.qs.QSContainerImpl
 import com.android.systemui.qs.QSImpl
 import com.android.systemui.qs.dagger.QSSceneComponent
 import com.android.systemui.res.R
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.util.kotlin.sample
+import java.io.PrintWriter
 import javax.inject.Inject
 import javax.inject.Provider
 import kotlin.coroutines.resume
@@ -107,11 +112,17 @@
         }
 
         /** State for appearing QQS from Lockscreen or Gone */
-        data class Unsquishing(override val squishiness: Float) : State {
+        data class UnsquishingQQS(override val squishiness: Float) : State {
             override val isVisible = true
             override val expansion = 0f
         }
 
+        /** State for appearing QS from Lockscreen or Gone, used in Split shade */
+        data class UnsquishingQS(override val squishiness: Float) : State {
+            override val isVisible = true
+            override val expansion = 1f
+        }
+
         companion object {
             // These are special cases of the expansion.
             val QQS = Expanding(0f)
@@ -129,22 +140,28 @@
 constructor(
     private val qsSceneComponentFactory: QSSceneComponent.Factory,
     private val qsImplProvider: Provider<QSImpl>,
+    shadeInteractor: ShadeInteractor,
+    dumpManager: DumpManager,
     @Main private val mainDispatcher: CoroutineDispatcher,
     @Application applicationScope: CoroutineScope,
     private val configurationInteractor: ConfigurationInteractor,
     private val asyncLayoutInflaterFactory: (Context) -> AsyncLayoutInflater,
-) : QSContainerController, QSSceneAdapter {
+) : QSContainerController, QSSceneAdapter, Dumpable {
 
     @Inject
     constructor(
         qsSceneComponentFactory: QSSceneComponent.Factory,
         qsImplProvider: Provider<QSImpl>,
+        shadeInteractor: ShadeInteractor,
+        dumpManager: DumpManager,
         @Main dispatcher: CoroutineDispatcher,
         @Application scope: CoroutineScope,
         configurationInteractor: ConfigurationInteractor,
     ) : this(
         qsSceneComponentFactory,
         qsImplProvider,
+        shadeInteractor,
+        dumpManager,
         dispatcher,
         scope,
         configurationInteractor,
@@ -182,6 +199,7 @@
         )
 
     init {
+        dumpManager.registerDumpable(this)
         applicationScope.launch {
             launch {
                 state.sample(_isCustomizing, ::Pair).collect { (state, customizing) ->
@@ -210,6 +228,11 @@
                     it.second.applyBottomNavBarToCustomizerPadding(it.first)
                 }
             }
+            launch {
+                shadeInteractor.shadeMode.collect {
+                    qsImpl.value?.setInSplitShade(it == ShadeMode.Split)
+                }
+            }
         }
     }
 
@@ -256,9 +279,17 @@
 
     private fun QSImpl.applyState(state: QSSceneAdapter.State) {
         setQsVisible(state.isVisible)
-        setExpanded(state.isVisible)
+        setExpanded(state.isVisible && state.expansion > 0f)
         setListening(state.isVisible)
         setQsExpansion(state.expansion, 1f, 0f, state.squishiness)
-        setTransitionToFullShadeProgress(false, 1f, state.squishiness)
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        pw.apply {
+            println("Last state: ${state.value}")
+            println("Customizing: ${isCustomizing.value}")
+            println("QQS height: $qqsHeight")
+            println("QS height: $qsHeight")
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
index 34f66b8..c695d4c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
@@ -48,6 +48,8 @@
         qsSceneAdapter.isCustomizing.map { customizing ->
             if (customizing) {
                 mapOf<UserAction, UserActionResult>(Back to UserActionResult(Scenes.QuickSettings))
+                // TODO(b/330200163) Add an Up from Bottom to be able to collapse the shade
+                // while customizing
             } else {
                 mapOf(
                     Back to UserActionResult(Scenes.Shade),
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index d0ff338..7c1a2c0 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -86,7 +86,6 @@
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.KeyguardWmStateRefactor;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
@@ -146,7 +145,6 @@
     private static final long MAX_BACKOFF_MILLIS = 10 * 60 * 1000;
 
     private final Context mContext;
-    private final FeatureFlags mFeatureFlags;
     private final SceneContainerFlags mSceneContainerFlags;
     private final Executor mMainExecutor;
     private final ShellInterface mShellInterface;
@@ -209,8 +207,10 @@
         @Override
         public void onStatusBarTouchEvent(MotionEvent event) {
             verifyCallerAndClearCallingIdentity("onStatusBarTouchEvent", () -> {
-                // TODO move this logic to message queue
-                if (event.getActionMasked() == ACTION_DOWN) {
+                if (mSceneContainerFlags.isEnabled()) {
+                    //TODO(b/329863123) implement latency tracking for shade scene
+                    Log.i(TAG_OPS, "Scene container enabled. Latency tracking not started.");
+                } else if (event.getActionMasked() == ACTION_DOWN) {
                     mShadeViewControllerLazy.get().startExpandLatencyTracking();
                 }
                 mHandler.post(() -> {
@@ -600,7 +600,6 @@
             KeyguardUnlockAnimationController sysuiUnlockAnimationController,
             InWindowLauncherUnlockAnimationManager inWindowLauncherUnlockAnimationManager,
             AssistUtils assistUtils,
-            FeatureFlags featureFlags,
             SceneContainerFlags sceneContainerFlags,
             DumpManager dumpManager,
             Optional<UnfoldTransitionProgressForwarder> unfoldTransitionProgressForwarder,
@@ -613,7 +612,6 @@
         }
 
         mContext = context;
-        mFeatureFlags = featureFlags;
         mSceneContainerFlags = sceneContainerFlags;
         mMainExecutor = mainExecutor;
         mShellInterface = shellInterface;
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 3a2a081..9cb920a 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -3257,7 +3257,6 @@
         mKeyguardStatusViewController.setStatusAccessibilityImportance(mode);
     }
 
-    @Override
     public void performHapticFeedback(int constant) {
         mVibratorHelper.performHapticFeedback(mView, constant);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
index 8ba0544..8dbcead 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
@@ -1280,18 +1280,20 @@
 
         mScrimController.setScrimCornerRadius(radius);
 
-        // Convert global clipping coordinates to local ones,
-        // relative to NotificationStackScrollLayout
-        int nsslLeft = calculateNsslLeft(left);
-        int nsslRight = calculateNsslRight(right);
-        int nsslTop = getNotificationsClippingTopBounds(top);
-        int nsslBottom = bottom - mNotificationStackScrollLayoutController.getTop();
-        int bottomRadius = mSplitShadeEnabled ? radius : 0;
-        // TODO (b/265193930): remove dependency on NPVC
-        int topRadius = mSplitShadeEnabled
-                && mPanelViewControllerLazy.get().isExpandingFromHeadsUp() ? 0 : radius;
-        mNotificationStackScrollLayoutController.setRoundedClippingBounds(
-                nsslLeft, nsslTop, nsslRight, nsslBottom, topRadius, bottomRadius);
+        if (!SceneContainerFlag.isEnabled()) {
+            // Convert global clipping coordinates to local ones,
+            // relative to NotificationStackScrollLayout
+            int nsslLeft = calculateNsslLeft(left);
+            int nsslRight = calculateNsslRight(right);
+            int nsslTop = getNotificationsClippingTopBounds(top);
+            int nsslBottom = bottom - mNotificationStackScrollLayoutController.getTop();
+            int bottomRadius = mSplitShadeEnabled ? radius : 0;
+            // TODO (b/265193930): remove dependency on NPVC
+            int topRadius = mSplitShadeEnabled
+                    && mPanelViewControllerLazy.get().isExpandingFromHeadsUp() ? 0 : radius;
+            mNotificationStackScrollLayoutController.setRoundedClippingBounds(
+                    nsslLeft, nsslTop, nsslRight, nsslBottom, topRadius, bottomRadius);
+        }
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java b/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java
index 0a57b64..813df11 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java
@@ -232,6 +232,13 @@
     /** Called when a launch animation ends. */
     void onLaunchAnimationEnd(boolean launchIsFullScreen);
 
+    /**
+     * Performs haptic feedback from a view with a haptic feedback constant.
+     *
+     * @param constant One of android.view.HapticFeedbackConstants
+     */
+    void performHapticFeedback(int constant);
+
     /** Sets the listener for when the visibility of the shade changes. */
     default void setVisibilityListener(ShadeVisibilityListener listener) {}
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerEmptyImpl.kt
index 093690f..d703a27 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerEmptyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerEmptyImpl.kt
@@ -63,4 +63,5 @@
     override fun onStatusBarTouch(event: MotionEvent?) {}
     override fun onLaunchAnimationCancelled(isLaunchForActivity: Boolean) {}
     override fun onLaunchAnimationEnd(launchIsFullScreen: Boolean) {}
+    override fun performHapticFeedback(constant: Int) {}
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java
index d99d607..5f5e5ce 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java
@@ -271,6 +271,11 @@
     }
 
     @Override
+    public void performHapticFeedback(int constant) {
+        getNpvc().performHapticFeedback(constant);
+    }
+
+    @Override
     public void instantCollapseShade() {
         getNpvc().instantCollapse();
         runPostCollapseActions();
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
index 177c3db..c20efea 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
@@ -33,6 +33,7 @@
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.NotificationShadeWindowController
+import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
 import dagger.Lazy
@@ -62,6 +63,7 @@
     private val deviceEntryInteractor: DeviceEntryInteractor,
     private val notificationStackScrollLayout: NotificationStackScrollLayout,
     @ShadeTouchLog private val touchLog: LogBuffer,
+    private val vibratorHelper: VibratorHelper,
     commandQueue: CommandQueue,
     statusBarKeyguardViewManager: StatusBarKeyguardViewManager,
     notificationShadeWindowController: NotificationShadeWindowController,
@@ -249,4 +251,8 @@
         // The only call to this doesn't happen with migrateClocksToBlueprint() enabled
         throw UnsupportedOperationException()
     }
+
+    override fun performHapticFeedback(constant: Int) {
+        vibratorHelper.performHapticFeedback(notificationStackScrollLayout, constant)
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt
index d90bb0b..9902a32 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt
@@ -44,6 +44,7 @@
     fun disableHeader(state1: Int, state2: Int, animated: Boolean)
 
     /** If the latency tracker is enabled, begins tracking expand latency. */
+    @Deprecated("No longer supported. Do not add new calls to this.")
     fun startExpandLatencyTracking()
 
     /** Sets the alpha value of the shade to a value between 0 and 255. */
@@ -57,13 +58,14 @@
     fun setAlphaChangeAnimationEndAction(r: Runnable)
 
     /** Sets Qs ScrimEnabled and updates QS state. */
+    @Deprecated("Does nothing when scene container is enabled.")
     fun setQsScrimEnabled(qsScrimEnabled: Boolean)
 
     /** Sets the top spacing for the ambient indicator. */
     fun setAmbientIndicationTop(ambientIndicationTop: Int, ambientTextVisible: Boolean)
 
     /** Updates notification panel-specific flags on [SysUiState]. */
-    fun updateSystemUiStateFlags()
+    @Deprecated("Does nothing when scene container is enabled.") fun updateSystemUiStateFlags()
 
     /** Ensures that the touchable region is updated. */
     fun updateTouchableRegion()
@@ -105,16 +107,6 @@
     @Deprecated("No longer supported. Do not add new calls to this.")
     fun finishInputFocusTransfer(velocity: Float)
 
-    /**
-     * Performs haptic feedback from a view with a haptic feedback constant.
-     *
-     * The implementation of this method should use the [android.view.View.performHapticFeedback]
-     * method with the provided constant.
-     *
-     * @param[constant] One of [android.view.HapticFeedbackConstants]
-     */
-    fun performHapticFeedback(constant: Int)
-
     /** Returns the ShadeHeadsUpTracker. */
     val shadeHeadsUpTracker: ShadeHeadsUpTracker
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt
index 69849e8..93c3772 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt
@@ -84,8 +84,6 @@
     override fun startInputFocusTransfer() {}
     override fun cancelInputFocusTransfer() {}
     override fun finishInputFocusTransfer(velocity: Float) {}
-    override fun performHapticFeedback(constant: Int) {}
-
     override val shadeHeadsUpTracker = ShadeHeadsUpTrackerEmptyImpl()
     override val shadeFoldAnimator = ShadeFoldAnimatorEmptyImpl()
     @Deprecated("Use SceneInteractor.currentScene instead.")
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
index ea549f2..24b7533 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
@@ -66,11 +66,13 @@
                 deviceEntryInteractor.isUnlocked,
                 deviceEntryInteractor.canSwipeToEnter,
                 shadeInteractor.shadeMode,
-            ) { isUnlocked, canSwipeToDismiss, shadeMode ->
+                qsSceneAdapter.isCustomizing
+            ) { isUnlocked, canSwipeToDismiss, shadeMode, isCustomizing ->
                 destinationScenes(
                     isUnlocked = isUnlocked,
                     canSwipeToDismiss = canSwipeToDismiss,
                     shadeMode = shadeMode,
+                    isCustomizing = isCustomizing
                 )
             }
             .stateIn(
@@ -81,6 +83,7 @@
                         isUnlocked = deviceEntryInteractor.isUnlocked.value,
                         canSwipeToDismiss = deviceEntryInteractor.canSwipeToEnter.value,
                         shadeMode = shadeInteractor.shadeMode.value,
+                        isCustomizing = qsSceneAdapter.isCustomizing.value,
                     ),
             )
 
@@ -120,6 +123,7 @@
         isUnlocked: Boolean,
         canSwipeToDismiss: Boolean?,
         shadeMode: ShadeMode,
+        isCustomizing: Boolean,
     ): Map<UserAction, UserActionResult> {
         val up =
             when {
@@ -131,7 +135,9 @@
         val down = Scenes.QuickSettings.takeIf { shadeMode is ShadeMode.Single }
 
         return buildMap {
-            this[Swipe(SwipeDirection.Up)] = UserActionResult(up)
+            if (!isCustomizing) {
+                this[Swipe(SwipeDirection.Up)] = UserActionResult(up)
+            } // TODO(b/330200163) Add an else to be able to collapse the shade while customizing
             down?.let { this[Swipe(SwipeDirection.Down)] = UserActionResult(down) }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
index a12b970..da8c1be 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
@@ -560,11 +560,6 @@
                                 Pair.create(
                                         KeyEvent.KEYCODE_TAB,
                                         KeyEvent.META_SHIFT_ON | KeyEvent.META_ALT_ON))),
-                /* Hide and (re)show taskbar: Meta + T */
-                new ShortcutKeyGroupMultiMappingInfo(
-                        context.getString(R.string.group_system_hide_reshow_taskbar),
-                        Arrays.asList(
-                                Pair.create(KeyEvent.KEYCODE_T, KeyEvent.META_META_ON))),
                 /* Access notification shade: Meta + N */
                 new ShortcutKeyGroupMultiMappingInfo(
                         context.getString(R.string.group_system_access_notification_shade),
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index c904621..815236e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -757,8 +757,8 @@
             mRotateTextViewController.updateIndication(
                     INDICATION_TYPE_ADAPTIVE_AUTH,
                     new KeyguardIndication.Builder()
-                            .setMessage(mContext
-                                    .getString(R.string.kg_prompt_after_adaptive_auth_lock))
+                            .setMessage(mContext.getString(
+                                    R.string.keyguard_indication_after_adaptive_auth_lock))
                             .setTextColor(mInitialTextColorState)
                             .build(),
                     true);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 9479762..f2c593d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -812,6 +812,10 @@
         } else {
             mDebugTextUsedYPositions.clear();
         }
+
+        mDebugPaint.setColor(Color.DKGRAY);
+        canvas.drawPath(mRoundedClipPath, mDebugPaint);
+
         int y = 0;
         drawDebugInfo(canvas, y, Color.RED, /* label= */ "y = " + y);
 
@@ -843,14 +847,14 @@
         drawDebugInfo(canvas, y, Color.LTGRAY,
                 /* label= */ "mAmbientState.getStackY() + mAmbientState.getStackHeight() = " + y);
 
-        y = (int) mAmbientState.getStackY() + mContentHeight;
-        drawDebugInfo(canvas, y, Color.MAGENTA,
-                /* label= */ "mAmbientState.getStackY() + mContentHeight = " + y);
-
         y = (int) (mAmbientState.getStackY() + mIntrinsicContentHeight);
         drawDebugInfo(canvas, y, Color.YELLOW,
                 /* label= */ "mAmbientState.getStackY() + mIntrinsicContentHeight = " + y);
 
+        y = mContentHeight;
+        drawDebugInfo(canvas, y, Color.MAGENTA,
+                /* label= */ "mContentHeight = " + y);
+
         drawDebugInfo(canvas, mRoundedRectClippingBottom, Color.DKGRAY,
                 /* label= */ "mRoundedRectClippingBottom) = " + y);
     }
@@ -4940,6 +4944,9 @@
             println(pw, "intrinsicPadding", mIntrinsicPadding);
             println(pw, "topPadding", mTopPadding);
             println(pw, "bottomPadding", mBottomPadding);
+            dumpRoundedRectClipping(pw);
+            println(pw, "requestedClipBounds", mRequestedClipBounds);
+            println(pw, "isClipped", mIsClipped);
             println(pw, "translationX", getTranslationX());
             println(pw, "translationY", getTranslationY());
             println(pw, "translationZ", getTranslationZ());
@@ -4994,6 +5001,15 @@
                 });
     }
 
+    private void dumpRoundedRectClipping(IndentingPrintWriter pw) {
+        pw.append("roundedRectClipping{l=").print(mRoundedRectClippingLeft);
+        pw.append(" t=").print(mRoundedRectClippingTop);
+        pw.append(" r=").print(mRoundedRectClippingRight);
+        pw.append(" b=").print(mRoundedRectClippingBottom);
+        pw.append("} topRadius=").print(mBgCornerRadii[0]);
+        pw.append(" bottomRadius=").println(mBgCornerRadii[4]);
+    }
+
     private void dumpFooterViewVisibility(IndentingPrintWriter pw) {
         FooterViewRefactor.assertInLegacyMode();
         final boolean showDismissView = shouldShowDismissView();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt
index 9efe632..9fffb66 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt
@@ -17,8 +17,9 @@
 
 package com.android.systemui.statusbar.notification.stack.data.repository
 
-import com.android.systemui.common.shared.model.NotificationContainerBounds
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds
+import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding
 import javax.inject.Inject
 import kotlinx.coroutines.flow.MutableStateFlow
 
@@ -26,7 +27,11 @@
 @SysUISingleton
 class NotificationStackAppearanceRepository @Inject constructor() {
     /** The bounds of the notification stack in the current scene. */
-    val stackBounds = MutableStateFlow(NotificationContainerBounds())
+    val stackBounds = MutableStateFlow(StackBounds())
+
+    /** The whether the corners of the notification stack should be rounded */
+    // TODO: replace with the logic from QSController
+    val stackRounding = MutableStateFlow(StackRounding(roundTop = true, roundBottom = false))
 
     /**
      * The height in px of the contents of notification stack. Depending on the number of
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
index 08df473..5a56ca1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
@@ -17,9 +17,10 @@
 
 package com.android.systemui.statusbar.notification.stack.domain.interactor
 
-import com.android.systemui.common.shared.model.NotificationContainerBounds
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.statusbar.notification.stack.data.repository.NotificationStackAppearanceRepository
+import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds
+import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.StateFlow
@@ -33,7 +34,10 @@
     private val repository: NotificationStackAppearanceRepository,
 ) {
     /** The bounds of the notification stack in the current scene. */
-    val stackBounds: StateFlow<NotificationContainerBounds> = repository.stackBounds.asStateFlow()
+    val stackBounds: StateFlow<StackBounds> = repository.stackBounds.asStateFlow()
+
+    /** The rounding of the notification stack. */
+    val stackRounding: StateFlow<StackRounding> = repository.stackRounding.asStateFlow()
 
     /**
      * The height in px of the contents of notification stack. Depending on the number of
@@ -59,7 +63,7 @@
     val syntheticScroll: Flow<Float> = repository.syntheticScroll.asStateFlow()
 
     /** Sets the position of the notification stack in the current scene. */
-    fun setStackBounds(bounds: NotificationContainerBounds) {
+    fun setStackBounds(bounds: StackBounds) {
         check(bounds.top <= bounds.bottom) { "Invalid bounds: $bounds" }
         repository.stackBounds.value = bounds
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackBounds.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackBounds.kt
new file mode 100644
index 0000000..1fc9a18
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackBounds.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack.shared.model
+
+/** Models the bounds of the notification stack. */
+data class StackBounds(
+    /** The position of the left of the stack in its window coordinate system, in pixels. */
+    val left: Float = 0f,
+    /** The position of the top of the stack in its window coordinate system, in pixels. */
+    val top: Float = 0f,
+    /** The position of the right of the stack in its window coordinate system, in pixels. */
+    val right: Float = 0f,
+    /** The position of the bottom of the stack in its window coordinate system, in pixels. */
+    val bottom: Float = 0f,
+) {
+    /** The current height of the notification container. */
+    val height: Float = bottom - top
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackClipping.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackClipping.kt
new file mode 100644
index 0000000..0c92b50
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackClipping.kt
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack.shared.model
+
+/** Models the clipping rounded rectangle of the notification stack */
+data class StackClipping(val bounds: StackBounds, val rounding: StackRounding)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackRounding.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackRounding.kt
new file mode 100644
index 0000000..ddc5d7ea
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackRounding.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack.shared.model
+
+/** Models the corner rounds of the notification stack. */
+data class StackRounding(
+    /** Whether the top corners of the notification stack should be rounded. */
+    val roundTop: Boolean = false,
+    /** Whether the bottom corners of the notification stack should be rounded. */
+    val roundBottom: Boolean = false,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt
index f10e5f1..189c5e0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt
@@ -47,16 +47,17 @@
         return view.repeatWhenAttached(mainImmediateDispatcher) {
             repeatOnLifecycle(Lifecycle.State.CREATED) {
                 launch {
-                    viewModel.stackBounds.collect { bounds ->
+                    viewModel.stackClipping.collect { (bounds, rounding) ->
                         val viewLeft = controller.view.left
                         val viewTop = controller.view.top
+                        val roundRadius = SCRIM_CORNER_RADIUS.dpToPx(context)
                         controller.setRoundedClippingBounds(
                             bounds.left.roundToInt() - viewLeft,
                             bounds.top.roundToInt() - viewTop,
                             bounds.right.roundToInt() - viewLeft,
                             bounds.bottom.roundToInt() - viewTop,
-                            SCRIM_CORNER_RADIUS.dpToPx(context),
-                            0,
+                            if (rounding.roundTop) roundRadius else 0,
+                            if (rounding.roundBottom) roundRadius else 0,
                         )
                     }
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt
index b6167e1..a7cbc33 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt
@@ -18,7 +18,6 @@
 package com.android.systemui.statusbar.notification.stack.ui.viewmodel
 
 import com.android.compose.animation.scene.ObservableTransitionState
-import com.android.systemui.common.shared.model.NotificationContainerBounds
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dump.DumpManager
@@ -27,6 +26,7 @@
 import com.android.systemui.scene.shared.model.Scenes.Shade
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor
+import com.android.systemui.statusbar.notification.stack.shared.model.StackClipping
 import com.android.systemui.util.kotlin.FlowDumperImpl
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
@@ -83,8 +83,13 @@
             .dumpWhileCollecting("expandFraction")
 
     /** The bounds of the notification stack in the current scene. */
-    val stackBounds: Flow<NotificationContainerBounds> =
-        stackAppearanceInteractor.stackBounds.dumpValue("stackBounds")
+    val stackClipping: Flow<StackClipping> =
+        combine(
+                stackAppearanceInteractor.stackBounds,
+                stackAppearanceInteractor.stackRounding,
+                ::StackClipping
+            )
+            .dumpWhileCollecting("stackClipping")
 
     /** The y-coordinate in px of top of the contents of the notification stack. */
     val contentTop: StateFlow<Float> = stackAppearanceInteractor.contentTop.dumpValue("contentTop")
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
index 9e2497d..ed44f20 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
@@ -24,8 +24,11 @@
 import com.android.systemui.scene.shared.flag.SceneContainerFlags
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor
+import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds
+import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
 
 /**
  * ViewModel used by the Notification placeholders inside the scene container to update the
@@ -61,12 +64,17 @@
         right: Float,
         bottom: Float,
     ) {
-        val notificationContainerBounds =
-            NotificationContainerBounds(top = top, bottom = bottom, left = left, right = right)
-        keyguardInteractor.setNotificationContainerBounds(notificationContainerBounds)
-        interactor.setStackBounds(notificationContainerBounds)
+        keyguardInteractor.setNotificationContainerBounds(
+            NotificationContainerBounds(top = top, bottom = bottom)
+        )
+        interactor.setStackBounds(
+            StackBounds(top = top, bottom = bottom, left = left, right = right)
+        )
     }
 
+    /** Corner rounding of the stack */
+    val stackRounding: StateFlow<StackRounding> = interactor.stackRounding
+
     /**
      * The height in px of the contents of notification stack. Depending on the number of
      * notifications, this can exceed the space available on screen to show notifications, at which
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
index 0db5c64..665fc0a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
@@ -537,7 +537,7 @@
 
     @VisibleForTesting
     void vibrateOnNavigationKeyDown() {
-        mShadeViewController.performHapticFeedback(
+        mShadeController.performHapticFeedback(
                 HapticFeedbackConstants.GESTURE_START
         );
     }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt
index d134e60..155102c9 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt
@@ -21,7 +21,6 @@
 import com.android.settingslib.volume.data.repository.LocalMediaRepository
 import com.android.settingslib.volume.data.repository.MediaControllerRepository
 import com.android.settingslib.volume.data.repository.MediaControllerRepositoryImpl
-import com.android.settingslib.volume.domain.interactor.LocalMediaInteractor
 import com.android.settingslib.volume.shared.AudioManagerEventsReceiver
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
@@ -52,13 +51,6 @@
 
         @Provides
         @SysUISingleton
-        fun provideLocalMediaInteractor(
-            repository: LocalMediaRepository,
-            @Application scope: CoroutineScope,
-        ): LocalMediaInteractor = LocalMediaInteractor(repository, scope)
-
-        @Provides
-        @SysUISingleton
         fun provideMediaDeviceSessionRepository(
             intentsReceiver: AudioManagerEventsReceiver,
             mediaSessionManager: MediaSessionManager,
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModel.kt
index 8ff2837..46ea382 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModel.kt
@@ -38,7 +38,8 @@
     fun onSettingsClicked() {
         volumePanelViewModel.dismissPanel()
         activityStarter.startActivity(
-            Intent(Settings.ACTION_SOUND_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
+            Intent(Settings.ACTION_SOUND_SETTINGS)
+                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT),
             true,
         )
     }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt
index 11b4690..e052f24 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt
@@ -15,15 +15,12 @@
  */
 package com.android.systemui.volume.panel.component.mediaoutput.data.repository
 
-import android.media.MediaRouter2Manager
 import com.android.settingslib.volume.data.repository.LocalMediaRepository
 import com.android.settingslib.volume.data.repository.LocalMediaRepositoryImpl
 import com.android.settingslib.volume.shared.AudioManagerEventsReceiver
 import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.media.controls.util.LocalMediaManagerFactory
 import javax.inject.Inject
-import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.CoroutineScope
 
 interface LocalMediaRepositoryFactory {
@@ -35,18 +32,14 @@
 @Inject
 constructor(
     private val eventsReceiver: AudioManagerEventsReceiver,
-    private val mediaRouter2Manager: MediaRouter2Manager,
     private val localMediaManagerFactory: LocalMediaManagerFactory,
     @Application private val coroutineScope: CoroutineScope,
-    @Background private val backgroundCoroutineContext: CoroutineContext,
 ) : LocalMediaRepositoryFactory {
 
     override fun create(packageName: String?): LocalMediaRepository =
         LocalMediaRepositoryImpl(
             eventsReceiver,
             localMediaManagerFactory.create(packageName),
-            mediaRouter2Manager,
             coroutineScope,
-            backgroundCoroutineContext,
         )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractor.kt
new file mode 100644
index 0000000..b0c8a4a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractor.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.panel.component.mediaoutput.domain.interactor
+
+import android.media.session.MediaController
+import android.media.session.PlaybackState
+import android.os.Handler
+import com.android.settingslib.volume.data.repository.MediaControllerChange
+import com.android.settingslib.volume.data.repository.MediaControllerRepository
+import com.android.settingslib.volume.data.repository.stateChanges
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSession
+import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.FlowCollector
+import kotlinx.coroutines.flow.filterIsInstance
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.withContext
+
+/** Allows to observe and change [MediaDeviceSession] state. */
+@OptIn(ExperimentalCoroutinesApi::class)
+@VolumePanelScope
+class MediaDeviceSessionInteractor
+@Inject
+constructor(
+    @Background private val backgroundCoroutineContext: CoroutineContext,
+    @Background private val backgroundHandler: Handler,
+    private val mediaControllerRepository: MediaControllerRepository,
+) {
+
+    /** [PlaybackState] changes for the [MediaDeviceSession]. */
+    fun playbackState(session: MediaDeviceSession): Flow<PlaybackState?> {
+        return stateChanges(session) {
+                emit(MediaControllerChange.PlaybackStateChanged(it.playbackState))
+            }
+            .filterIsInstance(MediaControllerChange.PlaybackStateChanged::class)
+            .map { it.state }
+    }
+
+    /** [MediaController.PlaybackInfo] changes for the [MediaDeviceSession]. */
+    fun playbackInfo(session: MediaDeviceSession): Flow<MediaController.PlaybackInfo?> {
+        return stateChanges(session) {
+                emit(MediaControllerChange.AudioInfoChanged(it.playbackInfo))
+            }
+            .filterIsInstance(MediaControllerChange.AudioInfoChanged::class)
+            .map { it.info }
+    }
+
+    private fun stateChanges(
+        session: MediaDeviceSession,
+        onStart: suspend FlowCollector<MediaControllerChange>.(controller: MediaController) -> Unit,
+    ): Flow<MediaControllerChange?> =
+        mediaControllerRepository.activeSessions
+            .flatMapLatest { controllers ->
+                val controller: MediaController =
+                    findControllerForSession(controllers, session)
+                        ?: return@flatMapLatest flowOf(null)
+                controller.stateChanges(backgroundHandler).onStart { onStart(controller) }
+            }
+            .flowOn(backgroundCoroutineContext)
+
+    /** Set [MediaDeviceSession] volume to [volume]. */
+    suspend fun setSessionVolume(mediaDeviceSession: MediaDeviceSession, volume: Int): Boolean {
+        if (!mediaDeviceSession.canAdjustVolume) {
+            return false
+        }
+        return withContext(backgroundCoroutineContext) {
+            val controller =
+                findControllerForSession(
+                    mediaControllerRepository.activeSessions.value,
+                    mediaDeviceSession,
+                )
+            if (controller == null) {
+                false
+            } else {
+                controller.setVolumeTo(volume, 0)
+                true
+            }
+        }
+    }
+
+    private fun findControllerForSession(
+        controllers: Collection<MediaController>,
+        mediaDeviceSession: MediaDeviceSession,
+    ): MediaController? =
+        controllers.firstOrNull { it.sessionToken == mediaDeviceSession.sessionToken }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt
index cb16abe..ea4c082 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt
@@ -33,23 +33,15 @@
     private val mediaOutputDialogManager: MediaOutputDialogManager,
 ) {
 
-    fun onBarClick(session: MediaDeviceSession, expandable: Expandable) {
-        when (session) {
-            is MediaDeviceSession.Active -> {
-                mediaOutputDialogManager.createAndShowWithController(
-                    session.packageName,
-                    false,
-                    expandable.dialogController()
-                )
-            }
-            is MediaDeviceSession.Inactive -> {
-                mediaOutputDialogManager.createAndShowForSystemRouting(
-                    expandable.dialogController()
-                )
-            }
-            else -> {
-                /* do nothing */
-            }
+    fun onBarClick(session: MediaDeviceSession, isPlaybackActive: Boolean, expandable: Expandable) {
+        if (isPlaybackActive) {
+            mediaOutputDialogManager.createAndShowWithController(
+                session.packageName,
+                false,
+                expandable.dialogController()
+            )
+        } else {
+            mediaOutputDialogManager.createAndShowForSystemRouting(expandable.dialogController())
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt
index 0f53437..e60139e 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt
@@ -17,17 +17,16 @@
 package com.android.systemui.volume.panel.component.mediaoutput.domain.interactor
 
 import android.content.pm.PackageManager
+import android.media.VolumeProvider
 import android.media.session.MediaController
-import android.os.Handler
 import android.util.Log
 import com.android.settingslib.media.MediaDevice
 import com.android.settingslib.volume.data.repository.LocalMediaRepository
-import com.android.settingslib.volume.data.repository.MediaControllerChange
 import com.android.settingslib.volume.data.repository.MediaControllerRepository
-import com.android.settingslib.volume.data.repository.stateChanges
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.volume.panel.component.mediaoutput.data.repository.LocalMediaRepositoryFactory
 import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSession
+import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSessions
 import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
 import javax.inject.Inject
 import kotlin.coroutines.CoroutineContext
@@ -38,12 +37,9 @@
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.filterIsInstance
 import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.shareIn
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.withContext
@@ -58,35 +54,40 @@
     private val packageManager: PackageManager,
     @VolumePanelScope private val coroutineScope: CoroutineScope,
     @Background private val backgroundCoroutineContext: CoroutineContext,
-    @Background private val backgroundHandler: Handler,
-    mediaControllerRepository: MediaControllerRepository
+    mediaControllerRepository: MediaControllerRepository,
 ) {
 
-    /** Current [MediaDeviceSession]. Emits when the session playback changes. */
-    val mediaDeviceSession: StateFlow<MediaDeviceSession> =
-        mediaControllerRepository.activeLocalMediaController
-            .flatMapLatest { it?.mediaDeviceSession() ?: flowOf(MediaDeviceSession.Inactive) }
-            .flowOn(backgroundCoroutineContext)
-            .stateIn(coroutineScope, SharingStarted.Eagerly, MediaDeviceSession.Inactive)
+    private val activeMediaControllers: Flow<MediaControllers> =
+        mediaControllerRepository.activeSessions
+            .map { getMediaControllers(it) }
+            .shareIn(coroutineScope, SharingStarted.Eagerly, replay = 1)
 
-    private fun MediaController.mediaDeviceSession(): Flow<MediaDeviceSession> {
-        return stateChanges(backgroundHandler)
-            .onStart { emit(MediaControllerChange.PlaybackStateChanged(playbackState)) }
-            .filterIsInstance<MediaControllerChange.PlaybackStateChanged>()
+    /** [MediaDeviceSessions] that contains currently active sessions. */
+    val activeMediaDeviceSessions: Flow<MediaDeviceSessions> =
+        activeMediaControllers.map {
+            MediaDeviceSessions(
+                local = it.local?.mediaDeviceSession(),
+                remote = it.remote?.mediaDeviceSession()
+            )
+        }
+
+    /** Returns the default [MediaDeviceSession] from [activeMediaDeviceSessions] */
+    val defaultActiveMediaSession: StateFlow<MediaDeviceSession?> =
+        activeMediaControllers
             .map {
-                MediaDeviceSession.Active(
-                    appLabel = getApplicationLabel(packageName)
-                            ?: return@map MediaDeviceSession.Inactive,
-                    packageName = packageName,
-                    sessionToken = sessionToken,
-                    playbackState = playbackState,
-                )
+                when {
+                    it.local?.playbackState?.isActive == true -> it.local.mediaDeviceSession()
+                    it.remote?.playbackState?.isActive == true -> it.remote.mediaDeviceSession()
+                    it.local != null -> it.local.mediaDeviceSession()
+                    else -> null
+                }
             }
-    }
+            .flowOn(backgroundCoroutineContext)
+            .stateIn(coroutineScope, SharingStarted.Eagerly, null)
 
     private val localMediaRepository: SharedFlow<LocalMediaRepository> =
-        mediaDeviceSession
-            .map { (it as? MediaDeviceSession.Active)?.packageName }
+        defaultActiveMediaSession
+            .map { it?.packageName }
             .distinctUntilChanged()
             .map { localMediaRepositoryFactory.create(it) }
             .shareIn(coroutineScope, SharingStarted.Eagerly, replay = 1)
@@ -111,6 +112,54 @@
         }
     }
 
+    /** Finds local and remote media controllers. */
+    private fun getMediaControllers(
+        controllers: Collection<MediaController>,
+    ): MediaControllers {
+        var localController: MediaController? = null
+        var remoteController: MediaController? = null
+        val remoteMediaSessions: MutableSet<String> = mutableSetOf()
+        for (controller in controllers) {
+            val playbackInfo: MediaController.PlaybackInfo = controller.playbackInfo ?: continue
+            when (playbackInfo.playbackType) {
+                MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE -> {
+                    // MediaController can't be local if there is a remote one for the same package
+                    if (localController?.packageName.equals(controller.packageName)) {
+                        localController = null
+                    }
+                    if (!remoteMediaSessions.contains(controller.packageName)) {
+                        remoteMediaSessions.add(controller.packageName)
+                        if (remoteController == null) {
+                            remoteController = controller
+                        }
+                    }
+                }
+                MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL -> {
+                    if (controller.packageName in remoteMediaSessions) continue
+                    if (localController != null) continue
+                    localController = controller
+                }
+            }
+        }
+        return MediaControllers(local = localController, remote = remoteController)
+    }
+
+    private suspend fun MediaController.mediaDeviceSession(): MediaDeviceSession? {
+        return MediaDeviceSession(
+            packageName = packageName,
+            sessionToken = sessionToken,
+            canAdjustVolume =
+                playbackInfo != null &&
+                    playbackInfo?.volumeControl != VolumeProvider.VOLUME_CONTROL_FIXED,
+            appLabel = getApplicationLabel(packageName) ?: return null
+        )
+    }
+
+    private data class MediaControllers(
+        val local: MediaController?,
+        val remote: MediaController?,
+    )
+
     private companion object {
         const val TAG = "MediaOutputInteractor"
     }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt
index 1bceee9..2a2ce79 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt
@@ -17,26 +17,15 @@
 package com.android.systemui.volume.panel.component.mediaoutput.domain.model
 
 import android.media.session.MediaSession
-import android.media.session.PlaybackState
 
 /** Represents media playing on the connected device. */
-sealed interface MediaDeviceSession {
+data class MediaDeviceSession(
+    val appLabel: CharSequence,
+    val packageName: String,
+    val sessionToken: MediaSession.Token,
+    val canAdjustVolume: Boolean,
+)
 
-    /** Media is playing. */
-    data class Active(
-        val appLabel: CharSequence,
-        val packageName: String,
-        val sessionToken: MediaSession.Token,
-        val playbackState: PlaybackState?,
-    ) : MediaDeviceSession
-
-    /** Media is not playing. */
-    data object Inactive : MediaDeviceSession
-
-    /** Current media state is unknown yet. */
-    data object Unknown : MediaDeviceSession
-}
-
-/** Returns true when the audio is playing for the [MediaDeviceSession]. */
-fun MediaDeviceSession.isPlaying(): Boolean =
-    this is MediaDeviceSession.Active && playbackState?.isActive == true
+/** Returns true when [other] controls the same sessions as [this]. */
+fun MediaDeviceSession.isTheSameSession(other: MediaDeviceSession?): Boolean =
+    sessionToken == other?.sessionToken
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSessions.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSessions.kt
new file mode 100644
index 0000000..ddc0784
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSessions.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.panel.component.mediaoutput.domain.model
+
+/** Models a pair of local and remote [MediaDeviceSession]s. */
+data class MediaDeviceSessions(
+    val local: MediaDeviceSession?,
+    val remote: MediaDeviceSession?,
+) {
+
+    companion object {
+        /** Returns [MediaDeviceSessions.local]. */
+        val Local: (MediaDeviceSessions) -> MediaDeviceSession? = { it.local }
+        /** Returns [MediaDeviceSessions.remote]. */
+        val Remote: (MediaDeviceSessions) -> MediaDeviceSession? = { it.remote }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt
index d49cb1e..2530a3a 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt
@@ -17,24 +17,30 @@
 package com.android.systemui.volume.panel.component.mediaoutput.ui.viewmodel
 
 import android.content.Context
+import android.media.session.PlaybackState
 import com.android.systemui.animation.Expandable
 import com.android.systemui.common.shared.model.Color
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.res.R
+import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaDeviceSessionInteractor
 import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputActionsInteractor
 import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor
 import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSession
-import com.android.systemui.volume.panel.component.mediaoutput.domain.model.isPlaying
 import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
 import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
 
 /** Models the UI of the Media Output Volume Panel component. */
+@OptIn(ExperimentalCoroutinesApi::class)
 @VolumePanelScope
 class MediaOutputViewModel
 @Inject
@@ -43,25 +49,36 @@
     @VolumePanelScope private val coroutineScope: CoroutineScope,
     private val volumePanelViewModel: VolumePanelViewModel,
     private val actionsInteractor: MediaOutputActionsInteractor,
+    private val mediaDeviceSessionInteractor: MediaDeviceSessionInteractor,
     interactor: MediaOutputInteractor,
 ) {
 
-    private val mediaDeviceSession: StateFlow<MediaDeviceSession> =
-        interactor.mediaDeviceSession.stateIn(
-            coroutineScope,
-            SharingStarted.Eagerly,
-            MediaDeviceSession.Unknown,
-        )
+    private val sessionWithPlayback: StateFlow<SessionWithPlayback?> =
+        interactor.defaultActiveMediaSession
+            .flatMapLatest { session ->
+                if (session == null) {
+                    flowOf(null)
+                } else {
+                    mediaDeviceSessionInteractor.playbackState(session).map { playback ->
+                        playback?.let { SessionWithPlayback(session, it) }
+                    }
+                }
+            }
+            .stateIn(
+                coroutineScope,
+                SharingStarted.Eagerly,
+                null,
+            )
 
     val connectedDeviceViewModel: StateFlow<ConnectedDeviceViewModel?> =
-        combine(mediaDeviceSession, interactor.currentConnectedDevice) {
+        combine(sessionWithPlayback, interactor.currentConnectedDevice) {
                 mediaDeviceSession,
                 currentConnectedDevice ->
                 ConnectedDeviceViewModel(
-                    if (mediaDeviceSession.isPlaying()) {
+                    if (mediaDeviceSession?.playback?.isActive == true) {
                         context.getString(
                             R.string.media_output_label_title,
-                            (mediaDeviceSession as MediaDeviceSession.Active).appLabel
+                            mediaDeviceSession.session.appLabel
                         )
                     } else {
                         context.getString(R.string.media_output_title_without_playing)
@@ -76,10 +93,10 @@
             )
 
     val deviceIconViewModel: StateFlow<DeviceIconViewModel?> =
-        combine(mediaDeviceSession, interactor.currentConnectedDevice) {
+        combine(sessionWithPlayback, interactor.currentConnectedDevice) {
                 mediaDeviceSession,
                 currentConnectedDevice ->
-                if (mediaDeviceSession.isPlaying()) {
+                if (mediaDeviceSession?.playback?.isActive == true) {
                     val icon =
                         currentConnectedDevice?.icon?.let { Icon.Loaded(it, null) }
                             ?: Icon.Resource(
@@ -112,7 +129,14 @@
             )
 
     fun onBarClick(expandable: Expandable) {
-        actionsInteractor.onBarClick(mediaDeviceSession.value, expandable)
+        sessionWithPlayback.value?.let {
+            actionsInteractor.onBarClick(it.session, it.playback.isActive, expandable)
+        }
         volumePanelViewModel.dismissPanel()
     }
+
+    private data class SessionWithPlayback(
+        val session: MediaDeviceSession,
+        val playback: PlaybackState,
+    )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/interactor/CastVolumeInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/interactor/CastVolumeInteractor.kt
deleted file mode 100644
index 6b62074..0000000
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/interactor/CastVolumeInteractor.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *       http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-
-package com.android.systemui.volume.panel.component.volume.domain.interactor
-
-import com.android.settingslib.volume.domain.interactor.LocalMediaInteractor
-import com.android.settingslib.volume.domain.model.RoutingSession
-import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
-
-/** Provides a remote media casting state. */
-@VolumePanelScope
-class CastVolumeInteractor
-@Inject
-constructor(
-    @VolumePanelScope private val coroutineScope: CoroutineScope,
-    private val localMediaInteractor: LocalMediaInteractor,
-) {
-
-    /** Returns a list of [RoutingSession] to show in the UI. */
-    val remoteRoutingSessions: StateFlow<List<RoutingSession>> =
-        localMediaInteractor.remoteRoutingSessions
-            .map { it.filter { routingSession -> routingSession.isVolumeSeekBarEnabled } }
-            .stateIn(coroutineScope, SharingStarted.Eagerly, emptyList())
-
-    /** Sets [routingSession] volume to [volume]. */
-    suspend fun setVolume(routingSession: RoutingSession, volume: Int) {
-        localMediaInteractor.adjustSessionVolume(routingSession.routingSessionInfo.id, volume)
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
index 1b73208..d49442c 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
@@ -80,7 +80,7 @@
             ) { model, isEnabled, ringerMode ->
                 model.toState(isEnabled, ringerMode)
             }
-            .stateIn(coroutineScope, SharingStarted.Eagerly, EmptyState)
+            .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty)
 
     override fun onValueChanged(state: SliderState, newValue: Float) {
         val audioViewModel = state as? State
@@ -163,17 +163,6 @@
         val audioStreamModel: AudioStreamModel,
     ) : SliderState
 
-    private data object EmptyState : SliderState {
-        override val value: Float = 0f
-        override val valueRange: ClosedFloatingPointRange<Float> = 0f..1f
-        override val icon: Icon? = null
-        override val valueText: String = ""
-        override val label: String = ""
-        override val disabledMessage: String? = null
-        override val a11yStep: Int = 0
-        override val isEnabled: Boolean = true
-    }
-
     @AssistedFactory
     interface Factory {
 
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt
index 86b2d73..0f240b3 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt
@@ -17,11 +17,11 @@
 package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel
 
 import android.content.Context
-import com.android.settingslib.volume.domain.model.RoutingSession
+import android.media.session.MediaController.PlaybackInfo
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.res.R
-import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor
-import com.android.systemui.volume.panel.component.volume.domain.interactor.CastVolumeInteractor
+import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaDeviceSessionInteractor
+import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSession
 import com.android.systemui.volume.panel.component.volume.domain.interactor.VolumeSliderInteractor
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
@@ -30,30 +30,29 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.mapNotNull
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
 
 class CastVolumeSliderViewModel
 @AssistedInject
 constructor(
-    @Assisted private val routingSession: RoutingSession,
+    @Assisted private val session: MediaDeviceSession,
     @Assisted private val coroutineScope: CoroutineScope,
     private val context: Context,
-    mediaOutputInteractor: MediaOutputInteractor,
+    private val mediaDeviceSessionInteractor: MediaDeviceSessionInteractor,
     private val volumeSliderInteractor: VolumeSliderInteractor,
-    private val castVolumeInteractor: CastVolumeInteractor,
 ) : SliderViewModel {
 
-    private val volumeRange = 0..routingSession.routingSessionInfo.volumeMax
-
     override val slider: StateFlow<SliderState> =
-        combine(mediaOutputInteractor.currentConnectedDevice) { _ -> getCurrentState() }
-            .stateIn(coroutineScope, SharingStarted.Eagerly, getCurrentState())
+        mediaDeviceSessionInteractor
+            .playbackInfo(session)
+            .mapNotNull { it?.getCurrentState() }
+            .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty)
 
     override fun onValueChanged(state: SliderState, newValue: Float) {
         coroutineScope.launch {
-            castVolumeInteractor.setVolume(routingSession, newValue.roundToInt())
+            mediaDeviceSessionInteractor.setSessionVolume(session, newValue.roundToInt())
         }
     }
 
@@ -61,15 +60,16 @@
         // do nothing because this action isn't supported for Cast sliders.
     }
 
-    private fun getCurrentState(): State =
-        State(
-            value = routingSession.routingSessionInfo.volume.toFloat(),
+    private fun PlaybackInfo.getCurrentState(): State {
+        val volumeRange = 0..maxVolume
+        return State(
+            value = currentVolume.toFloat(),
             valueRange = volumeRange.first.toFloat()..volumeRange.last.toFloat(),
             icon = Icon.Resource(R.drawable.ic_cast, null),
             valueText =
                 SliderViewModel.formatValue(
                     volumeSliderInteractor.processVolumeToValue(
-                        volume = routingSession.routingSessionInfo.volume,
+                        volume = currentVolume,
                         volumeRange = volumeRange,
                     )
                 ),
@@ -77,6 +77,7 @@
             isEnabled = true,
             a11yStep = 1
         )
+    }
 
     private data class State(
         override val value: Float,
@@ -95,7 +96,7 @@
     interface Factory {
 
         fun create(
-            routingSession: RoutingSession,
+            session: MediaDeviceSession,
             coroutineScope: CoroutineScope,
         ): CastVolumeSliderViewModel
     }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt
index b87d0a7..3dca272 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt
@@ -36,4 +36,15 @@
      */
     val a11yStep: Int
     val disabledMessage: String?
+
+    data object Empty : SliderState {
+        override val value: Float = 0f
+        override val valueRange: ClosedFloatingPointRange<Float> = 0f..1f
+        override val icon: Icon? = null
+        override val valueText: String = ""
+        override val label: String = ""
+        override val disabledMessage: String? = null
+        override val a11yStep: Int = 0
+        override val isEnabled: Boolean = true
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt
index aaee24b..4e9a456 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt
@@ -18,9 +18,10 @@
 
 import android.media.AudioManager
 import com.android.settingslib.volume.shared.model.AudioStream
+import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaDeviceSessionInteractor
 import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor
-import com.android.systemui.volume.panel.component.mediaoutput.domain.model.isPlaying
-import com.android.systemui.volume.panel.component.volume.domain.interactor.CastVolumeInteractor
+import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSession
+import com.android.systemui.volume.panel.component.mediaoutput.domain.model.isTheSameSession
 import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.AudioStreamSliderViewModel
 import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.CastVolumeSliderViewModel
 import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderViewModel
@@ -29,17 +30,15 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.combineTransform
+import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.merge
-import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.flow.transformLatest
 import kotlinx.coroutines.launch
 
 /**
@@ -52,50 +51,34 @@
 @Inject
 constructor(
     @VolumePanelScope private val scope: CoroutineScope,
-    castVolumeInteractor: CastVolumeInteractor,
     mediaOutputInteractor: MediaOutputInteractor,
+    private val mediaDeviceSessionInteractor: MediaDeviceSessionInteractor,
     private val streamSliderViewModelFactory: AudioStreamSliderViewModel.Factory,
     private val castVolumeSliderViewModelFactory: CastVolumeSliderViewModel.Factory,
 ) {
 
-    private val remoteSessionsViewModels: Flow<List<SliderViewModel>> =
-        castVolumeInteractor.remoteRoutingSessions.transformLatest { routingSessions ->
-            coroutineScope {
-                emit(
-                    routingSessions.map { routingSession ->
-                        castVolumeSliderViewModelFactory.create(routingSession, this)
-                    }
-                )
-            }
-        }
-    private val streamViewModels: Flow<List<SliderViewModel>> =
-        flowOf(
-                listOf(
-                    AudioStream(AudioManager.STREAM_MUSIC),
-                    AudioStream(AudioManager.STREAM_VOICE_CALL),
-                    AudioStream(AudioManager.STREAM_RING),
-                    AudioStream(AudioManager.STREAM_NOTIFICATION),
-                    AudioStream(AudioManager.STREAM_ALARM),
-                )
-            )
-            .transformLatest { streams ->
-                coroutineScope {
-                    emit(
-                        streams.map { stream ->
-                            streamSliderViewModelFactory.create(
-                                AudioStreamSliderViewModel.FactoryAudioStreamWrapper(stream),
-                                this,
-                            )
-                        }
-                    )
-                }
-            }
-
     val sliderViewModels: StateFlow<List<SliderViewModel>> =
-        combine(remoteSessionsViewModels, streamViewModels) {
-                remoteSessionsViewModels,
-                streamViewModels ->
-                remoteSessionsViewModels + streamViewModels
+        combineTransform(
+                mediaOutputInteractor.activeMediaDeviceSessions,
+                mediaOutputInteractor.defaultActiveMediaSession,
+            ) { activeSessions, defaultSession ->
+                coroutineScope {
+                    val viewModels = buildList {
+                        if (defaultSession?.isTheSameSession(activeSessions.remote) == true) {
+                            addRemoteViewModelIfNeeded(this, activeSessions.remote)
+                            addStreamViewModel(this, AudioManager.STREAM_MUSIC)
+                        } else {
+                            addStreamViewModel(this, AudioManager.STREAM_MUSIC)
+                            addRemoteViewModelIfNeeded(this, activeSessions.remote)
+                        }
+
+                        addStreamViewModel(this, AudioManager.STREAM_VOICE_CALL)
+                        addStreamViewModel(this, AudioManager.STREAM_RING)
+                        addStreamViewModel(this, AudioManager.STREAM_NOTIFICATION)
+                        addStreamViewModel(this, AudioManager.STREAM_ALARM)
+                    }
+                    emit(viewModels)
+                }
             }
             .stateIn(scope, SharingStarted.Eagerly, emptyList())
 
@@ -103,12 +86,41 @@
 
     val isExpanded: StateFlow<Boolean> =
         merge(
-                mutableIsExpanded.onStart { emit(false) },
-                mediaOutputInteractor.mediaDeviceSession.map { !it.isPlaying() },
+                mutableIsExpanded,
+                mediaOutputInteractor.defaultActiveMediaSession.flatMapLatest {
+                    if (it == null) flowOf(true)
+                    else mediaDeviceSessionInteractor.playbackState(it).map { it?.isActive != true }
+                },
             )
             .stateIn(scope, SharingStarted.Eagerly, false)
 
     fun onExpandedChanged(isExpanded: Boolean) {
         scope.launch { mutableIsExpanded.emit(isExpanded) }
     }
+
+    private fun CoroutineScope.addRemoteViewModelIfNeeded(
+        list: MutableList<SliderViewModel>,
+        remoteMediaDeviceSession: MediaDeviceSession?
+    ) {
+        if (remoteMediaDeviceSession?.canAdjustVolume == true) {
+            val viewModel =
+                castVolumeSliderViewModelFactory.create(
+                    remoteMediaDeviceSession,
+                    this,
+                )
+            list.add(viewModel)
+        }
+    }
+
+    private fun CoroutineScope.addStreamViewModel(
+        list: MutableList<SliderViewModel>,
+        stream: Int,
+    ) {
+        val viewModel =
+            streamSliderViewModelFactory.create(
+                AudioStreamSliderViewModel.FactoryAudioStreamWrapper(AudioStream(stream)),
+                this,
+            )
+        list.add(viewModel)
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationViewModelTransformerTest.java b/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationViewModelTransformerTest.java
index 206babf..09675e2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationViewModelTransformerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationViewModelTransformerTest.java
@@ -23,6 +23,7 @@
 
 import android.testing.AndroidTestingRunner;
 
+import androidx.lifecycle.ViewModel;
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
@@ -56,7 +57,8 @@
         MockitoAnnotations.initMocks(this);
         when(mFactory.create(Mockito.any(), Mockito.any())).thenReturn(mComponent);
         when(mComponent.getViewModelProvider()).thenReturn(mViewModelProvider);
-        when(mViewModelProvider.get(Mockito.any(), Mockito.any())).thenReturn(mViewModel);
+        when(mViewModelProvider.get(Mockito.any(), Mockito.<Class<ViewModel>>any()))
+                .thenReturn(mViewModel);
     }
 
     /**
diff --git a/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleSpaceWidgetManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleSpaceWidgetManagerTest.java
index a63b221..db0c0bc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleSpaceWidgetManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleSpaceWidgetManagerTest.java
@@ -80,6 +80,8 @@
 import android.app.people.PeopleManager;
 import android.app.people.PeopleSpaceTile;
 import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
@@ -101,11 +103,12 @@
 import androidx.preference.PreferenceManager;
 import androidx.test.filters.SmallTest;
 
-import com.android.systemui.res.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.people.PeopleBackupFollowUpJob;
 import com.android.systemui.people.PeopleSpaceUtils;
 import com.android.systemui.people.SharedPreferencesHelper;
+import com.android.systemui.res.R;
+import com.android.systemui.settings.FakeUserTracker;
 import com.android.systemui.statusbar.NotificationListener;
 import com.android.systemui.statusbar.NotificationListener.NotificationHandler;
 import com.android.systemui.statusbar.SbnBuilder;
@@ -265,6 +268,8 @@
 
     private final FakeExecutor mFakeExecutor = new FakeExecutor(mClock);
 
+    private final FakeUserTracker mUserTracker = new FakeUserTracker();
+
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
@@ -272,7 +277,7 @@
         mManager = new PeopleSpaceWidgetManager(mContext, mAppWidgetManager, mIPeopleManager,
                 mPeopleManager, mLauncherApps, mNotifCollection, mPackageManager,
                 Optional.of(mBubbles), mUserManager, mBackupManager, mINotificationManager,
-                mNotificationManager, mFakeExecutor);
+                mNotificationManager, mFakeExecutor, mUserTracker);
         mManager.attach(mListenerService);
 
         verify(mListenerService).addNotificationHandler(mListenerCaptor.capture());
@@ -309,6 +314,12 @@
                 .setId(1)
                 .setShortcutInfo(mShortcutInfo)
                 .build();
+
+        AppWidgetProviderInfo providerInfo = new AppWidgetProviderInfo();
+        providerInfo.provider = new ComponentName("com.android.systemui.tests",
+                "com.android.systemui.people.widget.PeopleSpaceWidgetProvider");
+        when(mAppWidgetManager.getInstalledProvidersForPackage(anyString(), any()))
+                .thenReturn(List.of(providerInfo));
     }
 
     @Test
@@ -1562,6 +1573,43 @@
                 String.valueOf(WIDGET_ID_WITH_KEY_IN_OPTIONS));
     }
 
+    @Test
+    public void testUpdateGeneratedPreview_flagDisabled() {
+        mSetFlagsRule.disableFlags(android.appwidget.flags.Flags.FLAG_GENERATED_PREVIEWS);
+        mManager.updateGeneratedPreviewForUser(mUserTracker.getUserHandle());
+        verify(mAppWidgetManager, times(0)).setWidgetPreview(any(), anyInt(), any());
+    }
+
+    @Test
+    public void testUpdateGeneratedPreview_userLocked() {
+        mSetFlagsRule.enableFlags(android.appwidget.flags.Flags.FLAG_GENERATED_PREVIEWS);
+        when(mUserManager.isUserUnlocked(mUserTracker.getUserHandle())).thenReturn(false);
+
+        mManager.updateGeneratedPreviewForUser(mUserTracker.getUserHandle());
+        verify(mAppWidgetManager, times(0)).setWidgetPreview(any(), anyInt(), any());
+    }
+
+    @Test
+    public void testUpdateGeneratedPreview_userUnlocked() {
+        mSetFlagsRule.enableFlags(android.appwidget.flags.Flags.FLAG_GENERATED_PREVIEWS);
+        when(mUserManager.isUserUnlocked(mUserTracker.getUserHandle())).thenReturn(true);
+        when(mAppWidgetManager.setWidgetPreview(any(), anyInt(), any())).thenReturn(true);
+
+        mManager.updateGeneratedPreviewForUser(mUserTracker.getUserHandle());
+        verify(mAppWidgetManager, times(1)).setWidgetPreview(any(), anyInt(), any());
+    }
+
+    @Test
+    public void testUpdateGeneratedPreview_doesNotSetTwice() {
+        mSetFlagsRule.enableFlags(android.appwidget.flags.Flags.FLAG_GENERATED_PREVIEWS);
+        when(mUserManager.isUserUnlocked(mUserTracker.getUserHandle())).thenReturn(true);
+        when(mAppWidgetManager.setWidgetPreview(any(), anyInt(), any())).thenReturn(true);
+
+        mManager.updateGeneratedPreviewForUser(mUserTracker.getUserHandle());
+        mManager.updateGeneratedPreviewForUser(mUserTracker.getUserHandle());
+        verify(mAppWidgetManager, times(1)).setWidgetPreview(any(), anyInt(), any());
+    }
+
     private void setFinalField(String fieldName, int value) {
         try {
             Field field = NotificationManager.Policy.class.getDeclaredField(fieldName);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
index cc48640..5c6ed70 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
@@ -21,6 +21,7 @@
 import android.testing.ViewUtils
 import android.view.ContextThemeWrapper
 import android.view.View
+import android.view.ViewGroup
 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
 import android.view.accessibility.AccessibilityNodeInfo
 import android.widget.FrameLayout
@@ -71,7 +72,7 @@
             qsPanel = QSPanel(themedContext, null)
             qsPanel.mUsingMediaPlayer = true
 
-            qsPanel.initialize(qsLogger)
+            qsPanel.initialize(qsLogger, true)
             // QSPanel inflates a footer inside of it, mocking it here
             footer = LinearLayout(themedContext).apply { id = R.id.qs_footer }
             qsPanel.addView(footer, MATCH_PARENT, 100)
@@ -218,6 +219,62 @@
         verify(tile).addCallback(record.callback)
     }
 
+    @Test
+    fun initializedWithNoMedia_tileLayoutParentIsAlwaysQsPanel() {
+        lateinit var panel: QSPanel
+        lateinit var tileLayout: View
+        testableLooper.runWithLooper {
+            panel = QSPanel(themedContext, null)
+            panel.mUsingMediaPlayer = true
+
+            panel.initialize(qsLogger, /* usingMediaPlayer= */ false)
+            tileLayout = panel.orCreateTileLayout as View
+            // QSPanel inflates a footer inside of it, mocking it here
+            footer = LinearLayout(themedContext).apply { id = R.id.qs_footer }
+            panel.addView(footer, MATCH_PARENT, 100)
+            panel.onFinishInflate()
+            // Provides a parent with non-zero size for QSPanel
+            ViewUtils.attachView(panel)
+        }
+        val mockMediaHost = mock(ViewGroup::class.java)
+
+        panel.setUsingHorizontalLayout(false, mockMediaHost, true)
+
+        assertThat(tileLayout.parent).isSameInstanceAs(panel)
+
+        panel.setUsingHorizontalLayout(true, mockMediaHost, true)
+        assertThat(tileLayout.parent).isSameInstanceAs(panel)
+
+        ViewUtils.detachView(panel)
+    }
+
+    @Test
+    fun initializeWithNoMedia_mediaNeverAttached() {
+        lateinit var panel: QSPanel
+        testableLooper.runWithLooper {
+            panel = QSPanel(themedContext, null)
+            panel.mUsingMediaPlayer = true
+
+            panel.initialize(qsLogger, /* usingMediaPlayer= */ false)
+            panel.orCreateTileLayout as View
+            // QSPanel inflates a footer inside of it, mocking it here
+            footer = LinearLayout(themedContext).apply { id = R.id.qs_footer }
+            panel.addView(footer, MATCH_PARENT, 100)
+            panel.onFinishInflate()
+            // Provides a parent with non-zero size for QSPanel
+            ViewUtils.attachView(panel)
+        }
+        val mockMediaHost = FrameLayout(themedContext)
+
+        panel.setUsingHorizontalLayout(false, mockMediaHost, true)
+        assertThat(mockMediaHost.parent).isNull()
+
+        panel.setUsingHorizontalLayout(true, mockMediaHost, true)
+        assertThat(mockMediaHost.parent).isNull()
+
+        ViewUtils.detachView(panel)
+    }
+
     private infix fun View.isLeftOf(other: View): Boolean {
         val rect = Rect()
         getBoundsOnScreen(rect)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelTest.kt
index 3fba393..e5369fc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelTest.kt
@@ -36,7 +36,7 @@
 
         testableLooper.runWithLooper {
             quickQSPanel = QuickQSPanel(mContext, null)
-            quickQSPanel.initialize(qsLogger)
+            quickQSPanel.initialize(qsLogger, true)
 
             quickQSPanel.onFinishInflate()
             // Provides a parent with non-zero size for QSPanel
diff --git a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
index 10d6ebf..1313227 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
@@ -21,7 +21,7 @@
 import android.content.pm.PackageManager
 import android.content.pm.ResolveInfo
 import android.os.PowerManager
-import android.os.Process;
+import android.os.Process
 import android.os.UserHandle
 import android.testing.AndroidTestingRunner
 import android.testing.TestableContext
@@ -34,8 +34,6 @@
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.flags.FakeFeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController
 import com.android.systemui.keyguard.WakefulnessLifecycle
 import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager
@@ -96,7 +94,6 @@
     private val displayTracker = FakeDisplayTracker(mContext)
     private val fakeSystemClock = FakeSystemClock()
     private val sysUiState = SysUiState(displayTracker, kosmos.sceneContainerPlugin)
-    private val featureFlags = FakeFeatureFlags()
     private val wakefulnessLifecycle =
         WakefulnessLifecycle(mContext, null, fakeSystemClock, dumpManager)
 
@@ -121,8 +118,7 @@
     @Mock
     private lateinit var unfoldTransitionProgressForwarder:
         Optional<UnfoldTransitionProgressForwarder>
-    @Mock
-    private lateinit var broadcastDispatcher: BroadcastDispatcher
+    @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
 
     @Before
     fun setUp() {
@@ -205,16 +201,14 @@
 
     @Test
     fun connectToOverviewService_primaryUser_expectBindService() {
-        val mockitoSession = ExtendedMockito.mockitoSession()
-                .spyStatic(Process::class.java)
-                .startMocking()
+        val mockitoSession =
+            ExtendedMockito.mockitoSession().spyStatic(Process::class.java).startMocking()
         try {
             `when`(Process.myUserHandle()).thenReturn(UserHandle.SYSTEM)
             val spyContext = spy(context)
             val ops = createOverviewProxyService(spyContext)
             ops.startConnectionToCurrentUser()
-            verify(spyContext, atLeast(1)).bindServiceAsUser(any(), any(),
-                anyInt(), any())
+            verify(spyContext, atLeast(1)).bindServiceAsUser(any(), any(), anyInt(), any())
         } finally {
             mockitoSession.finishMocking()
         }
@@ -222,22 +216,20 @@
 
     @Test
     fun connectToOverviewService_nonPrimaryUser_expectNoBindService() {
-        val mockitoSession = ExtendedMockito.mockitoSession()
-                .spyStatic(Process::class.java)
-                .startMocking()
+        val mockitoSession =
+            ExtendedMockito.mockitoSession().spyStatic(Process::class.java).startMocking()
         try {
             `when`(Process.myUserHandle()).thenReturn(UserHandle.of(12345))
             val spyContext = spy(context)
             val ops = createOverviewProxyService(spyContext)
             ops.startConnectionToCurrentUser()
-            verify(spyContext, times(0)).bindServiceAsUser(any(), any(),
-                anyInt(), any())
+            verify(spyContext, times(0)).bindServiceAsUser(any(), any(), anyInt(), any())
         } finally {
             mockitoSession.finishMocking()
         }
     }
 
-    private fun createOverviewProxyService(ctx: Context) : OverviewProxyService {
+    private fun createOverviewProxyService(ctx: Context): OverviewProxyService {
         return OverviewProxyService(
             ctx,
             executor,
@@ -257,7 +249,6 @@
             sysuiUnlockAnimationController,
             inWindowLauncherUnlockAnimationManager,
             assistUtils,
-            featureFlags,
             FakeSceneContainerFlags(),
             dumpManager,
             unfoldTransitionProgressForwarder,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java
index d5c4053..8e8dd4d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java
@@ -188,7 +188,7 @@
     public void vibrateOnNavigationKeyDown_usesPerformHapticFeedback() {
         mSbcqCallbacks.vibrateOnNavigationKeyDown();
 
-        verify(mShadeViewController).performHapticFeedback(
+        verify(mShadeController).performHapticFeedback(
                 HapticFeedbackConstants.GESTURE_START
         );
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryKosmos.kt
index 6ef7419..ba07a84 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryKosmos.kt
@@ -19,4 +19,5 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
 
-val Kosmos.facePropertyRepository by Fixture { FakeFacePropertyRepository() }
+val Kosmos.fakeFacePropertyRepository by Fixture { FakeFacePropertyRepository() }
+val Kosmos.facePropertyRepository by Fixture { fakeFacePropertyRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt
index 27803b2..c065545 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.bouncer.domain.interactor
 
-import android.content.applicationContext
 import com.android.systemui.authentication.domain.interactor.authenticationInteractor
 import com.android.systemui.bouncer.data.repository.bouncerRepository
 import com.android.systemui.classifier.domain.interactor.falsingInteractor
@@ -29,12 +28,10 @@
 val Kosmos.bouncerInteractor by Fixture {
     BouncerInteractor(
         applicationScope = testScope.backgroundScope,
-        applicationContext = applicationContext,
         repository = bouncerRepository,
         authenticationInteractor = authenticationInteractor,
         deviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor,
         falsingInteractor = falsingInteractor,
         powerInteractor = powerInteractor,
-        simBouncerInteractor = simBouncerInteractor,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.kt
new file mode 100644
index 0000000..4b64416
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.ui.viewmodel
+
+import android.content.applicationContext
+import com.android.systemui.authentication.domain.interactor.authenticationInteractor
+import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor
+import com.android.systemui.bouncer.shared.flag.composeBouncerFlags
+import com.android.systemui.deviceentry.domain.interactor.biometricMessageInteractor
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryFingerprintAuthInteractor
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.user.ui.viewmodel.userSwitcherViewModel
+import com.android.systemui.util.time.systemClock
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+@ExperimentalCoroutinesApi
+val Kosmos.bouncerMessageViewModel by
+    Kosmos.Fixture {
+        BouncerMessageViewModel(
+            applicationContext = applicationContext,
+            applicationScope = testScope.backgroundScope,
+            bouncerInteractor = bouncerInteractor,
+            simBouncerInteractor = simBouncerInteractor,
+            authenticationInteractor = authenticationInteractor,
+            selectedUser = userSwitcherViewModel.selectedUser,
+            clock = systemClock,
+            biometricMessageInteractor = biometricMessageInteractor,
+            faceAuthInteractor = deviceEntryFaceAuthInteractor,
+            deviceEntryInteractor = deviceEntryInteractor,
+            fingerprintInteractor = deviceEntryFingerprintAuthInteractor,
+            flags = composeBouncerFlags,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt
index 6d97238..0f6c7cf 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
 package com.android.systemui.bouncer.ui.viewmodel
 
 import android.content.applicationContext
@@ -30,7 +32,7 @@
 import com.android.systemui.user.domain.interactor.selectedUserInteractor
 import com.android.systemui.user.ui.viewmodel.userSwitcherViewModel
 import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.time.systemClock
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 
 val Kosmos.bouncerViewModel by Fixture {
     BouncerViewModel(
@@ -47,7 +49,7 @@
         users = userSwitcherViewModel.users,
         userSwitcherMenu = userSwitcherViewModel.menu,
         actionButton = bouncerActionButtonInteractor.actionButton,
-        clock = systemClock,
         devicePolicyManager = mock(),
+        bouncerMessageViewModel = bouncerMessageViewModel,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowKosmos.kt
index dad1887..f7de5a4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowKosmos.kt
@@ -23,11 +23,13 @@
 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 kotlinx.coroutines.ExperimentalCoroutinesApi
 
 val Kosmos.keyguardTransitionAnimationFlow by Fixture {
     KeyguardTransitionAnimationFlow(
         scope = applicationCoroutineScope,
+        mainDispatcher = testDispatcher,
         transitionInteractor = keyguardTransitionInteractor,
         logger = keyguardTransitionAnimationLogger,
     )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGoneTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGoneTransitionViewModelKosmos.kt
new file mode 100644
index 0000000..f389142
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGoneTransitionViewModelKosmos.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
+import com.android.systemui.kosmos.Kosmos
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+@ExperimentalCoroutinesApi
+val Kosmos.dreamingToGoneTransitionViewModel by
+    Kosmos.Fixture {
+        DreamingToGoneTransitionViewModel(
+            animationFlow = keyguardTransitionAnimationFlow,
+        )
+    }
\ No newline at end of file
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
index a863edf..a84899e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
@@ -46,10 +46,12 @@
         dozingToGoneTransitionViewModel = dozingToGoneTransitionViewModel,
         dozingToLockscreenTransitionViewModel = dozingToLockscreenTransitionViewModel,
         dozingToOccludedTransitionViewModel = dozingToOccludedTransitionViewModel,
+        dreamingToGoneTransitionViewModel = dreamingToGoneTransitionViewModel,
         dreamingToLockscreenTransitionViewModel = dreamingToLockscreenTransitionViewModel,
         glanceableHubToLockscreenTransitionViewModel = glanceableHubToLockscreenTransitionViewModel,
         goneToAodTransitionViewModel = goneToAodTransitionViewModel,
         goneToDozingTransitionViewModel = goneToDozingTransitionViewModel,
+        goneToDreamingTransitionViewModel = goneToDreamingTransitionViewModel,
         lockscreenToAodTransitionViewModel = lockscreenToAodTransitionViewModel,
         lockscreenToDozingTransitionViewModel = lockscreenToDozingTransitionViewModel,
         lockscreenToDreamingTransitionViewModel = lockscreenToDreamingTransitionViewModel,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt
index f4acf4d..16c5b72 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt
@@ -31,6 +31,7 @@
 import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.NotificationShadeWindowController
+import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.notification.row.NotificationGutsManager
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.windowRootViewVisibilityInteractor
@@ -52,6 +53,7 @@
             notificationStackScrollLayout = mock<NotificationStackScrollLayout>(),
             deviceEntryInteractor = deviceEntryInteractor,
             touchLog = mock<LogBuffer>(),
+            vibratorHelper = mock<VibratorHelper>(),
             commandQueue = mock<CommandQueue>(),
             statusBarKeyguardViewManager = mock<StatusBarKeyguardViewManager>(),
             notificationShadeWindowController = mock<NotificationShadeWindowController>(),
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaControllerKosmos.kt
new file mode 100644
index 0000000..5db1724
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaControllerKosmos.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume
+
+import android.content.packageManager
+import android.content.pm.ApplicationInfo
+import android.media.AudioAttributes
+import android.media.session.MediaController
+import android.media.session.MediaSession
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+
+private const val LOCAL_PACKAGE = "local.test.pkg"
+var Kosmos.localMediaController: MediaController by
+    Kosmos.Fixture {
+        val appInfo: ApplicationInfo = mock {
+            whenever(loadLabel(any())).thenReturn("local_media_controller_label")
+        }
+        whenever(packageManager.getApplicationInfo(eq(LOCAL_PACKAGE), any<Int>()))
+            .thenReturn(appInfo)
+
+        val localSessionToken: MediaSession.Token = MediaSession.Token(0, mock {})
+        mock {
+            whenever(packageName).thenReturn(LOCAL_PACKAGE)
+            whenever(playbackInfo)
+                .thenReturn(
+                    MediaController.PlaybackInfo(
+                        MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL,
+                        0,
+                        0,
+                        0,
+                        AudioAttributes.Builder().build(),
+                        "",
+                    )
+                )
+            whenever(sessionToken).thenReturn(localSessionToken)
+        }
+    }
+
+private const val REMOTE_PACKAGE = "remote.test.pkg"
+var Kosmos.remoteMediaController: MediaController by
+    Kosmos.Fixture {
+        val appInfo: ApplicationInfo = mock {
+            whenever(loadLabel(any())).thenReturn("remote_media_controller_label")
+        }
+        whenever(packageManager.getApplicationInfo(eq(REMOTE_PACKAGE), any<Int>()))
+            .thenReturn(appInfo)
+
+        val remoteSessionToken: MediaSession.Token = MediaSession.Token(0, mock {})
+        mock {
+            whenever(packageName).thenReturn(REMOTE_PACKAGE)
+            whenever(playbackInfo)
+                .thenReturn(
+                    MediaController.PlaybackInfo(
+                        MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE,
+                        0,
+                        0,
+                        0,
+                        AudioAttributes.Builder().build(),
+                        "",
+                    )
+                )
+            whenever(sessionToken).thenReturn(remoteSessionToken)
+        }
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt
index 3938f77..fa3a19b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt
@@ -18,7 +18,6 @@
 
 import android.content.packageManager
 import android.content.pm.ApplicationInfo
-import android.media.session.MediaController
 import android.os.Handler
 import android.testing.TestableLooper
 import com.android.systemui.kosmos.Kosmos
@@ -32,11 +31,10 @@
 import com.android.systemui.volume.data.repository.FakeMediaControllerRepository
 import com.android.systemui.volume.panel.component.mediaoutput.data.repository.FakeLocalMediaRepositoryFactory
 import com.android.systemui.volume.panel.component.mediaoutput.data.repository.LocalMediaRepositoryFactory
+import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaDeviceSessionInteractor
 import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputActionsInteractor
 import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor
 
-var Kosmos.mediaController: MediaController by Kosmos.Fixture { mock {} }
-
 val Kosmos.localMediaRepository by Kosmos.Fixture { FakeLocalMediaRepository() }
 val Kosmos.localMediaRepositoryFactory: LocalMediaRepositoryFactory by
     Kosmos.Fixture { FakeLocalMediaRepositoryFactory { localMediaRepository } }
@@ -56,6 +54,14 @@
             },
             testScope.backgroundScope,
             testScope.testScheduler,
+            mediaControllerRepository,
+        )
+    }
+
+val Kosmos.mediaDeviceSessionInteractor by
+    Kosmos.Fixture {
+        MediaDeviceSessionInteractor(
+            testScope.testScheduler,
             Handler(TestableLooper.get(testCase).looper),
             mediaControllerRepository,
         )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeLocalMediaRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeLocalMediaRepository.kt
index 284bd55..909be75 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeLocalMediaRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeLocalMediaRepository.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.volume.data.repository
 
 import com.android.settingslib.media.MediaDevice
-import com.android.settingslib.volume.data.model.RoutingSession
 import com.android.settingslib.volume.data.repository.LocalMediaRepository
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
@@ -25,35 +24,11 @@
 
 class FakeLocalMediaRepository : LocalMediaRepository {
 
-    private val volumeBySession: MutableMap<String?, Int> = mutableMapOf()
-
-    private val mutableMediaDevices = MutableStateFlow<List<MediaDevice>>(emptyList())
-    override val mediaDevices: StateFlow<List<MediaDevice>>
-        get() = mutableMediaDevices.asStateFlow()
-
     private val mutableCurrentConnectedDevice = MutableStateFlow<MediaDevice?>(null)
     override val currentConnectedDevice: StateFlow<MediaDevice?>
         get() = mutableCurrentConnectedDevice.asStateFlow()
 
-    private val mutableRemoteRoutingSessions = MutableStateFlow<List<RoutingSession>>(emptyList())
-    override val remoteRoutingSessions: StateFlow<List<RoutingSession>>
-        get() = mutableRemoteRoutingSessions.asStateFlow()
-
-    fun updateMediaDevices(devices: List<MediaDevice>) {
-        mutableMediaDevices.value = devices
-    }
-
     fun updateCurrentConnectedDevice(device: MediaDevice?) {
         mutableCurrentConnectedDevice.value = device
     }
-
-    fun updateRemoteRoutingSessions(sessions: List<RoutingSession>) {
-        mutableRemoteRoutingSessions.value = sessions
-    }
-
-    fun getSessionVolume(sessionId: String?): Int = volumeBySession.getOrDefault(sessionId, 0)
-
-    override suspend fun adjustSessionVolume(sessionId: String?, volume: Int) {
-        volumeBySession[sessionId] = volume
-    }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeMediaControllerRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeMediaControllerRepository.kt
index 6d52e52..8ab5bd90 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeMediaControllerRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeMediaControllerRepository.kt
@@ -24,11 +24,11 @@
 
 class FakeMediaControllerRepository : MediaControllerRepository {
 
-    private val mutableActiveLocalMediaController = MutableStateFlow<MediaController?>(null)
-    override val activeLocalMediaController: StateFlow<MediaController?> =
-        mutableActiveLocalMediaController.asStateFlow()
+    private val mutableActiveSessions = MutableStateFlow<List<MediaController>>(emptyList())
+    override val activeSessions: StateFlow<List<MediaController>>
+        get() = mutableActiveSessions.asStateFlow()
 
-    fun setActiveLocalMediaController(controller: MediaController?) {
-        mutableActiveLocalMediaController.value = controller
+    fun setActiveSessions(sessions: List<MediaController>) {
+        mutableActiveSessions.value = sessions
     }
 }
diff --git a/services/Android.bp b/services/Android.bp
index 98a7979..7bbb42e 100644
--- a/services/Android.bp
+++ b/services/Android.bp
@@ -253,6 +253,7 @@
 
     required: [
         "libukey2_jni_shared",
+        "protolog.conf.json.gz",
     ],
     lint: {
         baseline_filename: "lint-baseline.xml",
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 4e14dee..880a687 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -993,6 +993,12 @@
                                     intent.getStringExtra(Intent.EXTRA_SETTING_PREVIOUS_VALUE),
                                     intent.getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE));
                         }
+                    } else if (Settings.Secure.ACCESSIBILITY_QS_TARGETS.equals(which)) {
+                        if (!android.view.accessibility.Flags.a11yQsShortcut()) {
+                            return;
+                        }
+                        restoreAccessibilityQsTargets(
+                                    intent.getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE));
                     }
                 }
             }
@@ -2131,6 +2137,29 @@
         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
+     * TODO: http://b/22388012
+     */
+    private void restoreAccessibilityQsTargets(String newValue) {
+        synchronized (mLock) {
+            final AccessibilityUserState userState = getUserStateLocked(UserHandle.USER_SYSTEM);
+            final Set<String> mergedTargets = userState.getA11yQsTargets();
+            readColonDelimitedStringToSet(newValue, str -> str, mergedTargets,
+                    /* doMerge = */ true);
+
+            userState.updateA11yQsTargetLocked(mergedTargets);
+            persistColonDelimitedSetToSettingLocked(Settings.Secure.ACCESSIBILITY_QS_TARGETS,
+                    UserHandle.USER_SYSTEM, mergedTargets, str -> str);
+            scheduleNotifyClientsOfServicesStateChangeLocked(userState);
+            onUserStateChangedLocked(userState);
+        }
+    }
+
     private int getClientStateLocked(AccessibilityUserState userState) {
         return userState.getClientStateLocked(
             mUiAutomationManager.canIntrospect(),
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
index 9a1d379..7008e8e 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
@@ -112,6 +112,10 @@
      * TileService's or the a11y framework tile component names (e.g.
      * {@link AccessibilityShortcutController#COLOR_INVERSION_TILE_COMPONENT_NAME}) instead of the
      * A11y Feature's component names.
+     * <p/>
+     * In addition, {@link #mA11yTilesInQsPanel} stores what's on the QS Panel, whereas
+     * {@link #mAccessibilityQsTargets} stores the targets that configured qs as their shortcut and
+     * also grant full device control permission.
      */
     private final ArraySet<ComponentName> mA11yTilesInQsPanel = new ArraySet<>();
 
diff --git a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
index ecd14ce..cc40940 100644
--- a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
+++ b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
@@ -77,23 +77,24 @@
                 @Override
                 public void onStart(MediaProjectionInfo info) {
                     if (DEBUG) Log.d(TAG, "onStart projection: " + info);
-                    Trace.beginSection(
+                    Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER,
                             "SensitiveContentProtectionManagerService.onProjectionStart");
                     try {
                         onProjectionStart(info.getPackageName());
                     } finally {
-                        Trace.endSection();
+                        Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
                     }
                 }
 
                 @Override
                 public void onStop(MediaProjectionInfo info) {
                     if (DEBUG) Log.d(TAG, "onStop projection: " + info);
-                    Trace.beginSection("SensitiveContentProtectionManagerService.onProjectionStop");
+                    Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER,
+                            "SensitiveContentProtectionManagerService.onProjectionStop");
                     try {
                         onProjectionEnd();
                     } finally {
-                        Trace.endSection();
+                        Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
                     }
                 }
             };
@@ -285,7 +286,8 @@
         @Override
         public void onListenerConnected() {
             super.onListenerConnected();
-            Trace.beginSection("SensitiveContentProtectionManagerService.onListenerConnected");
+            Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER,
+                    "SensitiveContentProtectionManagerService.onListenerConnected");
             try {
                 // Projection started before notification listener was connected
                 synchronized (mSensitiveContentProtectionLock) {
@@ -294,14 +296,15 @@
                     }
                 }
             } finally {
-                Trace.endSection();
+                Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
             }
         }
 
         @Override
         public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) {
             super.onNotificationPosted(sbn, rankingMap);
-            Trace.beginSection("SensitiveContentProtectionManagerService.onNotificationPosted");
+            Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER,
+                    "SensitiveContentProtectionManagerService.onNotificationPosted");
             try {
                 synchronized (mSensitiveContentProtectionLock) {
                     if (!mProjectionActive) {
@@ -317,14 +320,14 @@
                     }
                 }
             } finally {
-                Trace.endSection();
+                Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
             }
         }
 
         @Override
         public void onNotificationRankingUpdate(RankingMap rankingMap) {
             super.onNotificationRankingUpdate(rankingMap);
-            Trace.beginSection(
+            Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER,
                     "SensitiveContentProtectionManagerService.onNotificationRankingUpdate");
             try {
                 synchronized (mSensitiveContentProtectionLock) {
@@ -333,7 +336,7 @@
                     }
                 }
             } finally {
-                Trace.endSection();
+                Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
             }
         }
     }
@@ -382,7 +385,7 @@
 
         public void setSensitiveContentProtection(IBinder windowToken, String packageName,
                 boolean isShowingSensitiveContent) {
-            Trace.beginSection(
+            Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER,
                     "SensitiveContentProtectionManagerService.setSensitiveContentProtection");
             try {
                 int callingUid = Binder.getCallingUid();
@@ -395,7 +398,7 @@
                     Binder.restoreCallingIdentity(identity);
                 }
             } finally {
-                Trace.endSection();
+                Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
             }
         }
 
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index de000bf..e8c05c6 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -73,7 +73,6 @@
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHeadset;
 import android.bluetooth.BluetoothProfile;
-import android.content.AttributionSource;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.ContentResolver;
@@ -12208,9 +12207,7 @@
     //==========================================================================================
     public String registerAudioPolicy(AudioPolicyConfig policyConfig, IAudioPolicyCallback pcb,
             boolean hasFocusListener, boolean isFocusPolicy, boolean isTestFocusPolicy,
-            boolean isVolumeController, IMediaProjection projection,
-            AttributionSource attributionSource) {
-        Objects.requireNonNull(attributionSource);
+            boolean isVolumeController, IMediaProjection projection) {
         AudioSystem.setDynamicPolicyCallback(mDynPolicyCallback);
 
         if (!isPolicyRegisterAllowed(policyConfig,
@@ -12231,8 +12228,7 @@
             }
             try {
                 AudioPolicyProxy app = new AudioPolicyProxy(policyConfig, pcb, hasFocusListener,
-                        isFocusPolicy, isTestFocusPolicy, isVolumeController, projection,
-                        attributionSource);
+                        isFocusPolicy, isTestFocusPolicy, isVolumeController, projection);
                 pcb.asBinder().linkToDeath(app, 0/*flags*/);
 
                 // logging after registration so we have the registration id
@@ -13204,7 +13200,6 @@
     public class AudioPolicyProxy extends AudioPolicyConfig implements IBinder.DeathRecipient {
         private static final String TAG = "AudioPolicyProxy";
         final IAudioPolicyCallback mPolicyCallback;
-        final AttributionSource mAttributionSource;
         final boolean mHasFocusListener;
         final boolean mIsVolumeController;
         final HashMap<Integer, AudioDeviceArray> mUidDeviceAffinities =
@@ -13244,12 +13239,10 @@
 
         AudioPolicyProxy(AudioPolicyConfig config, IAudioPolicyCallback token,
                 boolean hasFocusListener, boolean isFocusPolicy, boolean isTestFocusPolicy,
-                boolean isVolumeController, IMediaProjection projection,
-                AttributionSource attributionSource) {
+                boolean isVolumeController, IMediaProjection projection) {
             super(config);
             setRegistration(new String(config.hashCode() + ":ap:" + mAudioPolicyCounter++));
             mPolicyCallback = token;
-            mAttributionSource = attributionSource;
             mHasFocusListener = hasFocusListener;
             mIsVolumeController = isVolumeController;
             mProjection = projection;
@@ -13377,7 +13370,6 @@
                 if (android.media.audiopolicy.Flags.audioMixOwnership()) {
                     for (AudioMix mix : mixes) {
                         setMixRegistration(mix);
-                        mix.setVirtualDeviceId(mAttributionSource.getDeviceId());
                     }
 
                     int result = mAudioSystem.registerPolicyMixes(mixes, true);
@@ -13401,9 +13393,6 @@
         @AudioSystem.AudioSystemError int connectMixes() {
             final long identity = Binder.clearCallingIdentity();
             try {
-                for (AudioMix mix : mMixes) {
-                    mix.setVirtualDeviceId(mAttributionSource.getDeviceId());
-                }
                 return mAudioSystem.registerPolicyMixes(mMixes, true);
             } finally {
                 Binder.restoreCallingIdentity(identity);
@@ -13417,9 +13406,6 @@
             Objects.requireNonNull(mixesToUpdate);
             Objects.requireNonNull(updatedMixingRules);
 
-            for (AudioMix mix : mixesToUpdate) {
-                mix.setVirtualDeviceId(mAttributionSource.getDeviceId());
-            }
             if (mixesToUpdate.length != updatedMixingRules.length) {
                 Log.e(TAG, "Provided list of audio mixes to update and corresponding mixing rules "
                         + "have mismatching length (mixesToUpdate.length = " + mixesToUpdate.length
diff --git a/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java b/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java
index d93ff9d..086f3aa 100644
--- a/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java
+++ b/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java
@@ -138,7 +138,9 @@
 
     /**
      * Constructs BroadcastRadioServiceImpl using AIDL HAL using the list of names of AIDL
-     * BroadcastRadio HAL services {@code serviceNameList}
+     * BroadcastRadio HAL services
+     *
+     * @param serviceNameList list of names of AIDL BroadcastRadio HAL services
      */
     public BroadcastRadioServiceImpl(ArrayList<String> serviceNameList) {
         mNextModuleId = 0;
@@ -169,7 +171,11 @@
     }
 
     /**
-     * Gets the AIDL RadioModule for the given {@code moduleId}. Null will be returned if not found.
+     * Gets the AIDL RadioModule for the given module Id.
+     *
+     * @param id Id of {@link RadioModule}  of AIDL BroadcastRadio HAL service
+     * @return {@code true} if {@link RadioModule} of AIDL BroadcastRadio HAL service is found,
+     *         {@code false} otherwise
      */
     public boolean hasModule(int id) {
         synchronized (mLock) {
diff --git a/services/core/java/com/android/server/display/AutomaticBrightnessController.java b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
index 4032514..4aab9d2 100644
--- a/services/core/java/com/android/server/display/AutomaticBrightnessController.java
+++ b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
@@ -54,6 +54,7 @@
 import com.android.internal.os.BackgroundThread;
 import com.android.server.EventLogTags;
 import com.android.server.display.brightness.BrightnessEvent;
+import com.android.server.display.brightness.clamper.BrightnessClamperController;
 
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
@@ -252,6 +253,7 @@
 
     // Controls Brightness range (including High Brightness Mode).
     private final BrightnessRangeController mBrightnessRangeController;
+    private final BrightnessClamperController mBrightnessClamperController;
 
     // Throttles (caps) maximum allowed brightness
     private final BrightnessThrottler mBrightnessThrottler;
@@ -287,7 +289,8 @@
             HysteresisLevels screenBrightnessThresholdsIdle, Context context,
             BrightnessRangeController brightnessModeController,
             BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
-            int ambientLightHorizonLong, float userLux, float userNits) {
+            int ambientLightHorizonLong, float userLux, float userNits,
+            BrightnessClamperController brightnessClamperController) {
         this(new Injector(), callbacks, looper, sensorManager, lightSensor,
                 brightnessMappingStrategyMap, lightSensorWarmUpTime, brightnessMin, brightnessMax,
                 dozeScaleFactor, lightSensorRate, initialLightSensorRate,
@@ -297,7 +300,7 @@
                 screenBrightnessThresholds, ambientBrightnessThresholdsIdle,
                 screenBrightnessThresholdsIdle, context, brightnessModeController,
                 brightnessThrottler, ambientLightHorizonShort, ambientLightHorizonLong, userLux,
-                userNits
+                userNits, brightnessClamperController
         );
     }
 
@@ -313,9 +316,10 @@
             HysteresisLevels screenBrightnessThresholds,
             HysteresisLevels ambientBrightnessThresholdsIdle,
             HysteresisLevels screenBrightnessThresholdsIdle, Context context,
-            BrightnessRangeController brightnessModeController,
+            BrightnessRangeController brightnessRangeController,
             BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
-            int ambientLightHorizonLong, float userLux, float userNits) {
+            int ambientLightHorizonLong, float userLux, float userNits,
+            BrightnessClamperController brightnessClamperController) {
         mInjector = injector;
         mClock = injector.createClock();
         mContext = context;
@@ -358,7 +362,8 @@
         mPendingForegroundAppPackageName = null;
         mForegroundAppCategory = ApplicationInfo.CATEGORY_UNDEFINED;
         mPendingForegroundAppCategory = ApplicationInfo.CATEGORY_UNDEFINED;
-        mBrightnessRangeController = brightnessModeController;
+        mBrightnessRangeController = brightnessRangeController;
+        mBrightnessClamperController = brightnessClamperController;
         mBrightnessThrottler = brightnessThrottler;
         mBrightnessMappingStrategyMap = brightnessMappingStrategyMap;
 
@@ -791,7 +796,7 @@
                     mAmbientBrightnessThresholds.getDarkeningThreshold(lux);
         }
         mBrightnessRangeController.onAmbientLuxChange(mAmbientLux);
-
+        mBrightnessClamperController.onAmbientLuxChange(mAmbientLux);
 
         // If the short term model was invalidated and the change is drastic enough, reset it.
         mShortTermModel.maybeReset(mAmbientLux);
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index 4116669..04e7f77 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -61,6 +61,7 @@
 import com.android.server.display.config.IdleScreenRefreshRateTimeoutLuxThresholdPoint;
 import com.android.server.display.config.IdleScreenRefreshRateTimeoutLuxThresholds;
 import com.android.server.display.config.IntegerArray;
+import com.android.server.display.config.LowBrightnessData;
 import com.android.server.display.config.LuxThrottling;
 import com.android.server.display.config.NitsMap;
 import com.android.server.display.config.NonNegativeFloatToFloatPoint;
@@ -555,6 +556,24 @@
  *         <majorVersion>2</majorVersion>
  *         <minorVersion>0</minorVersion>
  *     </usiVersion>
+ *     <lowBrightness enabled="true">
+ *       <transitionPoint>0.1</transitionPoint>
+ *
+ *       <nits>0.2</nits>
+ *       <nits>2.0</nits>
+ *       <nits>500.0</nits>
+ *       <nits>1000.0</nits>
+ *
+ *       <backlight>0</backlight>
+ *       <backlight>0.0001</backlight>
+ *       <backlight>0.5</backlight>
+ *       <backlight>1.0</backlight>
+ *
+ *       <brightness>0</brightness>
+ *       <brightness>0.1</brightness>
+ *       <brightness>0.5</brightness>
+ *       <brightness>1.0</brightness>
+ *     </lowBrightness>
  *     <screenBrightnessCapForWearBedtimeMode>0.1</screenBrightnessCapForWearBedtimeMode>
  *     <idleScreenRefreshRateTimeout>
  *          <luxThresholds>
@@ -568,6 +587,8 @@
  *              </point>
  *          </luxThresholds>
  *     </idleScreenRefreshRateTimeout>
+ *
+ *
  *    </displayConfiguration>
  *  }
  *  </pre>
@@ -732,6 +753,7 @@
     private Spline mBacklightToBrightnessSpline;
     private Spline mBacklightToNitsSpline;
     private Spline mNitsToBacklightSpline;
+
     private List<String> mQuirks;
     private boolean mIsHighBrightnessModeEnabled = false;
     private HighBrightnessModeData mHbmData;
@@ -872,6 +894,9 @@
     @Nullable
     private HdrBrightnessData mHdrBrightnessData;
 
+    @Nullable
+    public LowBrightnessData mLowBrightnessData;
+
     /**
      * Maximum screen brightness setting when screen brightness capped in Wear Bedtime mode.
      */
@@ -1814,6 +1839,15 @@
     }
 
     /**
+     *
+     * @return true if low brightness mode is enabled
+     */
+    @VisibleForTesting
+    public boolean getLbmEnabled() {
+        return mLowBrightnessData != null;
+    }
+
+    /**
      * @return Maximum screen brightness setting when screen brightness capped in Wear Bedtime mode.
      */
     public float getBrightnessCapForWearBedtimeMode() {
@@ -1952,6 +1986,8 @@
                 + "mUsiVersion= " + mHostUsiVersion + "\n"
                 + "mHdrBrightnessData= " + mHdrBrightnessData + "\n"
                 + "mBrightnessCapForWearBedtimeMode= " + mBrightnessCapForWearBedtimeMode
+                + "\n"
+                + (mLowBrightnessData != null ? mLowBrightnessData.toString() : "")
                 + "}";
     }
 
@@ -2002,6 +2038,9 @@
                 loadDensityMapping(config);
                 loadBrightnessDefaultFromDdcXml(config);
                 loadBrightnessConstraintsFromConfigXml();
+                if (mFlags.isEvenDimmerEnabled()) {
+                    mLowBrightnessData = LowBrightnessData.loadConfig(config);
+                }
                 loadBrightnessMap(config);
                 loadThermalThrottlingConfig(config);
                 loadPowerThrottlingConfigData(config);
@@ -2793,6 +2832,18 @@
     // These splines are used to convert from the system brightness value to the HAL backlight
     // value
     private void createBacklightConversionSplines() {
+        if (mLowBrightnessData != null) {
+            mBrightnessToBacklightSpline = mLowBrightnessData.mBrightnessToBacklight;
+            mBacklightToBrightnessSpline = mLowBrightnessData.mBacklightToBrightness;
+            mBacklightToNitsSpline = mLowBrightnessData.mBacklightToNits;
+            mNitsToBacklightSpline = mLowBrightnessData.mNitsToBacklight;
+
+            mNits = mLowBrightnessData.mNits;
+            mBrightness = mLowBrightnessData.mBrightness;
+            mBacklight = mLowBrightnessData.mBacklight;
+            return;
+        }
+
         mBrightness = new float[mBacklight.length];
         for (int i = 0; i < mBrightness.length; i++) {
             mBrightness[i] = MathUtils.map(mBacklight[0],
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 87d017c..90ad8c0 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -1165,7 +1165,8 @@
                     screenBrightnessThresholds, ambientBrightnessThresholdsIdle,
                     screenBrightnessThresholdsIdle, mContext, mBrightnessRangeController,
                     mBrightnessThrottler, mDisplayDeviceConfig.getAmbientHorizonShort(),
-                    mDisplayDeviceConfig.getAmbientHorizonLong(), userLux, userNits);
+                    mDisplayDeviceConfig.getAmbientHorizonLong(), userLux, userNits,
+                    mBrightnessClamperController);
             mDisplayBrightnessController.setAutomaticBrightnessController(
                     mAutomaticBrightnessController);
 
@@ -2479,6 +2480,7 @@
     public void setBrightnessToFollow(float leadDisplayBrightness, float nits, float ambientLux,
             boolean slowChange) {
         mBrightnessRangeController.onAmbientLuxChange(ambientLux);
+        mBrightnessClamperController.onAmbientLuxChange(ambientLux);
         if (nits == BrightnessMappingStrategy.INVALID_NITS) {
             mDisplayBrightnessController.setBrightnessToFollow(leadDisplayBrightness, slowChange);
         } else {
@@ -3176,7 +3178,9 @@
                 HysteresisLevels screenBrightnessThresholdsIdle, Context context,
                 BrightnessRangeController brightnessModeController,
                 BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
-                int ambientLightHorizonLong, float userLux, float userNits) {
+                int ambientLightHorizonLong, float userLux, float userNits,
+                BrightnessClamperController brightnessClamperController) {
+
             return new AutomaticBrightnessController(callbacks, looper, sensorManager, lightSensor,
                     brightnessMappingStrategyMap, lightSensorWarmUpTime, brightnessMin,
                     brightnessMax, dozeScaleFactor, lightSensorRate, initialLightSensorRate,
@@ -3186,7 +3190,7 @@
                     screenBrightnessThresholds, ambientBrightnessThresholdsIdle,
                     screenBrightnessThresholdsIdle, context, brightnessModeController,
                     brightnessThrottler, ambientLightHorizonShort, ambientLightHorizonLong, userLux,
-                    userNits);
+                    userNits, brightnessClamperController);
         }
 
         BrightnessMappingStrategy getDefaultModeBrightnessMapper(Context context,
diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
index b2fd9ed..3b3a03b 100644
--- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
@@ -37,6 +37,7 @@
 import android.os.Trace;
 import android.util.DisplayUtils;
 import android.util.LongSparseArray;
+import android.util.MathUtils;
 import android.util.Slog;
 import android.util.SparseArray;
 import android.view.Display;
@@ -78,6 +79,13 @@
     private static final String UNIQUE_ID_PREFIX = "local:";
 
     private static final String PROPERTY_EMULATOR_CIRCULAR = "ro.boot.emulator.circular";
+    // Min and max strengths for even dimmer feature.
+    private static final float EVEN_DIMMER_MIN_STRENGTH = 0.0f;
+    private static final float EVEN_DIMMER_MAX_STRENGTH = 70.0f; // not too dim yet.
+    private static final float BRIGHTNESS_MIN = 0.0f;
+    // The brightness at which we start using color matrices rather than backlight,
+    // to dim the display
+    private static final float BACKLIGHT_COLOR_TRANSITION_POINT = 0.1f;
 
     private final LongSparseArray<LocalDisplayDevice> mDevices = new LongSparseArray<>();
 
@@ -91,6 +99,8 @@
 
     private Context mOverlayContext;
 
+    private int mEvenDimmerStrength = -1;
+
     // Called with SyncRoot lock held.
     LocalDisplayAdapter(DisplayManagerService.SyncRoot syncRoot, Context context,
             Handler handler, Listener listener, DisplayManagerFlags flags,
@@ -928,6 +938,10 @@
                             final float nits = backlightToNits(backlight);
                             final float sdrNits = backlightToNits(sdrBacklight);
 
+                            if (getFeatureFlags().isEvenDimmerEnabled()) {
+                                applyColorMatrixBasedDimming(brightnessState);
+                            }
+
                             mBacklightAdapter.setBacklight(sdrBacklight, sdrNits, backlight, nits);
                             Trace.traceCounter(Trace.TRACE_TAG_POWER,
                                     "ScreenBrightness",
@@ -974,6 +988,22 @@
                             }
                         }
                     }
+
+                    private void applyColorMatrixBasedDimming(float brightnessState) {
+                        int strength = (int) (MathUtils.constrainedMap(
+                                EVEN_DIMMER_MAX_STRENGTH, EVEN_DIMMER_MIN_STRENGTH, // to this range
+                                BRIGHTNESS_MIN, BACKLIGHT_COLOR_TRANSITION_POINT, // from this range
+                                brightnessState) + 0.5); // map this (+ rounded up)
+
+                        if (mEvenDimmerStrength < 0 // uninitialised
+                                || MathUtils.abs(mEvenDimmerStrength - strength) > 1
+                                || strength <= 1) {
+                            mEvenDimmerStrength = strength;
+                        }
+
+                        // TODO: use `enabled` and `mRbcStrength` to set color matrices here
+                        // TODO: boolean enabled = mEvenDimmerStrength > 0.0f;
+                    }
                 };
             }
             return null;
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
index 18e8fab..d8a4500 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
@@ -189,6 +189,13 @@
         mModifiers.forEach(BrightnessStateModifier::stop);
     }
 
+    /**
+     * Notifies modifiers that ambient lux has changed.
+     * @param ambientLux current lux, debounced
+     */
+    public void onAmbientLuxChange(float ambientLux) {
+        mModifiers.forEach(modifier -> modifier.onAmbientLuxChange(ambientLux));
+    }
 
     // Called in DisplayControllerHandler
     private void recalculateBrightnessCap() {
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java
index 7f1f7a9..a91bb59 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java
@@ -39,21 +39,21 @@
  * Class used to prevent the screen brightness dipping below a certain value, based on current
  * lux conditions and user preferred minimum.
  */
-public class BrightnessLowLuxModifier implements
-        BrightnessStateModifier {
+public class BrightnessLowLuxModifier extends BrightnessModifier {
 
     // To enable these logs, run:
     // 'adb shell setprop persist.log.tag.BrightnessLowLuxModifier DEBUG && adb reboot'
     private static final String TAG = "BrightnessLowLuxModifier";
     private static final boolean DEBUG = DebugUtils.isDebuggable(TAG);
+    private static final float MIN_NITS = 2.0f;
     private final SettingsObserver mSettingsObserver;
     private final ContentResolver mContentResolver;
     private final Handler mHandler;
     private final BrightnessClamperController.ClamperChangeListener mChangeListener;
-    protected float mSettingNitsLowerBound = PowerManager.BRIGHTNESS_MIN;
     private int mReason;
     private float mBrightnessLowerBound;
     private boolean mIsActive;
+    private float mAmbientLux;
 
     @VisibleForTesting
     BrightnessLowLuxModifier(Handler handler,
@@ -78,17 +78,17 @@
         int userId = UserHandle.USER_CURRENT;
         float settingNitsLowerBound = Settings.Secure.getFloatForUser(
                 mContentResolver, Settings.Secure.EVEN_DIMMER_MIN_NITS,
-                /* def= */ PowerManager.BRIGHTNESS_MIN, userId);
+                /* def= */ MIN_NITS, userId);
 
-        boolean isActive = Settings.Secure.getIntForUser(mContentResolver,
+        boolean isActive = Settings.Secure.getFloatForUser(mContentResolver,
                 Settings.Secure.EVEN_DIMMER_ACTIVATED,
-                /* def= */ 0, userId) == 1;
+                /* def= */ 0, userId) == 1.0f;
 
-        // TODO: luxBasedNitsLowerBound = mMinNitsToLuxSpline(currentLux);
-        float luxBasedNitsLowerBound = 0.0f;
+        // TODO: luxBasedNitsLowerBound = mMinLuxToNitsSpline(currentLux);
+        float luxBasedNitsLowerBound = 2.0f;
 
-        // TODO: final float nitsLowerBound = isActive ? Math.max(settingNitsLowerBound,
-                // luxBasedNitsLowerBound) : PowerManager.BRIGHTNESS_MIN;
+        final float nitsLowerBound = isActive ? Math.max(settingNitsLowerBound,
+                 luxBasedNitsLowerBound) : MIN_NITS;
 
         final int reason = settingNitsLowerBound > luxBasedNitsLowerBound
                 ? BrightnessReason.MODIFIER_MIN_USER_SET_LOWER_BOUND
@@ -104,8 +104,13 @@
             mReason = reason;
             if (DEBUG) {
                 Slog.i(TAG, "isActive: " + isActive
-                        + ", settingNitsLowerBound: " + settingNitsLowerBound
-                        + ", lowerBound: " + brightnessLowerBound);
+                        + ", brightnessLowerBound: " + brightnessLowerBound
+                        + ", mAmbientLux: " + mAmbientLux
+                        + ", mReason: " + (
+                        mReason == BrightnessReason.MODIFIER_MIN_USER_SET_LOWER_BOUND ? "minSetting"
+                                : "lux")
+                        + ", nitsLowerBound: " + nitsLowerBound
+                );
             }
             mBrightnessLowerBound = brightnessLowerBound;
             mChangeListener.onChanged();
@@ -132,6 +137,22 @@
     }
 
     @Override
+    boolean shouldApply(DisplayManagerInternal.DisplayPowerRequest request) {
+        return mIsActive;
+    }
+
+    @Override
+    float getBrightnessAdjusted(float currentBrightness,
+            DisplayManagerInternal.DisplayPowerRequest request) {
+        return Math.max(mBrightnessLowerBound, currentBrightness);
+    }
+
+    @Override
+    int getModifier() {
+        return mReason;
+    }
+
+    @Override
     public void apply(DisplayManagerInternal.DisplayPowerRequest request,
             DisplayBrightnessState.Builder stateBuilder) {
         stateBuilder.setMinBrightness(mBrightnessLowerBound);
@@ -150,10 +171,16 @@
     }
 
     @Override
+    public void onAmbientLuxChange(float ambientLux) {
+        mAmbientLux = ambientLux;
+        recalculateLowerBound();
+    }
+
+    @Override
     public void dump(PrintWriter pw) {
         pw.println("BrightnessLowLuxModifier:");
-        pw.println("  mBrightnessLowerBound=" + mBrightnessLowerBound);
         pw.println("  mIsActive=" + mIsActive);
+        pw.println("  mBrightnessLowerBound=" + mBrightnessLowerBound);
         pw.println("  mReason=" + mReason);
     }
 
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessModifier.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessModifier.java
index be8fa5a..2a3dd87 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessModifier.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessModifier.java
@@ -68,4 +68,9 @@
     public void stop() {
         // do nothing
     }
+
+    @Override
+    public void onAmbientLuxChange(float ambientLux) {
+        // do nothing
+    }
 }
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessStateModifier.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessStateModifier.java
index 441ba8f..2234258 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessStateModifier.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessStateModifier.java
@@ -42,4 +42,10 @@
      * Called when stopped. Listeners can be unregistered here.
      */
     void stop();
+
+    /**
+     * Allows modifiers to react to ambient lux changes.
+     * @param ambientLux current debounced lux.
+     */
+    void onAmbientLuxChange(float ambientLux);
 }
diff --git a/services/core/java/com/android/server/display/config/LowBrightnessData.java b/services/core/java/com/android/server/display/config/LowBrightnessData.java
new file mode 100644
index 0000000..aa82533
--- /dev/null
+++ b/services/core/java/com/android/server/display/config/LowBrightnessData.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display.config;
+
+import android.annotation.Nullable;
+import android.util.Slog;
+import android.util.Spline;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Brightness config for low brightness mode
+ */
+public class LowBrightnessData {
+    private static final String TAG = "LowBrightnessData";
+
+    /**
+     * Brightness value at which lower brightness methods are used.
+     */
+    public final float mTransitionPoint;
+
+    /**
+     * Nits array, maps to mBacklight
+     */
+    public final float[] mNits;
+
+    /**
+     * Backlight array, maps to mBrightness and mNits
+     */
+    public final float[] mBacklight;
+
+    /**
+     * Brightness array, maps to mBacklight
+     */
+    public final float[] mBrightness;
+    /**
+     * Spline, mapping between backlight and nits
+     */
+    public final Spline mBacklightToNits;
+    /**
+     * Spline, mapping between nits and backlight
+     */
+    public final Spline mNitsToBacklight;
+    /**
+     * Spline, mapping between brightness and backlight
+     */
+    public final Spline mBrightnessToBacklight;
+    /**
+     * Spline, mapping between backlight and brightness
+     */
+    public final Spline mBacklightToBrightness;
+
+    @VisibleForTesting
+    public LowBrightnessData(float transitionPoint, float[] nits,
+            float[] backlight, float[] brightness, Spline backlightToNits,
+            Spline nitsToBacklight, Spline brightnessToBacklight, Spline backlightToBrightness) {
+        mTransitionPoint = transitionPoint;
+        mNits = nits;
+        mBacklight = backlight;
+        mBrightness = brightness;
+        mBacklightToNits = backlightToNits;
+        mNitsToBacklight = nitsToBacklight;
+        mBrightnessToBacklight = brightnessToBacklight;
+        mBacklightToBrightness = backlightToBrightness;
+    }
+
+    @Override
+    public String toString() {
+        return "LowBrightnessData {"
+                + "mTransitionPoint: " + mTransitionPoint
+                + ", mNits: " + Arrays.toString(mNits)
+                + ", mBacklight: " + Arrays.toString(mBacklight)
+                + ", mBrightness: " + Arrays.toString(mBrightness)
+                + ", mBacklightToNits: " + mBacklightToNits
+                + ", mNitsToBacklight: " + mNitsToBacklight
+                + ", mBrightnessToBacklight: " + mBrightnessToBacklight
+                + ", mBacklightToBrightness: " + mBacklightToBrightness
+                + "} ";
+    }
+
+    /**
+     * Loads LowBrightnessData from DisplayConfiguration
+     */
+    @Nullable
+    public static LowBrightnessData loadConfig(DisplayConfiguration config) {
+        final LowBrightnessMode lbm = config.getLowBrightness();
+        if (lbm == null) {
+            return null;
+        }
+
+        boolean lbmIsEnabled = lbm.getEnabled();
+        if (!lbmIsEnabled) {
+            return null;
+        }
+
+        List<Float> nitsList = lbm.getNits();
+        List<Float> backlightList = lbm.getBacklight();
+        List<Float> brightnessList = lbm.getBrightness();
+        float transitionPoints = lbm.getTransitionPoint().floatValue();
+
+        if (nitsList.isEmpty()
+                || backlightList.size() != brightnessList.size()
+                || backlightList.size() != nitsList.size()) {
+            Slog.e(TAG, "Invalid low brightness array lengths");
+            return null;
+        }
+
+        float[] nits = new float[nitsList.size()];
+        float[] backlight = new float[nitsList.size()];
+        float[] brightness = new float[nitsList.size()];
+
+        for (int i = 0; i < nitsList.size(); i++) {
+            nits[i] = nitsList.get(i);
+            backlight[i] = backlightList.get(i);
+            brightness[i] = brightnessList.get(i);
+        }
+
+        return new LowBrightnessData(transitionPoints, nits, backlight, brightness,
+                Spline.createSpline(backlight, nits),
+                Spline.createSpline(nits, backlight),
+                Spline.createSpline(brightness, backlight),
+                Spline.createSpline(backlight, brightness)
+                );
+    }
+}
diff --git a/services/core/java/com/android/server/input/KeyboardLayoutManager.java b/services/core/java/com/android/server/input/KeyboardLayoutManager.java
index 283e692..6610081 100644
--- a/services/core/java/com/android/server/input/KeyboardLayoutManager.java
+++ b/services/core/java/com/android/server/input/KeyboardLayoutManager.java
@@ -459,13 +459,16 @@
         for (ResolveInfo resolveInfo : pm.queryBroadcastReceiversAsUser(intent,
                 PackageManager.GET_META_DATA | PackageManager.MATCH_DIRECT_BOOT_AWARE
                         | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, UserHandle.USER_SYSTEM)) {
+            if (resolveInfo == null || resolveInfo.activityInfo == null) {
+                continue;
+            }
             final ActivityInfo activityInfo = resolveInfo.activityInfo;
             final int priority = resolveInfo.priority;
             visitKeyboardLayoutsInPackage(pm, activityInfo, null, priority, visitor);
         }
     }
 
-    private void visitKeyboardLayout(String keyboardLayoutDescriptor,
+    private void visitKeyboardLayout(@NonNull String keyboardLayoutDescriptor,
             KeyboardLayoutVisitor visitor) {
         KeyboardLayoutDescriptor d = KeyboardLayoutDescriptor.parse(keyboardLayoutDescriptor);
         if (d != null) {
@@ -482,8 +485,8 @@
         }
     }
 
-    private void visitKeyboardLayoutsInPackage(PackageManager pm, ActivityInfo receiver,
-            String keyboardName, int requestedPriority, KeyboardLayoutVisitor visitor) {
+    private void visitKeyboardLayoutsInPackage(PackageManager pm, @NonNull ActivityInfo receiver,
+            @Nullable String keyboardName, int requestedPriority, KeyboardLayoutVisitor visitor) {
         Bundle metaData = receiver.metaData;
         if (metaData == null) {
             return;
@@ -1415,7 +1418,7 @@
             return packageName + "/" + receiverName + "/" + keyboardName;
         }
 
-        public static KeyboardLayoutDescriptor parse(String descriptor) {
+        public static KeyboardLayoutDescriptor parse(@NonNull String descriptor) {
             int pos = descriptor.indexOf('/');
             if (pos < 0 || pos + 1 == descriptor.length()) {
                 return null;
diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
index 18b495b..25095ed 100644
--- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
+++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
@@ -4328,7 +4328,9 @@
     @GuardedBy("mUidRulesFirstLock")
     private boolean updateUidStateUL(int uid, int procState, long procStateSeq,
             @ProcessCapability int capability) {
-        Trace.traceBegin(Trace.TRACE_TAG_NETWORK, "updateUidStateUL");
+        Trace.traceBegin(Trace.TRACE_TAG_NETWORK, "updateUidStateUL: " + uid + "/"
+                + ActivityManager.procStateToString(procState) + "/" + procStateSeq + "/"
+                + ActivityManager.getCapabilitiesSummary(capability));
         try {
             final UidState oldUidState = mUidState.get(uid);
             if (oldUidState != null && procStateSeq < oldUidState.procStateSeq) {
diff --git a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
index 28682e3..37023e1 100644
--- a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
+++ b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
@@ -37,8 +37,8 @@
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.content.pm.ServiceInfo;
-import android.os.Binder;
 import android.content.res.Resources;
+import android.os.Binder;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
@@ -360,7 +360,7 @@
         synchronized (mLock) {
             if (mRemoteOnDeviceIntelligenceService == null) {
                 String serviceName = getServiceNames()[0];
-                validateService(serviceName, false);
+                Binder.withCleanCallingIdentity(() -> validateServiceElevated(serviceName, false));
                 mRemoteOnDeviceIntelligenceService = new RemoteOnDeviceIntelligenceService(mContext,
                         ComponentName.unflattenFromString(serviceName),
                         UserHandle.SYSTEM.getIdentifier());
@@ -410,7 +410,7 @@
         synchronized (mLock) {
             if (mRemoteInferenceService == null) {
                 String serviceName = getServiceNames()[1];
-                validateService(serviceName, true);
+                Binder.withCleanCallingIdentity(() -> validateServiceElevated(serviceName, true));
                 mRemoteInferenceService = new RemoteOnDeviceSandboxedInferenceService(mContext,
                         ComponentName.unflattenFromString(serviceName),
                         UserHandle.SYSTEM.getIdentifier());
@@ -457,11 +457,10 @@
         };
     }
 
-    @GuardedBy("mLock")
-    private void validateService(String serviceName, boolean checkIsolated)
+    private static void validateServiceElevated(String serviceName, boolean checkIsolated)
             throws RemoteException {
         if (TextUtils.isEmpty(serviceName)) {
-            throw new RuntimeException("");
+            throw new IllegalArgumentException("Received null/empty service name : " + serviceName);
         }
         ComponentName serviceComponent = ComponentName.unflattenFromString(
                 serviceName);
@@ -501,8 +500,7 @@
         }
     }
 
-    @GuardedBy("mLock")
-    private boolean isIsolatedService(@NonNull ServiceInfo serviceInfo) {
+    private static boolean isIsolatedService(@NonNull ServiceInfo serviceInfo) {
         return (serviceInfo.flags & ServiceInfo.FLAG_ISOLATED_PROCESS) != 0
                 && (serviceInfo.flags & ServiceInfo.FLAG_EXTERNAL_SERVICE) == 0;
     }
diff --git a/services/core/java/com/android/server/pm/RemovePackageHelper.java b/services/core/java/com/android/server/pm/RemovePackageHelper.java
index 6b12781..3a0f7fb 100644
--- a/services/core/java/com/android/server/pm/RemovePackageHelper.java
+++ b/services/core/java/com/android/server/pm/RemovePackageHelper.java
@@ -261,11 +261,6 @@
         // Step 1: always destroy app profiles.
         mAppDataHelper.destroyAppProfilesLIF(packageName);
 
-        // Everything else is preserved if the DELETE_KEEP_DATA flag is on
-        if ((flags & PackageManager.DELETE_KEEP_DATA) != 0) {
-            return;
-        }
-
         final AndroidPackage pkg;
         final SharedUserSetting sus;
         synchronized (mPm.mLock) {
@@ -282,9 +277,20 @@
             resolvedPkg = PackageImpl.buildFakeForDeletion(packageName, ps.getVolumeUuid());
         }
 
+        int appDataDeletionFlags = FLAG_STORAGE_DE | FLAG_STORAGE_CE | FLAG_STORAGE_EXTERNAL;
+        // Personal data is preserved if the DELETE_KEEP_DATA flag is on
+        if ((flags & PackageManager.DELETE_KEEP_DATA) != 0) {
+            if ((flags & PackageManager.DELETE_ARCHIVE) != 0) {
+                mAppDataHelper.clearAppDataLIF(resolvedPkg, userId,
+                        appDataDeletionFlags | Installer.FLAG_CLEAR_CACHE_ONLY);
+                mAppDataHelper.clearAppDataLIF(resolvedPkg, userId,
+                        appDataDeletionFlags | Installer.FLAG_CLEAR_CODE_CACHE_ONLY);
+            }
+            return;
+        }
+
         // Step 2: destroy app data.
-        mAppDataHelper.destroyAppDataLIF(resolvedPkg, userId,
-                FLAG_STORAGE_DE | FLAG_STORAGE_CE | FLAG_STORAGE_EXTERNAL);
+        mAppDataHelper.destroyAppDataLIF(resolvedPkg, userId, appDataDeletionFlags);
         if (userId != UserHandle.USER_ALL) {
             ps.setCeDataInode(-1, userId);
             ps.setDeDataInode(-1, userId);
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 9e31748..12a5892 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -1905,6 +1905,7 @@
             accessibilityManager.performSystemAction(
                     AccessibilityService.GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS);
         }
+        dismissKeyboardShortcutsMenu();
     }
 
     private void toggleNotificationPanel() {
@@ -3478,13 +3479,6 @@
                     return true;
                 }
                 break;
-            case KeyEvent.KEYCODE_T:
-                if (firstDown && event.isMetaPressed()) {
-                    toggleTaskbar();
-                    logKeyboardSystemsEvent(event, KeyboardLogEvent.TOGGLE_TASKBAR);
-                    return true;
-                }
-                break;
             case KeyEvent.KEYCODE_DEL:
             case KeyEvent.KEYCODE_ESCAPE:
                 if (firstDown && event.isMetaPressed()) {
@@ -4735,7 +4729,7 @@
                 if (down) {
                     // There may have other embedded activities on the same Task. Try to move the
                     // focus before processing the back event.
-                    mWindowManagerInternal.moveFocusToTopEmbeddedWindowIfNeeded();
+                    mWindowManagerInternal.moveFocusToAdjacentEmbeddedActivityIfNeeded();
                     mBackKeyHandled = false;
                 } else {
                     if (!hasLongPressOnBackBehavior()) {
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 23f9743..17e6996 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -11012,6 +11012,20 @@
     }
 
     /**
+     * Returns the {@link #createTime} if the top window is the `base` window. Note that do not
+     * use the window creation time because the window could be re-created when the activity
+     * relaunched if configuration changed.
+     * <p>
+     * Otherwise, return the creation time of the top window.
+     */
+    long getLastWindowCreateTime() {
+        final WindowState window = getWindow(win -> true);
+        return window != null && window.mAttrs.type != TYPE_BASE_APPLICATION
+                ? window.getCreateTime()
+                : createTime;
+    }
+
+    /**
      * Adjust the source rect hint in {@link #pictureInPictureArgs} by window bounds since
      * it is relative to its root view (see also b/235599028).
      * It is caller's responsibility to make sure this is called exactly once when we update
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index e3ac35c..48d78f5 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -165,7 +165,7 @@
             }
 
             // Move focus to the top embedded window if possible
-            if (mWindowManagerService.moveFocusToTopEmbeddedWindow(window)) {
+            if (mWindowManagerService.moveFocusToAdjacentEmbeddedWindow(window)) {
                 window = wmService.getFocusedWindowLocked();
                 if (window == null) {
                     Slog.e(TAG, "New focused window is null, returning null.");
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 55dc30c..4c282bd 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -1274,7 +1274,8 @@
         if (!isLeafTaskFragment()) {
             final ActivityRecord top = topRunningActivity();
             final ActivityRecord resumedActivity = getResumedActivity();
-            if (resumedActivity != null && top.getTaskFragment() != this) {
+            if (resumedActivity != null
+                    && (top.getTaskFragment() != this || !canBeResumed(resuming))) {
                 // Pausing the resumed activity because it is occluded by other task fragment.
                 if (startPausing(false /* uiSleeping*/, resuming, reason)) {
                     someActivityPaused[0]++;
@@ -3753,11 +3754,9 @@
                 // Boost the adjacent TaskFragment for dimmer if needed.
                 final TaskFragment taskFragment = wc.asTaskFragment();
                 if (taskFragment != null && taskFragment.isEmbedded()) {
-                    taskFragment.mDimmerSurfaceBoosted = false;
                     final TaskFragment adjacentTf = taskFragment.getAdjacentTaskFragment();
                     if (adjacentTf != null && adjacentTf.shouldBoostDimmer()) {
                         adjacentTf.assignLayer(t, layer++);
-                        adjacentTf.mDimmerSurfaceBoosted = true;
                     }
                 }
 
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 3cf561c..dc0e034 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -216,9 +216,6 @@
     Dimmer mDimmer = Dimmer.DIMMER_REFACTOR
             ? new SmoothDimmer(this) : new LegacyDimmer(this);
 
-    /** {@code true} if the dimmer surface is boosted. {@code false} otherwise. */
-    boolean mDimmerSurfaceBoosted;
-
     /** Apply the dim layer on the embedded TaskFragment. */
     static final int EMBEDDED_DIM_AREA_TASK_FRAGMENT = 0;
 
diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java
index acc6330..daf8129 100644
--- a/services/core/java/com/android/server/wm/WindowManagerInternal.java
+++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java
@@ -1068,9 +1068,9 @@
     public abstract void clearBlockedApps();
 
     /**
-     * Moves the current focus to the top activity window if the top activity is embedded.
+     * Moves the current focus to the adjacent activity if it has the latest created window.
      */
-    public abstract boolean moveFocusToTopEmbeddedWindowIfNeeded();
+    public abstract boolean moveFocusToAdjacentEmbeddedActivityIfNeeded();
 
     /**
      * Returns an instance of {@link ScreenCapture.ScreenshotHardwareBuffer} containing the current
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 207b1bb..2934574 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -8700,14 +8700,14 @@
         }
 
         @Override
-        public boolean moveFocusToTopEmbeddedWindowIfNeeded() {
+        public boolean moveFocusToAdjacentEmbeddedActivityIfNeeded() {
             synchronized (mGlobalLock) {
                 final WindowState focusedWindow = getFocusedWindow();
                 if (focusedWindow == null) {
                     return false;
                 }
 
-                if (moveFocusToTopEmbeddedWindow(focusedWindow)) {
+                if (moveFocusToAdjacentEmbeddedWindow(focusedWindow)) {
                     // Sync the input transactions to ensure the input focus updates as well.
                     syncInputTransactions(false);
                     return true;
@@ -9219,9 +9219,10 @@
     }
 
     /**
-     * Move focus to the top embedded window if possible.
+     * Move focus to the adjacent embedded activity if the adjacent activity is more recently
+     * created or has a window more recently added.
      */
-    boolean moveFocusToTopEmbeddedWindow(@NonNull WindowState focusedWindow) {
+    boolean moveFocusToAdjacentEmbeddedWindow(@NonNull WindowState focusedWindow) {
         final TaskFragment taskFragment = focusedWindow.getTaskFragment();
         if (taskFragment == null) {
             // Skip if not an Activity window.
@@ -9233,31 +9234,25 @@
             return false;
         }
 
-        if (taskFragment.mDimmerSurfaceBoosted) {
-            // Skip if the TaskFragment currently has dimmer surface boosted.
+        if (!focusedWindow.mActivityRecord.isEmbedded()) {
+            // Skip if the focused activity is not embedded
             return false;
         }
 
-        final ActivityRecord topActivity =
-                taskFragment.getTask().topRunningActivity(true /* focusableOnly */);
-        if (topActivity == null || topActivity == focusedWindow.mActivityRecord) {
-            // Skip if the focused activity is already the top-most activity on the Task.
+        final TaskFragment adjacentTaskFragment = taskFragment.getAdjacentTaskFragment();
+        final ActivityRecord adjacentTopActivity =
+                adjacentTaskFragment != null ? adjacentTaskFragment.topRunningActivity() : null;
+        if (adjacentTopActivity == null) {
             return false;
         }
 
-        if (!topActivity.isEmbedded()) {
-            // Skip if the top activity is not embedded
+        if (adjacentTopActivity.getLastWindowCreateTime()
+                < focusedWindow.mActivityRecord.getLastWindowCreateTime()) {
+            // Skip if the current focus activity has more recently active window.
             return false;
         }
 
-        final TaskFragment topTaskFragment = topActivity.getTaskFragment();
-        if (topTaskFragment.isIsolatedNav()
-                && taskFragment.getAdjacentTaskFragment() == topTaskFragment) {
-            // Skip if the top TaskFragment is adjacent to current focus and is set to isolated nav.
-            return false;
-        }
-
-        moveFocusToActivity(topActivity);
+        moveFocusToActivity(adjacentTopActivity);
         return !focusedWindow.isFocused();
     }
 
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 2b337ae..c0cf97d 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -364,6 +364,7 @@
     private boolean mDragResizing;
     private boolean mDragResizingChangeReported = true;
     private boolean mRedrawForSyncReported = true;
+    private long mCreateTime = System.currentTimeMillis();
 
     /**
      * Used to assosciate a given set of state changes sent from MSG_RESIZED
@@ -1714,6 +1715,10 @@
                 : DEFAULT_DISPATCHING_TIMEOUT_MILLIS;
     }
 
+    long getCreateTime() {
+        return mCreateTime;
+    }
+
     /**
      * Returns true if, at any point, the application token associated with this window has actually
      * displayed any windows. This is most useful with the "starting up" window to determine if any
diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd
index d0df2b2..1f54518 100644
--- a/services/core/xsd/display-device-config/display-device-config.xsd
+++ b/services/core/xsd/display-device-config/display-device-config.xsd
@@ -162,6 +162,10 @@
                 <xs:element type="usiVersion" name="usiVersion">
                     <xs:annotation name="final"/>
                 </xs:element>
+                <xs:element type="lowBrightnessMode" name="lowBrightness">
+                    <xs:attribute name="enabled" type="xs:boolean" use="optional"/>
+                    <xs:annotation name="final"/>
+                </xs:element>
                 <!-- Maximum screen brightness setting when screen brightness capped in
                 Wear Bedtime mode. This must be a non-negative decimal within the range defined by
                 the first and the last brightness value in screenBrightnessMap. -->
@@ -172,6 +176,7 @@
                 <xs:element type="idleScreenRefreshRateTimeout" name="idleScreenRefreshRateTimeout" minOccurs="0">
                     <xs:annotation name="final"/>
                 </xs:element>
+
             </xs:sequence>
         </xs:complexType>
     </xs:element>
@@ -216,6 +221,21 @@
         </xs:restriction>
     </xs:simpleType>
 
+    <xs:complexType name="lowBrightnessMode">
+        <xs:sequence>
+            <xs:element name="transitionPoint" type="nonNegativeDecimal" minOccurs="1"
+                maxOccurs="1">
+            </xs:element>
+            <xs:element name="nits" type="xs:float" maxOccurs="unbounded">
+            </xs:element>
+            <xs:element name="backlight" type="xs:float" maxOccurs="unbounded">
+            </xs:element>
+            <xs:element name="brightness" type="xs:float" maxOccurs="unbounded">
+            </xs:element>
+        </xs:sequence>
+        <xs:attribute name="enabled" type="xs:boolean" use="optional"/>
+    </xs:complexType>
+
     <xs:complexType name="highBrightnessMode">
         <xs:all>
             <xs:element name="transitionPoint" type="nonNegativeDecimal" minOccurs="1"
diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt
index 00dc908..c39c3d7 100644
--- a/services/core/xsd/display-device-config/schema/current.txt
+++ b/services/core/xsd/display-device-config/schema/current.txt
@@ -113,6 +113,7 @@
     method public com.android.server.display.config.HighBrightnessMode getHighBrightnessMode();
     method public final com.android.server.display.config.IdleScreenRefreshRateTimeout getIdleScreenRefreshRateTimeout();
     method public final com.android.server.display.config.SensorDetails getLightSensor();
+    method public final com.android.server.display.config.LowBrightnessMode getLowBrightness();
     method public com.android.server.display.config.LuxThrottling getLuxThrottling();
     method @Nullable public final String getName();
     method public com.android.server.display.config.PowerThrottlingConfig getPowerThrottlingConfig();
@@ -149,6 +150,7 @@
     method public void setHighBrightnessMode(com.android.server.display.config.HighBrightnessMode);
     method public final void setIdleScreenRefreshRateTimeout(com.android.server.display.config.IdleScreenRefreshRateTimeout);
     method public final void setLightSensor(com.android.server.display.config.SensorDetails);
+    method public final void setLowBrightness(com.android.server.display.config.LowBrightnessMode);
     method public void setLuxThrottling(com.android.server.display.config.LuxThrottling);
     method public final void setName(@Nullable String);
     method public void setPowerThrottlingConfig(com.android.server.display.config.PowerThrottlingConfig);
@@ -248,6 +250,17 @@
     method public java.util.List<java.math.BigInteger> getItem();
   }
 
+  public class LowBrightnessMode {
+    ctor public LowBrightnessMode();
+    method public java.util.List<java.lang.Float> getBacklight();
+    method public java.util.List<java.lang.Float> getBrightness();
+    method public boolean getEnabled();
+    method public java.util.List<java.lang.Float> getNits();
+    method public java.math.BigDecimal getTransitionPoint();
+    method public void setEnabled(boolean);
+    method public void setTransitionPoint(java.math.BigDecimal);
+  }
+
   public class LuxThrottling {
     ctor public LuxThrottling();
     method @NonNull public final java.util.List<com.android.server.display.config.BrightnessLimitMap> getBrightnessLimitMap();
diff --git a/services/devicepolicy/Android.bp b/services/devicepolicy/Android.bp
index 8dfa685..da965bb 100644
--- a/services/devicepolicy/Android.bp
+++ b/services/devicepolicy/Android.bp
@@ -24,5 +24,6 @@
         "app-compat-annotations",
         "service-permission.stubs.system_server",
         "device_policy_aconfig_flags_lib",
+        "androidx.annotation_annotation",
     ],
 }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/OverlayPackagesProvider.java b/services/devicepolicy/java/com/android/server/devicepolicy/OverlayPackagesProvider.java
index f3b164c..94c1374 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/OverlayPackagesProvider.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/OverlayPackagesProvider.java
@@ -25,15 +25,16 @@
 import static android.content.pm.PackageManager.GET_META_DATA;
 
 import static com.android.internal.util.Preconditions.checkArgument;
-import static com.android.internal.util.Preconditions.checkNotNull;
-import static com.android.server.devicepolicy.DevicePolicyManagerService.dumpResources;
+import static com.android.server.devicepolicy.DevicePolicyManagerService.dumpApps;
 
 import static java.util.Objects.requireNonNull;
 
+import android.annotation.ArrayRes;
 import android.annotation.NonNull;
 import android.annotation.UserIdInt;
 import android.app.admin.DeviceAdminReceiver;
 import android.app.admin.DevicePolicyManager;
+import android.app.admin.flags.Flags;
 import android.app.role.RoleManager;
 import android.content.ComponentName;
 import android.content.Context;
@@ -67,13 +68,16 @@
 
     protected static final String TAG = "OverlayPackagesProvider";
     private static final Map<String, String> sActionToMetadataKeyMap = new HashMap<>();
-    {
+
+    static {
         sActionToMetadataKeyMap.put(ACTION_PROVISION_MANAGED_USER, REQUIRED_APP_MANAGED_USER);
         sActionToMetadataKeyMap.put(ACTION_PROVISION_MANAGED_PROFILE, REQUIRED_APP_MANAGED_PROFILE);
         sActionToMetadataKeyMap.put(ACTION_PROVISION_MANAGED_DEVICE, REQUIRED_APP_MANAGED_DEVICE);
     }
+
     private static final Set<String> sAllowedActions = new HashSet<>();
-    {
+
+    static {
         sAllowedActions.add(ACTION_PROVISION_MANAGED_USER);
         sAllowedActions.add(ACTION_PROVISION_MANAGED_PROFILE);
         sAllowedActions.add(ACTION_PROVISION_MANAGED_DEVICE);
@@ -83,8 +87,13 @@
     private final Context mContext;
     private final Injector mInjector;
 
+    private final RecursiveStringArrayResourceResolver mRecursiveStringArrayResourceResolver;
+
     public OverlayPackagesProvider(Context context) {
-        this(context, new DefaultInjector());
+        this(
+                context,
+                new DefaultInjector(),
+                new RecursiveStringArrayResourceResolver(context.getResources()));
     }
 
     @VisibleForTesting
@@ -113,8 +122,8 @@
         public String getDevicePolicyManagementRoleHolderPackageName(Context context) {
             return Binder.withCleanCallingIdentity(() -> {
                 RoleManager roleManager = context.getSystemService(RoleManager.class);
-                List<String> roleHolders =
-                        roleManager.getRoleHolders(RoleManager.ROLE_DEVICE_POLICY_MANAGEMENT);
+                List<String> roleHolders = roleManager.getRoleHolders(
+                        RoleManager.ROLE_DEVICE_POLICY_MANAGEMENT);
                 if (roleHolders.isEmpty()) {
                     return null;
                 }
@@ -124,17 +133,20 @@
     }
 
     @VisibleForTesting
-    OverlayPackagesProvider(Context context, Injector injector) {
+    OverlayPackagesProvider(Context context, Injector injector,
+            RecursiveStringArrayResourceResolver recursiveStringArrayResourceResolver) {
         mContext = context;
-        mPm = checkNotNull(context.getPackageManager());
-        mInjector = checkNotNull(injector);
+        mPm = requireNonNull(context.getPackageManager());
+        mInjector = requireNonNull(injector);
+        mRecursiveStringArrayResourceResolver = requireNonNull(
+                recursiveStringArrayResourceResolver);
     }
 
     /**
      * Computes non-required apps. All the system apps with a launcher that are not in
      * the required set of packages, and all mainline modules that are not declared as required
      * via metadata in their manifests, will be considered as non-required apps.
-     *
+     * <p>
      * Note: If an app is mistakenly listed as both required and disallowed, it will be treated as
      * disallowed.
      *
@@ -176,12 +188,12 @@
     /**
      * Returns a subset of {@code packageNames} whose packages are mainline modules declared as
      * required apps via their app metadata.
+     *
      * @see DevicePolicyManager#REQUIRED_APP_MANAGED_USER
      * @see DevicePolicyManager#REQUIRED_APP_MANAGED_DEVICE
      * @see DevicePolicyManager#REQUIRED_APP_MANAGED_PROFILE
      */
-    private Set<String> getRequiredAppsMainlineModules(
-            Set<String> packageNames,
+    private Set<String> getRequiredAppsMainlineModules(Set<String> packageNames,
             String provisioningAction) {
         final Set<String> result = new HashSet<>();
         for (String packageName : packageNames) {
@@ -225,8 +237,8 @@
     }
 
     private boolean isApkInApexMainlineModule(String packageName) {
-        final String apexPackageName =
-                mInjector.getActiveApexPackageNameContainingPackage(packageName);
+        final String apexPackageName = mInjector.getActiveApexPackageNameContainingPackage(
+                packageName);
         return apexPackageName != null;
     }
 
@@ -274,112 +286,94 @@
     }
 
     private Set<String> getRequiredAppsSet(String provisioningAction) {
-        final int resId;
-        switch (provisioningAction) {
-            case ACTION_PROVISION_MANAGED_USER:
-                resId = R.array.required_apps_managed_user;
-                break;
-            case ACTION_PROVISION_MANAGED_PROFILE:
-                resId = R.array.required_apps_managed_profile;
-                break;
-            case ACTION_PROVISION_MANAGED_DEVICE:
-                resId = R.array.required_apps_managed_device;
-                break;
-            default:
-                throw new IllegalArgumentException("Provisioning type "
-                        + provisioningAction + " not supported.");
-        }
-        return new ArraySet<>(Arrays.asList(mContext.getResources().getStringArray(resId)));
+        final int resId = switch (provisioningAction) {
+            case ACTION_PROVISION_MANAGED_USER -> R.array.required_apps_managed_user;
+            case ACTION_PROVISION_MANAGED_PROFILE -> R.array.required_apps_managed_profile;
+            case ACTION_PROVISION_MANAGED_DEVICE -> R.array.required_apps_managed_device;
+            default -> throw new IllegalArgumentException(
+                    "Provisioning type " + provisioningAction + " not supported.");
+        };
+        return resolveStringArray(resId);
     }
 
     private Set<String> getDisallowedAppsSet(String provisioningAction) {
-        final int resId;
-        switch (provisioningAction) {
-            case ACTION_PROVISION_MANAGED_USER:
-                resId = R.array.disallowed_apps_managed_user;
-                break;
-            case ACTION_PROVISION_MANAGED_PROFILE:
-                resId = R.array.disallowed_apps_managed_profile;
-                break;
-            case ACTION_PROVISION_MANAGED_DEVICE:
-                resId = R.array.disallowed_apps_managed_device;
-                break;
-            default:
-                throw new IllegalArgumentException("Provisioning type "
-                        + provisioningAction + " not supported.");
-        }
-        return new ArraySet<>(Arrays.asList(mContext.getResources().getStringArray(resId)));
+        final int resId = switch (provisioningAction) {
+            case ACTION_PROVISION_MANAGED_USER -> R.array.disallowed_apps_managed_user;
+            case ACTION_PROVISION_MANAGED_PROFILE -> R.array.disallowed_apps_managed_profile;
+            case ACTION_PROVISION_MANAGED_DEVICE -> R.array.disallowed_apps_managed_device;
+            default -> throw new IllegalArgumentException(
+                    "Provisioning type " + provisioningAction + " not supported.");
+        };
+        return resolveStringArray(resId);
     }
 
     private Set<String> getVendorRequiredAppsSet(String provisioningAction) {
-        final int resId;
-        switch (provisioningAction) {
-            case ACTION_PROVISION_MANAGED_USER:
-                resId = R.array.vendor_required_apps_managed_user;
-                break;
-            case ACTION_PROVISION_MANAGED_PROFILE:
-                resId = R.array.vendor_required_apps_managed_profile;
-                break;
-            case ACTION_PROVISION_MANAGED_DEVICE:
-                resId = R.array.vendor_required_apps_managed_device;
-                break;
-            default:
-                throw new IllegalArgumentException("Provisioning type "
-                        + provisioningAction + " not supported.");
-        }
-        return new ArraySet<>(Arrays.asList(mContext.getResources().getStringArray(resId)));
+        final int resId = switch (provisioningAction) {
+            case ACTION_PROVISION_MANAGED_USER -> R.array.vendor_required_apps_managed_user;
+            case ACTION_PROVISION_MANAGED_PROFILE -> R.array.vendor_required_apps_managed_profile;
+            case ACTION_PROVISION_MANAGED_DEVICE -> R.array.vendor_required_apps_managed_device;
+            default -> throw new IllegalArgumentException(
+                    "Provisioning type " + provisioningAction + " not supported.");
+        };
+        return resolveStringArray(resId);
     }
 
     private Set<String> getVendorDisallowedAppsSet(String provisioningAction) {
-        final int resId;
-        switch (provisioningAction) {
-            case ACTION_PROVISION_MANAGED_USER:
-                resId = R.array.vendor_disallowed_apps_managed_user;
-                break;
-            case ACTION_PROVISION_MANAGED_PROFILE:
-                resId = R.array.vendor_disallowed_apps_managed_profile;
-                break;
-            case ACTION_PROVISION_MANAGED_DEVICE:
-                resId = R.array.vendor_disallowed_apps_managed_device;
-                break;
-            default:
-                throw new IllegalArgumentException("Provisioning type "
-                        + provisioningAction + " not supported.");
+        final int resId = switch (provisioningAction) {
+            case ACTION_PROVISION_MANAGED_USER -> R.array.vendor_disallowed_apps_managed_user;
+            case ACTION_PROVISION_MANAGED_PROFILE -> R.array.vendor_disallowed_apps_managed_profile;
+            case ACTION_PROVISION_MANAGED_DEVICE -> R.array.vendor_disallowed_apps_managed_device;
+            default -> throw new IllegalArgumentException(
+                    "Provisioning type " + provisioningAction + " not supported.");
+        };
+        return resolveStringArray(resId);
+    }
+
+    private Set<String> resolveStringArray(@ArrayRes int resId) {
+        if (Flags.isRecursiveRequiredAppMergingEnabled()) {
+            return mRecursiveStringArrayResourceResolver.resolve(mContext.getPackageName(), resId);
+        } else {
+            return new ArraySet<>(Arrays.asList(mContext.getResources().getStringArray(resId)));
         }
-        return new ArraySet<>(Arrays.asList(mContext.getResources().getStringArray(resId)));
     }
 
     void dump(IndentingPrintWriter pw) {
         pw.println("OverlayPackagesProvider");
         pw.increaseIndent();
 
-        dumpResources(pw, mContext, "required_apps_managed_device",
-                R.array.required_apps_managed_device);
-        dumpResources(pw, mContext, "required_apps_managed_user",
-                R.array.required_apps_managed_user);
-        dumpResources(pw, mContext, "required_apps_managed_profile",
-                R.array.required_apps_managed_profile);
+        dumpApps(pw, "required_apps_managed_device",
+                resolveStringArray(R.array.required_apps_managed_device).toArray(String[]::new));
+        dumpApps(pw, "required_apps_managed_user",
+                resolveStringArray(R.array.required_apps_managed_user).toArray(String[]::new));
+        dumpApps(pw, "required_apps_managed_profile",
+                resolveStringArray(R.array.required_apps_managed_profile).toArray(String[]::new));
 
-        dumpResources(pw, mContext, "disallowed_apps_managed_device",
-                R.array.disallowed_apps_managed_device);
-        dumpResources(pw, mContext, "disallowed_apps_managed_user",
-                R.array.disallowed_apps_managed_user);
-        dumpResources(pw, mContext, "disallowed_apps_managed_device",
-                R.array.disallowed_apps_managed_device);
+        dumpApps(pw, "disallowed_apps_managed_device",
+                resolveStringArray(R.array.disallowed_apps_managed_device).toArray(String[]::new));
+        dumpApps(pw, "disallowed_apps_managed_user",
+                resolveStringArray(R.array.disallowed_apps_managed_user).toArray(String[]::new));
+        dumpApps(pw, "disallowed_apps_managed_device",
+                resolveStringArray(R.array.disallowed_apps_managed_device).toArray(String[]::new));
 
-        dumpResources(pw, mContext, "vendor_required_apps_managed_device",
-                R.array.vendor_required_apps_managed_device);
-        dumpResources(pw, mContext, "vendor_required_apps_managed_user",
-                R.array.vendor_required_apps_managed_user);
-        dumpResources(pw, mContext, "vendor_required_apps_managed_profile",
-                R.array.vendor_required_apps_managed_profile);
+        dumpApps(pw, "vendor_required_apps_managed_device",
+                resolveStringArray(R.array.vendor_required_apps_managed_device).toArray(
+                        String[]::new));
+        dumpApps(pw, "vendor_required_apps_managed_user",
+                resolveStringArray(R.array.vendor_required_apps_managed_user).toArray(
+                        String[]::new));
+        dumpApps(pw, "vendor_required_apps_managed_profile",
+                resolveStringArray(R.array.vendor_required_apps_managed_profile).toArray(
+                        String[]::new));
 
-        dumpResources(pw, mContext, "vendor_disallowed_apps_managed_user",
-                R.array.vendor_disallowed_apps_managed_user);
-        dumpResources(pw, mContext, "vendor_disallowed_apps_managed_device",
-                R.array.vendor_disallowed_apps_managed_device);
-        dumpResources(pw, mContext, "vendor_disallowed_apps_managed_profile",
-                R.array.vendor_disallowed_apps_managed_profile);
+        dumpApps(pw, "vendor_disallowed_apps_managed_user",
+                resolveStringArray(R.array.vendor_disallowed_apps_managed_user).toArray(
+                        String[]::new));
+        dumpApps(pw, "vendor_disallowed_apps_managed_device",
+                resolveStringArray(R.array.vendor_disallowed_apps_managed_device).toArray(
+                        String[]::new));
+        dumpApps(pw, "vendor_disallowed_apps_managed_profile",
+                resolveStringArray(R.array.vendor_disallowed_apps_managed_profile).toArray(
+                        String[]::new));
 
         pw.decreaseIndent();
     }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/RecursiveStringArrayResourceResolver.java b/services/devicepolicy/java/com/android/server/devicepolicy/RecursiveStringArrayResourceResolver.java
new file mode 100644
index 0000000..935e051
--- /dev/null
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/RecursiveStringArrayResourceResolver.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.devicepolicy;
+
+import android.annotation.SuppressLint;
+import android.content.res.Resources;
+
+import androidx.annotation.ArrayRes;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * A class encapsulating all the logic for recursive string-array resource resolution.
+ */
+public class RecursiveStringArrayResourceResolver {
+    private static final String IMPORT_PREFIX = "#import:";
+    private static final String SEPARATOR = "/";
+    private static final String PWP = ".";
+
+    private final Resources mResources;
+
+    /**
+     * @param resources Android resource access object to use when resolving resources
+     */
+    public RecursiveStringArrayResourceResolver(Resources resources) {
+        this.mResources = resources;
+    }
+
+    /**
+     * Resolves a given {@code <string-array/>} resource specified via
+     * {@param rootId} in {@param pkg}. During resolution all values prefixed with
+     * {@link #IMPORT_PREFIX} are expanded and injected
+     * into the final list at the position of the import statement,
+     * pushing all the following values (and their expansions) down.
+     * Circular imports are tracked and skipped to avoid infinite resolution loops without losing
+     * data.
+     *
+     * <p>
+     * The import statements are expected in a form of
+     * "{@link #IMPORT_PREFIX}{package}{@link #SEPARATOR}{resourceName}"
+     * If the resource being imported is from the same package, its package can be specified as a
+     * {@link #PWP} shorthand `.`
+     * > e.g.:
+     * >   {@code "#import:com.android.internal/disallowed_apps_managed_user"}
+     * >   {@code "#import:./disallowed_apps_managed_user"}
+     *
+     * <p>
+     * Any incorrect or unresolvable import statement
+     * will cause the entire resolution to fail with an error.
+     *
+     * @param pkg    the package owning the resource
+     * @param rootId the id of the {@code <string-array>} resource within {@param pkg} to start the
+     *               resolution from
+     * @return a flattened list of all the resolved string array values from the root resource
+     * as well as all the imported arrays
+     */
+    public Set<String> resolve(String pkg, @ArrayRes int rootId) {
+        return resolve(List.of(), pkg, rootId);
+    }
+
+    /**
+     * A version of resolve that tracks already imported resources
+     * to avoid circular imports and wasted work.
+     *
+     * @param cache a list of already resolved packages to be skipped for further resolution
+     */
+    private Set<String> resolve(Collection<String> cache, String pkg, @ArrayRes int rootId) {
+        final var strings = mResources.getStringArray(rootId);
+        final var runningCache = new ArrayList<>(cache);
+
+        final var result = new HashSet<String>();
+        for (var string : strings) {
+            final String ref;
+            if (string.startsWith(IMPORT_PREFIX)) {
+                ref = string.substring(IMPORT_PREFIX.length());
+            } else {
+                ref = null;
+            }
+
+            if (ref == null) {
+                result.add(string);
+            } else if (!runningCache.contains(ref)) {
+                final var next = resolveImport(runningCache, pkg, ref);
+                runningCache.addAll(next);
+                result.addAll(next);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Resolves an import of the {@code <string-array>} resource
+     * in the context of {@param importingPackage} by the provided {@param ref}.
+     *
+     * @param cache            a list of already resolved packages to be passed along into chained
+     *                         {@link #resolve} calls
+     * @param importingPackage the package that owns the resource which defined the import being
+     *                         processed.
+     *                         It is also used to expand all {@link #PWP} shorthands in
+     *                         {@param ref}
+     * @param ref              reference to the resource to be imported in a form of
+     *                         "{package}{@link #SEPARATOR}{resourceName}".
+     *                         e.g.: {@code com.android.internal/disallowed_apps_managed_user}
+     */
+    private Set<String> resolveImport(
+            Collection<String> cache,
+            String importingPackage,
+            String ref) {
+        final var chunks = ref.split(SEPARATOR, 2);
+        final var pkg = chunks[0];
+        final var name = chunks[1];
+        final String resolvedPkg;
+        if (Objects.equals(pkg, PWP)) {
+            resolvedPkg = importingPackage;
+        } else {
+            resolvedPkg = pkg;
+        }
+        @SuppressLint("DiscouragedApi") final var importId = mResources.getIdentifier(
+                /* name = */ name,
+                /* defType = */ "array",
+                /* defPackage = */ resolvedPkg);
+        if (importId == 0) {
+            throw new Resources.NotFoundException(
+                    /* name= */ String.format("%s:array/%s", resolvedPkg, name));
+        }
+        return resolve(cache, resolvedPkg, importId);
+    }
+}
diff --git a/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
index b0f7bfa..54de64e 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
@@ -52,6 +52,7 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.server.display.brightness.clamper.BrightnessClamperController;
 import com.android.server.testutils.OffsettableClock;
 
 import org.junit.After;
@@ -96,6 +97,8 @@
     @Mock HysteresisLevels mScreenBrightnessThresholdsIdle;
     @Mock Handler mNoOpHandler;
     @Mock BrightnessRangeController mBrightnessRangeController;
+    @Mock
+    BrightnessClamperController mBrightnessClamperController;
     @Mock BrightnessThrottler mBrightnessThrottler;
 
     @Before
@@ -161,7 +164,8 @@
                 mAmbientBrightnessThresholdsIdle, mScreenBrightnessThresholdsIdle,
                 mContext, mBrightnessRangeController, mBrightnessThrottler,
                 useHorizon ? AMBIENT_LIGHT_HORIZON_SHORT : 1,
-                useHorizon ? AMBIENT_LIGHT_HORIZON_LONG : 10000, userLux, userNits
+                useHorizon ? AMBIENT_LIGHT_HORIZON_LONG : 10000, userLux, userNits,
+                mBrightnessClamperController
         );
 
         when(mBrightnessRangeController.getCurrentBrightnessMax()).thenReturn(
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
index 35b69f8..73a2f65 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
@@ -44,6 +44,7 @@
 import android.hardware.display.DisplayManagerInternal;
 import android.os.PowerManager;
 import android.os.Temperature;
+import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.provider.Settings;
 import android.util.SparseArray;
 import android.util.Spline;
@@ -57,6 +58,7 @@
 import com.android.server.display.config.IdleScreenRefreshRateTimeoutLuxThresholdPoint;
 import com.android.server.display.config.ThermalStatus;
 import com.android.server.display.feature.DisplayManagerFlags;
+import com.android.server.display.feature.flags.Flags;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -380,7 +382,7 @@
     public void testInvalidLuxThrottling() throws Exception {
         setupDisplayDeviceConfigFromDisplayConfigFile(
                 getContent(getInvalidLuxThrottling(), getValidProxSensor(),
-                        /* includeIdleMode= */ true));
+                        /* includeIdleMode= */ true, /* enableEvenDimmer */ false));
 
         Map<DisplayDeviceConfig.BrightnessLimitMapType, Map<Float, Float>> luxThrottlingData =
                 mDisplayDeviceConfig.getLuxThrottlingData();
@@ -588,7 +590,7 @@
     public void testProximitySensorWithEmptyValuesFromDisplayConfig() throws IOException {
         setupDisplayDeviceConfigFromDisplayConfigFile(
                 getContent(getValidLuxThrottling(), getProxSensorWithEmptyValues(),
-                        /* includeIdleMode= */ true));
+                        /* includeIdleMode= */ true, /* enableEvenDimmer */ false));
         assertNull(mDisplayDeviceConfig.getProximitySensor());
     }
 
@@ -596,7 +598,7 @@
     public void testProximitySensorWithRefreshRatesFromDisplayConfig() throws IOException {
         setupDisplayDeviceConfigFromDisplayConfigFile(
                 getContent(getValidLuxThrottling(), getValidProxSensorWithRefreshRateAndVsyncRate(),
-                        /* includeIdleMode= */ true));
+                        /* includeIdleMode= */ true, /* enableEvenDimmer */ false));
         assertEquals("test_proximity_sensor",
                 mDisplayDeviceConfig.getProximitySensor().type);
         assertEquals("Test Proximity Sensor",
@@ -784,7 +786,7 @@
     @Test
     public void testBrightnessRamps_IdleFallsBackToConfigInteractive() throws IOException {
         setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(),
-                getValidProxSensor(), /* includeIdleMode= */ false));
+                getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ false));
 
         assertEquals(mDisplayDeviceConfig.getBrightnessRampDecreaseMaxMillis(), 3000);
         assertEquals(mDisplayDeviceConfig.getBrightnessRampIncreaseMaxMillis(), 2000);
@@ -801,14 +803,14 @@
     @Test
     public void testBrightnessCapForWearBedtimeMode() throws IOException {
         setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(),
-                getValidProxSensor(), /* includeIdleMode= */ false));
+                getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ false));
         assertEquals(0.1f, mDisplayDeviceConfig.getBrightnessCapForWearBedtimeMode(), ZERO_DELTA);
     }
 
     @Test
     public void testAutoBrightnessBrighteningLevels() throws IOException {
         setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(),
-                getValidProxSensor(), /* includeIdleMode= */ false));
+                getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ false));
 
         assertArrayEquals(new float[]{0.0f, 80},
                 mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(
@@ -871,7 +873,7 @@
         when(mFlags.areAutoBrightnessModesEnabled()).thenReturn(false);
         setupDisplayDeviceConfigFromConfigResourceFile();
         setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(),
-                getValidProxSensor(), /* includeIdleMode= */ false));
+                getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ false));
 
         assertArrayEquals(new float[]{brightnessIntToFloat(50), brightnessIntToFloat(100),
                         brightnessIntToFloat(150)},
@@ -904,6 +906,18 @@
         assertFalse(mDisplayDeviceConfig.isAutoBrightnessAvailable());
     }
 
+    @RequiresFlagsEnabled(Flags.FLAG_EVEN_DIMMER)
+    @Test
+    public void testEvenDimmer() throws IOException {
+        when(mFlags.isEvenDimmerEnabled()).thenReturn(true);
+        setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(),
+                getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ true));
+
+        assertTrue(mDisplayDeviceConfig.getLbmEnabled());
+        assertEquals(0.0001f, mDisplayDeviceConfig.getBacklightFromBrightness(0.1f), ZERO_DELTA);
+        assertEquals(0.2f, mDisplayDeviceConfig.getNitsFromBacklight(0.0f), ZERO_DELTA);
+    }
+
     private String getValidLuxThrottling() {
         return "<luxThrottling>\n"
                 + "    <brightnessLimitMap>\n"
@@ -1229,11 +1243,11 @@
 
     private String getContent() {
         return getContent(getValidLuxThrottling(), getValidProxSensor(),
-                /* includeIdleMode= */ true);
+                /* includeIdleMode= */ true, false);
     }
 
     private String getContent(String brightnessCapConfig, String proxSensor,
-            boolean includeIdleMode) {
+            boolean includeIdleMode, boolean enableEvenDimmer) {
         return "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
                 + "<displayConfiguration>\n"
                 +   "<name>Example Display</name>\n"
@@ -1603,6 +1617,7 @@
                 +       "<majorVersion>2</majorVersion>\n"
                 +       "<minorVersion>0</minorVersion>\n"
                 +   "</usiVersion>\n"
+                + evenDimmerConfig(enableEvenDimmer)
                 +   "<screenBrightnessCapForWearBedtimeMode>"
                 +       "0.1"
                 +   "</screenBrightnessCapForWearBedtimeMode>"
@@ -1621,6 +1636,24 @@
                 + "</displayConfiguration>\n";
     }
 
+    private String evenDimmerConfig(boolean enabled) {
+        return (enabled ? "<lowBrightness enabled=\"true\">" : "<lowBrightness enabled=\"false\">")
+                + "  <transitionPoint>0.1</transitionPoint>\n"
+                + "  <nits>0.2</nits>\n"
+                + "  <nits>2.0</nits>\n"
+                + "  <nits>500.0</nits>\n"
+                + "  <nits>1000.0</nits>\n"
+                + "  <backlight>0</backlight>\n"
+                + "  <backlight>0.0001</backlight>\n"
+                + "  <backlight>0.5</backlight>\n"
+                + "  <backlight>1.0</backlight>\n"
+                + "  <brightness>0</brightness>\n"
+                + "  <brightness>0.1</brightness>\n"
+                + "  <brightness>0.5</brightness>\n"
+                + "  <brightness>1.0</brightness>\n"
+                + "</lowBrightness>";
+    }
+
     private void mockDeviceConfigs() {
         when(mResources.getFloat(com.android.internal.R.dimen
                 .config_screenBrightnessSettingDefaultFloat)).thenReturn(0.5f);
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
index 01598ae..740ffc9 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -1184,7 +1184,8 @@
                 /* ambientLightHorizonShort= */ anyInt(),
                 /* ambientLightHorizonLong= */ anyInt(),
                 eq(lux),
-                eq(nits)
+                eq(nits),
+                any(BrightnessClamperController.class)
         );
     }
 
@@ -2121,7 +2122,8 @@
                 HysteresisLevels screenBrightnessThresholdsIdle, Context context,
                 BrightnessRangeController brightnessRangeController,
                 BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
-                int ambientLightHorizonLong, float userLux, float userNits) {
+                int ambientLightHorizonLong, float userLux, float userNits,
+                BrightnessClamperController brightnessClamperController) {
             return mAutomaticBrightnessController;
         }
 
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt
index ac7d1f5..e4a7d98 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt
@@ -65,7 +65,7 @@
         Settings.Secure.putIntForUser(context.contentResolver,
                 Settings.Secure.EVEN_DIMMER_ACTIVATED, 1, userId)
         Settings.Secure.putFloatForUser(context.contentResolver,
-                Settings.Secure.EVEN_DIMMER_MIN_NITS, 0.7f, userId)
+                Settings.Secure.EVEN_DIMMER_MIN_NITS, 30.0f, userId)
         modifier.recalculateLowerBound()
         testHandler.flush()
         assertThat(modifier.isActive).isTrue()
@@ -81,11 +81,22 @@
                 Settings.Secure.EVEN_DIMMER_ACTIVATED, 1, userId)
         Settings.Secure.putFloatForUser(context.contentResolver,
                 Settings.Secure.EVEN_DIMMER_MIN_NITS, 0.0f, userId)
-        modifier.recalculateLowerBound()
+        modifier.onAmbientLuxChange(3000.0f)
         testHandler.flush()
         assertThat(modifier.isActive).isTrue()
 
         // Test restriction from lux setting
         assertThat(modifier.brightnessReason).isEqualTo(BrightnessReason.MODIFIER_MIN_LUX)
     }
+
+    @Test
+    fun testSettingOffDisablesModifier() {
+        Settings.Secure.putIntForUser(context.contentResolver,
+            Settings.Secure.EVEN_DIMMER_ACTIVATED, 0, userId)
+        assertThat(modifier.brightnessLowerBound).isEqualTo(PowerManager.BRIGHTNESS_MIN)
+        modifier.onAmbientLuxChange(3000.0f)
+        testHandler.flush()
+        assertThat(modifier.isActive).isFalse()
+        assertThat(modifier.brightnessLowerBound).isEqualTo(PowerManager.BRIGHTNESS_MIN)
+    }
 }
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index 37967fa..65986ea 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -62,6 +62,7 @@
         "cts-wm-util",
         "platform-compat-test-rules",
         "mockito-target-minus-junit4",
+        "mockito-kotlin2",
         "platform-test-annotations",
         "ShortcutManagerTestUtils",
         "truth",
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 b2ecea1..53c460c 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
@@ -50,9 +50,11 @@
 import android.accessibilityservice.IAccessibilityServiceClient;
 import android.app.PendingIntent;
 import android.app.RemoteAction;
+import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
@@ -67,6 +69,7 @@
 import android.os.IBinder;
 import android.os.LocaleList;
 import android.os.UserHandle;
+import android.platform.test.annotations.RequiresFlagsDisabled;
 import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.platform.test.flag.junit.CheckFlagsRule;
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
@@ -74,6 +77,7 @@
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableContext;
 import android.testing.TestableLooper;
+import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.view.Display;
 import android.view.DisplayAdjustments;
@@ -123,6 +127,7 @@
 
 import java.util.ArrayList;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -1464,6 +1469,52 @@
                 AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME.flattenToString());
     }
 
+    @Test
+    @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT)
+    public void restoreAccessibilityQsTargets_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));
+        mA11yms.mUserStates.put(UserHandle.USER_SYSTEM, userState);
+
+        Intent intent = new Intent(Intent.ACTION_SETTING_RESTORED)
+                .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY)
+                .putExtra(Intent.EXTRA_SETTING_NAME, Settings.Secure.ACCESSIBILITY_QS_TARGETS)
+                .putExtra(Intent.EXTRA_SETTING_NEW_VALUE, colorInversionTile);
+        sendBroadcastToAccessibilityManagerService(intent);
+        mTestableLooper.processAllMessages();
+
+        assertThat(mA11yms.mUserStates.get(UserHandle.USER_SYSTEM).getA11yQsTargets())
+                .containsExactlyElementsIn(Set.of(daltonizerTile, colorInversionTile));
+    }
+
+    @Test
+    @RequiresFlagsDisabled(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT)
+    public void restoreAccessibilityQsTargets_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));
+        mA11yms.mUserStates.put(UserHandle.USER_SYSTEM, userState);
+
+        Intent intent = new Intent(Intent.ACTION_SETTING_RESTORED)
+                .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY)
+                .putExtra(Intent.EXTRA_SETTING_NAME, Settings.Secure.ACCESSIBILITY_QS_TARGETS)
+                .putExtra(Intent.EXTRA_SETTING_NEW_VALUE, colorInversionTile);
+        sendBroadcastToAccessibilityManagerService(intent);
+        mTestableLooper.processAllMessages();
+
+        assertThat(userState.getA11yQsTargets())
+                .containsExactlyElementsIn(Set.of(daltonizerTile));
+    }
+
     private static AccessibilityServiceInfo mockAccessibilityServiceInfo(
             ComponentName componentName) {
         return mockAccessibilityServiceInfo(
@@ -1542,6 +1593,14 @@
         mA11yms.getCurrentUserState().updateTileServiceMapForAccessibilityServiceLocked();
     }
 
+    private void sendBroadcastToAccessibilityManagerService(Intent intent) {
+        if (!mTestableContext.getBroadcastReceivers().containsKey(intent.getAction())) {
+            return;
+        }
+        mTestableContext.getBroadcastReceivers().get(intent.getAction()).forEach(
+                broadcastReceiver -> broadcastReceiver.onReceive(mTestableContext, intent));
+    }
+
     public static class FakeInputFilter extends AccessibilityInputFilter {
         FakeInputFilter(Context context,
                 AccessibilityManagerService service) {
@@ -1552,6 +1611,7 @@
     private static class A11yTestableContext extends TestableContext {
 
         private final Context mMockContext;
+        private final Map<String, List<BroadcastReceiver>> mBroadcastReceivers = new ArrayMap<>();
 
         A11yTestableContext(Context base) {
             super(base);
@@ -1563,8 +1623,29 @@
             mMockContext.startActivityAsUser(intent, options, user);
         }
 
+        @Override
+        public Intent registerReceiverAsUser(BroadcastReceiver receiver, UserHandle user,
+                IntentFilter filter, String broadcastPermission, Handler scheduler) {
+            Iterator<String> actions = filter.actionsIterator();
+            if (actions != null) {
+                while (actions.hasNext()) {
+                    String action = actions.next();
+                    List<BroadcastReceiver> actionReceivers =
+                            mBroadcastReceivers.getOrDefault(action, new ArrayList<>());
+                    actionReceivers.add(receiver);
+                    mBroadcastReceivers.put(action, actionReceivers);
+                }
+            }
+            return super.registerReceiverAsUser(
+                    receiver, user, filter, broadcastPermission, scheduler);
+        }
+
         Context getMockContext() {
             return mMockContext;
         }
+
+        Map<String, List<BroadcastReceiver>> getBroadcastReceivers() {
+            return mBroadcastReceivers;
+        }
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/OverlayPackagesProviderTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/OverlayPackagesProviderTest.java
index 4f6fc3d..0a696ef 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/OverlayPackagesProviderTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/OverlayPackagesProviderTest.java
@@ -47,7 +47,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.test.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import com.android.internal.R;
 
@@ -67,9 +67,7 @@
 
 /**
  * Run this test with:
- *
  * {@code atest FrameworksServicesTests:com.android.server.devicepolicy.OwnersTest}
- *
  */
 @RunWith(AndroidJUnit4.class)
 public class OverlayPackagesProviderTest {
@@ -87,8 +85,8 @@
 
     private FakePackageManager mPackageManager;
     private String[] mSystemAppsWithLauncher;
-    private Set<String> mRegularMainlineModules = new HashSet<>();
-    private Map<String, String> mMainlineModuleToDeclaredMetadataMap = new HashMap<>();
+    private final Set<String> mRegularMainlineModules = new HashSet<>();
+    private final Map<String, String> mMainlineModuleToDeclaredMetadataMap = new HashMap<>();
     private OverlayPackagesProvider mHelper;
 
     @Before
@@ -115,7 +113,8 @@
         setVendorDisallowedAppsManagedUser();
 
         mRealResources = InstrumentationRegistry.getTargetContext().getResources();
-        mHelper = new OverlayPackagesProvider(mTestContext, mInjector);
+        mHelper = new OverlayPackagesProvider(mTestContext, mInjector,
+                new RecursiveStringArrayResourceResolver(mResources));
     }
 
     @Test
@@ -213,7 +212,7 @@
     }
 
     /**
-     * @see {@link #testAllowedAndDisallowedAtTheSameTimeManagedDevice}
+     * @see #testAllowedAndDisallowedAtTheSameTimeManagedDevice
      */
     @Test
     public void testAllowedAndDisallowedAtTheSameTimeManagedUser() {
@@ -224,7 +223,7 @@
     }
 
     /**
-     * @see {@link #testAllowedAndDisallowedAtTheSameTimeManagedDevice}
+     * @see #testAllowedAndDisallowedAtTheSameTimeManagedDevice
      */
     @Test
     public void testAllowedAndDisallowedAtTheSameTimeManagedProfile() {
@@ -447,7 +446,7 @@
     }
 
     private void setSystemInputMethods(String... packageNames) {
-        List<InputMethodInfo> inputMethods = new ArrayList<InputMethodInfo>();
+        List<InputMethodInfo> inputMethods = new ArrayList<>();
         for (String packageName : packageNames) {
             ApplicationInfo aInfo = new ApplicationInfo();
             aInfo.flags = ApplicationInfo.FLAG_SYSTEM;
@@ -467,6 +466,7 @@
         mSystemAppsWithLauncher = apps;
     }
 
+    @SafeVarargs
     private <T> Set<T> setFromArray(T... array) {
         if (array == null) {
             return null;
@@ -475,6 +475,7 @@
     }
 
     class FakePackageManager extends MockPackageManager {
+        @NonNull
         @Override
         public List<ResolveInfo> queryIntentActivitiesAsUser(Intent intent, int flags, int userId) {
             assertWithMessage("Expected an intent with action ACTION_MAIN")
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/RecursiveStringArrayResourceResolverTest.kt b/services/tests/servicestests/src/com/android/server/devicepolicy/RecursiveStringArrayResourceResolverTest.kt
new file mode 100644
index 0000000..647f6c7
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/RecursiveStringArrayResourceResolverTest.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.devicepolicy
+
+import android.annotation.ArrayRes
+import android.content.res.Resources
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertWithMessage
+import com.google.errorprone.annotations.CanIgnoreReturnValue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+
+/**
+ * Run this test with:
+ * `atest FrameworksServicesTests:com.android.server.devicepolicy.RecursiveStringArrayResourceResolverTest`
+ */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class RecursiveStringArrayResourceResolverTest {
+    private companion object {
+        const val PACKAGE = "com.android.test"
+        const val ROOT_RESOURCE = "my_root_resource"
+        const val SUB_RESOURCE = "my_sub_resource"
+        const val EXTERNAL_PACKAGE = "com.external.test"
+        const val EXTERNAL_RESOURCE = "my_external_resource"
+    }
+
+    private val mResources = mock<Resources>()
+    private val mTarget = RecursiveStringArrayResourceResolver(mResources)
+
+    /**
+     * Mocks [Resources.getIdentifier] and [Resources.getStringArray] to return [values] and reference under a generated ID.
+     * @receiver mocked [Resources] container to configure
+     * @param pkg package name to "contain" mocked resource
+     * @param name mocked resource name
+     * @param values string-array resource values to return when mock is queried
+     * @return generated resource ID
+     */
+    @ArrayRes
+    @CanIgnoreReturnValue
+    private fun Resources.mockStringArrayResource(pkg: String, name: String, vararg values: String): Int {
+        val anId = (pkg + name).hashCode()
+        println("Mocking Resources::getIdentifier(name=\"$name\", defType=\"array\", defPackage=\"$pkg\") -> $anId")
+        whenever(getIdentifier(eq(name), eq("array"), eq(pkg))).thenReturn(anId)
+        println("Mocking Resources::getStringArray(id=$anId) -> ${values.asList()}")
+        whenever(getStringArray(eq(anId))).thenReturn(values)
+        return anId
+    }
+
+    @Test
+    fun testCanResolveTheArrayWithoutImports() {
+        val values = arrayOf("app.a", "app.b")
+        val mockId = mResources.mockStringArrayResource(pkg = PACKAGE, name = ROOT_RESOURCE, values = values)
+
+        val actual = mTarget.resolve(/* pkg= */ PACKAGE, /* rootId = */ mockId)
+
+        assertWithMessage("Values are resolved correctly")
+                .that(actual).containsExactlyElementsIn(values)
+    }
+
+    @Test
+    fun testCanResolveTheArrayWithImports() {
+        val externalValues = arrayOf("ext.a", "ext.b", "#import:$PACKAGE/$SUB_RESOURCE")
+        mResources.mockStringArrayResource(pkg = EXTERNAL_PACKAGE, name = EXTERNAL_RESOURCE, values = externalValues)
+        val subValues = arrayOf("sub.a", "sub.b")
+        mResources.mockStringArrayResource(pkg = PACKAGE, name = SUB_RESOURCE, values = subValues)
+        val values = arrayOf("app.a", "#import:./$SUB_RESOURCE", "app.b", "#import:$EXTERNAL_PACKAGE/$EXTERNAL_RESOURCE", "app.c")
+        val mockId = mResources.mockStringArrayResource(pkg = PACKAGE, name = ROOT_RESOURCE, values = values)
+
+        val actual = mTarget.resolve(/* pkg= */ PACKAGE, /* rootId= */ mockId)
+
+        assertWithMessage("Values are resolved correctly")
+                .that(actual).containsExactlyElementsIn((externalValues + subValues + values)
+                        .filterNot { it.startsWith("#import:") }
+                        .toSet())
+    }
+}
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
index 8cbcc22..5861d88 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
@@ -500,7 +500,8 @@
         InOrder batteryVerifier = inOrder(mBatteryStatsMock);
         batteryVerifier.verify(mBatteryStatsMock)
                 .noteVibratorOn(UID, oneShotDuration + mVibrationConfig.getRampDownDurationMs());
-        batteryVerifier.verify(mBatteryStatsMock).noteVibratorOff(UID);
+        batteryVerifier
+                .verify(mBatteryStatsMock, timeout(TEST_TIMEOUT_MILLIS)).noteVibratorOff(UID);
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java b/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java
index 0a29dfb..60716cb 100644
--- a/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java
@@ -95,8 +95,6 @@
                         new int[]{KeyEvent.KEYCODE_NOTIFICATION},
                         KeyboardLogEvent.TOGGLE_NOTIFICATION_PANEL, KeyEvent.KEYCODE_NOTIFICATION,
                         0},
-                {"Meta + T -> Toggle Taskbar", new int[]{META_KEY, KeyEvent.KEYCODE_T},
-                        KeyboardLogEvent.TOGGLE_TASKBAR, KeyEvent.KEYCODE_T, META_ON},
                 {"Meta + Ctrl + S -> Take Screenshot",
                         new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_S},
                         KeyboardLogEvent.TAKE_SCREENSHOT, KeyEvent.KEYCODE_S, META_ON | CTRL_ON},
diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
index c29547f..b9e87dc 100644
--- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
@@ -633,18 +633,23 @@
     @Test
     public void testAdjacentFocusInActivityEmbedding() {
         mSetFlagsRule.enableFlags(Flags.FLAG_EMBEDDED_ACTIVITY_BACK_NAV_FLAG);
-        Task task = createTask(mDefaultDisplay);
-        TaskFragment primary = createTaskFragmentWithActivity(task);
-        TaskFragment secondary = createTaskFragmentWithActivity(task);
-        primary.setAdjacentTaskFragment(secondary);
-        secondary.setAdjacentTaskFragment(primary);
+        final Task task = createTask(mDefaultDisplay);
+        final TaskFragment primaryTf = createTaskFragmentWithActivity(task);
+        final TaskFragment secondaryTf = createTaskFragmentWithActivity(task);
+        final ActivityRecord primaryActivity = primaryTf.getTopMostActivity();
+        final ActivityRecord secondaryActivity = secondaryTf.getTopMostActivity();
+        primaryTf.setAdjacentTaskFragment(secondaryTf);
+        secondaryTf.setAdjacentTaskFragment(primaryTf);
 
-        WindowState windowState = mock(WindowState.class);
+        final WindowState windowState = mock(WindowState.class);
+        windowState.mActivityRecord = primaryActivity;
         doReturn(windowState).when(mWm).getFocusedWindowLocked();
-        doReturn(primary).when(windowState).getTaskFragment();
+        doReturn(primaryTf).when(windowState).getTaskFragment();
+        doReturn(1L).when(primaryActivity).getLastWindowCreateTime();
+        doReturn(2L).when(secondaryActivity).getLastWindowCreateTime();
 
         startBackNavigation();
-        verify(mWm).moveFocusToActivity(any());
+        verify(mWm).moveFocusToActivity(eq(secondaryActivity));
     }
 
     /**
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
index 5360a10..6b1bf26bf 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
@@ -887,20 +887,14 @@
         assertEquals(winLeftTop, mDisplayContent.mCurrentFocus);
 
         if (Flags.embeddedActivityBackNavFlag()) {
-            // Send request to move the focus to top window from the left window.
-            assertTrue(mWm.moveFocusToTopEmbeddedWindow(winLeftTop));
-            // The focus should change.
-            assertEquals(winRightTop, mDisplayContent.mCurrentFocus);
+            // Move focus if the adjacent activity is more recently active.
+            doReturn(1L).when(appLeftTop).getLastWindowCreateTime();
+            doReturn(2L).when(appRightTop).getLastWindowCreateTime();
+            assertTrue(mWm.moveFocusToAdjacentEmbeddedWindow(winLeftTop));
 
-            // Send request to move the focus to top window from the right window.
-            assertFalse(mWm.moveFocusToTopEmbeddedWindow(winRightTop));
-            // The focus should NOT change.
-            assertEquals(winRightTop, mDisplayContent.mCurrentFocus);
-
-            // Do not move focus if the dim is boosted.
-            taskFragmentLeft.mDimmerSurfaceBoosted = true;
-            assertFalse(mWm.moveFocusToTopEmbeddedWindow(winLeftTop));
-            assertEquals(winRightTop, mDisplayContent.mCurrentFocus);
+            // Do not move the focus if the adjacent activity is less recently active.
+            doReturn(3L).when(appLeftTop).getLastWindowCreateTime();
+            assertFalse(mWm.moveFocusToAdjacentEmbeddedWindow(winLeftTop));
         }
     }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
index 3bd6496..a88680a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
@@ -1945,6 +1945,21 @@
         assertEquals(2, finishCount[0]);
     }
 
+    @Test
+    public void testPauseActivityWhenHasEmptyLeafTaskFragment() {
+        // Creating a task that has a RESUMED activity and an empty TaskFragment.
+        final Task task = new TaskBuilder(mSupervisor).setCreateActivity(true).build();
+        final ActivityRecord activity = task.getTopMostActivity();
+        new TaskFragmentBuilder(mAtm).setParentTask(task).build();
+        activity.setState(ActivityRecord.State.RESUMED, "test");
+
+        // Ensure the activity is paused if cannot be resumed.
+        doReturn(false).when(task).canBeResumed(any());
+        mSupervisor.mUserLeaving = true;
+        task.pauseActivityIfNeeded(null /* resuming */, "test");
+        verify(task).startPausing(eq(true) /* userLeaving */, anyBoolean(), any(), any());
+    }
+
     private Task getTestTask() {
         return new TaskBuilder(mSupervisor).setCreateActivity(true).build();
     }
diff --git a/test-base/Android.bp b/test-base/Android.bp
index 70a9540..d65a4e4 100644
--- a/test-base/Android.bp
+++ b/test-base/Android.bp
@@ -14,37 +14,22 @@
 // limitations under the License.
 //
 
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
 // Build the android.test.base library
 // ===================================
 // This contains the junit.framework and android.test classes that were in
 // Android API level 25 excluding those from android.test.runner.
 // Also contains the com.android.internal.util.Predicate[s] classes.
-package {
-    // See: http://go/android-license-faq
-    // A large-scale-change added 'default_applicable_licenses' to import
-    // all of the 'license_kinds' from "frameworks_base_license"
-    // to get the below license kinds:
-    //   SPDX-license-identifier-Apache-2.0
-    //   SPDX-license-identifier-CPL-1.0
-    default_applicable_licenses: ["frameworks_base_test-base_license"],
-}
-
-license {
-    name: "frameworks_base_test-base_license",
-    visibility: [":__subpackages__"],
-    license_kinds: [
-        "SPDX-license-identifier-Apache-2.0",
-        "SPDX-license-identifier-CPL-1.0",
-    ],
-    license_text: [
-        "src/junit/cpl-v10.html",
-    ],
-}
-
 java_sdk_library {
     name: "android.test.base",
 
-    srcs: [":android-test-base-sources"],
+    srcs: [
+        ":android-test-base-sources",
+        ":frameworks-base-test-junit-framework",
+    ],
 
     errorprone: {
         javacflags: ["-Xep:DepAnn:ERROR"],
@@ -84,7 +69,10 @@
     ],
     installable: false,
 
-    srcs: [":android-test-base-sources"],
+    srcs: [
+        ":android-test-base-sources",
+        ":frameworks-base-test-junit-framework",
+    ],
 
     errorprone: {
         javacflags: ["-Xep:DepAnn:ERROR"],
@@ -104,8 +92,7 @@
     name: "android.test.base-minus-junit",
 
     srcs: [
-        "src/android/**/*.java",
-        "src/com/**/*.java",
+        "src/**/*.java",
     ],
 
     sdk_version: "current",
diff --git a/test-base/hiddenapi/Android.bp b/test-base/hiddenapi/Android.bp
index 1466590..4c59b10 100644
--- a/test-base/hiddenapi/Android.bp
+++ b/test-base/hiddenapi/Android.bp
@@ -15,12 +15,7 @@
 //
 
 package {
-    // See: http://go/android-license-faq
-    // A large-scale-change added 'default_applicable_licenses' to import
-    // all of the 'license_kinds' from "frameworks_base_license"
-    // to get the below license kinds:
-    //   SPDX-license-identifier-Apache-2.0
-    default_applicable_licenses: ["frameworks_base_license"],
+    default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
 // Provided solely to contribute information about which hidden parts of the android.test.base
diff --git a/test-junit/Android.bp b/test-junit/Android.bp
new file mode 100644
index 0000000..8d3d439
--- /dev/null
+++ b/test-junit/Android.bp
@@ -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 {
+    default_applicable_licenses: ["frameworks-base-test-junit-license"],
+}
+
+license {
+    name: "frameworks-base-test-junit-license",
+    visibility: [":__subpackages__"],
+    license_kinds: [
+        "SPDX-license-identifier-CPL-1.0",
+    ],
+    license_text: [
+        "src/junit/cpl-v10.html",
+    ],
+}
+
+filegroup {
+    name: "frameworks-base-test-junit-framework",
+    srcs: [
+        "src/junit/framework/**/*.java",
+    ],
+    path: "src",
+    visibility: [
+        "//frameworks/base/test-base",
+    ],
+}
+
+filegroup {
+    name: "frameworks-base-test-junit-runner",
+    srcs: [
+        "src/junit/runner/**/*.java",
+        "src/junit/textui/**/*.java",
+    ],
+    path: "src",
+    visibility: [
+        "//frameworks/base/test-runner",
+    ],
+}
diff --git a/test-base/src/junit/MODULE_LICENSE_CPL b/test-junit/src/junit/MODULE_LICENSE_CPL
similarity index 100%
rename from test-base/src/junit/MODULE_LICENSE_CPL
rename to test-junit/src/junit/MODULE_LICENSE_CPL
diff --git a/test-base/src/junit/README.android b/test-junit/src/junit/README.android
similarity index 100%
rename from test-base/src/junit/README.android
rename to test-junit/src/junit/README.android
diff --git a/test-base/src/junit/cpl-v10.html b/test-junit/src/junit/cpl-v10.html
similarity index 100%
rename from test-base/src/junit/cpl-v10.html
rename to test-junit/src/junit/cpl-v10.html
diff --git a/test-base/src/junit/framework/Assert.java b/test-junit/src/junit/framework/Assert.java
similarity index 100%
rename from test-base/src/junit/framework/Assert.java
rename to test-junit/src/junit/framework/Assert.java
diff --git a/test-base/src/junit/framework/AssertionFailedError.java b/test-junit/src/junit/framework/AssertionFailedError.java
similarity index 100%
rename from test-base/src/junit/framework/AssertionFailedError.java
rename to test-junit/src/junit/framework/AssertionFailedError.java
diff --git a/test-base/src/junit/framework/ComparisonCompactor.java b/test-junit/src/junit/framework/ComparisonCompactor.java
similarity index 100%
rename from test-base/src/junit/framework/ComparisonCompactor.java
rename to test-junit/src/junit/framework/ComparisonCompactor.java
diff --git a/test-base/src/junit/framework/ComparisonFailure.java b/test-junit/src/junit/framework/ComparisonFailure.java
similarity index 100%
rename from test-base/src/junit/framework/ComparisonFailure.java
rename to test-junit/src/junit/framework/ComparisonFailure.java
diff --git a/test-base/src/junit/framework/Protectable.java b/test-junit/src/junit/framework/Protectable.java
similarity index 100%
rename from test-base/src/junit/framework/Protectable.java
rename to test-junit/src/junit/framework/Protectable.java
diff --git a/test-base/src/junit/framework/Test.java b/test-junit/src/junit/framework/Test.java
similarity index 100%
rename from test-base/src/junit/framework/Test.java
rename to test-junit/src/junit/framework/Test.java
diff --git a/test-base/src/junit/framework/TestCase.java b/test-junit/src/junit/framework/TestCase.java
similarity index 100%
rename from test-base/src/junit/framework/TestCase.java
rename to test-junit/src/junit/framework/TestCase.java
diff --git a/test-base/src/junit/framework/TestFailure.java b/test-junit/src/junit/framework/TestFailure.java
similarity index 100%
rename from test-base/src/junit/framework/TestFailure.java
rename to test-junit/src/junit/framework/TestFailure.java
diff --git a/test-base/src/junit/framework/TestListener.java b/test-junit/src/junit/framework/TestListener.java
similarity index 100%
rename from test-base/src/junit/framework/TestListener.java
rename to test-junit/src/junit/framework/TestListener.java
diff --git a/test-base/src/junit/framework/TestResult.java b/test-junit/src/junit/framework/TestResult.java
similarity index 100%
rename from test-base/src/junit/framework/TestResult.java
rename to test-junit/src/junit/framework/TestResult.java
diff --git a/test-base/src/junit/framework/TestSuite.java b/test-junit/src/junit/framework/TestSuite.java
similarity index 100%
rename from test-base/src/junit/framework/TestSuite.java
rename to test-junit/src/junit/framework/TestSuite.java
diff --git a/test-runner/src/junit/runner/BaseTestRunner.java b/test-junit/src/junit/runner/BaseTestRunner.java
similarity index 100%
rename from test-runner/src/junit/runner/BaseTestRunner.java
rename to test-junit/src/junit/runner/BaseTestRunner.java
diff --git a/test-runner/src/junit/runner/StandardTestSuiteLoader.java b/test-junit/src/junit/runner/StandardTestSuiteLoader.java
similarity index 100%
rename from test-runner/src/junit/runner/StandardTestSuiteLoader.java
rename to test-junit/src/junit/runner/StandardTestSuiteLoader.java
diff --git a/test-runner/src/junit/runner/TestRunListener.java b/test-junit/src/junit/runner/TestRunListener.java
similarity index 100%
rename from test-runner/src/junit/runner/TestRunListener.java
rename to test-junit/src/junit/runner/TestRunListener.java
diff --git a/test-runner/src/junit/runner/TestSuiteLoader.java b/test-junit/src/junit/runner/TestSuiteLoader.java
similarity index 100%
rename from test-runner/src/junit/runner/TestSuiteLoader.java
rename to test-junit/src/junit/runner/TestSuiteLoader.java
diff --git a/test-runner/src/junit/runner/Version.java b/test-junit/src/junit/runner/Version.java
similarity index 100%
rename from test-runner/src/junit/runner/Version.java
rename to test-junit/src/junit/runner/Version.java
diff --git a/test-runner/src/junit/runner/package-info.java b/test-junit/src/junit/runner/package-info.java
similarity index 100%
rename from test-runner/src/junit/runner/package-info.java
rename to test-junit/src/junit/runner/package-info.java
diff --git a/test-runner/src/junit/textui/ResultPrinter.java b/test-junit/src/junit/textui/ResultPrinter.java
similarity index 100%
rename from test-runner/src/junit/textui/ResultPrinter.java
rename to test-junit/src/junit/textui/ResultPrinter.java
diff --git a/test-runner/src/junit/textui/TestRunner.java b/test-junit/src/junit/textui/TestRunner.java
similarity index 100%
rename from test-runner/src/junit/textui/TestRunner.java
rename to test-junit/src/junit/textui/TestRunner.java
diff --git a/test-runner/src/junit/textui/package-info.java b/test-junit/src/junit/textui/package-info.java
similarity index 100%
rename from test-runner/src/junit/textui/package-info.java
rename to test-junit/src/junit/textui/package-info.java
diff --git a/test-mock/Android.bp b/test-mock/Android.bp
index f37d2d1..e29d321 100644
--- a/test-mock/Android.bp
+++ b/test-mock/Android.bp
@@ -17,12 +17,7 @@
 // Build the android.test.mock library
 // ===================================
 package {
-    // See: http://go/android-license-faq
-    // A large-scale-change added 'default_applicable_licenses' to import
-    // all of the 'license_kinds' from "frameworks_base_license"
-    // to get the below license kinds:
-    //   SPDX-license-identifier-Apache-2.0
-    default_applicable_licenses: ["frameworks_base_license"],
+    default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
 java_sdk_library {
diff --git a/test-runner/Android.bp b/test-runner/Android.bp
index 21e09d3..6b5be3cb 100644
--- a/test-runner/Android.bp
+++ b/test-runner/Android.bp
@@ -14,29 +14,19 @@
 // limitations under the License.
 //
 
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
 // Build the android.test.runner library
 // =====================================
-package {
-    // See: http://go/android-license-faq
-    default_applicable_licenses: ["frameworks_base_test-runner_license"],
-}
-
-license {
-    name: "frameworks_base_test-runner_license",
-    visibility: [":__subpackages__"],
-    license_kinds: [
-        "SPDX-license-identifier-Apache-2.0",
-        "SPDX-license-identifier-CPL-1.0",
-    ],
-    license_text: [
-        "src/junit/cpl-v10.html",
-    ],
-}
-
 java_sdk_library {
     name: "android.test.runner",
 
-    srcs: [":android-test-runner-sources"],
+    srcs: [
+        ":android-test-runner-sources",
+        ":frameworks-base-test-junit-runner",
+    ],
 
     errorprone: {
         javacflags: ["-Xep:DepAnn:ERROR"],
diff --git a/test-runner/src/junit/MODULE_LICENSE_CPL b/test-runner/src/junit/MODULE_LICENSE_CPL
deleted file mode 100644
index e69de29..0000000
--- a/test-runner/src/junit/MODULE_LICENSE_CPL
+++ /dev/null
diff --git a/test-runner/src/junit/README.android b/test-runner/src/junit/README.android
deleted file mode 100644
index 1384a1f..0000000
--- a/test-runner/src/junit/README.android
+++ /dev/null
@@ -1,11 +0,0 @@
-URL: https://github.com/junit-team/junit4
-License: Common Public License Version 1.0
-License File: cpl-v10.html
-
-This is JUnit 4.10 source that was previously part of the Android Public API.
-Where necessary it has been patched to be compatible (according to Android API
-requirements) with JUnit 3.8.
-
-These are copied here to ensure that the android.test.runner target remains
-compatible with the last version of the Android API (25) that contained these
-classes even when external/junit is upgraded to a later version.
diff --git a/test-runner/src/junit/cpl-v10.html b/test-runner/src/junit/cpl-v10.html
deleted file mode 100644
index 36aa208..0000000
--- a/test-runner/src/junit/cpl-v10.html
+++ /dev/null
@@ -1,125 +0,0 @@
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN">
-<HTML>
-<HEAD>
-<TITLE>Common Public License - v 1.0</TITLE>
-<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
-</HEAD>
-
-<BODY BGCOLOR="#FFFFFF" VLINK="#800000">
-
-
-<P ALIGN="CENTER"><B>Common Public License - v 1.0</B>
-<P><B></B><FONT SIZE="3"></FONT>
-<P><FONT SIZE="3"></FONT><FONT SIZE="2">THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS COMMON PUBLIC LICENSE ("AGREEMENT").  ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.</FONT>
-<P><FONT SIZE="2"></FONT>
-<P><FONT SIZE="2"><B>1.  DEFINITIONS</B></FONT>
-<P><FONT SIZE="2">"Contribution" means:</FONT>
-
-<UL><FONT SIZE="2">a) in the case of the initial Contributor, the initial code and documentation distributed under this Agreement, and<BR CLEAR="LEFT">
-b) in the case of each subsequent Contributor:</FONT></UL>
-
-
-<UL><FONT SIZE="2">i)	 	changes to the Program, and</FONT></UL>
-
-
-<UL><FONT SIZE="2">ii)		additions to the Program;</FONT></UL>
-
-
-<UL><FONT SIZE="2">where such changes and/or additions to the Program originate from and are distributed by that particular Contributor.  </FONT><FONT SIZE="2">A Contribution 'originates' from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf.  </FONT><FONT SIZE="2">Contributions do not include additions to the Program which:  (i) are separate modules of software distributed in conjunction with the Program under their own license agreement, and (ii) are not derivative works of the Program.  </FONT></UL>
-
-<P><FONT SIZE="2"></FONT>
-<P><FONT SIZE="2">"Contributor" means any person or entity that distributes the Program.</FONT>
-<P><FONT SIZE="2"></FONT><FONT SIZE="2"></FONT>
-<P><FONT SIZE="2">"Licensed Patents " mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program.  </FONT>
-<P><FONT SIZE="2"></FONT><FONT SIZE="2"></FONT>
-<P><FONT SIZE="2"></FONT><FONT SIZE="2">"Program" means the Contributions distributed in accordance with this Agreement.</FONT>
-<P><FONT SIZE="2"></FONT>
-<P><FONT SIZE="2">"Recipient" means anyone who receives the Program under this Agreement, including all Contributors.</FONT>
-<P><FONT SIZE="2"><B></B></FONT>
-<P><FONT SIZE="2"><B>2.  GRANT OF RIGHTS</B></FONT>
-
-<UL><FONT SIZE="2"></FONT><FONT SIZE="2">a)	</FONT><FONT SIZE="2">Subject to the terms of this Agreement, each Contributor hereby grants</FONT><FONT SIZE="2"> Recipient a non-exclusive, worldwide, royalty-free copyright license to</FONT><FONT SIZE="2" COLOR="#FF0000"> </FONT><FONT SIZE="2">reproduce, prepare derivative works of, publicly display, publicly perform, distribute and sublicense the Contribution of such Contributor, if any, and such derivative works, in source code and object code form.</FONT></UL>
-
-
-<UL><FONT SIZE="2"></FONT></UL>
-
-
-<UL><FONT SIZE="2"></FONT><FONT SIZE="2">b) 	Subject to the terms of this Agreement, each Contributor hereby grants </FONT><FONT SIZE="2">Recipient a non-exclusive, worldwide,</FONT><FONT SIZE="2" COLOR="#008000"> </FONT><FONT SIZE="2">royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in source code and object code form.  This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents.  The patent license shall not apply to any other combinations which include the Contribution.  No hardware per se is licensed hereunder.   </FONT></UL>
-
-
-<UL><FONT SIZE="2"></FONT></UL>
-
-
-<UL><FONT SIZE="2">c)	Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity.  Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise.  As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any.  For example, if a third party patent license is required to allow Recipient to distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program.</FONT></UL>
-
-
-<UL><FONT SIZE="2"></FONT></UL>
-
-
-<UL><FONT SIZE="2">d)	Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. </FONT></UL>
-
-
-<UL><FONT SIZE="2"></FONT></UL>
-
-<P><FONT SIZE="2"><B>3.  REQUIREMENTS</B></FONT>
-<P><FONT SIZE="2"><B></B>A Contributor may choose to distribute the Program in object code form under its own license agreement, provided that:</FONT>
-
-<UL><FONT SIZE="2">a)	it complies with the terms and conditions of this Agreement; and</FONT></UL>
-
-
-<UL><FONT SIZE="2">b)	its license agreement:</FONT></UL>
-
-
-<UL><FONT SIZE="2">i)	effectively disclaims</FONT><FONT SIZE="2"> on behalf of all Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose; </FONT></UL>
-
-
-<UL><FONT SIZE="2">ii) 	effectively excludes on behalf of all Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits; </FONT></UL>
-
-
-<UL><FONT SIZE="2">iii)</FONT><FONT SIZE="2">	states that any provisions which differ from this Agreement are offered by that Contributor alone and not by any other party; and</FONT></UL>
-
-
-<UL><FONT SIZE="2">iv)	states that source code for the Program is available from such Contributor, and informs licensees how to obtain it in a reasonable manner on or through a medium customarily used for software exchange.</FONT><FONT SIZE="2" COLOR="#0000FF"> </FONT><FONT SIZE="2" COLOR="#FF0000"></FONT></UL>
-
-
-<UL><FONT SIZE="2" COLOR="#FF0000"></FONT><FONT SIZE="2"></FONT></UL>
-
-<P><FONT SIZE="2">When the Program is made available in source code form:</FONT>
-
-<UL><FONT SIZE="2">a)	it must be made available under this Agreement; and </FONT></UL>
-
-
-<UL><FONT SIZE="2">b)	a copy of this Agreement must be included with each copy of the Program.  </FONT></UL>
-
-<P><FONT SIZE="2"></FONT><FONT SIZE="2" COLOR="#0000FF"><STRIKE></STRIKE></FONT>
-<P><FONT SIZE="2" COLOR="#0000FF"><STRIKE></STRIKE></FONT><FONT SIZE="2">Contributors may not remove or alter any copyright notices contained within the Program.  </FONT>
-<P><FONT SIZE="2"></FONT>
-<P><FONT SIZE="2">Each Contributor must identify itself as the originator of its Contribution, if any, in a manner that reasonably allows subsequent Recipients to identify the originator of the Contribution.  </FONT>
-<P><FONT SIZE="2"></FONT>
-<P><FONT SIZE="2"><B>4.  COMMERCIAL DISTRIBUTION</B></FONT>
-<P><FONT SIZE="2">Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like.  While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors.   Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor ("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering.  The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement.  In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations.  The Indemnified Contributor may participate in any such claim at its own expense.</FONT>
-<P><FONT SIZE="2"></FONT>
-<P><FONT SIZE="2">For example, a Contributor might include the Program in a commercial product offering, Product X.  That Contributor is then a Commercial Contributor.  If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone.  Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages.</FONT>
-<P><FONT SIZE="2"></FONT><FONT SIZE="2" COLOR="#0000FF"></FONT>
-<P><FONT SIZE="2" COLOR="#0000FF"></FONT><FONT SIZE="2"><B>5.  NO WARRANTY</B></FONT>
-<P><FONT SIZE="2">EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is</FONT><FONT SIZE="2"> solely responsible for determining the appropriateness of using and distributing </FONT><FONT SIZE="2">the Program</FONT><FONT SIZE="2"> and assumes all risks associated with its exercise of rights under this Agreement</FONT><FONT SIZE="2">, including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, </FONT><FONT SIZE="2">programs or equipment, and unavailability or interruption of operations</FONT><FONT SIZE="2">.  </FONT><FONT SIZE="2"></FONT>
-<P><FONT SIZE="2"></FONT>
-<P><FONT SIZE="2"></FONT><FONT SIZE="2"><B>6.  DISCLAIMER OF LIABILITY</B></FONT>
-<P><FONT SIZE="2"></FONT><FONT SIZE="2">EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES </FONT><FONT SIZE="2">(INCLUDING WITHOUT LIMITATION LOST PROFITS),</FONT><FONT SIZE="2"> HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.</FONT>
-<P><FONT SIZE="2"></FONT><FONT SIZE="2"></FONT>
-<P><FONT SIZE="2"><B>7.  GENERAL</B></FONT>
-<P><FONT SIZE="2"></FONT><FONT SIZE="2">If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable.</FONT>
-<P><FONT SIZE="2"></FONT>
-<P><FONT SIZE="2">If Recipient institutes patent litigation against a Contributor with respect to a patent applicable to software (including a cross-claim or counterclaim in a lawsuit), then any patent licenses granted by that Contributor to such Recipient under this Agreement shall terminate as of the date such litigation is filed.  In addition, if Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed. </FONT><FONT SIZE="2"></FONT>
-<P><FONT SIZE="2"></FONT>
-<P><FONT SIZE="2">All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance.  If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable.  However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive.  </FONT><FONT SIZE="2"></FONT>
-<P><FONT SIZE="2"></FONT>
-<P><FONT SIZE="2"></FONT><FONT SIZE="2">Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted  and may only be modified in the following manner. The Agreement Steward reserves the right to </FONT><FONT SIZE="2">publish new versions (including revisions) of this Agreement from time to </FONT><FONT SIZE="2">time. No one other than the Agreement Steward has the right to modify this Agreement. IBM is the initial Agreement Steward.   IBM may assign the responsibility to serve as the Agreement Steward to a suitable separate entity.  </FONT><FONT SIZE="2">Each new version of the Agreement will be given a distinguishing version number.  The Program (including Contributions) may always be distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to distribute the Program (including its Contributions) under the new </FONT><FONT SIZE="2">version.  </FONT><FONT SIZE="2">Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, </FONT><FONT SIZE="2">by implication, estoppel or otherwise</FONT><FONT SIZE="2">.</FONT><FONT SIZE="2">  All rights in the Program not expressly granted under this Agreement are reserved.</FONT>
-<P><FONT SIZE="2"></FONT>
-<P><FONT SIZE="2">This Agreement is governed by the laws of the State of New York and the intellectual property laws of the United States of America. No party to this Agreement will bring a legal action under this Agreement more than one year after the cause of action arose.  Each party waives its rights to a jury trial in any resulting litigation.</FONT>
-<P><FONT SIZE="2"></FONT><FONT SIZE="2"></FONT>
-<P><FONT SIZE="2"></FONT>
-
-</BODY>
-
-</HTML>
\ No newline at end of file
diff --git a/test-runner/tests/Android.bp b/test-runner/tests/Android.bp
index ac21bcb..aad2bee 100644
--- a/test-runner/tests/Android.bp
+++ b/test-runner/tests/Android.bp
@@ -13,12 +13,7 @@
 // limitations under the License.
 
 package {
-    // See: http://go/android-license-faq
-    // A large-scale-change added 'default_applicable_licenses' to import
-    // all of the 'license_kinds' from "frameworks_base_license"
-    // to get the below license kinds:
-    //   SPDX-license-identifier-Apache-2.0
-    default_applicable_licenses: ["frameworks_base_license"],
+    default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
 android_test {
diff --git a/tests/inputmethod/ConcurrentMultiSessionImeTest/Android.bp b/tests/inputmethod/ConcurrentMultiSessionImeTest/Android.bp
index 8d05a97..0e0d212 100644
--- a/tests/inputmethod/ConcurrentMultiSessionImeTest/Android.bp
+++ b/tests/inputmethod/ConcurrentMultiSessionImeTest/Android.bp
@@ -26,6 +26,11 @@
         "platform-test-annotations",
         "platform-test-rules",
         "truth",
+
+        // beadstead
+        "Nene",
+        "Harrier",
+        "TestApp",
     ],
     test_suites: [
         "general-tests",
diff --git a/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java b/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java
index b66ceba..867c0a6 100644
--- a/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java
+++ b/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java
@@ -23,14 +23,20 @@
 
 import androidx.test.platform.app.InstrumentationRegistry;
 
+import com.android.bedstead.harrier.BedsteadJUnit4;
+import com.android.bedstead.harrier.DeviceState;
+
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
 
-@RunWith(JUnit4.class)
+@RunWith(BedsteadJUnit4.class)
 public final class ConcurrentMultiUserTest {
 
+    @Rule
+    public static final DeviceState sDeviceState = new DeviceState();
+
     @Before
     public void doBeforeEachTest() {
         // No op