Merge "Revert^2 "Set container to system for aconfig_declarations in AconfigFlags.bp"" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index c942616..873e952 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -92,6 +92,7 @@
         "com.android.window.flags.window-aconfig-java",
         "device_policy_aconfig_flags_lib",
         "display_flags_lib",
+        "dropbox_flags_lib",
         "framework-jobscheduler-job.flags-aconfig-java",
         "framework_graphics_flags_java_lib",
         "hwui_flags_java_lib",
@@ -1413,3 +1414,19 @@
     aconfig_declarations: "backstage_power_flags",
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
 }
+
+// Dropbox data
+aconfig_declarations {
+    name: "dropbox_flags",
+    package: "com.android.server.feature.flags",
+    container: "system",
+    srcs: [
+        "services/core/java/com/android/server/feature/dropbox_flags.aconfig",
+    ],
+}
+
+java_aconfig_library {
+    name: "dropbox_flags_lib",
+    aconfig_declarations: "dropbox_flags",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 1383096..443a6c0e 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -1769,16 +1769,14 @@
   }
 
   public final class InputManager {
-    method @FlaggedApi("com.android.input.flags.device_associations") @RequiresPermission("android.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY") public void addUniqueIdAssociationByDescriptor(@NonNull String, @NonNull String);
-    method @FlaggedApi("com.android.input.flags.device_associations") @RequiresPermission("android.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY") public void addUniqueIdAssociationByPort(@NonNull String, @NonNull String);
+    method public void addUniqueIdAssociation(@NonNull String, @NonNull String);
     method @RequiresPermission(android.Manifest.permission.REMAP_MODIFIER_KEYS) public void clearAllModifierKeyRemappings();
     method @NonNull public java.util.List<java.lang.String> getKeyboardLayoutDescriptors();
     method @NonNull public String getKeyboardLayoutTypeForLayoutDescriptor(@NonNull String);
     method @NonNull @RequiresPermission(android.Manifest.permission.REMAP_MODIFIER_KEYS) public java.util.Map<java.lang.Integer,java.lang.Integer> getModifierKeyRemapping();
     method public int getMousePointerSpeed();
     method @RequiresPermission(android.Manifest.permission.REMAP_MODIFIER_KEYS) public void remapModifierKey(int, int);
-    method @FlaggedApi("com.android.input.flags.device_associations") @RequiresPermission("android.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY") public void removeUniqueIdAssociationByDescriptor(@NonNull String);
-    method @FlaggedApi("com.android.input.flags.device_associations") @RequiresPermission("android.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY") public void removeUniqueIdAssociationByPort(@NonNull String);
+    method public void removeUniqueIdAssociation(@NonNull String);
     field public static final long BLOCK_UNTRUSTED_TOUCHES = 158002302L; // 0x96aec7eL
   }
 
diff --git a/core/api/test-lint-baseline.txt b/core/api/test-lint-baseline.txt
index e6265ae..685ea63 100644
--- a/core/api/test-lint-baseline.txt
+++ b/core/api/test-lint-baseline.txt
@@ -977,16 +977,8 @@
     Method 'setHdmiCecVersion' documentation mentions permissions already declared by @RequiresPermission
 RequiresPermission: android.hardware.input.InputManager#addUniqueIdAssociation(String, String):
     Method 'addUniqueIdAssociation' documentation mentions permissions without declaring @RequiresPermission
-RequiresPermission: android.hardware.input.InputManager#addUniqueIdAssociationByDescriptor(String, String):
-    Method 'addUniqueIdAssociationByDescriptor' documentation mentions permissions already declared by @RequiresPermission
-RequiresPermission: android.hardware.input.InputManager#addUniqueIdAssociationByPort(String, String):
-    Method 'addUniqueIdAssociationByPort' documentation mentions permissions already declared by @RequiresPermission
 RequiresPermission: android.hardware.input.InputManager#removeUniqueIdAssociation(String):
     Method 'removeUniqueIdAssociation' documentation mentions permissions without declaring @RequiresPermission
-RequiresPermission: android.hardware.input.InputManager#removeUniqueIdAssociationByDescriptor(String):
-    Method 'removeUniqueIdAssociationByDescriptor' documentation mentions permissions already declared by @RequiresPermission
-RequiresPermission: android.hardware.input.InputManager#removeUniqueIdAssociationByPort(String):
-    Method 'removeUniqueIdAssociationByPort' documentation mentions permissions already declared by @RequiresPermission
 RequiresPermission: android.hardware.location.GeofenceHardware#addGeofence(int, int, android.hardware.location.GeofenceHardwareRequest, android.hardware.location.GeofenceHardwareCallback):
     Method 'addGeofence' documentation mentions permissions without declaring @RequiresPermission
 RequiresPermission: android.hardware.location.GeofenceHardware#getMonitoringTypes():
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index e39928b..f7e0e9f 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -17,15 +17,14 @@
 package android.app;
 
 import static android.annotation.Dimension.DP;
+import static android.app.Flags.evenlyDividedCallStyleActionLayout;
+import static android.app.Flags.updateRankingTime;
 import static android.app.admin.DevicePolicyResources.Drawables.Source.NOTIFICATION;
 import static android.app.admin.DevicePolicyResources.Drawables.Style.SOLID_COLORED;
 import static android.app.admin.DevicePolicyResources.Drawables.WORK_PROFILE_ICON;
 import static android.app.admin.DevicePolicyResources.UNDEFINED;
 import static android.graphics.drawable.Icon.TYPE_URI;
 import static android.graphics.drawable.Icon.TYPE_URI_ADAPTIVE_BITMAP;
-import static android.app.Flags.cleanUpSpansAndNewLines;
-import static android.app.Flags.evenlyDividedCallStyleActionLayout;
-import static android.app.Flags.updateRankingTime;
 
 import static java.util.Objects.requireNonNull;
 
diff --git a/core/java/android/app/WindowConfiguration.java b/core/java/android/app/WindowConfiguration.java
index aa3b71a..a12faca 100644
--- a/core/java/android/app/WindowConfiguration.java
+++ b/core/java/android/app/WindowConfiguration.java
@@ -100,9 +100,6 @@
     /** The current windowing mode of the configuration. */
     private @WindowingMode int mWindowingMode;
 
-    /** The display windowing mode of the configuration */
-    private @WindowingMode int mDisplayWindowingMode;
-
     /** Windowing mode is currently not defined. */
     public static final int WINDOWING_MODE_UNDEFINED = 0;
     /** Occupies the full area of the screen or the parent container. */
@@ -193,12 +190,9 @@
     /** Bit that indicates that the {@link #mRotation} changed.
      * @hide */
     public static final int WINDOW_CONFIG_ROTATION = 1 << 6;
-    /** Bit that indicates that the {@link #mDisplayWindowingMode} changed.
-     * @hide */
-    public static final int WINDOW_CONFIG_DISPLAY_WINDOWING_MODE = 1 << 7;
     /** Bit that indicates that the apparent-display changed.
      * @hide */
-    public static final int WINDOW_CONFIG_DISPLAY_ROTATION = 1 << 8;
+    public static final int WINDOW_CONFIG_DISPLAY_ROTATION = 1 << 7;
 
     /** @hide */
     @IntDef(flag = true, prefix = { "WINDOW_CONFIG_" }, value = {
@@ -209,7 +203,6 @@
             WINDOW_CONFIG_ACTIVITY_TYPE,
             WINDOW_CONFIG_ALWAYS_ON_TOP,
             WINDOW_CONFIG_ROTATION,
-            WINDOW_CONFIG_DISPLAY_WINDOWING_MODE,
             WINDOW_CONFIG_DISPLAY_ROTATION,
     })
     public @interface WindowConfig {}
@@ -237,7 +230,6 @@
         dest.writeInt(mActivityType);
         dest.writeInt(mAlwaysOnTop);
         dest.writeInt(mRotation);
-        dest.writeInt(mDisplayWindowingMode);
         dest.writeInt(mDisplayRotation);
     }
 
@@ -250,7 +242,6 @@
         mActivityType = source.readInt();
         mAlwaysOnTop = source.readInt();
         mRotation = source.readInt();
-        mDisplayWindowingMode = source.readInt();
         mDisplayRotation = source.readInt();
     }
 
@@ -411,17 +402,6 @@
         return mWindowingMode;
     }
 
-    /** @hide */
-    public void setDisplayWindowingMode(@WindowingMode int windowingMode) {
-        mDisplayWindowingMode = windowingMode;
-    }
-
-    /** @hide */
-    @WindowingMode
-    public int getDisplayWindowingMode() {
-        return mDisplayWindowingMode;
-    }
-
     public void setActivityType(@ActivityType int activityType) {
         if (mActivityType == activityType) {
             return;
@@ -453,7 +433,6 @@
         setActivityType(other.mActivityType);
         setAlwaysOnTop(other.mAlwaysOnTop);
         setRotation(other.mRotation);
-        setDisplayWindowingMode(other.mDisplayWindowingMode);
     }
 
     /** Set this object to completely undefined.
@@ -472,7 +451,6 @@
         setActivityType(ACTIVITY_TYPE_UNDEFINED);
         setAlwaysOnTop(ALWAYS_ON_TOP_UNDEFINED);
         setRotation(ROTATION_UNDEFINED);
-        setDisplayWindowingMode(WINDOWING_MODE_UNDEFINED);
     }
 
     /** @hide */
@@ -543,11 +521,6 @@
             changed |= WINDOW_CONFIG_ROTATION;
             setRotation(delta.mRotation);
         }
-        if (delta.mDisplayWindowingMode != WINDOWING_MODE_UNDEFINED
-                && mDisplayWindowingMode != delta.mDisplayWindowingMode) {
-            changed |= WINDOW_CONFIG_DISPLAY_WINDOWING_MODE;
-            setDisplayWindowingMode(delta.mDisplayWindowingMode);
-        }
         if (delta.mDisplayRotation != ROTATION_UNDEFINED
                 && delta.mDisplayRotation != mDisplayRotation) {
             changed |= WINDOW_CONFIG_DISPLAY_ROTATION;
@@ -582,9 +555,6 @@
         if ((mask & WINDOW_CONFIG_ROTATION) != 0) {
             setRotation(delta.mRotation);
         }
-        if ((mask & WINDOW_CONFIG_DISPLAY_WINDOWING_MODE) != 0) {
-            setDisplayWindowingMode(delta.mDisplayWindowingMode);
-        }
         if ((mask & WINDOW_CONFIG_DISPLAY_ROTATION) != 0) {
             setDisplayRotation(delta.mDisplayRotation);
         }
@@ -639,11 +609,6 @@
             changes |= WINDOW_CONFIG_ROTATION;
         }
 
-        if ((compareUndefined || other.mDisplayWindowingMode != WINDOWING_MODE_UNDEFINED)
-                && mDisplayWindowingMode != other.mDisplayWindowingMode) {
-            changes |= WINDOW_CONFIG_DISPLAY_WINDOWING_MODE;
-        }
-
         if ((compareUndefined || other.mDisplayRotation != ROTATION_UNDEFINED)
                 && mDisplayRotation != other.mDisplayRotation) {
             changes |= WINDOW_CONFIG_DISPLAY_ROTATION;
@@ -697,8 +662,6 @@
         n = mRotation - that.mRotation;
         if (n != 0) return n;
 
-        n = mDisplayWindowingMode - that.mDisplayWindowingMode;
-        if (n != 0) return n;
         n = mDisplayRotation - that.mDisplayRotation;
         if (n != 0) return n;
 
@@ -728,7 +691,6 @@
         result = 31 * result + mActivityType;
         result = 31 * result + mAlwaysOnTop;
         result = 31 * result + mRotation;
-        result = 31 * result + mDisplayWindowingMode;
         result = 31 * result + mDisplayRotation;
         return result;
     }
@@ -742,7 +704,6 @@
                 + " mDisplayRotation=" + (mRotation == ROTATION_UNDEFINED
                         ? "undefined" : rotationToString(mDisplayRotation))
                 + " mWindowingMode=" + windowingModeToString(mWindowingMode)
-                + " mDisplayWindowingMode=" + windowingModeToString(mDisplayWindowingMode)
                 + " mActivityType=" + activityTypeToString(mActivityType)
                 + " mAlwaysOnTop=" + alwaysOnTopToString(mAlwaysOnTop)
                 + " mRotation=" + (mRotation == ROTATION_UNDEFINED
@@ -818,16 +779,6 @@
     }
 
     /**
-     * Returns true if the activities associated with this window configuration display a decor
-     * view.
-     * @hide
-     */
-    public boolean hasWindowDecorCaption() {
-        return mActivityType == ACTIVITY_TYPE_STANDARD && (mWindowingMode == WINDOWING_MODE_FREEFORM
-                || mDisplayWindowingMode == WINDOWING_MODE_FREEFORM);
-    }
-
-    /**
      * Returns true if the tasks associated with this window configuration can be resized
      * independently of their parent container.
      * @hide
diff --git a/core/java/android/app/ondeviceintelligence/Feature.java b/core/java/android/app/ondeviceintelligence/Feature.java
index fd0379a..bcc56073 100644
--- a/core/java/android/app/ondeviceintelligence/Feature.java
+++ b/core/java/android/app/ondeviceintelligence/Feature.java
@@ -137,8 +137,8 @@
         if (mModelName != null) flg |= 0x4;
         dest.writeByte(flg);
         dest.writeInt(mId);
-        if (mName != null) dest.writeString8(mName);
-        if (mModelName != null) dest.writeString8(mModelName);
+        if (mName != null) dest.writeString(mName);
+        if (mModelName != null) dest.writeString(mModelName);
         dest.writeInt(mType);
         dest.writeInt(mVariant);
         dest.writeTypedObject(mFeatureParams, flags);
diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl
index 2d474d6..1c37aa2 100644
--- a/core/java/android/hardware/input/IInputManager.aidl
+++ b/core/java/android/hardware/input/IInputManager.aidl
@@ -165,17 +165,10 @@
     // static association for the cleared input port will be restored.
     void removePortAssociation(in String inputPort);
 
-    // Add a runtime association between the input device and display, using device's descriptor.
-    void addUniqueIdAssociationByDescriptor(in String inputDeviceDescriptor,
-            in String displayUniqueId);
-    // Remove the runtime association between the input device and display, using device's
-    // descriptor.
-    void removeUniqueIdAssociationByDescriptor(in String inputDeviceDescriptor);
-
-    // Add a runtime association between the input device and display, using device's port.
-    void addUniqueIdAssociationByPort(in String inputPort, in String displayUniqueId);
-    // Remove the runtime association between the input device and display, using device's port.
-    void removeUniqueIdAssociationByPort(in String inputPort);
+    // Add a runtime association between the input device and display.
+    void addUniqueIdAssociation(in String inputPort, in String displayUniqueId);
+    // Remove the runtime association between the input device and display.
+    void removeUniqueIdAssociation(in String inputPort);
 
     InputSensorInfo[] getSensorList(int deviceId);
 
diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java
index 4dda2c7..f949158 100644
--- a/core/java/android/hardware/input/InputManager.java
+++ b/core/java/android/hardware/input/InputManager.java
@@ -17,7 +17,6 @@
 package android.hardware.input;
 
 import static com.android.input.flags.Flags.FLAG_INPUT_DEVICE_VIEW_BEHAVIOR_API;
-import static com.android.input.flags.Flags.FLAG_DEVICE_ASSOCIATIONS;
 import static com.android.hardware.input.Flags.keyboardLayoutPreviewFlag;
 
 import android.Manifest;
@@ -1055,14 +1054,13 @@
     /**
      * Add a runtime association between the input port and the display port. This overrides any
      * static associations.
-     * @param inputPort the port of the input device
-     * @param displayPort the physical port of the associated display
+     * @param inputPort The port of the input device.
+     * @param displayPort The physical port of the associated display.
      * <p>
      * Requires {@link android.Manifest.permission#ASSOCIATE_INPUT_DEVICE_TO_DISPLAY}.
      * </p>
      * @hide
      */
-    @RequiresPermission(android.Manifest.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY)
     public void addPortAssociation(@NonNull String inputPort, int displayPort) {
         try {
             mIm.addPortAssociation(inputPort, displayPort);
@@ -1074,13 +1072,12 @@
     /**
      * Remove the runtime association between the input port and the display port. Any existing
      * static association for the cleared input port will be restored.
-     * @param inputPort the port of the input device to be cleared
+     * @param inputPort The port of the input device to be cleared.
      * <p>
      * Requires {@link android.Manifest.permission#ASSOCIATE_INPUT_DEVICE_TO_DISPLAY}.
      * </p>
      * @hide
      */
-    @RequiresPermission(android.Manifest.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY)
     public void removePortAssociation(@NonNull String inputPort) {
         try {
             mIm.removePortAssociation(inputPort);
@@ -1092,74 +1089,30 @@
     /**
      * Add a runtime association between the input port and display, by unique id. Input ports are
      * expected to be unique.
-     * @param inputPort the port of the input device
-     * @param displayUniqueId the unique id of the associated display
+     * @param inputPort The port of the input device.
+     * @param displayUniqueId The unique id of the associated display.
      * <p>
      * Requires {@link android.Manifest.permission#ASSOCIATE_INPUT_DEVICE_TO_DISPLAY}.
      * </p>
      * @hide
      */
-    @FlaggedApi(FLAG_DEVICE_ASSOCIATIONS)
-    @RequiresPermission(android.Manifest.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY)
     @TestApi
-    public void addUniqueIdAssociationByPort(@NonNull String inputPort,
+    public void addUniqueIdAssociation(@NonNull String inputPort,
             @NonNull String displayUniqueId) {
-        mGlobal.addUniqueIdAssociationByPort(inputPort, displayUniqueId);
+        mGlobal.addUniqueIdAssociation(inputPort, displayUniqueId);
     }
 
     /**
      * Removes a runtime association between the input device and display.
-     * @param inputPort the port of the input device
+     * @param inputPort The port of the input device.
      * <p>
      * Requires {@link android.Manifest.permission#ASSOCIATE_INPUT_DEVICE_TO_DISPLAY}.
      * </p>
      * @hide
      */
-    @FlaggedApi(FLAG_DEVICE_ASSOCIATIONS)
-    @RequiresPermission(android.Manifest.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY)
     @TestApi
-    public void removeUniqueIdAssociationByPort(@NonNull String inputPort) {
-        mGlobal.removeUniqueIdAssociationByPort(inputPort);
-    }
-
-    /**
-     * Add a runtime association between the input device name and display, by descriptor. Input
-     * device descriptors are expected to be unique per physical device, though one physical
-     * device can have multiple virtual input devices that possess the same descriptor.
-     * E.g. a keyboard with built in trackpad will be 2 different input devices with the same
-     * descriptor.
-     * @param inputDeviceDescriptor the descriptor of the input device
-     * @param displayUniqueId the unique id of the associated display
-     * <p>
-     * Requires {@link android.Manifest.permissions.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY}.
-     * </p>
-     * @hide
-     */
-    @FlaggedApi(FLAG_DEVICE_ASSOCIATIONS)
-    @RequiresPermission(android.Manifest.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY)
-    @TestApi
-    public void addUniqueIdAssociationByDescriptor(@NonNull String inputDeviceDescriptor,
-                                                   @NonNull String displayUniqueId) {
-        mGlobal.addUniqueIdAssociationByDescriptor(inputDeviceDescriptor, displayUniqueId);
-    }
-
-    /**
-     * Removes a runtime association between the input device and display.
-    }
-
-    /**
-     * Removes a runtime association between the input device and display.
-     * @param inputDeviceDescriptor the descriptor of the input device
-     * <p>
-     * Requires {@link android.Manifest.permissions.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY}.
-     * </p>
-     * @hide
-     */
-    @FlaggedApi(FLAG_DEVICE_ASSOCIATIONS)
-    @RequiresPermission(android.Manifest.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY)
-    @TestApi
-    public void removeUniqueIdAssociationByDescriptor(@NonNull String inputDeviceDescriptor) {
-        mGlobal.removeUniqueIdAssociationByDescriptor(inputDeviceDescriptor);
+    public void removeUniqueIdAssociation(@NonNull String inputPort) {
+        mGlobal.removeUniqueIdAssociation(inputPort);
     }
 
     /**
diff --git a/core/java/android/hardware/input/InputManagerGlobal.java b/core/java/android/hardware/input/InputManagerGlobal.java
index 1d253d9..7b29666 100644
--- a/core/java/android/hardware/input/InputManagerGlobal.java
+++ b/core/java/android/hardware/input/InputManagerGlobal.java
@@ -1467,46 +1467,22 @@
     }
 
     /**
-     * @see InputManager#addUniqueIdAssociationByPort(String, String)
+     * @see InputManager#addUniqueIdAssociation(String, String)
      */
-    public void addUniqueIdAssociationByPort(@NonNull String inputPort,
-                                             @NonNull String displayUniqueId) {
+    public void addUniqueIdAssociation(@NonNull String inputPort, @NonNull String displayUniqueId) {
         try {
-            mIm.addUniqueIdAssociationByPort(inputPort, displayUniqueId);
+            mIm.addUniqueIdAssociation(inputPort, displayUniqueId);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
     }
 
     /**
-     * @see InputManager#removeUniqueIdAssociationByPort(String)
+     * @see InputManager#removeUniqueIdAssociation(String)
      */
-    public void removeUniqueIdAssociationByPort(@NonNull String inputPort) {
+    public void removeUniqueIdAssociation(@NonNull String inputPort) {
         try {
-            mIm.removeUniqueIdAssociationByPort(inputPort);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    /**
-     * @see InputManager#addUniqueIdAssociationByDescriptor(String, String)
-     */
-    public void addUniqueIdAssociationByDescriptor(@NonNull String inputDeviceDescriptor,
-                                                   @NonNull String displayUniqueId) {
-        try {
-            mIm.addUniqueIdAssociationByDescriptor(inputDeviceDescriptor, displayUniqueId);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    /**
-     * @see InputManager#removeUniqueIdAssociationByDescriptor(String)
-     */
-    public void removeUniqueIdAssociationByDescriptor(@NonNull String inputDeviceDescriptor) {
-        try {
-            mIm.removeUniqueIdAssociationByDescriptor(inputDeviceDescriptor);
+            mIm.removeUniqueIdAssociation(inputPort);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index cbfc5d1..278e863 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -765,6 +765,7 @@
 
         private boolean mSystemCallingShowSoftInput;
         private boolean mSystemCallingHideSoftInput;
+        private boolean mSimultaneousStylusAndTouchEnabled;
 
         /**
          * {@inheritDoc}
@@ -1129,9 +1130,11 @@
             mShowInputRequested = false;
 
             mInkWindow.show();
+            mSimultaneousStylusAndTouchEnabled =
+                    com.android.input.flags.Flags.enableMultiDeviceInput();
 
             // deliver previous @param stylusEvents
-            stylusEvents.forEach(InputMethodService.this::onStylusHandwritingMotionEvent);
+            stylusEvents.forEach(this::deliverStylusHandwritingMotionEvent);
 
             // create receiver for channel
             mHandwritingEventReceiver = new InputEventReceiver(channel, Looper.getMainLooper()) {
@@ -1139,10 +1142,15 @@
                 public void onInputEvent(InputEvent event) {
                     boolean handled = false;
                     try {
-                        if (!(event instanceof MotionEvent)) {
+                        if (!(event instanceof MotionEvent motionEvent)) {
                             return;
                         }
-                        onStylusHandwritingMotionEvent((MotionEvent) event);
+                        if (!motionEvent.isStylusPointer()) {
+                            // Handwriting surface is touchable, we don't want these touch events
+                            // to get to the IME.
+                            return;
+                        }
+                        deliverStylusHandwritingMotionEvent(motionEvent);
                         scheduleHandwritingSessionTimeout();
                         handled = true;
                     } finally {
@@ -1153,6 +1161,27 @@
             scheduleHandwritingSessionTimeout();
         }
 
+        private void deliverStylusHandwritingMotionEvent(MotionEvent motionEvent) {
+            onStylusHandwritingMotionEvent(motionEvent);
+            if (!mSimultaneousStylusAndTouchEnabled) {
+                return;
+            }
+            switch (motionEvent.getAction()) {
+                case MotionEvent.ACTION_DOWN:
+                    // Consume and ignore all touches while stylus is down to prevent
+                    // accidental touches from going to the app while writing.
+                    mPrivOps.setHandwritingSurfaceNotTouchable(false);
+                    break;
+                case MotionEvent.ACTION_UP:
+                case MotionEvent.ACTION_CANCEL:
+                    // Go back to only consuming stylus events so that the user
+                    // can continue to interact with the app using touch
+                    // when the stylus is not down.
+                    mPrivOps.setHandwritingSurfaceNotTouchable(true);
+                    break;
+            }
+        }
+
         /**
          * {@inheritDoc}
          * @hide
diff --git a/core/java/android/os/IHintManager.aidl b/core/java/android/os/IHintManager.aidl
index d97ea54..e057a85 100644
--- a/core/java/android/os/IHintManager.aidl
+++ b/core/java/android/os/IHintManager.aidl
@@ -18,16 +18,21 @@
 package android.os;
 
 import android.os.IHintSession;
+import android.hardware.power.SessionConfig;
+import android.hardware.power.SessionTag;
 
 /** {@hide} */
 interface IHintManager {
     /**
      * Creates a {@link Session} for the given set of threads and associates to a binder token.
+     * Returns a config if creation is not supported, and HMS had to use the
+     * legacy creation method.
      */
-    IHintSession createHintSession(in IBinder token, in int[] tids, long durationNanos);
+    IHintSession createHintSessionWithConfig(in IBinder token, in int[] threadIds,
+            in long durationNanos, in SessionTag tag, out @nullable SessionConfig config);
 
     /**
-     * Get preferred rate limit in nano second.
+     * Get preferred rate limit in nanoseconds.
      */
     long getHintSessionPreferredRate();
 
diff --git a/core/java/android/service/autofill/AutofillService.java b/core/java/android/service/autofill/AutofillService.java
index e6a84df..269839b 100644
--- a/core/java/android/service/autofill/AutofillService.java
+++ b/core/java/android/service/autofill/AutofillService.java
@@ -37,7 +37,6 @@
 import android.view.autofill.AutofillId;
 import android.view.autofill.AutofillManager;
 import android.view.autofill.AutofillValue;
-import android.view.autofill.IAutoFillManagerClient;
 
 import com.android.internal.os.IResultReceiver;
 
@@ -642,7 +641,7 @@
 
         @Override
         public void onFillCredentialRequest(FillRequest request, IFillCallback callback,
-                IAutoFillManagerClient autofillClientCallback) {
+                IBinder autofillClientCallback) {
             ICancellationSignal transport = CancellationSignal.createTransport();
             try {
                 callback.onCancellable(transport);
@@ -724,7 +723,7 @@
      */
     public void onFillCredentialRequest(@NonNull FillRequest request,
             @NonNull CancellationSignal cancellationSignal, @NonNull FillCallback callback,
-            @NonNull IAutoFillManagerClient autofillClientCallback) {}
+            @NonNull IBinder autofillClientCallback) {}
 
     /**
      * Called by the Android system to convert a credential manager response to a dataset
diff --git a/core/java/android/service/autofill/IAutoFillService.aidl b/core/java/android/service/autofill/IAutoFillService.aidl
index 2c2feae..3b64b8a 100644
--- a/core/java/android/service/autofill/IAutoFillService.aidl
+++ b/core/java/android/service/autofill/IAutoFillService.aidl
@@ -16,13 +16,13 @@
 
 package android.service.autofill;
 
+import android.os.IBinder;
 import android.service.autofill.ConvertCredentialRequest;
 import android.service.autofill.IConvertCredentialCallback;
 import android.service.autofill.FillRequest;
 import android.service.autofill.IFillCallback;
 import android.service.autofill.ISaveCallback;
 import android.service.autofill.SaveRequest;
-import android.view.autofill.IAutoFillManagerClient;
 import com.android.internal.os.IResultReceiver;
 
 /**
@@ -34,7 +34,7 @@
     void onConnectedStateChanged(boolean connected);
     void onFillRequest(in FillRequest request, in IFillCallback callback);
     void onFillCredentialRequest(in FillRequest request, in IFillCallback callback,
-        in IAutoFillManagerClient client);
+        in IBinder client);
     void onSaveRequest(in SaveRequest request, in ISaveCallback callback);
     void onSavedPasswordCountRequest(in IResultReceiver receiver);
     void onConvertCredentialRequest(in ConvertCredentialRequest convertCredentialRequest, in IConvertCredentialCallback convertCredentialCallback);
diff --git a/core/java/android/service/notification/flags.aconfig b/core/java/android/service/notification/flags.aconfig
index b0c55a9..c5b4b41 100644
--- a/core/java/android/service/notification/flags.aconfig
+++ b/core/java/android/service/notification/flags.aconfig
@@ -15,7 +15,17 @@
   namespace: "systemui"
   description: "This flag controls the redacting of sensitive notifications from untrusted NotificationListenerServices"
   bug: "306271190"
+}
+
+flag {
+  name: "redact_sensitive_notifications_big_text_style"
   is_exported: true
+  namespace: "systemui"
+  description: "This flag controls the redacting of BigTextStyle fields in sensitive notifications"
+  bug: "335488909"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
 }
 
 flag {
diff --git a/core/java/android/view/TextureView.java b/core/java/android/view/TextureView.java
index 5466bf5..ebc86ee 100644
--- a/core/java/android/view/TextureView.java
+++ b/core/java/android/view/TextureView.java
@@ -202,6 +202,14 @@
     // Set by native code, do not write!
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     private long mNativeWindow;
+    // Used for VRR detecting "normal" frame rate rather than "high". This is the previous
+    // interval for drawing. This can be removed when NORMAL is the default rate for Views.
+    // (b/329156944)
+    private long mMinusTwoFrameIntervalMillis = 0;
+    // Used for VRR detecting "normal" frame rate rather than "high". This is the last
+    // frame time for drawing. This can be removed when NORMAL is the default rate for Views.
+    // (b/329156944)
+    private long mLastFrameTimeMillis = 0;
 
     /**
      * Creates a new TextureView.
@@ -890,12 +898,26 @@
      */
     @Override
     protected int calculateFrameRateCategory() {
-        if (mMinusTwoFrameIntervalMillis > 15 && mMinusOneFrameIntervalMillis > 15) {
+        long now = getDrawingTime();
+        // This isn't necessary when the default frame rate is NORMAL (b/329156944)
+        if (mMinusTwoFrameIntervalMillis > 15 && (now - mLastFrameTimeMillis) > 15) {
             return FRAME_RATE_CATEGORY_NORMAL;
         }
         return super.calculateFrameRateCategory();
     }
 
+    /**
+     * @hide
+     */
+    @Override
+    protected void votePreferredFrameRate() {
+        super.votePreferredFrameRate();
+        // This isn't necessary when the default frame rate is NORMAL (b/329156944)
+        long now = getDrawingTime();
+        mMinusTwoFrameIntervalMillis = now - mLastFrameTimeMillis;
+        mLastFrameTimeMillis = now;
+    }
+
     @UnsupportedAppUsage
     private final SurfaceTexture.OnFrameAvailableListener mUpdateListener =
             surfaceTexture -> {
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 4c4a22c..47816f4 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -1133,7 +1133,7 @@
     private static final int FOCUSABLE_MASK = 0x00000011;
 
     /**
-     * This view will adjust its padding to fit sytem windows (e.g. status bar)
+     * This view will adjust its padding to fit system windows (e.g. status bar)
      */
     private static final int FITS_SYSTEM_WINDOWS = 0x00000002;
 
@@ -5764,23 +5764,10 @@
 
     static final float MAX_FRAME_RATE = 140;
 
-    private static final int INFREQUENT_UPDATE_INTERVAL_MILLIS = 100;
-    private static final int INFREQUENT_UPDATE_COUNTS = 2;
-
     // The preferred frame rate of the view that is mainly used for
     // touch boosting, view velocity handling, and TextureView.
     private float mPreferredFrameRate = REQUESTED_FRAME_RATE_CATEGORY_DEFAULT;
 
-    private int mInfrequentUpdateCount = 0;
-    private long mLastUpdateTimeMillis = 0;
-    /**
-     * @hide
-     */
-    protected int mMinusOneFrameIntervalMillis = 0;
-    /**
-     * @hide
-     */
-    protected int mMinusTwoFrameIntervalMillis = 0;
     private int mLastFrameRateCategory = FRAME_RATE_CATEGORY_NO_PREFERENCE;
 
     @FlaggedApi(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
@@ -23651,7 +23638,6 @@
         if (sToolkitSetFrameRateReadOnlyFlagValue
                 && sToolkitFrameRateViewEnablingReadOnlyFlagValue) {
             votePreferredFrameRate();
-            updateInfrequentCount();
         }
 
         mPrivateFlags4 = (mPrivateFlags4 & ~PFLAG4_HAS_MOVED) | PFLAG4_HAS_DRAWN;
@@ -32835,6 +32821,13 @@
             SENSITIVE_CONTENT_AUTOFILL_HINTS.add(View.AUTOFILL_HINT_USERNAME);
             SENSITIVE_CONTENT_AUTOFILL_HINTS.add(View.AUTOFILL_HINT_PASSWORD_AUTO);
             SENSITIVE_CONTENT_AUTOFILL_HINTS.add(View.AUTOFILL_HINT_PASSWORD);
+            SENSITIVE_CONTENT_AUTOFILL_HINTS.add(View.AUTOFILL_HINT_CREDIT_CARD_NUMBER);
+            SENSITIVE_CONTENT_AUTOFILL_HINTS.add(View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE);
+            SENSITIVE_CONTENT_AUTOFILL_HINTS.add(View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE);
+            SENSITIVE_CONTENT_AUTOFILL_HINTS.add(View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DAY);
+            SENSITIVE_CONTENT_AUTOFILL_HINTS.add(View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH);
+            SENSITIVE_CONTENT_AUTOFILL_HINTS.add(View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR);
+            SENSITIVE_CONTENT_AUTOFILL_HINTS.add(View.AUTOFILL_HINT_CREDENTIAL_MANAGER);
         }
 
         /**
@@ -33903,15 +33896,15 @@
      * @hide
      */
     protected int calculateFrameRateCategory() {
-        if (mMinusTwoFrameIntervalMillis + mMinusOneFrameIntervalMillis
-                < INFREQUENT_UPDATE_INTERVAL_MILLIS) {
-            return mSizeBasedFrameRateCategoryAndReason;
+        int category;
+        switch (getViewRootImpl().intermittentUpdateState()) {
+            case ViewRootImpl.INTERMITTENT_STATE_INTERMITTENT ->
+                    category = FRAME_RATE_CATEGORY_NORMAL | FRAME_RATE_CATEGORY_REASON_INTERMITTENT;
+            case ViewRootImpl.INTERMITTENT_STATE_NOT_INTERMITTENT ->
+                    category = mSizeBasedFrameRateCategoryAndReason;
+            default -> category = mLastFrameRateCategory;
         }
-
-        if (mInfrequentUpdateCount == INFREQUENT_UPDATE_COUNTS) {
-            return FRAME_RATE_CATEGORY_NORMAL | FRAME_RATE_CATEGORY_REASON_INTERMITTENT;
-        }
-        return mLastFrameRateCategory;
+        return category;
     }
 
     /**
@@ -33922,76 +33915,99 @@
     protected void votePreferredFrameRate() {
         // use toolkitSetFrameRate flag to gate the change
         ViewRootImpl viewRootImpl = getViewRootImpl();
-        int width = mRight - mLeft;
-        int height = mBottom - mTop;
-
-        if (viewRootImpl != null && (width != 0 && height != 0)) {
-            if (viewRootImpl.shouldCheckFrameRate(mPreferredFrameRate > 0f)) {
-                float velocityFrameRate = 0f;
-                if (mAttachInfo.mViewVelocityApi) {
-                    float velocity = mFrameContentVelocity;
-
-                    if (velocity < 0f
-                            && (mPrivateFlags4 & (PFLAG4_HAS_MOVED | PFLAG4_HAS_DRAWN)) == (
-                            PFLAG4_HAS_MOVED | PFLAG4_HAS_DRAWN)
-                            && mParent instanceof View
-                            && ((View) mParent).mFrameContentVelocity <= 0
-                    ) {
-                        // This current calculation is very simple. If something on the screen
-                        // moved, then it votes for the highest velocity.
-                        velocityFrameRate = MAX_FRAME_RATE;
-                    } else if (velocity > 0f) {
-                        velocityFrameRate = convertVelocityToFrameRate(velocity);
-                    }
-                }
-                if (velocityFrameRate > 0f || mPreferredFrameRate > 0f) {
-                    int compatibility = FRAME_RATE_COMPATIBILITY_GTE;
-                    float frameRate = velocityFrameRate;
-                    if (mPreferredFrameRate > velocityFrameRate) {
-                        compatibility = FRAME_RATE_COMPATIBILITY_FIXED_SOURCE;
-                        frameRate = mPreferredFrameRate;
-                    }
-                    viewRootImpl.votePreferredFrameRate(frameRate, compatibility);
-                }
+        if (viewRootImpl == null) {
+            return; // can't vote if not connected
+        }
+        float velocity = mFrameContentVelocity;
+        float frameRate = mPreferredFrameRate;
+        ViewParent parent = mParent;
+        if (velocity <= 0 && Float.isNaN(frameRate)) {
+            // The most common case is when nothing is set, so this special case is called
+            // often.
+            if (mAttachInfo.mViewVelocityApi
+                    && (mPrivateFlags4 & (PFLAG4_HAS_MOVED | PFLAG4_HAS_DRAWN)) == (
+                    PFLAG4_HAS_MOVED | PFLAG4_HAS_DRAWN)
+                    && viewRootImpl.shouldCheckFrameRate(false)
+                    && parent instanceof View
+                    && ((View) parent).mFrameContentVelocity <= 0) {
+                viewRootImpl.votePreferredFrameRate(MAX_FRAME_RATE, FRAME_RATE_COMPATIBILITY_GTE);
             }
-            if (!willNotDraw() && isDirty() && viewRootImpl.shouldCheckFrameRateCategory()) {
-                if (sToolkitMetricsForFrameRateDecisionFlagValue) {
-                    float sizePercentage = width * height / mAttachInfo.mDisplayPixelCount;
-                    viewRootImpl.recordViewPercentage(sizePercentage);
-                }
-
-                int frameRateCategory = FRAME_RATE_CATEGORY_NO_PREFERENCE;
-                if (Float.isNaN(mPreferredFrameRate)) {
-                    frameRateCategory = calculateFrameRateCategory();
-                } else if (mPreferredFrameRate < 0) {
-                    switch ((int) mPreferredFrameRate) {
-                        case (int) REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE ->
-                                frameRateCategory = FRAME_RATE_CATEGORY_NO_PREFERENCE
-                                        | FRAME_RATE_CATEGORY_REASON_REQUESTED;
-                        case (int) REQUESTED_FRAME_RATE_CATEGORY_LOW ->
-                                frameRateCategory = FRAME_RATE_CATEGORY_LOW
-                                        | FRAME_RATE_CATEGORY_REASON_REQUESTED;
-                        case (int) REQUESTED_FRAME_RATE_CATEGORY_NORMAL ->
-                                frameRateCategory = FRAME_RATE_CATEGORY_NORMAL
-                                        | FRAME_RATE_CATEGORY_REASON_REQUESTED;
-                        case (int) REQUESTED_FRAME_RATE_CATEGORY_HIGH ->
-                                frameRateCategory = FRAME_RATE_CATEGORY_HIGH
-                                        | FRAME_RATE_CATEGORY_REASON_REQUESTED;
-                        default -> {
-                            // invalid frame rate, use default
-                            int category = sToolkitFrameRateDefaultNormalReadOnlyFlagValue
-                                    ? FRAME_RATE_CATEGORY_NORMAL : FRAME_RATE_CATEGORY_HIGH;
-                            frameRateCategory = category
-                                    | FRAME_RATE_CATEGORY_REASON_INVALID;
-                        }
-                    }
-                }
-
+            if (!willNotDraw() && viewRootImpl.shouldCheckFrameRateCategory()) {
+                int frameRateCategory = calculateFrameRateCategory();
                 int category = frameRateCategory & ~FRAME_RATE_CATEGORY_REASON_MASK;
                 int reason = frameRateCategory & FRAME_RATE_CATEGORY_REASON_MASK;
                 viewRootImpl.votePreferredFrameRateCategory(category, reason, this);
                 mLastFrameRateCategory = frameRateCategory;
             }
+            return;
+        }
+        if (viewRootImpl.shouldCheckFrameRate(frameRate > 0f)) {
+            float velocityFrameRate = 0f;
+            if (mAttachInfo.mViewVelocityApi) {
+                if (velocity < 0f
+                        && (mPrivateFlags4 & (PFLAG4_HAS_MOVED | PFLAG4_HAS_DRAWN)) == (
+                        PFLAG4_HAS_MOVED | PFLAG4_HAS_DRAWN)
+                        && mParent instanceof View
+                        && ((View) mParent).mFrameContentVelocity <= 0
+                ) {
+                    // This current calculation is very simple. If something on the screen
+                    // moved, then it votes for the highest velocity.
+                    velocityFrameRate = MAX_FRAME_RATE;
+                } else if (velocity > 0f) {
+                    velocityFrameRate = convertVelocityToFrameRate(velocity);
+                }
+            }
+            if (velocityFrameRate > 0f || frameRate > 0f) {
+                int compatibility;
+                if (frameRate >= velocityFrameRate) {
+                    compatibility = FRAME_RATE_COMPATIBILITY_FIXED_SOURCE;
+                } else {
+                    compatibility = FRAME_RATE_COMPATIBILITY_GTE;
+                    frameRate = velocityFrameRate;
+                }
+                viewRootImpl.votePreferredFrameRate(frameRate, compatibility);
+            }
+        }
+
+        if (!willNotDraw() && viewRootImpl.shouldCheckFrameRateCategory()) {
+            if (sToolkitMetricsForFrameRateDecisionFlagValue) {
+                int width = mRight - mLeft;
+                int height = mBottom - mTop;
+                float sizePercentage = width * height / mAttachInfo.mDisplayPixelCount;
+                viewRootImpl.recordViewPercentage(sizePercentage);
+            }
+
+            int frameRateCategory = FRAME_RATE_CATEGORY_NO_PREFERENCE;
+            if (Float.isNaN(frameRate)) {
+                frameRateCategory = calculateFrameRateCategory();
+            } else if (frameRate < 0) {
+                switch ((int) frameRate) {
+                    case (int) REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE ->
+                            frameRateCategory = FRAME_RATE_CATEGORY_NO_PREFERENCE
+                                    | FRAME_RATE_CATEGORY_REASON_REQUESTED;
+                    case (int) REQUESTED_FRAME_RATE_CATEGORY_LOW ->
+                            frameRateCategory = FRAME_RATE_CATEGORY_LOW
+                                    | FRAME_RATE_CATEGORY_REASON_REQUESTED;
+                    case (int) REQUESTED_FRAME_RATE_CATEGORY_NORMAL ->
+                            frameRateCategory = FRAME_RATE_CATEGORY_NORMAL
+                                    | FRAME_RATE_CATEGORY_REASON_REQUESTED;
+                    case (int) REQUESTED_FRAME_RATE_CATEGORY_HIGH ->
+                            frameRateCategory = FRAME_RATE_CATEGORY_HIGH
+                                    | FRAME_RATE_CATEGORY_REASON_REQUESTED;
+                    default -> {
+                        // invalid frame rate, use default
+                        int category = sToolkitFrameRateDefaultNormalReadOnlyFlagValue
+                                ? FRAME_RATE_CATEGORY_NORMAL : FRAME_RATE_CATEGORY_HIGH;
+                        frameRateCategory = category
+                                | FRAME_RATE_CATEGORY_REASON_INVALID;
+                    }
+                }
+            }
+
+            int category = frameRateCategory & ~FRAME_RATE_CATEGORY_REASON_MASK;
+            int reason = frameRateCategory & FRAME_RATE_CATEGORY_REASON_MASK;
+            viewRootImpl.votePreferredFrameRateCategory(category, reason, this);
+            mLastFrameRateCategory = frameRateCategory;
         }
     }
 
@@ -34074,33 +34090,4 @@
         }
         return 0;
     }
-
-    /**
-     * This function is mainly used for migrating infrequent layer logic
-     * from SurfaceFlinger to Toolkit.
-     * The infrequent layer logic includes:
-     * - NORMAL for infrequent update: FT2-FT1 > 100 && FT3-FT2 > 100.
-     * - HIGH/NORMAL based on size for frequent update: (FT3-FT2) + (FT2 - FT1) < 100.
-     * - otherwise, use the previous category value.
-     */
-    private void updateInfrequentCount() {
-        if (!willNotDraw()) {
-            long currentTimeMillis = getDrawingTime();
-            int timeIntervalMillis =
-                    (int) Math.min(Integer.MAX_VALUE, currentTimeMillis - mLastUpdateTimeMillis);
-            mMinusTwoFrameIntervalMillis = mMinusOneFrameIntervalMillis;
-            mMinusOneFrameIntervalMillis = timeIntervalMillis;
-
-            mLastUpdateTimeMillis = currentTimeMillis;
-            if (mMinusTwoFrameIntervalMillis >= 30 && timeIntervalMillis < 2) {
-                return;
-            }
-            if (timeIntervalMillis >= INFREQUENT_UPDATE_INTERVAL_MILLIS) {
-                mInfrequentUpdateCount = mInfrequentUpdateCount == INFREQUENT_UPDATE_COUNTS
-                        ? mInfrequentUpdateCount : mInfrequentUpdateCount + 1;
-            } else {
-                mInfrequentUpdateCount = 0;
-            }
-        }
-    }
 }
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 8d55777..e2ed2b8 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -392,6 +392,26 @@
 
     private static final int UNSET_SYNC_ID = -1;
 
+    private static final int INFREQUENT_UPDATE_INTERVAL_MILLIS = 100;
+    private static final int INFREQUENT_UPDATE_COUNTS = 2;
+
+    /**
+     * The {@link #intermittentUpdateState()} value when the ViewRootImpl isn't intermittent.
+     */
+    public static final int INTERMITTENT_STATE_NOT_INTERMITTENT = 1;
+
+    /**
+     * The {@link #intermittentUpdateState()} value when the ViewRootImpl is transitioning either
+     * to or from intermittent to not intermittent. This indicates that the frame rate shouldn't
+     * change.
+     */
+    public static final int INTERMITTENT_STATE_IN_TRANSITION = -1;
+
+    /**
+     * The {@link #intermittentUpdateState()} value when the ViewRootImpl is intermittent.
+     */
+    public static final int INTERMITTENT_STATE_INTERMITTENT = 0;
+
     /**
      * Minimum time to wait before reporting changes to keep clear areas.
      */
@@ -623,6 +643,15 @@
     // Is the stylus pointer icon enabled
     private final boolean mIsStylusPointerIconEnabled;
 
+    // VRR check for number of infrequent updates
+    private int mInfrequentUpdateCount = 0;
+    // VRR time of last update
+    private long mLastUpdateTimeMillis = 0;
+    // VRR interval since the previous
+    private int mMinusOneFrameIntervalMillis = 0;
+    // VRR interval between the previous and the frame before
+    private int mMinusTwoFrameIntervalMillis = 0;
+
     /**
      * Update the Choreographer's FrameInfo object with the timing information for the current
      * ViewRootImpl instance. Erase the data in the current ViewFrameInfo to prepare for the next
@@ -1068,6 +1097,7 @@
     // Used to check if there is a message in the message queue
     // for idleness handling.
     private boolean mHasIdledMessage = false;
+    private boolean mDrawnThisFrame = false;
     // Used to check if there is a conflict between different frame rate voting.
     // Take 24 and 30 as an example, 24 is not a divisor of 30.
     // We consider there is a conflict.
@@ -4220,25 +4250,29 @@
         // For the variable refresh rate project.
         // We set the preferred frame rate and frame rate category at the end of performTraversals
         // when the values are applicable.
-        setCategoryFromCategoryCounts();
-        setPreferredFrameRate(mPreferredFrameRate);
-        setPreferredFrameRateCategory(mPreferredFrameRateCategory);
-        if (!mIsFrameRateConflicted) {
-            mHandler.removeMessages(MSG_FRAME_RATE_SETTING);
-            mHandler.sendEmptyMessageDelayed(MSG_FRAME_RATE_SETTING,
-                    FRAME_RATE_SETTING_REEVALUATE_TIME);
+        if (mDrawnThisFrame) {
+            mDrawnThisFrame = false;
+            updateInfrequentCount();
+            setCategoryFromCategoryCounts();
+            setPreferredFrameRate(mPreferredFrameRate);
+            setPreferredFrameRateCategory(mPreferredFrameRateCategory);
+            if (!mIsFrameRateConflicted) {
+                mHandler.removeMessages(MSG_FRAME_RATE_SETTING);
+                mHandler.sendEmptyMessageDelayed(MSG_FRAME_RATE_SETTING,
+                        FRAME_RATE_SETTING_REEVALUATE_TIME);
+            }
+            checkIdleness();
+            mFrameRateCategoryHighCount = mFrameRateCategoryHighCount > 0
+                    ? mFrameRateCategoryHighCount - 1 : mFrameRateCategoryHighCount;
+            mFrameRateCategoryNormalCount = mFrameRateCategoryNormalCount > 0
+                    ? mFrameRateCategoryNormalCount - 1 : mFrameRateCategoryNormalCount;
+            mFrameRateCategoryLowCount = mFrameRateCategoryLowCount > 0
+                    ? mFrameRateCategoryLowCount - 1 : mFrameRateCategoryLowCount;
+            mPreferredFrameRateCategory = FRAME_RATE_CATEGORY_DEFAULT;
+            mPreferredFrameRate = -1;
+            mIsFrameRateConflicted = false;
+            mFrameRateCategoryChangeReason = FRAME_RATE_CATEGORY_REASON_UNKNOWN;
         }
-        checkIdleness();
-        mFrameRateCategoryHighCount = mFrameRateCategoryHighCount > 0
-                ? mFrameRateCategoryHighCount - 1 : mFrameRateCategoryHighCount;
-        mFrameRateCategoryNormalCount = mFrameRateCategoryNormalCount > 0
-                ? mFrameRateCategoryNormalCount - 1 : mFrameRateCategoryNormalCount;
-        mFrameRateCategoryLowCount = mFrameRateCategoryLowCount > 0
-                ? mFrameRateCategoryLowCount - 1 : mFrameRateCategoryLowCount;
-        mPreferredFrameRateCategory = FRAME_RATE_CATEGORY_DEFAULT;
-        mPreferredFrameRate = -1;
-        mIsFrameRateConflicted = false;
-        mFrameRateCategoryChangeReason = FRAME_RATE_CATEGORY_REASON_UNKNOWN;
     }
 
     private void createSyncIfNeeded() {
@@ -9044,20 +9078,26 @@
                     mTempInsets, mTempControls, mRelayoutBundle);
             mRelayoutRequested = true;
 
+            if (activityWindowInfoFlag() && mPendingActivityWindowInfo != null) {
+                ActivityWindowInfo outInfo = null;
+                try {
+                    outInfo = mRelayoutBundle.getParcelable(
+                            IWindowSession.KEY_RELAYOUT_BUNDLE_ACTIVITY_WINDOW_INFO,
+                            ActivityWindowInfo.class);
+                    mRelayoutBundle.remove(IWindowSession.KEY_RELAYOUT_BUNDLE_ACTIVITY_WINDOW_INFO);
+                } catch (IllegalStateException e) {
+                    Log.e(TAG, "Failed to get ActivityWindowInfo from relayout Bundle", e);
+                }
+                if (outInfo != null) {
+                    mPendingActivityWindowInfo.set(outInfo);
+                }
+            }
             final int maybeSyncSeqId = mRelayoutBundle.getInt(
                     IWindowSession.KEY_RELAYOUT_BUNDLE_SEQID);
             if (maybeSyncSeqId > 0) {
                 mSyncSeqId = maybeSyncSeqId;
             }
-            if (activityWindowInfoFlag() && mPendingActivityWindowInfo != null) {
-                final ActivityWindowInfo outInfo = mRelayoutBundle.getParcelable(
-                        IWindowSession.KEY_RELAYOUT_BUNDLE_ACTIVITY_WINDOW_INFO,
-                        ActivityWindowInfo.class);
-                if (outInfo != null) {
-                    mPendingActivityWindowInfo.set(outInfo);
-                }
-            }
-            mRelayoutBundle.clear();
+
             mWinFrameInScreen.set(mTmpFrames.frame);
             if (mTranslator != null) {
                 mTranslator.translateRectInScreenToAppWindow(mTmpFrames.frame);
@@ -12510,6 +12550,15 @@
      * Sets the mPreferredFrameRateCategory from the high, high_hint, normal, and low counts.
      */
     private void setCategoryFromCategoryCounts() {
+        switch (mPreferredFrameRateCategory) {
+            case FRAME_RATE_CATEGORY_LOW -> mFrameRateCategoryLowCount = FRAME_RATE_CATEGORY_COUNT;
+            case FRAME_RATE_CATEGORY_NORMAL ->
+                    mFrameRateCategoryNormalCount = FRAME_RATE_CATEGORY_COUNT;
+            case FRAME_RATE_CATEGORY_HIGH_HINT ->
+                    mFrameRateCategoryHighHintCount = FRAME_RATE_CATEGORY_COUNT;
+            case FRAME_RATE_CATEGORY_HIGH ->
+                    mFrameRateCategoryHighCount = FRAME_RATE_CATEGORY_COUNT;
+        }
         if (mFrameRateCategoryHighCount > 0) {
             mPreferredFrameRateCategory = FRAME_RATE_CATEGORY_HIGH;
         } else if (mFrameRateCategoryHighHintCount > 0) {
@@ -12655,21 +12704,31 @@
      */
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PROTECTED)
     public void votePreferredFrameRateCategory(int frameRateCategory, int reason, View view) {
-        switch (frameRateCategory) {
-            case FRAME_RATE_CATEGORY_LOW -> mFrameRateCategoryLowCount = FRAME_RATE_CATEGORY_COUNT;
-            case FRAME_RATE_CATEGORY_NORMAL ->
-                    mFrameRateCategoryNormalCount = FRAME_RATE_CATEGORY_COUNT;
-            case FRAME_RATE_CATEGORY_HIGH_HINT ->
-                    mFrameRateCategoryHighHintCount = FRAME_RATE_CATEGORY_COUNT;
-            case FRAME_RATE_CATEGORY_HIGH ->
-                    mFrameRateCategoryHighCount = FRAME_RATE_CATEGORY_COUNT;
-        }
         if (frameRateCategory > mPreferredFrameRateCategory) {
             mPreferredFrameRateCategory = frameRateCategory;
             mFrameRateCategoryChangeReason = reason;
-            mFrameRateCategoryView = view == null ? "-" : view.getClass().getSimpleName();
+//            mFrameRateCategoryView = view == null ? "-" : view.getClass().getSimpleName();
         }
         mHasInvalidation = true;
+        mDrawnThisFrame = true;
+    }
+
+    /**
+     * Returns {@link #INTERMITTENT_STATE_INTERMITTENT} when the ViewRootImpl has only been
+     * updated intermittently, {@link #INTERMITTENT_STATE_NOT_INTERMITTENT} when it is
+     * not updated intermittently, and {@link #INTERMITTENT_STATE_IN_TRANSITION} when it
+     * is transitioning between {@link #INTERMITTENT_STATE_NOT_INTERMITTENT} and
+     * {@link #INTERMITTENT_STATE_INTERMITTENT}.
+     */
+    int intermittentUpdateState() {
+        if (mMinusOneFrameIntervalMillis + mMinusTwoFrameIntervalMillis
+                < INFREQUENT_UPDATE_INTERVAL_MILLIS) {
+            return INTERMITTENT_STATE_NOT_INTERMITTENT;
+        }
+        if (mInfrequentUpdateCount == INFREQUENT_UPDATE_COUNTS) {
+            return INTERMITTENT_STATE_INTERMITTENT;
+        }
+        return INTERMITTENT_STATE_IN_TRANSITION;
     }
 
     /**
@@ -12724,6 +12783,8 @@
                 mFrameRateCategoryHighCount = FRAME_RATE_CATEGORY_COUNT;
                 mFrameRateCategoryChangeReason = FRAME_RATE_CATEGORY_REASON_VELOCITY;
                 mFrameRateCategoryView = null;
+                mHasInvalidation = true;
+                mDrawnThisFrame = true;
                 return;
             }
         }
@@ -12755,6 +12816,7 @@
         mPreferredFrameRate = nextFrameRate;
         mFrameRateCompatibility = nextFrameRateCompatibility;
         mHasInvalidation = true;
+        mDrawnThisFrame = true;
     }
 
     /**
@@ -12888,4 +12950,29 @@
         mHandler.removeMessages(MSG_CHECK_INVALIDATION_IDLE);
         mHandler.removeMessages(MSG_FRAME_RATE_SETTING);
     }
+
+    /**
+     * This function is mainly used for migrating infrequent layer logic
+     * from SurfaceFlinger to Toolkit.
+     * The infrequent layer logic includes:
+     * - NORMAL for infrequent update: FT2-FT1 > 100 && FT3-FT2 > 100.
+     * - HIGH/NORMAL based on size for frequent update: (FT3-FT2) + (FT2 - FT1) < 100.
+     * - otherwise, use the previous category value.
+     */
+    private void updateInfrequentCount() {
+        long currentTimeMillis = mAttachInfo.mDrawingTime;
+        int timeIntervalMillis =
+                (int) Math.min(Integer.MAX_VALUE, currentTimeMillis - mLastUpdateTimeMillis);
+        mMinusTwoFrameIntervalMillis = mMinusOneFrameIntervalMillis;
+        mMinusOneFrameIntervalMillis = timeIntervalMillis;
+
+        mLastUpdateTimeMillis = currentTimeMillis;
+        if (timeIntervalMillis >= INFREQUENT_UPDATE_INTERVAL_MILLIS) {
+            int infrequentUpdateCount = mInfrequentUpdateCount;
+            mInfrequentUpdateCount = infrequentUpdateCount == INFREQUENT_UPDATE_COUNTS
+                    ? infrequentUpdateCount : infrequentUpdateCount + 1;
+        } else {
+            mInfrequentUpdateCount = 0;
+        }
+    }
 }
diff --git a/core/java/com/android/internal/inputmethod/IInputMethodPrivilegedOperations.aidl b/core/java/com/android/internal/inputmethod/IInputMethodPrivilegedOperations.aidl
index 457b9dd..63623c7 100644
--- a/core/java/com/android/internal/inputmethod/IInputMethodPrivilegedOperations.aidl
+++ b/core/java/com/android/internal/inputmethod/IInputMethodPrivilegedOperations.aidl
@@ -49,4 +49,5 @@
     void onStylusHandwritingReady(int requestId, int pid);
     void resetStylusHandwriting(int requestId);
     void switchKeyboardLayoutAsync(int direction);
+    void setHandwritingSurfaceNotTouchable(boolean notTouchable);
 }
diff --git a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
index 635a227..72c41be 100644
--- a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
+++ b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
@@ -144,6 +144,24 @@
     }
 
     /**
+     * Calls {@link IInputMethodPrivilegedOperations#setHandwritingSurfaceNotTouchable(boolean)}.
+     *
+     * @param notTouchable {@code true} to make handwriting surface not-touchable (pass-through).
+     */
+    @AnyThread
+    public void setHandwritingSurfaceNotTouchable(boolean notTouchable) {
+        final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
+        if (ops == null) {
+            return;
+        }
+        try {
+            ops.setHandwritingSurfaceNotTouchable(notTouchable);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Calls {@link IInputMethodPrivilegedOperations#createInputContentUriToken(Uri, String,
      * AndroidFuture)}.
      *
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index f31d390..60289c1 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -6436,10 +6436,8 @@
     <!-- Communal profile label on a screen. This can be used as a tab label for this profile in tabbed views and can be used to represent the profile in sharing surfaces, etc. [CHAR LIMIT=20] -->
     <string name="profile_label_communal">Communal</string>
 
-    <!-- Notification message used when a notification's normal message contains sensitive information. -->
-    <!-- TODO b/301960090: replace with redacted message string and action title, when/if UX provides one -->
-    <!-- DO NOT TRANSLATE -->
-    <string name="redacted_notification_message"></string>
+    <!-- Notification message used when a notification's normal message contains sensitive information [CHAR_LIMIT=NOTIF_BODY] -->
+    <string name="redacted_notification_message">Sensitive notification content hidden</string>
     <!-- Notification action title used instead of a notification's normal title sensitive [CHAR_LIMIT=NOTIF_BODY] -->
     <string name="redacted_notification_action_title"></string>
 
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
index 7cb5660..0799fe3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt
@@ -94,6 +94,8 @@
     private var scrimLayer: SurfaceControl? = null
     private var maxScrimAlpha: Float = 0f
 
+    private var isLetterboxed = false
+
     override fun onConfigurationChanged(newConfiguration: Configuration) {
         cornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context)
     }
@@ -112,9 +114,15 @@
         initialTouchPos.set(backMotionEvent.touchX, backMotionEvent.touchY)
 
         transaction.setAnimationTransaction()
-
+        isLetterboxed = closingTarget!!.taskInfo.appCompatTaskInfo.topActivityBoundsLetterboxed
+        if (isLetterboxed) {
+            // Include letterbox in back animation
+            backAnimRect.set(closingTarget!!.windowConfiguration.bounds)
+        } else {
+            // otherwise play animation on localBounds only
+            backAnimRect.set(closingTarget!!.localBounds)
+        }
         // Offset start rectangle to align task bounds.
-        backAnimRect.set(closingTarget!!.localBounds)
         backAnimRect.offsetTo(0, 0)
 
         startClosingRect.set(backAnimRect)
@@ -241,6 +249,7 @@
         }
         finishCallback = null
         removeScrimLayer()
+        isLetterboxed = false
     }
 
     private fun applyTransform(leash: SurfaceControl?, rect: RectF, alpha: Float) {
@@ -274,10 +283,11 @@
         scrimLayer = scrimBuilder.build()
         val colorComponents = floatArrayOf(0f, 0f, 0f)
         maxScrimAlpha = if (isDarkTheme) MAX_SCRIM_ALPHA_DARK else MAX_SCRIM_ALPHA_LIGHT
+        val scrimCrop = if (isLetterboxed) backAnimRect else closingTarget!!.localBounds
         transaction
             .setColor(scrimLayer, colorComponents)
             .setAlpha(scrimLayer!!, maxScrimAlpha)
-            .setCrop(scrimLayer!!, closingTarget!!.localBounds)
+            .setCrop(scrimLayer!!, scrimCrop)
             .setRelativeLayer(scrimLayer!!, closingTarget!!.leash, -1)
             .show(scrimLayer)
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManager.java
index 7c28099..8fb4bdb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManager.java
@@ -237,7 +237,8 @@
         final int letterboxWidth = taskInfo.topActivityLetterboxWidth;
         // App is not visibly letterboxed if it covers status bar/bottom insets or matches the
         // stable bounds, so don't show the button
-        if (stableBounds.height() <= letterboxHeight && stableBounds.width() <= letterboxWidth) {
+        if (stableBounds.height() <= letterboxHeight && stableBounds.width() <= letterboxWidth
+                && !taskInfo.isUserFullscreenOverrideEnabled) {
             return false;
         }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index 9adb67c..2d6ba6e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -594,7 +594,6 @@
                     .setName("animation-background")
                     .setCallsite("DefaultTransitionHandler")
                     .setColorLayer();
-            final SurfaceControl backgroundSurface = colorLayerBuilder.build();
 
             // Attaching the background surface to the transition root could unexpectedly make it
             // cover one of the split root tasks. To avoid this, put the background surface just
@@ -605,8 +604,10 @@
             if (isSplitTaskInvolved) {
                 mRootTDAOrganizer.attachToDisplayArea(displayId, colorLayerBuilder);
             } else {
-                startTransaction.reparent(backgroundSurface, info.getRootLeash());
+                colorLayerBuilder.setParent(info.getRootLeash());
             }
+
+            final SurfaceControl backgroundSurface = colorLayerBuilder.build();
             startTransaction.setColor(backgroundSurface, colorArray)
                     .setLayer(backgroundSurface, -1)
                     .show(backgroundSurface);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java
index 81ba4b3..94e168e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java
@@ -292,6 +292,24 @@
     }
 
     @Test
+    public void testUserFullscreenOverrideEnabled_buttonAlwaysShown() {
+        TaskInfo taskInfo = createTaskInfo(/* eligibleForUserAspectRatioButton= */
+                true, /* topActivityBoundsLetterboxed */ true, ACTION_MAIN, CATEGORY_LAUNCHER);
+
+        final Rect stableBounds = mWindowManager.getTaskStableBounds();
+
+        // Letterboxed activity that has user fullscreen override should always show button,
+        // layout should be inflated
+        taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = stableBounds.height();
+        taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = stableBounds.width();
+        taskInfo.appCompatTaskInfo.isUserFullscreenOverrideEnabled = true;
+
+        mWindowManager.updateCompatInfo(taskInfo, mTaskListener, /* canShow= */ true);
+
+        verify(mWindowManager).inflateLayout();
+    }
+
+    @Test
     public void testUpdateDisplayLayout() {
         final DisplayInfo displayInfo = new DisplayInfo();
         displayInfo.logicalWidth = 1000;
diff --git a/libs/hwui/SkiaInterpolator.cpp b/libs/hwui/SkiaInterpolator.cpp
index c67b135..5a45ad9 100644
--- a/libs/hwui/SkiaInterpolator.cpp
+++ b/libs/hwui/SkiaInterpolator.cpp
@@ -20,6 +20,7 @@
 #include "include/core/SkTypes.h"
 
 #include <cstdlib>
+#include <cstring>
 #include <log/log.h>
 
 typedef int Dot14;
diff --git a/libs/hwui/effects/GainmapRenderer.cpp b/libs/hwui/effects/GainmapRenderer.cpp
index 3ebf7d1..0a30c6c 100644
--- a/libs/hwui/effects/GainmapRenderer.cpp
+++ b/libs/hwui/effects/GainmapRenderer.cpp
@@ -32,6 +32,8 @@
 #include "src/core/SkColorFilterPriv.h"
 #include "src/core/SkImageInfoPriv.h"
 #include "src/core/SkRuntimeEffectPriv.h"
+
+#include <cmath>
 #endif
 
 namespace android::uirenderer {
@@ -206,12 +208,12 @@
 
     void setupGenericUniforms(const sk_sp<const SkImage>& gainmapImage,
                               const SkGainmapInfo& gainmapInfo) {
-        const SkColor4f logRatioMin({sk_float_log(gainmapInfo.fGainmapRatioMin.fR),
-                                     sk_float_log(gainmapInfo.fGainmapRatioMin.fG),
-                                     sk_float_log(gainmapInfo.fGainmapRatioMin.fB), 1.f});
-        const SkColor4f logRatioMax({sk_float_log(gainmapInfo.fGainmapRatioMax.fR),
-                                     sk_float_log(gainmapInfo.fGainmapRatioMax.fG),
-                                     sk_float_log(gainmapInfo.fGainmapRatioMax.fB), 1.f});
+        const SkColor4f logRatioMin({std::log(gainmapInfo.fGainmapRatioMin.fR),
+                                     std::log(gainmapInfo.fGainmapRatioMin.fG),
+                                     std::log(gainmapInfo.fGainmapRatioMin.fB), 1.f});
+        const SkColor4f logRatioMax({std::log(gainmapInfo.fGainmapRatioMax.fR),
+                                     std::log(gainmapInfo.fGainmapRatioMax.fG),
+                                     std::log(gainmapInfo.fGainmapRatioMax.fB), 1.f});
         const int noGamma = gainmapInfo.fGainmapGamma.fR == 1.f &&
                             gainmapInfo.fGainmapGamma.fG == 1.f &&
                             gainmapInfo.fGainmapGamma.fB == 1.f;
@@ -248,10 +250,10 @@
             float W = 0.f;
             if (targetHdrSdrRatio > mGainmapInfo.fDisplayRatioSdr) {
                 if (targetHdrSdrRatio < mGainmapInfo.fDisplayRatioHdr) {
-                    W = (sk_float_log(targetHdrSdrRatio) -
-                         sk_float_log(mGainmapInfo.fDisplayRatioSdr)) /
-                        (sk_float_log(mGainmapInfo.fDisplayRatioHdr) -
-                         sk_float_log(mGainmapInfo.fDisplayRatioSdr));
+                    W = (std::log(targetHdrSdrRatio) -
+                         std::log(mGainmapInfo.fDisplayRatioSdr)) /
+                        (std::log(mGainmapInfo.fDisplayRatioHdr) -
+                         std::log(mGainmapInfo.fDisplayRatioSdr));
                 } else {
                     W = 1.f;
                 }
diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp
index 66e0896..8bb11ba 100644
--- a/libs/hwui/renderthread/CanvasContext.cpp
+++ b/libs/hwui/renderthread/CanvasContext.cpp
@@ -1010,7 +1010,15 @@
 }
 
 void CanvasContext::onContextDestroyed() {
-    destroyHardwareResources();
+    // We don't want to destroyHardwareResources as that will invalidate display lists which
+    // the client may not be expecting. Instead just purge all scratch resources
+    if (mRenderPipeline->isContextReady()) {
+        freePrefetchedLayers();
+        for (const sp<RenderNode>& node : mRenderNodes) {
+            node->destroyLayers();
+        }
+        mRenderPipeline->onDestroyHardwareResources();
+    }
 }
 
 DeferredLayerUpdater* CanvasContext::createTextureLayer() {
diff --git a/libs/hwui/utils/Color.cpp b/libs/hwui/utils/Color.cpp
index f6c5792..6a560b3 100644
--- a/libs/hwui/utils/Color.cpp
+++ b/libs/hwui/utils/Color.cpp
@@ -403,7 +403,7 @@
 }
 
 static skcms_TransferFunction trfn_apply_gain(const skcms_TransferFunction trfn, float gain) {
-    float pow_gain_ginv = sk_float_pow(gain, 1 / trfn.g);
+    float pow_gain_ginv = std::pow(gain, 1 / trfn.g);
     skcms_TransferFunction result;
     result.g = trfn.g;
     result.a = trfn.a * pow_gain_ginv;
diff --git a/location/TEST_MAPPING b/location/TEST_MAPPING
index f5deb2b..10da632 100644
--- a/location/TEST_MAPPING
+++ b/location/TEST_MAPPING
@@ -2,12 +2,7 @@
   "presubmit": [
     {
       "name": "CtsLocationFineTestCases",
-      "options": [
-          {
-             // TODO: Wait for test to deflake - b/293934372
-             "exclude-filter":"android.location.cts.fine.ScanningSettingsTest"
-          }
-      ]
+      "options": []
     },
     {
       "name": "CtsLocationCoarseTestCases"
diff --git a/media/jni/android_media_MediaCodec.cpp b/media/jni/android_media_MediaCodec.cpp
index 4f9917b..8a13c03 100644
--- a/media/jni/android_media_MediaCodec.cpp
+++ b/media/jni/android_media_MediaCodec.cpp
@@ -2099,9 +2099,14 @@
         }
         if (i == 0) {
             *initialOffset = offset;
+            if (CC_UNLIKELY(*initialOffset < 0)) {
+                if (errorDetailMsg) {
+                    *errorDetailMsg = "Error: offset/size in BufferInfo";
+                }
+                return BAD_VALUE;
+            }
         }
-        if (CC_UNLIKELY((offset >  UINT32_MAX)
-                || ((long)(offset + size) > UINT32_MAX)
+        if (CC_UNLIKELY(((ssize_t)(UINT32_MAX - offset) < (ssize_t)size)
                 || ((offset - *initialOffset) != *totalSize))) {
             if (errorDetailMsg) {
                 *errorDetailMsg = "Error: offset/size in BufferInfo";
diff --git a/native/android/performance_hint.cpp b/native/android/performance_hint.cpp
index 882afca..fbb35e2 100644
--- a/native/android/performance_hint.cpp
+++ b/native/android/performance_hint.cpp
@@ -59,7 +59,8 @@
     ~APerformanceHintManager() = default;
 
     APerformanceHintSession* createSession(const int32_t* threadIds, size_t size,
-                                           int64_t initialTargetWorkDurationNanos);
+                                           int64_t initialTargetWorkDurationNanos,
+                                           hal::SessionTag tag = hal::SessionTag::OTHER);
     int64_t getPreferredRateNanos() const;
 
 private:
@@ -84,7 +85,8 @@
 public:
     APerformanceHintSession(std::shared_ptr<IHintManager> hintManager,
                             std::shared_ptr<IHintSession> session, int64_t preferredRateNanos,
-                            int64_t targetDurationNanos);
+                            int64_t targetDurationNanos,
+                            std::optional<hal::SessionConfig> sessionConfig);
     APerformanceHintSession() = delete;
     ~APerformanceHintSession();
 
@@ -116,9 +118,10 @@
     // Cached samples
     std::vector<hal::WorkDuration> mActualWorkDurations;
     std::string mSessionName;
-    static int32_t sIDCounter;
+    static int64_t sIDCounter;
     // The most recent set of thread IDs
     std::vector<int32_t> mLastThreadIDs;
+    std::optional<hal::SessionConfig> mSessionConfig;
     // Tracing helpers
     void traceThreads(std::vector<int32_t>& tids);
     void tracePowerEfficient(bool powerEfficient);
@@ -129,7 +132,8 @@
 
 static std::shared_ptr<IHintManager>* gIHintManagerForTesting = nullptr;
 static APerformanceHintManager* gHintManagerForTesting = nullptr;
-int32_t APerformanceHintSession::sIDCounter = 0;
+// Start above the int32 range so we don't collide with config sessions
+int64_t APerformanceHintSession::sIDCounter = INT32_MAX;
 
 // ===================================== APerformanceHintManager implementation
 APerformanceHintManager::APerformanceHintManager(std::shared_ptr<IHintManager> manager,
@@ -174,16 +178,20 @@
 }
 
 APerformanceHintSession* APerformanceHintManager::createSession(
-        const int32_t* threadIds, size_t size, int64_t initialTargetWorkDurationNanos) {
+        const int32_t* threadIds, size_t size, int64_t initialTargetWorkDurationNanos,
+        hal::SessionTag tag) {
     std::vector<int32_t> tids(threadIds, threadIds + size);
     std::shared_ptr<IHintSession> session;
-    ndk::ScopedAStatus ret =
-            mHintManager->createHintSession(mToken, tids, initialTargetWorkDurationNanos, &session);
+    ndk::ScopedAStatus ret;
+    std::optional<hal::SessionConfig> sessionConfig;
+    ret = mHintManager->createHintSessionWithConfig(mToken, tids, initialTargetWorkDurationNanos,
+                                                    tag, &sessionConfig, &session);
+
     if (!ret.isOk() || !session) {
         return nullptr;
     }
     auto out = new APerformanceHintSession(mHintManager, std::move(session), mPreferredRateNanos,
-                                           initialTargetWorkDurationNanos);
+                                           initialTargetWorkDurationNanos, sessionConfig);
     out->traceThreads(tids);
     out->traceTargetDuration(initialTargetWorkDurationNanos);
     out->tracePowerEfficient(false);
@@ -199,19 +207,23 @@
 APerformanceHintSession::APerformanceHintSession(std::shared_ptr<IHintManager> hintManager,
                                                  std::shared_ptr<IHintSession> session,
                                                  int64_t preferredRateNanos,
-                                                 int64_t targetDurationNanos)
+                                                 int64_t targetDurationNanos,
+                                                 std::optional<hal::SessionConfig> sessionConfig)
       : mHintManager(hintManager),
         mHintSession(std::move(session)),
         mPreferredRateNanos(preferredRateNanos),
         mTargetDurationNanos(targetDurationNanos),
         mFirstTargetMetTimestamp(0),
-        mLastTargetMetTimestamp(0) {
-    const std::vector<hal::SessionHint> sessionHintRange{ndk::enum_range<hal::SessionHint>()
-                                                                 .begin(),
-                                                         ndk::enum_range<hal::SessionHint>().end()};
-
-    mLastHintSentTimestamp = std::vector<int64_t>(sessionHintRange.size(), 0);
-    mSessionName = android::base::StringPrintf("ADPF Session %" PRId32, ++sIDCounter);
+        mLastTargetMetTimestamp(0),
+        mSessionConfig(sessionConfig) {
+    if (sessionConfig->id > INT32_MAX) {
+        ALOGE("Session ID too large, must fit 32-bit integer");
+    }
+    constexpr int numEnums =
+            ndk::enum_range<hal::SessionHint>().end() - ndk::enum_range<hal::SessionHint>().begin();
+    mLastHintSentTimestamp = std::vector<int64_t>(numEnums, 0);
+    int64_t traceId = sessionConfig.has_value() ? sessionConfig->id : ++sIDCounter;
+    mSessionName = android::base::StringPrintf("ADPF Session %" PRId64, traceId);
 }
 
 APerformanceHintSession::~APerformanceHintSession() {
diff --git a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
index bfbe34e..974e6e6 100644
--- a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
+++ b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
@@ -16,6 +16,8 @@
 
 #define LOG_TAG "PerformanceHintNativeTest"
 
+#include <aidl/android/hardware/power/SessionConfig.h>
+#include <aidl/android/hardware/power/SessionTag.h>
 #include <aidl/android/hardware/power/WorkDuration.h>
 #include <aidl/android/os/IHintManager.h>
 #include <android/binder_manager.h>
@@ -28,6 +30,8 @@
 #include <memory>
 #include <vector>
 
+using aidl::android::hardware::power::SessionConfig;
+using aidl::android::hardware::power::SessionTag;
 using aidl::android::hardware::power::WorkDuration;
 using aidl::android::os::IHintManager;
 using aidl::android::os::IHintSession;
@@ -39,8 +43,9 @@
 
 class MockIHintManager : public IHintManager {
 public:
-    MOCK_METHOD(ScopedAStatus, createHintSession,
+    MOCK_METHOD(ScopedAStatus, createHintSessionWithConfig,
                 (const SpAIBinder& token, const ::std::vector<int32_t>& tids, int64_t durationNanos,
+                 SessionTag tag, std::optional<SessionConfig>* config,
                  std::shared_ptr<IHintSession>* _aidl_return),
                 (override));
     MOCK_METHOD(ScopedAStatus, getHintSessionPreferredRate, (int64_t * _aidl_return), (override));
@@ -92,14 +97,18 @@
     APerformanceHintSession* createSession(APerformanceHintManager* manager,
                                            int64_t targetDuration = 56789L) {
         mMockSession = ndk::SharedRefBase::make<NiceMock<MockIHintSession>>();
-
+        int64_t sessionId = 123;
         std::vector<int32_t> tids;
         tids.push_back(1);
         tids.push_back(2);
 
-        ON_CALL(*mMockIHintManager, createHintSession(_, Eq(tids), Eq(targetDuration), _))
-                .WillByDefault(DoAll(SetArgPointee<3>(std::shared_ptr<IHintSession>(mMockSession)),
+        ON_CALL(*mMockIHintManager,
+                createHintSessionWithConfig(_, Eq(tids), Eq(targetDuration), _, _, _))
+                .WillByDefault(DoAll(SetArgPointee<4>(
+                                             std::make_optional<SessionConfig>({.id = sessionId})),
+                                     SetArgPointee<5>(std::shared_ptr<IHintSession>(mMockSession)),
                                      [] { return ScopedAStatus::ok(); }));
+
         ON_CALL(*mMockIHintManager, setHintSessionThreads(_, _)).WillByDefault([] {
             return ScopedAStatus::ok();
         });
@@ -115,7 +124,6 @@
         ON_CALL(*mMockSession, reportActualWorkDuration2(_)).WillByDefault([] {
             return ScopedAStatus::ok();
         });
-
         return APerformanceHint_createSession(manager, tids.data(), tids.size(), targetDuration);
     }
 
@@ -178,6 +186,14 @@
     APerformanceHint_closeSession(session);
 }
 
+TEST_F(PerformanceHintTest, TestUpdatedSessionCreation) {
+    EXPECT_CALL(*mMockIHintManager, createHintSessionWithConfig(_, _, _, _, _, _)).Times(1);
+    APerformanceHintManager* manager = createManager();
+    APerformanceHintSession* session = createSession(manager);
+    ASSERT_TRUE(session);
+    APerformanceHint_closeSession(session);
+}
+
 TEST_F(PerformanceHintTest, SetThreads) {
     APerformanceHintManager* manager = createManager();
 
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java
index a5bb34f..c8801bb 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java
@@ -188,12 +188,7 @@
                 filter(allFilters, BluetoothLeDeviceFilter.class);
         final List<WifiDeviceFilter> wifiFilters = filter(allFilters, WifiDeviceFilter.class);
 
-        // No need to startDiscovery if the device is already bound or connected for
-        // singleDevice dialog.
-        if (checkBoundDevicesIfNeeded(request, btFilters)) {
-            stopSelf();
-            return;
-        }
+        checkBoundDevicesIfNeeded(request, btFilters);
 
         // If no filters are specified: look for everything.
         final boolean forceStartScanningAll = isEmpty(allFilters);
@@ -253,37 +248,33 @@
         stopSelf();
     }
 
-    private boolean checkBoundDevicesIfNeeded(@NonNull AssociationRequest request,
+    private void checkBoundDevicesIfNeeded(@NonNull AssociationRequest request,
             @NonNull List<BluetoothDeviceFilter> btFilters) {
         // If filtering to get single device by mac address, also search in the set of already
         // bonded devices to allow linking those directly
-        if (btFilters.isEmpty() || !request.isSingleDevice()) return false;
+        if (btFilters.isEmpty() || !request.isSingleDevice()) return;
 
         final BluetoothDeviceFilter singleMacAddressFilter =
                 find(btFilters, filter -> !TextUtils.isEmpty(filter.getAddress()));
 
-        if (singleMacAddressFilter == null) return false;
+        if (singleMacAddressFilter == null) return;
 
-        return findAndReportMatches(mBtAdapter.getBondedDevices(), btFilters)
-                || findAndReportMatches(mBtManager.getConnectedDevices(
-                        BluetoothProfile.GATT), btFilters)
-                || findAndReportMatches(mBtManager.getConnectedDevices(
-                        BluetoothProfile.GATT_SERVER), btFilters);
+        findAndReportMatches(mBtAdapter.getBondedDevices(), btFilters);
+        findAndReportMatches(mBtManager.getConnectedDevices(BluetoothProfile.GATT), btFilters);
+        findAndReportMatches(
+                mBtManager.getConnectedDevices(BluetoothProfile.GATT_SERVER), btFilters);
     }
 
-    private boolean findAndReportMatches(@Nullable Collection<BluetoothDevice> devices,
+    private void findAndReportMatches(@Nullable Collection<BluetoothDevice> devices,
             @NonNull List<BluetoothDeviceFilter> filters) {
-        if (devices == null) return false;
+        if (devices == null) return;
 
         for (BluetoothDevice device : devices) {
             final DeviceFilterPair<BluetoothDevice> match = findMatch(device, filters);
             if (match != null) {
                 onDeviceFound(match);
-                return true;
             }
         }
-
-        return false;
     }
 
     private BluetoothBroadcastReceiver startBtScanningIfNeeded(
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
index 4109079..50ebdd5 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
@@ -47,9 +47,9 @@
 import android.service.credentials.CredentialProviderService
 import android.util.Log
 import android.content.Intent
+import android.os.IBinder
 import android.view.autofill.AutofillId
 import android.view.autofill.AutofillManager
-import android.view.autofill.IAutoFillManagerClient
 import android.widget.RemoteViews
 import android.widget.inline.InlinePresentationSpec
 import androidx.autofill.inline.v1.InlineSuggestionUi
@@ -95,7 +95,7 @@
             request: FillRequest,
             cancellationSignal: CancellationSignal,
             callback: FillCallback,
-            autofillCallback: IAutoFillManagerClient
+            autofillCallback: IBinder
     ) {
         val context = request.fillContexts
         val structure = context[context.size - 1].structure
@@ -160,7 +160,7 @@
                 CancellationSignal(),
                 Executors.newSingleThreadExecutor(),
                 outcome,
-                autofillCallback.asBinder()
+                autofillCallback
         )
     }
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/Utils.java b/packages/SettingsLib/src/com/android/settingslib/Utils.java
index 563f02d..c2506d3 100644
--- a/packages/SettingsLib/src/com/android/settingslib/Utils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/Utils.java
@@ -765,7 +765,11 @@
             return false;
         }
 
-        final List<UsbPort> usbPortList = context.getSystemService(UsbManager.class).getPorts();
+        final UsbManager usbManager = context.getSystemService(UsbManager.class);
+        if (usbManager == null) {
+            return false;
+        }
+        final List<UsbPort> usbPortList = usbManager.getPorts();
         if (usbPortList == null || usbPortList.isEmpty()) {
             return false;
         }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
index 9df23aa..a6b1dd3 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
@@ -292,7 +292,6 @@
                                         + ", sourceId = "
                                         + sourceId);
                     }
-                    updateFallbackActiveDeviceIfNeeded();
                 }
 
                 @Override
@@ -314,7 +313,18 @@
                 public void onSourceAddFailed(
                         @NonNull BluetoothDevice sink,
                         @NonNull BluetoothLeBroadcastMetadata source,
-                        int reason) {}
+                        int reason) {
+                    if (DEBUG) {
+                        Log.d(
+                                TAG,
+                                "onSourceAddFailed(), sink = "
+                                        + sink
+                                        + ", reason = "
+                                        + reason
+                                        + ", source = "
+                                        + source);
+                    }
+                }
 
                 @Override
                 public void onSourceModified(
@@ -369,6 +379,9 @@
                                         + ", state = "
                                         + state);
                     }
+                    if (BluetoothUtils.isConnected(state)) {
+                        updateFallbackActiveDeviceIfNeeded();
+                    }
                 }
             };
 
@@ -1056,7 +1069,9 @@
                                     List<BluetoothLeBroadcastReceiveState> sourceList =
                                             mServiceBroadcastAssistant.getAllSources(
                                                     bluetoothDevice);
-                                    return !sourceList.isEmpty();
+                                    return !sourceList.isEmpty()
+                                            && sourceList.stream()
+                                                    .anyMatch(BluetoothUtils::isConnected);
                                 })
                         .collect(Collectors.toList());
         if (devicesInSharing.isEmpty()) {
@@ -1091,7 +1106,8 @@
             return;
         }
         int fallbackActiveGroupId = getFallbackActiveGroupId();
-        if (getGroupId(targetCachedDevice) == fallbackActiveGroupId) {
+        if (fallbackActiveGroupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID
+                && getGroupId(targetCachedDevice) == fallbackActiveGroupId) {
             Log.d(
                     TAG,
                     "Skip updateFallbackActiveDeviceIfNeeded, already is fallback: "
@@ -1101,12 +1117,6 @@
         targetCachedDevice.setActive();
     }
 
-    private boolean isDecryptedSource(BluetoothLeBroadcastReceiveState state) {
-        return state.getPaSyncState() == BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_SYNCHRONIZED
-                && state.getBigEncryptionState()
-                        == BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_DECRYPTING;
-    }
-
     private int getFallbackActiveGroupId() {
         return Settings.Secure.getInt(
                 mContext.getContentResolver(),
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/ScreenDecorProvider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/ScreenDecorProvider.kt
index 76bd4ec..8144d15 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/ScreenDecorProvider.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/ScreenDecorProvider.kt
@@ -16,11 +16,16 @@
 
 package com.android.systemui.common.ui.compose.windowinsets
 
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.displayCutout
+import androidx.compose.foundation.layout.systemBars
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.staticCompositionLocalOf
+import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.unit.dp
 import kotlinx.coroutines.flow.StateFlow
@@ -31,6 +36,9 @@
 /** The corner radius in px of the current display. */
 val LocalScreenCornerRadius = staticCompositionLocalOf { 0.dp }
 
+/** The screen height in px without accounting for any screen insets (cutouts, status/nav bars) */
+val LocalRawScreenHeight = staticCompositionLocalOf { 0f }
+
 @Composable
 fun ScreenDecorProvider(
     displayCutout: StateFlow<DisplayCutout>,
@@ -39,9 +47,23 @@
 ) {
     val cutout by displayCutout.collectAsState()
     val screenCornerRadiusDp = with(LocalDensity.current) { screenCornerRadius.toDp() }
+
+    val density = LocalDensity.current
+    val navBarHeight =
+        with(density) { WindowInsets.systemBars.asPaddingValues().calculateBottomPadding().toPx() }
+    val statusBarHeight = WindowInsets.systemBars.asPaddingValues().calculateTopPadding()
+    val displayCutoutHeight = WindowInsets.displayCutout.asPaddingValues().calculateTopPadding()
+    val screenHeight =
+        with(density) {
+            (LocalConfiguration.current.screenHeightDp.dp +
+                    maxOf(statusBarHeight, displayCutoutHeight))
+                .toPx()
+        } + navBarHeight
+
     CompositionLocalProvider(
         LocalScreenCornerRadius provides screenCornerRadiusDp,
-        LocalDisplayCutout provides cutout
+        LocalDisplayCutout provides cutout,
+        LocalRawScreenHeight provides screenHeight,
     ) {
         content()
     }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt
index d3e4553..f0d356c 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt
@@ -53,6 +53,11 @@
                 val mediaFrame = carouselController.mediaFrame
                 (mediaFrame.parent as? ViewGroup)?.removeView(mediaFrame)
                 addView(mediaFrame)
+                layoutParams =
+                    FrameLayout.LayoutParams(
+                        FrameLayout.LayoutParams.MATCH_PARENT,
+                        FrameLayout.LayoutParams.MATCH_PARENT,
+                    )
             }
         },
         update = {
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 579e837..985d3a1 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
@@ -24,7 +24,6 @@
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.WindowInsets
 import androidx.compose.foundation.layout.asPaddingValues
-import androidx.compose.foundation.layout.displayCutout
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.offset
@@ -57,7 +56,6 @@
 import androidx.compose.ui.layout.onPlaced
 import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.layout.positionInWindow
-import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.res.dimensionResource
 import androidx.compose.ui.unit.Dp
@@ -67,15 +65,17 @@
 import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.NestedScrollBehavior
 import com.android.compose.animation.scene.SceneScope
+import com.android.compose.animation.scene.SceneTransitionLayoutState
 import com.android.compose.modifiers.height
+import com.android.systemui.common.ui.compose.windowinsets.LocalRawScreenHeight
 import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius
-import com.android.systemui.notifications.ui.composable.Notifications.TransitionThresholds.EXPANSION_FOR_MAX_CORNER_RADIUS
-import com.android.systemui.notifications.ui.composable.Notifications.TransitionThresholds.EXPANSION_FOR_MAX_SCRIM_ALPHA
 import com.android.systemui.res.R
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shade.ui.composable.ShadeHeader
 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimBounds
 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimRounding
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationTransitionThresholds.EXPANSION_FOR_MAX_CORNER_RADIUS
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationTransitionThresholds.EXPANSION_FOR_MAX_SCRIM_ALPHA
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
 import kotlin.math.roundToInt
 
@@ -168,14 +168,7 @@
 
     val navBarHeight =
         with(density) { WindowInsets.systemBars.asPaddingValues().calculateBottomPadding().toPx() }
-    val statusBarHeight = WindowInsets.systemBars.asPaddingValues().calculateTopPadding()
-    val displayCutoutHeight = WindowInsets.displayCutout.asPaddingValues().calculateTopPadding()
-    val screenHeight =
-        with(density) {
-            (LocalConfiguration.current.screenHeightDp.dp +
-                    maxOf(statusBarHeight, displayCutoutHeight))
-                .toPx()
-        } + navBarHeight
+    val screenHeight = LocalRawScreenHeight.current
 
     val stackHeight = viewModel.stackHeight.collectAsState()
 
@@ -253,7 +246,7 @@
                                 scrimCornerRadius,
                                 screenCornerRadius,
                                 { expansionFraction },
-                                layoutState.isTransitioningBetween(Scenes.Gone, Scenes.Shade)
+                                layoutState.isNotificationScrimTransitioning(),
                             )
                             .let { scrimRounding.value.toRoundedCornerShape(it) }
                     clip = true
@@ -288,7 +281,7 @@
                 Modifier.fillMaxSize()
                     .graphicsLayer {
                         alpha =
-                            if (layoutState.isTransitioningBetween(Scenes.Gone, Scenes.Shade)) {
+                            if (layoutState.isNotificationScrimTransitioning()) {
                                 (expansionFraction / EXPANSION_FOR_MAX_SCRIM_ALPHA).coerceAtMost(1f)
                             } else 1f
                     }
@@ -425,7 +418,7 @@
         this
     }
 
-fun ShadeScrimRounding.toRoundedCornerShape(radius: Dp): RoundedCornerShape {
+private fun ShadeScrimRounding.toRoundedCornerShape(radius: Dp): RoundedCornerShape {
     val topRadius = if (isTopRounded) radius else 0.dp
     val bottomRadius = if (isBottomRounded) radius else 0.dp
     return RoundedCornerShape(
@@ -436,6 +429,13 @@
     )
 }
 
+private fun SceneTransitionLayoutState.isNotificationScrimTransitioning(): Boolean {
+    return isTransitioningBetween(Scenes.Gone, Scenes.Shade) ||
+        isTransitioningBetween(Scenes.Lockscreen, Scenes.Shade) ||
+        isTransitioningBetween(Scenes.Gone, Scenes.QuickSettings) ||
+        isTransitioningBetween(Scenes.Lockscreen, Scenes.QuickSettings)
+}
+
 private const val TAG = "FlexiNotifs"
 private val DEBUG_STACK_COLOR = Color(1f, 0f, 0f, 0.2f)
 private val DEBUG_HUN_COLOR = Color(0f, 0f, 1f, 0.2f)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
index a376834..fc32440 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
@@ -38,6 +38,7 @@
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.navigationBars
+import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.wrapContentHeight
 import androidx.compose.foundation.rememberScrollState
@@ -50,19 +51,23 @@
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.CompositingStrategy
 import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.dp
 import com.android.compose.animation.scene.SceneScope
 import com.android.compose.animation.scene.TransitionState
 import com.android.compose.animation.scene.animateSceneFloatAsState
 import com.android.compose.windowsizeclass.LocalWindowSizeClass
 import com.android.systemui.battery.BatteryMeterViewController
+import com.android.systemui.common.ui.compose.windowinsets.LocalRawScreenHeight
 import com.android.systemui.compose.modifiers.sysuiResTag
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.notifications.ui.composable.NotificationScrollingStack
 import com.android.systemui.qs.footer.ui.compose.FooterActionsWithAnimatedVisibility
 import com.android.systemui.qs.ui.viewmodel.QuickSettingsSceneViewModel
 import com.android.systemui.res.R
@@ -72,10 +77,12 @@
 import com.android.systemui.shade.ui.composable.ExpandedShadeHeader
 import com.android.systemui.shade.ui.composable.Shade
 import com.android.systemui.shade.ui.composable.ShadeHeader
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
 import com.android.systemui.statusbar.phone.StatusBarIconController
 import com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconManager
 import com.android.systemui.statusbar.phone.StatusBarLocation
 import javax.inject.Inject
+import kotlin.math.roundToInt
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.stateIn
@@ -87,6 +94,7 @@
 constructor(
     @Application private val applicationScope: CoroutineScope,
     private val viewModel: QuickSettingsSceneViewModel,
+    private val notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel,
     private val tintedIconManagerFactory: TintedIconManager.Factory,
     private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory,
     private val statusBarIconController: StatusBarIconController,
@@ -106,6 +114,7 @@
     ) {
         QuickSettingsScene(
             viewModel = viewModel,
+            notificationsPlaceholderViewModel = notificationsPlaceholderViewModel,
             createTintedIconManager = tintedIconManagerFactory::create,
             createBatteryMeterViewController = batteryMeterViewControllerFactory::create,
             statusBarIconController = statusBarIconController,
@@ -117,6 +126,7 @@
 @Composable
 private fun SceneScope.QuickSettingsScene(
     viewModel: QuickSettingsSceneViewModel,
+    notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel,
     createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager,
     createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController,
     statusBarIconController: StatusBarIconController,
@@ -135,8 +145,17 @@
     )
 
     // TODO(b/280887232): implement the real UI.
-    Box(modifier = modifier.fillMaxSize().graphicsLayer { alpha = contentAlpha }) {
+    Box(
+        modifier =
+            modifier.fillMaxSize().graphicsLayer {
+                // Render the scene to an offscreen buffer so that BlendMode.DstOut only clears this
+                // scene (and not the one under it) during a scene transition.
+                compositingStrategy = CompositingStrategy.Offscreen
+                alpha = contentAlpha
+            }
+    ) {
         val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsState()
+        val screenHeight = LocalRawScreenHeight.current
 
         BackHandler(
             enabled = isCustomizing,
@@ -273,5 +292,11 @@
                 modifier = Modifier.align(Alignment.CenterHorizontally),
             )
         }
+        NotificationScrollingStack(
+            viewModel = notificationsPlaceholderViewModel,
+            maxScrimTop = { screenHeight },
+            modifier =
+                Modifier.fillMaxWidth().offset { IntOffset(x = 0, y = screenHeight.roundToInt()) },
+        )
     }
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
index fe6701c..7af9b7b 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
@@ -37,8 +37,6 @@
 import com.android.compose.animation.scene.MutableSceneTransitionLayoutState
 import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.SceneTransitionLayout
-import com.android.compose.animation.scene.UserAction
-import com.android.compose.animation.scene.UserActionResult
 import com.android.compose.animation.scene.observableTransitionState
 import com.android.systemui.ribbon.ui.composable.BottomRightCornerRibbon
 import com.android.systemui.scene.shared.model.SceneDataSourceDelegator
@@ -71,9 +69,7 @@
 ) {
     val coroutineScope = rememberCoroutineScope()
     val currentSceneKey: SceneKey by viewModel.currentScene.collectAsState()
-    val currentScene = checkNotNull(sceneByKey[currentSceneKey])
-    val currentDestinations: Map<UserAction, UserActionResult> by
-        currentScene.destinationScenes.collectAsState()
+    val currentDestinations by viewModel.currentDestinationScenes(coroutineScope).collectAsState()
     val state: MutableSceneTransitionLayoutState = remember {
         MutableSceneTransitionLayoutState(
             initialScene = currentSceneKey,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToQuickSettingsTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToQuickSettingsTransition.kt
index 5bd1583..851719d 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToQuickSettingsTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToQuickSettingsTransition.kt
@@ -1,12 +1,14 @@
 package com.android.systemui.scene.ui.composable.transitions
 
 import androidx.compose.animation.core.tween
-import com.android.compose.animation.scene.Edge
 import com.android.compose.animation.scene.TransitionBuilder
-import com.android.systemui.scene.shared.model.Scenes
+import kotlin.time.Duration.Companion.milliseconds
 
-fun TransitionBuilder.goneToQuickSettingsTransition() {
-    spec = tween(durationMillis = 500)
-
-    translate(Scenes.QuickSettings.rootElementKey, Edge.Top, true)
+fun TransitionBuilder.goneToQuickSettingsTransition(
+    durationScale: Double = 1.0,
+) {
+    spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt())
+    toQuickSettingsTransition()
 }
+
+private val DefaultDuration = 500.milliseconds
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 9b59708..a0f410a 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
@@ -1,31 +1,14 @@
 package com.android.systemui.scene.ui.composable.transitions
 
 import androidx.compose.animation.core.tween
-import com.android.compose.animation.scene.Edge
 import com.android.compose.animation.scene.TransitionBuilder
-import com.android.systemui.notifications.ui.composable.Notifications
-import com.android.systemui.qs.ui.composable.QuickSettings
-import com.android.systemui.shade.ui.composable.ShadeHeader
 import kotlin.time.Duration.Companion.milliseconds
 
 fun TransitionBuilder.goneToShadeTransition(
     durationScale: Double = 1.0,
 ) {
-    spec = tween(durationMillis = DefaultDuration.times(durationScale).inWholeMilliseconds.toInt())
-
-    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)
+    spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt())
+    toShadeTransition()
 }
 
 private val DefaultDuration = 500.milliseconds
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToQuickSettingsTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToQuickSettingsTransition.kt
index 962d822..319438c 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToQuickSettingsTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToQuickSettingsTransition.kt
@@ -1,12 +1,14 @@
 package com.android.systemui.scene.ui.composable.transitions
 
 import androidx.compose.animation.core.tween
-import com.android.compose.animation.scene.Edge
 import com.android.compose.animation.scene.TransitionBuilder
-import com.android.systemui.scene.shared.model.Scenes
+import kotlin.time.Duration.Companion.milliseconds
 
-fun TransitionBuilder.lockscreenToQuickSettingsTransition() {
-    spec = tween(durationMillis = 500)
-
-    translate(Scenes.QuickSettings.rootElementKey, Edge.Top, true)
+fun TransitionBuilder.lockscreenToQuickSettingsTransition(
+    durationScale: Double = 1.0,
+) {
+    spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt())
+    toQuickSettingsTransition()
 }
+
+private val DefaultDuration = 500.milliseconds
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToShadeTransition.kt
index 48ab68a..f078b8c 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToShadeTransition.kt
@@ -2,23 +2,13 @@
 
 import androidx.compose.animation.core.tween
 import com.android.compose.animation.scene.TransitionBuilder
-import com.android.systemui.notifications.ui.composable.Notifications
-import com.android.systemui.qs.ui.composable.QuickSettings
-import com.android.systemui.shade.ui.composable.Shade
-import com.android.systemui.shade.ui.composable.ShadeHeader
 import kotlin.time.Duration.Companion.milliseconds
 
 fun TransitionBuilder.lockscreenToShadeTransition(
     durationScale: Double = 1.0,
 ) {
-    spec = tween(durationMillis = DefaultDuration.times(durationScale).inWholeMilliseconds.toInt())
-
-    fractionRange(end = 0.5f) { fade(Shade.Elements.BackgroundScrim) }
-    translate(QuickSettings.Elements.Content, y = -ShadeHeader.Dimensions.CollapsedHeight * .66f)
-    fractionRange(start = 0.5f) {
-        fade(QuickSettings.Elements.Content)
-        fade(Notifications.Elements.NotificationScrim)
-    }
+    spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt())
+    toShadeTransition()
 }
 
 private val DefaultDuration = 500.milliseconds
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromShadeToQuickSettingsTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromShadeToQuickSettingsTransition.kt
index ffb6f31..a9e5be9 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromShadeToQuickSettingsTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromShadeToQuickSettingsTransition.kt
@@ -6,9 +6,12 @@
 import com.android.systemui.notifications.ui.composable.Notifications
 import com.android.systemui.qs.ui.composable.QuickSettings
 import com.android.systemui.shade.ui.composable.ShadeHeader
+import kotlin.time.Duration.Companion.milliseconds
 
-fun TransitionBuilder.shadeToQuickSettingsTransition() {
-    spec = tween(durationMillis = 500)
+fun TransitionBuilder.shadeToQuickSettingsTransition(
+    durationScale: Double = 1.0,
+) {
+    spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt())
 
     translate(Notifications.Elements.NotificationScrim, Edge.Bottom)
     timestampRange(endMillis = 83) { fade(QuickSettings.Elements.FooterActions) }
@@ -24,9 +27,15 @@
     )
     translate(ShadeHeader.Elements.ShadeCarrierGroup, y = -ShadeHeader.Dimensions.CollapsedHeight)
 
-    fractionRange(end = .14f) { fade(ShadeHeader.Elements.CollapsedContentStart) }
-    fractionRange(end = .14f) { fade(ShadeHeader.Elements.CollapsedContentEnd) }
+    fractionRange(end = .14f) {
+        fade(ShadeHeader.Elements.CollapsedContentStart)
+        fade(ShadeHeader.Elements.CollapsedContentEnd)
+    }
 
-    fractionRange(start = .58f) { fade(ShadeHeader.Elements.ExpandedContent) }
-    fractionRange(start = .58f) { fade(ShadeHeader.Elements.ShadeCarrierGroup) }
+    fractionRange(start = .58f) {
+        fade(ShadeHeader.Elements.ExpandedContent)
+        fade(ShadeHeader.Elements.ShadeCarrierGroup)
+    }
 }
+
+private val DefaultDuration = 500.milliseconds
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsTransition.kt
new file mode 100644
index 0000000..e0a6310
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsTransition.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene.ui.composable.transitions
+
+import androidx.compose.animation.core.tween
+import com.android.compose.animation.scene.Edge
+import com.android.compose.animation.scene.TransitionBuilder
+import com.android.systemui.notifications.ui.composable.Notifications
+import com.android.systemui.qs.ui.composable.QuickSettings
+import com.android.systemui.shade.ui.composable.ShadeHeader
+import kotlin.time.Duration.Companion.milliseconds
+
+fun TransitionBuilder.toQuickSettingsTransition(
+    durationScale: Double = 1.0,
+) {
+    spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt())
+
+    translate(
+        ShadeHeader.Elements.ExpandedContent,
+        y = -(ShadeHeader.Dimensions.ExpandedHeight - ShadeHeader.Dimensions.CollapsedHeight)
+    )
+    translate(ShadeHeader.Elements.Clock, y = -ShadeHeader.Dimensions.CollapsedHeight)
+    translate(ShadeHeader.Elements.ShadeCarrierGroup, y = -ShadeHeader.Dimensions.CollapsedHeight)
+
+    fractionRange(start = .58f) {
+        fade(ShadeHeader.Elements.ExpandedContent)
+        fade(ShadeHeader.Elements.Clock)
+        fade(ShadeHeader.Elements.ShadeCarrierGroup)
+    }
+
+    translate(QuickSettings.Elements.Content, y = -ShadeHeader.Dimensions.ExpandedHeight * .66f)
+    translate(Notifications.Elements.NotificationScrim, Edge.Top, false)
+}
+
+private val DefaultDuration = 500.milliseconds
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt
new file mode 100644
index 0000000..2f59217
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene.ui.composable.transitions
+
+import androidx.compose.animation.core.tween
+import com.android.compose.animation.scene.Edge
+import com.android.compose.animation.scene.TransitionBuilder
+import com.android.systemui.notifications.ui.composable.Notifications
+import com.android.systemui.qs.ui.composable.QuickSettings
+import com.android.systemui.shade.ui.composable.ShadeHeader
+import kotlin.time.Duration.Companion.milliseconds
+
+fun TransitionBuilder.toShadeTransition(
+    durationScale: Double = 1.0,
+) {
+    spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt())
+
+    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)
+}
+
+private val DefaultDuration = 500.milliseconds
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
index c6c6f57..516e140 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
@@ -531,8 +531,14 @@
             state.currentScene == Scenes.QuickSettings
         }
         is TransitionState.Transition -> {
-            (state.isTransitioning(Scenes.Shade, Scenes.QuickSettings) && state.progress >= 0.5) ||
-                (state.isTransitioning(Scenes.QuickSettings, Scenes.Shade) && state.progress < 0.5)
+            ((state.isTransitioning(Scenes.Shade, Scenes.QuickSettings) ||
+                state.isTransitioning(Scenes.Gone, Scenes.QuickSettings) ||
+                state.isTransitioning(Scenes.Lockscreen, Scenes.QuickSettings)) &&
+                state.progress >= 0.5) ||
+                ((state.isTransitioning(Scenes.QuickSettings, Scenes.Shade) ||
+                    state.isTransitioning(Scenes.QuickSettings, Scenes.Gone) ||
+                    state.isTransitioning(Scenes.QuickSettings, Scenes.Lockscreen)) &&
+                    state.progress <= 0.5)
         }
     }
 }
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 84b1a4b..944d6ef 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
@@ -47,6 +47,7 @@
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.CompositingStrategy
 import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.layout.layout
@@ -200,13 +201,15 @@
         animateSceneFloatAsState(value = 1f, key = QuickSettings.SharedValues.TilesSquishiness)
     val isClickable by viewModel.isClickable.collectAsState()
 
-    Box(
-        modifier =
-            modifier
-                .element(Shade.Elements.BackgroundScrim)
-                .background(colorResource(R.color.shade_scrim_background_dark)),
-    )
-    Box {
+    // Render the scene to an offscreen buffer so that BlendMode.DstOut only clears this scene
+    // (and not the one under it) during a scene transition.
+    Box(modifier = modifier.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)) {
+        Box(
+            modifier =
+                Modifier.fillMaxSize()
+                    .element(Shade.Elements.BackgroundScrim)
+                    .background(colorResource(R.color.shade_scrim_background_dark)),
+        )
         Layout(
             contents =
                 listOf(
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
index a8a1d88..f4009ee 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
@@ -300,14 +300,16 @@
     val fromScene = transition.fromScene
     val toScene = transition.toScene
 
-    val chosenByPicker =
+    val pickedScene =
         scenePicker.sceneDuringTransition(
             element = element,
             transition = transition,
             fromSceneZIndex = layoutImpl.scenes.getValue(fromScene).zIndex,
             toSceneZIndex = layoutImpl.scenes.getValue(toScene).zIndex,
-        ) == scene
-    return chosenByPicker || transition.currentOverscrollSpec?.scene == scene
+        )
+            ?: return false
+
+    return pickedScene == scene || transition.currentOverscrollSpec?.scene == scene
 }
 
 private fun isSharedElementEnabled(
@@ -356,7 +358,9 @@
     val toState = element.sceneStates[toScene]
 
     if (fromState == null && toState == null) {
-        error("This should not happen, element $element is neither in $fromScene or $toScene")
+        // TODO(b/311600838): Throw an exception instead once layers of disposed elements are not
+        // run anymore.
+        return true
     }
 
     val isSharedElement = fromState != null && toState != null
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 418c6bb..7fb5a4d 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
@@ -59,7 +59,10 @@
     ): Map<UserAction, UserActionResult> {
         userActions.forEach { (action, result) ->
             if (key == result.toScene) {
-                error("Transition to the same scene is not supported. Scene $key, action $action")
+                error(
+                    "Transition to the same scene is not supported. Scene $key, action $action," +
+                        " result $result"
+                )
             }
         }
         return userActions
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
index 2c109a3..6bc397e 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
@@ -205,7 +205,9 @@
 interface ElementScenePicker {
     /**
      * Return the scene in which [element] should be drawn (when using `Modifier.element(key)`) or
-     * composed (when using `MovableElement(key)`) during the given [transition].
+     * composed (when using `MovableElement(key)`) during the given [transition]. If this element
+     * should not be drawn or composed in neither [transition.fromScene] nor [transition.toScene],
+     * return `null`.
      *
      * Important: For [MovableElements][SceneScope.MovableElement], this scene picker will *always*
      * be used during transitions to decide whether we should compose that element in a given scene
@@ -217,12 +219,13 @@
         transition: TransitionState.Transition,
         fromSceneZIndex: Float,
         toSceneZIndex: Float,
-    ): SceneKey
+    ): SceneKey?
 
     /**
      * Return [transition.fromScene] if it is in [scenes] and [transition.toScene] is not, or return
-     * [transition.toScene] if it is in [scenes] and [transition.fromScene] is not, otherwise throw
-     * an exception (i.e. if neither or both of fromScene and toScene are in [scenes]).
+     * [transition.toScene] if it is in [scenes] and [transition.fromScene] is not. If neither
+     * [transition.fromScene] and [transition.toScene] are in [scenes], return `null`. If both
+     * [transition.fromScene] and [transition.toScene] are in [scenes], throw an exception.
      *
      * This function can be useful when computing the scene in which a movable element should be
      * composed.
@@ -231,31 +234,22 @@
         scenes: Set<SceneKey>,
         transition: TransitionState.Transition,
         element: ElementKey,
-    ): SceneKey {
+    ): SceneKey? {
         val fromScene = transition.fromScene
         val toScene = transition.toScene
         val fromSceneInScenes = scenes.contains(fromScene)
         val toSceneInScenes = scenes.contains(toScene)
-        if (fromSceneInScenes && toSceneInScenes) {
-            error(
-                "Element $element can be in both $fromScene and $toScene. You should add a " +
-                    "special case for this transition before calling pickSingleSceneIn()."
-            )
-        }
 
-        if (!fromSceneInScenes && !toSceneInScenes) {
-            error(
-                "Element $element can be neither in $fromScene and $toScene. This either means " +
-                    "that you should add one of them in the scenes set passed to " +
-                    "pickSingleSceneIn(), or there is an internal error and this element was " +
-                    "composed when it shouldn't be."
-            )
-        }
-
-        return if (fromSceneInScenes) {
-            fromScene
-        } else {
-            toScene
+        return when {
+            fromSceneInScenes && toSceneInScenes -> {
+                error(
+                    "Element $element can be in both $fromScene and $toScene. You should add a " +
+                        "special case for this transition before calling pickSingleSceneIn()."
+                )
+            }
+            fromSceneInScenes -> fromScene
+            toSceneInScenes -> toScene
+            else -> null
         }
     }
 }
@@ -312,8 +306,12 @@
         transition: TransitionState.Transition,
         fromSceneZIndex: Float,
         toSceneZIndex: Float,
-    ): SceneKey {
-        return if (scenes.contains(transition.toScene)) transition.toScene else transition.fromScene
+    ): SceneKey? {
+        return when {
+            scenes.contains(transition.toScene) -> transition.toScene
+            scenes.contains(transition.fromScene) -> transition.fromScene
+            else -> null
+        }
     }
 }
 
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementScenePickerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementScenePickerTest.kt
index fb46a34..6745fbe 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementScenePickerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementScenePickerTest.kt
@@ -38,8 +38,8 @@
     }
 
     @Test
-    fun toSceneNotInScenes() {
-        val picker = MovableElementScenePicker(scenes = emptySet())
+    fun fromSceneInScenes() {
+        val picker = MovableElementScenePicker(scenes = setOf(TestScenes.SceneA))
         assertThat(
                 picker.sceneDuringTransition(
                     TestElements.Foo,
@@ -50,4 +50,18 @@
             )
             .isEqualTo(TestScenes.SceneA)
     }
+
+    @Test
+    fun noneInScenes() {
+        val picker = MovableElementScenePicker(scenes = emptySet())
+        assertThat(
+                picker.sceneDuringTransition(
+                    TestElements.Foo,
+                    transition(from = TestScenes.SceneA, to = TestScenes.SceneB),
+                    fromSceneZIndex = 0f,
+                    toSceneZIndex = 1f,
+                )
+            )
+            .isEqualTo(null)
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
index 37a6ac6..af48802 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
@@ -25,6 +25,9 @@
 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.biometrics.data.repository.fakeFingerprintPropertyRepository
+import com.android.systemui.bouncer.data.repository.keyguardBouncerRepository
+import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
@@ -40,11 +43,16 @@
 import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason.UserLockdown
 import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.flags.fakeSystemPropertiesHelper
+import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
+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.fakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.data.repository.fakeTrustRepository
 import com.android.systemui.keyguard.shared.model.AuthenticationFlags
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.scene.domain.interactor.sceneInteractor
@@ -371,6 +379,42 @@
         }
 
     @Test
+    fun showOrUnlockDevice_noAlternateBouncer_switchesToBouncerScene() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene)
+            switchToScene(Scenes.Lockscreen)
+            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+
+            kosmos.fakeFingerprintPropertyRepository.supportsRearFps() // altBouncer unsupported
+            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin
+            )
+            runCurrent()
+
+            underTest.attemptDeviceEntry()
+
+            assertThat(currentScene).isEqualTo(Scenes.Bouncer)
+        }
+
+    @Test
+    fun showOrUnlockDevice_showsAlternateBouncer_staysOnLockscreenScene() =
+        testScope.runTest {
+            val currentScene by collectLastValue(sceneInteractor.currentScene)
+            switchToScene(Scenes.Lockscreen)
+            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+
+            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin
+            )
+            givenCanShowAlternateBouncer()
+            runCurrent()
+
+            underTest.attemptDeviceEntry()
+
+            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+        }
+
+    @Test
     fun isBypassEnabled_disabledInRepository_false() =
         testScope.runTest {
             kosmos.fakeDeviceEntryRepository.setBypassEnabled(false)
@@ -593,4 +637,20 @@
     private fun switchToScene(sceneKey: SceneKey) {
         sceneInteractor.changeScene(sceneKey, "reason")
     }
+
+    private suspend fun givenCanShowAlternateBouncer() {
+        val canShowAlternateBouncer by
+            testScope.collectLastValue(kosmos.alternateBouncerInteractor.canShowAlternateBouncer)
+        kosmos.fakeFingerprintPropertyRepository.supportsUdfps()
+        kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
+            from = KeyguardState.GONE,
+            to = KeyguardState.LOCKSCREEN,
+            testScheduler = testScope.testScheduler,
+        )
+        kosmos.deviceEntryFingerprintAuthRepository.setLockedOut(false)
+        kosmos.biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
+        kosmos.fakeKeyguardRepository.setKeyguardDismissible(false)
+        kosmos.keyguardBouncerRepository.setPrimaryShow(false)
+        assertThat(canShowAlternateBouncer).isTrue()
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt
index f685058..e39511f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt
@@ -148,37 +148,6 @@
         }
 
     @Test
-    fun addMediaDataLoadingState() =
-        testScope.runTest {
-            val mediaDataLoadedStates by collectLastValue(underTest.mediaDataLoadedStates)
-            val instanceId = InstanceId.fakeInstanceId(123)
-            val mediaLoadedStates = mutableListOf(MediaDataLoadingModel.Loaded(instanceId))
-
-            underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId))
-
-            assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStates)
-
-            mediaLoadedStates.remove(MediaDataLoadingModel.Loaded(instanceId))
-
-            underTest.addMediaDataLoadingState(MediaDataLoadingModel.Removed(instanceId))
-
-            assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStates)
-        }
-
-    @Test
-    fun setRecommendationsLoadingState() =
-        testScope.runTest {
-            val recommendationsLoadingState by
-                collectLastValue(underTest.recommendationsLoadingState)
-            val recommendationsLoadingModel =
-                SmartspaceMediaLoadingModel.Loaded(KEY_MEDIA_SMARTSPACE)
-
-            underTest.setRecommendationsLoadingState(recommendationsLoadingModel)
-
-            assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel)
-        }
-
-    @Test
     fun addMediaControlPlayingThenRemote() =
         testScope.runTest {
             val sortedMedia by collectLastValue(underTest.sortedMedia)
@@ -195,9 +164,10 @@
             assertThat(sortedMedia?.size).isEqualTo(2)
             assertThat(sortedMedia?.values)
                 .containsExactly(
-                    MediaCommonModel.MediaControl(playingInstanceId),
-                    MediaCommonModel.MediaControl(remoteInstanceId)
+                    MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(playingInstanceId)),
+                    MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(remoteInstanceId))
                 )
+                .inOrder()
         }
 
     @Test
@@ -217,8 +187,8 @@
             assertThat(sortedMedia?.size).isEqualTo(2)
             assertThat(sortedMedia?.values)
                 .containsExactly(
-                    MediaCommonModel.MediaControl(playingInstanceId1),
-                    MediaCommonModel.MediaControl(playingInstanceId2)
+                    MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(playingInstanceId1)),
+                    MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(playingInstanceId2))
                 )
                 .inOrder()
 
@@ -233,8 +203,8 @@
             assertThat(sortedMedia?.size).isEqualTo(2)
             assertThat(sortedMedia?.values)
                 .containsExactly(
-                    MediaCommonModel.MediaControl(playingInstanceId2),
-                    MediaCommonModel.MediaControl(playingInstanceId1)
+                    MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(playingInstanceId2)),
+                    MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(playingInstanceId1))
                 )
                 .inOrder()
         }
@@ -285,12 +255,14 @@
             assertThat(sortedMedia?.size).isEqualTo(6)
             assertThat(sortedMedia?.values)
                 .containsExactly(
-                    MediaCommonModel.MediaControl(instanceId1),
-                    MediaCommonModel.MediaControl(instanceId2),
-                    MediaCommonModel.MediaRecommendations(KEY_MEDIA_SMARTSPACE),
-                    MediaCommonModel.MediaControl(instanceId4),
-                    MediaCommonModel.MediaControl(instanceId3),
-                    MediaCommonModel.MediaControl(instanceId5),
+                    MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(instanceId1)),
+                    MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(instanceId2)),
+                    MediaCommonModel.MediaRecommendations(
+                        SmartspaceMediaLoadingModel.Loaded(KEY_MEDIA_SMARTSPACE, true)
+                    ),
+                    MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(instanceId4)),
+                    MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(instanceId3)),
+                    MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(instanceId5)),
                 )
                 .inOrder()
         }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt
index c15776e..a2991fd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt
@@ -20,7 +20,6 @@
 import android.graphics.drawable.Icon
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.internal.logging.InstanceId
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.flags.Flags
@@ -29,18 +28,14 @@
 import com.android.systemui.media.controls.MediaTestHelper
 import com.android.systemui.media.controls.data.repository.MediaFilterRepository
 import com.android.systemui.media.controls.data.repository.mediaFilterRepository
-import com.android.systemui.media.controls.domain.pipeline.MediaDataFilterImpl
 import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor
 import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor
-import com.android.systemui.media.controls.domain.pipeline.mediaDataFilter
 import com.android.systemui.media.controls.shared.model.MediaCommonModel
 import com.android.systemui.media.controls.shared.model.MediaData
 import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel
 import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
 import com.android.systemui.media.controls.shared.model.SmartspaceMediaLoadingModel
-import com.android.systemui.statusbar.notificationLockscreenUserManager
 import com.android.systemui.testKosmos
-import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -54,8 +49,6 @@
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
 
-    private val mediaDataFilter: MediaDataFilterImpl = kosmos.mediaDataFilter
-    private val notificationLockscreenUserManager = kosmos.notificationLockscreenUserManager
     private val mediaFilterRepository: MediaFilterRepository = kosmos.mediaFilterRepository
 
     private val underTest: MediaCarouselInteractor = kosmos.mediaCarouselInteractor
@@ -95,7 +88,6 @@
                 collectLastValue(underTest.hasActiveMediaOrRecommendation)
             val hasActiveMedia by collectLastValue(underTest.hasActiveMedia)
             val hasAnyMedia by collectLastValue(underTest.hasAnyMedia)
-            val sortedMedia by collectLastValue(underTest.sortedMedia)
 
             val userMedia = MediaData(active = false)
             val instanceId = userMedia.instanceId
@@ -106,7 +98,6 @@
             assertThat(hasActiveMediaOrRecommendation).isFalse()
             assertThat(hasActiveMedia).isFalse()
             assertThat(hasAnyMedia).isTrue()
-            assertThat(sortedMedia).containsExactly(MediaCommonModel.MediaControl(instanceId))
 
             assertThat(mediaFilterRepository.removeSelectedUserMediaEntry(instanceId, userMedia))
                 .isTrue()
@@ -117,7 +108,6 @@
             assertThat(hasActiveMediaOrRecommendation).isFalse()
             assertThat(hasActiveMedia).isFalse()
             assertThat(hasAnyMedia).isFalse()
-            assertThat(sortedMedia).isEmpty()
         }
 
     @Test
@@ -138,28 +128,26 @@
                     recommendations = MediaTestHelper.getValidRecommendationList(icon),
                 )
             val userMedia = MediaData(active = false)
+            val recsLoadingModel = SmartspaceMediaLoadingModel.Loaded(KEY_MEDIA_SMARTSPACE, true)
+            val mediaLoadingModel = MediaDataLoadingModel.Loaded(userMedia.instanceId)
 
             mediaFilterRepository.setRecommendation(userMediaRecommendation)
-            mediaFilterRepository.setRecommendationsLoadingState(
-                SmartspaceMediaLoadingModel.Loaded(KEY_MEDIA_SMARTSPACE, true)
-            )
+            mediaFilterRepository.setRecommendationsLoadingState(recsLoadingModel)
 
             assertThat(hasActiveMediaOrRecommendation).isTrue()
             assertThat(hasAnyMediaOrRecommendation).isTrue()
             assertThat(sortedMedia)
-                .containsExactly(MediaCommonModel.MediaRecommendations(KEY_MEDIA_SMARTSPACE))
+                .containsExactly(MediaCommonModel.MediaRecommendations(recsLoadingModel))
 
             mediaFilterRepository.addSelectedUserMediaEntry(userMedia)
-            mediaFilterRepository.addMediaDataLoadingState(
-                MediaDataLoadingModel.Loaded(userMedia.instanceId)
-            )
+            mediaFilterRepository.addMediaDataLoadingState(mediaLoadingModel)
 
             assertThat(hasActiveMediaOrRecommendation).isTrue()
             assertThat(hasAnyMediaOrRecommendation).isTrue()
             assertThat(sortedMedia)
                 .containsExactly(
-                    MediaCommonModel.MediaRecommendations(KEY_MEDIA_SMARTSPACE),
-                    MediaCommonModel.MediaControl(userMedia.instanceId)
+                    MediaCommonModel.MediaRecommendations(recsLoadingModel),
+                    MediaCommonModel.MediaControl(mediaLoadingModel, true)
                 )
                 .inOrder()
         }
@@ -238,80 +226,7 @@
     fun hasActiveMediaOrRecommendation_nothingSet_returnsFalse() =
         testScope.runTest { assertThat(underTest.hasActiveMediaOrRecommendation.value).isFalse() }
 
-    @Test
-    fun onMediaDataUpdated_updatesLoadingState() =
-        testScope.runTest {
-            whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true)
-            whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true)
-            val mediaDataLoadedStates by collectLastValue(underTest.mediaDataLoadedStates)
-            val instanceId = InstanceId.fakeInstanceId(123)
-            val mediaLoadedStates: MutableList<MediaDataLoadingModel> = mutableListOf()
-
-            mediaLoadedStates.add(MediaDataLoadingModel.Loaded(instanceId))
-            mediaDataFilter.onMediaDataLoaded(
-                KEY,
-                KEY,
-                MediaData(userId = USER_ID, instanceId = instanceId)
-            )
-
-            assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStates)
-
-            val newInstanceId = InstanceId.fakeInstanceId(321)
-
-            mediaLoadedStates.add(MediaDataLoadingModel.Loaded(newInstanceId))
-            mediaDataFilter.onMediaDataLoaded(
-                KEY_2,
-                KEY_2,
-                MediaData(userId = USER_ID, instanceId = newInstanceId)
-            )
-
-            assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStates)
-
-            mediaLoadedStates.remove(MediaDataLoadingModel.Loaded(instanceId))
-
-            mediaDataFilter.onMediaDataRemoved(KEY)
-
-            assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStates)
-
-            mediaLoadedStates.remove(MediaDataLoadingModel.Loaded(newInstanceId))
-
-            mediaDataFilter.onMediaDataRemoved(KEY_2)
-
-            assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStates)
-        }
-
-    @Test
-    fun onMediaRecommendationsUpdated_updatesLoadingState() =
-        testScope.runTest {
-            whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true)
-            whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true)
-            val recommendationsLoadingState by
-                collectLastValue(underTest.recommendationsLoadingState)
-            val icon = Icon.createWithResource(context, R.drawable.ic_media_play)
-            val mediaRecommendations =
-                SmartspaceMediaData(
-                    targetId = KEY_MEDIA_SMARTSPACE,
-                    isActive = true,
-                    recommendations = MediaTestHelper.getValidRecommendationList(icon),
-                )
-            var recommendationsLoadingModel: SmartspaceMediaLoadingModel =
-                SmartspaceMediaLoadingModel.Loaded(KEY_MEDIA_SMARTSPACE, isPrioritized = true)
-
-            mediaDataFilter.onSmartspaceMediaDataLoaded(KEY_MEDIA_SMARTSPACE, mediaRecommendations)
-
-            assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel)
-
-            recommendationsLoadingModel = SmartspaceMediaLoadingModel.Removed(KEY_MEDIA_SMARTSPACE)
-
-            mediaDataFilter.onSmartspaceMediaDataRemoved(KEY_MEDIA_SMARTSPACE)
-
-            assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel)
-        }
-
     companion object {
-        private const val KEY = "key"
-        private const val KEY_2 = "key2"
-        private const val USER_ID = 0
         private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID"
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/util/MediaDiffUtilTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/util/MediaDiffUtilTest.kt
new file mode 100644
index 0000000..4a9d0d1
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/util/MediaDiffUtilTest.kt
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui.util
+
+import androidx.recyclerview.widget.DiffUtil
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.InstanceId
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.media.controls.ui.viewmodel.MediaCommonViewModel
+import com.android.systemui.media.controls.ui.viewmodel.mediaControlViewModel
+import com.android.systemui.media.controls.ui.viewmodel.mediaRecommendationsViewModel
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class MediaDiffUtilTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+
+    @Test
+    fun newMediaControlAdded() {
+        val mediaControl = createMediaControl(InstanceId.fakeInstanceId(123), true)
+        val oldList = listOf<MediaCommonViewModel>()
+        val newList = listOf(mediaControl)
+        val mediaLoadedCallback = MediaViewModelCallback(oldList, newList)
+        val mediaLoadedListUpdateCallback =
+            MediaViewModelListUpdateCallback(
+                oldList,
+                newList,
+                { commonViewModel -> assertThat(commonViewModel).isEqualTo(mediaControl) },
+                { fail("Unexpected to update $it") },
+                { fail("Unexpected to remove $it") },
+                { commonViewModel, _, _ -> fail("Unexpected to move $commonViewModel ") },
+            )
+
+        DiffUtil.calculateDiff(mediaLoadedCallback).dispatchUpdatesTo(mediaLoadedListUpdateCallback)
+    }
+
+    @Test
+    fun newMediaRecommendationsAdded() {
+        val mediaRecs = createMediaRecommendations(KEY_MEDIA_SMARTSPACE, true)
+        val oldList = listOf<MediaCommonViewModel>()
+        val newList = listOf(mediaRecs)
+        val mediaLoadedCallback = MediaViewModelCallback(oldList, newList)
+        val mediaLoadedListUpdateCallback =
+            MediaViewModelListUpdateCallback(
+                oldList,
+                newList,
+                { commonViewModel -> assertThat(commonViewModel).isEqualTo(mediaRecs) },
+                { fail("Unexpected to update $it") },
+                { fail("Unexpected to remove $it") },
+                { commonViewModel, _, _ -> fail("Unexpected to move $commonViewModel ") },
+            )
+
+        DiffUtil.calculateDiff(mediaLoadedCallback).dispatchUpdatesTo(mediaLoadedListUpdateCallback)
+    }
+
+    @Test
+    fun updateMediaControl_contentChanged() {
+        val mediaControl = createMediaControl(InstanceId.fakeInstanceId(123), true)
+        val oldList = listOf(mediaControl)
+        val newList = listOf(mediaControl.copy(immediatelyUpdateUi = false))
+        val mediaLoadedCallback = MediaViewModelCallback(oldList, newList)
+        val mediaLoadedListUpdateCallback =
+            MediaViewModelListUpdateCallback(
+                oldList,
+                newList,
+                { fail("Unexpected to add $it") },
+                { commonViewModel -> assertThat(commonViewModel).isNotEqualTo(mediaControl) },
+                { fail("Unexpected to remove $it") },
+                { commonViewModel, _, _ -> fail("Unexpected to move $commonViewModel ") },
+            )
+
+        DiffUtil.calculateDiff(mediaLoadedCallback).dispatchUpdatesTo(mediaLoadedListUpdateCallback)
+    }
+
+    @Test
+    fun updateMediaRecommendations_contentChanged() {
+        val mediaRecs = createMediaRecommendations(KEY_MEDIA_SMARTSPACE, true)
+        val oldList = listOf(mediaRecs)
+        val newList = listOf(mediaRecs.copy(key = KEY_MEDIA_SMARTSPACE_2))
+        val mediaLoadedCallback = MediaViewModelCallback(oldList, newList)
+        val mediaLoadedListUpdateCallback =
+            MediaViewModelListUpdateCallback(
+                oldList,
+                newList,
+                { fail("Unexpected to add $it") },
+                { commonViewModel -> assertThat(commonViewModel).isNotEqualTo(mediaRecs) },
+                { fail("Unexpected to remove $it") },
+                { commonViewModel, _, _ -> fail("Unexpected to move $commonViewModel ") },
+            )
+
+        DiffUtil.calculateDiff(mediaLoadedCallback).dispatchUpdatesTo(mediaLoadedListUpdateCallback)
+    }
+
+    @Test
+    fun mediaControlMoved() {
+        val mediaControl1 = createMediaControl(InstanceId.fakeInstanceId(123), true)
+        val mediaControl2 = createMediaControl(InstanceId.fakeInstanceId(456), false)
+        val oldList = listOf(mediaControl1, mediaControl2)
+        val newList = listOf(mediaControl2, mediaControl1)
+        val mediaLoadedCallback = MediaViewModelCallback(oldList, newList)
+        val mediaLoadedListUpdateCallback =
+            MediaViewModelListUpdateCallback(
+                oldList,
+                newList,
+                { fail("Unexpected to add $it") },
+                { fail("Unexpected to update $it") },
+                { fail("Unexpected to remove $it") },
+                { commonViewModel, _, _ -> assertThat(commonViewModel).isEqualTo(mediaControl1) },
+            )
+
+        DiffUtil.calculateDiff(mediaLoadedCallback).dispatchUpdatesTo(mediaLoadedListUpdateCallback)
+    }
+
+    @Test
+    fun mediaRecommendationsMoved() {
+        val mediaControl1 = createMediaControl(InstanceId.fakeInstanceId(123), true)
+        val mediaControl2 = createMediaControl(InstanceId.fakeInstanceId(456), false)
+        val mediaRecs = createMediaRecommendations(KEY_MEDIA_SMARTSPACE, true)
+        val oldList = listOf(mediaRecs, mediaControl1, mediaControl2)
+        val newList = listOf(mediaControl1, mediaControl2, mediaRecs)
+        val mediaLoadedCallback = MediaViewModelCallback(oldList, newList)
+        val mediaLoadedListUpdateCallback =
+            MediaViewModelListUpdateCallback(
+                oldList,
+                newList,
+                { fail("Unexpected to add $it") },
+                { fail("Unexpected to update $it") },
+                { fail("Unexpected to remove $it") },
+                { commonViewModel, _, _ -> assertThat(commonViewModel).isEqualTo(mediaRecs) },
+            )
+
+        DiffUtil.calculateDiff(mediaLoadedCallback).dispatchUpdatesTo(mediaLoadedListUpdateCallback)
+    }
+
+    @Test
+    fun mediaControlRemoved() {
+        val mediaControl = createMediaControl(InstanceId.fakeInstanceId(123), true)
+        val oldList = listOf(mediaControl)
+        val newList = listOf<MediaCommonViewModel>()
+        val mediaLoadedCallback = MediaViewModelCallback(oldList, newList)
+        val mediaLoadedListUpdateCallback =
+            MediaViewModelListUpdateCallback(
+                oldList,
+                newList,
+                { fail("Unexpected to add $it") },
+                { fail("Unexpected to update $it") },
+                { commonViewModel -> assertThat(commonViewModel).isEqualTo(mediaControl) },
+                { commonViewModel, _, _ -> fail("Unexpected to move $commonViewModel ") },
+            )
+
+        DiffUtil.calculateDiff(mediaLoadedCallback).dispatchUpdatesTo(mediaLoadedListUpdateCallback)
+    }
+
+    @Test
+    fun mediaRecommendationsRemoved() {
+        val mediaRecs = createMediaRecommendations(KEY_MEDIA_SMARTSPACE_2, false)
+        val oldList = listOf(mediaRecs)
+        val newList = listOf<MediaCommonViewModel>()
+        val mediaLoadedCallback = MediaViewModelCallback(oldList, newList)
+        val mediaLoadedListUpdateCallback =
+            MediaViewModelListUpdateCallback(
+                oldList,
+                newList,
+                { fail("Unexpected to add $it") },
+                { fail("Unexpected to update $it") },
+                { commonViewModel -> assertThat(commonViewModel).isEqualTo(mediaRecs) },
+                { commonViewModel, _, _ -> fail("Unexpected to move $commonViewModel ") },
+            )
+
+        DiffUtil.calculateDiff(mediaLoadedCallback).dispatchUpdatesTo(mediaLoadedListUpdateCallback)
+    }
+
+    private fun createMediaControl(
+        instanceId: InstanceId,
+        immediatelyUpdateUi: Boolean,
+    ): MediaCommonViewModel.MediaControl {
+        return MediaCommonViewModel.MediaControl(
+            instanceId = instanceId,
+            immediatelyUpdateUi = immediatelyUpdateUi,
+            controlViewModel = kosmos.mediaControlViewModel,
+            onAdded = {},
+            onRemoved = { _, _ -> },
+            onUpdated = {}
+        )
+    }
+
+    private fun createMediaRecommendations(
+        key: String,
+        loadingEnabled: Boolean,
+    ): MediaCommonViewModel.MediaRecommendations {
+        return MediaCommonViewModel.MediaRecommendations(
+            key = key,
+            loadingEnabled = loadingEnabled,
+            recsViewModel = kosmos.mediaRecommendationsViewModel,
+            onAdded = {},
+            onRemoved = { _, _ -> },
+            onUpdated = {}
+        )
+    }
+
+    companion object {
+        private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID"
+        private const val KEY_MEDIA_SMARTSPACE_2 = "MEDIA_SMARTSPACE_ID_2"
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModelTest.kt
new file mode 100644
index 0000000..4b5fecd
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModelTest.kt
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui.viewmodel
+
+import android.R
+import android.content.packageManager
+import android.content.pm.ApplicationInfo
+import android.graphics.drawable.Icon
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.InstanceId
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.Flags
+import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.media.controls.MediaTestHelper
+import com.android.systemui.media.controls.domain.pipeline.MediaDataFilterImpl
+import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor
+import com.android.systemui.media.controls.domain.pipeline.mediaDataFilter
+import com.android.systemui.media.controls.shared.model.MediaData
+import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
+import com.android.systemui.statusbar.notificationLockscreenUserManager
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class MediaCarouselViewModelTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+
+    private val mediaDataFilter: MediaDataFilterImpl = kosmos.mediaDataFilter
+    private val notificationLockscreenUserManager = kosmos.notificationLockscreenUserManager
+    private val packageManager = kosmos.packageManager
+    private val icon = Icon.createWithResource(context, R.drawable.ic_media_play)
+    private val drawable = context.getDrawable(R.drawable.ic_media_play)
+    private val smartspaceMediaData: SmartspaceMediaData =
+        SmartspaceMediaData(
+            targetId = KEY_MEDIA_SMARTSPACE,
+            isActive = true,
+            packageName = PACKAGE_NAME,
+            recommendations = MediaTestHelper.getValidRecommendationList(icon),
+        )
+
+    private val underTest: MediaCarouselViewModel = kosmos.mediaCarouselViewModel
+
+    @Before
+    fun setUp() {
+        kosmos.mediaCarouselInteractor.start()
+
+        whenever(packageManager.getApplicationIcon(Mockito.anyString())).thenReturn(drawable)
+        whenever(packageManager.getApplicationIcon(any(ApplicationInfo::class.java)))
+            .thenReturn(drawable)
+        whenever(packageManager.getApplicationInfo(eq(PACKAGE_NAME), ArgumentMatchers.anyInt()))
+            .thenReturn(ApplicationInfo())
+        whenever(packageManager.getApplicationLabel(any())).thenReturn(PACKAGE_NAME)
+
+        context.setMockPackageManager(packageManager)
+    }
+
+    @Test
+    fun loadMediaControls_mediaItemsAreUpdated() =
+        testScope.runTest {
+            val sortedMedia by collectLastValue(underTest.mediaItems)
+            val instanceId1 = InstanceId.fakeInstanceId(123)
+            val instanceId2 = InstanceId.fakeInstanceId(456)
+
+            loadMediaControl(KEY, instanceId1)
+            loadMediaControl(KEY_2, instanceId2)
+
+            val firstMediaControl = sortedMedia?.get(0) as MediaCommonViewModel.MediaControl
+            val secondMediaControl = sortedMedia?.get(1) as MediaCommonViewModel.MediaControl
+            assertThat(firstMediaControl.instanceId).isEqualTo(instanceId2)
+            assertThat(secondMediaControl.instanceId).isEqualTo(instanceId1)
+        }
+
+    @Test
+    fun loadMediaControlsAndRecommendations_mediaItemsAreUpdated() =
+        testScope.runTest {
+            val sortedMedia by collectLastValue(underTest.mediaItems)
+            kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, false)
+            val instanceId1 = InstanceId.fakeInstanceId(123)
+            val instanceId2 = InstanceId.fakeInstanceId(456)
+
+            loadMediaControl(KEY, instanceId1)
+            loadMediaControl(KEY_2, instanceId2)
+            loadMediaRecommendations()
+
+            val firstMediaControl = sortedMedia?.get(0) as MediaCommonViewModel.MediaControl
+            val secondMediaControl = sortedMedia?.get(1) as MediaCommonViewModel.MediaControl
+            val recsCard = sortedMedia?.get(2) as MediaCommonViewModel.MediaRecommendations
+            assertThat(firstMediaControl.instanceId).isEqualTo(instanceId2)
+            assertThat(secondMediaControl.instanceId).isEqualTo(instanceId1)
+            assertThat(recsCard.key).isEqualTo(KEY_MEDIA_SMARTSPACE)
+        }
+
+    private fun loadMediaControl(key: String, instanceId: InstanceId) {
+        whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true)
+        whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true)
+        val mediaData =
+            MediaData(
+                userId = USER_ID,
+                packageName = PACKAGE_NAME,
+                notificationKey = key,
+                instanceId = instanceId
+            )
+
+        mediaDataFilter.onMediaDataLoaded(key, key, mediaData)
+    }
+
+    private fun loadMediaRecommendations(key: String = KEY_MEDIA_SMARTSPACE) {
+        mediaDataFilter.onSmartspaceMediaDataLoaded(key, smartspaceMediaData)
+    }
+
+    companion object {
+        private const val USER_ID = 0
+        private const val KEY = "key"
+        private const val KEY_2 = "key2"
+        private const val PACKAGE_NAME = "com.example.app"
+        private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID"
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
index 3727c11..ab1f458 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
@@ -106,7 +106,7 @@
             val destinations by collectLastValue(underTest.destinationScenes)
 
             val currentScene by collectLastValue(sceneInteractor.currentScene)
-            val previousScene by collectLastValue(sceneInteractor.previousScene)
+            val previousScene by collectLastValue(sceneInteractor.previousScene())
             sceneInteractor.changeScene(Scenes.Lockscreen, "reason")
             sceneInteractor.changeScene(Scenes.QuickSettings, "reason")
             assertThat(currentScene).isEqualTo(Scenes.QuickSettings)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
index 65fd101..a889007 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -150,6 +150,7 @@
                 sceneInteractor = sceneInteractor,
                 falsingInteractor = kosmos.falsingInteractor,
                 powerInteractor = kosmos.powerInteractor,
+                scenes = kosmos.scenes,
             )
             .apply { setTransitionState(transitionState) }
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
index 63f4816..871ce6d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
@@ -296,7 +296,7 @@
     fun previousScene() =
         testScope.runTest {
             val currentScene by collectLastValue(underTest.currentScene)
-            val previousScene by collectLastValue(underTest.previousScene)
+            val previousScene by collectLastValue(underTest.previousScene())
             assertThat(previousScene).isNull()
 
             val firstScene = currentScene
@@ -306,4 +306,19 @@
             underTest.changeScene(toScene = Scenes.QuickSettings, "reason")
             assertThat(previousScene).isEqualTo(Scenes.Shade)
         }
+
+    @Test
+    fun previousScene_withIgnoredScene() =
+        testScope.runTest {
+            val currentScene by collectLastValue(underTest.currentScene)
+            val previousScene by collectLastValue(underTest.previousScene(ignored = Scenes.Shade))
+            assertThat(previousScene).isNull()
+
+            val firstScene = currentScene
+            underTest.changeScene(toScene = Scenes.Shade, "reason")
+            assertThat(previousScene).isEqualTo(firstScene)
+
+            underTest.changeScene(toScene = Scenes.QuickSettings, "reason")
+            assertThat(previousScene).isNull()
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
index ea95aab..5c30379 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
@@ -28,8 +28,10 @@
 import com.android.systemui.power.data.repository.fakePowerRepository
 import com.android.systemui.power.domain.interactor.powerInteractor
 import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.fakeScenes
 import com.android.systemui.scene.sceneContainerConfig
 import com.android.systemui.scene.sceneKeys
+import com.android.systemui.scene.scenes
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.shared.model.fakeSceneDataSource
 import com.android.systemui.testKosmos
@@ -64,6 +66,7 @@
                 sceneInteractor = sceneInteractor,
                 falsingInteractor = kosmos.falsingInteractor,
                 powerInteractor = kosmos.powerInteractor,
+                scenes = kosmos.scenes,
             )
     }
 
@@ -214,4 +217,23 @@
 
             assertThat(isVisible).isFalse()
         }
+
+    @Test
+    fun currentDestinationScenes_onlyTheCurrentSceneIsCollected() =
+        testScope.runTest {
+            val unused by collectLastValue(underTest.currentDestinationScenes(backgroundScope))
+            val currentScene by collectLastValue(sceneInteractor.currentScene)
+            kosmos.fakeScenes.forEach { scene ->
+                fakeSceneDataSource.changeScene(toScene = scene.key)
+                runCurrent()
+                assertThat(currentScene).isEqualTo(scene.key)
+
+                assertThat(scene.isDestinationScenesBeingCollected).isTrue()
+                kosmos.fakeScenes
+                    .filter { it.key != scene.key }
+                    .forEach { otherScene ->
+                        assertThat(otherScene.isDestinationScenesBeingCollected).isFalse()
+                    }
+            }
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImplTest.kt
index 26f342a..468c39d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImplTest.kt
@@ -22,7 +22,12 @@
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.compose.animation.scene.SceneKey
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.shared.model.Scenes
@@ -31,77 +36,93 @@
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Assume
 import org.junit.Before
-import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
-@Ignore("b/328827631")
 @EnableSceneContainer
 class ShadeBackActionInteractorImplTest : SysuiTestCase() {
     val kosmos = testKosmos()
     val testScope = kosmos.testScope
-    val sceneInteractor = kosmos.sceneInteractor
-    val underTest = kosmos.shadeBackActionInteractor
+    val sceneInteractor by lazy { kosmos.sceneInteractor }
+    val shadeInteractor by lazy { kosmos.shadeInteractor }
+    val fakeAuthenticationRepository by lazy { kosmos.fakeAuthenticationRepository }
+    val deviceEntryFingerprintAuthRepository by lazy { kosmos.deviceEntryFingerprintAuthRepository }
+
+    lateinit var underTest: ShadeBackActionInteractor
 
     @Before
-    fun ignoreSplitShade() {
+    fun ignoreSplitShadeAndSetup() {
         Assume.assumeFalse(Utilities.isLargeScreen(kosmos.applicationContext))
+        underTest = kosmos.shadeBackActionInteractor
     }
 
     @Test
     fun animateCollapseQs_notOnQs() =
         testScope.runTest {
+            val actual by collectLastValue(sceneInteractor.currentScene)
             setScene(Scenes.Shade)
             underTest.animateCollapseQs(true)
             runCurrent()
-            assertThat(sceneInteractor.currentScene.value).isEqualTo(Scenes.Shade)
+            assertThat(actual).isEqualTo(Scenes.Shade)
         }
 
     @Test
     fun animateCollapseQs_fullyCollapse_entered() =
         testScope.runTest {
+            val actual by collectLastValue(sceneInteractor.currentScene)
             enterDevice()
             setScene(Scenes.QuickSettings)
             underTest.animateCollapseQs(true)
             runCurrent()
-            assertThat(sceneInteractor.currentScene.value).isEqualTo(Scenes.Gone)
+            assertThat(actual).isEqualTo(Scenes.Gone)
         }
 
     @Test
     fun animateCollapseQs_fullyCollapse_locked() =
         testScope.runTest {
+            val actual by collectLastValue(sceneInteractor.currentScene)
             setScene(Scenes.QuickSettings)
             underTest.animateCollapseQs(true)
             runCurrent()
-            assertThat(sceneInteractor.currentScene.value).isEqualTo(Scenes.Lockscreen)
+            assertThat(actual).isEqualTo(Scenes.Lockscreen)
         }
 
     @Test
     fun animateCollapseQs_notFullyCollapse() =
         testScope.runTest {
+            val actual by collectLastValue(sceneInteractor.currentScene)
             setScene(Scenes.QuickSettings)
             underTest.animateCollapseQs(false)
             runCurrent()
-            assertThat(sceneInteractor.currentScene.value).isEqualTo(Scenes.Shade)
+            assertThat(actual).isEqualTo(Scenes.Shade)
         }
 
-    private fun enterDevice() {
-        testScope.runCurrent()
+    private fun TestScope.enterDevice() {
+        // configure device unlocked state
+        fakeAuthenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
+        runCurrent()
+        deviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+            SuccessFingerprintAuthenticationStatus(0, true)
+        )
+        runCurrent()
         setScene(Scenes.Gone)
     }
 
-    private fun setScene(key: SceneKey) {
+    private fun TestScope.setScene(key: SceneKey) {
+        val actual by collectLastValue(sceneInteractor.currentScene)
         sceneInteractor.changeScene(key, "test")
         sceneInteractor.setTransitionState(
             MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key))
         )
-        testScope.runCurrent()
+        runCurrent()
+        assertThat(actual).isEqualTo(key)
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt
index 5b33ecb..96b2b7a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt
@@ -19,11 +19,12 @@
 import android.app.StatusBarManager.DISABLE2_NONE
 import android.app.StatusBarManager.DISABLE2_NOTIFICATION_SHADE
 import android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import android.platform.test.flag.junit.FlagsParameterization
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.andSceneContainer
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.DozeStateModel
@@ -36,9 +37,7 @@
 import com.android.systemui.power.data.repository.fakePowerRepository
 import com.android.systemui.power.shared.model.WakeSleepReason
 import com.android.systemui.power.shared.model.WakefulnessState
-import com.android.systemui.res.R
-import com.android.systemui.scene.domain.interactor.sceneInteractor
-import com.android.systemui.shade.data.repository.fakeShadeRepository
+import com.android.systemui.shade.shadeTestUtil
 import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel
 import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository
 import com.android.systemui.statusbar.phone.dozeParameters
@@ -52,28 +51,47 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
-@RunWith(AndroidJUnit4::class)
-class ShadeInteractorImplTest : SysuiTestCase() {
+@RunWith(ParameterizedAndroidJunit4::class)
+class ShadeInteractorImplTest(flags: FlagsParameterization?) : SysuiTestCase() {
     val kosmos = testKosmos()
     val testScope = kosmos.testScope
-    val configurationRepository = kosmos.fakeConfigurationRepository
-    val deviceProvisioningRepository = kosmos.fakeDeviceProvisioningRepository
-    val disableFlagsRepository = kosmos.fakeDisableFlagsRepository
-    val keyguardRepository = kosmos.fakeKeyguardRepository
-    val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
-    val powerRepository = kosmos.fakePowerRepository
-    val sceneInteractor = kosmos.sceneInteractor
-    val shadeRepository = kosmos.fakeShadeRepository
-    val userRepository = kosmos.fakeUserRepository
-    val userSetupRepository = kosmos.fakeUserSetupRepository
-    val dozeParameters = kosmos.dozeParameters
+    val configurationRepository by lazy { kosmos.fakeConfigurationRepository }
+    val deviceProvisioningRepository by lazy { kosmos.fakeDeviceProvisioningRepository }
+    val disableFlagsRepository by lazy { kosmos.fakeDisableFlagsRepository }
+    val keyguardRepository by lazy { kosmos.fakeKeyguardRepository }
+    val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository }
+    val powerRepository by lazy { kosmos.fakePowerRepository }
+    val shadeTestUtil by lazy { kosmos.shadeTestUtil }
+    val userRepository by lazy { kosmos.fakeUserRepository }
+    val userSetupRepository by lazy { kosmos.fakeUserSetupRepository }
+    val dozeParameters by lazy { kosmos.dozeParameters }
 
-    val underTest = kosmos.shadeInteractorImpl
+    lateinit var underTest: ShadeInteractorImpl
+
+    companion object {
+        @JvmStatic
+        @Parameters(name = "{0}")
+        fun getParams(): List<FlagsParameterization> {
+            return FlagsParameterization.allCombinationsOf().andSceneContainer()
+        }
+    }
+
+    init {
+        mSetFlagsRule.setFlagsParameterization(flags!!)
+    }
+
+    @Before
+    fun setup() {
+        underTest = kosmos.shadeInteractorImpl
+    }
 
     @Test
     fun isShadeEnabled_matchesDisableFlagsRepo() =
@@ -284,88 +302,13 @@
         }
 
     @Test
-    fun fullShadeExpansionWhenShadeLocked() =
-        testScope.runTest {
-            val actual by collectLastValue(underTest.shadeExpansion)
-
-            keyguardRepository.setStatusBarState(StatusBarState.SHADE_LOCKED)
-            shadeRepository.setLockscreenShadeExpansion(0.5f)
-
-            assertThat(actual).isEqualTo(1f)
-        }
-
-    @Test
-    fun fullShadeExpansionWhenStatusBarStateIsNotShadeLocked() =
-        testScope.runTest {
-            val actual by collectLastValue(underTest.shadeExpansion)
-
-            keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
-
-            shadeRepository.setLockscreenShadeExpansion(0.5f)
-            assertThat(actual).isEqualTo(0.5f)
-
-            shadeRepository.setLockscreenShadeExpansion(0.8f)
-            assertThat(actual).isEqualTo(0.8f)
-        }
-
-    @Test
-    fun shadeExpansionWhenInSplitShadeAndQsExpanded() =
-        testScope.runTest {
-            val actual by collectLastValue(underTest.shadeExpansion)
-
-            // WHEN split shade is enabled and QS is expanded
-            keyguardRepository.setStatusBarState(StatusBarState.SHADE)
-            overrideResource(R.bool.config_use_split_notification_shade, true)
-            configurationRepository.onAnyConfigurationChange()
-            shadeRepository.setQsExpansion(.5f)
-            shadeRepository.setLegacyShadeExpansion(.7f)
-            runCurrent()
-
-            // THEN legacy shade expansion is passed through
-            assertThat(actual).isEqualTo(.7f)
-        }
-
-    @Test
-    fun shadeExpansionWhenNotInSplitShadeAndQsPartiallyExpanded() =
-        testScope.runTest {
-            val actual by collectLastValue(underTest.shadeExpansion)
-
-            // WHEN split shade is not enabled and QS is expanded
-            keyguardRepository.setStatusBarState(StatusBarState.SHADE)
-            overrideResource(R.bool.config_use_split_notification_shade, false)
-            shadeRepository.setQsExpansion(.5f)
-            shadeRepository.setLegacyShadeExpansion(1f)
-            runCurrent()
-
-            // THEN shade expansion is zero
-            assertThat(actual).isEqualTo(.5f)
-        }
-
-    @Test
-    fun shadeExpansionWhenNotInSplitShadeAndQsFullyExpanded() =
-        testScope.runTest {
-            val actual by collectLastValue(underTest.shadeExpansion)
-
-            // WHEN split shade is not enabled and QS is expanded
-            keyguardRepository.setStatusBarState(StatusBarState.SHADE)
-            overrideResource(R.bool.config_use_split_notification_shade, false)
-            shadeRepository.setQsExpansion(1f)
-            shadeRepository.setLegacyShadeExpansion(1f)
-            runCurrent()
-
-            // THEN shade expansion is zero
-            assertThat(actual).isEqualTo(0f)
-        }
-
-    @Test
     fun shadeExpansionWhenNotInSplitShadeAndQsCollapsed() =
         testScope.runTest {
             val actual by collectLastValue(underTest.shadeExpansion)
 
             // WHEN split shade is not enabled and QS is expanded
             keyguardRepository.setStatusBarState(StatusBarState.SHADE)
-            shadeRepository.setQsExpansion(0f)
-            shadeRepository.setLegacyShadeExpansion(.6f)
+            shadeTestUtil.setShadeAndQsExpansion(.6f, 0f)
 
             // THEN shade expansion is zero
             assertThat(actual).isEqualTo(.6f)
@@ -375,8 +318,7 @@
     fun anyExpansion_shadeGreater() =
         testScope.runTest() {
             // WHEN shade is more expanded than QS
-            shadeRepository.setLegacyShadeExpansion(.5f)
-            shadeRepository.setQsExpansion(0f)
+            shadeTestUtil.setShadeAndQsExpansion(.5f, 0f)
             runCurrent()
 
             // THEN anyExpansion is .5f
@@ -387,8 +329,7 @@
     fun anyExpansion_qsGreater() =
         testScope.runTest() {
             // WHEN qs is more expanded than shade
-            shadeRepository.setLegacyShadeExpansion(0f)
-            shadeRepository.setQsExpansion(.5f)
+            shadeTestUtil.setShadeAndQsExpansion(0f, .5f)
             runCurrent()
 
             // THEN anyExpansion is .5f
@@ -396,229 +337,6 @@
         }
 
     @Test
-    fun userInteractingWithShade_shadeDraggedUpAndDown() =
-        testScope.runTest() {
-            val actual by collectLastValue(underTest.isUserInteractingWithShade)
-            // GIVEN shade collapsed and not tracking input
-            shadeRepository.setLegacyShadeExpansion(0f)
-            shadeRepository.setLegacyShadeTracking(false)
-            runCurrent()
-
-            // THEN user is not interacting
-            assertThat(actual).isFalse()
-
-            // WHEN shade tracking starts
-            shadeRepository.setLegacyShadeTracking(true)
-            runCurrent()
-
-            // THEN user is interacting
-            assertThat(actual).isTrue()
-
-            // WHEN shade dragged down halfway
-            shadeRepository.setLegacyShadeExpansion(.5f)
-            runCurrent()
-
-            // THEN user is interacting
-            assertThat(actual).isTrue()
-
-            // WHEN shade fully expanded but tracking is not stopped
-            shadeRepository.setLegacyShadeExpansion(1f)
-            runCurrent()
-
-            // THEN user is interacting
-            assertThat(actual).isTrue()
-
-            // WHEN shade fully collapsed but tracking is not stopped
-            shadeRepository.setLegacyShadeExpansion(0f)
-            runCurrent()
-
-            // THEN user is interacting
-            assertThat(actual).isTrue()
-
-            // WHEN shade dragged halfway and tracking is stopped
-            shadeRepository.setLegacyShadeExpansion(.6f)
-            shadeRepository.setLegacyShadeTracking(false)
-            runCurrent()
-
-            // THEN user is interacting
-            assertThat(actual).isTrue()
-
-            // WHEN shade completes expansion stopped
-            shadeRepository.setLegacyShadeExpansion(1f)
-            runCurrent()
-
-            // THEN user is not interacting
-            assertThat(actual).isFalse()
-        }
-
-    @Test
-    fun userInteractingWithShade_shadeExpanded() =
-        testScope.runTest() {
-            val actual by collectLastValue(underTest.isUserInteractingWithShade)
-            // GIVEN shade collapsed and not tracking input
-            shadeRepository.setLegacyShadeExpansion(0f)
-            shadeRepository.setLegacyShadeTracking(false)
-            runCurrent()
-
-            // THEN user is not interacting
-            assertThat(actual).isFalse()
-
-            // WHEN shade tracking starts
-            shadeRepository.setLegacyShadeTracking(true)
-            runCurrent()
-
-            // THEN user is interacting
-            assertThat(actual).isTrue()
-
-            // WHEN shade dragged down halfway
-            shadeRepository.setLegacyShadeExpansion(.5f)
-            runCurrent()
-
-            // THEN user is interacting
-            assertThat(actual).isTrue()
-
-            // WHEN shade fully expanded and tracking is stopped
-            shadeRepository.setLegacyShadeExpansion(1f)
-            shadeRepository.setLegacyShadeTracking(false)
-            runCurrent()
-
-            // THEN user is not interacting
-            assertThat(actual).isFalse()
-        }
-
-    @Test
-    fun userInteractingWithShade_shadePartiallyExpanded() =
-        testScope.runTest() {
-            val actual by collectLastValue(underTest.isUserInteractingWithShade)
-            // GIVEN shade collapsed and not tracking input
-            shadeRepository.setLegacyShadeExpansion(0f)
-            shadeRepository.setLegacyShadeTracking(false)
-            runCurrent()
-
-            // THEN user is not interacting
-            assertThat(actual).isFalse()
-
-            // WHEN shade tracking starts
-            shadeRepository.setLegacyShadeTracking(true)
-            runCurrent()
-
-            // THEN user is interacting
-            assertThat(actual).isTrue()
-
-            // WHEN shade partially expanded
-            shadeRepository.setLegacyShadeExpansion(.4f)
-            runCurrent()
-
-            // THEN user is interacting
-            assertThat(actual).isTrue()
-
-            // WHEN tracking is stopped
-            shadeRepository.setLegacyShadeTracking(false)
-            runCurrent()
-
-            // THEN user is interacting
-            assertThat(actual).isTrue()
-
-            // WHEN shade goes back to collapsed
-            shadeRepository.setLegacyShadeExpansion(0f)
-            runCurrent()
-
-            // THEN user is not interacting
-            assertThat(actual).isFalse()
-        }
-
-    @Test
-    fun userInteractingWithShade_shadeCollapsed() =
-        testScope.runTest() {
-            val actual by collectLastValue(underTest.isUserInteractingWithShade)
-            // GIVEN shade expanded and not tracking input
-            shadeRepository.setLegacyShadeExpansion(1f)
-            shadeRepository.setLegacyShadeTracking(false)
-            runCurrent()
-
-            // THEN user is not interacting
-            assertThat(actual).isFalse()
-
-            // WHEN shade tracking starts
-            shadeRepository.setLegacyShadeTracking(true)
-            runCurrent()
-
-            // THEN user is interacting
-            assertThat(actual).isTrue()
-
-            // WHEN shade dragged up halfway
-            shadeRepository.setLegacyShadeExpansion(.5f)
-            runCurrent()
-
-            // THEN user is interacting
-            assertThat(actual).isTrue()
-
-            // WHEN shade fully collapsed and tracking is stopped
-            shadeRepository.setLegacyShadeExpansion(0f)
-            shadeRepository.setLegacyShadeTracking(false)
-            runCurrent()
-
-            // THEN user is not interacting
-            assertThat(actual).isFalse()
-        }
-
-    @Test
-    fun userInteractingWithQs_qsDraggedUpAndDown() =
-        testScope.runTest() {
-            val actual by collectLastValue(underTest.isUserInteractingWithQs)
-            // GIVEN qs collapsed and not tracking input
-            shadeRepository.setQsExpansion(0f)
-            shadeRepository.setLegacyQsTracking(false)
-            runCurrent()
-
-            // THEN user is not interacting
-            assertThat(actual).isFalse()
-
-            // WHEN qs tracking starts
-            shadeRepository.setLegacyQsTracking(true)
-            runCurrent()
-
-            // THEN user is interacting
-            assertThat(actual).isTrue()
-
-            // WHEN qs dragged down halfway
-            shadeRepository.setQsExpansion(.5f)
-            runCurrent()
-
-            // THEN user is interacting
-            assertThat(actual).isTrue()
-
-            // WHEN qs fully expanded but tracking is not stopped
-            shadeRepository.setQsExpansion(1f)
-            runCurrent()
-
-            // THEN user is interacting
-            assertThat(actual).isTrue()
-
-            // WHEN qs fully collapsed but tracking is not stopped
-            shadeRepository.setQsExpansion(0f)
-            runCurrent()
-
-            // THEN user is interacting
-            assertThat(actual).isTrue()
-
-            // WHEN qs dragged halfway and tracking is stopped
-            shadeRepository.setQsExpansion(.6f)
-            shadeRepository.setLegacyQsTracking(false)
-            runCurrent()
-
-            // THEN user is interacting
-            assertThat(actual).isTrue()
-
-            // WHEN qs completes expansion stopped
-            shadeRepository.setQsExpansion(1f)
-            runCurrent()
-
-            // THEN user is not interacting
-            assertThat(actual).isFalse()
-        }
-
-    @Test
     fun isShadeTouchable_isFalse_whenDeviceAsleepAndNotPulsing() =
         testScope.runTest {
             powerRepository.updateWakefulness(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImplTest.kt
index 0ae95e7..109cd053 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImplTest.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.DisableSceneContainer
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.StatusBarState
@@ -40,6 +41,7 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@DisableSceneContainer
 class ShadeInteractorLegacyImplTest : SysuiTestCase() {
     val kosmos = testKosmos()
     val testScope = kosmos.testScope
@@ -95,6 +97,22 @@
         }
 
     @Test
+    fun shadeExpansionWhenNotInSplitShadeAndQsPartiallyExpanded() =
+        testScope.runTest {
+            val actual by collectLastValue(underTest.shadeExpansion)
+
+            // WHEN split shade is not enabled and QS is expanded
+            keyguardRepository.setStatusBarState(StatusBarState.SHADE)
+            overrideResource(R.bool.config_use_split_notification_shade, false)
+            shadeRepository.setQsExpansion(.5f)
+            shadeRepository.setLegacyShadeExpansion(1f)
+            runCurrent()
+
+            // THEN shade expansion is zero
+            assertThat(actual).isEqualTo(.5f)
+        }
+
+    @Test
     fun shadeExpansionWhenNotInSplitShadeAndQsFullyExpanded() =
         testScope.runTest {
             val actual by collectLastValue(underTest.shadeExpansion)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt
index 8c9036a..e1908b9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt
@@ -23,7 +23,6 @@
 import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
-import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.StatusBarState
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.res.R
@@ -32,7 +31,6 @@
 import com.android.systemui.shade.data.repository.shadeRepository
 import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.testKosmos
-import com.android.systemui.user.data.repository.userRepository
 import com.google.common.truth.Truth
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -52,9 +50,7 @@
     private val testScope = kosmos.testScope
     private val configurationRepository = kosmos.fakeConfigurationRepository
     private val keyguardRepository = kosmos.fakeKeyguardRepository
-    private val keyguardTransitionRepository = kosmos.keyguardTransitionRepository
     private val sceneInteractor = kosmos.sceneInteractor
-    private val userRepository = kosmos.userRepository
     private val shadeRepository = kosmos.shadeRepository
 
     private val underTest = kosmos.shadeInteractorSceneContainerImpl
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt
index 29f286f..7420ea0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt
@@ -248,6 +248,35 @@
     }
 
     @Test
+    fun testGetDurationMs_untrackedEntryEmptyAvalanche_useAutoDismissTime() {
+        val givenEntry = createHeadsUpEntry(id = 0)
+
+        // Nothing is showing
+        mAvalancheController.headsUpEntryShowing = null
+
+        // Nothing is next
+        mAvalancheController.clearNext()
+
+        val durationMs = mAvalancheController.getDurationMs(givenEntry, autoDismissMs = 5000)
+        Truth.assertThat(durationMs).isEqualTo(5000)
+    }
+
+    @Test
+    fun testGetDurationMs_untrackedEntryNonEmptyAvalanche_useAutoDismissTime() {
+        val givenEntry = createHeadsUpEntry(id = 0)
+
+        // Given entry not tracked
+        mAvalancheController.headsUpEntryShowing = createHeadsUpEntry(id = 1)
+
+        mAvalancheController.clearNext()
+        val nextEntry = createHeadsUpEntry(id = 2)
+        mAvalancheController.addToNext(nextEntry, runnableMock!!)
+
+        val durationMs = mAvalancheController.getDurationMs(givenEntry, autoDismissMs = 5000)
+        Truth.assertThat(durationMs).isEqualTo(5000)
+    }
+
+    @Test
     fun testGetDurationMs_lastEntry_useAutoDismissTime() {
         // Entry is showing
         val showingEntry = createHeadsUpEntry(id = 0)
@@ -261,7 +290,7 @@
     }
 
     @Test
-    fun testGetDurationMs_nextEntryLowerPriority_500() {
+    fun testGetDurationMs_nextEntryLowerPriority_5000() {
         // Entry is showing
         val showingEntry = createFsiHeadsUpEntry(id = 1)
         mAvalancheController.headsUpEntryShowing = showingEntry
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java
index 7c130be..db8e14c1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java
@@ -38,9 +38,9 @@
 import android.app.Notification;
 import android.app.PendingIntent;
 import android.app.Person;
+import android.platform.test.flag.junit.FlagsParameterization;
 import android.testing.TestableLooper;
 
-import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.logging.testing.UiEventLoggerFake;
@@ -62,9 +62,14 @@
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 
+import java.util.List;
+
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
 @SmallTest
 @TestableLooper.RunWithLooper
-@RunWith(AndroidJUnit4.class)
+@RunWith(ParameterizedAndroidJunit4.class)
 public class BaseHeadsUpManagerTest extends SysuiTestCase {
     @Rule
     public MockitoRule rule = MockitoJUnit.rule();
@@ -129,10 +134,18 @@
         }
     }
 
+    @Parameters(name = "{0}")
+    public static List<FlagsParameterization> getFlags() {
+        return FlagsParameterization.allCombinationsOf(NotificationThrottleHun.FLAG_NAME);
+    }
+
+    public BaseHeadsUpManagerTest(FlagsParameterization flags) {
+        mSetFlagsRule.setFlagsParameterization(flags);
+    }
+
     @Override
     public void SysuiSetup() throws Exception {
         super.SysuiSetup();
-        mSetFlagsRule.disableFlags(NotificationThrottleHun.FLAG_NAME);
         mAvalancheController = new AvalancheController(dumpManager);
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.java
index a8a75c0..f66e75a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.java
@@ -26,9 +26,9 @@
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
+import android.platform.test.flag.junit.FlagsParameterization;
 import android.testing.TestableLooper;
 
-import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.logging.UiEventLogger;
@@ -58,10 +58,14 @@
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 
+import java.util.List;
+
 import kotlinx.coroutines.flow.StateFlowKt;
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
 
 @SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(ParameterizedAndroidJunit4.class)
 @TestableLooper.RunWithLooper
 public class HeadsUpManagerPhoneTest extends BaseHeadsUpManagerTest {
     @Rule public MockitoRule rule = MockitoJUnit.rule();
@@ -141,12 +145,17 @@
         );
     }
 
+    @Parameters(name = "{0}")
+    public static List<FlagsParameterization> getFlags() {
+        return FlagsParameterization.allCombinationsOf(NotificationThrottleHun.FLAG_NAME);
+    }
+
+    public HeadsUpManagerPhoneTest(FlagsParameterization flags) {
+        super(flags);
+    }
+
     @Before
     public void setUp() {
-        // TODO(b/315362456) create separate test with the flag disabled
-        //  then modify this file to test with the flag enabled
-        mSetFlagsRule.disableFlags(NotificationThrottleHun.FLAG_NAME);
-
         when(mShadeInteractor.isAnyExpanded()).thenReturn(StateFlowKt.MutableStateFlow(false));
         final AccessibilityManagerWrapper accessibilityMgr =
                 mDependency.injectMockDependency(AccessibilityManagerWrapper.class);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt
index 96d1c0d..03a39f8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt
@@ -20,6 +20,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.coroutines.collectValues
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.testKosmos
 import com.android.systemui.util.kotlin.BooleanFlowOperators.and
@@ -27,6 +28,7 @@
 import com.android.systemui.util.kotlin.BooleanFlowOperators.or
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
@@ -62,6 +64,21 @@
         }
 
     @Test
+    fun and_onlyEmitsWhenValueChanges() =
+        testScope.runTest {
+            val flow1 = MutableStateFlow(false)
+            val flow2 = MutableStateFlow(false)
+            val values by collectValues(and(flow1, flow2))
+
+            assertThat(values).containsExactly(false)
+            flow1.value = true
+            // Overall value is still false, we should not have emitted again.
+            assertThat(values).containsExactly(false)
+            flow2.value = true
+            assertThat(values).containsExactly(false, true).inOrder()
+        }
+
+    @Test
     fun or_allTrue_returnsTrue() =
         testScope.runTest {
             val result by collectLastValue(or(TRUE, TRUE))
@@ -83,6 +100,20 @@
         }
 
     @Test
+    fun or_onlyEmitsWhenValueChanges() =
+        testScope.runTest {
+            val flow1 = MutableStateFlow(false)
+            val flow2 = MutableStateFlow(false)
+            val values by collectValues(or(flow1, flow2))
+
+            assertThat(values).containsExactly(false)
+            flow1.value = true
+            assertThat(values).containsExactly(false, true).inOrder()
+            flow2.value = true
+            assertThat(values).containsExactly(false, true).inOrder()
+        }
+
+    @Test
     fun not_true_returnsFalse() =
         testScope.runTest {
             val result by collectLastValue(not(TRUE))
diff --git a/packages/SystemUI/res/drawable/notification_material_bg.xml b/packages/SystemUI/res/drawable/notification_material_bg.xml
index 587a5a0..715be07 100644
--- a/packages/SystemUI/res/drawable/notification_material_bg.xml
+++ b/packages/SystemUI/res/drawable/notification_material_bg.xml
@@ -28,9 +28,10 @@
             <solid android:color="@color/notification_state_color_default" />
         </shape>
     </item>
-    <item>
+    <item android:id="@+id/notification_focus_overlay">
         <shape>
-            <stroke android:width="3dp" android:color="@color/notification_focus_overlay_color"/>
+            <stroke android:width="@dimen/notification_focus_stroke_width"
+                android:color="@color/notification_focus_overlay_color"/>
         </shape>
     </item>
 </layer-list>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/media_carousel.xml b/packages/SystemUI/res/layout/media_carousel.xml
index 825ece85..ffe269a 100644
--- a/packages/SystemUI/res/layout/media_carousel.xml
+++ b/packages/SystemUI/res/layout/media_carousel.xml
@@ -19,7 +19,7 @@
 <FrameLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
-    android:layout_height="wrap_content"
+    android:layout_height="match_parent"
     android:clipChildren="false"
     android:clipToPadding="false"
     android:forceHasOverlappingRendering="false"
@@ -27,7 +27,7 @@
     <com.android.systemui.media.controls.ui.view.MediaScrollView
         android:id="@+id/media_carousel_scroller"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
+        android:layout_height="match_parent"
         android:scrollbars="none"
         android:clipChildren="false"
         android:clipToPadding="false"
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 82395e4..df57f2a 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -310,6 +310,9 @@
     <!-- Radius for notifications corners without adjacent notifications -->
     <dimen name="notification_corner_radius">28dp</dimen>
 
+    <!-- Stroke width for notifications focus state overlay, see id/notification_focus_outline -->
+    <dimen name="notification_focus_stroke_width">3dp</dimen>
+
     <!-- Distance over which notification corner animations run, near the shelf while scrolling. -->
     <dimen name="notification_corner_animation_distance">48dp</dimen>
 
diff --git a/packages/SystemUI/src/com/android/systemui/CoreStartable.java b/packages/SystemUI/src/com/android/systemui/CoreStartable.java
index 39e1c41..55ccaa6 100644
--- a/packages/SystemUI/src/com/android/systemui/CoreStartable.java
+++ b/packages/SystemUI/src/com/android/systemui/CoreStartable.java
@@ -33,12 +33,23 @@
  *  abstract fun bind(impl: FoobarStartable): CoreStartable
  *  </pre>
  *
- * If your CoreStartable depends on different CoreStartables starting before it, use a
- * {@link com.android.systemui.startable.Dependencies} annotation to list out those dependencies.
+ * If your CoreStartable depends on different CoreStartables starting before it, you can specify
+ * another map binding listing out its dependencies:
+ *  <pre>
+ *  &#64;Provides
+ *  &#64;IntoMap
+ *  &#64;Dependencies  // Important! com.android.systemui.startable.Dependencies.
+ *  &#64;ClassKey(FoobarStartable::class)
+ *  fun providesDeps(): Set&lt;Class&lt;out CoreStartable&gt;&gt; {
+ *      return setOf(OtherStartable::class.java)
+ *  }
+ *  </pre>
+ *
  *
  * @see SystemUIApplication#startSystemUserServicesIfNeeded()
  */
 public interface CoreStartable extends Dumpable {
+    String STARTABLE_DEPENDENCIES = "startable_dependencies";
 
     /** Main entry point for implementations. Called shortly after SysUI startup. */
     void start();
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
index fb88f0e..08d45fa 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
@@ -44,16 +44,15 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.process.ProcessWrapper;
 import com.android.systemui.res.R;
-import com.android.systemui.startable.Dependencies;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.util.NotificationChannels;
 
 import java.lang.reflect.InvocationTargetException;
 import java.util.ArrayDeque;
-import java.util.Arrays;
 import java.util.Comparator;
 import java.util.HashSet;
 import java.util.Map;
+import java.util.Set;
 import java.util.StringJoiner;
 import java.util.TreeMap;
 
@@ -204,7 +203,7 @@
      */
 
     public void startSystemUserServicesIfNeeded() {
-        if (!mProcessWrapper.isSystemUser()) {
+        if (!shouldStartSystemUserServices()) {
             Log.wtf(TAG, "Tried starting SystemUser services on non-SystemUser");
             return;  // Per-user startables are handled in #startSystemUserServicesIfNeeded.
         }
@@ -227,7 +226,7 @@
      * <p>This method must only be called from the main thread.</p>
      */
     void startSecondaryUserServicesIfNeeded() {
-        if (mProcessWrapper.isSystemUser()) {
+        if (!shouldStartSecondaryUserServices()) {
             return;  // Per-user startables are handled in #startSystemUserServicesIfNeeded.
         }
         // Sort the startables so that we get a deterministic ordering.
@@ -238,6 +237,14 @@
                 sortedStartables, "StartSecondaryServices", null);
     }
 
+    protected boolean shouldStartSystemUserServices() {
+        return mProcessWrapper.isSystemUser();
+    }
+
+    protected boolean shouldStartSecondaryUserServices() {
+        return !mProcessWrapper.isSystemUser();
+    }
+
     private void startServicesIfNeeded(
             Map<Class<?>, Provider<CoreStartable>> startables,
             String metricsPrefix,
@@ -299,9 +306,9 @@
                 Map.Entry<Class<?>, Provider<CoreStartable>> entry = queue.removeFirst();
 
                 Class<?> cls = entry.getKey();
-                Dependencies dep = cls.getAnnotation(Dependencies.class);
-                Class<?>[] deps = (dep == null ? null : dep.value());
-                if (deps == null || startedStartables.containsAll(Arrays.asList(deps))) {
+                Set<Class<? extends CoreStartable>> deps =
+                        mSysUIComponent.getStartableDependencies().get(cls);
+                if (deps == null || startedStartables.containsAll(deps)) {
                     String clsName = cls.getName();
                     int i = serviceIndex;  // Copied to make lambda happy.
                     timeInitialization(
@@ -323,12 +330,12 @@
             while (!nextQueue.isEmpty()) {
                 Map.Entry<Class<?>, Provider<CoreStartable>> entry = nextQueue.removeFirst();
                 Class<?> cls = entry.getKey();
-                Dependencies dep = cls.getAnnotation(Dependencies.class);
-                Class<?>[] deps = (dep == null ? null : dep.value());
+                Set<Class<? extends CoreStartable>> deps =
+                        mSysUIComponent.getStartableDependencies().get(cls);
                 StringJoiner stringJoiner = new StringJoiner(", ");
-                for (int i = 0; deps != null && i < deps.length; i++) {
-                    if (!startedStartables.contains(deps[i])) {
-                        stringJoiner.add(deps[i].getName());
+                for (Class<? extends CoreStartable> c : deps) {
+                    if (!startedStartables.contains(c)) {
+                        stringJoiner.add(c.getName());
                     }
                 }
                 Log.e(TAG, "Failed to start " + cls.getName()
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 dd71bc7..cb458ef 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
@@ -95,7 +95,7 @@
 
     /** The scene to show when bouncer is dismissed. */
     val dismissDestination: Flow<SceneKey> =
-        sceneInteractor.previousScene.map { it ?: Scenes.Lockscreen }
+        sceneInteractor.previousScene(Scenes.Bouncer).map { it ?: Scenes.Lockscreen }
 
     /** Notifies that the user has places down a pointer, not necessarily dragging just yet. */
     fun onDown() {
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
index 71d719d..4ac43bc 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
@@ -20,6 +20,7 @@
 import android.os.UserHandle
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.TransitionKey
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.communal.domain.model.CommunalContentModel
 import com.android.systemui.communal.widgets.WidgetConfigurator
@@ -52,8 +53,8 @@
         communalInteractor.signalUserInteraction()
     }
 
-    fun changeScene(scene: SceneKey) {
-        communalInteractor.changeScene(scene)
+    fun changeScene(scene: SceneKey, transitionKey: TransitionKey? = null) {
+        communalInteractor.changeScene(scene, transitionKey)
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
index 5f4b394..f20fafc 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
@@ -36,6 +36,7 @@
 import com.android.internal.logging.UiEventLogger
 import com.android.systemui.communal.shared.log.CommunalUiEvent
 import com.android.systemui.communal.shared.model.CommunalScenes
+import com.android.systemui.communal.shared.model.CommunalTransitionKeys
 import com.android.systemui.communal.ui.compose.CommunalHub
 import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
 import com.android.systemui.communal.util.WidgetPickerIntentUtils.getWidgetExtraFromIntent
@@ -149,7 +150,10 @@
 
     private fun onEditDone() {
         try {
-            communalViewModel.changeScene(CommunalScenes.Communal)
+            communalViewModel.changeScene(
+                CommunalScenes.Communal,
+                CommunalTransitionKeys.SimpleFade
+            )
             checkNotNull(windowManagerService).lockNow(/* options */ null)
             finish()
         } catch (e: RemoteException) {
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
index e1041669..871ef1e 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
@@ -25,6 +25,7 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.keyguard.KeyguardSliceProvider;
 import com.android.systemui.people.PeopleProvider;
+import com.android.systemui.startable.Dependencies;
 import com.android.systemui.statusbar.NotificationInsetsModule;
 import com.android.systemui.statusbar.QsFrameTranslateModule;
 import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -47,6 +48,7 @@
 
 import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
 
 import javax.inject.Provider;
 
@@ -160,6 +162,11 @@
     @PerUser Map<Class<?>, Provider<CoreStartable>> getPerUserStartables();
 
     /**
+     * Returns {@link CoreStartable} dependencies if there are any.
+     */
+    @Dependencies Map<Class<?>, Set<Class<? extends CoreStartable>>> getStartableDependencies();
+
+    /**
      * Member injection into the supplied argument.
      */
     void inject(SystemUIAppComponentFactoryBase factory);
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
index 5c1ca64..662974d 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
@@ -19,6 +19,7 @@
 import androidx.annotation.VisibleForTesting
 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.deviceentry.data.repository.DeviceEntryRepository
@@ -63,6 +64,7 @@
     private val trustInteractor: TrustInteractor,
     private val deviceUnlockedInteractor: DeviceUnlockedInteractor,
     private val systemPropertiesHelper: SystemPropertiesHelper,
+    private val alternateBouncerInteractor: AlternateBouncerInteractor,
 ) {
     /**
      * Whether the device is unlocked.
@@ -211,10 +213,14 @@
         //       4. Transition to bouncer scene
         applicationScope.launch {
             if (isAuthenticationRequired()) {
-                sceneInteractor.changeScene(
-                    toScene = Scenes.Bouncer,
-                    loggingReason = "request to unlock device while authentication required",
-                )
+                if (alternateBouncerInteractor.canShowAlternateBouncer.value) {
+                    alternateBouncerInteractor.forceShow()
+                } else {
+                    sceneInteractor.changeScene(
+                        toScene = Scenes.Bouncer,
+                        loggingReason = "request to unlock device while authentication required",
+                    )
+                }
             } else {
                 sceneInteractor.changeScene(
                     toScene = Scenes.Gone,
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 b8ceab3..2c869bf 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
@@ -65,6 +65,7 @@
     ) {
 
     override fun start() {
+        listenForDreamingToAlternateBouncer()
         listenForDreamingToOccluded()
         listenForDreamingToGoneWhenDismissable()
         listenForDreamingToGoneFromBiometricUnlock()
@@ -75,6 +76,16 @@
         listenForDreamingToPrimaryBouncer()
     }
 
+    private fun listenForDreamingToAlternateBouncer() {
+        scope.launch("$TAG#listenForDreamingToAlternateBouncer") {
+            keyguardInteractor.alternateBouncerShowing
+                .filterRelevantKeyguardStateAnd { isAlternateBouncerShowing ->
+                    isAlternateBouncerShowing
+                }
+                .collect { startTransitionTo(KeyguardState.ALTERNATE_BOUNCER) }
+        }
+    }
+
     private fun listenForDreamingToGlanceableHub() {
         if (!communalHub()) return
         scope.launch("$TAG#listenForDreamingToGlanceableHub", mainDispatcher) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
index e456a55..2850165 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
@@ -224,20 +224,12 @@
     ) {
         if (!KeyguardWmStateRefactor.isEnabled) {
             scope.launch {
-                keyguardInteractor.onCameraLaunchDetected
-                    .sample(transitionInteractor.finishedKeyguardState)
-                    .collect { finishedKeyguardState ->
-                        // Other keyguard state transitions may trigger on the first power button
-                        // push,
-                        // so use the last finishedKeyguardState to determine the overriding FROM
-                        // state
-                        if (finishedKeyguardState == fromState) {
-                            startTransitionTo(
-                                toState = KeyguardState.OCCLUDED,
-                                modeOnCanceled = TransitionModeOnCanceled.RESET,
-                            )
-                        }
-                    }
+                keyguardInteractor.onCameraLaunchDetected.filterRelevantKeyguardState().collect {
+                    startTransitionTo(
+                        toState = KeyguardState.OCCLUDED,
+                        modeOnCanceled = TransitionModeOnCanceled.RESET,
+                    )
+                }
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt b/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt
index 7e57cf4..8ee3adc 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt
@@ -52,16 +52,6 @@
         MutableStateFlow(LinkedHashMap())
     val allUserEntries: StateFlow<Map<String, MediaData>> = _allUserEntries.asStateFlow()
 
-    private val _mediaDataLoadedStates: MutableStateFlow<List<MediaDataLoadingModel>> =
-        MutableStateFlow(mutableListOf())
-    val mediaDataLoadedStates: StateFlow<List<MediaDataLoadingModel>> =
-        _mediaDataLoadedStates.asStateFlow()
-
-    private val _recommendationsLoadingState: MutableStateFlow<SmartspaceMediaLoadingModel> =
-        MutableStateFlow(SmartspaceMediaLoadingModel.Unknown)
-    val recommendationsLoadingState: StateFlow<SmartspaceMediaLoadingModel> =
-        _recommendationsLoadingState.asStateFlow()
-
     private val comparator =
         compareByDescending<MediaSortKeyModel> {
                 it.isPlaying == true && it.playbackLocation == MediaData.PLAYBACK_LOCAL
@@ -148,46 +138,15 @@
     }
 
     fun addMediaDataLoadingState(mediaDataLoadingModel: MediaDataLoadingModel) {
-        // Filter out previous loading state that has same [InstanceId].
-        val loadedStates =
-            _mediaDataLoadedStates.value.filter { loadedModel ->
-                loadedModel !is MediaDataLoadingModel.Loaded ||
-                    !loadedModel.equalInstanceIds(mediaDataLoadingModel)
-            }
-
-        _mediaDataLoadedStates.value =
-            loadedStates +
-                if (mediaDataLoadingModel is MediaDataLoadingModel.Loaded) {
-                    listOf(mediaDataLoadingModel)
-                } else {
-                    emptyList()
-                }
-
-        addMediaLoadingToSortedMap(mediaDataLoadingModel)
-    }
-
-    fun setRecommendationsLoadingState(smartspaceMediaLoadingModel: SmartspaceMediaLoadingModel) {
-        _recommendationsLoadingState.value = smartspaceMediaLoadingModel
-
-        addRecsLoadingToSortedMap(smartspaceMediaLoadingModel)
-    }
-
-    private fun addMediaLoadingToSortedMap(mediaDataLoadingModel: MediaDataLoadingModel) {
-        val instanceId =
-            when (mediaDataLoadingModel) {
-                is MediaDataLoadingModel.Loaded -> mediaDataLoadingModel.instanceId
-                is MediaDataLoadingModel.Removed -> mediaDataLoadingModel.instanceId
-                MediaDataLoadingModel.Unknown -> null
-            }
         val sortedMap = TreeMap<MediaSortKeyModel, MediaCommonModel>(comparator)
         sortedMap.putAll(
             _sortedMedia.value.filter { (_, commonModel) ->
                 commonModel !is MediaCommonModel.MediaControl ||
-                    commonModel.instanceId != instanceId
+                    commonModel.mediaLoadedModel.instanceId != mediaDataLoadingModel.instanceId
             }
         )
 
-        _selectedUserEntries.value[instanceId]?.let {
+        _selectedUserEntries.value[mediaDataLoadingModel.instanceId]?.let {
             val sortKey =
                 MediaSortKeyModel(
                     isPrioritizedRec = false,
@@ -202,51 +161,41 @@
                 )
 
             if (mediaDataLoadingModel is MediaDataLoadingModel.Loaded) {
-                sortedMap[sortKey] = MediaCommonModel.MediaControl(it.instanceId)
+                sortedMap[sortKey] =
+                    MediaCommonModel.MediaControl(mediaDataLoadingModel, canBeRemoved(it))
             }
         }
 
         _sortedMedia.value = sortedMap
     }
 
-    private fun addRecsLoadingToSortedMap(
-        smartspaceMediaLoadingModel: SmartspaceMediaLoadingModel
-    ) {
-        val isPrioritized: Boolean
-        val key: String?
-        when (smartspaceMediaLoadingModel) {
-            is SmartspaceMediaLoadingModel.Loaded -> {
-                isPrioritized = smartspaceMediaLoadingModel.isPrioritized
-                key = smartspaceMediaLoadingModel.key
+    fun setRecommendationsLoadingState(smartspaceMediaLoadingModel: SmartspaceMediaLoadingModel) {
+        val isPrioritized =
+            when (smartspaceMediaLoadingModel) {
+                is SmartspaceMediaLoadingModel.Loaded -> smartspaceMediaLoadingModel.isPrioritized
+                else -> false
             }
-            is SmartspaceMediaLoadingModel.Removed -> {
-                isPrioritized = false
-                key = smartspaceMediaLoadingModel.key
-            }
-            SmartspaceMediaLoadingModel.Unknown -> {
-                isPrioritized = false
-                key = null
-            }
-        }
         val sortedMap = TreeMap<MediaSortKeyModel, MediaCommonModel>(comparator)
         sortedMap.putAll(
             _sortedMedia.value.filter { (_, commonModel) ->
-                commonModel !is MediaCommonModel.MediaRecommendations || commonModel.key != key
+                commonModel !is MediaCommonModel.MediaRecommendations
             }
         )
 
-        key?.let {
-            val sortKey =
-                MediaSortKeyModel(
-                    isPrioritizedRec = isPrioritized,
-                    isPlaying = false,
-                    active = _smartspaceMediaData.value.isActive,
-                )
-            if (smartspaceMediaLoadingModel is SmartspaceMediaLoadingModel.Loaded) {
-                sortedMap[sortKey] = MediaCommonModel.MediaRecommendations(key)
-            }
+        val sortKey =
+            MediaSortKeyModel(
+                isPrioritizedRec = isPrioritized,
+                isPlaying = false,
+                active = _smartspaceMediaData.value.isActive,
+            )
+        if (smartspaceMediaLoadingModel is SmartspaceMediaLoadingModel.Loaded) {
+            sortedMap[sortKey] = MediaCommonModel.MediaRecommendations(smartspaceMediaLoadingModel)
         }
 
         _sortedMedia.value = sortedMap
     }
+
+    private fun canBeRemoved(data: MediaData): Boolean {
+        return data.isPlaying?.let { !it } ?: data.isClearable && !data.active
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt
index b04e938..dc2c651 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt
@@ -21,6 +21,7 @@
 import android.media.session.MediaSession
 import android.media.session.PlaybackState
 import android.service.notification.StatusBarNotification
+import com.android.internal.logging.InstanceId
 import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
@@ -35,20 +36,15 @@
 import com.android.systemui.media.controls.domain.pipeline.MediaTimeoutListener
 import com.android.systemui.media.controls.domain.resume.MediaResumeListener
 import com.android.systemui.media.controls.shared.model.MediaCommonModel
-import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel
-import com.android.systemui.media.controls.shared.model.SmartspaceMediaLoadingModel
 import com.android.systemui.media.controls.util.MediaControlsRefactorFlag
 import com.android.systemui.media.controls.util.MediaFlags
 import java.io.PrintWriter
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.mapLatest
 import kotlinx.coroutines.flow.stateIn
 
@@ -82,8 +78,11 @@
                     (smartspaceMediaData.isActive &&
                         (smartspaceMediaData.isValid() || reactivatedKey != null))
             }
-            .distinctUntilChanged()
-            .stateIn(applicationScope, SharingStarted.WhileSubscribed(), false)
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = false,
+            )
 
     /** Are there any media entries we should display, including the recommendations? */
     val hasAnyMediaOrRecommendation: StateFlow<Boolean> =
@@ -98,34 +97,41 @@
                         smartspaceMediaData.isActive && smartspaceMediaData.isValid()
                     })
             }
-            .distinctUntilChanged()
-            .stateIn(applicationScope, SharingStarted.WhileSubscribed(), false)
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = false,
+            )
 
     /** Are there any media notifications active, excluding the recommendations? */
     val hasActiveMedia: StateFlow<Boolean> =
         mediaFilterRepository.selectedUserEntries
             .mapLatest { entries -> entries.any { it.value.active } }
-            .distinctUntilChanged()
-            .stateIn(applicationScope, SharingStarted.WhileSubscribed(), false)
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = false,
+            )
 
     /** Are there any media notifications, excluding the recommendations? */
     val hasAnyMedia: StateFlow<Boolean> =
         mediaFilterRepository.selectedUserEntries
             .mapLatest { entries -> entries.isNotEmpty() }
-            .distinctUntilChanged()
-            .stateIn(applicationScope, SharingStarted.WhileSubscribed(), false)
-
-    /** The most recent list of loaded media controls. */
-    val mediaDataLoadedStates: Flow<List<MediaDataLoadingModel>> =
-        mediaFilterRepository.mediaDataLoadedStates
-
-    /** The most recent change to loaded media recommendations. */
-    val recommendationsLoadingState: Flow<SmartspaceMediaLoadingModel> =
-        mediaFilterRepository.recommendationsLoadingState
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = false,
+            )
 
     /** The most recent sorted set for user media instances */
-    val sortedMedia: Flow<List<MediaCommonModel>> =
-        mediaFilterRepository.sortedMedia.map { it.values.toList() }
+    val sortedMedia: StateFlow<List<MediaCommonModel>> =
+        mediaFilterRepository.sortedMedia
+            .mapLatest { it.values.toList() }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = emptyList(),
+            )
 
     override fun start() {
         if (!mediaFlags.isMediaControlsRefactorEnabled()) {
@@ -210,6 +216,10 @@
         return mediaDataProcessor.dismissMediaData(key, delay)
     }
 
+    fun removeMediaControl(instanceId: InstanceId, delay: Long) {
+        mediaDataProcessor.dismissMediaData(instanceId, delay)
+    }
+
     override fun dismissSmartspaceRecommendation(key: String, delay: Long) {
         return mediaDataProcessor.dismissSmartspaceRecommendation(key, delay)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt
index 74cd2fe..c0bb628 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt
@@ -43,14 +43,18 @@
 import com.android.systemui.statusbar.NotificationLockscreenUserManager
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.kotlin.pairwiseBy
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedInject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.map
 
 /** Encapsulates business logic for single media control. */
-class MediaControlInteractor(
+class MediaControlInteractor
+@AssistedInject
+constructor(
     @Application applicationContext: Context,
-    private val instanceId: InstanceId,
+    @Assisted private val instanceId: InstanceId,
     repository: MediaFilterRepository,
     private val mediaDataProcessor: MediaDataProcessor,
     private val keyguardStateController: KeyguardStateController,
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/factory/MediaControlInteractorFactory.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/factory/MediaControlInteractorFactory.kt
new file mode 100644
index 0000000..d568600
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/factory/MediaControlInteractorFactory.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.domain.pipeline.interactor.factory
+
+import com.android.internal.logging.InstanceId
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.media.controls.domain.pipeline.interactor.MediaControlInteractor
+import dagger.assisted.AssistedFactory
+
+/** Factory to create [MediaControlInteractor] for each media control. */
+@SysUISingleton
+@AssistedFactory
+interface MediaControlInteractorFactory {
+
+    fun create(instanceId: InstanceId): MediaControlInteractor
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaCommonModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaCommonModel.kt
index 83e2765..562fe7a 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaCommonModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaCommonModel.kt
@@ -16,11 +16,13 @@
 
 package com.android.systemui.media.controls.shared.model
 
-import com.android.internal.logging.InstanceId
-
 /** Models any type of media. */
 sealed class MediaCommonModel {
-    data class MediaControl(val instanceId: InstanceId) : MediaCommonModel()
+    data class MediaControl(
+        val mediaLoadedModel: MediaDataLoadingModel.Loaded,
+        val canBeRemoved: Boolean = false
+    ) : MediaCommonModel()
 
-    data class MediaRecommendations(val key: String) : MediaCommonModel()
+    data class MediaRecommendations(val recsLoadingModel: SmartspaceMediaLoadingModel) :
+        MediaCommonModel()
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaDataLoadingModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaDataLoadingModel.kt
index bd42a4d..170f1f7 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaDataLoadingModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaDataLoadingModel.kt
@@ -20,27 +20,17 @@
 
 /** Models media data loading state. */
 sealed class MediaDataLoadingModel {
-    /** The initial loading state when no media data has yet loaded. */
-    data object Unknown : MediaDataLoadingModel()
+
+    abstract val instanceId: InstanceId
 
     /** Media data has been loaded. */
     data class Loaded(
-        val instanceId: InstanceId,
+        override val instanceId: InstanceId,
         val immediatelyUpdateUi: Boolean = true,
-    ) : MediaDataLoadingModel() {
-
-        /** Returns true if [other] has the same instance id, false otherwise. */
-        fun equalInstanceIds(other: MediaDataLoadingModel): Boolean {
-            return when (other) {
-                is Loaded -> other.instanceId == instanceId
-                is Removed -> other.instanceId == instanceId
-                Unknown -> false
-            }
-        }
-    }
+    ) : MediaDataLoadingModel()
 
     /** Media data has been removed. */
     data class Removed(
-        val instanceId: InstanceId,
+        override val instanceId: InstanceId,
     ) : MediaDataLoadingModel()
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/SmartspaceMediaLoadingModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/SmartspaceMediaLoadingModel.kt
index 6c1e536..90ddadf 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/SmartspaceMediaLoadingModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/SmartspaceMediaLoadingModel.kt
@@ -18,18 +18,18 @@
 
 /** Models smartspace media loading state. */
 sealed class SmartspaceMediaLoadingModel {
-    /** The initial loading state when no smartspace media has yet loaded. */
-    data object Unknown : SmartspaceMediaLoadingModel()
+
+    abstract val key: String
 
     /** Smartspace media has been loaded. */
     data class Loaded(
-        val key: String,
+        override val key: String,
         val isPrioritized: Boolean = false,
     ) : SmartspaceMediaLoadingModel()
 
     /** Smartspace media has been removed. */
     data class Removed(
-        val key: String,
+        override val key: String,
         val immediatelyUpdateUi: Boolean = true,
     ) : SmartspaceMediaLoadingModel()
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
index d15d45a..eb716d4c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
@@ -656,8 +656,14 @@
         if (width == widthInSceneContainerPx && height == heightInSceneContainerPx) {
             return
         }
+        if (width <= 0 || height <= 0) {
+            // reject as invalid
+            return
+        }
         widthInSceneContainerPx = width
         heightInSceneContainerPx = height
+        mediaCarouselScrollHandler.playerWidthPlusPadding =
+            width + context.resources.getDimensionPixelSize(R.dimen.qs_media_padding)
         updatePlayers(recreateMedia = true)
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaViewModelCallback.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaViewModelCallback.kt
new file mode 100644
index 0000000..952b134
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaViewModelCallback.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui.util
+
+import androidx.recyclerview.widget.DiffUtil
+import com.android.systemui.media.controls.ui.viewmodel.MediaCommonViewModel
+
+/** A [DiffUtil.Callback] to calculate difference between old and new media view-model list. */
+class MediaViewModelCallback(
+    private val old: List<MediaCommonViewModel>,
+    private val new: List<MediaCommonViewModel>,
+) : DiffUtil.Callback() {
+
+    override fun getOldListSize(): Int {
+        return old.size
+    }
+
+    override fun getNewListSize(): Int {
+        return new.size
+    }
+
+    override fun areItemsTheSame(oldIndex: Int, newIndex: Int): Boolean {
+        val oldItem = old[oldIndex]
+        val newItem = new[newIndex]
+        return if (
+            oldItem is MediaCommonViewModel.MediaControl &&
+                newItem is MediaCommonViewModel.MediaControl
+        ) {
+            oldItem.instanceId == newItem.instanceId
+        } else {
+            oldItem is MediaCommonViewModel.MediaRecommendations &&
+                newItem is MediaCommonViewModel.MediaRecommendations
+        }
+    }
+
+    override fun areContentsTheSame(oldIndex: Int, newIndex: Int): Boolean {
+        val oldItem = old[oldIndex]
+        val newItem = new[newIndex]
+        return if (
+            oldItem is MediaCommonViewModel.MediaControl &&
+                newItem is MediaCommonViewModel.MediaControl
+        ) {
+            oldItem.immediatelyUpdateUi == newItem.immediatelyUpdateUi
+        } else if (
+            oldItem is MediaCommonViewModel.MediaRecommendations &&
+                newItem is MediaCommonViewModel.MediaRecommendations
+        ) {
+            oldItem.key == newItem.key && oldItem.loadingEnabled == newItem.loadingEnabled
+        } else {
+            false
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaViewModelListUpdateCallback.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaViewModelListUpdateCallback.kt
new file mode 100644
index 0000000..c356ae2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaViewModelListUpdateCallback.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui.util
+
+import androidx.recyclerview.widget.ListUpdateCallback
+import com.android.systemui.media.controls.ui.viewmodel.MediaCommonViewModel
+
+/** A [ListUpdateCallback] to apply media events needed to reach the new state. */
+class MediaViewModelListUpdateCallback(
+    private val old: List<MediaCommonViewModel>,
+    private val new: List<MediaCommonViewModel>,
+    private val onAdded: (MediaCommonViewModel) -> Unit,
+    private val onUpdated: (MediaCommonViewModel) -> Unit,
+    private val onRemoved: (MediaCommonViewModel) -> Unit,
+    private val onMoved: (MediaCommonViewModel, Int, Int) -> Unit,
+) : ListUpdateCallback {
+
+    override fun onInserted(position: Int, count: Int) {
+        for (i in position until position + count) {
+            onAdded(new[i])
+        }
+    }
+
+    override fun onRemoved(position: Int, count: Int) {
+        for (i in position until position + count) {
+            onRemoved(old[i])
+        }
+    }
+
+    override fun onMoved(fromPosition: Int, toPosition: Int) {
+        onMoved(old[fromPosition], fromPosition, toPosition)
+    }
+
+    override fun onChanged(position: Int, count: Int, payload: Any?) {
+        for (i in position until position + count) {
+            onUpdated(new[i])
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModel.kt
new file mode 100644
index 0000000..303a5e9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModel.kt
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui.viewmodel
+
+import android.content.Context
+import com.android.internal.logging.InstanceId
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor
+import com.android.systemui.media.controls.domain.pipeline.interactor.factory.MediaControlInteractorFactory
+import com.android.systemui.media.controls.shared.model.MediaCommonModel
+import com.android.systemui.media.controls.util.MediaFlags
+import com.android.systemui.media.controls.util.MediaUiEventLogger
+import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener
+import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider
+import com.android.systemui.util.Utils
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+/** Models UI state and handles user inputs for media carousel */
+@SysUISingleton
+class MediaCarouselViewModel
+@Inject
+constructor(
+    @Application private val applicationScope: CoroutineScope,
+    @Application private val applicationContext: Context,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+    private val visualStabilityProvider: VisualStabilityProvider,
+    private val interactor: MediaCarouselInteractor,
+    private val controlInteractorFactory: MediaControlInteractorFactory,
+    private val recommendationsViewModel: MediaRecommendationsViewModel,
+    private val logger: MediaUiEventLogger,
+    private val mediaFlags: MediaFlags,
+) {
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    val mediaItems: StateFlow<List<MediaCommonViewModel>> =
+        conflatedCallbackFlow {
+                val listener = OnReorderingAllowedListener { trySend(Unit) }
+                visualStabilityProvider.addPersistentReorderingAllowedListener(listener)
+                trySend(Unit)
+                awaitClose { visualStabilityProvider.removeReorderingAllowedListener(listener) }
+            }
+            .flatMapLatest {
+                interactor.sortedMedia.map { sortedItems ->
+                    buildList {
+                        val reorderAllowed = isReorderingAllowed()
+                        sortedItems.forEach { commonModel ->
+                            if (!reorderAllowed || !modelsPendingRemoval.contains(commonModel)) {
+                                when (commonModel) {
+                                    is MediaCommonModel.MediaControl ->
+                                        add(toViewModel(commonModel))
+                                    is MediaCommonModel.MediaRecommendations ->
+                                        add(toViewModel(commonModel))
+                                }
+                            }
+                        }
+                        if (reorderAllowed) {
+                            modelsPendingRemoval.clear()
+                        }
+                    }
+                }
+            }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = emptyList(),
+            )
+
+    private val mediaControlByInstanceId =
+        mutableMapOf<InstanceId, MediaCommonViewModel.MediaControl>()
+
+    private var mediaRecs: MediaCommonViewModel.MediaRecommendations? = null
+
+    private var modelsPendingRemoval: MutableSet<MediaCommonModel> = mutableSetOf()
+
+    fun onSwipeToDismiss() {
+        logger.logSwipeDismiss()
+        interactor.onSwipeToDismiss()
+    }
+
+    private fun toViewModel(
+        commonModel: MediaCommonModel.MediaControl
+    ): MediaCommonViewModel.MediaControl {
+        val instanceId = commonModel.mediaLoadedModel.instanceId
+        return mediaControlByInstanceId[instanceId]?.copy(
+            immediatelyUpdateUi = commonModel.mediaLoadedModel.immediatelyUpdateUi
+        )
+            ?: MediaCommonViewModel.MediaControl(
+                    instanceId = instanceId,
+                    immediatelyUpdateUi = commonModel.mediaLoadedModel.immediatelyUpdateUi,
+                    controlViewModel = createMediaControlViewModel(instanceId),
+                    onAdded = { onMediaControlAddedOrUpdated(it, commonModel) },
+                    onRemoved = { _, _ ->
+                        interactor.removeMediaControl(instanceId, delay = 0L)
+                        mediaControlByInstanceId.remove(instanceId)
+                    },
+                    onUpdated = { onMediaControlAddedOrUpdated(it, commonModel) },
+                )
+                .also { mediaControlByInstanceId[instanceId] = it }
+    }
+
+    private fun createMediaControlViewModel(instanceId: InstanceId): MediaControlViewModel {
+        return MediaControlViewModel(
+            applicationScope = applicationScope,
+            applicationContext = applicationContext,
+            backgroundDispatcher = backgroundDispatcher,
+            interactor = controlInteractorFactory.create(instanceId),
+            logger = logger,
+        )
+    }
+
+    private fun toViewModel(
+        commonModel: MediaCommonModel.MediaRecommendations
+    ): MediaCommonViewModel.MediaRecommendations {
+        return mediaRecs?.copy(
+            key = commonModel.recsLoadingModel.key,
+            loadingEnabled =
+                interactor.isRecommendationActive() || mediaFlags.isPersistentSsCardEnabled()
+        )
+            ?: MediaCommonViewModel.MediaRecommendations(
+                    key = commonModel.recsLoadingModel.key,
+                    loadingEnabled =
+                        interactor.isRecommendationActive() ||
+                            mediaFlags.isPersistentSsCardEnabled(),
+                    recsViewModel = recommendationsViewModel,
+                    onAdded = { commonViewModel ->
+                        onMediaRecommendationAddedOrUpdated(commonViewModel)
+                    },
+                    onRemoved = { _, immediatelyRemove ->
+                        onMediaRecommendationRemoved(commonModel, immediatelyRemove)
+                    },
+                    onUpdated = { commonViewModel ->
+                        onMediaRecommendationAddedOrUpdated(commonViewModel)
+                    },
+                )
+                .also { mediaRecs = it }
+    }
+
+    private fun onMediaControlAddedOrUpdated(
+        commonViewModel: MediaCommonViewModel,
+        commonModel: MediaCommonModel.MediaControl
+    ) {
+        // TODO (b/330897926) log smartspace card reported (SMARTSPACE_CARD_RECEIVED)
+        if (commonModel.canBeRemoved && !Utils.useMediaResumption(applicationContext)) {
+            // This media control is due for removal as it is now paused + timed out, and resumption
+            // setting is off.
+            if (isReorderingAllowed()) {
+                commonViewModel.onRemoved(commonViewModel, true)
+            } else {
+                modelsPendingRemoval.add(commonModel)
+            }
+        } else {
+            modelsPendingRemoval.remove(commonModel)
+        }
+    }
+
+    private fun onMediaRecommendationAddedOrUpdated(commonViewModel: MediaCommonViewModel) {
+        if (!interactor.isRecommendationActive()) {
+            if (!mediaFlags.isPersistentSsCardEnabled()) {
+                commonViewModel.onRemoved(commonViewModel, true)
+            }
+        } else {
+            // TODO (b/330897926) log smartspace card reported (SMARTSPACE_CARD_RECEIVED)
+        }
+    }
+
+    private fun onMediaRecommendationRemoved(
+        commonModel: MediaCommonModel.MediaRecommendations,
+        immediatelyRemove: Boolean
+    ) {
+        if (immediatelyRemove || isReorderingAllowed()) {
+            interactor.dismissSmartspaceRecommendation(commonModel.recsLoadingModel.key, 0L)
+            // TODO if not immediate remove update host visibility
+        } else {
+            modelsPendingRemoval.add(commonModel)
+        }
+    }
+
+    private fun isReorderingAllowed(): Boolean {
+        return visualStabilityProvider.isReorderingAllowed
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCommonViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCommonViewModel.kt
new file mode 100644
index 0000000..253f194
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCommonViewModel.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui.viewmodel
+
+import com.android.internal.logging.InstanceId
+
+/** Models media view model UI state. */
+sealed class MediaCommonViewModel {
+
+    abstract val onAdded: (MediaCommonViewModel) -> Unit
+    abstract val onRemoved: (MediaCommonViewModel, Boolean) -> Unit
+    abstract val onUpdated: (MediaCommonViewModel) -> Unit
+
+    data class MediaControl(
+        val instanceId: InstanceId,
+        val immediatelyUpdateUi: Boolean,
+        val controlViewModel: MediaControlViewModel,
+        override val onAdded: (MediaCommonViewModel) -> Unit,
+        override val onRemoved: (MediaCommonViewModel, Boolean) -> Unit,
+        override val onUpdated: (MediaCommonViewModel) -> Unit,
+    ) : MediaCommonViewModel()
+
+    data class MediaRecommendations(
+        val key: String,
+        val loadingEnabled: Boolean,
+        val recsViewModel: MediaRecommendationsViewModel,
+        override val onAdded: (MediaCommonViewModel) -> Unit,
+        override val onRemoved: (MediaCommonViewModel, Boolean) -> Unit,
+        override val onUpdated: (MediaCommonViewModel) -> Unit,
+    ) : MediaCommonViewModel()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt
index d74506d..52e49d6 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt
@@ -41,19 +41,21 @@
 import com.android.systemui.monet.Style
 import com.android.systemui.res.R
 import com.android.systemui.util.kotlin.sample
-import java.util.concurrent.Executor
 import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
 
 /** Models UI state and handles user input for a media control. */
 class MediaControlViewModel(
     @Application private val applicationContext: Context,
+    @Application private val applicationScope: CoroutineScope,
     @Background private val backgroundDispatcher: CoroutineDispatcher,
-    @Background private val backgroundExecutor: Executor,
     private val interactor: MediaControlInteractor,
     private val logger: MediaUiEventLogger,
 ) {
@@ -159,10 +161,12 @@
                 if (model.isResume && model.resumeProgress != null) {
                     seekBarViewModel.updateStaticProgress(model.resumeProgress)
                 } else {
-                    backgroundExecutor.execute {
-                        seekBarViewModel.updateController(
-                            model.token?.let { MediaController(applicationContext, it) }
-                        )
+                    applicationScope.launch {
+                        withContext(backgroundDispatcher) {
+                            seekBarViewModel.updateController(
+                                model.token?.let { MediaController(applicationContext, it) }
+                            )
+                        }
                     }
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
index 042fb63f..4ee2db7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
@@ -75,10 +75,6 @@
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
-        // set layer to make alpha animation of brightness slider nicer - otherwise elements
-        // of slider are animated separately and it doesn't look good. See b/329244723
-        setLayerType(LAYER_TYPE_HARDWARE, null);
-
         mQSPanelContainer = findViewById(R.id.expanded_qs_scroll_view);
         mQSPanel = findViewById(R.id.quick_settings_panel);
         mHeader = findViewById(R.id.header);
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 ab0b0b7..2f6919f 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
@@ -56,7 +56,7 @@
                 // TODO(b/330200163) Add an Up from Bottom to be able to collapse the shade
                 // while customizing
             } else {
-                sceneInteractor.previousScene.map { previousScene ->
+                sceneInteractor.previousScene(ignored = Scenes.QuickSettings).map { previousScene ->
                     mapOf(
                         Back to UserActionResult(previousScene ?: Scenes.Shade),
                         Swipe(SwipeDirection.Up) to UserActionResult(previousScene ?: Scenes.Shade),
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
index 0239455..8ced222 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
@@ -140,12 +140,31 @@
             )
 
     /**
-     * The previous scene.
+     * The previous scene (or `null` if the previous scene is the [ignored] scene).
      *
      * This is effectively the previous value of [currentScene] which means that all caveats, for
      * example regarding when in a transition the current scene changes, apply.
+     *
+     * @param ignored If the previous scene is the same as [ignored], `null` is emitted. This is
+     *   designed to reduce the chances of a scene using [previousScene] naively to then set up a
+     *   user action that ends up leading to itself, which is an illegal operation that would cause
+     *   a crash.
      */
-    val previousScene: StateFlow<SceneKey?> = repository.previousScene
+    fun previousScene(
+        ignored: SceneKey? = null,
+    ): StateFlow<SceneKey?> {
+        fun SceneKey?.nullifyIfIgnored(): SceneKey? {
+            return this?.takeIf { this != ignored }
+        }
+
+        return repository.previousScene
+            .map { it.nullifyIfIgnored() }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = repository.previousScene.value.nullifyIfIgnored(),
+            )
+    }
 
     /**
      * Returns the keys of all scenes in the container.
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
index 4774eb3..98b4ab8 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
@@ -253,7 +253,8 @@
             // Track the previous scene (sans Bouncer), so that we know where to go when the device
             // is unlocked whilst on the bouncer.
             val previousScene =
-                sceneInteractor.previousScene
+                sceneInteractor
+                    .previousScene()
                     .filterNot { it == Scenes.Bouncer }
                     .stateIn(this, SharingStarted.Eagerly, initialValue = null)
             deviceUnlockedInteractor.deviceUnlockStatus
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
index 231b284..ef7829f 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
@@ -19,15 +19,22 @@
 import android.view.MotionEvent
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.UserAction
+import com.android.compose.animation.scene.UserActionResult
 import com.android.systemui.classifier.Classifier
 import com.android.systemui.classifier.domain.interactor.FalsingInteractor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.Scene
 import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.stateIn
 
 /** Models UI state for the scene container. */
 @SysUISingleton
@@ -37,6 +44,7 @@
     private val sceneInteractor: SceneInteractor,
     private val falsingInteractor: FalsingInteractor,
     private val powerInteractor: PowerInteractor,
+    scenes: Set<@JvmSuppressWildcards Scene>,
 ) {
     /**
      * Keys of all scenes in the container.
@@ -52,6 +60,23 @@
     /** Whether the container is visible. */
     val isVisible: StateFlow<Boolean> = sceneInteractor.isVisible
 
+    private val destinationScenesBySceneKey =
+        scenes.associate { scene -> scene.key to scene.destinationScenes }
+
+    fun currentDestinationScenes(
+        scope: CoroutineScope,
+    ): StateFlow<Map<UserAction, UserActionResult>> {
+        return currentScene
+            .flatMapLatestConflated { currentSceneKey ->
+                checkNotNull(destinationScenesBySceneKey[currentSceneKey])
+            }
+            .stateIn(
+                scope = scope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = emptyMap(),
+            )
+    }
+
     /**
      * Binds the given flow so the system remembers it.
      *
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt
index 254c133..e2dc092 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt
@@ -42,6 +42,7 @@
 import com.android.systemui.screenshot.scroll.ScrollCaptureController
 import com.android.systemui.screenshot.ui.ScreenshotAnimationController
 import com.android.systemui.screenshot.ui.ScreenshotShelfView
+import com.android.systemui.screenshot.ui.SwipeGestureListener
 import com.android.systemui.screenshot.ui.binder.ScreenshotShelfViewBinder
 import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel
 import dagger.assisted.Assisted
@@ -75,9 +76,17 @@
     override var isPendingSharedTransition = false
 
     private val animationController = ScreenshotAnimationController(view)
+    private val swipeGestureListener =
+        SwipeGestureListener(
+            view,
+            onDismiss = { requestDismissal(ScreenshotEvent.SCREENSHOT_SWIPE_DISMISSED, it) },
+            onCancel = { animationController.getSwipeReturnAnimation().start() }
+        )
 
     init {
-        ScreenshotShelfViewBinder.bind(view, viewModel, LayoutInflater.from(context))
+        ScreenshotShelfViewBinder.bind(view, viewModel, LayoutInflater.from(context)) {
+            swipeGestureListener.onMotionEvent(it)
+        }
         addPredictiveBackListener { requestDismissal(SCREENSHOT_DISMISSED_OTHER) }
         setOnKeyListener { requestDismissal(SCREENSHOT_DISMISSED_OTHER) }
         debugLog(DEBUG_WINDOW) { "adding OnComputeInternalInsetsListener" }
@@ -111,6 +120,10 @@
     override fun setChipIntents(imageData: SavedImageData) {}
 
     override fun requestDismissal(event: ScreenshotEvent?) {
+        requestDismissal(event, getDismissalVelocity())
+    }
+
+    private fun requestDismissal(event: ScreenshotEvent?, velocity: Float) {
         debugLog(DEBUG_DISMISS) { "screenshot dismissal requested: $event" }
 
         // If we're already animating out, don't restart the animation
@@ -119,7 +132,7 @@
             return
         }
         event?.let { logger.log(it, 0, packageName) }
-        val animator = animationController.getExitAnimation()
+        val animator = animationController.getSwipeDismissAnimation(velocity)
         animator.addListener(
             object : AnimatorListenerAdapter() {
                 override fun onAnimationStart(animator: Animator) {
@@ -222,6 +235,12 @@
         )
     }
 
+    private fun getDismissalVelocity(): Float {
+        val isLTR = view.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR
+        // dismiss to the left in LTR locales, to the right in RTL
+        return if (isLTR) -1.5f else 1.5f
+    }
+
     @AssistedFactory
     interface Factory : ScreenshotViewProxy.Factory {
         override fun getProxy(context: Context, displayId: Int): ScreenshotShelfViewProxy
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt
index 2c17873..f3c421e 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt
@@ -20,9 +20,12 @@
 import android.animation.AnimatorListenerAdapter
 import android.animation.ValueAnimator
 import android.view.View
+import com.android.systemui.res.R
+import kotlin.math.abs
 
-class ScreenshotAnimationController(private val view: View) {
+class ScreenshotAnimationController(private val view: ScreenshotShelfView) {
     private var animator: Animator? = null
+    private val actionContainer = view.requireViewById<View>(R.id.actions_container_background)
 
     fun getEntranceAnimation(): Animator {
         val animator = ValueAnimator.ofFloat(0f, 1f)
@@ -41,19 +44,32 @@
         return animator
     }
 
-    fun getExitAnimation(): Animator {
-        val animator = ValueAnimator.ofFloat(1f, 0f)
-        animator.addUpdateListener { view.alpha = it.animatedValue as Float }
-        animator.addListener(
-            object : AnimatorListenerAdapter() {
-                override fun onAnimationStart(animator: Animator) {
-                    view.alpha = 1f
-                }
-                override fun onAnimationEnd(animator: Animator) {
-                    view.alpha = 0f
-                }
+    fun getSwipeReturnAnimation(): Animator {
+        animator?.cancel()
+        val animator = ValueAnimator.ofFloat(view.translationX, 0f)
+        animator.addUpdateListener { view.translationX = it.animatedValue as Float }
+        this.animator = animator
+        return animator
+    }
+
+    fun getSwipeDismissAnimation(velocity: Float): Animator {
+        val screenWidth = view.resources.displayMetrics.widthPixels
+        // translation at which point the visible UI is fully off the screen (in the direction
+        // according to velocity)
+        val endX =
+            if (velocity < 0) {
+                -1f * actionContainer.right
+            } else {
+                (screenWidth - actionContainer.left).toFloat()
             }
-        )
+        val distance = endX - view.translationX
+        val animator = ValueAnimator.ofFloat(view.translationX, endX)
+        animator.addUpdateListener {
+            view.translationX = it.animatedValue as Float
+            view.alpha = 1f - it.animatedFraction
+        }
+        animator.duration = ((abs(distance / velocity))).toLong()
+
         this.animator = animator
         return animator
     }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt
index b7a03ef..16e23c5 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt
@@ -21,6 +21,7 @@
 import android.graphics.Rect
 import android.graphics.Region
 import android.util.AttributeSet
+import android.view.MotionEvent
 import android.view.View
 import android.widget.ImageView
 import androidx.constraintlayout.widget.ConstraintLayout
@@ -30,6 +31,7 @@
 class ScreenshotShelfView(context: Context, attrs: AttributeSet? = null) :
     ConstraintLayout(context, attrs) {
     lateinit var screenshotPreview: ImageView
+    var onTouchInterceptListener: ((MotionEvent) -> Boolean)? = null
 
     private val displayMetrics = context.resources.displayMetrics
     private val tmpRect = Rect()
@@ -83,4 +85,11 @@
     companion object {
         private const val TOUCH_PADDING_DP = 12f
     }
+
+    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
+        if (onTouchInterceptListener?.invoke(ev) == true) {
+            return true
+        }
+        return super.onInterceptTouchEvent(ev)
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/SwipeGestureListener.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/SwipeGestureListener.kt
new file mode 100644
index 0000000..f288960
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/SwipeGestureListener.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.ui
+
+import android.view.MotionEvent
+import android.view.VelocityTracker
+import android.view.View
+import com.android.systemui.screenshot.FloatingWindowUtil
+import kotlin.math.abs
+import kotlin.math.sign
+
+class SwipeGestureListener(
+    private val view: View,
+    private val onDismiss: (Float) -> Unit,
+    private val onCancel: () -> Unit
+) {
+    private val velocityTracker = VelocityTracker.obtain()
+    private val displayMetrics = view.resources.displayMetrics
+
+    private var startX = 0f
+
+    fun onMotionEvent(ev: MotionEvent): Boolean {
+        ev.offsetLocation(view.translationX, 0f)
+        when (ev.actionMasked) {
+            MotionEvent.ACTION_DOWN -> {
+                velocityTracker.addMovement(ev)
+                startX = ev.rawX
+            }
+            MotionEvent.ACTION_UP -> {
+                velocityTracker.computeCurrentVelocity(1)
+                val xVelocity = velocityTracker.xVelocity
+                if (
+                    abs(xVelocity) > FloatingWindowUtil.dpToPx(displayMetrics, FLING_THRESHOLD_DP)
+                ) {
+                    onDismiss.invoke(xVelocity)
+                    return true
+                } else if (
+                    abs(view.translationX) >
+                        FloatingWindowUtil.dpToPx(displayMetrics, DISMISS_THRESHOLD_DP)
+                ) {
+                    onDismiss.invoke(1.5f * sign(view.translationX))
+                    return true
+                } else {
+                    velocityTracker.clear()
+                    onCancel.invoke()
+                }
+            }
+            MotionEvent.ACTION_MOVE -> {
+                velocityTracker.addMovement(ev)
+                view.translationX = ev.rawX - startX
+            }
+        }
+        return false
+    }
+
+    companion object {
+        private const val DISMISS_THRESHOLD_DP = 80f
+        private const val FLING_THRESHOLD_DP = .8f // dp per ms
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt
index 5f835b3..b8b9ce5 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt
@@ -17,8 +17,8 @@
 package com.android.systemui.screenshot.ui.binder
 
 import android.view.LayoutInflater
+import android.view.MotionEvent
 import android.view.View
-import android.view.ViewGroup
 import android.widget.ImageView
 import android.widget.LinearLayout
 import androidx.lifecycle.Lifecycle
@@ -26,16 +26,20 @@
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.res.R
+import com.android.systemui.screenshot.ui.ScreenshotShelfView
 import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel
 import com.android.systemui.util.children
 import kotlinx.coroutines.launch
 
 object ScreenshotShelfViewBinder {
     fun bind(
-        view: ViewGroup,
+        view: ScreenshotShelfView,
         viewModel: ScreenshotViewModel,
         layoutInflater: LayoutInflater,
+        onTouchListener: (MotionEvent) -> Boolean,
     ) {
+        view.onTouchInterceptListener = onTouchListener
+
         val previewView: ImageView = view.requireViewById(R.id.screenshot_preview)
         val previewBorder = view.requireViewById<View>(R.id.screenshot_preview_border)
         previewView.clipToOutline = true
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderView.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderView.java
index e051dab..92006a4 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderView.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderView.java
@@ -63,6 +63,7 @@
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
+        setLayerType(LAYER_TYPE_HARDWARE, null);
 
         mSlider = requireViewById(R.id.slider);
         mSlider.setAccessibilityLabel(getContentDescription().toString());
diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
index 8f6b9d0..a8481cd 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
@@ -45,6 +45,7 @@
 import com.android.systemui.scene.shared.model.SceneDataSourceDelegator
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.phone.SystemUIDialogFactory
+import com.android.systemui.util.kotlin.BooleanFlowOperators.or
 import com.android.systemui.util.kotlin.collectFlow
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
@@ -137,7 +138,8 @@
     private var isDreaming = false
 
     /** Returns a flow that tracks whether communal hub is available. */
-    fun communalAvailable(): Flow<Boolean> = communalInteractor.isCommunalAvailable
+    fun communalAvailable(): Flow<Boolean> =
+        or(communalInteractor.isCommunalAvailable, communalInteractor.editModeOpen)
 
     /**
      * Creates the container view containing the glanceable hub UI.
diff --git a/packages/SystemUI/src/com/android/systemui/startable/Dependencies.kt b/packages/SystemUI/src/com/android/systemui/startable/Dependencies.kt
index 8eed097..396c5f2 100644
--- a/packages/SystemUI/src/com/android/systemui/startable/Dependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/startable/Dependencies.kt
@@ -16,7 +16,8 @@
 package com.android.systemui.startable
 
 import com.android.systemui.CoreStartable
-import kotlin.reflect.KClass
+import java.lang.annotation.Documented
+import javax.inject.Qualifier
 
 /**
  * Allows a [CoreStartable] to declare that it must be started after its dependencies.
@@ -24,7 +25,4 @@
  * This creates a partial, topological ordering. See [com.android.systemui.SystemUIApplication] for
  * how this ordering is enforced at runtime.
  */
-@MustBeDocumented
-@Target(AnnotationTarget.CLASS)
-@Retention(AnnotationRetention.RUNTIME)
-annotation class Dependencies(vararg val value: KClass<*> = [])
+@Qualifier @Documented @Retention(AnnotationRetention.RUNTIME) annotation class Dependencies()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java b/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java
index d2fe20d..8104755 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java
@@ -23,7 +23,6 @@
 
 import com.android.systemui.CoreStartable;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.startable.Dependencies;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 
 import java.lang.annotation.Retention;
@@ -31,7 +30,6 @@
 /**
  * Sends updates to {@link StateListener}s about changes to the status bar state and dozing state
  */
-@Dependencies(CentralSurfaces.class)
 public interface SysuiStatusBarStateController extends StatusBarStateController, CoreStartable {
 
     // TODO: b/115739177 (remove this explicit ordering if we can)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
index 594c191..9bb860f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
@@ -41,6 +41,7 @@
 import com.android.systemui.shade.ShadeSurface;
 import com.android.systemui.shade.ShadeSurfaceImpl;
 import com.android.systemui.shade.carrier.ShadeCarrierGroupController;
+import com.android.systemui.startable.Dependencies;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.NotificationClickNotifier;
 import com.android.systemui.statusbar.NotificationMediaManager;
@@ -52,6 +53,7 @@
 import com.android.systemui.statusbar.notification.collection.NotifCollection;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
 import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
+import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.phone.CentralSurfacesImpl;
 import com.android.systemui.statusbar.phone.ManagedProfileController;
 import com.android.systemui.statusbar.phone.ManagedProfileControllerImpl;
@@ -68,6 +70,8 @@
 import dagger.multibindings.ClassKey;
 import dagger.multibindings.IntoMap;
 
+import java.util.Set;
+
 import javax.inject.Provider;
 
 /**
@@ -157,6 +161,15 @@
     CoreStartable bindsStartStatusBarStateController(StatusBarStateControllerImpl sbsc);
 
     /** */
+    @Provides
+    @IntoMap
+    @Dependencies
+    @ClassKey(SysuiStatusBarStateController.class)
+    static Set<Class<? extends CoreStartable>> providesStatusBarStateControllerDeps() {
+        return Set.of(CentralSurfaces.class);
+    }
+
+    /** */
     @Binds
     StatusBarIconController provideStatusBarIconController(
             StatusBarIconControllerImpl controllerImpl);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java
index ed3a38d..d0db514 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java
@@ -53,6 +53,8 @@
     private int mTintColor;
     @Nullable private Integer mRippleColor;
     private final float[] mCornerRadii = new float[8];
+    private final float[] mFocusOverlayCornerRadii = new float[8];
+    private float mFocusOverlayStroke = 0;
     private boolean mBottomIsRounded;
     private boolean mBottomAmountClips = true;
     private int mActualHeight = -1;
@@ -74,6 +76,7 @@
                 R.color.notification_state_color_dark);
         mNormalColor = Utils.getColorAttrDefaultColor(mContext,
                 com.android.internal.R.attr.materialColorSurfaceContainerHigh);
+        mFocusOverlayStroke = getResources().getDimension(R.dimen.notification_focus_stroke_width);
     }
 
     @Override
@@ -290,16 +293,30 @@
         if (mDontModifyCorners) {
             return;
         }
-        if (mBackground instanceof LayerDrawable) {
-            int numberOfLayers = ((LayerDrawable) mBackground).getNumberOfLayers();
+        if (mBackground instanceof LayerDrawable layerDrawable) {
+            int numberOfLayers = layerDrawable.getNumberOfLayers();
             for (int i = 0; i < numberOfLayers; i++) {
-                GradientDrawable gradientDrawable =
-                        (GradientDrawable) ((LayerDrawable) mBackground).getDrawable(i);
+                GradientDrawable gradientDrawable = (GradientDrawable) layerDrawable.getDrawable(i);
                 gradientDrawable.setCornerRadii(mCornerRadii);
             }
+            updateFocusOverlayRadii(layerDrawable);
         }
     }
 
+    private void updateFocusOverlayRadii(LayerDrawable background) {
+        GradientDrawable overlay =
+                (GradientDrawable) background.findDrawableByLayerId(
+                        R.id.notification_focus_overlay);
+        for (int i = 0; i < mCornerRadii.length; i++) {
+            // in theory subtracting mFocusOverlayStroke/2 should be enough but notification
+            // background is still peeking a bit from below - probably due to antialiasing or
+            // overlay uneven scaling. So let's subtract full mFocusOverlayStroke to make sure the
+            // radius is a bit smaller and covers background corners fully
+            mFocusOverlayCornerRadii[i] = Math.max(0, mCornerRadii[i] - mFocusOverlayStroke);
+        }
+        overlay.setCornerRadii(mFocusOverlayCornerRadii);
+    }
+
     /** Set the current expand animation size. */
     public void setExpandAnimationSize(int width, int height) {
         mExpandAnimationHeight = height;
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 8a1a4f1..fd67586 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
@@ -482,7 +482,6 @@
     private Interpolator mHideXInterpolator = Interpolators.FAST_OUT_SLOW_IN;
 
     private final NotificationSectionsManager mSectionsManager;
-    private boolean mAnimateBottomOnLayout;
     private float mLastSentAppear;
     private float mLastSentExpandedHeight;
     private boolean mWillExpand;
@@ -2941,23 +2940,11 @@
     }
 
     private void updateFirstAndLastBackgroundViews() {
-        NotificationSection firstSection = getFirstVisibleSection();
-        NotificationSection lastSection = getLastVisibleSection();
-        ExpandableView previousFirstChild =
-                firstSection == null ? null : firstSection.getFirstVisibleChild();
-        ExpandableView previousLastChild =
-                lastSection == null ? null : lastSection.getLastVisibleChild();
-
-        ExpandableView firstChild = getFirstChildWithBackground();
         ExpandableView lastChild = getLastChildWithBackground();
         boolean sectionViewsChanged = mSectionsManager.updateFirstAndLastViewsForAllSections(
                 mSections, getChildrenWithBackground());
 
-        if (mAnimationsEnabled && mIsExpanded) {
-        } else {
-        }
         mAmbientState.setLastVisibleBackgroundChild(lastChild);
-        mAnimateBottomOnLayout = false;
         invalidate();
     }
 
@@ -5465,10 +5452,6 @@
         }
     }
 
-    void setAnimateBottomOnLayout(boolean animateBottomOnLayout) {
-        mAnimateBottomOnLayout = animateBottomOnLayout;
-    }
-
     public void setOnPulseHeightChangedListener(Runnable listener) {
         mAmbientState.setOnPulseHeightChangedListener(listener);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 5e719b1..d8a16ce 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -306,10 +306,6 @@
     };
 
     private final DynamicPrivacyController.Listener mDynamicPrivacyControllerListener = () -> {
-        if (mView.isExpanded()) {
-            // The bottom might change because we're using the final actual height of the view
-            mView.setAnimateBottomOnLayout(true);
-        }
         if (!FooterViewRefactor.isEnabled()) {
             // Let's update the footer once the notifications have been updated (in the next frame)
             mView.post(this::updateFooter);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
index 516ec31..4ae5e69 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
@@ -22,11 +22,12 @@
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.scene.shared.model.Scenes
-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.ShadeScrimClipping
 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationTransitionThresholds.EXPANSION_FOR_DELAYED_STACK_FADE_IN
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationTransitionThresholds.EXPANSION_FOR_MAX_SCRIM_ALPHA
 import com.android.systemui.util.kotlin.FlowDumperImpl
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
@@ -52,8 +53,9 @@
     val expandFraction: Flow<Float> =
         combine(
                 shadeInteractor.shadeExpansion,
+                shadeInteractor.qsExpansion,
                 sceneInteractor.transitionState,
-            ) { shadeExpansion, transitionState ->
+            ) { shadeExpansion, qsExpansion, transitionState ->
                 when (transitionState) {
                     is ObservableTransitionState.Idle -> {
                         if (transitionState.scene == Scenes.Lockscreen) {
@@ -70,6 +72,16 @@
                                     transitionState.toScene == Scenes.Shade)
                         ) {
                             1f
+                        } else if (
+                            (transitionState.fromScene == Scenes.Gone ||
+                                transitionState.fromScene == Scenes.Lockscreen) &&
+                                transitionState.toScene == Scenes.QuickSettings
+                        ) {
+                            // during QS expansion, increase fraction at same rate as scrim alpha,
+                            // but start when scrim alpha is at EXPANSION_FOR_DELAYED_STACK_FADE_IN.
+                            (qsExpansion / EXPANSION_FOR_MAX_SCRIM_ALPHA -
+                                    EXPANSION_FOR_DELAYED_STACK_FADE_IN)
+                                .coerceIn(0f, 1f)
                         } else {
                             shadeExpansion
                         }
@@ -125,5 +137,5 @@
 
     /** Whether the notification stack is scrollable or not. */
     val isScrollable: Flow<Boolean> =
-        sceneInteractor.currentScene.map { it == Shade }.dumpWhileCollecting("isScrollable")
+        sceneInteractor.currentScene.map { it == Scenes.Shade }.dumpWhileCollecting("isScrollable")
 }
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 ca19da5..bf3b2c9 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
@@ -93,10 +93,10 @@
     val headsUpHeight: StateFlow<Float> = interactor.headsUpHeight.dumpValue("headsUpHeight")
 
     /**
-     * The amount [0-1] that the shade has been opened. At 0, the shade is closed; at 1, the shade
-     * is open.
+     * The amount [0-1] that the shade or quick settings has been opened. At 0, the shade is closed;
+     * at 1, either the shade or quick settings is open.
      */
-    val expandFraction: Flow<Float> = shadeInteractor.shadeExpansion.dumpValue("expandFraction")
+    val expandFraction: Flow<Float> = shadeInteractor.anyExpansion.dumpValue("expandFraction")
 
     /**
      * The amount in px that the notification stack should scroll due to internal expansion. This
@@ -111,3 +111,11 @@
         interactor.setScrolledToTop(scrolledToTop)
     }
 }
+
+// Expansion fraction thresholds (between 0-1f) at which the corresponding value should be
+// at its maximum, given they are at their minimum value at expansion = 0f.
+object NotificationTransitionThresholds {
+    const val EXPANSION_FOR_MAX_CORNER_RADIUS = 0.1f
+    const val EXPANSION_FOR_MAX_SCRIM_ALPHA = 0.3f
+    const val EXPANSION_FOR_DELAYED_STACK_FADE_IN = 0.5f
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
index 2651d2e..8fb552f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
@@ -37,6 +37,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.keyguard.AuthKeyguardMessageArea;
+import com.android.systemui.CoreStartable;
 import com.android.systemui.Dumpable;
 import com.android.systemui.animation.ActivityTransitionAnimator;
 import com.android.systemui.animation.RemoteAnimationRunnerCompat;
@@ -50,7 +51,7 @@
 import java.io.PrintWriter;
 
 /** */
-public interface CentralSurfaces extends Dumpable, LifecycleOwner {
+public interface CentralSurfaces extends Dumpable, LifecycleOwner, CoreStartable {
     boolean MULTIUSER_DEBUG = false;
     // Should match the values in PhoneWindowManager
     String SYSTEM_DIALOG_REASON_KEY = "reason";
@@ -182,6 +183,9 @@
         return contextForUser.getPackageManager();
     }
 
+    /** Default impl for CoreStartable. */
+    default void start() {}
+
     boolean updateIsKeyguard();
 
     boolean updateIsKeyguard(boolean forceStateChange);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
index 6e24412..4129b3a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
@@ -194,7 +194,7 @@
         @SysUISingleton
         @MobileInputLog
         fun provideMobileInputLogBuffer(factory: LogBufferFactory): LogBuffer {
-            return factory.create("MobileInputLog", 100)
+            return factory.create("MobileInputLog", 300)
         }
 
         @Provides
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
index f573530..36c23d3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
@@ -153,27 +153,45 @@
             // Use default duration, like we did before AvalancheController existed
             return autoDismissMs
         }
-
         val showingList: MutableList<HeadsUpEntry> = mutableListOf()
-        headsUpEntryShowing?.let { showingList.add(it) }
-
+        if (headsUpEntryShowing != null) {
+            showingList.add(headsUpEntryShowing!!)
+        }
         nextList.sort()
         val entryList = showingList + nextList
-        val thisEntryIndex = entryList.indexOf(entry)
+        if (entryList.isEmpty()) {
+            log { "No avalanche HUNs, use default ms: $autoDismissMs" }
+            return autoDismissMs
+        }
+        // entryList.indexOf(entry) returns -1 even when the entry is in entryList
+        var thisEntryIndex = -1
+        for ((i, e) in entryList.withIndex()) {
+            if (e == entry) {
+                thisEntryIndex = i
+            }
+        }
+        if (thisEntryIndex == -1) {
+            log { "Untracked entry, use default ms: $autoDismissMs" }
+            return autoDismissMs
+        }
         val nextEntryIndex = thisEntryIndex + 1
 
         // If last entry, use default duration
         if (nextEntryIndex >= entryList.size) {
+            log { "Last entry, use default ms: $autoDismissMs" }
             return autoDismissMs
         }
         val nextEntry = entryList[nextEntryIndex]
         if (nextEntry.compareNonTimeFields(entry) == -1) {
             // Next entry is higher priority
+            log { "Next entry is higher priority: 500ms" }
             return 500
         } else if (nextEntry.compareNonTimeFields(entry) == 0) {
             // Next entry is same priority
+            log { "Next entry is same priority: 1000ms" }
             return 1000
         } else {
+            log { "Next entry is lower priority, use default ms: $autoDismissMs" }
             return autoDismissMs
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java
index 0d53277..40bb67f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java
@@ -34,6 +34,7 @@
 import android.os.Trace;
 import android.os.UserHandle;
 import android.service.notification.StatusBarNotification;
+import android.telephony.TelephonyManager;
 import android.util.ArraySet;
 import android.util.Log;
 
@@ -62,7 +63,10 @@
     private static final String LOG_TAG = "SNPC";
     private final SensitiveNotificationProtectionControllerLogger mLogger;
     private final PackageManager mPackageManager;
-    private final ArraySet<String> mExemptPackages = new ArraySet<>();
+    // Packages exempt from projection session protections (if they start a projection session)
+    private final ArraySet<String> mSessionProtectionExemptPackages = new ArraySet<>();
+    // Packages exempt from individual notification protections (if they post a notification)
+    private final ArraySet<String> mNotificationProtectionExemptPackages = new ArraySet<>();
     private final ListenerSet<Runnable> mListeners = new ListenerSet<>();
     private volatile MediaProjectionInfo mProjection;
     private SensitiveNotificatioMediaProjectionSession mActiveMediaProjectionSession;
@@ -161,6 +165,7 @@
             MediaProjectionManager mediaProjectionManager,
             IActivityManager activityManager,
             PackageManager packageManager,
+            TelephonyManager telephonyManager,
             @Main Handler mainHandler,
             @Background Executor bgExecutor,
             SensitiveNotificationProtectionControllerLogger logger) {
@@ -191,26 +196,18 @@
         bgExecutor.execute(() -> developerOptionsObserver.onChange(true));
 
         bgExecutor.execute(() -> {
-            ArraySet<String> exemptPackages = new ArraySet<>();
-            // Exempt SystemUI
-            exemptPackages.add(context.getPackageName());
+            ArraySet<String> sessionProtectionExemptPackages =
+                    getSessionProtectionExemptPackages(context, activityManager);
 
-            // Exempt approved bug report handlers
-            try {
-                exemptPackages.addAll(activityManager.getBugreportWhitelistedPackages());
-            } catch (RemoteException e) {
-                Log.e(
-                        LOG_TAG,
-                        "Error adding bug report handlers to exemption, continuing without",
-                        e);
-                // silent failure, skip adding packages to exemption
-            }
+            ArraySet<String> notificationProtectionExemptPackages =
+                    getNotificationProtectionExemptPackages(telephonyManager);
 
             // if currently projecting, notify listeners of exemption changes
             mainHandler.post(() -> {
                 Trace.beginSection("SNPC.exemptPackagesUpdated");
                 try {
-                    updateExemptPackagesAndNotifyListeners(exemptPackages);
+                    updateExemptPackagesAndNotifyListeners(sessionProtectionExemptPackages,
+                            notificationProtectionExemptPackages);
                 } finally {
                     Trace.endSection();
                 }
@@ -220,15 +217,66 @@
         mediaProjectionManager.addCallback(mMediaProjectionCallback, mainHandler);
     }
 
+    @NonNull
+    private static ArraySet<String> getSessionProtectionExemptPackages(Context context,
+            IActivityManager activityManager) {
+        ArraySet<String> sessionProtectionExemptPackages = new ArraySet<>();
+        // Exempt SystemUI
+        sessionProtectionExemptPackages.add(context.getPackageName());
+
+        // Exempt approved bug report handlers
+        try {
+            sessionProtectionExemptPackages.addAll(
+                    activityManager.getBugreportWhitelistedPackages());
+        } catch (RemoteException e) {
+            Log.w(
+                    LOG_TAG,
+                    "Error adding bug report handlers to exemption, continuing without",
+                    e);
+            // silent failure, skip adding packages to exemption
+        }
+        return sessionProtectionExemptPackages;
+    }
+
+    @NonNull
+    private static ArraySet<String> getNotificationProtectionExemptPackages(
+            TelephonyManager telephonyManager) {
+        ArraySet<String> notificationProtectionExemptPackages = new ArraySet<>();
+
+        // Get Emergency Assistance Package, all notifications from this package should not be
+        // hidden/redacted.
+        if (screenshareNotificationHidingBugFix()) {
+            try {
+                String emergencyAssistancePackageName =
+                        telephonyManager.getEmergencyAssistancePackageName();
+                if (emergencyAssistancePackageName != null) {
+                    notificationProtectionExemptPackages.add(emergencyAssistancePackageName);
+                }
+            } catch (IllegalStateException e) {
+                Log.w(
+                        LOG_TAG,
+                        "Error adding emergency assistance package to exemption",
+                        e);
+                // silent failure, skip adding packages to exemption
+            }
+        }
+        return notificationProtectionExemptPackages;
+    }
+
     /**
      * Notify listeners of possible ProjectionState change regardless of current
      * isSensitiveStateActive value. Method used to ensure updates occur after mExemptPackages gets
      * updated, which directly changes the outcome of isSensitiveStateActive
      */
     @MainThread
-    private void updateExemptPackagesAndNotifyListeners(ArraySet<String> exemptPackages) {
+    private void updateExemptPackagesAndNotifyListeners(
+            ArraySet<String> sessionProtectionExemptPackages,
+            ArraySet<String> notificationProtectionExemptPackages) {
         Assert.isMainThread();
-        mExemptPackages.addAll(exemptPackages);
+        mSessionProtectionExemptPackages.addAll(sessionProtectionExemptPackages);
+        if (screenshareNotificationHidingBugFix()) {
+            mNotificationProtectionExemptPackages.addAll(notificationProtectionExemptPackages);
+        }
 
         if (mProjection != null) {
             updateProjectionStateAndNotifyListeners(mProjection);
@@ -258,7 +306,8 @@
         if (mDisableScreenShareProtections) {
             Log.w(LOG_TAG, "Screen share protections disabled");
             return null;
-        } else if (info != null && mExemptPackages.contains(info.getPackageName())) {
+        } else if (info != null
+                && mSessionProtectionExemptPackages.contains(info.getPackageName())) {
             Log.w(LOG_TAG, "Screen share protections exempt for package " + info.getPackageName());
             return null;
         } else if (info != null && canRecordSensitiveContent(info.getPackageName())) {
@@ -322,6 +371,11 @@
             return false; // do not hide/redact notifications from system uid
         }
 
+        if (screenshareNotificationHidingBugFix()
+                && mNotificationProtectionExemptPackages.contains(sbn.getPackageName())) {
+            return false; // do not hide/redact notifications from emergency app
+        }
+
         // Only protect/redact notifications if the developer has not explicitly set notification
         // visibility as public and users has not adjusted default channel visibility to private
         boolean notificationRequestsRedaction = entry.isNotificationVisibilityPrivate();
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/BooleanFlowOperators.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/BooleanFlowOperators.kt
index 693a835..b300885 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/BooleanFlowOperators.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/BooleanFlowOperators.kt
@@ -18,6 +18,7 @@
 
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.map
 
 object BooleanFlowOperators {
@@ -31,7 +32,7 @@
      * ```
      */
     fun and(vararg flows: Flow<Boolean>): Flow<Boolean> =
-        combine(flows.asIterable()) { values -> values.all { it } }
+        combine(flows.asIterable()) { values -> values.all { it } }.distinctUntilChanged()
 
     /**
      * Logical NOT operator for a boolean flow.
@@ -48,5 +49,5 @@
      * determine the result.
      */
     fun or(vararg flows: Flow<Boolean>): Flow<Boolean> =
-        combine(flows.asIterable()) { values -> values.any { it } }
+        combine(flows.asIterable()) { values -> values.any { it } }.distinctUntilChanged()
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/SystemUIApplicationTest.kt b/packages/SystemUI/tests/src/com/android/systemui/SystemUIApplicationTest.kt
index e157fc5..4f7610a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/SystemUIApplicationTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/SystemUIApplicationTest.kt
@@ -27,7 +27,6 @@
 import com.android.systemui.flags.systemPropertiesHelper
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.process.processWrapper
-import com.android.systemui.startable.Dependencies
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import javax.inject.Provider
@@ -56,6 +55,19 @@
     @Mock private lateinit var bootCompleteCache: BootCompleteCacheImpl
     @Mock private lateinit var initController: InitController
 
+    class StartableA : TestableStartable()
+    class StartableB : TestableStartable()
+    class StartableC : TestableStartable()
+    class StartableD : TestableStartable()
+    class StartableE : TestableStartable()
+
+    val dependencyMap: Map<Class<*>, Set<Class<out CoreStartable>>> =
+        mapOf(
+            StartableC::class.java to setOf(StartableA::class.java),
+            StartableD::class.java to setOf(StartableA::class.java, StartableB::class.java),
+            StartableE::class.java to setOf(StartableD::class.java, StartableB::class.java),
+        )
+
     private val startableA = StartableA()
     private val startableB = StartableB()
     private val startableC = StartableC()
@@ -76,6 +88,7 @@
         whenever(sysuiComponent.provideBootCacheImpl()).thenReturn(bootCompleteCache)
         whenever(sysuiComponent.createDumpManager()).thenReturn(kosmos.dumpManager)
         whenever(sysuiComponent.initController).thenReturn(initController)
+        whenever(sysuiComponent.startableDependencies).thenReturn(dependencyMap)
         kosmos.processWrapper.systemUser = true
 
         app.setContextAvailableCallback(contextAvailableCallback)
@@ -168,13 +181,4 @@
             startOrder++
         }
     }
-
-    class StartableA : TestableStartable()
-    class StartableB : TestableStartable()
-
-    @Dependencies(StartableA::class) class StartableC : TestableStartable()
-
-    @Dependencies(StartableA::class, StartableB::class) class StartableD : TestableStartable()
-
-    @Dependencies(StartableD::class, StartableB::class) class StartableE : TestableStartable()
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt
index f534ba5..8435a03 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt
@@ -37,6 +37,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
@@ -160,4 +161,17 @@
                     to = KeyguardState.LOCKSCREEN,
                 )
         }
+
+    @Test
+    fun testTransitionToAlternateBouncer() =
+        testScope.runTest {
+            kosmos.fakeKeyguardBouncerRepository.setAlternateVisible(true)
+            runCurrent()
+
+            assertThat(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.DREAMING,
+                    to = KeyguardState.ALTERNATE_BOUNCER,
+                )
+        }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
index 085b70e..671b09b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
@@ -755,35 +755,6 @@
         }
 
     @Test
-    fun goneToDreamingLockscreenHosted() =
-        testScope.runTest {
-            // GIVEN a device that is not dreaming or dozing
-            keyguardRepository.setDreamingWithOverlay(false)
-            keyguardRepository.setDozeTransitionModel(
-                DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH)
-            )
-            runCurrent()
-
-            // GIVEN a prior transition has run to GONE
-            runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.GONE)
-
-            // WHEN the device begins to dream with the lockscreen hosted dream
-            keyguardRepository.setDreamingWithOverlay(true)
-            keyguardRepository.setIsActiveDreamLockscreenHosted(true)
-            advanceTimeBy(100L)
-
-            assertThat(transitionRepository)
-                .startedTransition(
-                    to = KeyguardState.DREAMING_LOCKSCREEN_HOSTED,
-                    from = KeyguardState.GONE,
-                    ownerName = "FromGoneTransitionInteractor",
-                    animatorAssertion = { it.isNotNull() }
-                )
-
-            coroutineContext.cancelChildren()
-        }
-
-    @Test
     fun goneToGlanceableHub() =
         testScope.runTest {
             // GIVEN a prior transition has run to GONE
@@ -1244,9 +1215,15 @@
             keyguardRepository.setKeyguardOccluded(true)
             runCurrent()
 
-            // GIVEN device is docked
+            // GIVEN device is docked/communal is available
             dockManager.setIsDocked(true)
             dockManager.setDockEvent(DockManager.STATE_DOCKED)
+            val idleTransitionState =
+                MutableStateFlow<ObservableTransitionState>(
+                    ObservableTransitionState.Idle(CommunalScenes.Communal)
+                )
+            communalInteractor.setTransitionState(idleTransitionState)
+            runCurrent()
 
             // WHEN occlusion ends
             keyguardRepository.setKeyguardOccluded(false)
@@ -1372,6 +1349,8 @@
 
             // WHEN the keyguard is occluded and device wakes up and is no longer dreaming
             keyguardRepository.setDreaming(false)
+            testScheduler.advanceTimeBy(150) // The dreaming signal is debounced.
+            runCurrent()
             keyguardRepository.setKeyguardOccluded(true)
             powerInteractor.setAwakeForTest()
             runCurrent()
@@ -1379,7 +1358,7 @@
             // THEN a transition to OCCLUDED should occur
             assertThat(transitionRepository)
                 .startedTransition(
-                    ownerName = "FromDreamingTransitionInteractor",
+                    ownerName = "FromDreamingTransitionInteractor(Occluded but no longer dreaming)",
                     from = KeyguardState.DREAMING,
                     to = KeyguardState.OCCLUDED,
                     animatorAssertion = { it.isNotNull() },
@@ -1516,7 +1495,7 @@
             // THEN a transition to OCCLUDED should occur
             assertThat(transitionRepository)
                 .startedTransition(
-                    ownerName = "FromAodTransitionInteractor",
+                    ownerName = "FromAodTransitionInteractor(isOccluded = true)",
                     from = KeyguardState.AOD,
                     to = KeyguardState.OCCLUDED,
                     animatorAssertion = { it.isNotNull() },
@@ -1555,7 +1534,8 @@
             runTransitionAndSetWakefulness(KeyguardState.AOD, KeyguardState.LOCKSCREEN)
             runCurrent()
 
-            // WHEN the device begins to sleep (first power button press)...
+            // WHEN the device begins to sleep (first power button press), which starts
+            // LS -> DOZING...
             powerInteractor.setAsleepForTest()
             runCurrent()
             reset(transitionRepository)
@@ -1568,11 +1548,11 @@
             }
             runCurrent()
 
-            // THEN a transition from LOCKSCREEN => OCCLUDED should occur
+            // THEN a transition from DOZING => OCCLUDED should occur
             assertThat(transitionRepository)
                 .startedTransition(
-                    ownerName = "FromLockscreenTransitionInteractor",
-                    from = KeyguardState.LOCKSCREEN,
+                    ownerName = "FromDozingTransitionInteractor",
+                    from = KeyguardState.DOZING,
                     to = KeyguardState.OCCLUDED,
                     animatorAssertion = { it.isNotNull() },
                 )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/util/KeyguardTransitionRepositorySpySubject.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/util/KeyguardTransitionRepositorySpySubject.kt
index a08e491..af785c2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/util/KeyguardTransitionRepositorySpySubject.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/util/KeyguardTransitionRepositorySpySubject.kt
@@ -21,6 +21,8 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionInfo
 import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.withArgCaptor
 import com.google.common.truth.FailureMetadata
 import com.google.common.truth.Subject
 import com.google.common.truth.Truth
@@ -28,6 +30,8 @@
 import junit.framework.Assert.assertEquals
 import kotlin.test.fail
 import org.mockito.Mockito
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
 
 /** [Subject] used to make assertions about a [Mockito.spy] KeyguardTransitionRepository. */
 class KeyguardTransitionRepositorySpySubject
@@ -75,34 +79,19 @@
         animatorAssertion: (Subject) -> Unit,
         modeOnCanceled: TransitionModeOnCanceled? = null,
     ) {
-        // TODO(b/331799060): Remove this workaround once atest supports mocking suspend functions.
-        Mockito.mockingDetails(repository).invocations.forEach { invocation ->
-            if (invocation.method.equals(KeyguardTransitionRepository::startTransition.name)) {
-                val transitionInfo = invocation.arguments.firstOrNull() as TransitionInfo
+        withArgCaptor<TransitionInfo> { verify(repository).startTransition(capture()) }
+            .also { transitionInfo ->
                 assertEquals(to, transitionInfo.to)
                 animatorAssertion.invoke(Truth.assertThat(transitionInfo.animator))
                 from?.let { assertEquals(it, transitionInfo.from) }
                 ownerName?.let { assertEquals(it, transitionInfo.ownerName) }
                 modeOnCanceled?.let { assertEquals(it, transitionInfo.modeOnCanceled) }
-                invocation.markVerified()
             }
-        }
     }
 
     /** Verifies that [KeyguardTransitionRepository.startTransition] was never called. */
     suspend fun noTransitionsStarted() {
-        // TODO(b/331799060): Remove this workaround once atest supports mocking suspend functions.
-        Mockito.mockingDetails(repository).invocations.forEach {
-            if (
-                it.method.equals(KeyguardTransitionRepository::startTransition.name) &&
-                    !it.isVerified
-            ) {
-                fail(
-                    "Expected no transitions started, however this transition was started: " +
-                        it.arguments.firstOrNull()
-                )
-            }
-        }
+        verify(repository, never()).startTransition(any())
     }
 
     companion object {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt
index 8f73811..b3cfcf2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt
@@ -28,6 +28,7 @@
 import com.android.systemui.media.controls.MediaTestUtils
 import com.android.systemui.media.controls.data.repository.MediaFilterRepository
 import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_RESUME
+import com.android.systemui.media.controls.shared.model.MediaCommonModel
 import com.android.systemui.media.controls.shared.model.MediaData
 import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel
 import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
@@ -165,86 +166,88 @@
     @Test
     fun onDataLoadedForCurrentUser_updatesLoadedStates() =
         testScope.runTest {
-            val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates)
-            val mediaDataLoadingModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId))
+            val sortedMedia by collectLastValue(repository.sortedMedia)
+            val mediaCommonModel =
+                MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(dataMain.instanceId))
 
             mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
 
             verify(listener)
                 .onMediaDataLoaded(eq(KEY), eq(null), eq(dataMain), eq(true), eq(0), eq(false))
-            assertThat(mediaDataLoadedStates).isEqualTo(mediaDataLoadingModel)
+            assertThat(sortedMedia?.values).containsExactly(mediaCommonModel)
         }
 
     @Test
     fun onDataLoadedForGuest_doesNotUpdateLoadedStates() =
         testScope.runTest {
-            val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates)
-            val mediaLoadedStatesModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId))
+            val sortedMedia by collectLastValue(repository.sortedMedia)
+            val mediaCommonModel =
+                MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(dataMain.instanceId))
 
             mediaDataFilter.onMediaDataLoaded(KEY, null, dataGuest)
 
             verify(listener, never())
                 .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean())
-            assertThat(mediaDataLoadedStates).isNotEqualTo(mediaLoadedStatesModel)
+            assertThat(sortedMedia?.values).doesNotContain(mediaCommonModel)
         }
 
     @Test
     fun onRemovedForCurrent_updatesLoadedStates() =
         testScope.runTest {
-            val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates)
-            val mediaLoadedStatesModel =
-                mutableListOf(MediaDataLoadingModel.Loaded(dataMain.instanceId))
+            val sortedMedia by collectLastValue(repository.sortedMedia)
+            val mediaCommonModel =
+                MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(dataMain.instanceId))
 
             // GIVEN a media was removed for main user
             mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
 
-            assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel)
+            assertThat(sortedMedia?.values).containsExactly(mediaCommonModel)
 
-            mediaLoadedStatesModel.remove(MediaDataLoadingModel.Loaded(dataMain.instanceId))
             mediaDataFilter.onMediaDataRemoved(KEY)
 
             verify(listener).onMediaDataRemoved(eq(KEY))
-            assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel)
+            assertThat(sortedMedia?.values).doesNotContain(mediaCommonModel)
         }
 
     @Test
     fun onRemovedForGuest_doesNotUpdateLoadedStates() =
         testScope.runTest {
-            val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates)
+            val sortedMedia by collectLastValue(repository.sortedMedia)
 
             // GIVEN a media was removed for guest user
             mediaDataFilter.onMediaDataLoaded(KEY, null, dataGuest)
             mediaDataFilter.onMediaDataRemoved(KEY)
 
             verify(listener, never()).onMediaDataRemoved(eq(KEY))
-            assertThat(mediaDataLoadedStates).isEmpty()
+            assertThat(sortedMedia).isEmpty()
         }
 
     @Test
     fun onUserSwitched_removesOldUserControls() =
         testScope.runTest {
-            val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates)
-            val mediaLoadedStatesModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId))
+            val sortedMedia by collectLastValue(repository.sortedMedia)
+            val mediaLoaded = MediaDataLoadingModel.Loaded(dataMain.instanceId)
 
             // GIVEN that we have a media loaded for main user
             mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
 
-            assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel)
+            assertThat(sortedMedia?.values)
+                .containsExactly(MediaCommonModel.MediaControl(mediaLoaded))
 
             // and we switch to guest user
             setUser(USER_GUEST)
 
             // THEN we should remove the main user's media
             verify(listener).onMediaDataRemoved(eq(KEY))
-            assertThat(mediaDataLoadedStates).isEmpty()
+            assertThat(sortedMedia).isEmpty()
         }
 
     @Test
     fun onUserSwitched_addsNewUserControls() =
         testScope.runTest {
-            val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates)
-            val guestLoadedStatesModel = listOf(MediaDataLoadingModel.Loaded(dataGuest.instanceId))
-            val mainLoadedStatesModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId))
+            val sortedMedia by collectLastValue(repository.sortedMedia)
+            val guestLoadedStatesModel = MediaDataLoadingModel.Loaded(dataGuest.instanceId)
+            val mainLoadedStatesModel = MediaDataLoadingModel.Loaded(dataMain.instanceId)
 
             // GIVEN that we had some media for both users
             mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
@@ -267,14 +270,16 @@
                     anyInt(),
                     anyBoolean()
                 )
-            assertThat(mediaDataLoadedStates).isEqualTo(guestLoadedStatesModel)
-            assertThat(mediaDataLoadedStates).isNotEqualTo(mainLoadedStatesModel)
+            assertThat(sortedMedia?.values)
+                .containsExactly(MediaCommonModel.MediaControl(guestLoadedStatesModel))
+            assertThat(sortedMedia?.values)
+                .doesNotContain(MediaCommonModel.MediaControl(mainLoadedStatesModel))
         }
 
     @Test
     fun onProfileChanged_profileUnavailable_updateStates() =
         testScope.runTest {
-            val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates)
+            val sortedMedia by collectLastValue(repository.sortedMedia)
 
             // GIVEN that we had some media for both profiles
             mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
@@ -283,10 +288,11 @@
             // and we change profile status
             setPrivateProfileUnavailable()
 
-            val mediaLoadedStatesModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId))
+            val mediaLoadedStatesModel = MediaDataLoadingModel.Loaded(dataMain.instanceId)
             // THEN we should remove the private profile media
             verify(listener).onMediaDataRemoved(eq(KEY_ALT))
-            assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel)
+            assertThat(sortedMedia?.values)
+                .containsExactly(MediaCommonModel.MediaControl(mediaLoadedStatesModel))
         }
 
     @Test
@@ -515,14 +521,14 @@
             val selectedUserEntries by collectLastValue(repository.selectedUserEntries)
             val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData)
             val reactivatedKey by collectLastValue(repository.reactivatedId)
-            val recommendationsLoadingState by
-                collectLastValue(repository.recommendationsLoadingState)
+            val sortedMedia by collectLastValue(repository.sortedMedia)
             val recommendationsLoadingModel =
                 SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY, isPrioritized = true)
 
             mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
 
-            assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel)
+            assertThat(sortedMedia?.values)
+                .containsExactly(MediaCommonModel.MediaRecommendations(recommendationsLoadingModel))
             assertThat(
                     hasActiveMediaOrRecommendation(
                         selectedUserEntries,
@@ -544,14 +550,13 @@
             val selectedUserEntries by collectLastValue(repository.selectedUserEntries)
             val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData)
             val reactivatedKey by collectLastValue(repository.reactivatedId)
-            val recommendationsLoadingState by
-                collectLastValue(repository.recommendationsLoadingState)
+            val sortedMedia by collectLastValue(repository.sortedMedia)
 
             whenever(smartspaceData.isActive).thenReturn(false)
 
             mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
 
-            assertThat(recommendationsLoadingState).isEqualTo(SmartspaceMediaLoadingModel.Unknown)
+            assertThat(sortedMedia).isEmpty()
             assertThat(
                     hasActiveMediaOrRecommendation(
                         selectedUserEntries,
@@ -574,16 +579,22 @@
             val selectedUserEntries by collectLastValue(repository.selectedUserEntries)
             val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData)
             val reactivatedKey by collectLastValue(repository.reactivatedId)
-            val recommendationsLoadingState by
-                collectLastValue(repository.recommendationsLoadingState)
-            val recommendationsLoadingModel =
-                SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY, isPrioritized = true)
+            val sortedMedia by collectLastValue(repository.sortedMedia)
+            val recsCommonModel =
+                MediaCommonModel.MediaRecommendations(
+                    SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY, isPrioritized = true)
+                )
+            val controlCommonModel =
+                MediaCommonModel.MediaControl(
+                    MediaDataLoadingModel.Loaded(dataMain.instanceId),
+                    true
+                )
             val dataOld = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
             mediaDataFilter.onMediaDataLoaded(KEY, null, dataOld)
             clock.advanceTime(MediaDataFilterImpl.SMARTSPACE_MAX_AGE + 100)
             mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
 
-            assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel)
+            assertThat(sortedMedia?.values).containsExactly(recsCommonModel, controlCommonModel)
             assertThat(
                     hasActiveMediaOrRecommendation(
                         selectedUserEntries,
@@ -605,8 +616,7 @@
             val selectedUserEntries by collectLastValue(repository.selectedUserEntries)
             val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData)
             val reactivatedKey by collectLastValue(repository.reactivatedId)
-            val recommendationsLoadingState by
-                collectLastValue(repository.recommendationsLoadingState)
+            val sortedMedia by collectLastValue(repository.sortedMedia)
             whenever(smartspaceData.isActive).thenReturn(false)
 
             val dataOld = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
@@ -614,7 +624,12 @@
             clock.advanceTime(MediaDataFilterImpl.SMARTSPACE_MAX_AGE + 100)
             mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
 
-            assertThat(recommendationsLoadingState).isEqualTo(SmartspaceMediaLoadingModel.Unknown)
+            assertThat(sortedMedia?.values)
+                .doesNotContain(
+                    MediaCommonModel.MediaRecommendations(
+                        SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY)
+                    )
+                )
             assertThat(
                     hasActiveMediaOrRecommendation(
                         selectedUserEntries,
@@ -635,18 +650,20 @@
             val selectedUserEntries by collectLastValue(repository.selectedUserEntries)
             val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData)
             val reactivatedKey by collectLastValue(repository.reactivatedId)
-            val recommendationsLoadingState by
-                collectLastValue(repository.recommendationsLoadingState)
-            val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates)
+            val sortedMedia by collectLastValue(repository.sortedMedia)
 
             whenever(smartspaceData.isActive).thenReturn(false)
 
             // WHEN we have media that was recently played, but not currently active
             val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
-            val mediaLoadedStatesModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId))
+            val controlCommonModel =
+                MediaCommonModel.MediaControl(
+                    MediaDataLoadingModel.Loaded(dataMain.instanceId),
+                    true
+                )
             mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
 
-            assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel)
+            assertThat(sortedMedia?.values).containsExactly(controlCommonModel)
             verify(listener)
                 .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
 
@@ -654,7 +671,7 @@
             mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
 
             // THEN we should treat the media as not active instead
-            assertThat(recommendationsLoadingState).isEqualTo(SmartspaceMediaLoadingModel.Unknown)
+            assertThat(sortedMedia?.values).containsExactly(controlCommonModel)
             assertThat(
                     hasActiveMediaOrRecommendation(
                         selectedUserEntries,
@@ -677,16 +694,18 @@
             val selectedUserEntries by collectLastValue(repository.selectedUserEntries)
             val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData)
             val reactivatedKey by collectLastValue(repository.reactivatedId)
-            val recommendationsLoadingState by
-                collectLastValue(repository.recommendationsLoadingState)
-            val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates)
+            val sortedMedia by collectLastValue(repository.sortedMedia)
             whenever(smartspaceData.isValid()).thenReturn(false)
 
             // WHEN we have media that was recently played, but not currently active
             val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
-            val mediaLoadedStatesModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId))
+            val controlCommonModel =
+                MediaCommonModel.MediaControl(
+                    MediaDataLoadingModel.Loaded(dataMain.instanceId),
+                    true
+                )
             mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
-            assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel)
+            assertThat(sortedMedia?.values).containsExactly(controlCommonModel)
             verify(listener)
                 .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
 
@@ -696,7 +715,7 @@
 
             // THEN we should treat the media as active instead
             val dataCurrentAndActive = dataCurrent.copy(active = true)
-            assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel)
+            assertThat(sortedMedia?.values).containsExactly(controlCommonModel)
             assertThat(
                     hasActiveMediaOrRecommendation(
                         selectedUserEntries,
@@ -715,7 +734,6 @@
                     eq(true)
                 )
             // Smartspace update shouldn't be propagated for the empty rec list.
-            assertThat(recommendationsLoadingState).isEqualTo(SmartspaceMediaLoadingModel.Unknown)
             verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
             verify(logger, never()).logRecommendationAdded(any(), any())
             verify(logger).logRecommendationActivated(eq(APP_UID), eq(PACKAGE), eq(INSTANCE_ID))
@@ -727,17 +745,22 @@
             val selectedUserEntries by collectLastValue(repository.selectedUserEntries)
             val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData)
             val reactivatedKey by collectLastValue(repository.reactivatedId)
-            val recommendationsLoadingState by
-                collectLastValue(repository.recommendationsLoadingState)
-            val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates)
+            val sortedMedia by collectLastValue(repository.sortedMedia)
             // WHEN we have media that was recently played, but not currently active
             val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
-            val mediaDataLoadingModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId))
-            val recommendationsLoadingModel = SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY)
+            val controlCommonModel =
+                MediaCommonModel.MediaControl(
+                    MediaDataLoadingModel.Loaded(dataMain.instanceId),
+                    true
+                )
+            val recsCommonModel =
+                MediaCommonModel.MediaRecommendations(
+                    SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY)
+                )
 
             mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
 
-            assertThat(mediaDataLoadedStates).isEqualTo(mediaDataLoadingModel)
+            assertThat(sortedMedia?.values).containsExactly(controlCommonModel)
             verify(listener)
                 .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
 
@@ -756,7 +779,6 @@
                     eq(100),
                     eq(true)
                 )
-            assertThat(mediaDataLoadedStates).isEqualTo(mediaDataLoadingModel)
             assertThat(
                     hasActiveMediaOrRecommendation(
                         selectedUserEntries,
@@ -766,7 +788,7 @@
                 )
                 .isTrue()
             // Smartspace update should also be propagated but not prioritized.
-            assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel)
+            assertThat(sortedMedia?.values).containsExactly(controlCommonModel, recsCommonModel)
             verify(listener)
                 .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
             verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID)
@@ -779,15 +801,13 @@
             val selectedUserEntries by collectLastValue(repository.selectedUserEntries)
             val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData)
             val reactivatedKey by collectLastValue(repository.reactivatedId)
-            val recommendationsLoadingState by
-                collectLastValue(repository.recommendationsLoadingState)
-            val recommendationsLoadingModel = SmartspaceMediaLoadingModel.Removed(SMARTSPACE_KEY)
+            val sortedMedia by collectLastValue(repository.sortedMedia)
 
             mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
             mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
 
             verify(listener).onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
-            assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel)
+            assertThat(sortedMedia?.values).isEmpty()
             assertThat(
                     hasActiveMediaOrRecommendation(
                         selectedUserEntries,
@@ -805,15 +825,16 @@
             val selectedUserEntries by collectLastValue(repository.selectedUserEntries)
             val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData)
             val reactivatedKey by collectLastValue(repository.reactivatedId)
-            val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates)
-            val recommendationsLoadingState by
-                collectLastValue(repository.recommendationsLoadingState)
-            val recommendationsLoadingModel = SmartspaceMediaLoadingModel.Removed(SMARTSPACE_KEY)
-            val mediaLoadedStatesModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId))
+            val sortedMedia by collectLastValue(repository.sortedMedia)
+            val controlCommonModel =
+                MediaCommonModel.MediaControl(
+                    MediaDataLoadingModel.Loaded(dataMain.instanceId),
+                    true
+                )
             val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
             mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
 
-            assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel)
+            assertThat(sortedMedia?.values).containsExactly(controlCommonModel)
             verify(listener)
                 .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
 
@@ -830,12 +851,11 @@
                     eq(100),
                     eq(true)
                 )
-            assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel)
 
             mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
 
             verify(listener).onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
-            assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel)
+            assertThat(sortedMedia?.values).containsExactly(controlCommonModel)
             assertThat(
                     hasActiveMediaOrRecommendation(
                         selectedUserEntries,
@@ -853,9 +873,11 @@
             val selectedUserEntries by collectLastValue(repository.selectedUserEntries)
             val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData)
             val reactivatedKey by collectLastValue(repository.reactivatedId)
-            val recommendationsLoadingState by
-                collectLastValue(repository.recommendationsLoadingState)
-            val recommendationsLoadingModel = SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY)
+            val sortedMedia by collectLastValue(repository.sortedMedia)
+            val recsCommonModel =
+                MediaCommonModel.MediaRecommendations(
+                    SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY)
+                )
             whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
             whenever(smartspaceData.isActive).thenReturn(false)
 
@@ -863,7 +885,7 @@
 
             verify(listener)
                 .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
-            assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel)
+            assertThat(sortedMedia?.values).containsExactly(recsCommonModel)
             assertThat(
                     hasActiveMediaOrRecommendation(
                         selectedUserEntries,
@@ -882,11 +904,16 @@
             val selectedUserEntries by collectLastValue(repository.selectedUserEntries)
             val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData)
             val reactivatedKey by collectLastValue(repository.reactivatedId)
-            val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates)
-            val recommendationsLoadingState by
-                collectLastValue(repository.recommendationsLoadingState)
-            val recommendationsLoadingModel = SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY)
-            val mediaLoadedStatesModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId))
+            val sortedMedia by collectLastValue(repository.sortedMedia)
+            val recsCommonModel =
+                MediaCommonModel.MediaRecommendations(
+                    SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY)
+                )
+            val controlCommonModel =
+                MediaCommonModel.MediaControl(
+                    MediaDataLoadingModel.Loaded(dataMain.instanceId),
+                    true
+                )
 
             whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
             whenever(smartspaceData.isActive).thenReturn(false)
@@ -897,7 +924,7 @@
 
             verify(listener)
                 .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
-            assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel)
+            assertThat(sortedMedia?.values).containsExactly(controlCommonModel)
 
             // And an inactive recommendation is loaded
             mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
@@ -907,7 +934,7 @@
                 .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
             verify(listener, never())
                 .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean())
-            assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel)
+            assertThat(sortedMedia?.values).containsExactly(controlCommonModel, recsCommonModel)
             assertThat(
                     hasActiveMediaOrRecommendation(
                         selectedUserEntries,
@@ -945,18 +972,23 @@
             val selectedUserEntries by collectLastValue(repository.selectedUserEntries)
             val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData)
             val reactivatedKey by collectLastValue(repository.reactivatedId)
-            val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates)
-            val recommendationsLoadingState by
-                collectLastValue(repository.recommendationsLoadingState)
-            val recommendationsLoadingModel = SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY)
-            val mediaLoadedStatesModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId))
+            val sortedMedia by collectLastValue(repository.sortedMedia)
+            val recsCommonModel =
+                MediaCommonModel.MediaRecommendations(
+                    SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY)
+                )
+            val controlCommonModel =
+                MediaCommonModel.MediaControl(
+                    MediaDataLoadingModel.Loaded(dataMain.instanceId),
+                    true
+                )
             // WHEN we have media that was recently played, but not currently active
             val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
             mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
 
             verify(listener)
                 .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
-            assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel)
+            assertThat(sortedMedia?.values).containsExactly(controlCommonModel)
 
             // AND we get a smartspace signal with extra to trigger resume
             runCurrent()
@@ -975,7 +1007,7 @@
                     eq(100),
                     eq(true)
                 )
-            assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel)
+            assertThat(sortedMedia?.values).containsExactly(controlCommonModel, recsCommonModel)
             assertThat(
                     hasActiveMediaOrRecommendation(
                         selectedUserEntries,
@@ -985,7 +1017,6 @@
                 )
                 .isTrue()
             // And update the smartspace data state, but not prioritized
-            assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel)
             verify(listener)
                 .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
         }
@@ -993,11 +1024,16 @@
     @Test
     fun smartspaceLoaded_notShouldTriggerResume_doesNotTrigger() =
         testScope.runTest {
-            val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates)
-            val recommendationsLoadingState by
-                collectLastValue(repository.recommendationsLoadingState)
-            val recommendationsLoadingModel = SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY)
-            val mediaLoadedStatesModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId))
+            val sortedMedia by collectLastValue(repository.sortedMedia)
+            val recsCommonModel =
+                MediaCommonModel.MediaRecommendations(
+                    SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY)
+                )
+            val controlCommonModel =
+                MediaCommonModel.MediaControl(
+                    MediaDataLoadingModel.Loaded(dataMain.instanceId),
+                    true
+                )
 
             // WHEN we have media that was recently played, but not currently active
             val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
@@ -1005,7 +1041,7 @@
 
             verify(listener)
                 .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
-            assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel)
+            assertThat(sortedMedia?.values).containsExactly(controlCommonModel)
 
             // AND we get a smartspace signal with extra to not trigger resume
             val extras = Bundle().apply { putBoolean(EXTRA_KEY_TRIGGER_RESUME, false) }
@@ -1018,7 +1054,7 @@
             // But the smartspace update is still propagated
             verify(listener)
                 .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
-            assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel)
+            assertThat(sortedMedia?.values).containsExactly(controlCommonModel, recsCommonModel)
         }
 
     private fun hasActiveMediaOrRecommendation(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
index ee03236..fd9daf8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
@@ -37,6 +37,7 @@
 import com.android.systemui.communal.shared.model.CommunalScenes
 import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
 import com.android.systemui.communal.util.CommunalColors
+import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
@@ -325,6 +326,19 @@
             }
         }
 
+    @Test
+    fun editMode_communalAvailable() =
+        with(kosmos) {
+            testScope.runTest {
+                val available by collectLastValue(underTest.communalAvailable())
+                setCommunalAvailable(false)
+
+                assertThat(available).isFalse()
+                communalInteractor.setEditModeOpen(true)
+                assertThat(available).isTrue()
+            }
+        }
+
     private fun initAndAttachContainerView() {
         containerView = View(context)
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
index 7b73528c..efd7f99 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
@@ -22,6 +22,7 @@
 import android.app.Notification
 import android.app.PendingIntent
 import android.app.Person
+import android.platform.test.annotations.DisableFlags
 import android.service.notification.NotificationListenerService.REASON_USER_STOPPED
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
@@ -193,6 +194,7 @@
 
     /** Regression test for b/192379214. */
     @Test
+    @DisableFlags(android.app.Flags.FLAG_UPDATE_RANKING_TIME)
     fun onEntryUpdated_notificationWhenIsZero_timeHidden() {
         val notification = NotificationEntryBuilder(createOngoingCallNotifEntry())
         notification.modifyNotification(context).setWhen(0)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerFlagDisabledTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerFlagDisabledTest.kt
index 358709f..3e20f68 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerFlagDisabledTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerFlagDisabledTest.kt
@@ -21,6 +21,7 @@
 import android.media.projection.MediaProjectionManager
 import android.os.Handler
 import android.platform.test.annotations.DisableFlags
+import android.telephony.TelephonyManager
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.server.notification.Flags
@@ -46,6 +47,7 @@
     @Mock private lateinit var activityManager: IActivityManager
     @Mock private lateinit var mediaProjectionManager: MediaProjectionManager
     @Mock private lateinit var packageManager: PackageManager
+    @Mock private lateinit var telephonyManager: TelephonyManager
     private lateinit var controller: SensitiveNotificationProtectionControllerImpl
 
     @Before
@@ -59,6 +61,7 @@
                 mediaProjectionManager,
                 activityManager,
                 packageManager,
+                telephonyManager,
                 handler,
                 FakeExecutor(FakeSystemClock()),
                 logger
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt
index 4ace163..2127057 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt
@@ -37,6 +37,7 @@
 import android.platform.test.annotations.RequiresFlagsEnabled
 import android.platform.test.flag.junit.DeviceFlagsValueProvider
 import android.provider.Settings.Global.DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS
+import android.telephony.TelephonyManager
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
 import androidx.test.filters.SmallTest
@@ -89,6 +90,7 @@
     @Mock private lateinit var activityManager: IActivityManager
     @Mock private lateinit var mediaProjectionManager: MediaProjectionManager
     @Mock private lateinit var packageManager: PackageManager
+    @Mock private lateinit var telephonyManager: TelephonyManager
     @Mock private lateinit var listener1: Runnable
     @Mock private lateinit var listener2: Runnable
     @Mock private lateinit var listener3: Runnable
@@ -141,6 +143,9 @@
         whenever(packageManager.checkPermission(anyString(), anyString()))
             .thenReturn(PackageManager.PERMISSION_DENIED)
 
+        whenever(telephonyManager.getEmergencyAssistancePackageName())
+            .thenReturn(EMERGENCY_ASSISTANCE_PACKAGE_NAME)
+
         executor = FakeExecutor(FakeSystemClock())
         globalSettings = FakeGlobalSettings()
         controller =
@@ -150,6 +155,7 @@
                 mediaProjectionManager,
                 activityManager,
                 packageManager,
+                telephonyManager,
                 mockExecutorHandler(executor),
                 executor,
                 logger
@@ -407,6 +413,26 @@
     }
 
     @Test
+    @DisableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING_BUG_FIX)
+    fun shouldProtectNotification_projectionActive_isFromEmergencyPackage_fixDisabled_true() {
+        mediaProjectionCallback.onStart(mediaProjectionInfo)
+
+        val notificationEntry = setupNotificationEntry(EMERGENCY_ASSISTANCE_PACKAGE_NAME)
+
+        assertTrue(controller.shouldProtectNotification(notificationEntry))
+    }
+
+    @Test
+    @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING_BUG_FIX)
+    fun shouldProtectNotification_projectionActive_isFromEmergencyPackage_false() {
+        mediaProjectionCallback.onStart(mediaProjectionInfo)
+
+        val notificationEntry = setupNotificationEntry(EMERGENCY_ASSISTANCE_PACKAGE_NAME)
+
+        assertFalse(controller.shouldProtectNotification(notificationEntry))
+    }
+
+    @Test
     fun shouldProtectNotification_projectionActive_sysuiExempt_false() {
         // SystemUi context package name is exempt, but in test scenarios its
         // com.android.systemui.tests so use that instead of hardcoding
@@ -742,6 +768,7 @@
         private const val TEST_PROJECTION_PACKAGE_NAME =
             "com.android.systemui.statusbar.policy.projectionpackage"
         private const val TEST_PACKAGE_NAME = "com.android.systemui.statusbar.policy.testpackage"
+        private const val EMERGENCY_ASSISTANCE_PACKAGE_NAME = "com.android.test.emergencyassistance"
         private const val BUGREPORT_PACKAGE_NAME = "com.android.test.bugreporthandler"
     }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt
index bff10a1..1200866 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.deviceentry.domain.interactor
 
 import com.android.systemui.authentication.domain.interactor.authenticationInteractor
+import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
 import com.android.systemui.deviceentry.data.repository.deviceEntryRepository
 import com.android.systemui.flags.fakeSystemPropertiesHelper
 import com.android.systemui.keyguard.domain.interactor.trustInteractor
@@ -39,5 +40,6 @@
             trustInteractor = trustInteractor,
             deviceUnlockedInteractor = deviceUnlockedInteractor,
             systemPropertiesHelper = fakeSystemPropertiesHelper,
+            alternateBouncerInteractor = alternateBouncerInteractor,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/SceneContainerRule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/SceneContainerRule.kt
index 9ec1481..5f9f6b4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/SceneContainerRule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/SceneContainerRule.kt
@@ -43,6 +43,21 @@
                         SceneContainerFlag.isEnabled
                     )
                 }
+                // Get the flag value, treating the unset error as false.
+                val sceneContainerAconfigEnabled = try {
+                    com.android.systemui.Flags.sceneContainer()
+                } catch (e: Exception) {
+                    false
+                }
+                if (sceneContainerAconfigEnabled) {
+                    Assert.assertTrue(
+                            "FLAG_SCENE_CONTAINER is enabled but SceneContainerFlag.isEnabled" +
+                                    " is false.  Use `.andSceneContainer()` from" +
+                                    " SceneContainerFlagParameterization.kt to parameterize this" +
+                                    " flag correctly.",
+                            SceneContainerFlag.isEnabled
+                    )
+                }
                 if (
                     description.hasAnnotation<BrokenWithSceneContainer>() &&
                         SceneContainerFlag.isEnabled
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/factory/MediaControlInteractorFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/factory/MediaControlInteractorFactoryKosmos.kt
new file mode 100644
index 0000000..461eaa2
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/factory/MediaControlInteractorFactoryKosmos.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.domain.pipeline.interactor.factory
+
+import android.content.applicationContext
+import com.android.internal.logging.InstanceId
+import com.android.systemui.activityIntentHelper
+import com.android.systemui.bluetooth.mockBroadcastDialogController
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.media.controls.data.repository.mediaFilterRepository
+import com.android.systemui.media.controls.domain.pipeline.interactor.MediaControlInteractor
+import com.android.systemui.media.controls.domain.pipeline.mediaDataProcessor
+import com.android.systemui.media.mediaOutputDialogManager
+import com.android.systemui.plugins.activityStarter
+import com.android.systemui.statusbar.notificationLockscreenUserManager
+import com.android.systemui.statusbar.policy.keyguardStateController
+
+val Kosmos.mediaControlInteractorFactory by
+    Kosmos.Fixture {
+        object : MediaControlInteractorFactory {
+            override fun create(instanceId: InstanceId): MediaControlInteractor {
+                return MediaControlInteractor(
+                    applicationContext = applicationContext,
+                    instanceId = instanceId,
+                    repository = mediaFilterRepository,
+                    mediaDataProcessor = mediaDataProcessor,
+                    keyguardStateController = keyguardStateController,
+                    activityStarter = activityStarter,
+                    activityIntentHelper = activityIntentHelper,
+                    lockscreenUserManager = notificationLockscreenUserManager,
+                    mediaOutputDialogManager = mediaOutputDialogManager,
+                    broadcastDialogController = mockBroadcastDialogController,
+                )
+            }
+        }
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModelKosmos.kt
new file mode 100644
index 0000000..9a181cd4
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModelKosmos.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui.viewmodel
+
+import android.content.applicationContext
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.media.controls.domain.pipeline.interactor.factory.mediaControlInteractorFactory
+import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor
+import com.android.systemui.media.controls.util.mediaFlags
+import com.android.systemui.media.controls.util.mediaUiEventLogger
+import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider
+
+val Kosmos.mediaCarouselViewModel by
+    Kosmos.Fixture {
+        MediaCarouselViewModel(
+            applicationScope = applicationCoroutineScope,
+            applicationContext = applicationContext,
+            backgroundDispatcher = testDispatcher,
+            visualStabilityProvider = VisualStabilityProvider(),
+            interactor = mediaCarouselInteractor,
+            controlInteractorFactory = mediaControlInteractorFactory,
+            recommendationsViewModel = mediaRecommendationsViewModel,
+            logger = mediaUiEventLogger,
+            mediaFlags = mediaFlags,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModelKosmos.kt
index 2f3d3c3..b3fb15f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModelKosmos.kt
@@ -17,8 +17,8 @@
 package com.android.systemui.media.controls.ui.viewmodel
 
 import android.content.applicationContext
-import com.android.systemui.concurrency.fakeExecutor
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.media.controls.domain.pipeline.interactor.mediaControlInteractor
 import com.android.systemui.media.controls.util.mediaUiEventLogger
@@ -26,9 +26,9 @@
 val Kosmos.mediaControlViewModel by
     Kosmos.Fixture {
         MediaControlViewModel(
+            applicationScope = applicationCoroutineScope,
             applicationContext = applicationContext,
             backgroundDispatcher = testDispatcher,
-            backgroundExecutor = fakeExecutor,
             interactor = mediaControlInteractor,
             logger = mediaUiEventLogger,
         )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
index 2cdf76d..7f6a7bd3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
@@ -2,6 +2,8 @@
 
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.scene.shared.model.FakeScene
 import com.android.systemui.scene.shared.model.SceneContainerConfig
 import com.android.systemui.scene.shared.model.Scenes
 
@@ -16,5 +18,18 @@
     )
 }
 
+val Kosmos.fakeScenes by Fixture {
+    sceneKeys
+        .map { key ->
+            FakeScene(
+                scope = testScope.backgroundScope,
+                key = key,
+            )
+        }
+        .toSet()
+}
+
+val Kosmos.scenes by Fixture { fakeScenes }
+
 val Kosmos.initialSceneKey by Fixture { Scenes.Lockscreen }
 val Kosmos.sceneContainerConfig by Fixture { SceneContainerConfig(sceneKeys, initialSceneKey) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeScene.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeScene.kt
new file mode 100644
index 0000000..eeaa9db
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeScene.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene.shared.model
+
+import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.UserAction
+import com.android.compose.animation.scene.UserActionResult
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.onCompletion
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.flow.stateIn
+
+class FakeScene(
+    val scope: CoroutineScope,
+    override val key: SceneKey,
+) : Scene {
+    var isDestinationScenesBeingCollected = false
+
+    private val destinationScenesChannel = Channel<Map<UserAction, UserActionResult>>()
+
+    override val destinationScenes =
+        destinationScenesChannel
+            .receiveAsFlow()
+            .onStart { isDestinationScenesBeingCollected = true }
+            .onCompletion { isDestinationScenesBeingCollected = false }
+            .stateIn(
+                scope = scope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = emptyMap(),
+            )
+
+    suspend fun setDestinationScenes(value: Map<UserAction, UserActionResult>) {
+        destinationScenesChannel.send(value)
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt
new file mode 100644
index 0000000..a47b2e8
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.shade
+
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.compose.animation.scene.SceneKey
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shade.data.repository.FakeShadeRepository
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import org.junit.Assert
+
+/** Sets up shade state for tests for either value of the scene container flag. */
+class ShadeTestUtil constructor(val delegate: ShadeTestUtilDelegate) {
+
+    /** Sets both shade and QS expansion. One value must be zero or values must add up to 1f. */
+    fun setShadeAndQsExpansion(shadeExpansion: Float, qsExpansion: Float) {
+        Assert.assertTrue(
+            "One expansion must be zero or both must add up to 1",
+            shadeExpansion == 0f || qsExpansion == 0f || shadeExpansion + qsExpansion == 1f,
+        )
+        delegate.assertFlagValid()
+        delegate.setShadeAndQsExpansionInternal(shadeExpansion, qsExpansion)
+    }
+}
+
+/** Sets up shade state for tests for a specific value of the scene container flag. */
+interface ShadeTestUtilDelegate {
+    /** Sets both shade and QS expansion. One value must be zero or values must add up to 1f. */
+    fun setShadeAndQsExpansionInternal(shadeExpansion: Float, qsExpansion: Float)
+
+    /** Asserts that the scene container flag matches this implementation. */
+    fun assertFlagValid()
+}
+
+/** Sets up shade state for tests when the scene container flag is disabled. */
+class ShadeTestUtilLegacyImpl(val testScope: TestScope, val shadeRepository: FakeShadeRepository) :
+    ShadeTestUtilDelegate {
+    override fun setShadeAndQsExpansionInternal(shadeExpansion: Float, qsExpansion: Float) {
+        shadeRepository.setLegacyShadeExpansion(shadeExpansion)
+        shadeRepository.setQsExpansion(qsExpansion)
+        testScope.runCurrent()
+    }
+
+    override fun assertFlagValid() {
+        Assert.assertFalse(SceneContainerFlag.isEnabled)
+    }
+}
+
+/** Sets up shade state for tests when the scene container flag is disabled. */
+class ShadeTestUtilSceneImpl(val testScope: TestScope, val sceneInteractor: SceneInteractor) :
+    ShadeTestUtilDelegate {
+    override fun setShadeAndQsExpansionInternal(shadeExpansion: Float, qsExpansion: Float) {
+        if (shadeExpansion == 0f) {
+            setTransitionProgress(Scenes.Lockscreen, Scenes.QuickSettings, qsExpansion)
+        } else if (qsExpansion == 0f) {
+            setTransitionProgress(Scenes.Lockscreen, Scenes.Shade, shadeExpansion)
+        } else {
+            setTransitionProgress(Scenes.Shade, Scenes.QuickSettings, qsExpansion)
+        }
+    }
+
+    private fun setTransitionProgress(from: SceneKey, to: SceneKey, progress: Float) {
+        sceneInteractor.changeScene(from, "test")
+        val transitionState =
+            MutableStateFlow<ObservableTransitionState>(
+                ObservableTransitionState.Transition(
+                    fromScene = from,
+                    toScene = to,
+                    progress = MutableStateFlow(progress),
+                    isInitiatedByUserInput = false,
+                    isUserInputOngoing = flowOf(false),
+                )
+            )
+        sceneInteractor.setTransitionState(transitionState)
+        testScope.runCurrent()
+    }
+
+    override fun assertFlagValid() {
+        Assert.assertTrue(SceneContainerFlag.isEnabled)
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtilKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtilKosmos.kt
new file mode 100644
index 0000000..9eeb345
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtilKosmos.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.shade
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.shade.data.repository.fakeShadeRepository
+
+var Kosmos.shadeTestUtil: ShadeTestUtil by
+    Kosmos.Fixture {
+        ShadeTestUtil(
+            if (SceneContainerFlag.isEnabled) {
+                ShadeTestUtilSceneImpl(testScope, sceneInteractor)
+            } else {
+                ShadeTestUtilLegacyImpl(testScope, fakeShadeRepository)
+            }
+        )
+    }
diff --git a/services/autofill/java/com/android/server/autofill/RemoteFillService.java b/services/autofill/java/com/android/server/autofill/RemoteFillService.java
index c96688c..7ceb3bb 100644
--- a/services/autofill/java/com/android/server/autofill/RemoteFillService.java
+++ b/services/autofill/java/com/android/server/autofill/RemoteFillService.java
@@ -28,6 +28,7 @@
 import android.content.Intent;
 import android.content.IntentSender;
 import android.os.Handler;
+import android.os.IBinder;
 import android.os.ICancellationSignal;
 import android.os.RemoteException;
 import android.service.autofill.AutofillService;
@@ -42,7 +43,6 @@
 import android.service.autofill.SaveRequest;
 import android.text.format.DateUtils;
 import android.util.Slog;
-import android.view.autofill.IAutoFillManagerClient;
 
 import com.android.internal.infra.AbstractRemoteService;
 import com.android.internal.infra.ServiceConnector;
@@ -283,8 +283,7 @@
         return callback;
     }
 
-    public void onFillCredentialRequest(@NonNull FillRequest request,
-            IAutoFillManagerClient autofillCallback) {
+    public void onFillCredentialRequest(@NonNull FillRequest request, IBinder autofillCallback) {
         if (sVerbose) {
             Slog.v(TAG, "onFillRequest:" + request);
         }
diff --git a/services/autofill/java/com/android/server/autofill/SecondaryProviderHandler.java b/services/autofill/java/com/android/server/autofill/SecondaryProviderHandler.java
index ce9d180..044a064 100644
--- a/services/autofill/java/com/android/server/autofill/SecondaryProviderHandler.java
+++ b/services/autofill/java/com/android/server/autofill/SecondaryProviderHandler.java
@@ -21,11 +21,11 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.IntentSender;
+import android.os.IBinder;
 import android.service.autofill.ConvertCredentialResponse;
 import android.service.autofill.FillRequest;
 import android.service.autofill.FillResponse;
 import android.util.Slog;
-import android.view.autofill.IAutoFillManagerClient;
 
 /**
  * Requests autofill response from a Remote Autofill Service. This autofill service can be
@@ -105,8 +105,7 @@
     /**
      * Requests a new fill response.
      */
-    public void onFillRequest(FillRequest pendingFillRequest, int flag,
-            IAutoFillManagerClient client) {
+    public void onFillRequest(FillRequest pendingFillRequest, int flag, IBinder client) {
         Slog.v(TAG, "Requesting fill response to secondary provider.");
         mLastFlag = flag;
         if (mRemoteFillService != null && mRemoteFillService.isCredentialAutofillService()) {
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 3a38406..cd1ef88 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -757,13 +757,14 @@
                             mPendingInlineSuggestionsRequest, id);
                 }
                 mSecondaryProviderHandler.onFillRequest(mPendingFillRequest,
-                        mPendingFillRequest.getFlags(), mClient);
+                        mPendingFillRequest.getFlags(), mClient.asBinder());
             } else if (mRemoteFillService != null) {
                 if (mIsPrimaryCredential) {
                     mPendingFillRequest = addCredentialManagerDataToClientState(
                             mPendingFillRequest,
                             mPendingInlineSuggestionsRequest, id);
-                    mRemoteFillService.onFillCredentialRequest(mPendingFillRequest, mClient);
+                    mRemoteFillService.onFillCredentialRequest(mPendingFillRequest,
+                            mClient.asBinder());
                 } else {
                     mRemoteFillService.onFillRequest(mPendingFillRequest);
                 }
@@ -2897,7 +2898,7 @@
                     + ", clientState=" + newClientState + ", authenticationId=" + authenticationId);
         }
         if (Flags.autofillCredmanDevIntegration() && exception != null
-                && exception instanceof GetCredentialException) {
+                && !exception.getType().equals(GetCredentialException.TYPE_USER_CANCELED)) {
             if (dataset != null && dataset.getFieldIds().size() == 1) {
                 if (sDebug) {
                     Slog.d(TAG, "setAuthenticationResultLocked(): result returns with"
@@ -6494,21 +6495,15 @@
                     }
                 }
                 if (exception != null) {
-                    mClient.onGetCredentialException(id, viewId, exception.getType(),
-                            exception.getMessage());
+                    if (viewId.isVirtualInt()) {
+                        sendResponseToViewNode(viewId, /*response=*/ null, exception);
+                    } else {
+                        mClient.onGetCredentialException(id, viewId, exception.getType(),
+                                exception.getMessage());
+                    }
                 } else if (response != null) {
                     if (viewId.isVirtualInt()) {
-                        ViewNode viewNode = getViewNodeFromContextsLocked(viewId);
-                        if (viewNode != null && viewNode.getPendingCredentialCallback() != null) {
-                            Bundle resultData = new Bundle();
-                            resultData.putParcelable(
-                                    CredentialProviderService.EXTRA_GET_CREDENTIAL_RESPONSE,
-                                    response);
-                            viewNode.getPendingCredentialCallback().send(SUCCESS_CREDMAN_SELECTOR,
-                                        resultData);
-                        } else {
-                            Slog.w(TAG, "View node not found after GetCredentialResponse");
-                        }
+                        sendResponseToViewNode(viewId, response, /*exception=*/ null);
                     } else {
                         mClient.onGetCredentialResponse(id, viewId, response);
                     }
@@ -6522,6 +6517,30 @@
         }
     }
 
+    @GuardedBy("mLock")
+    private void sendResponseToViewNode(AutofillId viewId, GetCredentialResponse response,
+            GetCredentialException exception) {
+        ViewNode viewNode = getViewNodeFromContextsLocked(viewId);
+        if (viewNode != null && viewNode.getPendingCredentialCallback() != null) {
+            Bundle resultData = new Bundle();
+            if (response != null) {
+                resultData.putParcelable(
+                        CredentialProviderService.EXTRA_GET_CREDENTIAL_RESPONSE,
+                        response);
+                viewNode.getPendingCredentialCallback().send(SUCCESS_CREDMAN_SELECTOR,
+                        resultData);
+            } else if (exception != null) {
+                resultData.putStringArray(
+                        CredentialProviderService.EXTRA_GET_CREDENTIAL_EXCEPTION,
+                        new String[] {exception.getType(), exception.getMessage()});
+                viewNode.getPendingCredentialCallback().send(FAILURE_CREDMAN_SELECTOR,
+                        resultData);
+            }
+        } else {
+            Slog.w(TAG, "View node not found after GetCredentialResponse");
+        }
+    }
+
     void autoFillApp(Dataset dataset) {
         synchronized (mLock) {
             if (mDestroyed) {
diff --git a/services/backup/java/com/android/server/backup/BackupManagerService.java b/services/backup/java/com/android/server/backup/BackupManagerService.java
index 0353d5a..5f0071d 100644
--- a/services/backup/java/com/android/server/backup/BackupManagerService.java
+++ b/services/backup/java/com/android/server/backup/BackupManagerService.java
@@ -1516,7 +1516,7 @@
         String op = nextArg(args, argIndex);
         argIndex++;
 
-        if ("--help".equals(op)) {
+        if ("--help".equals(op) || "-h".equals(op)) {
             showDumpUsage(pw);
             return;
         }
@@ -1548,19 +1548,17 @@
             }
             return;
         }
-        if (op == null || "agents".startsWith(op) || "transportclients".equals(op)
-                || "transportstats".equals(op)) {
-            for (int i = 0; i < mUserServices.size(); i++) {
-                UserBackupManagerService userBackupManagerService =
-                        getServiceForUserIfCallerHasPermission(mUserServices.keyAt(i), "dump()");
-                if (userBackupManagerService != null) {
-                    userBackupManagerService.dump(fd, pw, args);
-                }
-            }
-            return;
-        }
 
-        showDumpUsage(pw);
+        // We get here if we have no parameters or parameters unkonwn to us.
+        // Print dumpsys info in either case: bug report creation passes some parameter to
+        // dumpsys that we don't need to check.
+        for (int i = 0; i < mUserServices.size(); i++) {
+            UserBackupManagerService userBackupManagerService =
+                    getServiceForUserIfCallerHasPermission(mUserServices.keyAt(i), "dump()");
+            if (userBackupManagerService != null) {
+                userBackupManagerService.dump(fd, pw, args);
+            }
+        }
     }
 
     private String nextArg(String[] args, int argIndex) {
diff --git a/services/companion/java/com/android/server/companion/virtual/InputController.java b/services/companion/java/com/android/server/companion/virtual/InputController.java
index 3c25835..9b72288 100644
--- a/services/companion/java/com/android/server/companion/virtual/InputController.java
+++ b/services/companion/java/com/android/server/companion/virtual/InputController.java
@@ -226,7 +226,7 @@
         token.unlinkToDeath(inputDeviceDescriptor.getDeathRecipient(), /* flags= */ 0);
         mNativeWrapper.closeUinput(inputDeviceDescriptor.getNativePointer());
         String phys = inputDeviceDescriptor.getPhys();
-        InputManagerGlobal.getInstance().removeUniqueIdAssociationByDescriptor(phys);
+        InputManagerGlobal.getInstance().removeUniqueIdAssociation(phys);
         // Type associations are added in the case of navigation touchpads. Those should be removed
         // once the input device gets closed.
         if (inputDeviceDescriptor.getType() == InputDeviceDescriptor.TYPE_NAVIGATION_TOUCHPAD) {
@@ -319,9 +319,9 @@
         return formatSimple("virtual%s:%d", type, sNextPhysId.getAndIncrement());
     }
 
-    private void setUniqueIdAssociationByPort(int displayId, String phys) {
+    private void setUniqueIdAssociation(int displayId, String phys) {
         final String displayUniqueId = mDisplayManagerInternal.getDisplayInfo(displayId).uniqueId;
-        InputManagerGlobal.getInstance().addUniqueIdAssociationByPort(phys, displayUniqueId);
+        InputManagerGlobal.getInstance().addUniqueIdAssociation(phys, displayUniqueId);
     }
 
     boolean sendDpadKeyEvent(@NonNull IBinder token, @NonNull VirtualKeyEvent event) {
@@ -809,7 +809,7 @@
 
         final int inputDeviceId;
 
-        setUniqueIdAssociationByPort(displayId, phys);
+        setUniqueIdAssociation(displayId, phys);
         try (WaitForDevice waiter = new WaitForDevice(deviceName, vendorId, productId, displayId)) {
             ptr = deviceOpener.get();
             // See INVALID_PTR in libs/input/VirtualInputDevice.cpp.
@@ -835,7 +835,7 @@
                 throw e;
             }
         } catch (DeviceCreationException e) {
-            InputManagerGlobal.getInstance().removeUniqueIdAssociationByPort(phys);
+            InputManagerGlobal.getInstance().removeUniqueIdAssociation(phys);
             throw e;
         }
 
diff --git a/services/core/Android.bp b/services/core/Android.bp
index e095fa3..300b147 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -234,7 +234,6 @@
         "android.hidl.manager-V1.2-java",
         "cbor-java",
         "com.android.media.audio-aconfig-java",
-        "dropbox_flags_lib",
         "icu4j_calendar_astronomer",
         "android.security.aaid_aidl-java",
         "netd-client",
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 26e9bf5..2fa0be2 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -3867,8 +3867,10 @@
                 Slog.w(TAG_SERVICE, "Exception from scheduleTimeoutServiceForType: " + e);
             }
 
-            // ANR the service after giving the service some time to clean up.
-            mFGSAnrTimer.start(sr, mAm.mConstants.mFgsAnrExtraWaitDuration);
+            if (android.app.Flags.introduceNewServiceOntimeoutCallback()) {
+                // ANR the service after giving the service some time to clean up.
+                mFGSAnrTimer.start(sr, mAm.mConstants.mFgsAnrExtraWaitDuration);
+            }
         }
     }
 
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 1f89ca7..8b64538 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -163,6 +163,7 @@
         "media_reliability",
         "media_solutions",
         "media_tv",
+        "nearby",
         "nfc",
         "pdf_viewer",
         "perfetto",
diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java
index 3737d6f..0e81eb9 100644
--- a/services/core/java/com/android/server/biometrics/BiometricService.java
+++ b/services/core/java/com/android/server/biometrics/BiometricService.java
@@ -850,8 +850,10 @@
 
             Slog.d(TAG, "resetLockout(userId=" + userId
                     + ", hat=" + (hardwareAuthToken == null ? "null " : "present") + ")");
-            mBiometricContext.getAuthSessionCoordinator()
+            mHandler.post(() -> {
+                mBiometricContext.getAuthSessionCoordinator()
                     .resetLockoutFor(userId, Authenticators.BIOMETRIC_STRONG, -1);
+            });
         }
 
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthSessionCoordinator.java b/services/core/java/com/android/server/biometrics/sensors/AuthSessionCoordinator.java
index d9947dd..dc2eff4 100644
--- a/services/core/java/com/android/server/biometrics/sensors/AuthSessionCoordinator.java
+++ b/services/core/java/com/android/server/biometrics/sensors/AuthSessionCoordinator.java
@@ -235,7 +235,7 @@
             mApiCallNumber = 0;
         }
 
-        void addApiCall(String str) {
+        synchronized void addApiCall(String str) {
             mApiCalls[mCurr] = str;
             mCurr++;
             mCurr %= mSize;
@@ -243,7 +243,7 @@
         }
 
         @Override
-        public String toString() {
+        public synchronized String toString() {
             String buffer = "";
             int apiCall = mApiCallNumber > mSize ? mApiCallNumber - mSize : 0;
             for (int i = 0; i < mSize; i++) {
diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig
index c68ef9b..a7dd243 100644
--- a/services/core/java/com/android/server/display/feature/display_flags.aconfig
+++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig
@@ -126,7 +126,7 @@
     name: "even_dimmer"
     namespace: "display_manager"
     description: "Feature flag for extending the brightness below traditional range"
-    bug: "179428400"
+    bug: "294760970"
     is_fixed_read_only: true
 }
 
diff --git a/services/core/java/com/android/server/feature/Android.bp b/services/core/java/com/android/server/feature/Android.bp
deleted file mode 100644
index b0fbab6..0000000
--- a/services/core/java/com/android/server/feature/Android.bp
+++ /dev/null
@@ -1,13 +0,0 @@
-aconfig_declarations {
-    name: "dropbox_flags",
-    package: "com.android.server.feature.flags",
-    container: "system",
-    srcs: [
-        "dropbox_flags.aconfig",
-    ],
-}
-
-java_aconfig_library {
-    name: "dropbox_flags_lib",
-    aconfig_declarations: "dropbox_flags",
-}
diff --git a/services/core/java/com/android/server/flags/services.aconfig b/services/core/java/com/android/server/flags/services.aconfig
index 8e0eb05..e43d086 100644
--- a/services/core/java/com/android/server/flags/services.aconfig
+++ b/services/core/java/com/android/server/flags/services.aconfig
@@ -6,4 +6,14 @@
      name: "enable_odp_feature_guard"
      description: "Enable guard based on system feature to prevent OnDevicePersonalization service from starting on form factors."
      bug: "322249125"
-}
\ No newline at end of file
+}
+
+flag {
+    namespace: "input"
+    name: "new_bugreport_keyboard_shortcut"
+    description: "Enable META+CTRL+BACKSPACE keyboard shortcut for taking a bug report"
+    bug: "335607520"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 73df594..f32c11d 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -250,19 +250,8 @@
     private final Object mAssociationsLock = new Object();
     @GuardedBy("mAssociationsLock")
     private final Map<String, Integer> mRuntimeAssociations = new ArrayMap<>();
-
-    // The associations of input devices to displays by port. Maps from {InputDevice#mName} (String)
-    // to {DisplayInfo#uniqueId} (String) so that events from the Input Device go to a
-    // specific display.
     @GuardedBy("mAssociationsLock")
-    private final Map<String, String> mUniqueIdAssociationsByPort = new ArrayMap<>();
-
-    // The associations of input devices to displays by descriptor. Maps from
-    // {InputDevice#mDescriptor} to {DisplayInfo#uniqueId} (String) so that events from the
-    // input device go to a specific display.
-    @GuardedBy("mAssociationsLock")
-    private final Map<String, String> mUniqueIdAssociationsByDescriptor = new ArrayMap<>();
-
+    private final Map<String, String> mUniqueIdAssociations = new ArrayMap<>();
     // The map from input port (String) to the keyboard layout identifiers (comma separated string
     // containing language tag and layout type) associated with the corresponding keyboard device.
     // Currently only accessed by InputReader.
@@ -1752,8 +1741,8 @@
     /**
      * Add a runtime association between the input port and the display port. This overrides any
      * static associations.
-     * @param inputPort the port of the input device
-     * @param displayPort the physical port of the associated display
+     * @param inputPort The port of the input device.
+     * @param displayPort The physical port of the associated display.
      */
     @Override // Binder call
     public void addPortAssociation(@NonNull String inputPort, int displayPort) {
@@ -1774,7 +1763,7 @@
     /**
      * Remove the runtime association between the input port and the display port. Any existing
      * static association for the cleared input port will be restored.
-     * @param inputPort the port of the input device to be cleared
+     * @param inputPort The port of the input device to be cleared.
      */
     @Override // Binder call
     public void removePortAssociation(@NonNull String inputPort) {
@@ -1793,11 +1782,10 @@
     }
 
     @Override // Binder call
-    public void addUniqueIdAssociationByPort(@NonNull String inputPort,
-                                             @NonNull String displayUniqueId) {
+    public void addUniqueIdAssociation(@NonNull String inputPort, @NonNull String displayUniqueId) {
         if (!checkCallingPermission(
                 android.Manifest.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY,
-                "addUniqueIdAssociationByPort()")) {
+                "addUniqueIdAssociation()")) {
             throw new SecurityException(
                     "Requires ASSOCIATE_INPUT_DEVICE_TO_DISPLAY permission");
         }
@@ -1805,65 +1793,22 @@
         Objects.requireNonNull(inputPort);
         Objects.requireNonNull(displayUniqueId);
         synchronized (mAssociationsLock) {
-            mUniqueIdAssociationsByPort.put(inputPort, displayUniqueId);
+            mUniqueIdAssociations.put(inputPort, displayUniqueId);
         }
         mNative.changeUniqueIdAssociation();
     }
 
     @Override // Binder call
-    public void removeUniqueIdAssociationByPort(@NonNull String inputPort) {
+    public void removeUniqueIdAssociation(@NonNull String inputPort) {
         if (!checkCallingPermission(
                 android.Manifest.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY,
-                "removeUniqueIdAssociationByPort()")) {
+                "removeUniqueIdAssociation()")) {
             throw new SecurityException("Requires ASSOCIATE_INPUT_DEVICE_TO_DISPLAY permission");
         }
 
         Objects.requireNonNull(inputPort);
         synchronized (mAssociationsLock) {
-            mUniqueIdAssociationsByPort.remove(inputPort);
-        }
-        mNative.changeUniqueIdAssociation();
-    }
-
-    /**
-     * Adds a runtime association between the input device descriptor and the display unique id.
-     * @param inputDeviceDescriptor the descriptor of the input device
-     * @param displayUniqueId the unique ID of the display
-     */
-    @Override // Binder call
-    public void addUniqueIdAssociationByDescriptor(@NonNull String inputDeviceDescriptor,
-                                                   @NonNull String displayUniqueId) {
-        if (!checkCallingPermission(
-                android.Manifest.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY,
-                "addUniqueIdAssociationByDescriptor()")) {
-            throw new SecurityException(
-                    "Requires ASSOCIATE_INPUT_DEVICE_TO_DISPLAY permission");
-        }
-
-        Objects.requireNonNull(inputDeviceDescriptor);
-        Objects.requireNonNull(displayUniqueId);
-        synchronized (mAssociationsLock) {
-            mUniqueIdAssociationsByDescriptor.put(inputDeviceDescriptor, displayUniqueId);
-        }
-        mNative.changeUniqueIdAssociation();
-    }
-
-    /**
-     * Removes the runtime association between the input device and the display.
-     * @param inputDeviceDescriptor the descriptor of the input device
-     */
-    @Override // Binder call
-    public void removeUniqueIdAssociationByDescriptor(@NonNull String inputDeviceDescriptor) {
-        if (!checkCallingPermission(
-                android.Manifest.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY,
-                "removeUniqueIdAssociationByDescriptor()")) {
-            throw new SecurityException(
-                    "Requires ASSOCIATE_INPUT_DEVICE_TO_DISPLAY permission");
-        }
-
-        Objects.requireNonNull(inputDeviceDescriptor);
-        synchronized (mAssociationsLock) {
-            mUniqueIdAssociationsByDescriptor.remove(inputDeviceDescriptor);
+            mUniqueIdAssociations.remove(inputPort);
         }
         mNative.changeUniqueIdAssociation();
     }
@@ -2238,20 +2183,13 @@
                     pw.println("  display: " + v);
                 });
             }
-            if (!mUniqueIdAssociationsByPort.isEmpty()) {
+            if (!mUniqueIdAssociations.isEmpty()) {
                 pw.println("Unique Id Associations:");
-                mUniqueIdAssociationsByPort.forEach((k, v) -> {
+                mUniqueIdAssociations.forEach((k, v) -> {
                     pw.print("  port: " + k);
                     pw.println("  uniqueId: " + v);
                 });
             }
-            if (!mUniqueIdAssociationsByDescriptor.isEmpty()) {
-                pw.println("Unique Id Associations:");
-                mUniqueIdAssociationsByDescriptor.forEach((k, v) -> {
-                    pw.print("  descriptor: " + k);
-                    pw.println("  uniqueId: " + v);
-                });
-            }
             if (!mDeviceTypeAssociations.isEmpty()) {
                 pw.println("Type Associations:");
                 mDeviceTypeAssociations.forEach((k, v) -> {
@@ -2684,21 +2622,10 @@
 
     // Native callback
     @SuppressWarnings("unused")
-    private String[] getInputUniqueIdAssociationsByPort() {
+    private String[] getInputUniqueIdAssociations() {
         final Map<String, String> associations;
         synchronized (mAssociationsLock) {
-            associations = new HashMap<>(mUniqueIdAssociationsByPort);
-        }
-
-        return flatten(associations);
-    }
-
-    // Native callback
-    @SuppressWarnings("unused")
-    private String[] getInputUniqueIdAssociationsByDescriptor() {
-        final Map<String, String> associations;
-        synchronized (mAssociationsLock) {
-            associations = new HashMap<>(mUniqueIdAssociationsByDescriptor);
+            associations = new HashMap<>(mUniqueIdAssociations);
         }
 
         return flatten(associations);
diff --git a/services/core/java/com/android/server/inputmethod/HandwritingEventReceiverSurface.java b/services/core/java/com/android/server/inputmethod/HandwritingEventReceiverSurface.java
index dbbbed3..d132449 100644
--- a/services/core/java/com/android/server/inputmethod/HandwritingEventReceiverSurface.java
+++ b/services/core/java/com/android/server/inputmethod/HandwritingEventReceiverSurface.java
@@ -85,6 +85,17 @@
         mIsIntercepting = true;
     }
 
+    void setNotTouchable(boolean notTouchable) {
+        if (notTouchable) {
+            mWindowHandle.inputConfig |= InputConfig.NOT_TOUCHABLE;
+        } else {
+            mWindowHandle.inputConfig &=  ~InputConfig.NOT_TOUCHABLE;
+        }
+        new SurfaceControl.Transaction()
+                .setInputWindowInfo(mInputSurface, mWindowHandle)
+                .apply();
+    }
+
     boolean isIntercepting() {
         return mIsIntercepting;
     }
diff --git a/services/core/java/com/android/server/inputmethod/HandwritingModeController.java b/services/core/java/com/android/server/inputmethod/HandwritingModeController.java
index dbdac41..7956e03 100644
--- a/services/core/java/com/android/server/inputmethod/HandwritingModeController.java
+++ b/services/core/java/com/android/server/inputmethod/HandwritingModeController.java
@@ -159,6 +159,13 @@
         return OptionalInt.of(mCurrentRequestId);
     }
 
+    void setNotTouchable(boolean notTouchable) {
+        if (!getCurrentRequestId().isPresent()) {
+            return;
+        }
+        mHandwritingSurface.setNotTouchable(notTouchable);
+    }
+
     boolean isStylusGestureOngoing() {
         if (mRecordingGestureAfterStylusUp && !mHandwritingBuffer.isEmpty()) {
             // If it is less than AFTER_STYLUS_UP_ALLOW_PERIOD_MS after the stylus up event, return
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 808e296..7bd47f5 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -6548,6 +6548,12 @@
 
         @BinderThread
         @Override
+        public void setHandwritingSurfaceNotTouchable(boolean notTouchable) {
+            mImms.mHwController.setNotTouchable(notTouchable);
+        }
+
+        @BinderThread
+        @Override
         public void createInputContentUriToken(Uri contentUri, String packageName,
                 AndroidFuture future /* T=IBinder */) {
             @SuppressWarnings("unchecked")
diff --git a/services/core/java/com/android/server/media/MediaServerUtils.java b/services/core/java/com/android/server/media/MediaServerUtils.java
index 6a954d6..f16d426 100644
--- a/services/core/java/com/android/server/media/MediaServerUtils.java
+++ b/services/core/java/com/android/server/media/MediaServerUtils.java
@@ -74,10 +74,7 @@
         }
         final PackageManagerInternal packageManagerInternal =
                 LocalServices.getService(PackageManagerInternal.class);
-        final int actualUid =
-                packageManagerInternal.getPackageUid(
-                        packageName, 0 /* flags */, UserHandle.getUserId(uid));
-        if (!UserHandle.isSameApp(uid, actualUid)) {
+        if (!packageManagerInternal.isSameApp(packageName, uid, UserHandle.getUserId(uid))) {
             String[] uidPackages = context.getPackageManager().getPackagesForUid(uid);
             throw new IllegalArgumentException(
                     "packageName does not belong to the calling uid; "
diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java
index a3c5d2d..3d68555 100644
--- a/services/core/java/com/android/server/media/MediaSessionRecord.java
+++ b/services/core/java/com/android/server/media/MediaSessionRecord.java
@@ -66,6 +66,7 @@
 import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
+import android.os.DeadObjectException;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
@@ -89,6 +90,7 @@
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.List;
 import java.util.NoSuchElementException;
 import java.util.Objects;
@@ -189,9 +191,6 @@
     private final ForegroundServiceDelegationOptions mForegroundServiceDelegationOptions;
 
     private final Object mLock = new Object();
-    // This field is partially guarded by mLock. Writes and non-atomic iterations (for example:
-    // index-based-iterations) must be guarded by mLock. But it is safe to acquire an iterator
-    // without acquiring mLock.
     private final CopyOnWriteArrayList<ISessionControllerCallbackHolder>
             mControllerCallbackHolders = new CopyOnWriteArrayList<>();
 
@@ -887,9 +886,24 @@
             }
             playbackState = mPlaybackState;
         }
-        performOnCallbackHolders(
-                "pushPlaybackStateUpdate",
-                holder -> holder.mCallback.onPlaybackStateChanged(playbackState));
+        Collection<ISessionControllerCallbackHolder> deadCallbackHolders = null;
+        for (ISessionControllerCallbackHolder holder : mControllerCallbackHolders) {
+            try {
+                holder.mCallback.onPlaybackStateChanged(playbackState);
+            } catch (DeadObjectException e) {
+                if (deadCallbackHolders == null) {
+                    deadCallbackHolders = new ArrayList<>();
+                }
+                deadCallbackHolders.add(holder);
+                logCallbackException("Removing dead callback in pushPlaybackStateUpdate", holder,
+                        e);
+            } catch (RemoteException e) {
+                logCallbackException("unexpected exception in pushPlaybackStateUpdate", holder, e);
+            }
+        }
+        if (deadCallbackHolders != null) {
+            removeControllerHoldersSafely(deadCallbackHolders);
+        }
     }
 
     private void pushMetadataUpdate() {
@@ -900,8 +914,23 @@
             }
             metadata = mMetadata;
         }
-        performOnCallbackHolders(
-                "pushMetadataUpdate", holder -> holder.mCallback.onMetadataChanged(metadata));
+        Collection<ISessionControllerCallbackHolder> deadCallbackHolders = null;
+        for (ISessionControllerCallbackHolder holder : mControllerCallbackHolders) {
+            try {
+                holder.mCallback.onMetadataChanged(metadata);
+            } catch (DeadObjectException e) {
+                if (deadCallbackHolders == null) {
+                    deadCallbackHolders = new ArrayList<>();
+                }
+                deadCallbackHolders.add(holder);
+                logCallbackException("Removing dead callback in pushMetadataUpdate", holder, e);
+            } catch (RemoteException e) {
+                logCallbackException("unexpected exception in pushMetadataUpdate", holder, e);
+            }
+        }
+        if (deadCallbackHolders != null) {
+            removeControllerHoldersSafely(deadCallbackHolders);
+        }
     }
 
     private void pushQueueUpdate() {
@@ -912,18 +941,31 @@
             }
             toSend = mQueue == null ? null : new ArrayList<>(mQueue);
         }
-        performOnCallbackHolders(
-                "pushQueueUpdate",
-                holder -> {
-                    ParceledListSlice<QueueItem> parcelableQueue = null;
-                    if (toSend != null) {
-                        parcelableQueue = new ParceledListSlice<>(toSend);
-                        // Limit the size of initial Parcel to prevent binder buffer overflow
-                        // as onQueueChanged is an async binder call.
-                        parcelableQueue.setInlineCountLimit(1);
-                    }
-                    holder.mCallback.onQueueChanged(parcelableQueue);
-                });
+        Collection<ISessionControllerCallbackHolder> deadCallbackHolders = null;
+        for (ISessionControllerCallbackHolder holder : mControllerCallbackHolders) {
+            ParceledListSlice<QueueItem> parcelableQueue = null;
+            if (toSend != null) {
+                parcelableQueue = new ParceledListSlice<>(toSend);
+                // Limit the size of initial Parcel to prevent binder buffer overflow
+                // as onQueueChanged is an async binder call.
+                parcelableQueue.setInlineCountLimit(1);
+            }
+
+            try {
+                holder.mCallback.onQueueChanged(parcelableQueue);
+            } catch (DeadObjectException e) {
+                if (deadCallbackHolders == null) {
+                    deadCallbackHolders = new ArrayList<>();
+                }
+                deadCallbackHolders.add(holder);
+                logCallbackException("Removing dead callback in pushQueueUpdate", holder, e);
+            } catch (RemoteException e) {
+                logCallbackException("unexpected exception in pushQueueUpdate", holder, e);
+            }
+        }
+        if (deadCallbackHolders != null) {
+            removeControllerHoldersSafely(deadCallbackHolders);
+        }
     }
 
     private void pushQueueTitleUpdate() {
@@ -934,8 +976,23 @@
             }
             queueTitle = mQueueTitle;
         }
-        performOnCallbackHolders(
-                "pushQueueTitleUpdate", holder -> holder.mCallback.onQueueTitleChanged(queueTitle));
+        Collection<ISessionControllerCallbackHolder> deadCallbackHolders = null;
+        for (ISessionControllerCallbackHolder holder : mControllerCallbackHolders) {
+            try {
+                holder.mCallback.onQueueTitleChanged(queueTitle);
+            } catch (DeadObjectException e) {
+                if (deadCallbackHolders == null) {
+                    deadCallbackHolders = new ArrayList<>();
+                }
+                deadCallbackHolders.add(holder);
+                logCallbackException("Removing dead callback in pushQueueTitleUpdate", holder, e);
+            } catch (RemoteException e) {
+                logCallbackException("unexpected exception in pushQueueTitleUpdate", holder, e);
+            }
+        }
+        if (deadCallbackHolders != null) {
+            removeControllerHoldersSafely(deadCallbackHolders);
+        }
     }
 
     private void pushExtrasUpdate() {
@@ -946,8 +1003,23 @@
             }
             extras = mExtras;
         }
-        performOnCallbackHolders(
-                "pushExtrasUpdate", holder -> holder.mCallback.onExtrasChanged(extras));
+        Collection<ISessionControllerCallbackHolder> deadCallbackHolders = null;
+        for (ISessionControllerCallbackHolder holder : mControllerCallbackHolders) {
+            try {
+                holder.mCallback.onExtrasChanged(extras);
+            } catch (DeadObjectException e) {
+                if (deadCallbackHolders == null) {
+                    deadCallbackHolders = new ArrayList<>();
+                }
+                deadCallbackHolders.add(holder);
+                logCallbackException("Removing dead callback in pushExtrasUpdate", holder, e);
+            } catch (RemoteException e) {
+                logCallbackException("unexpected exception in pushExtrasUpdate", holder, e);
+            }
+        }
+        if (deadCallbackHolders != null) {
+            removeControllerHoldersSafely(deadCallbackHolders);
+        }
     }
 
     private void pushVolumeUpdate() {
@@ -958,8 +1030,23 @@
             }
             info = getVolumeAttributes();
         }
-        performOnCallbackHolders(
-                "pushVolumeUpdate", holder -> holder.mCallback.onVolumeInfoChanged(info));
+        Collection<ISessionControllerCallbackHolder> deadCallbackHolders = null;
+        for (ISessionControllerCallbackHolder holder : mControllerCallbackHolders) {
+            try {
+                holder.mCallback.onVolumeInfoChanged(info);
+            } catch (DeadObjectException e) {
+                if (deadCallbackHolders == null) {
+                    deadCallbackHolders = new ArrayList<>();
+                }
+                deadCallbackHolders.add(holder);
+                logCallbackException("Removing dead callback in pushVolumeUpdate", holder, e);
+            } catch (RemoteException e) {
+                logCallbackException("unexpected exception in pushVolumeUpdate", holder, e);
+            }
+        }
+        if (deadCallbackHolders != null) {
+            removeControllerHoldersSafely(deadCallbackHolders);
+        }
     }
 
     private void pushEvent(String event, Bundle data) {
@@ -968,7 +1055,23 @@
                 return;
             }
         }
-        performOnCallbackHolders("pushEvent", holder -> holder.mCallback.onEvent(event, data));
+        Collection<ISessionControllerCallbackHolder> deadCallbackHolders = null;
+        for (ISessionControllerCallbackHolder holder : mControllerCallbackHolders) {
+            try {
+                holder.mCallback.onEvent(event, data);
+            } catch (DeadObjectException e) {
+                if (deadCallbackHolders == null) {
+                    deadCallbackHolders = new ArrayList<>();
+                }
+                deadCallbackHolders.add(holder);
+                logCallbackException("Removing dead callback in pushEvent", holder, e);
+            } catch (RemoteException e) {
+                logCallbackException("unexpected exception in pushEvent", holder, e);
+            }
+        }
+        if (deadCallbackHolders != null) {
+            removeControllerHoldersSafely(deadCallbackHolders);
+        }
     }
 
     private void pushSessionDestroyed() {
@@ -979,37 +1082,20 @@
                 return;
             }
         }
-        performOnCallbackHolders(
-                "pushSessionDestroyed",
-                holder -> {
-                    holder.mCallback.asBinder().unlinkToDeath(holder.mDeathMonitor, 0);
-                    holder.mCallback.onSessionDestroyed();
-                });
-        // After notifying clear all listeners
-        synchronized (mLock) {
-            mControllerCallbackHolders.clear();
-        }
-    }
-
-    private interface ControllerCallbackCall {
-
-        void performOn(ISessionControllerCallbackHolder holder) throws RemoteException;
-    }
-
-    private void performOnCallbackHolders(String operationName, ControllerCallbackCall call) {
-        ArrayList<ISessionControllerCallbackHolder> deadCallbackHolders = new ArrayList<>();
         for (ISessionControllerCallbackHolder holder : mControllerCallbackHolders) {
             try {
-                call.performOn(holder);
-            } catch (RemoteException | NoSuchElementException exception) {
-                deadCallbackHolders.add(holder);
-                logCallbackException(
-                        "Exception while executing: " + operationName, holder, exception);
+                holder.mCallback.asBinder().unlinkToDeath(holder.mDeathMonitor, 0);
+                holder.mCallback.onSessionDestroyed();
+            } catch (NoSuchElementException e) {
+                logCallbackException("error unlinking to binder death", holder, e);
+            } catch (DeadObjectException e) {
+                logCallbackException("Removing dead callback in pushSessionDestroyed", holder, e);
+            } catch (RemoteException e) {
+                logCallbackException("unexpected exception in pushSessionDestroyed", holder, e);
             }
         }
-        synchronized (mLock) {
-            mControllerCallbackHolders.removeAll(deadCallbackHolders);
-        }
+        // After notifying clear all listeners
+        removeControllerHoldersSafely(null);
     }
 
     private PlaybackState getStateWithUpdatedPosition() {
@@ -1057,6 +1143,17 @@
         return -1;
     }
 
+    private void removeControllerHoldersSafely(
+            Collection<ISessionControllerCallbackHolder> holders) {
+        synchronized (mLock) {
+            if (holders == null) {
+                mControllerCallbackHolders.clear();
+            } else {
+                mControllerCallbackHolders.removeAll(holders);
+            }
+        }
+    }
+
     private PlaybackInfo getVolumeAttributes() {
         int volumeType;
         AudioAttributes attributes;
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index ebea05d..d923b37 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -98,6 +98,7 @@
 import static android.os.UserHandle.USER_SYSTEM;
 import static android.service.notification.Flags.callstyleCallbackApi;
 import static android.service.notification.Flags.redactSensitiveNotificationsFromUntrustedListeners;
+import static android.service.notification.Flags.redactSensitiveNotificationsBigTextStyle;
 import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING;
 import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_CONVERSATIONS;
 import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ONGOING;
@@ -139,6 +140,7 @@
 import static android.service.notification.NotificationListenerService.TRIM_LIGHT;
 import static android.view.WindowManager.LayoutParams.TYPE_TOAST;
 import static android.view.contentprotection.flags.Flags.rapidClearNotificationsByListenerAppOpEnabled;
+
 import static com.android.internal.util.FrameworkStatsLog.DND_MODE_RULE;
 import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_CHANNEL_GROUP_PREFERENCES;
 import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_CHANNEL_PREFERENCES;
@@ -306,6 +308,7 @@
 import android.view.accessibility.AccessibilityManager;
 import android.widget.RemoteViews;
 import android.widget.Toast;
+
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
@@ -357,7 +360,9 @@
 import com.android.server.wm.ActivityTaskManagerInternal;
 import com.android.server.wm.BackgroundActivityStartCallback;
 import com.android.server.wm.WindowManagerInternal;
+
 import libcore.io.IoUtils;
+
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.xmlpull.v1.XmlPullParserException;
@@ -7235,6 +7240,10 @@
                 }
             }
         }
+        if (Flags.traceCancelEvents()) {
+            Trace.instant(Trace.TRACE_TAG_SYSTEM_SERVER, "cancelNotificationInternal: " +
+                    SmallHash.hash(Objects.hashCode(tag) ^ id));
+        }
 
         cancelNotification(uid, callingPid, pkg, tag, id, 0,
                 mustNotHaveFlags, false, userId, REASON_APP_CANCEL, null);
@@ -11775,10 +11784,18 @@
                     }
 
                     if (lifetimeExtensionRefactor()) {
+                        if (sendRedacted && redactedSbn == null) {
+                            redactedSbn = redactStatusBarNotification(sbn);
+                            redactedCache = new TrimCache(redactedSbn);
+                        }
+                        final StatusBarNotification sbnToPost = sendRedacted
+                                ? redactedCache.ForListener(info) : trimCache.ForListener(info);
+
                         // Checks if this is a request to notify system UI about a notification that
                         // has been lifetime extended.
                         // (We only need to check old for the flag, because in both cancellation and
-                        // update cases, old should have the flag.)
+                        // update cases, old should have the flag, whereas in update cases the
+                        // new will NOT have the flag.)
                         // If it is such a request, and this is system UI, we send the post request
                         // only to System UI, and break as we don't need to continue checking other
                         // Managed Services.
@@ -11786,7 +11803,7 @@
                                 && (old.getNotification().flags
                                 & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) > 0) {
                             final NotificationRankingUpdate update = makeRankingUpdateLocked(info);
-                            listenerCalls.add(() -> notifyPosted(info, oldSbn, update));
+                            listenerCalls.add(() -> notifyPosted(info, sbnToPost, update));
                             break;
                         }
                     }
@@ -11917,6 +11934,14 @@
                         redactedText, System.currentTimeMillis(), empty));
                 redactedNotifBuilder.setStyle(messageStyle);
             }
+            if (redactSensitiveNotificationsBigTextStyle()
+                    && oldNotif.isStyle(Notification.BigTextStyle.class)) {
+                Notification.BigTextStyle bigTextStyle = new Notification.BigTextStyle();
+                bigTextStyle.bigText(mContext.getString(R.string.redacted_notification_message));
+                bigTextStyle.setBigContentTitle("");
+                bigTextStyle.setSummaryText("");
+                redactedNotifBuilder.setStyle(bigTextStyle);
+            }
 
             Notification redacted = redactedNotifBuilder.build();
             // Notification extras can't always be overridden by a builder (configured by a system
diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java
index c69bead..8dd3e52 100644
--- a/services/core/java/com/android/server/notification/NotificationRecord.java
+++ b/services/core/java/com/android/server/notification/NotificationRecord.java
@@ -66,15 +66,16 @@
 import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.Log;
-import android.util.TimeUtils;
 import android.util.proto.ProtoOutputStream;
 import android.widget.RemoteViews;
+
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.server.EventLogTags;
 import com.android.server.LocalServices;
 import com.android.server.uri.UriGrantsManagerInternal;
+
 import dalvik.annotation.optimization.NeverCompile;
 
 import java.io.PrintWriter;
diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig
index 28598711..af3db6c 100644
--- a/services/core/java/com/android/server/notification/flags.aconfig
+++ b/services/core/java/com/android/server/notification/flags.aconfig
@@ -101,3 +101,30 @@
   description: "Stores restore data for not-yet-installed pkgs for 48 hours"
   bug: "334999659"
 }
+
+flag {
+  name: "trace_cancel_events"
+  namespace: "systemui"
+  description: "Adds performance tracing for binder cancel calls"
+  bug: "331677193"
+  metadata {
+      purpose: PURPOSE_BUGFIX
+  }
+}
+
+flag {
+  name: "exit_invalid_cancel_early"
+  namespace: "systemui"
+  description: "aborts cancel binder events early if notif doesn't exist"
+  bug: "331677193"
+  metadata {
+      purpose: PURPOSE_BUGFIX
+  }
+}
+
+flag {
+  name: "use_ipcdatacache_channels"
+  namespace: "systemui"
+  description: "Adds an IPCDataCache for notification channel/group lookups"
+  bug: "331677193"
+}
diff --git a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
index af339df..8f6aa95 100644
--- a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
+++ b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
@@ -662,7 +662,11 @@
         }
     }
 
+    @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)
     public void resetTemporaryServices() {
+        enforceShellOnly(Binder.getCallingUid(), "resetTemporaryServices");
+        mContext.enforceCallingPermission(
+                Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG);
         synchronized (mLock) {
             if (mTemporaryHandler != null) {
                 mTemporaryHandler.removeMessages(MSG_RESET_TEMPORARY_SERVICE);
diff --git a/services/core/java/com/android/server/pm/PackageManagerNative.java b/services/core/java/com/android/server/pm/PackageManagerNative.java
index d035084..66ecd6e 100644
--- a/services/core/java/com/android/server/pm/PackageManagerNative.java
+++ b/services/core/java/com/android/server/pm/PackageManagerNative.java
@@ -68,6 +68,11 @@
         }
     }
 
+    @Override
+    public int getPackageUid(String packageName, long flags, int userId) throws RemoteException {
+        return mPm.snapshotComputer().getPackageUid(packageName, flags, userId);
+    }
+
     // NB: this differentiates between preloads and sideloads
     @Override
     public String getInstallerForPackage(String packageName) throws RemoteException {
diff --git a/services/core/java/com/android/server/power/batterysaver/TEST_MAPPING b/services/core/java/com/android/server/power/batterysaver/TEST_MAPPING
index c091b8e..eb91a72 100644
--- a/services/core/java/com/android/server/power/batterysaver/TEST_MAPPING
+++ b/services/core/java/com/android/server/power/batterysaver/TEST_MAPPING
@@ -5,12 +5,7 @@
     },
     {
       "name": "CtsLocationFineTestCases",
-      "options": [
-          {
-             // TODO: Wait for test to deflake - b/293934372
-             "exclude-filter":"android.location.cts.fine.ScanningSettingsTest"
-          }
-      ]
+      "options": []
     },
     {
       "name": "CtsLocationNoneTestCases"
diff --git a/services/core/java/com/android/server/power/hint/HintManagerService.java b/services/core/java/com/android/server/power/hint/HintManagerService.java
index 101983e..6e17fd3 100644
--- a/services/core/java/com/android/server/power/hint/HintManagerService.java
+++ b/services/core/java/com/android/server/power/hint/HintManagerService.java
@@ -20,11 +20,14 @@
 import static com.android.server.power.hint.Flags.powerhintThreadCleanup;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
 import android.app.StatsManager;
 import android.app.UidObserver;
 import android.content.Context;
+import android.hardware.power.SessionConfig;
+import android.hardware.power.SessionTag;
 import android.hardware.power.WorkDuration;
 import android.os.Binder;
 import android.os.Handler;
@@ -67,6 +70,7 @@
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 /** An hint service implementation that runs in System Server process. */
 public final class HintManagerService extends SystemService {
@@ -87,7 +91,7 @@
     @GuardedBy("mLock")
     private final ArrayMap<Integer, ArrayMap<IBinder, ArraySet<AppHintSession>>> mActiveSessions;
 
-    /** Lock to protect HAL handles and listen list. */
+    /** Lock to protect mActiveSessions. */
     private final Object mLock = new Object();
 
     @GuardedBy("mNonIsolatedTidsLock")
@@ -104,6 +108,8 @@
 
     private final Context mContext;
 
+    private AtomicBoolean mConfigCreationSupport = new AtomicBoolean(true);
+
     private static final String PROPERTY_SF_ENABLE_CPU_HINT = "debug.sf.enable_adpf_cpu_hint";
     private static final String PROPERTY_HWUI_ENABLE_HINT_MANAGER = "debug.hwui.use_hint_manager";
 
@@ -217,6 +223,9 @@
         private static native long nativeCreateHintSession(int tgid, int uid, int[] tids,
                 long durationNanos);
 
+        private static native long nativeCreateHintSessionWithConfig(int tgid, int uid, int[] tids,
+                long durationNanos, int tag, SessionConfig config);
+
         private static native void nativePauseHintSession(long halPtr);
 
         private static native void nativeResumeHintSession(long halPtr);
@@ -253,6 +262,12 @@
             return nativeCreateHintSession(tgid, uid, tids, durationNanos);
         }
 
+        /** Wrapper for HintManager.nativeCreateHintSessionWithConfig */
+        public long halCreateHintSessionWithConfig(
+                int tgid, int uid, int[] tids, long durationNanos, int tag, SessionConfig config) {
+            return nativeCreateHintSessionWithConfig(tgid, uid, tids, durationNanos, tag, config);
+        }
+
         /** Wrapper for HintManager.nativePauseHintSession */
         public void halPauseHintSession(long halPtr) {
             nativePauseHintSession(halPtr);
@@ -612,8 +627,12 @@
     @VisibleForTesting
     final class BinderService extends IHintManager.Stub {
         @Override
-        public IHintSession createHintSession(IBinder token, int[] tids, long durationNanos) {
-            if (!isHalSupported()) return null;
+        public IHintSession createHintSessionWithConfig(@NonNull IBinder token,
+                @NonNull int[] tids, long durationNanos, @SessionTag int tag,
+                @Nullable SessionConfig config) {
+            if (!isHalSupported()) {
+                throw new UnsupportedOperationException("PowerHAL is not supported!");
+            }
 
             java.util.Objects.requireNonNull(token);
             java.util.Objects.requireNonNull(tids);
@@ -634,8 +653,35 @@
                     throw new SecurityException(errMsg);
                 }
 
-                long halSessionPtr = mNativeWrapper.halCreateHintSession(callingTgid, callingUid,
-                        tids, durationNanos);
+                Long halSessionPtr = null;
+                if (mConfigCreationSupport.get()) {
+                    try {
+                        halSessionPtr = mNativeWrapper.halCreateHintSessionWithConfig(
+                                callingTgid, callingUid, tids, durationNanos, tag, config);
+                    } catch (UnsupportedOperationException e) {
+                        mConfigCreationSupport.set(false);
+                    } catch (IllegalStateException e) {
+                        Slog.e("createHintSessionWithConfig failed: ", e.getMessage());
+                        throw new IllegalStateException(
+                            "createHintSessionWithConfig failed: " + e.getMessage());
+                    }
+                }
+
+                if (halSessionPtr == null) {
+                    try {
+                        halSessionPtr = mNativeWrapper.halCreateHintSession(callingTgid,
+                                callingUid, tids, durationNanos);
+                    } catch (UnsupportedOperationException e) {
+                        Slog.w("createHintSession unsupported: ", e.getMessage());
+                        throw new UnsupportedOperationException(
+                            "createHintSession unsupported: " + e.getMessage());
+                    } catch (IllegalStateException e) {
+                        Slog.e("createHintSession failed: ", e.getMessage());
+                        throw new IllegalStateException(
+                            "createHintSession failed: " + e.getMessage());
+                    }
+                }
+
                 if (powerhintThreadCleanup()) {
                     synchronized (mNonIsolatedTidsLock) {
                         for (int i = nonIsolated.size() - 1; i >= 0; i--) {
@@ -644,9 +690,6 @@
                         }
                     }
                 }
-                if (halSessionPtr == 0) {
-                    return null;
-                }
 
                 AppHintSession hs = new AppHintSession(callingUid, callingTgid, tids, token,
                         halSessionPtr, durationNanos);
diff --git a/services/core/java/com/android/server/vcn/Vcn.java b/services/core/java/com/android/server/vcn/Vcn.java
index 37f0450..5a5f7ef 100644
--- a/services/core/java/com/android/server/vcn/Vcn.java
+++ b/services/core/java/com/android/server/vcn/Vcn.java
@@ -626,8 +626,12 @@
      * Dumps the state of this Vcn for logging and debugging purposes.
      *
      * <p>PII and credentials MUST NEVER be dumped here.
+     *
+     * <p>This method is not thread safe and MUST run on the VCN thread.
      */
     public void dump(IndentingPrintWriter pw) {
+        mVcnContext.ensureRunningOnLooperThread();
+
         pw.println("Vcn (" + mSubscriptionGroup + "):");
         pw.increaseIndent();
 
diff --git a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
index 3094b18..8d378a0 100644
--- a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
+++ b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
@@ -2580,8 +2580,12 @@
      * Dumps the state of this VcnGatewayConnection for logging and debugging purposes.
      *
      * <p>PII and credentials MUST NEVER be dumped here.
+     *
+     * <p>This method is not thread safe and MUST run on the VCN thread.
      */
     public void dump(IndentingPrintWriter pw) {
+        mVcnContext.ensureRunningOnLooperThread();
+
         pw.println("VcnGatewayConnection (" + mConnectionConfig.getGatewayConnectionName() + "):");
         pw.increaseIndent();
 
@@ -2602,6 +2606,19 @@
         mUnderlyingNetworkController.dump(pw);
         pw.println();
 
+        if (mIkeSession == null) {
+            pw.println("mIkeSession: null");
+        } else {
+            pw.println("mIkeSession:");
+
+            // Add a try catch block in case IkeSession#dump is not thread-safe
+            try {
+                mIkeSession.dump(pw);
+            } catch (Exception e) {
+                Slog.wtf(TAG, "Failed to dump IkeSession: " + e);
+            }
+        }
+
         pw.decreaseIndent();
     }
 
@@ -2905,6 +2922,11 @@
         public void setNetwork(@NonNull Network network) {
             mImpl.setNetwork(network);
         }
+
+        /** Dumps the state of the IkeSession */
+        public void dump(@NonNull IndentingPrintWriter pw) {
+            mImpl.dump(pw);
+        }
     }
 
     /** Proxy Implementation of WakeLock, used for testing. */
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 54e932a..8c998c3 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -8363,7 +8363,7 @@
      *         aspect ratio.
      */
     boolean shouldCreateCompatDisplayInsets() {
-        if (mLetterboxUiController.shouldApplyUserFullscreenOverride()) {
+        if (mLetterboxUiController.hasFullscreenOverride()) {
             // If the user has forced the applications aspect ratio to be fullscreen, don't use size
             // compatibility mode in any situation. The user has been warned and therefore accepts
             // the risk of the application misbehaving.
@@ -8378,7 +8378,11 @@
             default:
                 // Fall through
         }
-        if (inMultiWindowMode() || getWindowConfiguration().hasWindowDecorCaption()) {
+        // Use root activity's info for tasks in multi-window mode, or fullscreen tasks in freeform
+        // task display areas, to ensure visual consistency across activity launches and exits in
+        // the same task.
+        final TaskDisplayArea tda = getTaskDisplayArea();
+        if (inMultiWindowMode() || (tda != null && tda.inFreeformWindowingMode())) {
             final ActivityRecord root = task != null ? task.getRootActivity() : null;
             if (root != null && root != this && !root.shouldCreateCompatDisplayInsets()) {
                 // If the root activity doesn't use size compatibility mode, the activities above
diff --git a/services/core/java/com/android/server/wm/ConfigurationContainer.java b/services/core/java/com/android/server/wm/ConfigurationContainer.java
index be7c18c..31754bf 100644
--- a/services/core/java/com/android/server/wm/ConfigurationContainer.java
+++ b/services/core/java/com/android/server/wm/ConfigurationContainer.java
@@ -467,13 +467,6 @@
         onRequestedOverrideConfigurationChanged(mRequestsTmpConfig);
     }
 
-    /** Sets the windowing mode for the configuration container. */
-    void setDisplayWindowingMode(int windowingMode) {
-        mRequestsTmpConfig.setTo(getRequestedOverrideConfiguration());
-        mRequestsTmpConfig.windowConfiguration.setDisplayWindowingMode(windowingMode);
-        onRequestedOverrideConfigurationChanged(mRequestsTmpConfig);
-    }
-
     /**
      * Returns true if this container is currently in multi-window mode. I.e. sharing the screen
      * with another activity.
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 00d42e0..6c757a3 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -2456,7 +2456,6 @@
         config.windowConfiguration.setBounds(mTmpRect);
         config.windowConfiguration.setMaxBounds(mTmpRect);
         config.windowConfiguration.setWindowingMode(getWindowingMode());
-        config.windowConfiguration.setDisplayWindowingMode(getWindowingMode());
 
         computeScreenAppConfiguration(config, dw, dh, displayInfo.rotation);
 
@@ -2834,11 +2833,6 @@
         }
     }
 
-    @Override
-    void setDisplayWindowingMode(int windowingMode) {
-        setWindowingMode(windowingMode);
-    }
-
     /**
      * See {@code WindowState#applyImeWindowsIfNeeded} for the details that we won't traverse the
      * IME window in some cases.
@@ -2956,7 +2950,6 @@
     void onDisplayChanged(DisplayContent dc) {
         super.onDisplayChanged(dc);
         updateSystemGestureExclusionLimit();
-        updateKeepClearAreas();
     }
 
     void updateSystemGestureExclusionLimit() {
@@ -4035,7 +4028,6 @@
         }
 
         adjustForImeIfNeeded();
-        updateKeepClearAreas();
 
         // We may need to schedule some toast windows to be removed. The toasts for an app that
         // does not have input focus are removed within a timeout to prevent apps to redress
@@ -6156,6 +6148,7 @@
                     Region touchableRegion = Region.obtain();
                     w.getEffectiveTouchableRegion(touchableRegion);
                     RegionUtils.forEachRect(touchableRegion, rect -> outUnrestricted.add(rect));
+                    touchableRegion.recycle();
                 }
             }
 
diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java
index 3a04bcd..dfee164 100644
--- a/services/core/java/com/android/server/wm/InsetsStateController.java
+++ b/services/core/java/com/android/server/wm/InsetsStateController.java
@@ -224,7 +224,6 @@
         if (changed) {
             notifyInsetsChanged();
             mDisplayContent.updateSystemGestureExclusion();
-            mDisplayContent.updateKeepClearAreas();
             mDisplayContent.getDisplayPolicy().updateSystemBarAttributes();
         }
     }
diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java
index 90a3b25..2c27b98 100644
--- a/services/core/java/com/android/server/wm/TaskDisplayArea.java
+++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java
@@ -1880,7 +1880,6 @@
         mTempConfiguration.setTo(getRequestedOverrideConfiguration());
         WindowConfiguration tempRequestWindowConfiguration = mTempConfiguration.windowConfiguration;
         tempRequestWindowConfiguration.setWindowingMode(windowingMode);
-        tempRequestWindowConfiguration.setDisplayWindowingMode(windowingMode);
         onRequestedOverrideConfigurationChanged(mTempConfiguration);
     }
 
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index fc85af5..7a8bea0 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -1971,6 +1971,15 @@
         return SCREEN_ORIENTATION_UNSET;
     }
 
+    @ActivityInfo.ScreenOrientation
+    @Override
+    protected int getOverrideOrientation() {
+        if (isEmbedded() && !isVisibleRequested()) {
+            return SCREEN_ORIENTATION_UNSPECIFIED;
+        }
+        return super.getOverrideOrientation();
+    }
+
     /**
      * Whether or not to allow this container to specify an app requested orientation.
      *
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 4180475..7afd34c 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -1025,7 +1025,9 @@
                     break;
                 }
                 final Task task = wc.asTask();
-
+                if (task.isVisibleRequested() || task.isVisible()) {
+                    effects |= TRANSACT_EFFECTS_LIFECYCLE;
+                }
                 if (task.isLeafTask()) {
                     mService.mTaskSupervisor
                             .removeTask(task, true, REMOVE_FROM_RECENTS, "remove-task"
diff --git a/services/core/jni/com_android_server_hint_HintManagerService.cpp b/services/core/jni/com_android_server_hint_HintManagerService.cpp
index 5b8ef19..be18835 100644
--- a/services/core/jni/com_android_server_hint_HintManagerService.cpp
+++ b/services/core/jni/com_android_server_hint_HintManagerService.cpp
@@ -34,13 +34,13 @@
 
 #include "jni.h"
 
+using aidl::android::hardware::power::SessionConfig;
 using aidl::android::hardware::power::SessionHint;
 using aidl::android::hardware::power::SessionMode;
+using aidl::android::hardware::power::SessionTag;
 using aidl::android::hardware::power::WorkDuration;
 using android::power::PowerHintSessionWrapper;
 
-using android::base::StringPrintf;
-
 namespace android {
 
 static struct {
@@ -66,6 +66,15 @@
     return rate;
 }
 
+void throwUnsupported(JNIEnv* env, const char* msg) {
+    env->ThrowNew(env->FindClass("java/lang/UnsupportedOperationException"), msg);
+}
+
+void throwFailed(JNIEnv* env, const char* msg) {
+    // We throw IllegalStateException for all errors other than the "unsupported" ones
+    env->ThrowNew(env->FindClass("java/lang/IllegalStateException"), msg);
+}
+
 static jlong createHintSession(JNIEnv* env, int32_t tgid, int32_t uid,
                                std::vector<int32_t>& threadIds, int64_t durationNanos) {
     auto result = gPowerHalController.createHintSession(tgid, uid, threadIds, durationNanos);
@@ -76,10 +85,38 @@
         return res.second ? session_ptr : 0;
     } else if (result.isFailed()) {
         ALOGW("createHintSession failed with message: %s", result.errorMessage());
+        throwFailed(env, result.errorMessage());
+    } else if (result.isUnsupported()) {
+        throwUnsupported(env, result.errorMessage());
+        return -1;
     }
     return 0;
 }
 
+static jlong createHintSessionWithConfig(JNIEnv* env, int32_t tgid, int32_t uid,
+                                         std::vector<int32_t> threadIds, int64_t durationNanos,
+                                         int32_t sessionTag, SessionConfig& config) {
+    auto result =
+            gPowerHalController.createHintSessionWithConfig(tgid, uid, threadIds, durationNanos,
+                                                            static_cast<SessionTag>(sessionTag),
+                                                            &config);
+    if (result.isOk()) {
+        jlong session_ptr = reinterpret_cast<jlong>(result.value().get());
+        std::scoped_lock sessionLock(gSessionMapLock);
+        auto res = gSessionMap.insert({session_ptr, result.value()});
+        if (!res.second) {
+            throwFailed(env, "PowerHAL provided an invalid session");
+            return 0;
+        }
+        return session_ptr;
+    } else if (result.isUnsupported()) {
+        throwUnsupported(env, result.errorMessage());
+        return -1;
+    }
+    throwFailed(env, result.errorMessage());
+    return 0;
+}
+
 static void pauseHintSession(JNIEnv* env, int64_t session_ptr) {
     auto appSession = reinterpret_cast<PowerHintSessionWrapper*>(session_ptr);
     appSession->pause();
@@ -136,13 +173,34 @@
                                      jintArray tids, jlong durationNanos) {
     ScopedIntArrayRO tidArray(env, tids);
     if (nullptr == tidArray.get() || tidArray.size() == 0) {
-        ALOGW("GetIntArrayElements returns nullptr.");
+        ALOGW("nativeCreateHintSession: GetIntArrayElements returns nullptr.");
         return 0;
     }
     std::vector<int32_t> threadIds(tidArray.get(), tidArray.get() + tidArray.size());
     return createHintSession(env, tgid, uid, threadIds, durationNanos);
 }
 
+static jlong nativeCreateHintSessionWithConfig(JNIEnv* env, jclass /* clazz */, jint tgid, jint uid,
+                                               jintArray tids, jlong durationNanos, jint sessionTag,
+                                               jobject sessionConfig) {
+    ScopedIntArrayRO tidArray(env, tids);
+    if (nullptr == tidArray.get() || tidArray.size() == 0) {
+        ALOGW("nativeCreateHintSessionWithConfig: GetIntArrayElements returns nullptr.");
+        return 0;
+    }
+    std::vector<int32_t> threadIds(tidArray.get(), tidArray.get() + tidArray.size());
+    SessionConfig config;
+    jlong out = createHintSessionWithConfig(env, tgid, uid, std::move(threadIds), durationNanos,
+                                            sessionTag, config);
+    if (out <= 0) {
+        return out;
+    }
+    static jclass configClass = env->FindClass("android/hardware/power/SessionConfig");
+    static jfieldID fid = env->GetFieldID(configClass, "id", "J");
+    env->SetLongField(sessionConfig, fid, config.id);
+    return out;
+}
+
 static void nativePauseHintSession(JNIEnv* env, jclass /* clazz */, jlong session_ptr) {
     pauseHintSession(env, session_ptr);
 }
@@ -215,6 +273,8 @@
         {"nativeInit", "()V", (void*)nativeInit},
         {"nativeGetHintSessionPreferredRate", "()J", (void*)nativeGetHintSessionPreferredRate},
         {"nativeCreateHintSession", "(II[IJ)J", (void*)nativeCreateHintSession},
+        {"nativeCreateHintSessionWithConfig", "(II[IJILandroid/hardware/power/SessionConfig;)J",
+         (void*)nativeCreateHintSessionWithConfig},
         {"nativePauseHintSession", "(J)V", (void*)nativePauseHintSession},
         {"nativeResumeHintSession", "(J)V", (void*)nativeResumeHintSession},
         {"nativeCloseHintSession", "(J)V", (void*)nativeCloseHintSession},
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 2f880ba..88c47f3 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -128,8 +128,7 @@
     jmethodID getVirtualKeyQuietTimeMillis;
     jmethodID getExcludedDeviceNames;
     jmethodID getInputPortAssociations;
-    jmethodID getInputUniqueIdAssociationsByPort;
-    jmethodID getInputUniqueIdAssociationsByDescriptor;
+    jmethodID getInputUniqueIdAssociations;
     jmethodID getDeviceTypeAssociations;
     jmethodID getKeyboardLayoutAssociations;
     jmethodID getHoverTapTimeout;
@@ -635,13 +634,10 @@
         env->DeleteLocalRef(portAssociations);
     }
 
-    outConfig->uniqueIdAssociationsByPort = readMapFromInterleavedJavaArray<
-            std::string>(gServiceClassInfo.getInputUniqueIdAssociationsByPort,
-                         "getInputUniqueIdAssociationsByPort");
-
-    outConfig->uniqueIdAssociationsByDescriptor = readMapFromInterleavedJavaArray<
-            std::string>(gServiceClassInfo.getInputUniqueIdAssociationsByDescriptor,
-                         "getInputUniqueIdAssociationsByDescriptor");
+    outConfig->uniqueIdAssociations =
+            readMapFromInterleavedJavaArray<std::string>(gServiceClassInfo
+                                                                 .getInputUniqueIdAssociations,
+                                                         "getInputUniqueIdAssociations");
 
     outConfig->deviceTypeAssociations =
             readMapFromInterleavedJavaArray<std::string>(gServiceClassInfo
@@ -3094,11 +3090,8 @@
     GET_METHOD_ID(gServiceClassInfo.getInputPortAssociations, clazz,
             "getInputPortAssociations", "()[Ljava/lang/String;");
 
-    GET_METHOD_ID(gServiceClassInfo.getInputUniqueIdAssociationsByPort, clazz,
-                  "getInputUniqueIdAssociationsByPort", "()[Ljava/lang/String;");
-
-    GET_METHOD_ID(gServiceClassInfo.getInputUniqueIdAssociationsByDescriptor, clazz,
-                  "getInputUniqueIdAssociationsByDescriptor", "()[Ljava/lang/String;");
+    GET_METHOD_ID(gServiceClassInfo.getInputUniqueIdAssociations, clazz,
+                  "getInputUniqueIdAssociations", "()[Ljava/lang/String;");
 
     GET_METHOD_ID(gServiceClassInfo.getDeviceTypeAssociations, clazz, "getDeviceTypeAssociations",
                   "()[Ljava/lang/String;");
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java
index 95c4407..cc5573b 100644
--- a/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java
@@ -103,6 +103,12 @@
         final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
 
         mEnablePostureBasedClosedState = featureFlags.enableFoldablesPostureBasedClosedState();
+        if (mEnablePostureBasedClosedState) {
+            // This configuration doesn't require listening to hall sensor, it solely relies
+            // on the fused hinge angle sensor
+            hallSensor = null;
+        }
+
         mIsDualDisplayBlockingEnabled = featureFlags.enableDualDisplayBlocking();
 
         final DeviceStatePredicateWrapper[] configuration = createConfiguration(
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java b/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java
index bc8643f..daeaa98 100644
--- a/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java
@@ -95,6 +95,7 @@
 
     private final Sensor mHingeAngleSensor;
     private final DisplayManager mDisplayManager;
+    @Nullable
     private final Sensor mHallSensor;
     private static final Predicate<FoldableDeviceStateProvider> ALLOWED = p -> true;
 
@@ -122,7 +123,7 @@
             @NonNull Context context,
             @NonNull SensorManager sensorManager,
             @NonNull Sensor hingeAngleSensor,
-            @NonNull Sensor hallSensor,
+            @Nullable Sensor hallSensor,
             @NonNull DisplayManager displayManager,
             @NonNull DeviceStatePredicateWrapper[] deviceStatePredicateWrappers) {
         this(new FeatureFlagsImpl(), context, sensorManager, hingeAngleSensor, hallSensor,
@@ -135,7 +136,7 @@
             @NonNull Context context,
             @NonNull SensorManager sensorManager,
             @NonNull Sensor hingeAngleSensor,
-            @NonNull Sensor hallSensor,
+            @Nullable Sensor hallSensor,
             @NonNull DisplayManager displayManager,
             @NonNull DeviceStatePredicateWrapper[] deviceStatePredicateWrappers) {
 
@@ -149,7 +150,9 @@
         mIsDualDisplayBlockingEnabled = featureFlags.enableDualDisplayBlocking();
 
         sensorManager.registerListener(this, mHingeAngleSensor, SENSOR_DELAY_FASTEST);
-        sensorManager.registerListener(this, mHallSensor, SENSOR_DELAY_FASTEST);
+        if (hallSensor != null) {
+            sensorManager.registerListener(this, mHallSensor, SENSOR_DELAY_FASTEST);
+        }
 
         mOrderedStates = new DeviceState[deviceStatePredicateWrappers.length];
         for (int i = 0; i < deviceStatePredicateWrappers.length; i++) {
@@ -324,7 +327,7 @@
     @Override
     public void onSensorChanged(SensorEvent event) {
         synchronized (mLock) {
-            if (event.sensor == mHallSensor) {
+            if (mHallSensor != null && event.sensor == mHallSensor) {
                 mLastHallSensorEvent = event;
             } else if (event.sensor == mHingeAngleSensor) {
                 mLastHingeAngleSensorEvent = event;
@@ -359,11 +362,11 @@
     }
 
     @GuardedBy("mLock")
-    private void dumpSensorValues(Sensor sensor, @Nullable SensorEvent event) {
+    private void dumpSensorValues(@Nullable Sensor sensor, @Nullable SensorEvent event) {
         Slog.i(TAG, toSensorValueString(sensor, event));
     }
 
-    private String toSensorValueString(Sensor sensor, @Nullable SensorEvent event) {
+    private String toSensorValueString(@Nullable Sensor sensor, @Nullable SensorEvent event) {
         String sensorString = sensor == null ? "null" : sensor.getName();
         String eventValues = event == null ? "null" : Arrays.toString(event.values);
         return sensorString + " : " + eventValues;
diff --git a/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStyleDeviceStatePolicyTest.java b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStyleDeviceStatePolicyTest.java
index 901f24d..9f07aa8 100644
--- a/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStyleDeviceStatePolicyTest.java
+++ b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStyleDeviceStatePolicyTest.java
@@ -219,6 +219,26 @@
     }
 
     @Test
+    public void test_postureBasedClosedState_createPolicy_doesNotRegisterHallSensor() {
+        mFakeFeatureFlags.setFlag(Flags.FLAG_ENABLE_FOLDABLES_POSTURE_BASED_CLOSED_STATE, true);
+        clearInvocations(mSensorManager);
+
+        mInstrumentation.runOnMainSync(() -> mProvider = createProvider());
+
+        verify(mSensorManager, never()).registerListener(any(), eq(mHallSensor), anyInt());
+    }
+
+    @Test
+    public void test_postureBasedClosedStateDisabled_createPolicy_registersHallSensor() {
+        mFakeFeatureFlags.setFlag(Flags.FLAG_ENABLE_FOLDABLES_POSTURE_BASED_CLOSED_STATE, false);
+        clearInvocations(mSensorManager);
+
+        mInstrumentation.runOnMainSync(() -> mProvider = createProvider());
+
+        verify(mSensorManager).registerListener(any(), eq(mHallSensor), anyInt());
+    }
+
+    @Test
     public void test_noSensorEventsYet_reportOpenedState() {
         mProvider.setListener(mListener);
         verify(mListener).onStateChanged(mDeviceStateCaptor.capture());
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/InputManagerMockHelper.java b/services/tests/servicestests/src/com/android/server/companion/virtual/InputManagerMockHelper.java
index 00c8ed1..74e854e4 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/InputManagerMockHelper.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/InputManagerMockHelper.java
@@ -53,7 +53,7 @@
     private IInputDevicesChangedListener mDevicesChangedListener;
     private final Map<String /* uniqueId */, Integer /* displayId */> mDisplayIdMapping =
             new HashMap<>();
-    private final Map<String /* phys */, String /* uniqueId */> mUniqueIdAssociationByPort =
+    private final Map<String /* phys */, String /* uniqueId */> mUniqueIdAssociation =
             new HashMap<>();
 
     InputManagerMockHelper(TestableLooper testableLooper,
@@ -79,11 +79,10 @@
         when(mIInputManagerMock.getInputDeviceIds()).thenReturn(new int[0]);
         doAnswer(inv -> mDevices.get(inv.getArgument(0)))
                 .when(mIInputManagerMock).getInputDevice(anyInt());
-        doAnswer(inv -> mUniqueIdAssociationByPort.put(inv.getArgument(0),
-                inv.getArgument(1))).when(mIInputManagerMock).addUniqueIdAssociationByPort(
-                        anyString(), anyString());
-        doAnswer(inv -> mUniqueIdAssociationByPort.remove(inv.getArgument(0))).when(
-                mIInputManagerMock).removeUniqueIdAssociationByPort(anyString());
+        doAnswer(inv -> mUniqueIdAssociation.put(inv.getArgument(0), inv.getArgument(1))).when(
+                mIInputManagerMock).addUniqueIdAssociation(anyString(), anyString());
+        doAnswer(inv -> mUniqueIdAssociation.remove(inv.getArgument(0))).when(
+                mIInputManagerMock).removeUniqueIdAssociation(anyString());
 
         // Set a new instance of InputManager for testing that uses the IInputManager mock as the
         // interface to the server.
@@ -113,7 +112,7 @@
                 .setDescriptor(phys)
                 .setExternal(true)
                 .setAssociatedDisplayId(
-                        mDisplayIdMapping.getOrDefault(mUniqueIdAssociationByPort.get(phys),
+                        mDisplayIdMapping.getOrDefault(mUniqueIdAssociation.get(phys),
                                 Display.INVALID_DISPLAY))
                 .build();
 
diff --git a/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java
index 510e7c4..5902caa 100644
--- a/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java
@@ -22,6 +22,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
@@ -41,6 +42,8 @@
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
 import android.content.Context;
+import android.hardware.power.SessionConfig;
+import android.hardware.power.SessionTag;
 import android.hardware.power.WorkDuration;
 import android.os.Binder;
 import android.os.IBinder;
@@ -63,6 +66,8 @@
 import org.junit.Test;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -97,6 +102,7 @@
 
     private static final long DEFAULT_HINT_PREFERRED_RATE = 16666666L;
     private static final long DEFAULT_TARGET_DURATION = 16666666L;
+    private static final long DOUBLED_TARGET_DURATION = 33333333L;
     private static final long CONCURRENCY_TEST_DURATION_SEC = 10;
     private static final int UID = Process.myUid();
     private static final int TID = Process.myPid();
@@ -106,6 +112,8 @@
     private static final int[] SESSION_TIDS_C = new int[] {TID};
     private static final long[] DURATIONS_THREE = new long[] {1L, 100L, 1000L};
     private static final long[] TIMESTAMPS_THREE = new long[] {1L, 2L, 3L};
+    private static final long[] SESSION_PTRS = new long[] {11L, 22L, 33L};
+    private static final long[] SESSION_IDS = new long[] {1L, 11L, 111L};
     private static final long[] DURATIONS_ZERO = new long[] {};
     private static final long[] TIMESTAMPS_ZERO = new long[] {};
     private static final long[] TIMESTAMPS_TWO = new long[] {1L, 2L};
@@ -129,21 +137,61 @@
 
     private HintManagerService mService;
 
+    private static Answer<Long> fakeCreateWithConfig(Long ptr, Long sessionId) {
+        return new Answer<Long>() {
+            public Long answer(InvocationOnMock invocation) {
+                ((SessionConfig) invocation.getArguments()[5]).id = sessionId;
+                return ptr;
+            }
+        };
+    }
+
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
         when(mNativeWrapperMock.halGetHintSessionPreferredRate())
                 .thenReturn(DEFAULT_HINT_PREFERRED_RATE);
         when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(SESSION_TIDS_A),
-                eq(DEFAULT_TARGET_DURATION))).thenReturn(1L);
+                eq(DEFAULT_TARGET_DURATION))).thenReturn(SESSION_PTRS[0]);
         when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(SESSION_TIDS_B),
-                eq(DEFAULT_TARGET_DURATION))).thenReturn(2L);
+                eq(DOUBLED_TARGET_DURATION))).thenReturn(SESSION_PTRS[1]);
         when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(SESSION_TIDS_C),
-                eq(0L))).thenReturn(1L);
+                eq(0L))).thenReturn(SESSION_PTRS[2]);
+        when(mNativeWrapperMock.halCreateHintSessionWithConfig(eq(TGID), eq(UID),
+                eq(SESSION_TIDS_A), eq(DEFAULT_TARGET_DURATION), anyInt(),
+                any(SessionConfig.class))).thenAnswer(fakeCreateWithConfig(SESSION_PTRS[0],
+                    SESSION_IDS[0]));
+        when(mNativeWrapperMock.halCreateHintSessionWithConfig(eq(TGID), eq(UID),
+                eq(SESSION_TIDS_B), eq(DOUBLED_TARGET_DURATION), anyInt(),
+                any(SessionConfig.class))).thenAnswer(fakeCreateWithConfig(SESSION_PTRS[1],
+                    SESSION_IDS[1]));
+        when(mNativeWrapperMock.halCreateHintSessionWithConfig(eq(TGID), eq(UID),
+                eq(SESSION_TIDS_C), eq(0L), anyInt(),
+                any(SessionConfig.class))).thenAnswer(fakeCreateWithConfig(SESSION_PTRS[2],
+                    SESSION_IDS[2]));
+
         LocalServices.removeServiceForTest(ActivityManagerInternal.class);
         LocalServices.addService(ActivityManagerInternal.class, mAmInternalMock);
     }
 
+    /**
+     * Mocks the creation calls, but without support for new createHintSessionWithConfig method
+     */
+    public void makeConfigCreationUnsupported() {
+        reset(mNativeWrapperMock);
+        when(mNativeWrapperMock.halGetHintSessionPreferredRate())
+                .thenReturn(DEFAULT_HINT_PREFERRED_RATE);
+        when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(SESSION_TIDS_A),
+                eq(DEFAULT_TARGET_DURATION))).thenReturn(SESSION_PTRS[0]);
+        when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(SESSION_TIDS_B),
+                eq(DOUBLED_TARGET_DURATION))).thenReturn(SESSION_PTRS[1]);
+        when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(SESSION_TIDS_C),
+                eq(0L))).thenReturn(SESSION_PTRS[2]);
+        when(mNativeWrapperMock.halCreateHintSessionWithConfig(anyInt(), anyInt(),
+            any(int[].class), anyLong(), anyInt(),
+            any(SessionConfig.class))).thenThrow(new UnsupportedOperationException());
+    }
+
     static class NativeWrapperFake extends NativeWrapper {
         @Override
         public void halInit() {
@@ -160,6 +208,12 @@
         }
 
         @Override
+        public long halCreateHintSessionWithConfig(int tgid, int uid, int[] tids,
+                long durationNanos, int tag, SessionConfig config) {
+            return 1;
+        }
+
+        @Override
         public void halPauseHintSession(long halPtr) {
         }
 
@@ -224,27 +278,57 @@
         IBinder token = new Binder();
         // Make sure we throw exception when adding a TID doesn't belong to the processes
         // In this case, we add `init` PID into the list.
+        SessionConfig config = new SessionConfig();
         assertThrows(SecurityException.class,
-                () -> service.getBinderServiceInstance().createHintSession(token,
-                        new int[]{TID, 1}, DEFAULT_TARGET_DURATION));
+                () -> service.getBinderServiceInstance().createHintSessionWithConfig(token,
+                        new int[]{TID, 1}, DEFAULT_TARGET_DURATION, SessionTag.OTHER, config));
     }
 
     @Test
-    public void testCreateHintSession() throws Exception {
+    public void testCreateHintSessionFallback() throws Exception {
+        HintManagerService service = createService();
+        IBinder token = new Binder();
+        makeConfigCreationUnsupported();
+
+        IHintSession a = service.getBinderServiceInstance().createHintSessionWithConfig(token,
+                SESSION_TIDS_A, DEFAULT_TARGET_DURATION, SessionTag.OTHER, new SessionConfig());
+        assertNotNull(a);
+
+        IHintSession b = service.getBinderServiceInstance().createHintSessionWithConfig(token,
+                SESSION_TIDS_B, DOUBLED_TARGET_DURATION, SessionTag.OTHER, new SessionConfig());
+        assertNotEquals(a, b);
+
+        IHintSession c = service.getBinderServiceInstance().createHintSessionWithConfig(token,
+                SESSION_TIDS_C, 0L, SessionTag.OTHER, new SessionConfig());
+        assertNotNull(c);
+        verify(mNativeWrapperMock, times(3)).halCreateHintSession(anyInt(), anyInt(),
+                                                                  any(int[].class), anyLong());
+    }
+
+    @Test
+    public void testCreateHintSessionWithConfig() throws Exception {
         HintManagerService service = createService();
         IBinder token = new Binder();
 
-        IHintSession a = service.getBinderServiceInstance().createHintSession(token,
-                SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
+        SessionConfig config = new SessionConfig();
+        IHintSession a = service.getBinderServiceInstance().createHintSessionWithConfig(token,
+                SESSION_TIDS_A, DEFAULT_TARGET_DURATION, SessionTag.OTHER, config);
         assertNotNull(a);
+        assertEquals(SESSION_IDS[0], config.id);
 
-        IHintSession b = service.getBinderServiceInstance().createHintSession(token,
-                SESSION_TIDS_B, DEFAULT_TARGET_DURATION);
+        SessionConfig config2 = new SessionConfig();
+        IHintSession b = service.getBinderServiceInstance().createHintSessionWithConfig(token,
+                SESSION_TIDS_B, DOUBLED_TARGET_DURATION, SessionTag.APP, config2);
         assertNotEquals(a, b);
+        assertEquals(SESSION_IDS[1], config2.id);
 
-        IHintSession c = service.getBinderServiceInstance().createHintSession(token,
-                SESSION_TIDS_C, 0L);
+        SessionConfig config3 = new SessionConfig();
+        IHintSession c = service.getBinderServiceInstance().createHintSessionWithConfig(token,
+                SESSION_TIDS_C, 0L, SessionTag.GAME, config3);
         assertNotNull(c);
+        assertEquals(SESSION_IDS[2], config3.id);
+        verify(mNativeWrapperMock, times(3)).halCreateHintSessionWithConfig(anyInt(), anyInt(),
+                any(int[].class), anyLong(), anyInt(), any(SessionConfig.class));
     }
 
     @Test
@@ -253,7 +337,8 @@
         IBinder token = new Binder();
 
         AppHintSession a = (AppHintSession) service.getBinderServiceInstance()
-                .createHintSession(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
+                .createHintSessionWithConfig(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION,
+                        SessionTag.OTHER, new SessionConfig());
 
         // Set session to background and calling updateHintAllowed() would invoke pause();
         service.mUidObserver.onUidStateChanged(
@@ -288,8 +373,8 @@
         HintManagerService service = createService();
         IBinder token = new Binder();
 
-        IHintSession a = service.getBinderServiceInstance().createHintSession(token,
-                SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
+        IHintSession a = service.getBinderServiceInstance().createHintSessionWithConfig(token,
+                SESSION_TIDS_A, DEFAULT_TARGET_DURATION, SessionTag.OTHER, new SessionConfig());
 
         a.close();
         verify(mNativeWrapperMock, times(1)).halCloseHintSession(anyLong());
@@ -300,8 +385,8 @@
         HintManagerService service = createService();
         IBinder token = new Binder();
 
-        IHintSession a = service.getBinderServiceInstance().createHintSession(token,
-                SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
+        IHintSession a = service.getBinderServiceInstance().createHintSessionWithConfig(token,
+                SESSION_TIDS_A, DEFAULT_TARGET_DURATION, SessionTag.OTHER, new SessionConfig());
 
         assertThrows(IllegalArgumentException.class, () -> {
             a.updateTargetWorkDuration(-1L);
@@ -321,7 +406,8 @@
         IBinder token = new Binder();
 
         AppHintSession a = (AppHintSession) service.getBinderServiceInstance()
-                .createHintSession(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
+                .createHintSessionWithConfig(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION,
+                        SessionTag.OTHER, new SessionConfig());
 
         a.updateTargetWorkDuration(100L);
         a.reportActualWorkDuration(DURATIONS_THREE, TIMESTAMPS_THREE);
@@ -363,7 +449,8 @@
         IBinder token = new Binder();
 
         AppHintSession a = (AppHintSession) service.getBinderServiceInstance()
-                .createHintSession(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
+                .createHintSessionWithConfig(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION,
+                    SessionTag.OTHER, new SessionConfig());
 
         a.sendHint(PerformanceHintManager.Session.CPU_LOAD_RESET);
         verify(mNativeWrapperMock, times(1)).halSendHint(anyLong(),
@@ -389,7 +476,8 @@
         IBinder token = new Binder();
 
         AppHintSession a = (AppHintSession) service.getBinderServiceInstance()
-                .createHintSession(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
+                .createHintSessionWithConfig(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION,
+                    SessionTag.OTHER, new SessionConfig());
 
         service.mUidObserver.onUidStateChanged(
                 a.mUid, ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND, 0, 0);
@@ -410,7 +498,8 @@
         IBinder token = new Binder();
 
         AppHintSession a = (AppHintSession) service.getBinderServiceInstance()
-                .createHintSession(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
+                .createHintSessionWithConfig(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION,
+                        SessionTag.OTHER, new SessionConfig());
 
         service.mUidObserver.onUidStateChanged(
                 a.mUid, ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND, 0, 0);
@@ -423,7 +512,8 @@
         IBinder token = new Binder();
 
         AppHintSession a = (AppHintSession) service.getBinderServiceInstance()
-                .createHintSession(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
+                .createHintSessionWithConfig(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION,
+                        SessionTag.OTHER, new SessionConfig());
 
         a.updateTargetWorkDuration(100L);
 
@@ -454,10 +544,12 @@
         int threadCount = 3;
         int[] tids1 = createThreads(threadCount, stopLatch1);
         long sessionPtr1 = 111;
-        when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(tids1),
-                eq(DEFAULT_TARGET_DURATION))).thenReturn(sessionPtr1);
+        when(mNativeWrapperMock.halCreateHintSessionWithConfig(eq(TGID), eq(UID), eq(tids1),
+                eq(DEFAULT_TARGET_DURATION), anyInt(), any(SessionConfig.class)))
+                .thenReturn(sessionPtr1);
         AppHintSession session1 = (AppHintSession) service.getBinderServiceInstance()
-                .createHintSession(token, tids1, DEFAULT_TARGET_DURATION);
+                .createHintSessionWithConfig(token, tids1, DEFAULT_TARGET_DURATION,
+                        SessionTag.OTHER, new SessionConfig());
         assertNotNull(session1);
 
         // for test only to avoid conflicting with any real thread that exists on device
@@ -473,10 +565,12 @@
         tids2WithIsolated[threadCount] = isoProc1;
         tids2WithIsolated[threadCount + 1] = isoProc2;
         long sessionPtr2 = 222;
-        when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(tids2WithIsolated),
-                eq(DEFAULT_TARGET_DURATION))).thenReturn(sessionPtr2);
+        when(mNativeWrapperMock.halCreateHintSessionWithConfig(eq(TGID), eq(UID),
+                eq(tids2WithIsolated), eq(DEFAULT_TARGET_DURATION), anyInt(),
+                any(SessionConfig.class))).thenReturn(sessionPtr2);
         AppHintSession session2 = (AppHintSession) service.getBinderServiceInstance()
-                .createHintSession(token, tids2WithIsolated, DEFAULT_TARGET_DURATION);
+                .createHintSessionWithConfig(token, tids2WithIsolated,
+                        DEFAULT_TARGET_DURATION, SessionTag.OTHER, new SessionConfig());
         assertNotNull(session2);
 
         // trigger clean up through UID state change by making the process background
@@ -608,7 +702,8 @@
         IBinder token = new Binder();
 
         AppHintSession a = (AppHintSession) service.getBinderServiceInstance()
-                .createHintSession(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
+                .createHintSessionWithConfig(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION,
+                        SessionTag.OTHER, new SessionConfig());
 
         a.setMode(0, true);
         verify(mNativeWrapperMock, times(1)).halSetMode(anyLong(),
@@ -726,7 +821,8 @@
             AtomicReference<Boolean> shouldRun) throws Exception {
         IBinder token = new Binder();
         AppHintSession a = (AppHintSession) service.getBinderServiceInstance()
-                .createHintSession(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
+                .createHintSessionWithConfig(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION,
+                        SessionTag.OTHER, new SessionConfig());
         // we will start some threads and get their valid TIDs to update
         int threadCount = 3;
         // the list of TIDs
@@ -793,7 +889,8 @@
         IBinder token = new Binder();
 
         AppHintSession a = (AppHintSession) service.getBinderServiceInstance()
-                .createHintSession(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
+                .createHintSessionWithConfig(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION,
+                        SessionTag.OTHER, new SessionConfig());
 
         a.updateTargetWorkDuration(100L);
         a.reportActualWorkDuration2(WORK_DURATIONS_FIVE);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java
index ae36839..983e694 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java
@@ -70,6 +70,7 @@
 import android.os.UserHandle;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.service.notification.INotificationListener;
+import android.service.notification.IStatusBarNotificationHolder;
 import android.service.notification.NotificationListenerFilter;
 import android.service.notification.NotificationListenerService;
 import android.service.notification.NotificationRankingUpdate;
@@ -90,6 +91,7 @@
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
 import org.mockito.ArgumentMatcher;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
@@ -154,6 +156,11 @@
                 .thenReturn(new ArrayList<>());
         mNm.mHandler = mock(NotificationManagerService.WorkerHandler.class);
         mNm.mAssistants = mock(NotificationManagerService.NotificationAssistants.class);
+        FieldSetter.setField(mNm,
+                NotificationManagerService.class.getDeclaredField("mListeners"),
+                mListeners);
+        doReturn(android.service.notification.NotificationListenerService.TRIM_FULL)
+                .when(mListeners).getOnNotificationPostedTrim(any());
     }
 
     @Test
@@ -827,6 +834,68 @@
         verify(mListeners, never()).redactStatusBarNotification(eq(sbn));
     }
 
+    @Test
+    public void testListenerPost_UpdateLifetimeExtended() throws Exception {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR);
+
+        // Create original notification, with FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY.
+        String pkg = "pkg";
+        int uid = 9;
+        UserHandle userHandle = UserHandle.getUserHandleForUid(uid);
+        NotificationChannel channel = new NotificationChannel("id", "name",
+                NotificationManager.IMPORTANCE_HIGH);
+        Notification.Builder nb = new Notification.Builder(mContext, channel.getId())
+                .setContentTitle("foo")
+                .setSmallIcon(android.R.drawable.sym_def_app_icon)
+                .setFlag(Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, true);
+        StatusBarNotification sbn = new StatusBarNotification(pkg, pkg, 8, "tag", uid, 0,
+                nb.build(), userHandle, null, 0);
+        NotificationRecord old = new NotificationRecord(mContext, sbn, channel);
+
+        // Creates updated notification (without FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY)
+        Notification.Builder nb2 = new Notification.Builder(mContext, channel.getId())
+                .setContentTitle("new title")
+                .setSmallIcon(android.R.drawable.sym_def_app_icon)
+                .setFlag(Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, false);
+        StatusBarNotification sbn2 = new StatusBarNotification(pkg, pkg, 8, "tag", uid, 0,
+                nb2.build(), userHandle, null, 0);
+        NotificationRecord toPost = new NotificationRecord(mContext, sbn2, channel);
+
+        // Create system ui-like service.
+        ManagedServices.ManagedServiceInfo info = mListeners.new ManagedServiceInfo(
+                null, new ComponentName("a", "a"), sbn2.getUserId(), false, null, 33, 33);
+        info.isSystemUi = true;
+        INotificationListener l1 = mock(INotificationListener.class);
+        info.service = l1;
+        List<ManagedServices.ManagedServiceInfo> services = ImmutableList.of(info);
+        when(mListeners.getServices()).thenReturn(services);
+
+        FieldSetter.setField(mNm,
+                NotificationManagerService.class.getDeclaredField("mHandler"),
+                mock(NotificationManagerService.WorkerHandler.class));
+        doReturn(true).when(mNm).isVisibleToListener(any(), anyInt(), any());
+        doReturn(mock(NotificationRankingUpdate.class)).when(mNm).makeRankingUpdateLocked(info);
+        doReturn(false).when(mNm).isInLockDownMode(anyInt());
+        doNothing().when(mNm).updateUriPermissions(any(), any(), any(), anyInt());
+        doReturn(sbn2).when(mListeners).redactStatusBarNotification(sbn2);
+        doReturn(sbn2).when(mListeners).redactStatusBarNotification(any());
+
+        // The notification change is posted to the service listener.
+        mListeners.notifyPostedLocked(toPost, old);
+
+        // Verify that the post occcurs with the updated notification value.
+        ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
+        verify(mNm.mHandler, times(1)).post(runnableCaptor.capture());
+        runnableCaptor.getValue().run();
+        ArgumentCaptor<IStatusBarNotificationHolder> sbnCaptor =
+                ArgumentCaptor.forClass(IStatusBarNotificationHolder.class);
+        verify(l1, times(1)).onNotificationPosted(sbnCaptor.capture(), any());
+        StatusBarNotification sbnResult = sbnCaptor.getValue().get();
+        assertThat(sbnResult.getNotification()
+                .extras.getCharSequence(Notification.EXTRA_TITLE).toString())
+                .isEqualTo("new title");
+    }
+
     /**
      * Helper method to test the thread safety of some operations.
      *
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 011f2e3..aec4714 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -106,6 +106,7 @@
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.INVALID_DISPLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_TOAST;
+
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN;
 import static com.android.server.am.PendingIntentRecord.FLAG_ACTIVITY_SENDER;
 import static com.android.server.am.PendingIntentRecord.FLAG_BROADCAST_SENDER;
@@ -118,11 +119,11 @@
 import static com.android.server.notification.NotificationRecordLogger.NotificationReportedEvent.NOTIFICATION_ADJUSTED;
 import static com.android.server.notification.NotificationRecordLogger.NotificationReportedEvent.NOTIFICATION_POSTED;
 import static com.android.server.notification.NotificationRecordLogger.NotificationReportedEvent.NOTIFICATION_UPDATED;
+
 import static com.google.common.collect.Iterables.getOnlyElement;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
-import static java.util.Collections.emptyList;
-import static java.util.Collections.singletonList;
+
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertNotNull;
@@ -131,6 +132,7 @@
 import static junit.framework.Assert.assertSame;
 import static junit.framework.Assert.assertTrue;
 import static junit.framework.Assert.fail;
+
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertThrows;
 import static org.mockito.ArgumentMatchers.isNull;
@@ -157,6 +159,9 @@
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+
 import android.Manifest;
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
@@ -270,8 +275,10 @@
 import android.util.Xml;
 import android.view.accessibility.AccessibilityManager;
 import android.widget.RemoteViews;
+
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
+
 import com.android.internal.R;
 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
 import com.android.internal.config.sysui.TestableFlagResolver;
@@ -303,10 +310,12 @@
 import com.android.server.utils.quota.MultiRateLimiter;
 import com.android.server.wm.ActivityTaskManagerInternal;
 import com.android.server.wm.WindowManagerInternal;
+
 import com.google.android.collect.Lists;
 import com.google.common.collect.ImmutableList;
 import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
 import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
+
 import org.junit.After;
 import org.junit.Assert;
 import org.junit.Before;
@@ -5934,6 +5943,45 @@
     }
 
     @Test
+    @EnableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR)
+    public void testUpdate_DirectReplyLifetimeExtendedUpdateSucceeds() throws Exception {
+        // Creates a lifetime extended notification.
+        NotificationRecord original = generateNotificationRecord(mTestNotificationChannel);
+        original.getSbn().getNotification().flags |= FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
+        mService.addNotification(original);
+
+        // Post an update for that notification.
+        StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, original.getSbn().getId(),
+                original.getSbn().getTag(), mUid, 0,
+                new Notification.Builder(mContext, mTestNotificationChannel.getId())
+                        .setSmallIcon(android.R.drawable.sym_def_app_icon)
+                        .setContentTitle("new title").build(),
+                UserHandle.getUserHandleForUid(mUid), null, 0);
+        NotificationRecord update = new NotificationRecord(mContext, sbn, mTestNotificationChannel);
+        mService.addEnqueuedNotification(update);
+
+        NotificationManagerService.PostNotificationRunnable runnable =
+                mService.new PostNotificationRunnable(update.getKey(),
+                        update.getSbn().getPackageName(),
+                        update.getUid(),
+                        mPostNotificationTrackerFactory.newTracker(null));
+        runnable.run();
+        waitForIdle();
+
+        // Checks the update was sent, and that update contains the new title, and does not contain
+        // the lifetime extension flag.
+        ArgumentCaptor<NotificationRecord> captor =
+                ArgumentCaptor.forClass(NotificationRecord.class);
+        verify(mListeners, times(1)).prepareNotifyPostedLocked(captor.capture(), any(),
+                anyBoolean());
+        assertThat(captor.getValue().getNotification().flags
+                & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo(0);
+        assertThat(captor.getValue()
+                .getNotification().extras.getCharSequence(Notification.EXTRA_TITLE).toString())
+                .isEqualTo("new title");
+    }
+
+    @Test
     public void testStats_updatedOnUserExpansion() throws Exception {
         NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
         mService.addNotification(r);
@@ -12602,7 +12650,6 @@
 
         // the notifyPostedLocked function is called twice.
         verify(mWorkerHandler, times(2)).postDelayed(any(Runnable.class), anyLong());
-        //verify(mListeners, times(2)).notifyPostedLocked(any(), any());
     }
 
     @Test
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenEnumTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenEnumTest.java
index f724510..8add2f9 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenEnumTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenEnumTest.java
@@ -21,8 +21,8 @@
 import android.app.AutomaticZenRule;
 import android.provider.Settings;
 import android.service.notification.ZenPolicy;
-import android.test.suitebuilder.annotation.SmallTest;
 
+import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.os.dnd.ActiveRuleType;
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index 9697c65..000162a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -502,7 +502,6 @@
         final WindowConfiguration translucentWinConf = requestedConfig.windowConfiguration;
         translucentWinConf.setActivityType(ACTIVITY_TYPE_STANDARD);
         translucentWinConf.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
-        translucentWinConf.setDisplayWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
         translucentWinConf.setAlwaysOnTop(true);
         translucentActivity.onRequestedOverrideConfigurationChanged(requestedConfig);
 
@@ -511,7 +510,6 @@
         // The original override of WindowConfiguration should keep.
         assertEquals(ACTIVITY_TYPE_STANDARD, translucentActivity.getActivityType());
         assertEquals(WINDOWING_MODE_MULTI_WINDOW, translucentWinConf.getWindowingMode());
-        assertEquals(WINDOWING_MODE_MULTI_WINDOW, translucentWinConf.getDisplayWindowingMode());
         assertTrue(translucentWinConf.isAlwaysOnTop());
         // Unless display is going to be rotated, it should always inherit from parent.
         assertEquals(ROTATION_UNDEFINED, translucentWinConf.getDisplayRotation());
@@ -1384,6 +1382,25 @@
     }
 
     @Test
+    @EnableCompatChanges({ActivityInfo.OVERRIDE_ANY_ORIENTATION_TO_USER})
+    public void testShouldNotCreateCompatDisplays_systemFullscreenOverride() {
+        setUpDisplaySizeWithApp(1000, 2500);
+
+        // Make the task root resizable.
+        mActivity.info.resizeMode = RESIZE_MODE_RESIZEABLE;
+
+        // Create an activity on the same task.
+        final ActivityRecord activity = buildActivityRecord(/* supportsSizeChanges= */false,
+                RESIZE_MODE_UNRESIZEABLE, ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+
+        // Simulate the user selecting the fullscreen user aspect ratio override
+        spyOn(activity.mLetterboxUiController);
+        doReturn(true).when(activity.mLetterboxUiController)
+                .isSystemOverrideToFullscreenEnabled();
+        assertFalse(activity.shouldCreateCompatDisplayInsets());
+    }
+
+    @Test
     @EnableCompatChanges({ActivityInfo.NEVER_SANDBOX_DISPLAY_APIS})
     public void testNeverSandboxDisplayApis_configEnabled_sandboxingNotApplied() {
         setUpDisplaySizeWithApp(1000, 1200);
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
index 002a3d5..65a81c4 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -1050,6 +1050,8 @@
         // TaskFragment override orientation should be set for a system organizer.
         final TaskFragment taskFragment = mWindowOrganizerController.getTaskFragment(fragmentToken);
         assertNotNull(taskFragment);
+
+        taskFragment.setVisibleRequested(true);
         assertEquals(SCREEN_ORIENTATION_BEHIND, taskFragment.getOverrideOrientation());
     }
 
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 4837fcb..a90a158 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
@@ -757,6 +757,7 @@
         final Task task = createTask(mDisplayContent);
         final TaskFragment tf = createTaskFragmentWithActivity(task);
         final ActivityRecord activity = tf.getTopMostActivity();
+        tf.setVisibleRequested(true);
         tf.setOverrideOrientation(SCREEN_ORIENTATION_BEHIND);
 
         // Should report the override orientation
@@ -768,6 +769,26 @@
     }
 
     @Test
+    public void testGetOrientation_reportOverrideOrientation_whenInvisible() {
+        final Task task = createTask(mDisplayContent);
+        final TaskFragment tf = createTaskFragmentWithActivity(task);
+        final ActivityRecord activity = tf.getTopMostActivity();
+        tf.setVisibleRequested(false);
+        tf.setOverrideOrientation(SCREEN_ORIENTATION_BEHIND);
+
+        // Should report SCREEN_ORIENTATION_UNSPECIFIED for the override orientation when invisible
+        assertEquals(SCREEN_ORIENTATION_UNSPECIFIED, tf.getOverrideOrientation());
+
+        // Should report SCREEN_ORIENTATION_UNSET for the orientation
+        assertEquals(SCREEN_ORIENTATION_UNSET, tf.getOrientation(SCREEN_ORIENTATION_UNSET));
+
+        // Should report SCREEN_ORIENTATION_UNSET even if the activity requests a different
+        // value
+        activity.setRequestedOrientation(SCREEN_ORIENTATION_LANDSCAPE);
+        assertEquals(SCREEN_ORIENTATION_UNSET, tf.getOrientation(SCREEN_ORIENTATION_UNSET));
+    }
+
+    @Test
     public void testUpdateImeParentForActivityEmbedding() {
         // Setup two activities in ActivityEmbedding.
         final Task task = createTask(mDisplayContent);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowConfigurationTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowConfigurationTests.java
index 38aac7c..eca51ae 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowConfigurationTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowConfigurationTests.java
@@ -16,9 +16,7 @@
 
 package com.android.server.wm;
 
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
@@ -208,43 +206,6 @@
 
     /** Ensure the window always has a caption in Freeform window mode or display mode. */
     @Test
-    public void testCaptionShownForFreeformWindowingMode() {
-        final WindowConfiguration config = new WindowConfiguration();
-        config.setActivityType(ACTIVITY_TYPE_STANDARD);
-        config.setWindowingMode(WINDOWING_MODE_FREEFORM);
-        config.setDisplayWindowingMode(WINDOWING_MODE_FULLSCREEN);
-        assertTrue(config.hasWindowDecorCaption());
-
-        config.setDisplayWindowingMode(WINDOWING_MODE_FREEFORM);
-        assertTrue(config.hasWindowDecorCaption());
-
-        config.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
-        assertTrue(config.hasWindowDecorCaption());
-
-        config.setDisplayWindowingMode(WINDOWING_MODE_FULLSCREEN);
-        assertFalse(config.hasWindowDecorCaption());
-    }
-
-    /** Caption should not show for non-standard activity window. */
-    @Test
-    public void testCaptionNotShownForNonStandardActivityType() {
-        final WindowConfiguration config = new WindowConfiguration();
-        config.setActivityType(ACTIVITY_TYPE_HOME);
-        config.setWindowingMode(WINDOWING_MODE_FREEFORM);
-        config.setDisplayWindowingMode(WINDOWING_MODE_FREEFORM);
-        assertFalse(config.hasWindowDecorCaption());
-
-        config.setActivityType(ACTIVITY_TYPE_ASSISTANT);
-        assertFalse(config.hasWindowDecorCaption());
-
-        config.setActivityType(ACTIVITY_TYPE_RECENTS);
-        assertFalse(config.hasWindowDecorCaption());
-
-        config.setActivityType(ACTIVITY_TYPE_STANDARD);
-        assertTrue(config.hasWindowDecorCaption());
-    }
-
-    @Test
     public void testMaskedSetTo() {
         final WindowConfiguration config = new WindowConfiguration();
         final WindowConfiguration other = new WindowConfiguration();
diff --git a/tests/FsVerityTest/FsVerityTestApp/src/com/android/fsverity/Helper.java b/tests/FsVerityTest/FsVerityTestApp/src/com/android/fsverity/Helper.java
index 2ed4fec..c52be7c 100644
--- a/tests/FsVerityTest/FsVerityTestApp/src/com/android/fsverity/Helper.java
+++ b/tests/FsVerityTest/FsVerityTestApp/src/com/android/fsverity/Helper.java
@@ -27,6 +27,9 @@
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.platform.app.InstrumentationRegistry;
 
+import com.android.compatibility.common.util.AdoptShellPermissionsRule;
+
+import org.junit.Rule;
 import org.junit.Test;
 
 import java.io.FileOutputStream;
@@ -46,6 +49,12 @@
 
     private static final long BLOCK_SIZE = 4096;
 
+    @Rule
+    public final AdoptShellPermissionsRule mAdoptShellPermissionsRule =
+            new AdoptShellPermissionsRule(
+                    InstrumentationRegistry.getInstrumentation().getUiAutomation(),
+                    android.Manifest.permission.SETUP_FSVERITY);
+
     @Test
     public void prepareTest() throws Exception {
         Context context = ApplicationProvider.getApplicationContext();
diff --git a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
index 8d2b927..f6f766a 100644
--- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
+++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
@@ -19,26 +19,17 @@
 
 import android.content.Context
 import android.content.ContextWrapper
-import android.hardware.display.DisplayManager
 import android.hardware.display.DisplayViewport
-import android.hardware.display.VirtualDisplay
 import android.hardware.input.InputManager
 import android.hardware.input.InputManagerGlobal
-import android.os.InputEventInjectionSync
-import android.os.SystemClock
 import android.os.test.TestLooper
 import android.platform.test.annotations.Presubmit
 import android.platform.test.annotations.RequiresFlagsDisabled
 import android.platform.test.flag.junit.DeviceFlagsValueProvider
 import android.provider.Settings
-import android.view.View.OnKeyListener
-import android.view.Display
-import android.view.InputDevice
-import android.view.KeyEvent
-import android.view.PointerIcon
-import android.view.SurfaceHolder
-import android.view.SurfaceView
 import android.test.mock.MockContentResolver
+import android.view.Display
+import android.view.PointerIcon
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.internal.util.test.FakeSettingsProvider
 import com.google.common.truth.Truth.assertThat
@@ -57,7 +48,6 @@
 import org.mockito.Mockito.`when`
 import org.mockito.Mockito.clearInvocations
 import org.mockito.Mockito.doAnswer
-import org.mockito.Mockito.mock
 import org.mockito.Mockito.never
 import org.mockito.Mockito.spy
 import org.mockito.Mockito.times
@@ -422,174 +412,6 @@
         verify(wmCallbacks).notifyPointerDisplayIdChanged(overrideDisplayId, 0f, 0f)
         thread.join(100 /*millis*/)
     }
-
-    private fun createVirtualDisplays(count: Int): List<VirtualDisplay> {
-        val displayManager: DisplayManager = context.getSystemService(
-                DisplayManager::class.java
-        ) as DisplayManager
-        val virtualDisplays = mutableListOf<VirtualDisplay>()
-        for (i in 0 until count) {
-            virtualDisplays.add(displayManager.createVirtualDisplay(
-                    /* displayName= */ "testVirtualDisplay$i",
-                    /* width= */ 100,
-                    /* height= */ 100,
-                    /* densityDpi= */ 100,
-                    /* surface= */ null,
-                    /* flags= */ 0
-            ))
-        }
-        return virtualDisplays
-    }
-
-    // Helper function that creates a KeyEvent with Keycode A with the given action
-    private fun createKeycodeAEvent(inputDevice: InputDevice, action: Int): KeyEvent {
-        val eventTime = SystemClock.uptimeMillis()
-        return KeyEvent(
-                /* downTime= */ eventTime,
-                /* eventTime= */ eventTime,
-                /* action= */ action,
-                /* code= */ KeyEvent.KEYCODE_A,
-                /* repeat= */ 0,
-                /* metaState= */ 0,
-                /* deviceId= */ inputDevice.id,
-                /* scanCode= */ 0,
-                /* flags= */ KeyEvent.FLAG_FROM_SYSTEM,
-                /* source= */ InputDevice.SOURCE_KEYBOARD
-        )
-    }
-
-    private fun createInputDevice(): InputDevice {
-        return InputDevice.Builder()
-                .setId(123)
-                .setName("abc")
-                .setDescriptor("def")
-                .setSources(InputDevice.SOURCE_KEYBOARD)
-                .build()
-    }
-
-    @Test
-    fun addUniqueIdAssociationByDescriptor_verifyAssociations() {
-        // Overall goal is to have 2 displays and verify that events from the InputDevice are
-        // sent only to the view that is on the associated display.
-        // So, associate the InputDevice with display 1, then send and verify KeyEvents.
-        // Then remove associations, then associate the InputDevice with display 2, then send
-        // and verify commands.
-
-        // Make 2 virtual displays with some mock SurfaceViews
-        val mockSurfaceView1 = mock(SurfaceView::class.java)
-        val mockSurfaceView2 = mock(SurfaceView::class.java)
-        val mockSurfaceHolder1 = mock(SurfaceHolder::class.java)
-        `when`(mockSurfaceView1.holder).thenReturn(mockSurfaceHolder1)
-        val mockSurfaceHolder2 = mock(SurfaceHolder::class.java)
-        `when`(mockSurfaceView2.holder).thenReturn(mockSurfaceHolder2)
-
-        val virtualDisplays = createVirtualDisplays(2)
-
-        // Simulate an InputDevice
-        val inputDevice = createInputDevice()
-
-        // Associate input device with display
-        service.addUniqueIdAssociationByDescriptor(
-                inputDevice.descriptor,
-                virtualDisplays[0].display.displayId.toString()
-        )
-
-        // Simulate 2 different KeyEvents
-        val downEvent = createKeycodeAEvent(inputDevice, KeyEvent.ACTION_DOWN)
-        val upEvent = createKeycodeAEvent(inputDevice, KeyEvent.ACTION_UP)
-
-        // Create a mock OnKeyListener object
-        val mockOnKeyListener = mock(OnKeyListener::class.java)
-
-        // Verify that the event went to Display 1 not Display 2
-        service.injectInputEvent(downEvent, InputEventInjectionSync.NONE)
-
-        // Call the onKey method on the mock OnKeyListener object
-        mockOnKeyListener.onKey(mockSurfaceView1, /* keyCode= */ KeyEvent.KEYCODE_A, downEvent)
-        mockOnKeyListener.onKey(mockSurfaceView2, /* keyCode= */ KeyEvent.KEYCODE_A, upEvent)
-
-        // Verify that the onKey method was called with the expected arguments
-        verify(mockOnKeyListener).onKey(mockSurfaceView1, KeyEvent.KEYCODE_A, downEvent)
-        verify(mockOnKeyListener, never()).onKey(mockSurfaceView2, KeyEvent.KEYCODE_A, downEvent)
-
-        // Remove association
-        service.removeUniqueIdAssociationByDescriptor(inputDevice.descriptor)
-
-        // Associate with Display 2
-        service.addUniqueIdAssociationByDescriptor(
-                inputDevice.descriptor,
-                virtualDisplays[1].display.displayId.toString()
-        )
-
-        // Simulate a KeyEvent
-        service.injectInputEvent(upEvent, InputEventInjectionSync.NONE)
-
-        // Verify that the event went to Display 2 not Display 1
-        verify(mockOnKeyListener).onKey(mockSurfaceView2, KeyEvent.KEYCODE_A, upEvent)
-        verify(mockOnKeyListener, never()).onKey(mockSurfaceView1, KeyEvent.KEYCODE_A, upEvent)
-    }
-
-    @Test
-    fun addUniqueIdAssociationByPort_verifyAssociations() {
-        // Overall goal is to have 2 displays and verify that events from the InputDevice are
-        // sent only to the view that is on the associated display.
-        // So, associate the InputDevice with display 1, then send and verify KeyEvents.
-        // Then remove associations, then associate the InputDevice with display 2, then send
-        // and verify commands.
-
-        // Make 2 virtual displays with some mock SurfaceViews
-        val mockSurfaceView1 = mock(SurfaceView::class.java)
-        val mockSurfaceView2 = mock(SurfaceView::class.java)
-        val mockSurfaceHolder1 = mock(SurfaceHolder::class.java)
-        `when`(mockSurfaceView1.holder).thenReturn(mockSurfaceHolder1)
-        val mockSurfaceHolder2 = mock(SurfaceHolder::class.java)
-        `when`(mockSurfaceView2.holder).thenReturn(mockSurfaceHolder2)
-
-        val virtualDisplays = createVirtualDisplays(2)
-
-        // Simulate an InputDevice
-        val inputDevice = createInputDevice()
-
-        // Associate input device with display
-        service.addUniqueIdAssociationByPort(
-                inputDevice.name,
-                virtualDisplays[0].display.displayId.toString()
-        )
-
-        // Simulate 2 different KeyEvents
-        val downEvent = createKeycodeAEvent(inputDevice, KeyEvent.ACTION_DOWN)
-        val upEvent = createKeycodeAEvent(inputDevice, KeyEvent.ACTION_UP)
-
-        // Create a mock OnKeyListener object
-        val mockOnKeyListener = mock(OnKeyListener::class.java)
-
-        // Verify that the event went to Display 1 not Display 2
-        service.injectInputEvent(downEvent, InputEventInjectionSync.NONE)
-
-        // Call the onKey method on the mock OnKeyListener object
-        mockOnKeyListener.onKey(mockSurfaceView1, /* keyCode= */ KeyEvent.KEYCODE_A, downEvent)
-        mockOnKeyListener.onKey(mockSurfaceView2, /* keyCode= */ KeyEvent.KEYCODE_A, upEvent)
-
-        // Verify that the onKey method was called with the expected arguments
-        verify(mockOnKeyListener).onKey(mockSurfaceView1, KeyEvent.KEYCODE_A, downEvent)
-        verify(mockOnKeyListener, never()).onKey(mockSurfaceView2, KeyEvent.KEYCODE_A, downEvent)
-
-        // Remove association
-        service.removeUniqueIdAssociationByPort(inputDevice.name)
-
-        // Associate with Display 2
-        service.addUniqueIdAssociationByPort(
-                inputDevice.name,
-                virtualDisplays[1].display.displayId.toString()
-        )
-
-        // Simulate a KeyEvent
-        service.injectInputEvent(upEvent, InputEventInjectionSync.NONE)
-
-        // Verify that the event went to Display 2 not Display 1
-        verify(mockOnKeyListener).onKey(mockSurfaceView2, KeyEvent.KEYCODE_A, upEvent)
-        verify(mockOnKeyListener, never()).onKey(mockSurfaceView1, KeyEvent.KEYCODE_A, upEvent)
-    }
 }
 
 private fun <T> whenever(methodCall: T): OngoingStubbing<T> = `when`(methodCall)