Merge "Consider enforce property for desktop mode" into main
diff --git a/Android.bp b/Android.bp
index 127556f..e7c2041 100644
--- a/Android.bp
+++ b/Android.bp
@@ -427,6 +427,7 @@
         "framework-permission-aidl-java",
         "spatializer-aidl-java",
         "audiopolicy-aidl-java",
+        "volumegroupcallback-aidl-java",
         "sounddose-aidl-java",
         "modules-utils-expresslog",
         "perfetto_trace_javastream_protos_jarjar",
diff --git a/boot/boot-image-profile.txt b/boot/boot-image-profile.txt
index d7c409f..4c3e5dc 100644
--- a/boot/boot-image-profile.txt
+++ b/boot/boot-image-profile.txt
@@ -28829,7 +28829,6 @@
 Landroid/media/audiopolicy/AudioProductStrategy;
 Landroid/media/audiopolicy/AudioVolumeGroup$1;
 Landroid/media/audiopolicy/AudioVolumeGroup;
-Landroid/media/audiopolicy/AudioVolumeGroupChangeHandler;
 Landroid/media/audiopolicy/IAudioPolicyCallback$Stub$Proxy;
 Landroid/media/audiopolicy/IAudioPolicyCallback$Stub;
 Landroid/media/audiopolicy/IAudioPolicyCallback;
diff --git a/boot/preloaded-classes b/boot/preloaded-classes
index 7f4b324..0486877 100644
--- a/boot/preloaded-classes
+++ b/boot/preloaded-classes
@@ -5509,7 +5509,6 @@
 android.media.audiopolicy.AudioProductStrategy
 android.media.audiopolicy.AudioVolumeGroup$1
 android.media.audiopolicy.AudioVolumeGroup
-android.media.audiopolicy.AudioVolumeGroupChangeHandler
 android.media.audiopolicy.IAudioPolicyCallback$Stub$Proxy
 android.media.audiopolicy.IAudioPolicyCallback$Stub
 android.media.audiopolicy.IAudioPolicyCallback
diff --git a/config/preloaded-classes b/config/preloaded-classes
index 707acb0..3f0e00b 100644
--- a/config/preloaded-classes
+++ b/config/preloaded-classes
@@ -5514,7 +5514,6 @@
 android.media.audiopolicy.AudioProductStrategy
 android.media.audiopolicy.AudioVolumeGroup$1
 android.media.audiopolicy.AudioVolumeGroup
-android.media.audiopolicy.AudioVolumeGroupChangeHandler
 android.media.audiopolicy.IAudioPolicyCallback$Stub$Proxy
 android.media.audiopolicy.IAudioPolicyCallback$Stub
 android.media.audiopolicy.IAudioPolicyCallback
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 2ce3609..95b9b49 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -2940,6 +2940,13 @@
 
 package android.app.supervision {
 
+  @FlaggedApi("android.app.supervision.flags.enable_supervision_app_service") public class SupervisionAppService extends android.app.Service {
+    ctor public SupervisionAppService();
+    method @Nullable public final android.os.IBinder onBind(@Nullable android.content.Intent);
+    method @FlaggedApi("android.app.supervision.flags.enable_supervision_app_service") public void onDisabled();
+    method @FlaggedApi("android.app.supervision.flags.enable_supervision_app_service") public void onEnabled();
+  }
+
   @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") public class SupervisionManager {
     method @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.QUERY_USERS}) public android.content.Intent createConfirmSupervisionCredentialsIntent();
     method @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.QUERY_USERS}) public boolean isSupervisionEnabled();
@@ -7435,7 +7442,7 @@
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void muteAwaitConnection(@NonNull int[], @NonNull android.media.AudioDeviceAttributes, long, @NonNull java.util.concurrent.TimeUnit) throws java.lang.IllegalStateException;
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int registerAudioPolicy(@NonNull android.media.audiopolicy.AudioPolicy);
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void registerMuteAwaitConnectionCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.MuteAwaitConnectionCallback);
-    method public void registerVolumeGroupCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.VolumeGroupCallback);
+    method @FlaggedApi("android.media.audio.register_volume_callback_api_hardening") @RequiresPermission("Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED") public void registerVolumeGroupCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.VolumeGroupCallback);
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeAssistantServicesUids(@NonNull int[]);
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean removeDeviceAsNonDefaultForStrategy(@NonNull android.media.audiopolicy.AudioProductStrategy, @NonNull android.media.AudioDeviceAttributes);
     method @RequiresPermission(anyOf={android.Manifest.permission.MODIFY_AUDIO_ROUTING, "android.permission.QUERY_AUDIO_STATE"}) public void removeOnDevicesForAttributesChangedListener(@NonNull android.media.AudioManager.OnDevicesForAttributesChangedListener);
@@ -7466,7 +7473,7 @@
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void unregisterAudioPolicy(@NonNull android.media.audiopolicy.AudioPolicy);
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void unregisterAudioPolicyAsync(@NonNull android.media.audiopolicy.AudioPolicy);
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void unregisterMuteAwaitConnectionCallback(@NonNull android.media.AudioManager.MuteAwaitConnectionCallback);
-    method public void unregisterVolumeGroupCallback(@NonNull android.media.AudioManager.VolumeGroupCallback);
+    method @FlaggedApi("android.media.audio.register_volume_callback_api_hardening") @RequiresPermission("Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED") public void unregisterVolumeGroupCallback(@NonNull android.media.AudioManager.VolumeGroupCallback);
     field public static final String ACTION_VOLUME_CHANGED = "android.media.VOLUME_CHANGED_ACTION";
     field public static final int AUDIOFOCUS_FLAG_DELAY_OK = 1; // 0x1
     field public static final int AUDIOFOCUS_FLAG_LOCK = 4; // 0x4
diff --git a/core/java/android/app/StatusBarManager.java b/core/java/android/app/StatusBarManager.java
index 01868cc6..927d469 100644
--- a/core/java/android/app/StatusBarManager.java
+++ b/core/java/android/app/StatusBarManager.java
@@ -58,8 +58,10 @@
 import com.android.internal.statusbar.IUndoMediaTransferCallback;
 import com.android.internal.statusbar.NotificationVisibility;
 
+import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -119,6 +121,7 @@
             | DISABLE_SEARCH | DISABLE_ONGOING_CALL_CHIP;
 
     /** @hide */
+    @Target(ElementType.TYPE_USE)
     @IntDef(flag = true, prefix = {"DISABLE_"}, value = {
             DISABLE_NONE,
             DISABLE_EXPAND,
@@ -161,6 +164,7 @@
             | DISABLE2_NOTIFICATION_SHADE | DISABLE2_GLOBAL_ACTIONS | DISABLE2_ROTATE_SUGGESTIONS;
 
     /** @hide */
+    @Target(ElementType.TYPE_USE)
     @IntDef(flag = true, prefix = { "DISABLE2_" }, value = {
             DISABLE2_NONE,
             DISABLE2_MASK,
diff --git a/core/java/android/app/supervision/SupervisionAppService.java b/core/java/android/app/supervision/SupervisionAppService.java
index 4530be5..93eb962 100644
--- a/core/java/android/app/supervision/SupervisionAppService.java
+++ b/core/java/android/app/supervision/SupervisionAppService.java
@@ -16,7 +16,11 @@
 
 package android.app.supervision;
 
+import android.annotation.FlaggedApi;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
 import android.app.Service;
+import android.app.supervision.flags.Flags;
 import android.content.Intent;
 import android.os.IBinder;
 
@@ -26,31 +30,43 @@
  *
  * @hide
  */
+@SystemApi
+@FlaggedApi(Flags.FLAG_ENABLE_SUPERVISION_APP_SERVICE)
 public class SupervisionAppService extends Service {
-    private final ISupervisionAppService mBinder = new ISupervisionAppService.Stub() {
-        @Override
-        public void onEnabled() {
-            SupervisionAppService.this.onEnabled();
-        }
+    private final ISupervisionAppService mBinder =
+            new ISupervisionAppService.Stub() {
+                @Override
+                public void onEnabled() {
+                    SupervisionAppService.this.onEnabled();
+                }
 
-        @Override
-        public void onDisabled() {
-            SupervisionAppService.this.onDisabled();
-        }
-    };
+                @Override
+                public void onDisabled() {
+                    SupervisionAppService.this.onDisabled();
+                }
+            };
 
+    @Nullable
     @Override
-    public final IBinder onBind(Intent intent) {
+    public final IBinder onBind(@Nullable Intent intent) {
         return mBinder.asBinder();
     }
 
     /**
      * Called when supervision is enabled.
+     *
+     * @hide
      */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_ENABLE_SUPERVISION_APP_SERVICE)
     public void onEnabled() {}
 
     /**
      * Called when supervision is disabled.
+     *
+     * @hide
      */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_ENABLE_SUPERVISION_APP_SERVICE)
     public void onDisabled() {}
 }
diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig
index 615a6df..161f05b 100644
--- a/core/java/android/companion/virtual/flags/flags.aconfig
+++ b/core/java/android/companion/virtual/flags/flags.aconfig
@@ -166,3 +166,10 @@
     bug: "393517834"
     is_exported: true
 }
+
+flag {
+    name: "external_virtual_cameras"
+    namespace: "virtual_devices"
+    description: "Allow external virtual cameras visible only in the Context of the virtual device"
+    bug: "375609768"
+}
diff --git a/core/java/android/content/pm/RegisteredServicesCache.java b/core/java/android/content/pm/RegisteredServicesCache.java
index ded35b2..1ddab2c 100644
--- a/core/java/android/content/pm/RegisteredServicesCache.java
+++ b/core/java/android/content/pm/RegisteredServicesCache.java
@@ -104,14 +104,6 @@
 
     private final Handler mBackgroundHandler;
 
-    private final Runnable mClearServiceInfoCachesRunnable = new Runnable() {
-        public void run() {
-            synchronized (mUserIdToServiceInfoCaches) {
-                mUserIdToServiceInfoCaches.clear();
-            }
-        }
-    };
-
     private static class UserServices<V> {
         @GuardedBy("mServicesLock")
         final Map<V, Integer> persistentServices = Maps.newHashMap();
@@ -565,9 +557,11 @@
 
         if (Flags.optimizeParsingInRegisteredServicesCache()) {
             synchronized (mUserIdToServiceInfoCaches) {
-                if (mUserIdToServiceInfoCaches.numMaps() > 0) {
-                    mBackgroundHandler.removeCallbacks(mClearServiceInfoCachesRunnable);
-                    mBackgroundHandler.postDelayed(mClearServiceInfoCachesRunnable,
+                if (mUserIdToServiceInfoCaches.numElementsForKey(userId) > 0) {
+                    final Integer token = Integer.valueOf(userId);
+                    mBackgroundHandler.removeCallbacksAndEqualMessages(token);
+                    mBackgroundHandler.postDelayed(
+                            new ClearServiceInfoCachesTimeoutRunnable(userId), token,
                             SERVICE_INFO_CACHES_TIMEOUT_MILLIS);
                 }
             }
@@ -953,4 +947,19 @@
             return BackgroundThread.getHandler();
         }
     }
+
+    class ClearServiceInfoCachesTimeoutRunnable implements Runnable {
+        final int mUserId;
+
+        ClearServiceInfoCachesTimeoutRunnable(int userId) {
+            this.mUserId = userId;
+        }
+
+        @Override
+        public void run() {
+            synchronized (mUserIdToServiceInfoCaches) {
+                mUserIdToServiceInfoCaches.delete(mUserId);
+            }
+        }
+    }
 }
diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl
index 1c2150f..5537135 100644
--- a/core/java/android/hardware/input/IInputManager.aidl
+++ b/core/java/android/hardware/input/IInputManager.aidl
@@ -273,7 +273,7 @@
     @PermissionManuallyEnforced
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
             + "android.Manifest.permission.MANAGE_KEY_GESTURES)")
-    void registerKeyGestureHandler(IKeyGestureHandler handler);
+    void registerKeyGestureHandler(in int[] keyGesturesToHandle, IKeyGestureHandler handler);
 
     @PermissionManuallyEnforced
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
diff --git a/core/java/android/hardware/input/IKeyGestureHandler.aidl b/core/java/android/hardware/input/IKeyGestureHandler.aidl
index 4da991e..08b0158 100644
--- a/core/java/android/hardware/input/IKeyGestureHandler.aidl
+++ b/core/java/android/hardware/input/IKeyGestureHandler.aidl
@@ -20,12 +20,12 @@
 import android.os.IBinder;
 
 /** @hide */
-interface IKeyGestureHandler {
+oneway interface IKeyGestureHandler {
 
     /**
-     * Called when a key gesture starts, ends, or is cancelled. If a handler returns {@code true},
-     * it means they intend to handle the full gesture and should handle all the events pertaining
-     * to that gesture.
+     * Called when a key gesture starts, ends, or is cancelled. It is only sent to the handler that
+     * registered the callback for that particular gesture type.
+     * {@see IInputManager#registerKeyGestureHandler(int[], IKeyGestureHandler)}
      */
-    boolean handleKeyGesture(in AidlKeyGestureEvent event, in IBinder focusedToken);
+    void handleKeyGesture(in AidlKeyGestureEvent event, in IBinder focusedToken);
 }
diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java
index d6419af..a66ac76 100644
--- a/core/java/android/hardware/input/InputManager.java
+++ b/core/java/android/hardware/input/InputManager.java
@@ -1446,16 +1446,18 @@
     /**
      * Registers a key gesture event handler for {@link KeyGestureEvent} handling.
      *
+     * @param keyGesturesToHandle list of KeyGestureTypes to listen to
      * @param handler the {@link KeyGestureEventHandler}
-     * @throws IllegalArgumentException if {@code handler} has already been registered previously.
+     * @throws IllegalArgumentException if {@code handler} has already been registered previously
+     * or key gestures provided are already registered by some other gesture handler.
      * @throws NullPointerException     if {@code handler} or {@code executor} is null.
      * @hide
      * @see #unregisterKeyGestureEventHandler(KeyGestureEventHandler)
      */
     @RequiresPermission(Manifest.permission.MANAGE_KEY_GESTURES)
-    public void registerKeyGestureEventHandler(@NonNull KeyGestureEventHandler handler)
-            throws IllegalArgumentException {
-        mGlobal.registerKeyGestureEventHandler(handler);
+    public void registerKeyGestureEventHandler(List<Integer> keyGesturesToHandle,
+            @NonNull KeyGestureEventHandler handler) throws IllegalArgumentException {
+        mGlobal.registerKeyGestureEventHandler(keyGesturesToHandle, handler);
     }
 
     /**
@@ -1463,7 +1465,7 @@
      *
      * @param handler the {@link KeyGestureEventHandler}
      * @hide
-     * @see #registerKeyGestureEventHandler(KeyGestureEventHandler)
+     * @see #registerKeyGestureEventHandler(List, KeyGestureEventHandler)
      */
     @RequiresPermission(Manifest.permission.MANAGE_KEY_GESTURES)
     public void unregisterKeyGestureEventHandler(@NonNull KeyGestureEventHandler handler) {
@@ -1741,7 +1743,7 @@
      * {@see KeyGestureEventListener} which is to listen to successfully handled key gestures, this
      * interface allows system components to register handler for handling key gestures.
      *
-     * @see #registerKeyGestureEventHandler(KeyGestureEventHandler)
+     * @see #registerKeyGestureEventHandler(List, KeyGestureEventHandler)
      * @see #unregisterKeyGestureEventHandler(KeyGestureEventHandler)
      *
      * <p> NOTE: All callbacks will occur on system main and input threads, so the caller needs
@@ -1750,14 +1752,11 @@
      */
     public interface KeyGestureEventHandler {
         /**
-         * Called when a key gesture event starts, is completed, or is cancelled. If a handler
-         * returns {@code true}, it implies that the handler intends to handle the key gesture and
-         * only this handler will receive the future events for this key gesture.
+         * Called when a key gesture event starts, is completed, or is cancelled.
          *
          * @param event the gesture event
          */
-        boolean handleKeyGestureEvent(@NonNull KeyGestureEvent event,
-                @Nullable IBinder focusedToken);
+        void handleKeyGestureEvent(@NonNull KeyGestureEvent event, @Nullable IBinder focusedToken);
     }
 
     /** @hide */
diff --git a/core/java/android/hardware/input/InputManagerGlobal.java b/core/java/android/hardware/input/InputManagerGlobal.java
index c4b4831..754182c 100644
--- a/core/java/android/hardware/input/InputManagerGlobal.java
+++ b/core/java/android/hardware/input/InputManagerGlobal.java
@@ -25,8 +25,8 @@
 import android.hardware.SensorManager;
 import android.hardware.input.InputManager.InputDeviceBatteryListener;
 import android.hardware.input.InputManager.InputDeviceListener;
-import android.hardware.input.InputManager.KeyGestureEventHandler;
 import android.hardware.input.InputManager.KeyEventActivityListener;
+import android.hardware.input.InputManager.KeyGestureEventHandler;
 import android.hardware.input.InputManager.KeyGestureEventListener;
 import android.hardware.input.InputManager.KeyboardBacklightListener;
 import android.hardware.input.InputManager.OnTabletModeChangedListener;
@@ -49,6 +49,7 @@
 import android.os.VibrationEffect;
 import android.os.Vibrator;
 import android.os.VibratorManager;
+import android.util.IntArray;
 import android.util.Log;
 import android.util.SparseArray;
 import android.view.Display;
@@ -132,13 +133,13 @@
     @Nullable
     private IKeyEventActivityListener mKeyEventActivityListener;
 
-    private final Object mKeyGestureEventHandlerLock = new Object();
-    @GuardedBy("mKeyGestureEventHandlerLock")
-    @Nullable
-    private ArrayList<KeyGestureEventHandler> mKeyGestureEventHandlers;
-    @GuardedBy("mKeyGestureEventHandlerLock")
+    @GuardedBy("mKeyGesturesToHandlerMap")
     @Nullable
     private IKeyGestureHandler mKeyGestureHandler;
+    @GuardedBy("mKeyGesturesToHandlerMap")
+    private final SparseArray<KeyGestureEventHandler> mKeyGesturesToHandlerMap =
+            new SparseArray<>();
+
 
     // InputDeviceSensorManager gets notified synchronously from the binder thread when input
     // devices change, so it must be synchronized with the input device listeners.
@@ -1177,50 +1178,69 @@
 
     private class LocalKeyGestureHandler extends IKeyGestureHandler.Stub {
         @Override
-        public boolean handleKeyGesture(@NonNull AidlKeyGestureEvent ev, IBinder focusedToken) {
-            synchronized (mKeyGestureEventHandlerLock) {
-                if (mKeyGestureEventHandlers == null) {
-                    return false;
+        public void handleKeyGesture(@NonNull AidlKeyGestureEvent ev, IBinder focusedToken) {
+            synchronized (mKeyGesturesToHandlerMap) {
+                KeyGestureEventHandler handler = mKeyGesturesToHandlerMap.get(ev.gestureType);
+                if (handler == null) {
+                    Log.w(TAG, "Key gesture event " + ev.gestureType
+                            + " occurred without a registered handler!");
+                    return;
                 }
-                final int numHandlers = mKeyGestureEventHandlers.size();
-                final KeyGestureEvent event = new KeyGestureEvent(ev);
-                for (int i = 0; i < numHandlers; i++) {
-                    KeyGestureEventHandler handler = mKeyGestureEventHandlers.get(i);
-                    if (handler.handleKeyGestureEvent(event, focusedToken)) {
-                        return true;
-                    }
-                }
+                handler.handleKeyGestureEvent(new KeyGestureEvent(ev), focusedToken);
             }
-            return false;
         }
     }
 
     /**
-     * @see InputManager#registerKeyGestureEventHandler(KeyGestureEventHandler)
+     * @see InputManager#registerKeyGestureEventHandler(List, KeyGestureEventHandler)
      */
     @RequiresPermission(Manifest.permission.MANAGE_KEY_GESTURES)
-    void registerKeyGestureEventHandler(@NonNull KeyGestureEventHandler handler)
-            throws IllegalArgumentException {
+    void registerKeyGestureEventHandler(List<Integer> keyGesturesToHandle,
+            @NonNull KeyGestureEventHandler handler) throws IllegalArgumentException {
+        Objects.requireNonNull(keyGesturesToHandle, "List of gestures should not be null");
         Objects.requireNonNull(handler, "handler should not be null");
 
-        synchronized (mKeyGestureEventHandlerLock) {
-            if (mKeyGestureHandler == null) {
-                mKeyGestureEventHandlers = new ArrayList<>();
-                mKeyGestureHandler = new LocalKeyGestureHandler();
+        if (keyGesturesToHandle.isEmpty()) {
+            throw new IllegalArgumentException("No key gestures provided!");
+        }
 
-                try {
-                    mIm.registerKeyGestureHandler(mKeyGestureHandler);
-                } catch (RemoteException e) {
-                    throw e.rethrowFromSystemServer();
-                }
-            }
-            final int numHandlers = mKeyGestureEventHandlers.size();
-            for (int i = 0; i < numHandlers; i++) {
-                if (mKeyGestureEventHandlers.get(i) == handler) {
+        synchronized (mKeyGesturesToHandlerMap) {
+            IntArray newKeyGestures = new IntArray(
+                    keyGesturesToHandle.size() + mKeyGesturesToHandlerMap.size());
+
+            // Check if the handler already exists
+            for (int i = 0; i < mKeyGesturesToHandlerMap.size(); i++) {
+                KeyGestureEventHandler h = mKeyGesturesToHandlerMap.valueAt(i);
+                if (h == handler) {
                     throw new IllegalArgumentException("Handler has already been registered!");
                 }
+                newKeyGestures.add(mKeyGesturesToHandlerMap.keyAt(i));
             }
-            mKeyGestureEventHandlers.add(handler);
+
+            // Check if any of the key gestures are already handled by existing handlers
+            for (int gesture : keyGesturesToHandle) {
+                if (mKeyGesturesToHandlerMap.contains(gesture)) {
+                    throw new IllegalArgumentException("Key gesture " + gesture
+                            + " is already registered by another handler!");
+                }
+                newKeyGestures.add(gesture);
+            }
+
+            try {
+                // If handler was already registered for this process, we need to unregister and
+                // re-register it for the new set of gestures
+                if (mKeyGestureHandler != null) {
+                    mIm.unregisterKeyGestureHandler(mKeyGestureHandler);
+                } else {
+                    mKeyGestureHandler = new LocalKeyGestureHandler();
+                }
+                mIm.registerKeyGestureHandler(newKeyGestures.toArray(), mKeyGestureHandler);
+                for (int gesture : keyGesturesToHandle) {
+                    mKeyGesturesToHandlerMap.put(gesture, handler);
+                }
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
         }
     }
 
@@ -1231,18 +1251,21 @@
     void unregisterKeyGestureEventHandler(@NonNull KeyGestureEventHandler handler) {
         Objects.requireNonNull(handler, "handler should not be null");
 
-        synchronized (mKeyGestureEventHandlerLock) {
-            if (mKeyGestureEventHandlers == null) {
+        synchronized (mKeyGesturesToHandlerMap) {
+            if (mKeyGestureHandler == null) {
                 return;
             }
-            mKeyGestureEventHandlers.removeIf(existingHandler -> existingHandler == handler);
-            if (mKeyGestureEventHandlers.isEmpty()) {
+            for (int i = mKeyGesturesToHandlerMap.size() - 1; i >= 0; i--) {
+                if (mKeyGesturesToHandlerMap.valueAt(i) == handler) {
+                    mKeyGesturesToHandlerMap.removeAt(i);
+                }
+            }
+            if (mKeyGesturesToHandlerMap.size() == 0) {
                 try {
                     mIm.unregisterKeyGestureHandler(mKeyGestureHandler);
                 } catch (RemoteException e) {
                     throw e.rethrowFromSystemServer();
                 }
-                mKeyGestureEventHandlers = null;
                 mKeyGestureHandler = null;
             }
         }
diff --git a/core/java/android/util/ArrayMap.java b/core/java/android/util/ArrayMap.java
index 7ee0ff1..c599079 100644
--- a/core/java/android/util/ArrayMap.java
+++ b/core/java/android/util/ArrayMap.java
@@ -129,7 +129,7 @@
             return ContainerHelpers.binarySearch(hashes, N, hash);
         } catch (ArrayIndexOutOfBoundsException e) {
             if (CONCURRENT_MODIFICATION_EXCEPTIONS) {
-                throw new ConcurrentModificationException();
+                throw new ConcurrentModificationException(e);
             } else {
                 throw e; // the cache is poisoned at this point, there's not much we can do
             }
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index 7e9dfe6..4c578fb 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -2039,8 +2039,8 @@
         } else if (Flags.refactorInsetsController()) {
             if ((typesToReport & ime()) != 0 && mImeSourceConsumer != null) {
                 InsetsSourceControl control = mImeSourceConsumer.getControl();
-                if (control != null && control.getLeash() == null) {
-                    // If the IME was requested twice, and we didn't receive the controls
+                if (control == null || control.getLeash() == null) {
+                    // If the IME was requested to show twice, and we didn't receive the controls
                     // yet, this request will not continue. It should be cancelled here, as
                     // it would time out otherwise.
                     ImeTracker.forLogging().onCancelled(statsToken,
diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java
index 20792c9..4bd0d97 100644
--- a/core/java/android/window/DesktopModeFlags.java
+++ b/core/java/android/window/DesktopModeFlags.java
@@ -44,7 +44,7 @@
     // All desktop mode related flags to be overridden by developer option toggle will be added here
     // go/keep-sorted start
     DISABLE_DESKTOP_LAUNCH_PARAMS_OUTSIDE_DESKTOP_BUG_FIX(
-            Flags::disableDesktopLaunchParamsOutsideDesktopBugFix, false),
+            Flags::disableDesktopLaunchParamsOutsideDesktopBugFix, true),
     DISABLE_NON_RESIZABLE_APP_SNAP_RESIZE(Flags::disableNonResizableAppSnapResizing, true),
     ENABLE_ACCESSIBLE_CUSTOM_HEADERS(Flags::enableAccessibleCustomHeaders, true),
     ENABLE_APP_HEADER_WITH_TASK_DENSITY(Flags::enableAppHeaderWithTaskDensity, true),
@@ -103,7 +103,7 @@
     ENABLE_DESKTOP_WINDOWING_TASK_LIMIT(Flags::enableDesktopWindowingTaskLimit, true),
     ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY(Flags::enableDesktopWindowingWallpaperActivity,
             true),
-    ENABLE_DRAG_RESIZE_SET_UP_IN_BG_THREAD(Flags::enableDragResizeSetUpInBgThread, false),
+    ENABLE_DRAG_RESIZE_SET_UP_IN_BG_THREAD(Flags::enableDragResizeSetUpInBgThread, true),
     ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX(
             Flags::enableDragToDesktopIncomingTransitionsBugfix, false),
     ENABLE_FULLY_IMMERSIVE_IN_DESKTOP(Flags::enableFullyImmersiveInDesktop, true),
@@ -120,7 +120,7 @@
     ENABLE_RESTORE_TO_PREVIOUS_SIZE_FROM_DESKTOP_IMMERSIVE(
             Flags::enableRestoreToPreviousSizeFromDesktopImmersive, true),
     ENABLE_SHELL_INITIAL_BOUNDS_REGRESSION_BUG_FIX(
-            Flags::enableShellInitialBoundsRegressionBugFix, false),
+            Flags::enableShellInitialBoundsRegressionBugFix, true),
     ENABLE_START_LAUNCH_TRANSITION_FROM_TASKBAR_BUGFIX(
             Flags::enableStartLaunchTransitionFromTaskbarBugfix, true),
     ENABLE_TASKBAR_OVERFLOW(Flags::enableTaskbarOverflow, false),
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index 648335c..0d87b73 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -996,3 +996,13 @@
     description: "Enables the home to be shown behind the desktop."
     bug: "375644149"
 }
+
+flag {
+    name: "enable_desktop_ime_bugfix"
+    namespace: "lse_desktop_experience"
+    description: "Enables bugfix to handle IME interactions in desktop windowing."
+    bug: "388570293"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/core/java/com/android/internal/statusbar/DisableStates.aidl b/core/java/com/android/internal/statusbar/DisableStates.aidl
new file mode 100644
index 0000000..fd9882f
--- /dev/null
+++ b/core/java/com/android/internal/statusbar/DisableStates.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.statusbar;
+
+parcelable DisableStates;
diff --git a/core/java/com/android/internal/statusbar/DisableStates.java b/core/java/com/android/internal/statusbar/DisableStates.java
new file mode 100644
index 0000000..ca2fd6c0
--- /dev/null
+++ b/core/java/com/android/internal/statusbar/DisableStates.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.statusbar;
+
+import android.app.StatusBarManager.Disable2Flags;
+import android.app.StatusBarManager.DisableFlags;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Pair;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Holds display ids with their disable flags.
+ */
+public class DisableStates implements Parcelable {
+
+    /**
+     * A map of display IDs (integers) with corresponding disable flags.
+     */
+    public Map<Integer, Pair<@DisableFlags Integer, @Disable2Flags Integer>> displaysWithStates;
+
+    /**
+     * Whether the disable state change should be animated.
+     */
+    public boolean animate;
+
+    public DisableStates(
+            Map<Integer, Pair<@DisableFlags Integer, @Disable2Flags Integer>> displaysWithStates,
+            boolean animate) {
+        this.displaysWithStates = displaysWithStates;
+        this.animate = animate;
+    }
+
+    public DisableStates(
+            Map<Integer, Pair<@DisableFlags Integer, @Disable2Flags Integer>> displaysWithStates) {
+        this(displaysWithStates, true);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(displaysWithStates.size()); // Write the size of the map
+        for (Map.Entry<Integer, Pair<Integer, Integer>> entry : displaysWithStates.entrySet()) {
+            dest.writeInt(entry.getKey());
+            dest.writeInt(entry.getValue().first);
+            dest.writeInt(entry.getValue().second);
+        }
+        dest.writeBoolean(animate);
+    }
+
+    /**
+     * Used to make this class parcelable.
+     */
+    public static final Parcelable.Creator<DisableStates> CREATOR = new Parcelable.Creator<>() {
+        @Override
+        public DisableStates createFromParcel(Parcel source) {
+            int size = source.readInt(); // Read the size of the map
+            Map<Integer, Pair<Integer, Integer>> displaysWithStates = new HashMap<>(size);
+            for (int i = 0; i < size; i++) {
+                int key = source.readInt();
+                int first = source.readInt();
+                int second = source.readInt();
+                displaysWithStates.put(key, new Pair<>(first, second));
+            }
+            final boolean animate = source.readBoolean();
+            return new DisableStates(displaysWithStates, animate);
+        }
+
+        @Override
+        public DisableStates[] newArray(int size) {
+            return new DisableStates[size];
+        }
+    };
+}
+
diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl
index 5a180d7..ce9b036 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -32,6 +32,7 @@
 import android.view.KeyEvent;
 import android.service.notification.StatusBarNotification;
 
+import com.android.internal.statusbar.DisableStates;
 import com.android.internal.statusbar.IAddTileResultCallback;
 import com.android.internal.statusbar.IUndoMediaTransferCallback;
 import com.android.internal.statusbar.LetterboxDetails;
@@ -44,6 +45,7 @@
     void setIcon(String slot, in StatusBarIcon icon);
     void removeIcon(String slot);
     void disable(int displayId, int state1, int state2);
+    void disableForAllDisplays(in DisableStates disableStates);
     void animateExpandNotificationsPanel();
     void animateExpandSettingsPanel(String subPanel);
     void animateCollapsePanels();
diff --git a/core/jni/Android.bp b/core/jni/Android.bp
index bfa0aa9..7ed73d7 100644
--- a/core/jni/Android.bp
+++ b/core/jni/Android.bp
@@ -210,7 +210,6 @@
                 "android_media_AudioAttributes.cpp",
                 "android_media_AudioProductStrategies.cpp",
                 "android_media_AudioVolumeGroups.cpp",
-                "android_media_AudioVolumeGroupCallback.cpp",
                 "android_media_DeviceCallback.cpp",
                 "android_media_MediaMetricsJNI.cpp",
                 "android_media_MicrophoneInfo.cpp",
@@ -311,6 +310,7 @@
                 "audioflinger-aidl-cpp",
                 "audiopolicy-types-aidl-cpp",
                 "spatializer-aidl-cpp",
+                "volumegroupcallback-aidl-cpp",
                 "av-types-aidl-cpp",
                 "android.hardware.camera.device@3.2",
                 "camera_platform_flags_c_lib",
diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp
index b2b8263..1ff0774 100644
--- a/core/jni/AndroidRuntime.cpp
+++ b/core/jni/AndroidRuntime.cpp
@@ -101,7 +101,6 @@
 extern int register_android_media_AudioAttributes(JNIEnv *env);
 extern int register_android_media_AudioProductStrategies(JNIEnv *env);
 extern int register_android_media_AudioVolumeGroups(JNIEnv *env);
-extern int register_android_media_AudioVolumeGroupChangeHandler(JNIEnv *env);
 extern int register_android_media_ImageReader(JNIEnv *env);
 extern int register_android_media_ImageWriter(JNIEnv *env);
 extern int register_android_media_MicrophoneInfo(JNIEnv *env);
@@ -1660,7 +1659,6 @@
         REG_JNI(register_android_media_AudioAttributes),
         REG_JNI(register_android_media_AudioProductStrategies),
         REG_JNI(register_android_media_AudioVolumeGroups),
-        REG_JNI(register_android_media_AudioVolumeGroupChangeHandler),
         REG_JNI(register_android_media_ImageReader),
         REG_JNI(register_android_media_ImageWriter),
         REG_JNI(register_android_media_MediaMetrics),
diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp
index b679688..1bbf811 100644
--- a/core/jni/android_media_AudioSystem.cpp
+++ b/core/jni/android_media_AudioSystem.cpp
@@ -20,16 +20,17 @@
 
 #include <atomic>
 #define LOG_TAG "AudioSystem-JNI"
+#include <android-base/properties.h>
 #include <android/binder_ibinder_jni.h>
 #include <android/binder_libbinder.h>
 #include <android/media/AudioVibratorInfo.h>
+#include <android/media/INativeAudioVolumeGroupCallback.h>
 #include <android/media/INativeSpatializerCallback.h>
 #include <android/media/ISpatializer.h>
 #include <android/media/audio/common/AudioConfigBase.h>
 #include <android_media_audiopolicy.h>
 #include <android_os_Parcel.h>
 #include <audiomanager/AudioManager.h>
-#include <android-base/properties.h>
 #include <binder/IBinder.h>
 #include <jni.h>
 #include <media/AidlConversion.h>
@@ -41,14 +42,14 @@
 #include <nativehelper/ScopedLocalRef.h>
 #include <nativehelper/ScopedPrimitiveArray.h>
 #include <nativehelper/jni_macros.h>
+#include <sys/system_properties.h>
 #include <system/audio.h>
 #include <system/audio_policy.h>
-#include <sys/system_properties.h>
 #include <utils/Log.h>
 
+#include <memory>
 #include <optional>
 #include <sstream>
-#include <memory>
 #include <vector>
 
 #include "android_media_AudioAttributes.h"
@@ -59,8 +60,8 @@
 #include "android_media_AudioFormat.h"
 #include "android_media_AudioMixerAttributes.h"
 #include "android_media_AudioProfile.h"
-#include "android_media_MicrophoneInfo.h"
 #include "android_media_JNIUtils.h"
+#include "android_media_MicrophoneInfo.h"
 #include "android_util_Binder.h"
 #include "core_jni_helpers.h"
 
@@ -3442,6 +3443,21 @@
     }
 }
 
+static int android_media_AudioSystem_registerAudioVolumeGroupCallback(
+        JNIEnv *env, jobject thiz, jobject jIAudioVolumeGroupCallback) {
+    sp<media::INativeAudioVolumeGroupCallback> nIAudioVolumeGroupCallback =
+            interface_cast<media::INativeAudioVolumeGroupCallback>(
+                    ibinderForJavaObject(env, jIAudioVolumeGroupCallback));
+    return AudioSystem::addAudioVolumeGroupCallback(nIAudioVolumeGroupCallback);
+}
+
+static int android_media_AudioSystem_unregisterAudioVolumeGroupCallback(
+        JNIEnv *env, jobject thiz, jobject jIAudioVolumeGroupCallback) {
+    sp<media::INativeAudioVolumeGroupCallback> nIAudioVolumeGroupCallback =
+            interface_cast<media::INativeAudioVolumeGroupCallback>(
+                    ibinderForJavaObject(env, jIAudioVolumeGroupCallback));
+    return AudioSystem::removeAudioVolumeGroupCallback(nIAudioVolumeGroupCallback);
+}
 
 // ----------------------------------------------------------------------------
 
@@ -3612,6 +3628,12 @@
         MAKE_JNI_NATIVE_METHOD("clearPreferredMixerAttributes",
                                "(Landroid/media/AudioAttributes;II)I",
                                android_media_AudioSystem_clearPreferredMixerAttributes),
+        MAKE_JNI_NATIVE_METHOD("registerAudioVolumeGroupCallback",
+                               "(Landroid/media/INativeAudioVolumeGroupCallback;)I",
+                               android_media_AudioSystem_registerAudioVolumeGroupCallback),
+        MAKE_JNI_NATIVE_METHOD("unregisterAudioVolumeGroupCallback",
+                               "(Landroid/media/INativeAudioVolumeGroupCallback;)I",
+                               android_media_AudioSystem_unregisterAudioVolumeGroupCallback),
         MAKE_AUDIO_SYSTEM_METHOD(supportsBluetoothVariableLatency),
         MAKE_AUDIO_SYSTEM_METHOD(setBluetoothVariableLatencyEnabled),
         MAKE_AUDIO_SYSTEM_METHOD(isBluetoothVariableLatencyEnabled),
diff --git a/core/jni/android_media_AudioVolumeGroupCallback.cpp b/core/jni/android_media_AudioVolumeGroupCallback.cpp
deleted file mode 100644
index d130a4b..0000000
--- a/core/jni/android_media_AudioVolumeGroupCallback.cpp
+++ /dev/null
@@ -1,176 +0,0 @@
-/*
- * Copyright (C) 2018 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.
- */
-#undef ANDROID_UTILS_REF_BASE_DISABLE_IMPLICIT_CONSTRUCTION // TODO:remove this and fix code
-
-//#define LOG_NDEBUG 0
-
-#define LOG_TAG "AudioVolumeGroupCallback-JNI"
-
-#include <utils/Log.h>
-#include <nativehelper/JNIHelp.h>
-#include "core_jni_helpers.h"
-
-#include "android_media_AudioVolumeGroupCallback.h"
-
-
-// ----------------------------------------------------------------------------
-using namespace android;
-
-static const char* const kAudioVolumeGroupChangeHandlerClassPathName =
-        "android/media/audiopolicy/AudioVolumeGroupChangeHandler";
-
-static struct {
-    jfieldID    mJniCallback;
-} gAudioVolumeGroupChangeHandlerFields;
-
-static struct {
-    jmethodID    postEventFromNative;
-} gAudioVolumeGroupChangeHandlerMethods;
-
-static Mutex gLock;
-
-JNIAudioVolumeGroupCallback::JNIAudioVolumeGroupCallback(JNIEnv* env,
-                                                         jobject thiz,
-                                                         jobject weak_thiz)
-{
-    jclass clazz = env->GetObjectClass(thiz);
-    if (clazz == NULL) {
-        ALOGE("Can't find class %s", kAudioVolumeGroupChangeHandlerClassPathName);
-        return;
-    }
-    mClass = (jclass)env->NewGlobalRef(clazz);
-
-    // We use a weak reference so the AudioVolumeGroupChangeHandler object can be garbage collected.
-    // The reference is only used as a proxy for callbacks.
-    mObject  = env->NewGlobalRef(weak_thiz);
-}
-
-JNIAudioVolumeGroupCallback::~JNIAudioVolumeGroupCallback()
-{
-    // remove global references
-    JNIEnv *env = AndroidRuntime::getJNIEnv();
-    if (env == NULL) {
-        return;
-    }
-    env->DeleteGlobalRef(mObject);
-    env->DeleteGlobalRef(mClass);
-}
-
-void JNIAudioVolumeGroupCallback::onAudioVolumeGroupChanged(volume_group_t group, int flags)
-{
-    JNIEnv *env = AndroidRuntime::getJNIEnv();
-    if (env == NULL) {
-        return;
-    }
-    ALOGV("%s volume group id %d", __FUNCTION__, group);
-    env->CallStaticVoidMethod(mClass,
-                              gAudioVolumeGroupChangeHandlerMethods.postEventFromNative,
-                              mObject,
-                              AUDIOVOLUMEGROUP_EVENT_VOLUME_CHANGED, group, flags, NULL);
-    if (env->ExceptionCheck()) {
-        ALOGW("An exception occurred while notifying an event.");
-        env->ExceptionClear();
-    }
-}
-
-void JNIAudioVolumeGroupCallback::onServiceDied()
-{
-    JNIEnv *env = AndroidRuntime::getJNIEnv();
-    if (env == NULL) {
-        return;
-    }
-    env->CallStaticVoidMethod(mClass,
-                              gAudioVolumeGroupChangeHandlerMethods.postEventFromNative,
-                              mObject,
-                              AUDIOVOLUMEGROUP_EVENT_SERVICE_DIED, 0, 0, NULL);
-    if (env->ExceptionCheck()) {
-        ALOGW("An exception occurred while notifying an event.");
-        env->ExceptionClear();
-    }
-}
-
-static
-sp<JNIAudioVolumeGroupCallback> setJniCallback(JNIEnv* env,
-                                               jobject thiz,
-                                               const sp<JNIAudioVolumeGroupCallback>& callback)
-{
-    Mutex::Autolock l(gLock);
-    sp<JNIAudioVolumeGroupCallback> old = (JNIAudioVolumeGroupCallback*)env->GetLongField(
-                thiz, gAudioVolumeGroupChangeHandlerFields.mJniCallback);
-    if (callback.get()) {
-        callback->incStrong((void*)setJniCallback);
-    }
-    if (old != 0) {
-        old->decStrong((void*)setJniCallback);
-    }
-    env->SetLongField(thiz, gAudioVolumeGroupChangeHandlerFields.mJniCallback,
-                      (jlong)callback.get());
-    return old;
-}
-
-static void
-android_media_AudioVolumeGroupChangeHandler_eventHandlerSetup(JNIEnv *env,
-                                                              jobject thiz,
-                                                              jobject weak_this)
-{
-    ALOGV("%s", __FUNCTION__);
-    sp<JNIAudioVolumeGroupCallback> callback =
-            new JNIAudioVolumeGroupCallback(env, thiz, weak_this);
-
-    if (AudioSystem::addAudioVolumeGroupCallback(callback) == NO_ERROR) {
-        setJniCallback(env, thiz, callback);
-    }
-}
-
-static void
-android_media_AudioVolumeGroupChangeHandler_eventHandlerFinalize(JNIEnv *env, jobject thiz)
-{
-    ALOGV("%s", __FUNCTION__);
-    sp<JNIAudioVolumeGroupCallback> callback = setJniCallback(env, thiz, 0);
-    if (callback != 0) {
-        AudioSystem::removeAudioVolumeGroupCallback(callback);
-    }
-}
-
-/*
- * JNI registration.
- */
-static const JNINativeMethod gMethods[] = {
-    {"native_setup", "(Ljava/lang/Object;)V",
-        (void *)android_media_AudioVolumeGroupChangeHandler_eventHandlerSetup},
-    {"native_finalize",  "()V",
-        (void *)android_media_AudioVolumeGroupChangeHandler_eventHandlerFinalize},
-};
-
-int register_android_media_AudioVolumeGroupChangeHandler(JNIEnv *env)
-{
-    jclass audioVolumeGroupChangeHandlerClass =
-            FindClassOrDie(env, kAudioVolumeGroupChangeHandlerClassPathName);
-    gAudioVolumeGroupChangeHandlerMethods.postEventFromNative =
-            GetStaticMethodIDOrDie(env, audioVolumeGroupChangeHandlerClass, "postEventFromNative",
-                                   "(Ljava/lang/Object;IIILjava/lang/Object;)V");
-
-    gAudioVolumeGroupChangeHandlerFields.mJniCallback =
-            GetFieldIDOrDie(env, audioVolumeGroupChangeHandlerClass, "mJniCallback", "J");
-
-    env->DeleteLocalRef(audioVolumeGroupChangeHandlerClass);
-
-    return RegisterMethodsOrDie(env,
-                                kAudioVolumeGroupChangeHandlerClassPathName,
-                                gMethods,
-                                NELEM(gMethods));
-}
-
diff --git a/core/jni/android_media_AudioVolumeGroupCallback.h b/core/jni/android_media_AudioVolumeGroupCallback.h
deleted file mode 100644
index de06549..0000000
--- a/core/jni/android_media_AudioVolumeGroupCallback.h
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#pragma once
-
-#include <system/audio.h>
-#include <media/AudioSystem.h>
-
-namespace android {
-
-// keep in sync with AudioManager.AudioVolumeGroupChangeHandler.java
-#define AUDIOVOLUMEGROUP_EVENT_VOLUME_CHANGED      1000
-#define AUDIOVOLUMEGROUP_EVENT_SERVICE_DIED        1001
-
-class JNIAudioVolumeGroupCallback: public AudioSystem::AudioVolumeGroupCallback
-{
-public:
-    JNIAudioVolumeGroupCallback(JNIEnv* env, jobject thiz, jobject weak_thiz);
-    ~JNIAudioVolumeGroupCallback();
-
-    void onAudioVolumeGroupChanged(volume_group_t group, int flags) override;
-    void onServiceDied() override;
-
-private:
-    void sendEvent(int event);
-
-    jclass      mClass; /**< Reference to AudioVolumeGroupChangeHandler class. */
-    jobject     mObject; /**< Weak ref to AudioVolumeGroupChangeHandler object to call on. */
-};
-
-} // namespace android
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index e47adc9..1a74fe6 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -1219,6 +1219,8 @@
             6 - Lock if keyguard enabled or go to sleep (doze)
             7 - Dream if possible or go to sleep (doze)
             8 - Go to glanceable hub or dream if possible, or sleep if neither is available (doze)
+            9 - Go to dream if device is not dreaming, stop dream if device is dreaming, or sleep if
+                neither is available (doze)
     -->
     <integer name="config_shortPressOnPowerBehavior">1</integer>
 
diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml
index ef6b918..849ca28 100644
--- a/core/res/res/values/config_telephony.xml
+++ b/core/res/res/values/config_telephony.xml
@@ -86,7 +86,7 @@
          CarrierConfigManager#KEY_AUTO_DATA_SWITCH_RAT_SIGNAL_SCORE_STRING_ARRAY.
          If 0, the device always switch to the higher score SIM.
          If < 0, the network type and signal strength based auto switch is disabled. -->
-    <integer name="auto_data_switch_score_tolerance">4000</integer>
+    <integer name="auto_data_switch_score_tolerance">7000</integer>
     <java-symbol type="integer" name="auto_data_switch_score_tolerance" />
 
     <!-- Boolean indicating whether the Iwlan data service supports persistence of iwlan ipsec
@@ -261,7 +261,9 @@
          to identify providers that should be ignored if the carrier config
          carrier_supported_satellite_services_per_provider_bundle does not support them.
          -->
-    <string-array name="config_satellite_providers" translatable="false"></string-array>
+    <string-array name="config_satellite_providers" translatable="false">
+        <item>"310830"</item>
+    </string-array>
     <java-symbol type="array" name="config_satellite_providers" />
 
     <!-- The identifier of the satellite's SIM profile. The identifier is composed of MCC and MNC
diff --git a/core/tests/FileSystemUtilsTest/OWNERS b/core/tests/FileSystemUtilsTest/OWNERS
new file mode 100644
index 0000000..74eeacf
--- /dev/null
+++ b/core/tests/FileSystemUtilsTest/OWNERS
@@ -0,0 +1,2 @@
+waghpawan@google.com
+kaleshsingh@google.com
diff --git a/core/tests/FileSystemUtilsTest/src/com/android/internal/content/FileSystemUtilsTest.java b/core/tests/FileSystemUtilsTest/src/com/android/internal/content/FileSystemUtilsTest.java
index 208d74e..dbfd3e8 100644
--- a/core/tests/FileSystemUtilsTest/src/com/android/internal/content/FileSystemUtilsTest.java
+++ b/core/tests/FileSystemUtilsTest/src/com/android/internal/content/FileSystemUtilsTest.java
@@ -38,6 +38,8 @@
     private static final String PAGE_SIZE_COMPAT_ENABLED_BY_PLATFORM =
             "app_with_4kb_elf_no_override.apk";
 
+    private static final int DEVICE_WAIT_TIMEOUT = 120000;
+
     @Test
     @AppModeFull
     public void runPunchedApp_embeddedNativeLibs() throws DeviceNotAvailableException {
@@ -98,8 +100,20 @@
     @AppModeFull
     public void runAppWith4KbLib_compatByAlignmentChecks()
             throws DeviceNotAvailableException, TargetSetupError {
+        // make sure that device is available for UI test
+        prepareDevice();
         // This test is expected to fail since compat is disabled in manifest
         runPageSizeCompatTest(PAGE_SIZE_COMPAT_ENABLED_BY_PLATFORM,
                 "testPageSizeCompat_compatByAlignmentChecks");
     }
+
+    private void prepareDevice() throws DeviceNotAvailableException {
+        // Verify that device is online before running test and enable root
+        getDevice().waitForDeviceAvailable(DEVICE_WAIT_TIMEOUT);
+        getDevice().enableAdbRoot();
+        getDevice().waitForDeviceAvailable(DEVICE_WAIT_TIMEOUT);
+
+        getDevice().executeShellCommand("input keyevent KEYCODE_WAKEUP");
+        getDevice().executeShellCommand("wm dismiss-keyguard");
+    }
 }
diff --git a/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheUnitTest.java b/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheUnitTest.java
index 8349659..b63fcdc 100644
--- a/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheUnitTest.java
+++ b/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheUnitTest.java
@@ -68,6 +68,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 
 /**
  * Unit tests for {@link android.content.pm.RegisteredServicesCache}
@@ -84,8 +85,8 @@
     @Rule
     public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
 
-    private final ResolveInfo mResolveInfo1 = new ResolveInfo();
-    private final ResolveInfo mResolveInfo2 = new ResolveInfo();
+    private final TestResolveInfo mResolveInfo1 = new TestResolveInfo();
+    private final TestResolveInfo mResolveInfo2 = new TestResolveInfo();
     private final TestServiceType mTestServiceType1 = new TestServiceType("t1", "value1");
     private final TestServiceType mTestServiceType2 = new TestServiceType("t2", "value2");
     @Mock
@@ -195,13 +196,13 @@
 
         reset(testServicesCache);
 
-        testServicesCache.clearServicesForQuerying();
         int u1uid = UserHandle.getUid(U1, UID1);
         assertThat(u1uid).isNotEqualTo(UID1);
 
         final RegisteredServicesCache.ServiceInfo<TestServiceType> serviceInfo2 = newServiceInfo(
                 mTestServiceType1, u1uid, mResolveInfo1.serviceInfo.getComponentName(),
                 1000L /* lastUpdateTime */);
+        mResolveInfo1.setResolveInfoId(U1);
         testServicesCache.addServiceForQuerying(U1, mResolveInfo1, serviceInfo2);
 
         testServicesCache.getAllServices(U1);
@@ -286,7 +287,7 @@
     }
 
     @Test
-    public void testClearServiceInfoCachesAfterTimeout() throws Exception {
+    public void testClearServiceInfoCachesForSingleUserAfterTimeout() throws Exception {
         PackageInfo packageInfo1 = createPackageInfo(1000L /* lastUpdateTime */);
         when(mMockPackageManager.getPackageInfoAsUser(eq(mResolveInfo1.serviceInfo.packageName),
                 anyInt(), eq(U0))).thenReturn(packageInfo1);
@@ -316,6 +317,58 @@
         verify(testServicesCache, times(1)).parseServiceInfo(eq(mResolveInfo1), eq(1000L));
     }
 
+    @Test
+    public void testClearServiceInfoCachesForMultiUserAfterTimeout() throws Exception {
+        PackageInfo packageInfo1 = createPackageInfo(1000L /* lastUpdateTime */);
+        when(mMockPackageManager.getPackageInfoAsUser(eq(mResolveInfo1.serviceInfo.packageName),
+                anyInt(), eq(U0))).thenReturn(packageInfo1);
+        PackageInfo packageInfo2 = createPackageInfo(2000L /* lastUpdateTime */);
+        when(mMockPackageManager.getPackageInfoAsUser(eq(mResolveInfo2.serviceInfo.packageName),
+                anyInt(), eq(U1))).thenReturn(packageInfo2);
+
+        TestRegisteredServicesCache testServicesCache = spy(
+                new TestRegisteredServicesCache(mMockInjector, null /* serializerAndParser */));
+        final RegisteredServicesCache.ServiceInfo<TestServiceType> serviceInfo1 = newServiceInfo(
+                mTestServiceType1, UID1, mResolveInfo1.serviceInfo.getComponentName(),
+                1000L /* lastUpdateTime */);
+        testServicesCache.addServiceForQuerying(U0, mResolveInfo1, serviceInfo1);
+
+        int u1uid = UserHandle.getUid(U1, UID1);
+        final RegisteredServicesCache.ServiceInfo<TestServiceType> serviceInfo2 = newServiceInfo(
+                mTestServiceType2, u1uid, mResolveInfo2.serviceInfo.getComponentName(),
+                2000L /* lastUpdateTime */);
+        testServicesCache.addServiceForQuerying(U1, mResolveInfo2, serviceInfo2);
+
+        // Don't invoke run on the Runnable for U0 user, and it will not clear the service info of
+        // U0 user. Invoke run on the Runnable for U1 user, and it will just clear the service info
+        // of U1 user.
+        doAnswer(invocation -> {
+            Message message = invocation.getArgument(0);
+            if (!message.obj.equals(Integer.valueOf(U0))) {
+                message.getCallback().run();
+            }
+            return true;
+        }).when(mMockBackgroundHandler).sendMessageAtTime(any(Message.class), anyLong());
+
+        // It will generate the service info of U0 user into cache.
+        testServicesCache.getAllServices(U0);
+        verify(testServicesCache, times(1)).parseServiceInfo(eq(mResolveInfo1), eq(1000L));
+        // It will generate the service info of U1 user into cache.
+        testServicesCache.getAllServices(U1);
+        verify(testServicesCache, times(1)).parseServiceInfo(eq(mResolveInfo2), eq(2000L));
+        verify(mMockBackgroundHandler, times(2)).sendMessageAtTime(any(Message.class), anyLong());
+
+        reset(testServicesCache);
+
+        testServicesCache.invalidateCache(U0);
+        testServicesCache.getAllServices(U0);
+        verify(testServicesCache, never()).parseServiceInfo(eq(mResolveInfo1), eq(1000L));
+
+        testServicesCache.invalidateCache(U1);
+        testServicesCache.getAllServices(U1);
+        verify(testServicesCache, times(1)).parseServiceInfo(eq(mResolveInfo2), eq(2000L));
+    }
+
     private static RegisteredServicesCache.ServiceInfo<TestServiceType> newServiceInfo(
             TestServiceType type, int uid, ComponentName componentName, long lastUpdateTime) {
         final ComponentInfo info = new ComponentInfo();
@@ -324,7 +377,7 @@
         return new RegisteredServicesCache.ServiceInfo<>(type, info, componentName, lastUpdateTime);
     }
 
-    private void addServiceInfoIntoResolveInfo(ResolveInfo resolveInfo, String packageName,
+    private void addServiceInfoIntoResolveInfo(TestResolveInfo resolveInfo, String packageName,
             String serviceName) {
         final ServiceInfo serviceInfo = new ServiceInfo();
         serviceInfo.packageName = packageName;
@@ -345,7 +398,7 @@
         static final String SERVICE_INTERFACE = "RegisteredServicesCacheUnitTest";
         static final String SERVICE_META_DATA = "RegisteredServicesCacheUnitTest";
         static final String ATTRIBUTES_NAME = "test";
-        private SparseArray<Map<ResolveInfo, ServiceInfo<TestServiceType>>> mServices =
+        private SparseArray<Map<TestResolveInfo, ServiceInfo<TestServiceType>>> mServices =
                 new SparseArray<>();
 
         public TestRegisteredServicesCache(Injector<TestServiceType> injector,
@@ -362,14 +415,14 @@
 
         @Override
         protected List<ResolveInfo> queryIntentServices(int userId) {
-            Map<ResolveInfo, ServiceInfo<TestServiceType>> map = mServices.get(userId,
-                    new HashMap<ResolveInfo, ServiceInfo<TestServiceType>>());
+            Map<TestResolveInfo, ServiceInfo<TestServiceType>> map = mServices.get(userId,
+                    new HashMap<TestResolveInfo, ServiceInfo<TestServiceType>>());
             return new ArrayList<>(map.keySet());
         }
 
-        void addServiceForQuerying(int userId, ResolveInfo resolveInfo,
+        void addServiceForQuerying(int userId, TestResolveInfo resolveInfo,
                 ServiceInfo<TestServiceType> serviceInfo) {
-            Map<ResolveInfo, ServiceInfo<TestServiceType>> map = mServices.get(userId);
+            Map<TestResolveInfo, ServiceInfo<TestServiceType>> map = mServices.get(userId);
             if (map == null) {
                 map = new HashMap<>();
                 mServices.put(userId, map);
@@ -377,16 +430,12 @@
             map.put(resolveInfo, serviceInfo);
         }
 
-        void clearServicesForQuerying() {
-            mServices.clear();
-        }
-
         @Override
         protected ServiceInfo<TestServiceType> parseServiceInfo(ResolveInfo resolveInfo,
                 long lastUpdateTime) throws XmlPullParserException, IOException {
             int size = mServices.size();
             for (int i = 0; i < size; i++) {
-                Map<ResolveInfo, ServiceInfo<TestServiceType>> map = mServices.valueAt(i);
+                Map<TestResolveInfo, ServiceInfo<TestServiceType>> map = mServices.valueAt(i);
                 ServiceInfo<TestServiceType> serviceInfo = map.get(resolveInfo);
                 if (serviceInfo != null) {
                     return serviceInfo;
@@ -400,4 +449,20 @@
             super.onUserRemoved(userId);
         }
     }
+
+    /**
+     * Create different hash code with the same {@link android.content.pm.ResolveInfo} for testing.
+     */
+    public static class TestResolveInfo extends ResolveInfo {
+        int mResolveInfoId = 0;
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mResolveInfoId, serviceInfo);
+        }
+
+        public void setResolveInfoId(int resolveInfoId) {
+            mResolveInfoId = resolveInfoId;
+        }
+    }
 }
diff --git a/core/tests/coretests/src/com/android/internal/app/MediaRouteDialogPresenterTest.kt b/core/tests/coretests/src/com/android/internal/app/MediaRouteDialogPresenterTest.kt
new file mode 100644
index 0000000..e80d3a6
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/app/MediaRouteDialogPresenterTest.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.app
+
+import android.content.Context
+import android.media.MediaRouter
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.stub
+
+@SmallTest
+@RunWithLooper(setAsMainLooper = true)
+@RunWith(AndroidJUnit4::class)
+class MediaRouteDialogPresenterTest {
+    private var selectedRoute: MediaRouter.RouteInfo = mock()
+    private var mediaRouter: MediaRouter = mock<MediaRouter> {
+        on { selectedRoute } doReturn selectedRoute
+    }
+    private var context: Context = mock<Context> {
+        on { getSystemServiceName(MediaRouter::class.java) } doReturn Context.MEDIA_ROUTER_SERVICE
+        on { getSystemService(MediaRouter::class.java) } doReturn mediaRouter
+    }
+
+    @Test
+    fun shouldShowChooserDialog_routeNotDefault_returnsFalse() {
+        selectedRoute.stub {
+            on { isDefault } doReturn false
+            on { matchesTypes(anyInt()) } doReturn true
+        }
+
+        assertThat(MediaRouteDialogPresenter.shouldShowChooserDialog(
+            context, MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY))
+            .isEqualTo(false)
+    }
+
+    @Test
+    fun shouldShowChooserDialog_routeDefault_returnsTrue() {
+        selectedRoute.stub {
+            on { isDefault } doReturn true
+            on { matchesTypes(anyInt()) } doReturn true
+        }
+
+        assertThat(MediaRouteDialogPresenter.shouldShowChooserDialog(
+            context, MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY))
+            .isEqualTo(true)
+    }
+
+    @Test
+    fun shouldShowChooserDialog_routeNotMatch_returnsTrue() {
+        selectedRoute.stub {
+            on { isDefault } doReturn false
+            on { matchesTypes(anyInt()) } doReturn false
+        }
+
+        assertThat(MediaRouteDialogPresenter.shouldShowChooserDialog(
+            context, MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY))
+            .isEqualTo(true)
+    }
+
+    @Test
+    fun shouldShowChooserDialog_routeDefaultAndNotMatch_returnsTrue() {
+        selectedRoute.stub {
+            on { isDefault } doReturn true
+            on { matchesTypes(anyInt()) } doReturn false
+        }
+
+        assertThat(MediaRouteDialogPresenter.shouldShowChooserDialog(
+            context, MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY))
+            .isEqualTo(true)
+    }
+}
\ No newline at end of file
diff --git a/core/tests/coretests/src/com/android/internal/statusbar/DisableStatesTest.java b/core/tests/coretests/src/com/android/internal/statusbar/DisableStatesTest.java
new file mode 100644
index 0000000..5b82696
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/statusbar/DisableStatesTest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.statusbar;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import android.os.Parcel;
+import android.util.Pair;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DisableStatesTest {
+
+    @Test
+    public void testParcelable() {
+        Map<Integer, Pair<Integer, Integer>> displaysWithStates = new HashMap<>();
+        displaysWithStates.put(1, new Pair<>(10, 20));
+        displaysWithStates.put(2, new Pair<>(30, 40));
+        boolean animate = true;
+        DisableStates original = new DisableStates(displaysWithStates, animate);
+
+        Parcel parcel = Parcel.obtain();
+        original.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        DisableStates restored = DisableStates.CREATOR.createFromParcel(parcel);
+
+        assertNotNull(restored);
+        assertEquals(original.displaysWithStates.size(), restored.displaysWithStates.size());
+        for (Map.Entry<Integer, Pair<Integer, Integer>> entry :
+                original.displaysWithStates.entrySet()) {
+            int displayId = entry.getKey();
+            Pair<Integer, Integer> originalDisplayStates = entry.getValue();
+            Pair<Integer, Integer> restoredDisplayStates = restored.displaysWithStates.get(
+                    displayId);
+            assertEquals(originalDisplayStates.first, restoredDisplayStates.first);
+            assertEquals(originalDisplayStates.second, restoredDisplayStates.second);
+        }
+        assertEquals(original.animate, restored.animate);
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDesktopState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDesktopState.java
deleted file mode 100644
index 1128fb2..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDesktopState.java
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * Copyright (C) 2025 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.common.pip;
-
-import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
-import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
-
-import android.window.DesktopExperienceFlags;
-import android.window.DesktopModeFlags;
-import android.window.DisplayAreaInfo;
-
-import com.android.wm.shell.Flags;
-import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
-import com.android.wm.shell.desktopmode.DesktopUserRepositories;
-import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler;
-
-import java.util.Optional;
-
-/** Helper class for PiP on Desktop Mode. */
-public class PipDesktopState {
-    private final PipDisplayLayoutState mPipDisplayLayoutState;
-    private final Optional<DesktopUserRepositories> mDesktopUserRepositoriesOptional;
-    private final Optional<DragToDesktopTransitionHandler> mDragToDesktopTransitionHandlerOptional;
-    private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
-
-    public PipDesktopState(PipDisplayLayoutState pipDisplayLayoutState,
-            Optional<DesktopUserRepositories> desktopUserRepositoriesOptional,
-            Optional<DragToDesktopTransitionHandler> dragToDesktopTransitionHandlerOptional,
-            RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
-        mPipDisplayLayoutState = pipDisplayLayoutState;
-        mDesktopUserRepositoriesOptional = desktopUserRepositoriesOptional;
-        mDragToDesktopTransitionHandlerOptional = dragToDesktopTransitionHandlerOptional;
-        mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
-    }
-
-    /**
-     * Returns whether PiP in Desktop Windowing is enabled by checking the following:
-     * - PiP in Desktop Windowing flag is enabled
-     * - DesktopUserRepositories is injected
-     * - DragToDesktopTransitionHandler is injected
-     */
-    public boolean isDesktopWindowingPipEnabled() {
-        return DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PIP.isTrue()
-                && mDesktopUserRepositoriesOptional.isPresent()
-                && mDragToDesktopTransitionHandlerOptional.isPresent();
-    }
-
-    /**
-     * Returns whether PiP in Connected Displays is enabled by checking the following:
-     * - PiP in Connected Displays flag is enabled
-     * - PiP2 flag is enabled
-     */
-    public boolean isConnectedDisplaysPipEnabled() {
-        return DesktopExperienceFlags.ENABLE_CONNECTED_DISPLAYS_PIP.isTrue() && Flags.enablePip2();
-    }
-
-    /** Returns whether the display with the PiP task is in freeform windowing mode. */
-    private boolean isDisplayInFreeform() {
-        final DisplayAreaInfo tdaInfo = mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(
-                mPipDisplayLayoutState.getDisplayId());
-        if (tdaInfo != null) {
-            return tdaInfo.configuration.windowConfiguration.getWindowingMode()
-                    == WINDOWING_MODE_FREEFORM;
-        }
-        return false;
-    }
-
-    /** Returns whether PiP is active in a display that is in active Desktop Mode session. */
-    public boolean isPipInDesktopMode() {
-        // Early return if PiP in Desktop Windowing is not supported.
-        if (!isDesktopWindowingPipEnabled()) {
-            return false;
-        }
-        final int displayId = mPipDisplayLayoutState.getDisplayId();
-        return mDesktopUserRepositoriesOptional.get().getCurrent().isAnyDeskActive(displayId);
-    }
-
-    /**
-     * The windowing mode to restore to when resizing out of PIP direction.
-     * Defaults to undefined and can be overridden to restore to an alternate windowing mode.
-     */
-    public int getOutPipWindowingMode() {
-        // If we are exiting PiP while the device is in Desktop mode (the task should expand to
-        // freeform windowing mode):
-        // 1) If the display windowing mode is freeform, set windowing mode to UNDEFINED so it will
-        //    resolve the windowing mode to the display's windowing mode.
-        // 2) If the display windowing mode is not FREEFORM, set windowing mode to FREEFORM.
-        if (isPipInDesktopMode()) {
-            if (isDisplayInFreeform()) {
-                return WINDOWING_MODE_UNDEFINED;
-            } else {
-                return WINDOWING_MODE_FREEFORM;
-            }
-        }
-
-        // By default, or if the task is going to fullscreen, reset the windowing mode to undefined.
-        return WINDOWING_MODE_UNDEFINED;
-    }
-
-    /** Returns whether there is a drag-to-desktop transition in progress. */
-    public boolean isDragToDesktopInProgress() {
-        // Early return if PiP in Desktop Windowing is not supported.
-        if (!isDesktopWindowingPipEnabled()) {
-            return false;
-        }
-        return mDragToDesktopTransitionHandlerOptional.get().getInProgress();
-    }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDesktopState.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDesktopState.kt
new file mode 100644
index 0000000..55bde89
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDesktopState.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.common.pip
+
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
+import android.window.DesktopExperienceFlags
+import android.window.DesktopModeFlags
+import com.android.wm.shell.Flags
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
+import com.android.wm.shell.desktopmode.DesktopUserRepositories
+import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler
+import java.util.Optional
+
+/** Helper class for PiP on Desktop Mode. */
+class PipDesktopState(
+    private val pipDisplayLayoutState: PipDisplayLayoutState,
+    private val desktopUserRepositoriesOptional: Optional<DesktopUserRepositories>,
+    private val dragToDesktopTransitionHandlerOptional: Optional<DragToDesktopTransitionHandler>,
+    private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
+) {
+    /**
+     * Returns whether PiP in Desktop Windowing is enabled by checking the following:
+     * - PiP in Desktop Windowing flag is enabled
+     * - DesktopUserRepositories is present
+     * - DragToDesktopTransitionHandler is present
+     */
+    fun isDesktopWindowingPipEnabled(): Boolean =
+        DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PIP.isTrue &&
+                desktopUserRepositoriesOptional.isPresent &&
+                dragToDesktopTransitionHandlerOptional.isPresent
+
+    /**
+     * Returns whether PiP in Connected Displays is enabled by checking the following:
+     * - PiP in Connected Displays flag is enabled
+     * - PiP2 flag is enabled
+     */
+    fun isConnectedDisplaysPipEnabled(): Boolean =
+        DesktopExperienceFlags.ENABLE_CONNECTED_DISPLAYS_PIP.isTrue && Flags.enablePip2()
+
+    /** Returns whether the display with the PiP task is in freeform windowing mode. */
+    private fun isDisplayInFreeform(): Boolean {
+        val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(
+            pipDisplayLayoutState.displayId
+        )
+
+        return tdaInfo?.configuration?.windowConfiguration?.windowingMode == WINDOWING_MODE_FREEFORM
+    }
+
+    /** Returns whether PiP is active in a display that is in active Desktop Mode session. */
+    fun isPipInDesktopMode(): Boolean {
+        if (!isDesktopWindowingPipEnabled()) {
+            return false
+        }
+
+        val displayId = pipDisplayLayoutState.displayId
+        return desktopUserRepositoriesOptional.get().current.isAnyDeskActive(displayId)
+    }
+
+    /** Returns the windowing mode to restore to when resizing out of PIP direction. */
+    // TODO(b/403345629): Update this for Multi-Desktop.
+    fun getOutPipWindowingMode(): Int {
+        // If we are exiting PiP while the device is in Desktop mode, the task should expand to
+        // freeform windowing mode.
+        // 1) If the display windowing mode is freeform, set windowing mode to UNDEFINED so it will
+        //    resolve the windowing mode to the display's windowing mode.
+        // 2) If the display windowing mode is not FREEFORM, set windowing mode to FREEFORM.
+        if (isPipInDesktopMode()) {
+            return if (isDisplayInFreeform()) {
+                WINDOWING_MODE_UNDEFINED
+            } else {
+                WINDOWING_MODE_FREEFORM
+            }
+        }
+
+        // By default, or if the task is going to fullscreen, reset the windowing mode to undefined.
+        return WINDOWING_MODE_UNDEFINED
+    }
+
+    /** Returns whether there is a drag-to-desktop transition in progress. */
+    fun isDragToDesktopInProgress(): Boolean =
+        isDesktopWindowingPipEnabled() && dragToDesktopTransitionHandlerOptional.get().inProgress
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/FlexParallaxSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/FlexParallaxSpec.java
index 9fa1621..d9a66e1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/FlexParallaxSpec.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/FlexParallaxSpec.java
@@ -57,6 +57,11 @@
      * @return 0f = no dim applied. 1f = full black.
      */
     public float getDimValue(int position, DividerSnapAlgorithm snapAlgorithm) {
+        // On tablets, apps don't go offscreen, so only dim for dismissal.
+        if (!snapAlgorithm.areOffscreenRatiosSupported()) {
+            return ParallaxSpec.super.getDimValue(position, snapAlgorithm);
+        }
+
         int startDismissPos = snapAlgorithm.getDismissStartTarget().getPosition();
         int firstTargetPos = snapAlgorithm.getFirstSplitTarget().getPosition();
         int middleTargetPos = snapAlgorithm.getMiddleTarget().getPosition();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
index 4413c87..d5f4a38 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
@@ -112,6 +112,12 @@
             new SparseArray<>(0);
 
     /**
+     * {@link SparseArray} that maps task ids to {@link CompatUIInfo}.
+     */
+    private final SparseArray<CompatUIInfo> mTaskIdToCompatUIInfoMap =
+            new SparseArray<>(0);
+
+    /**
      * {@link Set} of task ids for which we need to display a restart confirmation dialog
      */
     private Set<Integer> mSetOfTaskIdsShowingRestartDialog = new HashSet<>();
@@ -261,7 +267,11 @@
 
     private void handleDisplayCompatShowRestartDialog(
             CompatUIRequests.DisplayCompatShowRestartDialog request) {
-        onRestartButtonClicked(new Pair<>(request.getTaskInfo(), request.getTaskListener()));
+        final CompatUIInfo compatUIInfo = mTaskIdToCompatUIInfoMap.get(request.getTaskId());
+        if (compatUIInfo == null) {
+            return;
+        }
+        onRestartButtonClicked(new Pair<>(compatUIInfo.getTaskInfo(), compatUIInfo.getListener()));
     }
 
     /**
@@ -273,6 +283,11 @@
     public void onCompatInfoChanged(@NonNull CompatUIInfo compatUIInfo) {
         final TaskInfo taskInfo = compatUIInfo.getTaskInfo();
         final ShellTaskOrganizer.TaskListener taskListener = compatUIInfo.getListener();
+        if (taskListener == null) {
+            mTaskIdToCompatUIInfoMap.delete(taskInfo.taskId);
+        } else {
+            mTaskIdToCompatUIInfoMap.put(taskInfo.taskId, compatUIInfo);
+        }
         final boolean isInDisplayCompatMode =
                 taskInfo.appCompatTaskInfo.isRestartMenuEnabledForDisplayMove();
         if (taskInfo != null && !taskInfo.appCompatTaskInfo.isTopActivityInSizeCompat()
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/CompatUIRequests.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/CompatUIRequests.kt
index da4fc99..b7af596e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/CompatUIRequests.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/CompatUIRequests.kt
@@ -16,8 +16,6 @@
 
 package com.android.wm.shell.compatui.impl
 
-import android.app.TaskInfo
-import com.android.wm.shell.ShellTaskOrganizer
 import com.android.wm.shell.compatui.api.CompatUIRequest
 
 internal const val DISPLAY_COMPAT_SHOW_RESTART_DIALOG = 0
@@ -27,7 +25,6 @@
  */
 sealed class CompatUIRequests(override val requestId: Int) : CompatUIRequest {
     /** Sent when the restart handle menu is clicked, and a restart dialog is requested. */
-    data class DisplayCompatShowRestartDialog(val taskInfo: TaskInfo,
-        val taskListener: ShellTaskOrganizer.TaskListener) :
+    data class DisplayCompatShowRestartDialog(val taskId: Int) :
         CompatUIRequests(DISPLAY_COMPAT_SHOW_RESTART_DIALOG)
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 2cf671b..613e787 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -1213,7 +1213,8 @@
             ShellTaskOrganizer shellTaskOrganizer,
             TaskStackListenerImpl taskStackListener,
             ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler,
-            @DynamicOverride DesktopUserRepositories desktopUserRepositories) {
+            @DynamicOverride DesktopUserRepositories desktopUserRepositories,
+            DisplayController displayController) {
         if (DesktopModeStatus.canEnterDesktopMode(context)) {
             return Optional.of(
                     new DesktopActivityOrientationChangeHandler(
@@ -1222,7 +1223,8 @@
                             shellTaskOrganizer,
                             taskStackListener,
                             toggleResizeDesktopTaskTransitionHandler,
-                            desktopUserRepositories));
+                            desktopUserRepositories,
+                            displayController));
         }
         return Optional.empty();
     }
@@ -1341,7 +1343,9 @@
             Context context,
             ShellInit shellInit,
             @ShellMainThread CoroutineScope mainScope,
+            ShellController shellController,
             DisplayController displayController,
+            RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
             Optional<DesktopUserRepositories> desktopUserRepositories,
             Optional<DesktopTasksController> desktopTasksController,
             Optional<DesktopDisplayModeController> desktopDisplayModeController,
@@ -1355,7 +1359,9 @@
                         context,
                         shellInit,
                         mainScope,
+                        shellController,
                         displayController,
+                        rootTaskDisplayAreaOrganizer,
                         desktopRepositoryInitializer,
                         desktopUserRepositories.get(),
                         desktopTasksController.get(),
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandler.kt
index b8f4bb8..39ce5d9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandler.kt
@@ -23,10 +23,10 @@
 import android.content.res.Configuration.ORIENTATION_LANDSCAPE
 import android.content.res.Configuration.ORIENTATION_PORTRAIT
 import android.graphics.Rect
-import android.util.Size
 import android.window.WindowContainerTransaction
 import com.android.window.flags.Flags
 import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.common.DisplayController
 import com.android.wm.shell.common.TaskStackListenerCallback
 import com.android.wm.shell.common.TaskStackListenerImpl
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
@@ -40,6 +40,7 @@
     private val taskStackListener: TaskStackListenerImpl,
     private val resizeHandler: ToggleResizeDesktopTaskTransitionHandler,
     private val desktopUserRepositories: DesktopUserRepositories,
+    private val displayController: DisplayController,
 ) {
 
     init {
@@ -101,12 +102,24 @@
                 orientation == ORIENTATION_LANDSCAPE &&
                     ActivityInfo.isFixedOrientationPortrait(requestedOrientation)
         ) {
+            val displayLayout = displayController.getDisplayLayout(task.displayId) ?: return
+            val captionInsets =
+                task.configuration.windowConfiguration.appBounds?.let {
+                    it.top - task.configuration.windowConfiguration.bounds.top
+                } ?: 0
+            val newOrientationBounds =
+                calculateInitialBounds(
+                    displayLayout = displayLayout,
+                    taskInfo = task,
+                    captionInsets = captionInsets,
+                    requestedScreenOrientation = requestedOrientation,
+                )
 
-            val finalSize = Size(taskHeight, taskWidth)
             // Use the center x as the resizing anchor point.
-            val left = taskBounds.centerX() - finalSize.width / 2
-            val right = left + finalSize.width
-            val finalBounds = Rect(left, taskBounds.top, right, taskBounds.top + finalSize.height)
+            val left = taskBounds.centerX() - newOrientationBounds.width() / 2
+            val right = left + newOrientationBounds.width()
+            val finalBounds =
+                Rect(left, taskBounds.top, right, taskBounds.top + newOrientationBounds.height())
 
             val wct = WindowContainerTransaction().setBounds(task.token, finalBounds)
             resizeHandler.startTransition(wct)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt
index 683b743..3b98f81 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt
@@ -17,16 +17,20 @@
 package com.android.wm.shell.desktopmode
 
 import android.content.Context
+import android.view.Display
 import android.view.Display.DEFAULT_DISPLAY
 import android.window.DesktopExperienceFlags
 import com.android.internal.protolog.ProtoLog
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
 import com.android.wm.shell.common.DisplayController
 import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener
 import com.android.wm.shell.desktopmode.multidesks.OnDeskRemovedListener
 import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer
 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+import com.android.wm.shell.sysui.ShellController
 import com.android.wm.shell.sysui.ShellInit
+import com.android.wm.shell.sysui.UserChangeListener
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.cancel
 import kotlinx.coroutines.launch
@@ -36,7 +40,9 @@
     private val context: Context,
     shellInit: ShellInit,
     private val mainScope: CoroutineScope,
+    private val shellController: ShellController,
     private val displayController: DisplayController,
+    private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
     private val desktopRepositoryInitializer: DesktopRepositoryInitializer,
     private val desktopUserRepositories: DesktopUserRepositories,
     private val desktopTasksController: DesktopTasksController,
@@ -53,8 +59,17 @@
     private fun onInit() {
         displayController.addDisplayWindowListener(this)
 
-        if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue()) {
+        if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
             desktopTasksController.onDeskRemovedListener = this
+
+            shellController.addUserChangeListener(
+                object : UserChangeListener {
+                    override fun onUserChanged(newUserId: Int, userContext: Context) {
+                        val displayIds = rootTaskDisplayAreaOrganizer.displayIds
+                        createDefaultDesksIfNeeded(displayIds.toSet())
+                    }
+                }
+            )
         }
     }
 
@@ -63,23 +78,7 @@
             desktopDisplayModeController.refreshDisplayWindowingMode()
         }
 
-        if (!supportsDesks(displayId)) {
-            logV("Display #$displayId does not support desks")
-            return
-        }
-
-        mainScope.launch {
-            desktopRepositoryInitializer.isInitialized.collect { initialized ->
-                if (!initialized) return@collect
-                if (desktopRepository.getNumberOfDesks(displayId) == 0) {
-                    logV("Creating new desk in new display#$displayId")
-                    // TODO: b/393978539 - consider activating the desk on creation when
-                    //  applicable, such as for connected displays.
-                    desktopTasksController.createDesk(displayId)
-                }
-                cancel()
-            }
-        }
+        createDefaultDesksIfNeeded(displayIds = setOf(displayId))
     }
 
     override fun onDisplayRemoved(displayId: Int) {
@@ -93,8 +92,34 @@
     override fun onDeskRemoved(lastDisplayId: Int, deskId: Int) {
         val remainingDesks = desktopRepository.getNumberOfDesks(lastDisplayId)
         if (remainingDesks == 0) {
-            logV("All desks removed from display#$lastDisplayId, creating empty desk")
-            desktopTasksController.createDesk(lastDisplayId)
+            logV("All desks removed from display#$lastDisplayId")
+            createDefaultDesksIfNeeded(setOf(lastDisplayId))
+        }
+    }
+
+    private fun createDefaultDesksIfNeeded(displayIds: Set<Int>) {
+        if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return
+        logV("createDefaultDesksIfNeeded displays=%s", displayIds)
+        mainScope.launch {
+            desktopRepositoryInitializer.isInitialized.collect { initialized ->
+                if (!initialized) return@collect
+                displayIds
+                    .filter { displayId -> displayId != Display.INVALID_DISPLAY }
+                    .filter { displayId -> supportsDesks(displayId) }
+                    .filter { displayId -> desktopRepository.getNumberOfDesks(displayId) == 0 }
+                    .also { displaysNeedingDesk ->
+                        logV(
+                            "createDefaultDesksIfNeeded creating default desks in displays=%s",
+                            displaysNeedingDesk,
+                        )
+                    }
+                    .forEach { displayId ->
+                        // TODO: b/393978539 - consider activating the desk on creation when
+                        //  applicable, such as for connected displays.
+                        desktopTasksController.createDesk(displayId)
+                    }
+                cancel()
+            }
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt
index 1ea545f..19507c1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt
@@ -23,10 +23,7 @@
 import android.hardware.input.InputManager.KeyGestureEventHandler
 import android.hardware.input.KeyGestureEvent
 import android.os.IBinder
-import android.window.DesktopModeFlags
-import com.android.hardware.input.Flags.manageKeyGestures
 import com.android.internal.protolog.ProtoLog
-import com.android.window.flags.Flags.enableMoveToNextDisplayShortcut
 import com.android.wm.shell.ShellTaskOrganizer
 import com.android.wm.shell.common.DisplayController
 import com.android.wm.shell.common.ShellExecutor
@@ -51,16 +48,20 @@
 ) : KeyGestureEventHandler {
 
     init {
-        inputManager.registerKeyGestureEventHandler(this)
+        if (desktopTasksController.isPresent && desktopModeWindowDecorViewModel.isPresent) {
+            val supportedGestures =
+                listOf(
+                    KeyGestureEvent.KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY,
+                    KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW,
+                    KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW,
+                    KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW,
+                    KeyGestureEvent.KEY_GESTURE_TYPE_MINIMIZE_FREEFORM_WINDOW,
+                )
+            inputManager.registerKeyGestureEventHandler(supportedGestures, this)
+        }
     }
 
-    override fun handleKeyGestureEvent(event: KeyGestureEvent, focusedToken: IBinder?): Boolean {
-        if (
-                !desktopTasksController.isPresent ||
-                !desktopModeWindowDecorViewModel.isPresent
-        ) {
-            return false
-        }
+    override fun handleKeyGestureEvent(event: KeyGestureEvent, focusedToken: IBinder?) {
         when (event.keyGestureType) {
             KeyGestureEvent.KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY -> {
                 logV("Key gesture MOVE_TO_NEXT_DISPLAY is handled")
@@ -69,7 +70,6 @@
                         desktopTasksController.get().moveToNextDisplay(it.taskId)
                     }
                 }
-                return true
             }
             KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW -> {
                 logV("Key gesture SNAP_LEFT_FREEFORM_WINDOW is handled")
@@ -85,7 +85,6 @@
                             )
                     }
                 }
-                return true
             }
             KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW -> {
                 logV("Key gesture SNAP_RIGHT_FREEFORM_WINDOW is handled")
@@ -101,7 +100,6 @@
                             )
                     }
                 }
-                return true
             }
             KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW -> {
                 logV("Key gesture TOGGLE_MAXIMIZE_FREEFORM_WINDOW is handled")
@@ -120,7 +118,6 @@
                             )
                     }
                 }
-                return true
             }
             KeyGestureEvent.KEY_GESTURE_TYPE_MINIMIZE_FREEFORM_WINDOW -> {
                 logV("Key gesture MINIMIZE_FREEFORM_WINDOW is handled")
@@ -129,9 +126,7 @@
                         desktopTasksController.get().minimizeTask(it, MinimizeReason.KEY_GESTURE)
                     }
                 }
-                return true
             }
-            else -> return false
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
index a8b0baf..3c44fe8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
@@ -69,6 +69,7 @@
     taskInfo: RunningTaskInfo,
     scale: Float = DESKTOP_MODE_INITIAL_BOUNDS_SCALE,
     captionInsets: Int = 0,
+    requestedScreenOrientation: Int? = null,
 ): Rect {
     val screenBounds = Rect(0, 0, displayLayout.width(), displayLayout.height())
     val appAspectRatio = calculateAspectRatio(taskInfo)
@@ -85,12 +86,13 @@
     }
     val topActivityInfo =
         taskInfo.topActivityInfo ?: return positionInScreen(idealSize, stableBounds)
+    val screenOrientation = requestedScreenOrientation ?: topActivityInfo.screenOrientation
 
     val initialSize: Size =
         when (taskInfo.configuration.orientation) {
             ORIENTATION_LANDSCAPE -> {
                 if (taskInfo.canChangeAspectRatio) {
-                    if (isFixedOrientationPortrait(topActivityInfo.screenOrientation)) {
+                    if (isFixedOrientationPortrait(screenOrientation)) {
                         // For portrait resizeable activities, respect apps fullscreen width but
                         // apply ideal size height.
                         Size(
@@ -104,14 +106,20 @@
                 } else {
                     // If activity is unresizeable, regardless of orientation, calculate maximum
                     // size (within the ideal size) maintaining original aspect ratio.
-                    maximizeSizeGivenAspectRatio(taskInfo, idealSize, appAspectRatio, captionInsets)
+                    maximizeSizeGivenAspectRatio(
+                        taskInfo,
+                        idealSize,
+                        appAspectRatio,
+                        captionInsets,
+                        screenOrientation,
+                    )
                 }
             }
             ORIENTATION_PORTRAIT -> {
                 val customPortraitWidthForLandscapeApp =
                     screenBounds.width() - (DESKTOP_MODE_LANDSCAPE_APP_PADDING * 2)
                 if (taskInfo.canChangeAspectRatio) {
-                    if (isFixedOrientationLandscape(topActivityInfo.screenOrientation)) {
+                    if (isFixedOrientationLandscape(screenOrientation)) {
                         // For landscape resizeable activities, respect apps fullscreen height and
                         // apply custom app width.
                         Size(
@@ -123,7 +131,7 @@
                         idealSize
                     }
                 } else {
-                    if (isFixedOrientationLandscape(topActivityInfo.screenOrientation)) {
+                    if (isFixedOrientationLandscape(screenOrientation)) {
                         // For landscape unresizeable activities, apply custom app width to ideal
                         // size and calculate maximum size with this area while maintaining original
                         // aspect ratio.
@@ -132,6 +140,7 @@
                             Size(customPortraitWidthForLandscapeApp, idealSize.height),
                             appAspectRatio,
                             captionInsets,
+                            screenOrientation,
                         )
                     } else {
                         // For portrait unresizeable activities, calculate maximum size (within the
@@ -141,6 +150,7 @@
                             idealSize,
                             appAspectRatio,
                             captionInsets,
+                            screenOrientation,
                         )
                     }
                 }
@@ -190,13 +200,16 @@
     targetArea: Size,
     aspectRatio: Float,
     captionInsets: Int = 0,
+    requestedScreenOrientation: Int? = null,
 ): Size {
     val targetHeight = targetArea.height - captionInsets
     val targetWidth = targetArea.width
     val finalHeight: Int
     val finalWidth: Int
     // Get orientation either through top activity or task's orientation
-    if (taskInfo.hasPortraitTopActivity()) {
+    val screenOrientation =
+        requestedScreenOrientation ?: taskInfo.topActivityInfo?.screenOrientation
+    if (taskInfo.hasPortraitTopActivity(screenOrientation)) {
         val tempWidth = ceil(targetHeight / aspectRatio).toInt()
         if (tempWidth <= targetWidth) {
             finalHeight = targetHeight
@@ -354,9 +367,8 @@
     return Rect(newLeft, newTop, newRight, newBottom)
 }
 
-private fun TaskInfo.hasPortraitTopActivity(): Boolean {
-    val topActivityScreenOrientation =
-        topActivityInfo?.screenOrientation ?: SCREEN_ORIENTATION_UNSPECIFIED
+private fun TaskInfo.hasPortraitTopActivity(screenOrientation: Int?): Boolean {
+    val topActivityScreenOrientation = screenOrientation ?: SCREEN_ORIENTATION_UNSPECIFIED
     val appBounds = configuration.windowConfiguration.appBounds
 
     return when {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
index 2d44395..5f13ba9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
@@ -762,6 +762,7 @@
             floatingBtn.isEnabled = !taskInfo.isPinned
             floatingBtn.imageTintList = style.windowingButtonColor
             desktopBtn.isGone = !shouldShowDesktopModeButton
+            desktopBtnSpace.isGone = !shouldShowDesktopModeButton
             desktopBtn.isSelected = taskInfo.isFreeform
             desktopBtn.isEnabled = !taskInfo.isFreeform
             desktopBtn.imageTintList = style.windowingButtonColor
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipDesktopStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipDesktopStateTest.java
deleted file mode 100644
index 25dbc64..0000000
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipDesktopStateTest.java
+++ /dev/null
@@ -1,184 +0,0 @@
-/*
- * Copyright (C) 2025 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.common.pip;
-
-import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
-import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
-
-import static com.android.window.flags.Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_PIP;
-import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP;
-import static com.android.wm.shell.Flags.FLAG_ENABLE_PIP2;
-
-import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertFalse;
-import static junit.framework.Assert.assertTrue;
-
-import static org.mockito.Mockito.when;
-
-import android.app.ActivityManager;
-import android.platform.test.annotations.EnableFlags;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-import android.window.DisplayAreaInfo;
-import android.window.WindowContainerToken;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
-import com.android.wm.shell.desktopmode.DesktopRepository;
-import com.android.wm.shell.desktopmode.DesktopUserRepositories;
-import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.Mockito;
-import org.mockito.MockitoAnnotations;
-
-import java.util.Optional;
-
-/**
- * Unit test against {@link PipDesktopState}.
- */
-@SmallTest
-@TestableLooper.RunWithLooper
-@RunWith(AndroidTestingRunner.class)
-@EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
-public class PipDesktopStateTest {
-    @Mock private PipDisplayLayoutState mMockPipDisplayLayoutState;
-    @Mock private Optional<DesktopUserRepositories> mMockDesktopUserRepositoriesOptional;
-    @Mock private DesktopUserRepositories mMockDesktopUserRepositories;
-    @Mock private DesktopRepository mMockDesktopRepository;
-    @Mock
-    private Optional<DragToDesktopTransitionHandler> mMockDragToDesktopTransitionHandlerOptional;
-    @Mock private DragToDesktopTransitionHandler mMockDragToDesktopTransitionHandler;
-
-    @Mock private RootTaskDisplayAreaOrganizer mMockRootTaskDisplayAreaOrganizer;
-    @Mock private ActivityManager.RunningTaskInfo mMockTaskInfo;
-
-    private static final int DISPLAY_ID = 1;
-    private DisplayAreaInfo mDefaultTda;
-    private PipDesktopState mPipDesktopState;
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-        when(mMockDesktopUserRepositoriesOptional.get()).thenReturn(mMockDesktopUserRepositories);
-        when(mMockDesktopUserRepositories.getCurrent()).thenReturn(mMockDesktopRepository);
-        when(mMockDesktopUserRepositoriesOptional.isPresent()).thenReturn(true);
-
-        when(mMockDragToDesktopTransitionHandlerOptional.get()).thenReturn(
-                mMockDragToDesktopTransitionHandler);
-        when(mMockDragToDesktopTransitionHandlerOptional.isPresent()).thenReturn(true);
-
-        when(mMockTaskInfo.getDisplayId()).thenReturn(DISPLAY_ID);
-        when(mMockPipDisplayLayoutState.getDisplayId()).thenReturn(DISPLAY_ID);
-
-        mDefaultTda = new DisplayAreaInfo(Mockito.mock(WindowContainerToken.class), DISPLAY_ID, 0);
-        when(mMockRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DISPLAY_ID)).thenReturn(
-                mDefaultTda);
-
-        mPipDesktopState = new PipDesktopState(mMockPipDisplayLayoutState,
-                mMockDesktopUserRepositoriesOptional,
-                mMockDragToDesktopTransitionHandlerOptional,
-                mMockRootTaskDisplayAreaOrganizer);
-    }
-
-    @Test
-    public void isDesktopWindowingPipEnabled_returnsTrue() {
-        assertTrue(mPipDesktopState.isDesktopWindowingPipEnabled());
-    }
-
-    @Test
-    public void isDesktopWindowingPipEnabled_desktopRepositoryEmpty_returnsFalse() {
-        when(mMockDesktopUserRepositoriesOptional.isPresent()).thenReturn(false);
-
-        assertFalse(mPipDesktopState.isDesktopWindowingPipEnabled());
-    }
-
-    @Test
-    public void isDesktopWindowingPipEnabled_dragToDesktopTransitionHandlerEmpty_returnsFalse() {
-        when(mMockDragToDesktopTransitionHandlerOptional.isPresent()).thenReturn(false);
-
-        assertFalse(mPipDesktopState.isDesktopWindowingPipEnabled());
-    }
-
-    @Test
-    @EnableFlags({
-            FLAG_ENABLE_CONNECTED_DISPLAYS_PIP, FLAG_ENABLE_PIP2
-    })
-    public void isConnectedDisplaysPipEnabled_returnsTrue() {
-        assertTrue(mPipDesktopState.isConnectedDisplaysPipEnabled());
-    }
-
-    @Test
-    public void isPipInDesktopMode_anyDeskActive_returnsTrue() {
-        when(mMockDesktopRepository.isAnyDeskActive(DISPLAY_ID)).thenReturn(true);
-
-        assertTrue(mPipDesktopState.isPipInDesktopMode());
-    }
-
-    @Test
-    public void isPipInDesktopMode_noDeskActive_returnsFalse() {
-        when(mMockDesktopRepository.isAnyDeskActive(DISPLAY_ID)).thenReturn(false);
-
-        assertFalse(mPipDesktopState.isPipInDesktopMode());
-    }
-
-    @Test
-    public void getOutPipWindowingMode_exitToDesktop_displayFreeform_returnsUndefined() {
-        when(mMockDesktopRepository.isAnyDeskActive(DISPLAY_ID)).thenReturn(true);
-        setDisplayWindowingMode(WINDOWING_MODE_FREEFORM);
-
-        assertEquals(WINDOWING_MODE_UNDEFINED, mPipDesktopState.getOutPipWindowingMode());
-    }
-
-    @Test
-    public void getOutPipWindowingMode_exitToDesktop_displayFullscreen_returnsFreeform() {
-        when(mMockDesktopRepository.isAnyDeskActive(DISPLAY_ID)).thenReturn(true);
-        setDisplayWindowingMode(WINDOWING_MODE_FULLSCREEN);
-
-        assertEquals(WINDOWING_MODE_FREEFORM, mPipDesktopState.getOutPipWindowingMode());
-    }
-
-    @Test
-    public void getOutPipWindowingMode_exitToFullscreen_displayFullscreen_returnsUndefined() {
-        setDisplayWindowingMode(WINDOWING_MODE_FULLSCREEN);
-
-        assertEquals(WINDOWING_MODE_UNDEFINED, mPipDesktopState.getOutPipWindowingMode());
-    }
-
-    @Test
-    public void isDragToDesktopInProgress_inProgress_returnsTrue() {
-        when(mMockDragToDesktopTransitionHandler.getInProgress()).thenReturn(true);
-
-        assertTrue(mPipDesktopState.isDragToDesktopInProgress());
-    }
-
-    @Test
-    public void isDragToDesktopInProgress_notInProgress_returnsFalse() {
-        when(mMockDragToDesktopTransitionHandler.getInProgress()).thenReturn(false);
-
-        assertFalse(mPipDesktopState.isDragToDesktopInProgress());
-    }
-
-    private void setDisplayWindowingMode(int windowingMode) {
-        mDefaultTda.configuration.windowConfiguration.setWindowingMode(windowingMode);
-    }
-}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipDesktopStateTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipDesktopStateTest.kt
new file mode 100644
index 0000000..2c50cd9
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipDesktopStateTest.kt
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.common.pip
+
+import android.app.ActivityManager
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
+import android.platform.test.annotations.EnableFlags
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import android.window.DisplayAreaInfo
+import android.window.WindowContainerToken
+import androidx.test.filters.SmallTest
+import com.android.window.flags.Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_PIP
+import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP
+import com.android.wm.shell.Flags.FLAG_ENABLE_PIP2
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.desktopmode.DesktopRepository
+import com.android.wm.shell.desktopmode.DesktopUserRepositories
+import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler
+import com.google.common.truth.Truth.assertThat
+import java.util.Optional
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+/**
+ * Unit test against [PipDesktopState].
+ */
+@SmallTest
+@RunWithLooper
+@RunWith(AndroidTestingRunner::class)
+@EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+class PipDesktopStateTest : ShellTestCase() {
+    private val mockPipDisplayLayoutState = mock<PipDisplayLayoutState>()
+    private val mockDesktopUserRepositories = mock<DesktopUserRepositories>()
+    private val mockDesktopRepository = mock<DesktopRepository>()
+    private val mockDragToDesktopTransitionHandler = mock<DragToDesktopTransitionHandler>()
+    private val mockRootTaskDisplayAreaOrganizer = mock<RootTaskDisplayAreaOrganizer>()
+    private val mockTaskInfo = mock<ActivityManager.RunningTaskInfo>()
+    private lateinit var defaultTda: DisplayAreaInfo
+    private lateinit var pipDesktopState: PipDesktopState
+
+    @Before
+    fun setUp() {
+        whenever(mockDesktopUserRepositories.current).thenReturn(mockDesktopRepository)
+        whenever(mockTaskInfo.getDisplayId()).thenReturn(DISPLAY_ID)
+        whenever(mockPipDisplayLayoutState.displayId).thenReturn(DISPLAY_ID)
+
+        defaultTda = DisplayAreaInfo(mock<WindowContainerToken>(), DISPLAY_ID, /* featureId = */ 0)
+        whenever(mockRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DISPLAY_ID)).thenReturn(
+            defaultTda
+        )
+
+        pipDesktopState =
+            PipDesktopState(
+                mockPipDisplayLayoutState,
+                Optional.of(mockDesktopUserRepositories),
+                Optional.of(mockDragToDesktopTransitionHandler),
+                mockRootTaskDisplayAreaOrganizer
+            )
+    }
+
+    @Test
+    fun isDesktopWindowingPipEnabled_returnsTrue() {
+        assertThat(pipDesktopState.isDesktopWindowingPipEnabled()).isTrue()
+    }
+
+    @Test
+    @EnableFlags(
+        FLAG_ENABLE_CONNECTED_DISPLAYS_PIP,
+        FLAG_ENABLE_PIP2
+    )
+    fun isConnectedDisplaysPipEnabled_returnsTrue() {
+        assertThat(pipDesktopState.isConnectedDisplaysPipEnabled()).isTrue()
+    }
+
+    @Test
+    fun isPipInDesktopMode_anyDeskActive_returnsTrue() {
+        whenever(mockDesktopRepository.isAnyDeskActive(DISPLAY_ID)).thenReturn(true)
+
+        assertThat(pipDesktopState.isPipInDesktopMode()).isTrue()
+    }
+
+    @Test
+    fun isPipInDesktopMode_noDeskActive_returnsFalse() {
+        whenever(mockDesktopRepository.isAnyDeskActive(DISPLAY_ID)).thenReturn(false)
+
+        assertThat(pipDesktopState.isPipInDesktopMode()).isFalse()
+    }
+
+    @Test
+    fun outPipWindowingMode_exitToDesktop_displayFreeform_returnsUndefined() {
+        whenever(mockDesktopRepository.isAnyDeskActive(DISPLAY_ID)).thenReturn(true)
+        setDisplayWindowingMode(WINDOWING_MODE_FREEFORM)
+
+        assertThat(pipDesktopState.getOutPipWindowingMode()).isEqualTo(WINDOWING_MODE_UNDEFINED)
+    }
+
+    @Test
+    fun outPipWindowingMode_exitToDesktop_displayFullscreen_returnsFreeform() {
+        whenever(mockDesktopRepository.isAnyDeskActive(DISPLAY_ID)).thenReturn(true)
+        setDisplayWindowingMode(WINDOWING_MODE_FULLSCREEN)
+
+        assertThat(pipDesktopState.getOutPipWindowingMode()).isEqualTo(WINDOWING_MODE_FREEFORM)
+    }
+
+    @Test
+    fun outPipWindowingMode_exitToFullscreen_displayFullscreen_returnsUndefined() {
+        setDisplayWindowingMode(WINDOWING_MODE_FULLSCREEN)
+
+        assertThat(pipDesktopState.getOutPipWindowingMode()).isEqualTo(WINDOWING_MODE_UNDEFINED)
+    }
+
+    @Test
+    fun isDragToDesktopInProgress_inProgress_returnsTrue() {
+        whenever(mockDragToDesktopTransitionHandler.inProgress).thenReturn(true)
+
+        assertThat(pipDesktopState.isDragToDesktopInProgress()).isTrue()
+    }
+
+    @Test
+    fun isDragToDesktopInProgress_notInProgress_returnsFalse() {
+        whenever(mockDragToDesktopTransitionHandler.inProgress).thenReturn(false)
+
+        assertThat(pipDesktopState.isDragToDesktopInProgress()).isFalse()
+    }
+
+    private fun setDisplayWindowingMode(windowingMode: Int) {
+        defaultTda.configuration.windowConfiguration.windowingMode = windowingMode
+    }
+
+    companion object {
+        private const val DISPLAY_ID = 1
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/FlexParallaxSpecTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/FlexParallaxSpecTests.java
index 22a85fc..9f2534e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/FlexParallaxSpecTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/FlexParallaxSpecTests.java
@@ -71,6 +71,7 @@
         when(mockSnapAlgorithm.getMiddleTarget()).thenReturn(mockMiddleTarget);
         when(mockSnapAlgorithm.getLastSplitTarget()).thenReturn(mockLastTarget);
         when(mockSnapAlgorithm.getDismissEndTarget()).thenReturn(mockEndEdge);
+        when(mockSnapAlgorithm.areOffscreenRatiosSupported()).thenReturn(true);
 
         when(mockStartEdge.getPosition()).thenReturn(0);
         when(mockFirstTarget.getPosition()).thenReturn(250);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
index 597e4a5..9035df2 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
@@ -680,7 +680,8 @@
 
         // Create transparent task
         final TaskInfo taskInfo1 = createTaskInfo(DISPLAY_ID, newTaskId, /* hasSizeCompat= */ true,
-                /* isVisible */ true, /* isFocused */ true, /* isTopActivityTransparent */ true);
+                /* isVisible */ true, /* isFocused */ true, /* isTopActivityTransparent */ true,
+                /* isRestartMenuEnabledForDisplayMove */ true);
 
         // Simulate new task being shown
         mController.updateActiveTaskInfo(taskInfo1);
@@ -742,32 +743,38 @@
     @Test
     @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testSendCompatUIRequest_createRestartDialog() {
-        TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ false);
-        doReturn(true).when(mMockRestartDialogLayout)
-                .needsToBeRecreated(any(TaskInfo.class),
-                        any(ShellTaskOrganizer.TaskListener.class));
+        final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true,
+                /* isVisible */ true, /* isFocused */ true, /* isTopActivityTransparent */ false,
+                /* isRestartMenuEnabledForDisplayMove */ true);
         doReturn(true).when(mCompatUIConfiguration).isRestartDialogEnabled();
         doReturn(true).when(mCompatUIConfiguration).shouldShowRestartDialogAgain(eq(taskInfo));
 
-        mController.sendCompatUIRequest(new CompatUIRequests.DisplayCompatShowRestartDialog(
-                taskInfo, mMockTaskListener));
+        mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener));
         verify(mController).createRestartDialogWindowManager(any(), eq(taskInfo),
                 eq(mMockTaskListener));
+        verify(mMockRestartDialogLayout).setRequestRestartDialog(false);
+
+        mController.sendCompatUIRequest(
+                new CompatUIRequests.DisplayCompatShowRestartDialog(taskInfo.taskId));
+        verify(mMockRestartDialogLayout).setRequestRestartDialog(true);
     }
 
     private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat) {
         return createTaskInfo(displayId, taskId, hasSizeCompat, /* isVisible */ false,
-                /* isFocused */ false, /* isTopActivityTransparent */ false);
+                /* isFocused */ false, /* isTopActivityTransparent */ false,
+                /* isRestartMenuEnabledForDisplayMove */ false);
     }
 
     private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat,
             boolean isVisible, boolean isFocused) {
         return createTaskInfo(displayId, taskId, hasSizeCompat,
-                isVisible, isFocused, /* isTopActivityTransparent */ false);
+                isVisible, isFocused, /* isTopActivityTransparent */ false,
+                /* isRestartMenuEnabledForDisplayMove */ false);
     }
 
     private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat,
-            boolean isVisible, boolean isFocused, boolean isTopActivityTransparent) {
+            boolean isVisible, boolean isFocused, boolean isTopActivityTransparent,
+            boolean isRestartMenuEnabledForDisplayMove) {
         RunningTaskInfo taskInfo = new RunningTaskInfo();
         taskInfo.taskId = taskId;
         taskInfo.displayId = displayId;
@@ -777,6 +784,8 @@
         taskInfo.isTopActivityTransparent = isTopActivityTransparent;
         taskInfo.appCompatTaskInfo.setLetterboxEducationEnabled(true);
         taskInfo.appCompatTaskInfo.setTopActivityLetterboxed(true);
+        taskInfo.appCompatTaskInfo.setRestartMenuEnabledForDisplayMove(
+                isRestartMenuEnabledForDisplayMove);
         return taskInfo;
     }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt
index d58f8a3..94fe030 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt
@@ -37,6 +37,8 @@
 import com.android.window.flags.Flags.FLAG_RESPECT_ORIENTATION_CHANGE_FOR_UNRESIZEABLE
 import com.android.wm.shell.ShellTaskOrganizer
 import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.DisplayLayout
 import com.android.wm.shell.common.ShellExecutor
 import com.android.wm.shell.common.TaskStackListenerImpl
 import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask
@@ -96,12 +98,15 @@
     @Mock lateinit var repositoryInitializer: DesktopRepositoryInitializer
     @Mock lateinit var userManager: UserManager
     @Mock lateinit var shellController: ShellController
+    @Mock lateinit var displayController: DisplayController
+    @Mock lateinit var displayLayout: DisplayLayout
 
     private lateinit var mockitoSession: StaticMockitoSession
     private lateinit var handler: DesktopActivityOrientationChangeHandler
     private lateinit var shellInit: ShellInit
     private lateinit var userRepositories: DesktopUserRepositories
     private lateinit var testScope: CoroutineScope
+
     // Mock running tasks are registered here so we can get the list from mock shell task organizer.
     private val runningTasks = mutableListOf<RunningTaskInfo>()
 
@@ -131,6 +136,7 @@
         whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() }
         whenever(runBlocking { persistentRepository.readDesktop(any(), any()) })
             .thenReturn(Desktop.getDefaultInstance())
+        whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout)
 
         handler =
             DesktopActivityOrientationChangeHandler(
@@ -140,6 +146,7 @@
                 taskStackListener,
                 resizeTransitionHandler,
                 userRepositories,
+                displayController,
             )
 
         shellInit.init()
@@ -171,6 +178,7 @@
                 taskStackListener,
                 resizeTransitionHandler,
                 userRepositories,
+                displayController,
             )
 
         verify(shellInit, never())
@@ -251,6 +259,11 @@
         val oldBounds = task.configuration.windowConfiguration.bounds
         val newTask =
             setUpFreeformTask(isResizeable = false, orientation = SCREEN_ORIENTATION_LANDSCAPE)
+        whenever(displayLayout.height()).thenReturn(800)
+        whenever(displayLayout.width()).thenReturn(2000)
+        whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+            (i.arguments.first() as Rect).set(Rect(0, 0, 2000, 800))
+        }
 
         handler.handleActivityOrientationChange(task, newTask)
 
@@ -279,6 +292,11 @@
                 bounds = oldBounds,
             )
         val newTask = setUpFreeformTask(isResizeable = false, bounds = oldBounds)
+        whenever(displayLayout.height()).thenReturn(2000)
+        whenever(displayLayout.width()).thenReturn(800)
+        whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+            (i.arguments.first() as Rect).set(Rect(0, 0, 800, 2000))
+        }
 
         handler.handleActivityOrientationChange(task, newTask)
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt
index 2aebcdc..9268db6 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt
@@ -24,13 +24,16 @@
 import com.android.dx.mockito.inline.extended.ExtendedMockito.never
 import com.android.dx.mockito.inline.extended.StaticMockitoSession
 import com.android.window.flags.Flags
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
 import com.android.wm.shell.ShellTestCase
 import com.android.wm.shell.common.DisplayController
 import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener
 import com.android.wm.shell.common.ShellExecutor
 import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+import com.android.wm.shell.sysui.ShellController
 import com.android.wm.shell.sysui.ShellInit
+import com.android.wm.shell.sysui.UserChangeListener
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.cancel
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -46,6 +49,7 @@
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.clearInvocations
 import org.mockito.kotlin.whenever
 import org.mockito.quality.Strictness
 
@@ -60,6 +64,8 @@
 class DesktopDisplayEventHandlerTest : ShellTestCase() {
     @Mock lateinit var testExecutor: ShellExecutor
     @Mock lateinit var displayController: DisplayController
+    @Mock private lateinit var mockShellController: ShellController
+    @Mock private lateinit var mockRootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
     @Mock private lateinit var mockDesktopUserRepositories: DesktopUserRepositories
     @Mock private lateinit var mockDesktopRepository: DesktopRepository
     @Mock private lateinit var mockDesktopTasksController: DesktopTasksController
@@ -89,7 +95,9 @@
                 context,
                 shellInit,
                 testScope.backgroundScope,
+                mockShellController,
                 displayController,
+                mockRootTaskDisplayAreaOrganizer,
                 desktopRepositoryInitializer,
                 mockDesktopUserRepositories,
                 mockDesktopTasksController,
@@ -107,6 +115,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
     fun testDisplayAdded_supportsDesks_desktopRepositoryInitialized_createsDesk() =
         testScope.runTest {
             whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true)
@@ -119,6 +128,7 @@
         }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
     fun testDisplayAdded_supportsDesks_desktopRepositoryNotInitialized_doesNotCreateDesk() =
         testScope.runTest {
             whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true)
@@ -130,6 +140,7 @@
         }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
     fun testDisplayAdded_supportsDesks_desktopRepositoryInitializedTwice_createsDeskOnce() =
         testScope.runTest {
             whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true)
@@ -143,6 +154,7 @@
         }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
     fun testDisplayAdded_supportsDesks_desktopRepositoryInitialized_deskExists_doesNotCreateDesk() =
         testScope.runTest {
             whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true)
@@ -156,33 +168,71 @@
         }
 
     @Test
-    fun testDisplayAdded_cannotEnterDesktopMode_doesNotCreateDesk() {
-        whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(false)
+    @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+    fun testDisplayAdded_cannotEnterDesktopMode_doesNotCreateDesk() =
+        testScope.runTest {
+            whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(false)
+            desktopRepositoryInitializer.initialize(mockDesktopUserRepositories)
 
-        onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY)
+            onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY)
+            runCurrent()
 
-        verify(mockDesktopTasksController, never()).createDesk(DEFAULT_DISPLAY)
-    }
+            verify(mockDesktopTasksController, never()).createDesk(DEFAULT_DISPLAY)
+        }
 
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
-    fun testDeskRemoved_noDesksRemain_createsDesk() {
-        whenever(mockDesktopRepository.getNumberOfDesks(DEFAULT_DISPLAY)).thenReturn(0)
+    fun testDeskRemoved_noDesksRemain_createsDesk() =
+        testScope.runTest {
+            whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true)
+            whenever(mockDesktopRepository.getNumberOfDesks(DEFAULT_DISPLAY)).thenReturn(0)
+            desktopRepositoryInitializer.initialize(mockDesktopUserRepositories)
 
-        handler.onDeskRemoved(DEFAULT_DISPLAY, deskId = 1)
+            handler.onDeskRemoved(DEFAULT_DISPLAY, deskId = 1)
+            runCurrent()
 
-        verify(mockDesktopTasksController).createDesk(DEFAULT_DISPLAY)
-    }
+            verify(mockDesktopTasksController).createDesk(DEFAULT_DISPLAY)
+        }
 
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
-    fun testDeskRemoved_desksRemain_doesNotCreateDesk() {
-        whenever(mockDesktopRepository.getNumberOfDesks(DEFAULT_DISPLAY)).thenReturn(1)
+    fun testDeskRemoved_desksRemain_doesNotCreateDesk() =
+        testScope.runTest {
+            whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true)
+            whenever(mockDesktopRepository.getNumberOfDesks(DEFAULT_DISPLAY)).thenReturn(1)
+            desktopRepositoryInitializer.initialize(mockDesktopUserRepositories)
 
-        handler.onDeskRemoved(DEFAULT_DISPLAY, deskId = 1)
+            handler.onDeskRemoved(DEFAULT_DISPLAY, deskId = 1)
+            runCurrent()
 
-        verify(mockDesktopTasksController, never()).createDesk(DEFAULT_DISPLAY)
-    }
+            verify(mockDesktopTasksController, never()).createDesk(DEFAULT_DISPLAY)
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+    fun testUserChanged_createsDeskWhenNeeded() =
+        testScope.runTest {
+            whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true)
+            val userChangeListenerCaptor = argumentCaptor<UserChangeListener>()
+            verify(mockShellController).addUserChangeListener(userChangeListenerCaptor.capture())
+            whenever(mockDesktopRepository.getNumberOfDesks(displayId = 2)).thenReturn(0)
+            whenever(mockDesktopRepository.getNumberOfDesks(displayId = 3)).thenReturn(0)
+            whenever(mockDesktopRepository.getNumberOfDesks(displayId = 4)).thenReturn(1)
+            whenever(mockRootTaskDisplayAreaOrganizer.displayIds).thenReturn(intArrayOf(2, 3, 4))
+            desktopRepositoryInitializer.initialize(mockDesktopUserRepositories)
+            handler.onDisplayAdded(displayId = 2)
+            handler.onDisplayAdded(displayId = 3)
+            handler.onDisplayAdded(displayId = 4)
+            runCurrent()
+
+            clearInvocations(mockDesktopTasksController)
+            userChangeListenerCaptor.lastValue.onUserChanged(1, context)
+            runCurrent()
+
+            verify(mockDesktopTasksController).createDesk(displayId = 2)
+            verify(mockDesktopTasksController).createDesk(displayId = 3)
+            verify(mockDesktopTasksController, never()).createDesk(displayId = 4)
+        }
 
     @Test
     fun testConnectExternalDisplay() {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt
index d510570..e40da5e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt
@@ -50,7 +50,6 @@
 import com.android.wm.shell.sysui.ShellInit
 import com.android.wm.shell.transition.FocusTransitionObserver
 import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel
-import com.google.common.truth.Truth.assertThat
 import java.util.Optional
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
@@ -120,11 +119,11 @@
         whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)).thenReturn(tda)
 
         doAnswer {
-                keyGestureEventHandler = (it.arguments[0] as KeyGestureEventHandler)
+                keyGestureEventHandler = (it.arguments[1] as KeyGestureEventHandler)
                 null
             }
             .whenever(inputManager)
-            .registerKeyGestureEventHandler(any())
+            .registerKeyGestureEventHandler(any(), any())
         shellInit.init()
 
         desktopModeKeyGestureHandler =
@@ -176,10 +175,9 @@
                 .setKeycodes(intArrayOf(KeyEvent.KEYCODE_D))
                 .setModifierState(KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON)
                 .build()
-        val result = keyGestureEventHandler.handleKeyGestureEvent(event, null)
+        keyGestureEventHandler.handleKeyGestureEvent(event, null)
         testExecutor.flushAll()
 
-        assertThat(result).isTrue()
         verify(desktopTasksController).moveToNextDisplay(task.taskId)
     }
 
@@ -197,10 +195,9 @@
                 .setKeycodes(intArrayOf(KeyEvent.KEYCODE_LEFT_BRACKET))
                 .setModifierState(KeyEvent.META_META_ON)
                 .build()
-        val result = keyGestureEventHandler.handleKeyGestureEvent(event, null)
+        keyGestureEventHandler.handleKeyGestureEvent(event, null)
         testExecutor.flushAll()
 
-        assertThat(result).isTrue()
         verify(desktopModeWindowDecorViewModel)
             .onSnapResize(
                 task.taskId,
@@ -224,10 +221,9 @@
                 .setKeycodes(intArrayOf(KeyEvent.KEYCODE_RIGHT_BRACKET))
                 .setModifierState(KeyEvent.META_META_ON)
                 .build()
-        val result = keyGestureEventHandler.handleKeyGestureEvent(event, null)
+        keyGestureEventHandler.handleKeyGestureEvent(event, null)
         testExecutor.flushAll()
 
-        assertThat(result).isTrue()
         verify(desktopModeWindowDecorViewModel)
             .onSnapResize(
                 task.taskId,
@@ -251,10 +247,9 @@
                 .setKeycodes(intArrayOf(KeyEvent.KEYCODE_EQUALS))
                 .setModifierState(KeyEvent.META_META_ON)
                 .build()
-        val result = keyGestureEventHandler.handleKeyGestureEvent(event, null)
+        keyGestureEventHandler.handleKeyGestureEvent(event, null)
         testExecutor.flushAll()
 
-        assertThat(result).isTrue()
         verify(desktopTasksController)
             .toggleDesktopTaskSize(
                 task,
@@ -280,10 +275,9 @@
                 .setKeycodes(intArrayOf(KeyEvent.KEYCODE_MINUS))
                 .setModifierState(KeyEvent.META_META_ON)
                 .build()
-        val result = keyGestureEventHandler.handleKeyGestureEvent(event, null)
+        keyGestureEventHandler.handleKeyGestureEvent(event, null)
         testExecutor.flushAll()
 
-        assertThat(result).isTrue()
         verify(desktopTasksController).minimizeTask(task, MinimizeReason.KEY_GESTURE)
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
index f7b9c335..dd777a5 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
@@ -1916,6 +1916,7 @@
                 .setTaskDescriptionBuilder(taskDescriptionBuilder)
                 .setVisible(visible)
                 .build();
+        taskInfo.isVisibleRequested = visible;
         taskInfo.realActivity = new ComponentName("com.android.wm.shell.windowdecor",
                 "DesktopModeWindowDecorationTests");
         taskInfo.baseActivity = new ComponentName("com.android.wm.shell.windowdecor",
diff --git a/libs/hwui/renderthread/ReliableSurface.cpp b/libs/hwui/renderthread/ReliableSurface.cpp
index 01e8010..64d38b9 100644
--- a/libs/hwui/renderthread/ReliableSurface.cpp
+++ b/libs/hwui/renderthread/ReliableSurface.cpp
@@ -149,25 +149,9 @@
         return AHardwareBuffer_to_ANativeWindowBuffer(mScratchBuffer.get());
     }
 
-    int width = -1;
-    int result = mWindow->query(mWindow, NATIVE_WINDOW_DEFAULT_WIDTH, &width);
-    if (result != OK || width < 0) {
-        ALOGW("Failed to query window default width: %s (%d) value=%d", strerror(-result), result,
-              width);
-        width = 1;
-    }
-
-    int height = -1;
-    result = mWindow->query(mWindow, NATIVE_WINDOW_DEFAULT_HEIGHT, &height);
-    if (result != OK || height < 0) {
-        ALOGW("Failed to query window default height: %s (%d) value=%d", strerror(-result), result,
-              height);
-        height = 1;
-    }
-
     AHardwareBuffer_Desc desc = AHardwareBuffer_Desc{
-            .width = static_cast<uint32_t>(width),
-            .height = static_cast<uint32_t>(height),
+            .width = 1,
+            .height = 1,
             .layers = 1,
             .format = mFormat,
             .usage = mUsage,
@@ -176,9 +160,9 @@
     };
 
     AHardwareBuffer* newBuffer;
-    result = AHardwareBuffer_allocate(&desc, &newBuffer);
+    int result = AHardwareBuffer_allocate(&desc, &newBuffer);
 
-    if (result != OK) {
+    if (result != NO_ERROR) {
         // Allocate failed, that sucks
         ALOGW("Failed to allocate scratch buffer, error=%d", result);
         return nullptr;
diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp
index a67aea4..0cd9c53 100644
--- a/libs/hwui/renderthread/VulkanManager.cpp
+++ b/libs/hwui/renderthread/VulkanManager.cpp
@@ -238,6 +238,7 @@
     for (uint32_t i = 0; i < queueCount; i++) {
         queuePriorityProps[i].sType = VK_STRUCTURE_TYPE_QUEUE_FAMILY_GLOBAL_PRIORITY_PROPERTIES_EXT;
         queuePriorityProps[i].pNext = nullptr;
+        queueProps[i].sType = VK_STRUCTURE_TYPE_QUEUE_FAMILY_PROPERTIES_2;
         queueProps[i].pNext = &queuePriorityProps[i];
     }
     mGetPhysicalDeviceQueueFamilyProperties2(mPhysicalDevice, &queueCount, queueProps.get());
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 4aba491..0a1bfd55 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -19,14 +19,15 @@
 import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO;
 import static android.content.Context.DEVICE_ID_DEFAULT;
+import static android.media.audio.Flags.FLAG_DEPRECATE_STREAM_BT_SCO;
+import static android.media.audio.Flags.FLAG_FOCUS_EXCLUSIVE_WITH_RECORDING;
+import static android.media.audio.Flags.FLAG_FOCUS_FREEZE_TEST_API;
+import static android.media.audio.Flags.FLAG_REGISTER_VOLUME_CALLBACK_API_HARDENING;
+import static android.media.audio.Flags.FLAG_SUPPORTED_DEVICE_TYPES_API;
 import static android.media.audio.Flags.FLAG_UNIFY_ABSOLUTE_VOLUME_MANAGEMENT;
 import static android.media.audio.Flags.autoPublicVolumeApiHardening;
 import static android.media.audio.Flags.cacheGetStreamMinMaxVolume;
 import static android.media.audio.Flags.cacheGetStreamVolume;
-import static android.media.audio.Flags.FLAG_DEPRECATE_STREAM_BT_SCO;
-import static android.media.audio.Flags.FLAG_FOCUS_EXCLUSIVE_WITH_RECORDING;
-import static android.media.audio.Flags.FLAG_FOCUS_FREEZE_TEST_API;
-import static android.media.audio.Flags.FLAG_SUPPORTED_DEVICE_TYPES_API;
 import static android.media.audiopolicy.Flags.FLAG_ENABLE_FADE_MANAGER_CONFIGURATION;
 
 import android.Manifest;
@@ -65,7 +66,7 @@
 import android.media.audiopolicy.AudioPolicy.AudioPolicyFocusListener;
 import android.media.audiopolicy.AudioProductStrategy;
 import android.media.audiopolicy.AudioVolumeGroup;
-import android.media.audiopolicy.AudioVolumeGroupChangeHandler;
+import android.media.audiopolicy.IAudioVolumeChangeDispatcher;
 import android.media.projection.MediaProjection;
 import android.media.session.MediaController;
 import android.media.session.MediaSession;
@@ -128,8 +129,6 @@
     private static final String TAG = "AudioManager";
     private static final boolean DEBUG = false;
     private static final AudioPortEventHandler sAudioPortEventHandler = new AudioPortEventHandler();
-    private static final AudioVolumeGroupChangeHandler sAudioAudioVolumeGroupChangedHandler =
-            new AudioVolumeGroupChangeHandler();
 
     private static WeakReference<Context> sContext;
 
@@ -8761,9 +8760,13 @@
         }
     }
 
+    //====================================================================
+    // Notification of volume group changes
     /**
+     * Callback to receive updates on volume group changes, register using
+     * {@link AudioManager#registerVolumeGroupCallback(Executor, AudioVolumeCallback)}.
+     *
      * @hide
-     * Callback registered by client to be notified upon volume group change.
      */
     @SystemApi
     public abstract static class VolumeGroupCallback {
@@ -8774,35 +8777,70 @@
         public void onAudioVolumeGroupChanged(int group, int flags) {}
     }
 
-   /**
-    * @hide
-    * Register an audio volume group change listener.
-    * @param callback the {@link VolumeGroupCallback} to register
-    */
+    /**
+     * Register an audio volume group change listener.
+     *
+     * @param executor {@link Executor} to handle the callbacks
+     * @param callback the callback to receive the audio volume group changes
+     * @throws SecurityException if the caller doesn't have the required permission.
+     *
+     * @hide
+     */
     @SystemApi
-    public void registerVolumeGroupCallback(
-            @NonNull Executor executor,
+    @FlaggedApi(FLAG_REGISTER_VOLUME_CALLBACK_API_HARDENING)
+    @RequiresPermission("Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED")
+    public void registerVolumeGroupCallback(@NonNull Executor executor,
             @NonNull VolumeGroupCallback callback) {
-        Preconditions.checkNotNull(executor, "executor must not be null");
-        Preconditions.checkNotNull(callback, "volume group change cb must not be null");
-        sAudioAudioVolumeGroupChangedHandler.init();
-        // TODO: make use of executor
-        sAudioAudioVolumeGroupChangedHandler.registerListener(callback);
+        mVolumeChangedListenerMgr.addListener(executor, callback, "registerVolumeGroupCallback",
+                () -> new AudioVolumeChangeDispatcherStub());
     }
 
-   /**
-    * @hide
-    * Unregister an audio volume group change listener.
-    * @param callback the {@link VolumeGroupCallback} to unregister
-    */
+    /**
+     * Unregister an audio volume group change listener.
+     *
+     * @param callback the {@link VolumeGroupCallback} to unregister
+     *
+     * @hide
+     */
     @SystemApi
-    public void unregisterVolumeGroupCallback(
-            @NonNull VolumeGroupCallback callback) {
-        Preconditions.checkNotNull(callback, "volume group change cb must not be null");
-        sAudioAudioVolumeGroupChangedHandler.unregisterListener(callback);
+    @FlaggedApi(FLAG_REGISTER_VOLUME_CALLBACK_API_HARDENING)
+    @RequiresPermission("Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED")
+    public void unregisterVolumeGroupCallback(@NonNull VolumeGroupCallback callback) {
+        mVolumeChangedListenerMgr.removeListener(callback, "unregisterVolumeGroupCallback");
     }
 
     /**
+     * Manages the VolumeGroupCallback listeners and the AudioVolumeChangeDispatcherStub
+     */
+    private final CallbackUtil.LazyListenerManager<VolumeGroupCallback> mVolumeChangedListenerMgr =
+            new CallbackUtil.LazyListenerManager();
+
+    final class AudioVolumeChangeDispatcherStub extends IAudioVolumeChangeDispatcher.Stub
+            implements CallbackUtil.DispatcherStub {
+
+        @Override
+        public void register(boolean register) {
+            try {
+                if (register) {
+                    getService().registerAudioVolumeCallback(this);
+                } else {
+                    getService().unregisterAudioVolumeCallback(this);
+                }
+            } catch (RemoteException e) {
+                e.rethrowFromSystemServer();
+            }
+        }
+
+        @Override
+        public void onAudioVolumeGroupChanged(int group, int flags) {
+            mVolumeChangedListenerMgr.callListeners((listener) ->
+                    listener.onAudioVolumeGroupChanged(group, flags));
+        }
+    }
+
+    //====================================================================
+
+    /**
      * Return if an asset contains haptic channels or not.
      *
      * @param context the {@link Context} to resolve the uri.
diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java
index ad6f2e52..4906cd3 100644
--- a/media/java/android/media/AudioSystem.java
+++ b/media/java/android/media/AudioSystem.java
@@ -2732,4 +2732,25 @@
      * @hide
      */
     public static native void triggerSystemPropertyUpdate(long handle);
+
+    /**
+     * Registers the given {@link INativeAudioVolumeGroupCallback} to native audioserver.
+     * @param callback to register
+     * @return {@link #SUCCESS} if successfully registered.
+     *
+     * @hide
+     */
+    public static native int registerAudioVolumeGroupCallback(
+            INativeAudioVolumeGroupCallback callback);
+
+    /**
+     * Unegisters the given {@link INativeAudioVolumeGroupCallback} from native audioserver
+     * previously registered via {@link #registerAudioVolumeGroupCallback}.
+     * @param callback to register
+     * @return {@link #SUCCESS} if successfully registered.
+     *
+     * @hide
+     */
+    public static native int unregisterAudioVolumeGroupCallback(
+            INativeAudioVolumeGroupCallback callback);
 }
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index 8aadb41..c505bce 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -65,6 +65,7 @@
 import android.media.audiopolicy.AudioProductStrategy;
 import android.media.audiopolicy.AudioVolumeGroup;
 import android.media.audiopolicy.IAudioPolicyCallback;
+import android.media.audiopolicy.IAudioVolumeChangeDispatcher;
 import android.media.projection.IMediaProjection;
 import android.net.Uri;
 import android.os.PersistableBundle;
@@ -446,6 +447,12 @@
 
     boolean isAudioServerRunning();
 
+    @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED")
+    void registerAudioVolumeCallback(IAudioVolumeChangeDispatcher avc);
+
+    @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED")
+    oneway void unregisterAudioVolumeCallback(IAudioVolumeChangeDispatcher avc);
+
     int setUidDeviceAffinity(in IAudioPolicyCallback pcb, in int uid, in int[] deviceTypes,
              in String[] deviceAddresses);
 
diff --git a/media/java/android/media/audiopolicy/AudioVolumeGroupChangeHandler.java b/media/java/android/media/audiopolicy/AudioVolumeGroupChangeHandler.java
deleted file mode 100644
index 022cfee..0000000
--- a/media/java/android/media/audiopolicy/AudioVolumeGroupChangeHandler.java
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.media.audiopolicy;
-
-import android.annotation.NonNull;
-import android.media.AudioManager;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Message;
-
-import com.android.internal.util.Preconditions;
-
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-
-/**
- * The AudioVolumeGroupChangeHandler handles AudioManager.OnAudioVolumeGroupChangedListener
- * callbacks posted from JNI
- *
- * TODO: Make use of Executor of callbacks.
- * @hide
- */
-public class AudioVolumeGroupChangeHandler {
-    private Handler mHandler;
-    private HandlerThread mHandlerThread;
-    private final ArrayList<AudioManager.VolumeGroupCallback> mListeners =
-            new ArrayList<AudioManager.VolumeGroupCallback>();
-
-    private static final String TAG = "AudioVolumeGroupChangeHandler";
-
-    private static final int AUDIOVOLUMEGROUP_EVENT_VOLUME_CHANGED = 1000;
-    private static final int AUDIOVOLUMEGROUP_EVENT_NEW_LISTENER = 4;
-
-    /**
-     * Accessed by native methods: JNI Callback context.
-     */
-    @SuppressWarnings("unused")
-    private long mJniCallback;
-
-    /**
-     * Initialization
-     */
-    public void init() {
-        synchronized (this) {
-            if (mHandler != null) {
-                return;
-            }
-            // create a new thread for our new event handler
-            mHandlerThread = new HandlerThread(TAG);
-            mHandlerThread.start();
-
-            if (mHandlerThread.getLooper() == null) {
-                mHandler = null;
-                return;
-            }
-            mHandler = new Handler(mHandlerThread.getLooper()) {
-                @Override
-                public void handleMessage(Message msg) {
-                    ArrayList<AudioManager.VolumeGroupCallback> listeners;
-                    synchronized (this) {
-                        if (msg.what == AUDIOVOLUMEGROUP_EVENT_NEW_LISTENER) {
-                            listeners =
-                                    new ArrayList<AudioManager.VolumeGroupCallback>();
-                            if (mListeners.contains(msg.obj)) {
-                                listeners.add(
-                                        (AudioManager.VolumeGroupCallback) msg.obj);
-                            }
-                        } else {
-                            listeners = (ArrayList<AudioManager.VolumeGroupCallback>)
-                                    mListeners.clone();
-                        }
-                    }
-                    if (listeners.isEmpty()) {
-                        return;
-                    }
-
-                    switch (msg.what) {
-                        case AUDIOVOLUMEGROUP_EVENT_VOLUME_CHANGED:
-                            for (int i = 0; i < listeners.size(); i++) {
-                                listeners.get(i).onAudioVolumeGroupChanged((int) msg.arg1,
-                                                                           (int) msg.arg2);
-                            }
-                            break;
-
-                        default:
-                            break;
-                    }
-                }
-            };
-            native_setup(new WeakReference<AudioVolumeGroupChangeHandler>(this));
-        }
-    }
-
-    private native void native_setup(Object moduleThis);
-
-    @Override
-    protected void finalize() {
-        native_finalize();
-        if (mHandlerThread.isAlive()) {
-            mHandlerThread.quit();
-        }
-    }
-    private native void native_finalize();
-
-   /**
-    * @param cb the {@link AudioManager.VolumeGroupCallback} to register
-    */
-    public void registerListener(@NonNull AudioManager.VolumeGroupCallback cb) {
-        Preconditions.checkNotNull(cb, "volume group callback shall not be null");
-        synchronized (this) {
-            mListeners.add(cb);
-        }
-        if (mHandler != null) {
-            Message m = mHandler.obtainMessage(
-                    AUDIOVOLUMEGROUP_EVENT_NEW_LISTENER, 0, 0, cb);
-            mHandler.sendMessage(m);
-        }
-    }
-
-   /**
-    * @param cb the {@link AudioManager.VolumeGroupCallback} to unregister
-    */
-    public void unregisterListener(@NonNull AudioManager.VolumeGroupCallback cb) {
-        Preconditions.checkNotNull(cb, "volume group callback shall not be null");
-        synchronized (this) {
-            mListeners.remove(cb);
-        }
-    }
-
-    Handler handler() {
-        return mHandler;
-    }
-
-    @SuppressWarnings("unused")
-    private static void postEventFromNative(Object moduleRef,
-                                            int what, int arg1, int arg2, Object obj) {
-        AudioVolumeGroupChangeHandler eventHandler =
-                (AudioVolumeGroupChangeHandler) ((WeakReference) moduleRef).get();
-        if (eventHandler == null) {
-            return;
-        }
-
-        if (eventHandler != null) {
-            Handler handler = eventHandler.handler();
-            if (handler != null) {
-                Message m = handler.obtainMessage(what, arg1, arg2, obj);
-                // Do not remove previous messages, as we would lose notification of group changes
-                handler.sendMessage(m);
-            }
-        }
-    }
-}
diff --git a/media/java/android/media/audiopolicy/IAudioVolumeChangeDispatcher.aidl b/media/java/android/media/audiopolicy/IAudioVolumeChangeDispatcher.aidl
new file mode 100644
index 0000000..e6f9024
--- /dev/null
+++ b/media/java/android/media/audiopolicy/IAudioVolumeChangeDispatcher.aidl
@@ -0,0 +1,31 @@
+/* Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.audiopolicy;
+
+/**
+ * AIDL for the AudioService to signal audio volume groups changes
+ *
+ * {@hide}
+ */
+oneway interface IAudioVolumeChangeDispatcher {
+
+    /**
+     * Called when a volume group has been changed
+     * @param group id of the volume group that has changed.
+     * @param flags one or more flags to describe the volume change.
+     */
+    void onAudioVolumeGroupChanged(int group, int flags);
+}
diff --git a/media/java/android/media/quality/MediaQualityContract.java b/media/java/android/media/quality/MediaQualityContract.java
index fccdba8..ece87a6 100644
--- a/media/java/android/media/quality/MediaQualityContract.java
+++ b/media/java/android/media/quality/MediaQualityContract.java
@@ -72,6 +72,43 @@
      */
     public static final String LEVEL_OFF = "level_off";
 
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @StringDef(prefix = "COLOR_TEMP", value = {
+            COLOR_TEMP_USER,
+            COLOR_TEMP_COOL,
+            COLOR_TEMP_STANDARD,
+            COLOR_TEMP_WARM,
+            COLOR_TEMP_USER_HDR10PLUS,
+            COLOR_TEMP_COOL_HDR10PLUS,
+            COLOR_TEMP_STANDARD_HDR10PLUS,
+            COLOR_TEMP_WARM_HDR10PLUS,
+            COLOR_TEMP_FMMSDR,
+            COLOR_TEMP_FMMHDR,
+    })
+    public @interface ColorTempValue {}
+
+    /** @hide */
+    public static final String COLOR_TEMP_USER = "color_temp_user";
+    /** @hide */
+    public static final String COLOR_TEMP_COOL = "color_temp_cool";
+    /** @hide */
+    public static final String COLOR_TEMP_STANDARD = "color_temp_standard";
+    /** @hide */
+    public static final String COLOR_TEMP_WARM = "color_temp_warm";
+    /** @hide */
+    public static final String COLOR_TEMP_USER_HDR10PLUS = "color_temp_user_hdr10plus";
+    /** @hide */
+    public static final String COLOR_TEMP_COOL_HDR10PLUS = "color_temp_cool_hdr10plus";
+    /** @hide */
+    public static final String COLOR_TEMP_STANDARD_HDR10PLUS = "color_temp_standard_hdr10plus";
+    /** @hide */
+    public static final String COLOR_TEMP_WARM_HDR10PLUS = "color_temp_warm_hdr10plus";
+    /** @hide */
+    public static final String COLOR_TEMP_FMMSDR = "color_temp_fmmsdr";
+    /** @hide */
+    public static final String COLOR_TEMP_FMMHDR = "color_temp_fmmhdr";
+
 
     /**
      * @hide
@@ -82,7 +119,6 @@
         String PARAMETER_NAME = "_name";
         String PARAMETER_PACKAGE = "_package";
         String PARAMETER_INPUT_ID = "_input_id";
-        String VENDOR_PARAMETERS = "_vendor_parameters";
     }
 
     /**
diff --git a/media/tests/AudioPolicyTest/AndroidManifest.xml b/media/tests/AudioPolicyTest/AndroidManifest.xml
index 5c911b1..466da7e 100644
--- a/media/tests/AudioPolicyTest/AndroidManifest.xml
+++ b/media/tests/AudioPolicyTest/AndroidManifest.xml
@@ -19,6 +19,7 @@
 
     <uses-permission android:name="android.permission.RECORD_AUDIO" />
     <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
+    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED" />
     <uses-permission android:name="android.permission.MODIFY_AUDIO_ROUTING" />
     <uses-permission android:name="android.permission.CHANGE_ACCESSIBILITY_VOLUME" />
 
diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeGroupChangeHandlerTest.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeGroupChangeHandlerTest.java
deleted file mode 100644
index 82394a2..0000000
--- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeGroupChangeHandlerTest.java
+++ /dev/null
@@ -1,211 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.audiopolicytest;
-
-import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
-
-import static com.android.audiopolicytest.AudioVolumeTestUtil.DEFAULT_ATTRIBUTES;
-import static com.android.audiopolicytest.AudioVolumeTestUtil.incrementVolumeIndex;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertThrows;
-import static org.junit.Assert.assertTrue;
-
-import android.media.AudioAttributes;
-import android.media.AudioManager;
-import android.media.audiopolicy.AudioVolumeGroup;
-import android.media.audiopolicy.AudioVolumeGroupChangeHandler;
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.ArrayList;
-import java.util.List;
-
-@Presubmit
-@RunWith(AndroidJUnit4.class)
-public class AudioVolumeGroupChangeHandlerTest {
-    private static final String TAG = "AudioVolumeGroupChangeHandlerTest";
-
-    @Rule
-    public final AudioVolumesTestRule rule = new AudioVolumesTestRule();
-
-    private AudioManager mAudioManager;
-
-    @Before
-    public void setUp() {
-        mAudioManager = getApplicationContext().getSystemService(AudioManager.class);
-    }
-
-    @Test
-    public void testRegisterInvalidCallback() {
-        final AudioVolumeGroupChangeHandler audioAudioVolumeGroupChangedHandler =
-                new AudioVolumeGroupChangeHandler();
-
-        audioAudioVolumeGroupChangedHandler.init();
-
-        assertThrows(NullPointerException.class, () -> {
-            AudioManager.VolumeGroupCallback nullCb = null;
-            audioAudioVolumeGroupChangedHandler.registerListener(nullCb);
-        });
-    }
-
-    @Test
-    public void testUnregisterInvalidCallback() {
-        final AudioVolumeGroupChangeHandler audioAudioVolumeGroupChangedHandler =
-                new AudioVolumeGroupChangeHandler();
-
-        audioAudioVolumeGroupChangedHandler.init();
-
-        final AudioVolumeGroupCallbackHelper cb = new AudioVolumeGroupCallbackHelper();
-        audioAudioVolumeGroupChangedHandler.registerListener(cb);
-
-        assertThrows(NullPointerException.class, () -> {
-            AudioManager.VolumeGroupCallback nullCb = null;
-            audioAudioVolumeGroupChangedHandler.unregisterListener(nullCb);
-        });
-        audioAudioVolumeGroupChangedHandler.unregisterListener(cb);
-    }
-
-    @Test
-    public void testRegisterUnregisterCallback() {
-        final AudioVolumeGroupChangeHandler audioAudioVolumeGroupChangedHandler =
-                new AudioVolumeGroupChangeHandler();
-
-        audioAudioVolumeGroupChangedHandler.init();
-        final AudioVolumeGroupCallbackHelper validCb = new AudioVolumeGroupCallbackHelper();
-
-        // Should not assert, otherwise test will fail
-        audioAudioVolumeGroupChangedHandler.registerListener(validCb);
-
-        // Should not assert, otherwise test will fail
-        audioAudioVolumeGroupChangedHandler.unregisterListener(validCb);
-    }
-
-    @Test
-    public void testCallbackReceived() {
-        final AudioVolumeGroupChangeHandler audioAudioVolumeGroupChangedHandler =
-                new AudioVolumeGroupChangeHandler();
-
-        audioAudioVolumeGroupChangedHandler.init();
-
-        final AudioVolumeGroupCallbackHelper validCb = new AudioVolumeGroupCallbackHelper();
-        audioAudioVolumeGroupChangedHandler.registerListener(validCb);
-
-        List<AudioVolumeGroup> audioVolumeGroups = mAudioManager.getAudioVolumeGroups();
-        assertTrue(audioVolumeGroups.size() > 0);
-
-        try {
-            for (final AudioVolumeGroup audioVolumeGroup : audioVolumeGroups) {
-                int volumeGroupId = audioVolumeGroup.getId();
-
-                List<AudioAttributes> avgAttributes = audioVolumeGroup.getAudioAttributes();
-                // Set the volume per attributes (if valid) and wait the callback
-                if (avgAttributes.size() == 0 || avgAttributes.get(0).equals(DEFAULT_ATTRIBUTES)) {
-                    // Some volume groups may not have valid attributes, used for internal
-                    // volume management like patch/rerouting
-                    // so bailing out strategy retrieval from attributes
-                    continue;
-                }
-                final AudioAttributes aa = avgAttributes.get(0);
-
-                int index = mAudioManager.getVolumeIndexForAttributes(aa);
-                int indexMax = mAudioManager.getMaxVolumeIndexForAttributes(aa);
-                int indexMin = mAudioManager.getMinVolumeIndexForAttributes(aa);
-
-                final int indexForAa = incrementVolumeIndex(index, indexMin, indexMax);
-
-                // Set the receiver to filter only the current group callback
-                validCb.setExpectedVolumeGroup(volumeGroupId);
-                mAudioManager.setVolumeIndexForAttributes(aa, indexForAa, 0/*flags*/);
-                assertTrue(validCb.waitForExpectedVolumeGroupChanged(
-                        AudioVolumeGroupCallbackHelper.ASYNC_TIMEOUT_MS));
-
-                final int readIndex = mAudioManager.getVolumeIndexForAttributes(aa);
-                assertEquals(readIndex, indexForAa);
-            }
-        } finally {
-            audioAudioVolumeGroupChangedHandler.unregisterListener(validCb);
-        }
-    }
-
-    @Test
-    public void testMultipleCallbackReceived() {
-
-        final AudioVolumeGroupChangeHandler audioAudioVolumeGroupChangedHandler =
-                new AudioVolumeGroupChangeHandler();
-
-        audioAudioVolumeGroupChangedHandler.init();
-
-        final int callbackCount = 10;
-        final List<AudioVolumeGroupCallbackHelper> validCbs =
-                new ArrayList<AudioVolumeGroupCallbackHelper>();
-        for (int i = 0; i < callbackCount; i++) {
-            validCbs.add(new AudioVolumeGroupCallbackHelper());
-        }
-        for (final AudioVolumeGroupCallbackHelper cb : validCbs) {
-            audioAudioVolumeGroupChangedHandler.registerListener(cb);
-        }
-
-        List<AudioVolumeGroup> audioVolumeGroups = mAudioManager.getAudioVolumeGroups();
-        assertTrue(audioVolumeGroups.size() > 0);
-
-        try {
-            for (final AudioVolumeGroup audioVolumeGroup : audioVolumeGroups) {
-                int volumeGroupId = audioVolumeGroup.getId();
-
-                List<AudioAttributes> avgAttributes = audioVolumeGroup.getAudioAttributes();
-                // Set the volume per attributes (if valid) and wait the callback
-                if (avgAttributes.size() == 0 || avgAttributes.get(0).equals(DEFAULT_ATTRIBUTES)) {
-                    // Some volume groups may not have valid attributes, used for internal
-                    // volume management like patch/rerouting
-                    // so bailing out strategy retrieval from attributes
-                    continue;
-                }
-                AudioAttributes aa = avgAttributes.get(0);
-
-                int index = mAudioManager.getVolumeIndexForAttributes(aa);
-                int indexMax = mAudioManager.getMaxVolumeIndexForAttributes(aa);
-                int indexMin = mAudioManager.getMinVolumeIndexForAttributes(aa);
-
-                final int indexForAa = incrementVolumeIndex(index, indexMin, indexMax);
-
-                // Set the receiver to filter only the current group callback
-                for (final AudioVolumeGroupCallbackHelper cb : validCbs) {
-                    cb.setExpectedVolumeGroup(volumeGroupId);
-                }
-                mAudioManager.setVolumeIndexForAttributes(aa, indexForAa, 0/*flags*/);
-
-                for (final AudioVolumeGroupCallbackHelper cb : validCbs) {
-                    assertTrue(cb.waitForExpectedVolumeGroupChanged(
-                            AudioVolumeGroupCallbackHelper.ASYNC_TIMEOUT_MS));
-                }
-                int readIndex = mAudioManager.getVolumeIndexForAttributes(aa);
-                assertEquals(readIndex, indexForAa);
-            }
-        } finally {
-            for (final AudioVolumeGroupCallbackHelper cb : validCbs) {
-                audioAudioVolumeGroupChangedHandler.unregisterListener(cb);
-            }
-        }
-    }
-}
diff --git a/media/tests/projection/Android.bp b/media/tests/projection/Android.bp
index 94db2c0..48621e4 100644
--- a/media/tests/projection/Android.bp
+++ b/media/tests/projection/Android.bp
@@ -16,7 +16,6 @@
     name: "MediaProjectionTests",
 
     srcs: ["**/*.java"],
-
     libs: [
         "android.test.base.stubs.system",
         "android.test.mock.stubs.system",
@@ -30,6 +29,7 @@
         "frameworks-base-testutils",
         "mockito-target-extended-minus-junit4",
         "platform-test-annotations",
+        "cts-mediaprojection-common",
         "testng",
         "testables",
         "truth",
@@ -42,7 +42,11 @@
         "libstaticjvmtiagent",
     ],
 
-    test_suites: ["device-tests"],
+    data: [
+        ":CtsMediaProjectionTestCasesHelperApp",
+    ],
+
+    test_suites: ["general-tests"],
 
     platform_apis: true,
 
diff --git a/media/tests/projection/AndroidManifest.xml b/media/tests/projection/AndroidManifest.xml
index 0c97604..514fb5f 100644
--- a/media/tests/projection/AndroidManifest.xml
+++ b/media/tests/projection/AndroidManifest.xml
@@ -20,6 +20,8 @@
           android:sharedUserId="com.android.uid.test">
     <uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG" />
     <uses-permission android:name="android.permission.MANAGE_MEDIA_PROJECTION" />
+    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
+    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
 
     <application android:debuggable="true"
                  android:testOnly="true">
diff --git a/media/tests/projection/AndroidTest.xml b/media/tests/projection/AndroidTest.xml
index f64930a..99b42d1 100644
--- a/media/tests/projection/AndroidTest.xml
+++ b/media/tests/projection/AndroidTest.xml
@@ -22,6 +22,15 @@
         <option name="install-arg" value="-t" />
         <option name="test-file-name" value="MediaProjectionTests.apk" />
     </target_preparer>
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="force-install-mode" value="FULL"/>ss
+        <option name="test-file-name" value="CtsMediaProjectionTestCasesHelperApp.apk" />
+    </target_preparer>
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+        <option name="run-command" value="input keyevent KEYCODE_WAKEUP" />
+        <option name="run-command" value="wm dismiss-keyguard" />
+    </target_preparer>
 
     <option name="test-tag" value="MediaProjectionTests" />
     <test class="com.android.tradefed.testtype.AndroidJUnitTest">
diff --git a/media/tests/projection/src/android/media/projection/MediaProjectionStoppingTest.java b/media/tests/projection/src/android/media/projection/MediaProjectionStoppingTest.java
new file mode 100644
index 0000000..0b84e01
--- /dev/null
+++ b/media/tests/projection/src/android/media/projection/MediaProjectionStoppingTest.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.media.projection;
+
+import static com.android.compatibility.common.util.FeatureUtil.isAutomotive;
+import static com.android.compatibility.common.util.FeatureUtil.isTV;
+import static com.android.compatibility.common.util.FeatureUtil.isWatch;
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
+
+import android.Manifest;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.media.cts.MediaProjectionRule;
+import android.os.UserHandle;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.telecom.TelecomManager;
+import android.telephony.TelephonyCallback;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.Until;
+
+import com.android.compatibility.common.util.ApiTest;
+import com.android.compatibility.common.util.FrameworkSpecificTest;
+import com.android.media.projection.flags.Flags;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Test {@link MediaProjection} stopping behavior.
+ *
+ * Run with:
+ * atest MediaProjectionTests:MediaProjectionStoppingTest
+ */
+@FrameworkSpecificTest
+public class MediaProjectionStoppingTest {
+    private static final String TAG = "MediaProjectionStoppingTest";
+    private static final int STOP_DIALOG_WAIT_TIMEOUT_MS = 5000;
+    private static final String CALL_HELPER_START_CALL = "start_call";
+    private static final String CALL_HELPER_STOP_CALL = "stop_call";
+    private static final String STOP_DIALOG_TITLE_RES_ID = "android:id/alertTitle";
+    private static final String STOP_DIALOG_CLOSE_BUTTON_RES_ID = "android:id/button2";
+
+    @Rule public MediaProjectionRule mMediaProjectionRule = new MediaProjectionRule();
+
+    private Context mContext;
+    private int mTimeoutMs;
+    private TelecomManager mTelecomManager;
+    private TelephonyManager mTelephonyManager;
+    private TestCallStateListener mTestCallStateListener;
+
+    @Before
+    public void setUp() throws InterruptedException {
+        mContext = InstrumentationRegistry.getInstrumentation().getContext();
+        runWithShellPermissionIdentity(
+                () -> {
+                    mContext.getPackageManager()
+                            .revokeRuntimePermission(
+                                    mContext.getPackageName(),
+                                    Manifest.permission.SYSTEM_ALERT_WINDOW,
+                                    new UserHandle(mContext.getUserId()));
+                });
+        mTimeoutMs = 1000;
+
+        mTestCallStateListener = new TestCallStateListener(mContext);
+    }
+
+    @After
+    public void cleanup() {
+        mTestCallStateListener.release();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_STOP_MEDIA_PROJECTION_ON_CALL_END)
+    @ApiTest(apis = "android.media.projection.MediaProjection.Callback#onStop")
+    public void testMediaProjectionStop_callStartedAfterMediaProjection_doesNotStop()
+            throws Exception {
+        assumeTrue(mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELECOM));
+
+        mMediaProjectionRule.startMediaProjection();
+
+        CountDownLatch latch = new CountDownLatch(1);
+        mMediaProjectionRule.registerCallback(
+                new MediaProjection.Callback() {
+                    @Override
+                    public void onStop() {
+                        latch.countDown();
+                    }
+                });
+        mMediaProjectionRule.createVirtualDisplay();
+
+        try {
+            startPhoneCall();
+        } finally {
+            endPhoneCall();
+        }
+
+        assertWithMessage("MediaProjection should not be stopped on call end")
+                .that(latch.await(mTimeoutMs, TimeUnit.MILLISECONDS)).isFalse();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_STOP_MEDIA_PROJECTION_ON_CALL_END)
+    @RequiresFlagsDisabled(Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END)
+    @ApiTest(apis = "android.media.projection.MediaProjection.Callback#onStop")
+    public void
+    testMediaProjectionStop_callStartedBeforeMediaProjection_stopDialogFlagDisabled__shouldStop()
+            throws Exception {
+        assumeTrue(mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELECOM));
+        CountDownLatch latch = new CountDownLatch(1);
+        try {
+            startPhoneCall();
+
+            mMediaProjectionRule.startMediaProjection();
+
+            mMediaProjectionRule.registerCallback(
+                    new MediaProjection.Callback() {
+                        @Override
+                        public void onStop() {
+                            latch.countDown();
+                        }
+                    });
+            mMediaProjectionRule.createVirtualDisplay();
+
+        } finally {
+            endPhoneCall();
+        }
+
+        assertWithMessage("MediaProjection was not stopped after call end")
+                .that(latch.await(mTimeoutMs, TimeUnit.MILLISECONDS)).isTrue();
+    }
+
+    @Test
+    @RequiresFlagsEnabled({
+            Flags.FLAG_STOP_MEDIA_PROJECTION_ON_CALL_END,
+            Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END
+    })
+    public void
+    callEnds_mediaProjectionStartedDuringCallAndIsActive_stopDialogFlagEnabled_showsStopDialog()
+            throws Exception {
+        // MediaProjection stop Dialog is only available on phones.
+        assumeFalse(isWatch());
+        assumeFalse(isAutomotive());
+        assumeFalse(isTV());
+
+        assumeTrue(mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELECOM));
+
+        try {
+            startPhoneCall();
+            mMediaProjectionRule.startMediaProjection();
+
+            mMediaProjectionRule.registerCallback(
+                    new MediaProjection.Callback() {
+                        @Override
+                        public void onStop() {
+                            fail(
+                                    "MediaProjection should not be stopped when"
+                                            + " FLAG_SHOW_STOP_DIALOG_POST_CALL_END is enabled");
+                        }
+                    });
+            mMediaProjectionRule.createVirtualDisplay();
+
+        } finally {
+            endPhoneCall();
+        }
+
+        UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        boolean isDialogShown =
+                device.wait(
+                        Until.hasObject(By.res(STOP_DIALOG_TITLE_RES_ID)),
+                        STOP_DIALOG_WAIT_TIMEOUT_MS);
+        assertWithMessage("Stop dialog should be visible").that(isDialogShown).isTrue();
+
+        // Find and click the "Close" button
+        boolean hasCloseButton =
+                device.wait(
+                        Until.hasObject(By.res(STOP_DIALOG_CLOSE_BUTTON_RES_ID)),
+                        STOP_DIALOG_WAIT_TIMEOUT_MS);
+        if (hasCloseButton) {
+            device.findObject(By.res(STOP_DIALOG_CLOSE_BUTTON_RES_ID)).click();
+            Log.d(TAG, "Clicked on 'Close' button to dismiss the stop dialog.");
+        } else {
+            fail("Close button not found, unable to dismiss stop dialog.");
+        }
+    }
+
+    private void startPhoneCall() throws InterruptedException {
+        mTestCallStateListener.assertCallState(false);
+        mContext.startActivity(getCallHelperIntent(CALL_HELPER_START_CALL));
+        mTestCallStateListener.waitForNextCallState(true, mTimeoutMs, TimeUnit.MILLISECONDS);
+    }
+
+    private void endPhoneCall() throws InterruptedException {
+        mTestCallStateListener.assertCallState(true);
+        mContext.startActivity(getCallHelperIntent(CALL_HELPER_STOP_CALL));
+        mTestCallStateListener.waitForNextCallState(false, mTimeoutMs, TimeUnit.MILLISECONDS);
+    }
+
+    private Intent getCallHelperIntent(String action) {
+        return new Intent(action)
+                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK)
+                .setComponent(
+                        new ComponentName(
+                                "android.media.projection.cts.helper",
+                                "android.media.projection.cts.helper.CallHelperActivity"));
+    }
+
+    private static final class TestCallStateListener extends TelephonyCallback
+            implements TelephonyCallback.CallStateListener {
+        private final BlockingQueue<Boolean> mCallStates = new LinkedBlockingQueue<>();
+        private final TelecomManager mTelecomManager;
+        private final TelephonyManager mTelephonyManager;
+
+        private TestCallStateListener(Context context) throws InterruptedException {
+            mTelecomManager = context.getSystemService(TelecomManager.class);
+            mTelephonyManager = context.getSystemService(TelephonyManager.class);
+            mCallStates.offer(isInCall());
+
+            assertThat(mCallStates.take()).isFalse();
+
+            runWithShellPermissionIdentity(
+                    () ->
+                            mTelephonyManager.registerTelephonyCallback(
+                                    context.getMainExecutor(), this));
+        }
+
+        public void release() {
+            runWithShellPermissionIdentity(
+                    () -> mTelephonyManager.unregisterTelephonyCallback(this));
+        }
+
+        @Override
+        public void onCallStateChanged(int state) {
+            mCallStates.offer(isInCall());
+        }
+
+        public void waitForNextCallState(boolean expectedCallState, long timeout, TimeUnit unit)
+                throws InterruptedException {
+            String message =
+                    String.format(
+                            "Call was not %s after timeout",
+                            expectedCallState ? "started" : "ended");
+
+            boolean value;
+            do {
+                value = mCallStates.poll(timeout, unit);
+            } while (value != expectedCallState);
+            assertWithMessage(message).that(value).isEqualTo(expectedCallState);
+        }
+
+        private boolean isInCall() {
+            return runWithShellPermissionIdentity(mTelecomManager::isInCall);
+        }
+
+        public void assertCallState(boolean expected) {
+            assertWithMessage("Unexpected call state").that(isInCall()).isEqualTo(expected);
+        }
+    }
+}
diff --git a/packages/CredentialManager/wear/AndroidManifest.xml b/packages/CredentialManager/wear/AndroidManifest.xml
index b480ac3..c91bf13 100644
--- a/packages/CredentialManager/wear/AndroidManifest.xml
+++ b/packages/CredentialManager/wear/AndroidManifest.xml
@@ -32,7 +32,8 @@
         android:dataExtractionRules="@xml/data_extraction_rules"
         android:fullBackupContent="@xml/backup_rules"
         android:label="@string/app_name"
-        android:supportsRtl="true">
+        android:supportsRtl="true"
+        android:theme="@style/Theme.CredentialSelector">
 
         <!-- Activity called by GMS has to be exactly:
         com.android.credentialmanager.CredentialSelectorActivity -->
@@ -42,7 +43,8 @@
             android:exported="true"
             android:label="@string/app_name"
             android:launchMode="singleTop"
-            android:permission="android.permission.LAUNCH_CREDENTIAL_SELECTOR" />
+            android:permission="android.permission.LAUNCH_CREDENTIAL_SELECTOR"
+            android:theme="@style/Theme.CredentialSelector"/>
     </application>
 
 </manifest>
diff --git a/packages/CredentialManager/wear/res/values/themes.xml b/packages/CredentialManager/wear/res/values/themes.xml
new file mode 100644
index 0000000..22329e9f
--- /dev/null
+++ b/packages/CredentialManager/wear/res/values/themes.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources>
+  <style name="Theme.CredentialSelector" parent="@*android:style/ThemeOverlay.DeviceDefault.Accent.DayNight">
+    <item name="android:windowContentOverlay">@null</item>
+    <item name="android:windowNoTitle">true</item>
+    <item name="android:windowBackground">@android:color/transparent</item>
+    <item name="android:windowIsTranslucent">true</item>
+  </style>
+</resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsPreferenceGroupAdapter.kt b/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsPreferenceGroupAdapter.kt
index 2672787..d1c88de 100644
--- a/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsPreferenceGroupAdapter.kt
+++ b/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsPreferenceGroupAdapter.kt
@@ -71,18 +71,27 @@
     override fun onPreferenceHierarchyChange(preference: Preference) {
         super.onPreferenceHierarchyChange(preference)
 
-        // Post after super class has posted their sync runnable to update preferences.
-        mHandler.removeCallbacks(syncRunnable)
-        mHandler.post(syncRunnable)
+        if (SettingsThemeHelper.isExpressiveTheme(preference.context)) {
+            // Post after super class has posted their sync runnable to update preferences.
+            mHandler.removeCallbacks(syncRunnable)
+            mHandler.post(syncRunnable)
+        }
     }
 
     @SuppressLint("RestrictedApi")
     override fun onBindViewHolder(holder: PreferenceViewHolder, position: Int) {
         super.onBindViewHolder(holder, position)
-        updateBackground(holder, position)
+
+        if (SettingsThemeHelper.isExpressiveTheme(holder.itemView.context)) {
+            updateBackground(holder, position)
+        }
     }
 
     private fun updatePreferencesList() {
+        if (!SettingsThemeHelper.isExpressiveTheme(mPreferenceGroup.context)) {
+            return
+        }
+
         val oldList = ArrayList(mRoundCornerMappingList)
         mRoundCornerMappingList = ArrayList()
         mappingPreferenceGroup(mRoundCornerMappingList, mPreferenceGroup)
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt
index f10f96a..395328f 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt
@@ -78,6 +78,8 @@
 /**
  * A container that is used to group similar items. A [Category] displays a [CategoryTitle] and
  * visually separates groups of items.
+ *
+ * @param content The content of the category.
  */
 @Composable
 fun Category(
@@ -126,7 +128,8 @@
  *   be decided by the index.
  * @param bottomPadding Optional. Bottom outside padding of the category.
  * @param state Optional. State of LazyList.
- * @param content Optional. Content to be shown at the top of the category.
+ * @param footer Optional. Content to be shown at the bottom of the category.
+ * @param header Optional. Content to be shown at the top of the category.
  */
 @Composable
 fun LazyCategory(
@@ -136,7 +139,8 @@
     title: ((Int) -> String?)? = null,
     bottomPadding: Dp = SettingsDimension.paddingSmall,
     state: LazyListState = rememberLazyListState(),
-    content: @Composable () -> Unit,
+    footer: @Composable () -> Unit = {},
+    header: @Composable () -> Unit,
 ) {
     Column(
         Modifier.padding(
@@ -154,12 +158,14 @@
             verticalArrangement = Arrangement.spacedBy(SettingsDimension.paddingTiny),
             state = state,
         ) {
-            item { CompositionLocalProvider(LocalIsInCategory provides true) { content() } }
+            item { CompositionLocalProvider(LocalIsInCategory provides true) { header() } }
 
             items(count = list.size, key = key) {
                 title?.invoke(it)?.let { title -> CategoryTitle(title) }
                 CompositionLocalProvider(LocalIsInCategory provides true) { entry(it)() }
             }
+
+            item { CompositionLocalProvider(LocalIsInCategory provides true) { footer() } }
         }
     }
 }
@@ -189,3 +195,28 @@
         }
     }
 }
+
+@Preview
+@Composable
+private fun LazyCategoryPreview() {
+    SettingsTheme {
+        LazyCategory(
+            list = listOf(1, 2, 3),
+            entry = { key ->
+                @Composable {
+                    Preference(
+                        object : PreferenceModel {
+                            override val title = key.toString()
+                        }
+                    )
+                }
+            },
+            footer = @Composable {
+                Footer("Footer")
+            },
+            header = @Composable {
+                Text("Header")
+            },
+        )
+    }
+}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/CategoryTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/CategoryTest.kt
index 4b4a8c2..7d19951 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/CategoryTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/CategoryTest.kt
@@ -71,10 +71,17 @@
     }
 
     @Test
-    fun lazyCategory_content_displayed() {
+    fun lazyCategory_headerDisplayed() {
         composeTestRule.setContent { TestLazyCategory() }
 
-        composeTestRule.onNodeWithText("text").assertExists()
+        composeTestRule.onNodeWithText("Header").assertExists()
+    }
+
+    @Test
+    fun lazyCategory_footerDisplayed() {
+        composeTestRule.setContent { TestLazyCategory() }
+
+        composeTestRule.onNodeWithText("Footer").assertExists()
     }
 
     @Test
@@ -102,8 +109,8 @@
             list = list,
             entry = { index: Int -> @Composable { Preference(list[index]) } },
             title = { index: Int -> if (index == 0) "LazyCategory $index" else null },
-        ) {
-            Text("text")
-        }
+            footer = @Composable { Footer("Footer") },
+            header = @Composable { Text("Header") },
+        )
     }
 }
diff --git a/packages/SettingsLib/aconfig/settingslib.aconfig b/packages/SettingsLib/aconfig/settingslib.aconfig
index 349d13a..90e5a01 100644
--- a/packages/SettingsLib/aconfig/settingslib.aconfig
+++ b/packages/SettingsLib/aconfig/settingslib.aconfig
@@ -37,16 +37,6 @@
 }
 
 flag {
-    name: "enable_set_preferred_transport_for_le_audio_device"
-    namespace: "bluetooth"
-    description: "Enable setting preferred transport for Le Audio device"
-    bug: "330581926"
-    metadata {
-        purpose: PURPOSE_BUGFIX
-    }
-}
-
-flag {
     name: "enable_determining_advanced_details_header_with_metadata"
     namespace: "pixel_cross_device_control"
     description: "Use metadata instead of device type to determine whether a bluetooth device should use advanced details header."
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index 914d962..edec2e4 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -75,6 +75,7 @@
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.Executor;
+import java.util.stream.IntStream;
 import java.util.stream.Stream;
 
 /**
@@ -154,8 +155,8 @@
     private boolean mIsLeAudioProfileConnectedFail = false;
     private boolean mUnpairing;
     @Nullable
-    private final InputDevice mInputDevice;
-    private final boolean mIsDeviceStylus;
+    private InputDevice mInputDevice;
+    private boolean mIsDeviceStylus;
 
     // Group second device for Hearing Aid
     private CachedBluetoothDevice mSubDevice;
@@ -313,8 +314,7 @@
                         mLocalNapRoleConnected = true;
                     }
                 }
-                if (Flags.enableSetPreferredTransportForLeAudioDevice()
-                        && profile instanceof HidProfile) {
+                if (profile instanceof HidProfile) {
                     updatePreferredTransport();
                 }
             } else if (profile instanceof MapProfile
@@ -329,8 +329,7 @@
                 mLocalNapRoleConnected = false;
             }
 
-            if (Flags.enableSetPreferredTransportForLeAudioDevice()
-                    && profile instanceof LeAudioProfile) {
+            if (profile instanceof LeAudioProfile) {
                 updatePreferredTransport();
             }
 
@@ -762,11 +761,8 @@
      * {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN}
      */
     public int getMinBatteryLevelWithMemberDevices() {
-        return Stream.concat(Stream.of(this), mMemberDevices.stream())
-                .mapToInt(cachedDevice -> cachedDevice.getBatteryLevel())
-                .filter(batteryLevel -> batteryLevel > BluetoothDevice.BATTERY_LEVEL_UNKNOWN)
-                .min()
-                .orElse(BluetoothDevice.BATTERY_LEVEL_UNKNOWN);
+        return getMinBatteryLevels(Stream.concat(Stream.of(this), mMemberDevices.stream())
+                .mapToInt(CachedBluetoothDevice::getBatteryLevel));
     }
 
     /**
@@ -789,6 +785,13 @@
                 : null;
     }
 
+    private int getMinBatteryLevels(IntStream batteryLevels) {
+        return batteryLevels
+                .filter(battery -> battery > BluetoothDevice.BATTERY_LEVEL_UNKNOWN)
+                .min()
+                .orElse(BluetoothDevice.BATTERY_LEVEL_UNKNOWN);
+    }
+
     void refresh() {
         ListenableFuture<Void> future = ThreadUtils.getBackgroundExecutor().submit(() -> {
             if (BluetoothUtils.isAdvancedDetailsHeader(mDevice)) {
@@ -1674,10 +1677,8 @@
                 return null;
             } else {
                 int overallBattery =
-                        Arrays.stream(new int[]{leftBattery, rightBattery, caseBattery})
-                                .filter(battery -> battery > BluetoothDevice.BATTERY_LEVEL_UNKNOWN)
-                                .min()
-                                .orElse(BluetoothDevice.BATTERY_LEVEL_UNKNOWN);
+                        getMinBatteryLevels(
+                                Arrays.stream(new int[]{leftBattery, rightBattery, caseBattery}));
                 Log.d(TAG, "Acquired battery info from metadata for untethered device "
                         + mDevice.getAnonymizedAddress()
                         + " left earbud battery: " + leftBattery
@@ -1711,10 +1712,75 @@
 
     @Nullable
     private BatteryLevelsInfo getBatteryFromBluetoothService() {
-        // TODO(b/397847825): Implement the logic to get battery from Bluetooth service.
-        return null;
+        BatteryLevelsInfo batteryLevelsInfo;
+        if (isConnectedHearingAidDevice()) {
+            // If the device is hearing aid device, sides can be distinguished by HearingAidInfo.
+            batteryLevelsInfo = getBatteryOfHearingAidDeviceComponents();
+            if (batteryLevelsInfo != null) {
+                return batteryLevelsInfo;
+            }
+        }
+        if (isConnectedLeAudioDevice()) {
+            // If the device is LE Audio device, sides can be distinguished by LeAudioProfile.
+            batteryLevelsInfo = getBatteryOfLeAudioDeviceComponents();
+            if (batteryLevelsInfo != null) {
+                return batteryLevelsInfo;
+            }
+        }
+        int overallBattery = getMinBatteryLevelWithMemberDevices();
+        return overallBattery > BluetoothDevice.BATTERY_LEVEL_UNKNOWN
+                ? new BatteryLevelsInfo(
+                        BluetoothDevice.BATTERY_LEVEL_UNKNOWN,
+                        BluetoothDevice.BATTERY_LEVEL_UNKNOWN,
+                        BluetoothDevice.BATTERY_LEVEL_UNKNOWN,
+                        overallBattery)
+                : null;
     }
 
+    @Nullable
+    private BatteryLevelsInfo getBatteryOfHearingAidDeviceComponents() {
+        if (getDeviceSide() == HearingAidInfo.DeviceSide.SIDE_LEFT_AND_RIGHT) {
+            return new BatteryLevelsInfo(
+                    BluetoothDevice.BATTERY_LEVEL_UNKNOWN,
+                    BluetoothDevice.BATTERY_LEVEL_UNKNOWN,
+                    BluetoothDevice.BATTERY_LEVEL_UNKNOWN,
+                    mDevice.getBatteryLevel());
+        }
+
+        int leftBattery = getHearingAidSideBattery(HearingAidInfo.DeviceSide.SIDE_LEFT);
+        int rightBattery = getHearingAidSideBattery(HearingAidInfo.DeviceSide.SIDE_RIGHT);
+        int overallBattery = getMinBatteryLevels(
+                Arrays.stream(new int[]{leftBattery, rightBattery}));
+
+        Log.d(TAG, "Acquired battery info from Bluetooth service for hearing aid device "
+                + mDevice.getAnonymizedAddress()
+                + " left battery: " + leftBattery
+                + " right battery: " + rightBattery
+                + " overall battery: " + overallBattery);
+        return overallBattery > BluetoothDevice.BATTERY_LEVEL_UNKNOWN
+                ? new BatteryLevelsInfo(
+                        leftBattery,
+                        rightBattery,
+                        BluetoothDevice.BATTERY_LEVEL_UNKNOWN,
+                        overallBattery)
+                : null;
+    }
+
+    private int getHearingAidSideBattery(int side) {
+        Optional<CachedBluetoothDevice> connectedHearingAidSide = getConnectedHearingAidSide(side);
+        return connectedHearingAidSide.isPresent()
+                ? connectedHearingAidSide
+                    .map(CachedBluetoothDevice::getBatteryLevel)
+                    .filter(batteryLevel -> batteryLevel > BluetoothDevice.BATTERY_LEVEL_UNKNOWN)
+                    .orElse(BluetoothDevice.BATTERY_LEVEL_UNKNOWN)
+                : BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
+    }
+
+    @Nullable
+    private BatteryLevelsInfo getBatteryOfLeAudioDeviceComponents() {
+        // TODO(b/397847825): Implement the logic to get battery of LE audio device components.
+        return null;
+    }
     private CharSequence getTvBatterySummary(int mainBattery, int leftBattery, int rightBattery,
             int lowBatteryColorRes) {
         // Since there doesn't seem to be a way to use format strings to add the
@@ -1833,10 +1899,7 @@
 
         // Retrieve hearing aids (ASHA, HAP) individual side battery level
         if (leftBattery == BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
-            leftBattery = getConnectedHearingAidSide(HearingAidInfo.DeviceSide.SIDE_LEFT)
-                    .map(CachedBluetoothDevice::getBatteryLevel)
-                    .filter(batteryLevel -> batteryLevel > BluetoothDevice.BATTERY_LEVEL_UNKNOWN)
-                    .orElse(BluetoothDevice.BATTERY_LEVEL_UNKNOWN);
+            leftBattery = getHearingAidSideBattery(HearingAidInfo.DeviceSide.SIDE_LEFT);
         }
 
         return leftBattery;
@@ -1852,10 +1915,7 @@
 
         // Retrieve hearing aids (ASHA, HAP) individual side battery level
         if (rightBattery == BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
-            rightBattery = getConnectedHearingAidSide(HearingAidInfo.DeviceSide.SIDE_RIGHT)
-                    .map(CachedBluetoothDevice::getBatteryLevel)
-                    .filter(batteryLevel -> batteryLevel > BluetoothDevice.BATTERY_LEVEL_UNKNOWN)
-                    .orElse(BluetoothDevice.BATTERY_LEVEL_UNKNOWN);
+            rightBattery = getHearingAidSideBattery(HearingAidInfo.DeviceSide.SIDE_RIGHT);
         }
 
         return rightBattery;
@@ -2263,6 +2323,16 @@
         mBluetoothManager = bluetoothManager;
     }
 
+    @VisibleForTesting
+    void setIsDeviceStylus(Boolean isDeviceStylus) {
+        mIsDeviceStylus = isDeviceStylus;
+    }
+
+    @VisibleForTesting
+    void setInputDevice(@Nullable InputDevice inputDevice) {
+        mInputDevice = inputDevice;
+    }
+
     private boolean isAndroidAuto() {
         try {
             ParcelUuid[] uuids = mDevice.getUuids();
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt
index c4e7245..21d518a 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt
@@ -27,7 +27,6 @@
 import android.database.ContentObserver
 import android.os.Handler
 import android.provider.Settings
-import com.android.settingslib.flags.Flags
 import com.android.settingslib.notification.modes.ZenMode
 import com.android.settingslib.notification.modes.ZenModesBackend
 import java.time.Duration
@@ -35,6 +34,7 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.callbackFlow
@@ -72,7 +72,7 @@
     private val notificationManager: NotificationManager,
     private val backend: ZenModesBackend,
     private val contentResolver: ContentResolver,
-    val scope: CoroutineScope,
+    val applicationScope: CoroutineScope,
     val backgroundCoroutineContext: CoroutineContext,
     // This is nullable just to simplify testing, since SettingsLib doesn't have a good way
     // to create a fake handler.
@@ -104,7 +104,7 @@
                 awaitClose { context.unregisterReceiver(receiver) }
             }
             .flowOn(backgroundCoroutineContext)
-            .shareIn(started = SharingStarted.WhileSubscribed(), scope = scope)
+            .shareIn(started = SharingStarted.WhileSubscribed(), scope = applicationScope)
     }
 
     override val consolidatedNotificationPolicy: StateFlow<NotificationManager.Policy?> by lazy {
@@ -129,14 +129,11 @@
             .map { mapper(it) }
             .onStart { emit(mapper(null)) }
             .flowOn(backgroundCoroutineContext)
-            .stateIn(scope, SharingStarted.WhileSubscribed(), null)
+            .stateIn(applicationScope, SharingStarted.WhileSubscribed(), null)
 
     private val zenConfigChanged by lazy {
         if (android.app.Flags.modesUi()) {
             callbackFlow {
-                    // emit an initial value
-                    trySend(Unit)
-
                     val observer =
                         object : ContentObserver(backgroundHandler) {
                             override fun onChange(selfChange: Boolean) {
@@ -163,16 +160,18 @@
         }
     }
 
-    override val modes: Flow<List<ZenMode>> by lazy {
-        if (android.app.Flags.modesUi()) {
+    override val modes: StateFlow<List<ZenMode>> =
+        if (android.app.Flags.modesUi())
             zenConfigChanged
                 .map { backend.modes }
                 .distinctUntilChanged()
                 .flowOn(backgroundCoroutineContext)
-        } else {
-            flowOf(emptyList())
-        }
-    }
+                .stateIn(
+                    scope = applicationScope,
+                    started = SharingStarted.Eagerly,
+                    initialValue = backend.modes,
+                )
+        else MutableStateFlow<List<ZenMode>>(emptyList())
 
     /**
      * Gets the current list of [ZenMode] instances according to the backend.
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
index e29adc1..b4384b7 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
@@ -16,7 +16,6 @@
 package com.android.settingslib.bluetooth;
 
 import static com.android.settingslib.flags.Flags.FLAG_ENABLE_LE_AUDIO_SHARING;
-import static com.android.settingslib.flags.Flags.FLAG_ENABLE_SET_PREFERRED_TRANSPORT_FOR_LE_AUDIO_DEVICE;
 import static com.android.settingslib.flags.Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -140,7 +139,6 @@
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_TV_MEDIA_OUTPUT_DIALOG);
-        mSetFlagsRule.enableFlags(FLAG_ENABLE_SET_PREFERRED_TRANSPORT_FOR_LE_AUDIO_DEVICE);
         mSetFlagsRule.enableFlags(FLAG_ENABLE_LE_AUDIO_SHARING);
         mSetFlagsRule.enableFlags(FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI);
         mContext = RuntimeEnvironment.application;
@@ -2300,11 +2298,7 @@
                 "false".getBytes());
         when(mDevice.getMetadata(BluetoothDevice.METADATA_MAIN_BATTERY)).thenReturn(
                 MAIN_BATTERY.getBytes());
-        when(mContext.getSystemService(InputManager.class)).thenReturn(mInputManager);
-        when(mInputManager.getInputDeviceIds()).thenReturn(new int[]{TEST_DEVICE_ID});
-        when(mInputManager.getInputDeviceBluetoothAddress(TEST_DEVICE_ID)).thenReturn(
-                DEVICE_ADDRESS);
-        when(mInputManager.getInputDevice(TEST_DEVICE_ID)).thenReturn(mInputDevice);
+        mCachedDevice.setInputDevice(mInputDevice);
 
         BatteryLevelsInfo batteryLevelsInfo = mCachedDevice.getBatteryLevelsInfo();
 
@@ -2322,10 +2316,9 @@
     public void getBatteryLevelsInfo_stylusDeviceWithBattery_returnBatteryLevelsInfo() {
         when(mDevice.getMetadata(BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)).thenReturn(
                 "false".getBytes());
-        when(mDevice.getMetadata(BluetoothDevice.METADATA_DEVICE_TYPE)).thenReturn(
-                BluetoothDevice.DEVICE_TYPE_STYLUS.getBytes());
         when(mDevice.getMetadata(BluetoothDevice.METADATA_MAIN_BATTERY)).thenReturn(
                 MAIN_BATTERY.getBytes());
+        mCachedDevice.setIsDeviceStylus(true);
 
         BatteryLevelsInfo batteryLevelsInfo = mCachedDevice.getBatteryLevelsInfo();
 
@@ -2339,6 +2332,31 @@
                 Integer.parseInt(MAIN_BATTERY));
     }
 
+    @Test
+    public void getBatteryLevelsInfo_hearingAidDeviceWithBattery_returnBatteryLevelsInfo() {
+        when(mDevice.getMetadata(BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)).thenReturn(
+                "false".getBytes());
+        when(mProfileManager.getHearingAidProfile()).thenReturn(mHearingAidProfile);
+        updateProfileStatus(mHearingAidProfile, BluetoothProfile.STATE_CONNECTED);
+        mSubCachedDevice.setHearingAidInfo(getLeftAshaHearingAidInfo());
+        when(mSubCachedDevice.getBatteryLevel()).thenReturn(Integer.parseInt(TWS_BATTERY_LEFT));
+        updateSubDeviceProfileStatus(mHearingAidProfile, BluetoothProfile.STATE_CONNECTED);
+        mCachedDevice.setSubDevice(mSubCachedDevice);
+        mCachedDevice.setHearingAidInfo(getRightAshaHearingAidInfo());
+        when(mCachedDevice.getBatteryLevel()).thenReturn(Integer.parseInt(TWS_BATTERY_RIGHT));
+
+        BatteryLevelsInfo batteryLevelsInfo = mCachedDevice.getBatteryLevelsInfo();
+
+        assertThat(batteryLevelsInfo.getLeftBatteryLevel()).isEqualTo(
+                Integer.parseInt(TWS_BATTERY_LEFT));
+        assertThat(batteryLevelsInfo.getRightBatteryLevel()).isEqualTo(
+                Integer.parseInt(TWS_BATTERY_RIGHT));
+        assertThat(batteryLevelsInfo.getCaseBatteryLevel()).isEqualTo(
+                BluetoothDevice.BATTERY_LEVEL_UNKNOWN);
+        assertThat(batteryLevelsInfo.getOverallBatteryLevel()).isEqualTo(
+                Integer.parseInt(TWS_BATTERY_LEFT));
+    }
+
     private void updateProfileStatus(LocalBluetoothProfile profile, int status) {
         doReturn(status).when(profile).getConnectionStatus(mDevice);
         mCachedDevice.onProfileStateChanged(profile, status);
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt
index b364368..ec7baf6 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt
@@ -75,10 +75,14 @@
 
     private val testScope: TestScope = TestScope()
 
+    private val initialModes = listOf(TestModeBuilder().setId("Built-in").build())
+
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
 
+        `when`(zenModesBackend.modes).thenReturn(initialModes)
+
         underTest =
             ZenModeRepositoryImpl(
                 context,
@@ -151,8 +155,8 @@
     fun modesListEmitsOnSettingsChange() {
         testScope.runTest {
             val values = mutableListOf<List<ZenMode>>()
-            val modes1 = listOf(TestModeBuilder().setId("One").build())
-            `when`(zenModesBackend.modes).thenReturn(modes1)
+
+            // an initial list of modes is read when the stateflow is created
             underTest.modes.onEach { values.add(it) }.launchIn(backgroundScope)
             runCurrent()
 
@@ -172,7 +176,7 @@
             triggerZenModeSettingUpdate()
             runCurrent()
 
-            assertThat(values).containsExactly(modes1, modes2, modes3).inOrder()
+            assertThat(values).containsExactly(initialModes, modes2, modes3).inOrder()
         }
     }
 
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
index ed11e12..2273b4f 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
@@ -54,6 +54,7 @@
 import java.io.FileNotFoundException;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Set;
@@ -650,6 +651,10 @@
      *   e.g. current locale "en-US,zh-CN" and backup locale "ja-JP,zh-Hans-CN,en-US" are merged to
      *   "en-US,zh-CN,ja-JP".
      *
+     * - Same language codes and scripts are dropped.
+     *   e.g. current locale "en-US, zh-Hans-TW" and backup locale "en-UK, en-GB, zh-Hans-HK" are
+     *   merged to "en-US, zh-Hans-TW".
+     *
      * - Unsupported locales are dropped.
      *   e.g. current locale "en-US" and backup locale "ja-JP,zh-CN" but the supported locales
      *   are "en-US,zh-CN", the merged locale list is "en-US,zh-CN".
@@ -683,13 +688,23 @@
             filtered.add(locale);
         }
 
+        final HashSet<String> existingLanguageAndScript = new HashSet<>();
         for (int i = 0; i < restore.size(); i++) {
             final Locale restoredLocaleWithExtension = copyExtensionToTargetLocale(restoredLocale,
                     getFilteredLocale(restore.get(i), allLocales));
+
             if (restoredLocaleWithExtension != null) {
-                filtered.add(restoredLocaleWithExtension);
+                String language = restoredLocaleWithExtension.getLanguage();
+                String script = restoredLocaleWithExtension.getScript();
+
+                String restoredLanguageAndScript =
+                        script == null ? language : language + "-" + script;
+                if (existingLanguageAndScript.add(restoredLanguageAndScript)) {
+                    filtered.add(restoredLocaleWithExtension);
+                }
             }
         }
+
         if (filtered.size() == current.size()) {
             return current;  // Nothing added to current locale list.
         }
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java
index 40654b0..48c7785 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java
@@ -388,7 +388,11 @@
                         LocaleList.forLanguageTags("zh-Hant-TW"),  // current
                         new String[] { "fa-Arab-AF-u-nu-latn", "zh-Hant-TW" }));  // supported
 
-
+        assertEquals(LocaleList.forLanguageTags("en-US,zh-Hans-TW"),
+                SettingsHelper.resolveLocales(
+                        LocaleList.forLanguageTags("en-UK,en-GB,zh-Hans-HK"),  // restore
+                        LocaleList.forLanguageTags("en-US,zh-Hans-TW"),  // current
+                        new String[] { "en-US,zh-Hans-TW,en-UK,en-GB,zh-Hans-HK" }));  // supported
     }
 
     @Test
diff --git a/packages/SystemUI/aconfig/desktop_users_and_accounts.aconfig b/packages/SystemUI/aconfig/desktop_users_and_accounts.aconfig
new file mode 100644
index 0000000..c7e9c9f
--- /dev/null
+++ b/packages/SystemUI/aconfig/desktop_users_and_accounts.aconfig
@@ -0,0 +1,9 @@
+package: "com.android.systemui"
+container: "system"
+
+flag {
+    name: "user_switcher_add_sign_out_option"
+    namespace: "desktop_users_and_accounts"
+    description: "Add a sign out option to the user switcher menu if sign out is possible"
+    bug: "381478261"
+}
\ No newline at end of file
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 30bce35..685c689 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -289,6 +289,15 @@
   }
 }
 
+flag {
+    name: "notification_skip_silent_updates"
+    namespace: "systemui"
+    description: "Do not notify HeadsUpManager for silent updates."
+    bug: "401068530"
+    metadata {
+       purpose: PURPOSE_BUGFIX
+    }
+}
 
 flag {
     name: "scene_container"
@@ -1856,10 +1865,11 @@
 }
 
 flag {
-  name: "shade_header_fonts"
+  name: "shade_header_font_update"
   namespace: "systemui"
   description: "Updates the fonts of the shade header"
-  bug: "393609724"
+  bug: "393609960"
+  is_fixed_read_only: true
 }
 
 flag {
@@ -2012,7 +2022,14 @@
 flag {
   name: "permission_helper_ui_rich_ongoing"
   namespace: "systemui"
-  description: "[RONs] Guards inline permission helper for demoting RONs"
+  description: "[RONs] Guards inline permission helper for demoting RONs [Guts/card version]"
+  bug: "379186372"
+}
+
+flag {
+  name: "permission_helper_inline_ui_rich_ongoing"
+  namespace: "systemui"
+  description: "[RONs] Guards inline permission helper for demoting RONs [Inline version]"
   bug: "379186372"
 }
 
@@ -2138,3 +2155,12 @@
     }
 }
 
+flag {
+   name: "keyguard_wm_reorder_atms_calls"
+   namespace: "systemui"
+   description: "Calls ATMS#setLockScreenShown before default display callbacks in case they're slow"
+   bug: "399693427"
+   metadata {
+        purpose: PURPOSE_BUGFIX
+   }
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
index 5599db7..1fb7901 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
@@ -751,7 +751,8 @@
                 OriginTransition(createLongLivedRunner(controllerFactory, scope, forLaunch = true)),
                 "${cookie}_launchTransition",
             )
-        transitionRegister.register(launchFilter, launchRemoteTransition, includeTakeover = true)
+        // TODO(b/403529740): re-enable takeovers once we solve the Compose jank issues.
+        transitionRegister.register(launchFilter, launchRemoteTransition, includeTakeover = false)
 
         // Cross-task close transitions should not use this animation, so we only register it for
         // when the opening window is Launcher.
@@ -777,7 +778,8 @@
                 ),
                 "${cookie}_returnTransition",
             )
-        transitionRegister.register(returnFilter, returnRemoteTransition, includeTakeover = true)
+        // TODO(b/403529740): re-enable takeovers once we solve the Compose jank issues.
+        transitionRegister.register(returnFilter, returnRemoteTransition, includeTakeover = false)
 
         longLivedTransitions[cookie] = Pair(launchRemoteTransition, returnRemoteTransition)
     }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt
index 1f98cd8..90311ed 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt
@@ -35,7 +35,7 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.wrapContentHeight
 import androidx.compose.foundation.layout.wrapContentSize
@@ -83,10 +83,7 @@
 import kotlinx.coroutines.launch
 
 @Composable
-fun PinInputDisplay(
-    viewModel: PinBouncerViewModel,
-    modifier: Modifier = Modifier,
-) {
+fun PinInputDisplay(viewModel: PinBouncerViewModel, modifier: Modifier = Modifier) {
     val hintedPinLength: Int? by viewModel.hintedPinLength.collectAsStateWithLifecycle()
     val shapeAnimations = rememberShapeAnimations(viewModel.pinShapes)
 
@@ -173,7 +170,10 @@
     LaunchedEffect(Unit) { playAnimation = true }
 
     val dotColor = MaterialTheme.colorScheme.onSurfaceVariant
-    Row(modifier = modifier.heightIn(min = shapeAnimations.shapeSize)) {
+    Row(
+        verticalAlignment = Alignment.CenterVertically,
+        modifier = modifier.height(shapeAnimations.shapeSize),
+    ) {
         pinEntryDrawable.forEachIndexed { index, drawable ->
             // Key the loop by [index] and [drawable], so that updating a shape drawable at the same
             // index will play the new animation (by remembering a new [atEnd]).
@@ -316,17 +316,15 @@
     Box(modifier = Modifier.padding(bottom = 20.dp)) {
         // If isLockedEsim is null, then we do not show anything.
         if (isLockedEsim == true) {
-            PlatformOutlinedButton(
-                onClick = { viewModel.onDisableEsimButtonClicked() },
-            ) {
+            PlatformOutlinedButton(onClick = { viewModel.onDisableEsimButtonClicked() }) {
                 Row(
                     horizontalArrangement = Arrangement.spacedBy(10.dp),
-                    verticalAlignment = Alignment.CenterVertically
+                    verticalAlignment = Alignment.CenterVertically,
                 ) {
                     Image(
                         painter = painterResource(id = R.drawable.ic_no_sim),
                         contentDescription = null,
-                        colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface)
+                        colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface),
                     )
                     Text(
                         text = stringResource(R.string.disable_carrier_button_text),
@@ -339,15 +337,13 @@
             Image(
                 painter = painterResource(id = R.drawable.ic_lockscreen_sim),
                 contentDescription = null,
-                colorFilter = ColorFilter.tint(colorResource(id = R.color.background_protected))
+                colorFilter = ColorFilter.tint(colorResource(id = R.color.background_protected)),
             )
         }
     }
 }
 
-private class PinInputRow(
-    val shapeAnimations: ShapeAnimations,
-) {
+private class PinInputRow(val shapeAnimations: ShapeAnimations) {
     private val entries = mutableStateListOf<PinInputEntry>()
 
     @Composable
@@ -359,10 +355,11 @@
             contentAlignment = Alignment.Center,
         ) {
             Row(
-                modifier
-                    .heightIn(min = shapeAnimations.shapeSize)
-                    // Pins overflowing horizontally should still be shown as scrolling.
-                    .wrapContentSize(unbounded = true)
+                verticalAlignment = Alignment.CenterVertically,
+                modifier =
+                    Modifier.height(shapeAnimations.shapeSize)
+                        // Pins overflowing horizontally should still be shown as scrolling.
+                        .wrapContentSize(unbounded = true),
             ) {
                 entries.forEach { entry -> key(entry.digit) { entry.Content() } }
             }
@@ -439,10 +436,7 @@
     }
 }
 
-private class PinInputEntry(
-    val digit: Digit,
-    val shapeAnimations: ShapeAnimations,
-) {
+private class PinInputEntry(val digit: Digit, val shapeAnimations: ShapeAnimations) {
     private val shape = shapeAnimations.getShapeToDot(digit.sequenceNumber)
     // horizontal space occupied, used to shift contents as individual digits are animated in/out
     private val entryWidth =
@@ -474,7 +468,7 @@
     suspend fun animateRemoval() = coroutineScope {
         awaitAll(
             async { entryWidth.animateTo(0.dp, shapeAnimations.inputShiftAnimationSpec) },
-            async { shapeSize.animateTo(0.dp, shapeAnimations.deleteShapeSizeAnimationSpec) }
+            async { shapeSize.animateTo(0.dp, shapeAnimations.deleteShapeSizeAnimationSpec) },
         )
     }
 
@@ -505,7 +499,7 @@
                     layout(animatedEntryWidth.roundToPx(), shapeHeight.roundToPx()) {
                         placeable.place(
                             ((animatedEntryWidth - animatedShapeSize) / 2f).roundToPx(),
-                            ((shapeHeight - animatedShapeSize) / 2f).roundToPx()
+                            ((shapeHeight - animatedShapeSize) / 2f).roundToPx(),
                         )
                     }
                 },
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt
index 0db2bb5..5fac686 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt
@@ -33,6 +33,7 @@
 import com.android.systemui.biometrics.AuthController
 import com.android.systemui.customization.R as customR
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.flags.FeatureFlagsClassic
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.ui.binder.DeviceEntryIconViewBinder
@@ -49,12 +50,14 @@
 import com.android.systemui.statusbar.VibratorHelper
 import dagger.Lazy
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 
 class LockSection
 @Inject
 constructor(
     @Application private val applicationScope: CoroutineScope,
+    @Main private val mainDispatcher: CoroutineDispatcher,
     private val windowManager: WindowManager,
     private val authController: AuthController,
     private val featureFlags: FeatureFlagsClassic,
@@ -80,6 +83,7 @@
                         id = R.id.device_entry_icon_view
                         DeviceEntryIconViewBinder.bind(
                             applicationScope,
+                            mainDispatcher,
                             this,
                             deviceEntryIconViewModel.get(),
                             deviceEntryForegroundViewModel.get(),
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 09b8d17..3e907e8 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
@@ -574,11 +574,11 @@
         ) {
             Column(
                 modifier =
-                    Modifier.thenIf(supportNestedScrolling) {
+                    Modifier.disableSwipesWhenScrolling(NestedScrollableBound.BottomRight)
+                        .thenIf(supportNestedScrolling) {
                             Modifier.nestedScroll(scrimNestedScrollConnection)
                         }
                         .stackVerticalOverscroll(coroutineScope) { scrollState.canScrollForward }
-                        .disableSwipesWhenScrolling(NestedScrollableBound.BottomRight)
                         .verticalScroll(scrollState)
                         .padding(top = stackTopPadding, bottom = stackBottomPadding)
                         .fillMaxWidth()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
index 2845f6a..e75f607 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
@@ -507,8 +507,8 @@
                     0 /* flags */);
             users.add(new UserRecord(info, null, false /* isGuest */, false /* isCurrent */,
                     false /* isAddUser */, false /* isRestricted */, true /* isSwitchToEnabled */,
-                    false /* isAddSupervisedUser */, null /* enforcedAdmin */,
-                    false /* isManageUsers */));
+                    false /* isAddSupervisedUser */, false /* isSignOut */,
+                    null /* enforcedAdmin */, false /* isManageUsers */));
         }
         return users;
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt
index c1feca2..91ec1cb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt
@@ -77,6 +77,7 @@
     private lateinit var resources: TestableResources
     private lateinit var trustRepository: FakeTrustRepository
     private lateinit var testScope: TestScope
+    private val TEST_REASON = "reason"
 
     @Before
     fun setUp() {
@@ -118,7 +119,7 @@
             mainHandler.setMode(FakeHandler.Mode.QUEUEING)
 
             // WHEN bouncer show is requested
-            underTest.show(true)
+            underTest.show(true, TEST_REASON)
 
             // WHEN all queued messages are dispatched
             mainHandler.dispatchQueuedMessages()
@@ -134,7 +135,7 @@
 
     @Test
     fun testShow_isScrimmed() {
-        underTest.show(true)
+        underTest.show(true, TEST_REASON)
         verify(repository).setKeyguardAuthenticatedBiometrics(null)
         verify(repository).setPrimaryStartingToHide(false)
         verify(repository).setPrimaryScrimmed(true)
@@ -162,7 +163,7 @@
     @Test
     fun testShowReturnsFalseWhenDelegateIsNotSet() {
         whenever(bouncerView.delegate).thenReturn(null)
-        assertThat(underTest.show(true)).isEqualTo(false)
+        assertThat(underTest.show(true, TEST_REASON)).isEqualTo(false)
     }
 
     @Test
@@ -171,7 +172,7 @@
         whenever(keyguardSecurityModel.getSecurityMode(anyInt()))
             .thenReturn(KeyguardSecurityModel.SecurityMode.SimPuk)
 
-        underTest.show(true)
+        underTest.show(true, TEST_REASON)
         verify(repository).setPrimaryShow(false)
         verify(repository).setPrimaryShow(true)
     }
@@ -352,7 +353,7 @@
         whenever(faceAuthInteractor.canFaceAuthRun()).thenReturn(true)
 
         // WHEN bouncer show is requested
-        underTest.show(true)
+        underTest.show(true, TEST_REASON)
 
         // THEN primary show & primary showing soon aren't updated immediately
         verify(repository, never()).setPrimaryShow(true)
@@ -375,7 +376,7 @@
         whenever(faceAuthInteractor.canFaceAuthRun()).thenReturn(false)
 
         // WHEN bouncer show is requested
-        underTest.show(true)
+        underTest.show(true, TEST_REASON)
 
         // THEN primary show & primary showing soon are updated immediately
         verify(repository).setPrimaryShow(true)
@@ -394,7 +395,7 @@
             runCurrent()
 
             // WHEN bouncer show is requested
-            underTest.show(true)
+            underTest.show(true, TEST_REASON)
 
             // THEN primary show & primary showing soon were scheduled to update
             verify(repository, never()).setPrimaryShow(true)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorTest.kt
index 856a62e..95334b5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorTest.kt
@@ -25,10 +25,9 @@
 import com.android.systemui.communal.data.model.FEATURE_AUTO_OPEN
 import com.android.systemui.communal.data.model.FEATURE_MANUAL_OPEN
 import com.android.systemui.communal.data.model.SuppressionReason
-import com.android.systemui.communal.posturing.data.model.PositionState
 import com.android.systemui.communal.posturing.data.repository.fake
 import com.android.systemui.communal.posturing.data.repository.posturingRepository
-import com.android.systemui.communal.posturing.domain.interactor.advanceTimeBySlidingWindowAndRun
+import com.android.systemui.communal.posturing.shared.model.PosturedState
 import com.android.systemui.dock.DockManager
 import com.android.systemui.dock.fakeDockManager
 import com.android.systemui.kosmos.Kosmos
@@ -128,12 +127,7 @@
             )
 
             batteryRepository.fake.setDevicePluggedIn(true)
-            posturingRepository.fake.emitPositionState(
-                PositionState(
-                    stationary = PositionState.StationaryState.Stationary(confidence = 1f),
-                    orientation = PositionState.OrientationState.NotPostured(confidence = 1f),
-                )
-            )
+            posturingRepository.fake.setPosturedState(PosturedState.NotPostured)
 
             assertThat(shouldAutoOpen).isFalse()
             assertThat(suppressionReason)
@@ -141,13 +135,7 @@
                     SuppressionReason.ReasonWhenToAutoShow(FEATURE_AUTO_OPEN or FEATURE_MANUAL_OPEN)
                 )
 
-            posturingRepository.fake.emitPositionState(
-                PositionState(
-                    stationary = PositionState.StationaryState.Stationary(confidence = 1f),
-                    orientation = PositionState.OrientationState.Postured(confidence = 1f),
-                )
-            )
-            advanceTimeBySlidingWindowAndRun()
+            posturingRepository.fake.setPosturedState(PosturedState.Postured(1f))
             assertThat(shouldAutoOpen).isTrue()
             assertThat(suppressionReason).isNull()
         }
@@ -165,7 +153,7 @@
             )
 
             batteryRepository.fake.setDevicePluggedIn(true)
-            posturingRepository.fake.emitPositionState(PositionState())
+            posturingRepository.fake.setPosturedState(PosturedState.Postured(1f))
             fakeDockManager.setIsDocked(true)
 
             assertThat(shouldAutoOpen).isFalse()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractorTest.kt
index b4708d97..0df8834 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractorTest.kt
@@ -16,39 +16,22 @@
 
 package com.android.systemui.communal.posturing.domain.interactor
 
-import android.hardware.Sensor
-import android.hardware.TriggerEventListener
-import android.platform.test.annotations.EnableFlags
-import android.service.dreams.Flags.FLAG_ALLOW_DREAM_WHEN_POSTURED
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.communal.posturing.data.model.PositionState
 import com.android.systemui.communal.posturing.data.repository.fake
 import com.android.systemui.communal.posturing.data.repository.posturingRepository
 import com.android.systemui.communal.posturing.shared.model.PosturedState
-import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.advanceTimeBy
 import com.android.systemui.kosmos.collectLastValue
-import com.android.systemui.kosmos.runCurrent
 import com.android.systemui.kosmos.runTest
 import com.android.systemui.kosmos.useUnconfinedTestDispatcher
 import com.android.systemui.testKosmos
-import com.android.systemui.util.sensors.asyncSensorManager
 import com.google.common.truth.Truth.assertThat
-import kotlin.time.Duration.Companion.hours
-import kotlin.time.Duration.Companion.milliseconds
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.kotlin.any
-import org.mockito.kotlin.doAnswer
-import org.mockito.kotlin.doReturn
-import org.mockito.kotlin.mock
-import org.mockito.kotlin.stub
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
-@EnableFlags(FLAG_ALLOW_DREAM_WHEN_POSTURED)
 class PosturingInteractorTest : SysuiTestCase() {
 
     private val kosmos = testKosmos().useUnconfinedTestDispatcher()
@@ -61,220 +44,23 @@
             val postured by collectLastValue(underTest.postured)
             assertThat(postured).isFalse()
 
-            posturingRepository.fake.emitPositionState(
-                PositionState(
-                    stationary = PositionState.StationaryState.Stationary(confidence = 1f),
-                    orientation = PositionState.OrientationState.Postured(confidence = 1f),
-                )
-            )
-
-            advanceTimeBySlidingWindowAndRun()
+            posturingRepository.fake.setPosturedState(PosturedState.Postured(1f))
             assertThat(postured).isTrue()
         }
 
     @Test
-    fun testLowConfidenceOrientation() =
-        kosmos.runTest {
-            val postured by collectLastValue(underTest.postured)
-            assertThat(postured).isFalse()
-
-            posturingRepository.fake.emitPositionState(
-                PositionState(
-                    stationary = PositionState.StationaryState.Stationary(confidence = 1f),
-                    orientation = PositionState.OrientationState.Postured(confidence = 0.2f),
-                )
-            )
-
-            advanceTimeBySlidingWindowAndRun()
-            assertThat(postured).isFalse()
-        }
-
-    @Test
-    fun testLowConfidenceStationary() =
-        kosmos.runTest {
-            val postured by collectLastValue(underTest.postured)
-            assertThat(postured).isFalse()
-
-            posturingRepository.fake.emitPositionState(
-                PositionState(
-                    stationary = PositionState.StationaryState.Stationary(confidence = 1f),
-                    orientation = PositionState.OrientationState.Postured(confidence = 0.2f),
-                )
-            )
-
-            advanceTimeBySlidingWindowAndRun()
-            assertThat(postured).isFalse()
-        }
-
-    @Test
-    fun testSlidingWindow() =
-        kosmos.runTest {
-            val postured by collectLastValue(underTest.postured)
-            assertThat(postured).isFalse()
-
-            posturingRepository.fake.emitPositionState(
-                PositionState(
-                    stationary = PositionState.StationaryState.Stationary(confidence = 1f),
-                    orientation = PositionState.OrientationState.Postured(confidence = 0.2f),
-                )
-            )
-
-            advanceTimeBy(PosturingInteractor.SLIDING_WINDOW_DURATION / 2)
-            runCurrent()
-            assertThat(postured).isFalse()
-
-            posturingRepository.fake.emitPositionState(
-                PositionState(
-                    stationary = PositionState.StationaryState.Stationary(confidence = 1f),
-                    orientation = PositionState.OrientationState.Postured(confidence = 1f),
-                )
-            )
-            assertThat(postured).isFalse()
-            advanceTimeBy(PosturingInteractor.SLIDING_WINDOW_DURATION / 2)
-            runCurrent()
-
-            // The 0.2 confidence will have fallen out of the sliding window, and we should now flip
-            // to true.
-            assertThat(postured).isTrue()
-
-            advanceTimeBy(9999.hours)
-            // We should remain postured if no other updates are received.
-            assertThat(postured).isTrue()
-        }
-
-    @Test
-    fun testLiftGesture_afterSlidingWindow() =
-        kosmos.runTest {
-            val triggerSensor = stubSensorManager()
-            val sensor = asyncSensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE)!!
-
-            val postured by collectLastValue(underTest.postured)
-            assertThat(postured).isFalse()
-
-            posturingRepository.fake.emitPositionState(
-                PositionState(
-                    stationary = PositionState.StationaryState.Stationary(confidence = 1f),
-                    orientation = PositionState.OrientationState.Postured(confidence = 1f),
-                )
-            )
-
-            advanceTimeBySlidingWindowAndRun()
-            assertThat(postured).isTrue()
-
-            // If we detect a lift gesture, we should transition back to not postured.
-            triggerSensor(sensor)
-            assertThat(postured).isFalse()
-
-            advanceTimeBy(9999.hours)
-            assertThat(postured).isFalse()
-        }
-
-    @Test
-    fun testLiftGesture_overridesSlidingWindow() =
-        kosmos.runTest {
-            val triggerSensor = stubSensorManager()
-            val sensor = asyncSensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE)!!
-
-            val postured by collectLastValue(underTest.postured)
-            assertThat(postured).isFalse()
-
-            // Add multiple stationary + postured events to the sliding window.
-            repeat(100) {
-                advanceTimeBy(1.milliseconds)
-                posturingRepository.fake.emitPositionState(
-                    PositionState(
-                        stationary = PositionState.StationaryState.Stationary(confidence = 1f),
-                        orientation = PositionState.OrientationState.Postured(confidence = 1f),
-                    )
-                )
-            }
-
-            assertThat(postured).isTrue()
-
-            // If we detect a lift gesture, we should transition back to not postured immediately.
-            triggerSensor(sensor)
-            assertThat(postured).isFalse()
-        }
-
-    @Test
-    fun testSignificantMotion_afterSlidingWindow() =
-        kosmos.runTest {
-            val triggerSensor = stubSensorManager()
-            val sensor = asyncSensorManager.getDefaultSensor(Sensor.TYPE_SIGNIFICANT_MOTION)!!
-
-            val postured by collectLastValue(underTest.postured)
-            assertThat(postured).isFalse()
-
-            posturingRepository.fake.emitPositionState(
-                PositionState(
-                    stationary = PositionState.StationaryState.Stationary(confidence = 1f),
-                    orientation = PositionState.OrientationState.Postured(confidence = 1f),
-                )
-            )
-
-            advanceTimeBySlidingWindowAndRun()
-            assertThat(postured).isTrue()
-
-            // If we detect motion, we should transition back to not postured.
-            triggerSensor(sensor)
-            assertThat(postured).isFalse()
-
-            advanceTimeBy(9999.hours)
-            assertThat(postured).isFalse()
-        }
-
-    @Test
     fun testOverriddenByDebugValue() =
         kosmos.runTest {
             val postured by collectLastValue(underTest.postured)
             assertThat(postured).isFalse()
 
             underTest.setValueForDebug(PosturedState.NotPostured)
-            posturingRepository.fake.emitPositionState(
-                PositionState(
-                    stationary = PositionState.StationaryState.Stationary(confidence = 1f),
-                    orientation = PositionState.OrientationState.Postured(confidence = 1f),
-                )
-            )
+            posturingRepository.fake.setPosturedState(PosturedState.Postured(1f))
 
             // Repository value is overridden by debug value
             assertThat(postured).isFalse()
 
             underTest.setValueForDebug(PosturedState.Unknown)
-
-            advanceTimeBySlidingWindowAndRun()
             assertThat(postured).isTrue()
         }
-
-    private fun Kosmos.stubSensorManager(): (sensor: Sensor) -> Unit {
-        val callbacks = mutableMapOf<Sensor, List<TriggerEventListener>>()
-        val pickupSensor = mock<Sensor>()
-        val motionSensor = mock<Sensor>()
-
-        asyncSensorManager.stub {
-            on { getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE) } doReturn pickupSensor
-            on { getDefaultSensor(Sensor.TYPE_SIGNIFICANT_MOTION) } doReturn motionSensor
-            on { requestTriggerSensor(any(), any()) } doAnswer
-                {
-                    val callback = it.arguments[0] as TriggerEventListener
-                    val sensor = it.arguments[1] as Sensor
-                    callbacks[sensor] = callbacks.getOrElse(sensor) { emptyList() } + callback
-                    true
-                }
-            on { cancelTriggerSensor(any(), any()) } doAnswer
-                {
-                    val callback = it.arguments[0] as TriggerEventListener
-                    val sensor = it.arguments[1] as Sensor
-                    callbacks[sensor] = callbacks.getOrElse(sensor) { emptyList() } - callback
-                    true
-                }
-        }
-
-        return { sensor: Sensor ->
-            val list = callbacks.getOrElse(sensor) { emptyList() }
-            // Simulate a trigger sensor which unregisters callbacks after triggering.
-            callbacks[sensor] = emptyList()
-            list.forEach { it.onTrigger(mock()) }
-        }
-    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
index 0718d0d..83fd4c2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
@@ -264,6 +264,29 @@
         }
 
     @Test
+    fun dismissAlpha_doesNotEmitWhenNotDismissible() =
+        testScope.runTest {
+            val dismissAlpha by collectValues(underTest.dismissAlpha)
+            assertThat(dismissAlpha[0]).isEqualTo(1f)
+            assertThat(dismissAlpha.size).isEqualTo(1)
+
+            keyguardTransitionRepository.sendTransitionSteps(from = AOD, to = LOCKSCREEN, testScope)
+
+            // User begins to swipe up when not dimissible, which would show bouncer
+            repository.setStatusBarState(StatusBarState.KEYGUARD)
+            repository.setKeyguardDismissible(false)
+            shadeRepository.setLegacyShadeExpansion(0.98f)
+
+            assertThat(dismissAlpha[0]).isEqualTo(1f)
+            assertThat(dismissAlpha.size).isEqualTo(1)
+
+            // Shade reset should not affect dismiss alpha when not dismissible
+            shadeRepository.setLegacyShadeExpansion(0f)
+            assertThat(dismissAlpha[0]).isEqualTo(1f)
+            assertThat(dismissAlpha.size).isEqualTo(1)
+        }
+
+    @Test
     fun dismissAlpha_onGlanceableHub_doesNotEmitWhenShadeResets() =
         testScope.runTest {
             val dismissAlpha by collectValues(underTest.dismissAlpha)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt
index 6704d63..5826665 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt
@@ -267,12 +267,20 @@
         // action down: does NOT collapse the shade
         val actionDownMenuKeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, keycode)
         assertThat(underTest.dispatchKeyEvent(actionDownMenuKeyEvent)).isFalse()
-        verify(statusBarKeyguardViewManager, never()).showPrimaryBouncer(any())
+        verify(statusBarKeyguardViewManager, never())
+            .showPrimaryBouncer(
+                any(),
+                eq("KeyguardKeyEventInteractor#collapseShadeLockedOrShowPrimaryBouncer"),
+            )
 
         // action up: collapses the shade
         val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, keycode)
         assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isTrue()
-        verify(statusBarKeyguardViewManager).showPrimaryBouncer(eq(true))
+        verify(statusBarKeyguardViewManager)
+            .showPrimaryBouncer(
+                eq(true),
+                eq("KeyguardKeyEventInteractor#collapseShadeLockedOrShowPrimaryBouncer"),
+            )
     }
 
     private fun verifyActionsDoNothing(keycode: Int) {
@@ -280,12 +288,20 @@
         val actionDownMenuKeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, keycode)
         assertThat(underTest.dispatchKeyEvent(actionDownMenuKeyEvent)).isFalse()
         verify(shadeController, never()).animateCollapseShadeForced()
-        verify(statusBarKeyguardViewManager, never()).showPrimaryBouncer(any())
+        verify(statusBarKeyguardViewManager, never())
+            .showPrimaryBouncer(
+                any(),
+                eq("KeyguardKeyEventInteractor#collapseShadeLockedOrShowPrimaryBouncer"),
+            )
 
         // action up: doesNothing
         val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, keycode)
         assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isFalse()
         verify(shadeController, never()).animateCollapseShadeForced()
-        verify(statusBarKeyguardViewManager, never()).showPrimaryBouncer(any())
+        verify(statusBarKeyguardViewManager, never())
+            .showPrimaryBouncer(
+                any(),
+                eq("KeyguardKeyEventInteractor#collapseShadeLockedOrShowPrimaryBouncer"),
+            )
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt
index f0eedee..4f35114 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt
@@ -340,6 +340,22 @@
 
     @Test
     @EnableSceneContainer
+    fun surfaceBehindVisibility_whileSceneContainerNotVisible_alwaysTrue() =
+        testScope.runTest {
+            val isSurfaceBehindVisible by collectLastValue(underTest.value.surfaceBehindVisibility)
+            val currentScene by collectLastValue(kosmos.sceneInteractor.currentScene)
+            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+            assertThat(isSurfaceBehindVisible).isFalse()
+
+            kosmos.sceneInteractor.setVisible(false, "test")
+            runCurrent()
+
+            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+            assertThat(isSurfaceBehindVisible).isTrue()
+        }
+
+    @Test
+    @EnableSceneContainer
     fun surfaceBehindVisibility_idleWhileLocked_alwaysFalse() =
         testScope.runTest {
             val isSurfaceBehindVisible by collectLastValue(underTest.value.surfaceBehindVisibility)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt
index e1323c1..9aee4c9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt
@@ -35,6 +35,7 @@
 import com.android.systemui.statusbar.phone.statusBarKeyguardViewManager
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
 import com.google.common.collect.Range
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.runTest
@@ -56,7 +57,8 @@
     fun onTapped() =
         testScope.runTest {
             underTest.onTapped()
-            verify(statusBarKeyguardViewManager).showPrimaryBouncer(any())
+            verify(statusBarKeyguardViewManager)
+                .showPrimaryBouncer(any(), eq("AlternateBouncerViewModel#onTapped"))
         }
 
     @Test
@@ -154,7 +156,7 @@
 
     private fun stepToAlternateBouncer(
         value: Float,
-        state: TransitionState = TransitionState.RUNNING
+        state: TransitionState = TransitionState.RUNNING,
     ): TransitionStep {
         return step(
             from = KeyguardState.LOCKSCREEN,
@@ -166,7 +168,7 @@
 
     private fun stepFromAlternateBouncer(
         value: Float,
-        state: TransitionState = TransitionState.RUNNING
+        state: TransitionState = TransitionState.RUNNING,
     ): TransitionStep {
         return step(
             from = KeyguardState.ALTERNATE_BOUNCER,
@@ -180,14 +182,14 @@
         from: KeyguardState,
         to: KeyguardState,
         value: Float,
-        transitionState: TransitionState
+        transitionState: TransitionState,
     ): TransitionStep {
         return TransitionStep(
             from = from,
             to = to,
             value = value,
             transitionState = transitionState,
-            ownerName = "AlternateBouncerViewModelTest"
+            ownerName = "AlternateBouncerViewModelTest",
         )
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUiStateExtTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUiStateExtTest.kt
index d1b5529..5d4de02 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUiStateExtTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUiStateExtTest.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.model
 
-import android.view.Display
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -35,7 +34,7 @@
 
     @Test
     fun updateFlags() {
-        underTest.updateFlags(Display.DEFAULT_DISPLAY, 1L to true, 2L to false, 4L to true)
+        underTest.updateFlags(1L to true, 2L to false, 4L to true)
 
         assertThat(underTest.flags and 1L).isNotEqualTo(0L)
         assertThat(underTest.flags and 2L).isEqualTo(0L)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt
index 8b9ae9a..2dd2f7c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.qs.panels.shared.model.SizedTile
 import com.android.systemui.qs.panels.shared.model.SizedTileImpl
+import com.android.systemui.qs.panels.ui.compose.selection.PlacementEvent
 import com.android.systemui.qs.panels.ui.model.GridCell
 import com.android.systemui.qs.panels.ui.model.TileGridCell
 import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
@@ -108,6 +109,76 @@
         assertThat(underTest.tiles.toStrings()).doesNotContain(TestEditTiles[0].tile.tileSpec.spec)
     }
 
+    @Test
+    fun targetIndexForPlacementToTileSpec_returnsCorrectIndex() {
+        val placementEvent =
+            PlacementEvent.PlaceToTileSpec(
+                movingSpec = TestEditTiles[0].tile.tileSpec,
+                targetSpec = TestEditTiles[3].tile.tileSpec,
+            )
+        val index = underTest.targetIndexForPlacement(placementEvent)
+
+        assertThat(index).isEqualTo(3)
+    }
+
+    @Test
+    fun targetIndexForPlacementToIndex_indexOutOfBounds_returnsCorrectIndex() {
+        val placementEventTooLow =
+            PlacementEvent.PlaceToIndex(
+                movingSpec = TestEditTiles[0].tile.tileSpec,
+                targetIndex = -1,
+            )
+        val index1 = underTest.targetIndexForPlacement(placementEventTooLow)
+
+        assertThat(index1).isEqualTo(0)
+
+        val placementEventTooHigh =
+            PlacementEvent.PlaceToIndex(
+                movingSpec = TestEditTiles[0].tile.tileSpec,
+                targetIndex = 10,
+            )
+        val index2 = underTest.targetIndexForPlacement(placementEventTooHigh)
+        assertThat(index2).isEqualTo(TestEditTiles.size)
+    }
+
+    @Test
+    fun targetIndexForPlacementToIndex_movingBack_returnsCorrectIndex() {
+        /**
+         * With the grid: [ a ] [ b ] [ c ] [ Large D ] [ e ] [ f ]
+         *
+         * Moving 'e' to the spacer at index 3 will result in the tilespec order: a, b, c, e, d, f
+         *
+         * 'e' is now at index 3
+         */
+        val placementEvent =
+            PlacementEvent.PlaceToIndex(
+                movingSpec = TestEditTiles[4].tile.tileSpec,
+                targetIndex = 3,
+            )
+        val index = underTest.targetIndexForPlacement(placementEvent)
+
+        assertThat(index).isEqualTo(3)
+    }
+
+    @Test
+    fun targetIndexForPlacementToIndex_movingForward_returnsCorrectIndex() {
+        /**
+         * With the grid: [ a ] [ b ] [ c ] [ Large D ] [ e ] [ f ]
+         *
+         * Moving '1' to the spacer at index 3 will result in the tilespec order: b, c, a, d, e, f
+         *
+         * 'a' is now at index 2
+         */
+        val placementEvent =
+            PlacementEvent.PlaceToIndex(
+                movingSpec = TestEditTiles[0].tile.tileSpec,
+                targetIndex = 3,
+            )
+        val index = underTest.targetIndexForPlacement(placementEvent)
+
+        assertThat(index).isEqualTo(2)
+    }
+
     private fun List<GridCell>.toStrings(): List<String> {
         return map {
             if (it is TileGridCell) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt
index ab217a3..33ee337 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt
@@ -19,8 +19,14 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.qs.panels.ui.compose.selection.TileState.GreyedOut
+import com.android.systemui.qs.panels.ui.compose.selection.TileState.None
+import com.android.systemui.qs.panels.ui.compose.selection.TileState.Placeable
+import com.android.systemui.qs.panels.ui.compose.selection.TileState.Removable
+import com.android.systemui.qs.panels.ui.compose.selection.TileState.Selected
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -45,7 +51,104 @@
         assertThat(underTest.selection).isEqualTo(newSpec)
     }
 
+    @Test
+    fun placementModeEnabled_tapOnIndex_sendsCorrectPlacementEvent() {
+        // Tap while in placement mode
+        underTest.enterPlacementMode(TEST_SPEC)
+        underTest.onTap(2)
+
+        assertThat(underTest.placementEnabled).isFalse()
+        val event = underTest.placementEvent as PlacementEvent.PlaceToIndex
+        assertThat(event.movingSpec).isEqualTo(TEST_SPEC)
+        assertThat(event.targetIndex).isEqualTo(2)
+    }
+
+    @Test
+    fun placementModeDisabled_tapOnIndex_doesNotSendPlacementEvent() {
+        // Tap while placement mode is disabled
+        underTest.onTap(2)
+
+        assertThat(underTest.placementEnabled).isFalse()
+        assertThat(underTest.placementEvent).isNull()
+    }
+
+    @Test
+    fun placementModeEnabled_tapOnSelection_exitPlacementMode() {
+        // Tap while in placement mode
+        underTest.enterPlacementMode(TEST_SPEC)
+        underTest.onTap(TEST_SPEC)
+
+        assertThat(underTest.placementEnabled).isFalse()
+        assertThat(underTest.placementEvent).isNull()
+    }
+
+    @Test
+    fun placementModeEnabled_tapOnTileSpec_sendsCorrectPlacementEvent() {
+        // Tap while in placement mode
+        underTest.enterPlacementMode(TEST_SPEC)
+        underTest.onTap(TEST_SPEC_2)
+
+        assertThat(underTest.placementEnabled).isFalse()
+        val event = underTest.placementEvent as PlacementEvent.PlaceToTileSpec
+        assertThat(event.movingSpec).isEqualTo(TEST_SPEC)
+        assertThat(event.targetSpec).isEqualTo(TEST_SPEC_2)
+    }
+
+    @Test
+    fun placementModeDisabled_tapOnSelection_unselect() {
+        // Select the tile and tap on it
+        underTest.select(TEST_SPEC)
+        underTest.onTap(TEST_SPEC)
+
+        assertThat(underTest.placementEnabled).isFalse()
+        assertThat(underTest.selected).isFalse()
+    }
+
+    @Test
+    fun placementModeDisabled_tapOnTile_selects() {
+        // Select a tile but tap a second one
+        underTest.select(TEST_SPEC)
+        underTest.onTap(TEST_SPEC_2)
+
+        assertThat(underTest.placementEnabled).isFalse()
+        assertThat(underTest.selection).isEqualTo(TEST_SPEC_2)
+    }
+
+    @Test
+    fun tileStateFor_selectedTile_returnsSingleSelection() = runTest {
+        underTest.select(TEST_SPEC)
+
+        assertThat(underTest.tileStateFor(TEST_SPEC, None, canShowRemovalBadge = true))
+            .isEqualTo(Selected)
+        assertThat(underTest.tileStateFor(TEST_SPEC_2, None, canShowRemovalBadge = true))
+            .isEqualTo(Removable)
+        assertThat(underTest.tileStateFor(TEST_SPEC_3, None, canShowRemovalBadge = true))
+            .isEqualTo(Removable)
+    }
+
+    @Test
+    fun tileStateFor_placementMode_returnsSinglePlaceable() = runTest {
+        underTest.enterPlacementMode(TEST_SPEC)
+
+        assertThat(underTest.tileStateFor(TEST_SPEC, None, canShowRemovalBadge = true))
+            .isEqualTo(Placeable)
+        assertThat(underTest.tileStateFor(TEST_SPEC_2, None, canShowRemovalBadge = true))
+            .isEqualTo(GreyedOut)
+        assertThat(underTest.tileStateFor(TEST_SPEC_3, None, canShowRemovalBadge = true))
+            .isEqualTo(GreyedOut)
+    }
+
+    @Test
+    fun tileStateFor_nonRemovableTile_returnsNoneState() = runTest {
+        assertThat(underTest.tileStateFor(TEST_SPEC, None, canShowRemovalBadge = true))
+            .isEqualTo(Removable)
+        assertThat(underTest.tileStateFor(TEST_SPEC_2, None, canShowRemovalBadge = false))
+            .isEqualTo(None)
+    }
+
     companion object {
         private val TEST_SPEC = TileSpec.create("testSpec")
+        private val TEST_SPEC_2 = TileSpec.create("testSpec2")
+        private val TEST_SPEC_3 = TileSpec.create("testSpec3")
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/dialog/CastDetailsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/dialog/CastDetailsViewModelTest.kt
new file mode 100644
index 0000000..468c3dc
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/dialog/CastDetailsViewModelTest.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.dialog
+
+import android.content.Context
+import android.media.MediaRouter
+import android.provider.Settings
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.app.MediaRouteDialogPresenter
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.qs.tiles.base.domain.actions.FakeQSTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.base.domain.actions.intentInputs
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.stub
+
+@SmallTest
+@RunWithLooper(setAsMainLooper = true)
+@RunWith(AndroidJUnit4::class)
+class CastDetailsViewModelTest : SysuiTestCase() {
+    var inputHandler: FakeQSTileIntentUserInputHandler = FakeQSTileIntentUserInputHandler()
+    private var context: Context = mock()
+    private var mediaRouter: MediaRouter = mock()
+    private var selectedRoute: MediaRouter.RouteInfo = mock()
+
+    @Test
+    fun testClickOnSettingsButton() {
+        var viewModel = CastDetailsViewModel(inputHandler, context, MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY)
+
+        viewModel.clickOnSettingsButton()
+
+        assertThat(inputHandler.handledInputs).hasSize(1)
+        val intentInput = inputHandler.intentInputs.last()
+        assertThat(intentInput.expandable).isNull()
+        assertThat(intentInput.intent.action).isEqualTo(Settings.ACTION_CAST_SETTINGS)
+    }
+
+    @Test
+    fun testShouldShowChooserDialog() {
+        context.stub {
+            on { getSystemService(MediaRouter::class.java) } doReturn mediaRouter
+        }
+        mediaRouter.stub {
+            on { selectedRoute } doReturn selectedRoute
+        }
+
+        var viewModel =
+            CastDetailsViewModel(inputHandler, context, MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY)
+
+        assertThat(viewModel.shouldShowChooserDialog())
+            .isEqualTo(
+                MediaRouteDialogPresenter.shouldShowChooserDialog(
+                    context,
+                    MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY,
+                )
+            )
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index 1743e05..6d4fffd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -22,7 +22,6 @@
 import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
 import android.provider.Settings
-import android.view.Display
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.compose.animation.scene.ObservableTransitionState
@@ -1213,15 +1212,15 @@
                     fakeSceneDataSource.pause()
                     sceneInteractor.changeScene(sceneKey, "reason")
                     runCurrent()
-                    verify(sysUiState, times(index)).commitUpdate(Display.DEFAULT_DISPLAY)
+                    verify(sysUiState, times(index)).commitUpdate()
 
                     fakeSceneDataSource.unpause(expectedScene = sceneKey)
                     runCurrent()
-                    verify(sysUiState, times(index)).commitUpdate(Display.DEFAULT_DISPLAY)
+                    verify(sysUiState, times(index)).commitUpdate()
 
                     transitionStateFlow.value = ObservableTransitionState.Idle(sceneKey)
                     runCurrent()
-                    verify(sysUiState, times(index + 1)).commitUpdate(Display.DEFAULT_DISPLAY)
+                    verify(sysUiState, times(index + 1)).commitUpdate()
                 }
         }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java
index 70df82d..c26f18f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java
@@ -36,7 +36,9 @@
 import android.hardware.biometrics.PromptInfo;
 import android.hardware.fingerprint.IUdfpsRefreshRateRequestCallback;
 import android.os.Bundle;
+import android.os.RemoteException;
 import android.platform.test.annotations.EnableFlags;
+import android.util.Pair;
 import android.view.KeyEvent;
 import android.view.WindowInsets;
 import android.view.WindowInsets.Type.InsetsType;
@@ -46,6 +48,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.statusbar.DisableStates;
 import com.android.internal.statusbar.LetterboxDetails;
 import com.android.internal.statusbar.StatusBarIcon;
 import com.android.internal.view.AppearanceRegion;
@@ -58,11 +61,14 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.HashMap;
+import java.util.Map;
+
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class CommandQueueTest extends SysuiTestCase {
 
-    private static final LetterboxDetails[] TEST_LETTERBOX_DETAILS = new LetterboxDetails[] {
+    private static final LetterboxDetails[] TEST_LETTERBOX_DETAILS = new LetterboxDetails[]{
             new LetterboxDetails(
                     /* letterboxInnerBounds= */ new Rect(100, 0, 200, 500),
                     /* letterboxFullBounds= */ new Rect(0, 0, 500, 100),
@@ -119,6 +125,27 @@
     }
 
     @Test
+    public void testDisableForAllDisplays() throws RemoteException {
+        int state1 = 14;
+        int state2 = 42;
+        int secondaryDisplayState1 = 16;
+        int secondaryDisplayState2 = 44;
+        Map<Integer, Pair<Integer, Integer>> displaysWithStates = new HashMap<>();
+        displaysWithStates.put(DEFAULT_DISPLAY, new Pair<>(state1, state2)); // Example values
+        displaysWithStates.put(SECONDARY_DISPLAY,
+                new Pair<>(secondaryDisplayState1, secondaryDisplayState2)); // Example values
+        DisableStates expectedDisableStates = new DisableStates(displaysWithStates, true);
+
+        mCommandQueue.disableForAllDisplays(expectedDisableStates);
+        waitForIdleSync();
+
+        verify(mCallbacks).disable(eq(DEFAULT_DISPLAY), eq(state1), eq(state2), eq(true));
+        verify(mCallbacks).disable(eq(SECONDARY_DISPLAY), eq(secondaryDisplayState1),
+                eq(secondaryDisplayState2), eq(true));
+    }
+
+
+    @Test
     public void testExpandNotifications() {
         mCommandQueue.animateExpandNotificationsPanel();
         waitForIdleSync();
@@ -475,7 +502,8 @@
         final long requestId = 10;
 
         mCommandQueue.showAuthenticationDialog(promptInfo, receiver, sensorIds,
-                credentialAllowed, requireConfirmation, userId, operationId, packageName, requestId);
+                credentialAllowed, requireConfirmation, userId, operationId, packageName,
+                requestId);
         waitForIdleSync();
         verify(mCallbacks).showAuthenticationDialog(eq(promptInfo), eq(receiver), eq(sensorIds),
                 eq(credentialAllowed), eq(requireConfirmation), eq(userId), eq(operationId),
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
index 3ecf302..67af7a5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
@@ -33,6 +33,7 @@
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.shade.ShadeExpansionChangeEvent
+import com.android.systemui.shade.data.repository.fakeShadeDisplaysRepository
 import com.android.systemui.shade.domain.interactor.ShadeModeInteractor
 import com.android.systemui.statusbar.phone.BiometricUnlockController
 import com.android.systemui.statusbar.phone.DozeParameters
@@ -45,6 +46,8 @@
 import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor
 import com.android.wm.shell.appzoomout.AppZoomOut
 import com.google.common.truth.Truth.assertThat
+import java.util.Optional
+import java.util.function.Consumer
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -65,8 +68,6 @@
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
 import org.mockito.junit.MockitoJUnit
-import java.util.Optional
-import java.util.function.Consumer
 
 @RunWith(AndroidJUnit4::class)
 @RunWithLooper
@@ -75,6 +76,7 @@
     private val kosmos = testKosmos()
 
     private val applicationScope = kosmos.testScope.backgroundScope
+    private val shadeDisplayRepository = kosmos.fakeShadeDisplaysRepository
     @Mock private lateinit var statusBarStateController: StatusBarStateController
     @Mock private lateinit var blurUtils: BlurUtils
     @Mock private lateinit var biometricUnlockController: BiometricUnlockController
@@ -135,7 +137,8 @@
                 windowRootViewBlurInteractor,
                 appZoomOutOptional,
                 applicationScope,
-                dumpManager
+                dumpManager,
+                { shadeDisplayRepository },
             )
         notificationShadeDepthController.shadeAnimation = shadeAnimation
         notificationShadeDepthController.brightnessMirrorSpring = brightnessSpring
@@ -355,6 +358,36 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND)
+    fun updateBlurCallback_shadeInExternalDisplay_doesSetZeroZoom() {
+        notificationShadeDepthController.onPanelExpansionChanged(
+            ShadeExpansionChangeEvent(fraction = 1f, expanded = true, tracking = false)
+        )
+        notificationShadeDepthController.addListener(listener)
+        shadeDisplayRepository.setDisplayId(1) // not default display.
+
+        notificationShadeDepthController.updateBlurCallback.doFrame(0)
+
+        verify(wallpaperController).setNotificationShadeZoom(eq(0f))
+        verify(listener).onWallpaperZoomOutChanged(eq(0f))
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND)
+    fun updateBlurCallback_shadeInDefaultDisplay_doesNotSetZeroZoom() {
+        notificationShadeDepthController.onPanelExpansionChanged(
+            ShadeExpansionChangeEvent(fraction = 1f, expanded = true, tracking = false)
+        )
+        notificationShadeDepthController.addListener(listener)
+        shadeDisplayRepository.setDisplayId(0) // shade is in default display
+
+        notificationShadeDepthController.updateBlurCallback.doFrame(0)
+
+        verify(wallpaperController).setNotificationShadeZoom(floatThat { it != 0f })
+        verify(listener).onWallpaperZoomOutChanged(floatThat { it != 0f })
+    }
+
+    @Test
     @DisableFlags(Flags.FLAG_NOTIFICATION_SHADE_BLUR)
     fun updateBlurCallback_setsOpaque_whenScrim() {
         scrimVisibilityCaptor.value.accept(ScrimController.OPAQUE)
@@ -488,10 +521,10 @@
     }
 
     private fun enableSplitShade() {
-        `when` (shadeModeInteractor.isSplitShade).thenReturn(true)
+        `when`(shadeModeInteractor.isSplitShade).thenReturn(true)
     }
 
     private fun disableSplitShade() {
-        `when` (shadeModeInteractor.isSplitShade).thenReturn(false)
+        `when`(shadeModeInteractor.isSplitShade).thenReturn(false)
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
index 6c498c8..5ec9b60 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
@@ -44,6 +44,7 @@
 import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
 import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
 import com.android.systemui.statusbar.core.StatusBarRootModernization
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
 import com.android.systemui.statusbar.phone.ongoingcall.DisableChipsModernization
 import com.android.systemui.statusbar.phone.ongoingcall.EnableChipsModernization
@@ -799,8 +800,8 @@
             }
 
         private val PROMOTED_CONTENT_WITH_COLOR =
-            PromotedNotificationContentModel.Builder("notif")
-                .apply {
+            PromotedNotificationContentBuilder("notif")
+                .applyToShared {
                     this.colors =
                         PromotedNotificationContentModel.Colors(
                             backgroundColor = PROMOTED_BACKGROUND_COLOR,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt
index 7f8f5f4..9ce43a0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt
@@ -30,7 +30,7 @@
 import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
 import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
 import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder
 import com.android.systemui.testKosmos
 import com.android.systemui.util.time.fakeSystemClock
 import com.google.common.truth.Truth.assertThat
@@ -420,7 +420,7 @@
             // WHEN the notif gets a new UID that starts as visible
             activityManagerRepository.fake.startingIsAppVisibleValue = true
             val newPromotedContentBuilder =
-                PromotedNotificationContentModel.Builder("notif").apply {
+                PromotedNotificationContentBuilder("notif").applyToShared {
                     this.shortCriticalText = "Arrived"
                 }
             val newPromotedContent = newPromotedContentBuilder.build()
@@ -452,6 +452,6 @@
 
     companion object {
         private const val UID = 885
-        private val PROMOTED_CONTENT = PromotedNotificationContentModel.Builder("notif1").build()
+        private val PROMOTED_CONTENT = PromotedNotificationContentBuilder("notif1").build()
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt
index 0b9b297..202d5cf 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt
@@ -36,7 +36,7 @@
 import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
 import com.android.systemui.statusbar.notification.data.repository.addNotif
 import com.android.systemui.statusbar.notification.data.repository.removeNotif
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
 import com.android.systemui.statusbar.notification.shared.CallType
 import com.android.systemui.testKosmos
@@ -65,7 +65,7 @@
                     activeNotificationModel(
                         key = "notif",
                         statusBarChipIcon = mock<StatusBarIconView>(),
-                        promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif").build(),
                     )
                 )
             )
@@ -96,7 +96,7 @@
                     activeNotificationModel(
                         key = "notif",
                         statusBarChipIcon = null,
-                        promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif").build(),
                     )
                 )
             )
@@ -115,7 +115,7 @@
                     activeNotificationModel(
                         key = "notif",
                         statusBarChipIcon = null,
-                        promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif").build(),
                     )
                 )
             )
@@ -135,7 +135,7 @@
                     activeNotificationModel(
                         key = "notif",
                         statusBarChipIcon = icon,
-                        promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif").build(),
                     )
                 )
             )
@@ -158,12 +158,12 @@
                     activeNotificationModel(
                         key = "notif1",
                         statusBarChipIcon = firstIcon,
-                        promotedContent = PromotedNotificationContentModel.Builder("notif1").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif1").build(),
                     ),
                     activeNotificationModel(
                         key = "notif2",
                         statusBarChipIcon = secondIcon,
-                        promotedContent = PromotedNotificationContentModel.Builder("notif2").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif2").build(),
                     ),
                     activeNotificationModel(
                         key = "notif3",
@@ -195,7 +195,7 @@
                         key = "notif",
                         uid = uid,
                         statusBarChipIcon = mock<StatusBarIconView>(),
-                        promotedContent = PromotedNotificationContentModel.Builder("notif1").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif1").build(),
                     )
                 )
             )
@@ -223,14 +223,14 @@
                         key = "promotedNormal",
                         statusBarChipIcon = mock(),
                         promotedContent =
-                            PromotedNotificationContentModel.Builder("promotedNormal").build(),
+                            PromotedNotificationContentBuilder("promotedNormal").build(),
                         callType = CallType.None,
                     ),
                     activeNotificationModel(
                         key = "promotedCall",
                         statusBarChipIcon = mock(),
                         promotedContent =
-                            PromotedNotificationContentModel.Builder("promotedCall").build(),
+                            PromotedNotificationContentBuilder("promotedCall").build(),
                         callType = CallType.Ongoing,
                     ),
                 )
@@ -256,7 +256,7 @@
                     activeNotificationModel(
                         key = "notif",
                         statusBarChipIcon = firstIcon,
-                        promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif").build(),
                     )
                 )
             )
@@ -269,7 +269,7 @@
                     activeNotificationModel(
                         key = "notif",
                         statusBarChipIcon = secondIcon,
-                        promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif").build(),
                     )
                 )
             )
@@ -282,7 +282,7 @@
                     activeNotificationModel(
                         key = "notif",
                         statusBarChipIcon = thirdIcon,
-                        promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif").build(),
                     )
                 )
             )
@@ -302,7 +302,7 @@
                     activeNotificationModel(
                         key = "notif",
                         statusBarChipIcon = mock(),
-                        promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif").build(),
                     )
                 )
             )
@@ -325,7 +325,7 @@
                     activeNotificationModel(
                         key = "notif",
                         statusBarChipIcon = mock(),
-                        promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif").build(),
                     )
                 )
             )
@@ -348,7 +348,7 @@
                 activeNotificationModel(
                     key = "notif1",
                     statusBarChipIcon = firstIcon,
-                    promotedContent = PromotedNotificationContentModel.Builder("notif1").build(),
+                    promotedContent = PromotedNotificationContentBuilder("notif1").build(),
                 )
             setNotifs(listOf(notif1))
             assertThat(latest!!.map { it.key }).containsExactly("notif1").inOrder()
@@ -359,7 +359,7 @@
                 activeNotificationModel(
                     key = "notif2",
                     statusBarChipIcon = secondIcon,
-                    promotedContent = PromotedNotificationContentModel.Builder("notif2").build(),
+                    promotedContent = PromotedNotificationContentBuilder("notif2").build(),
                 )
             setNotifs(listOf(notif1, notif2))
 
@@ -380,7 +380,7 @@
 
             // WHEN notif1 gets an update
             val notif1NewPromotedContent =
-                PromotedNotificationContentModel.Builder("notif1").apply {
+                PromotedNotificationContentBuilder("notif1").applyToShared {
                     this.shortCriticalText = "Arrived"
                 }
             setNotifs(
@@ -426,8 +426,7 @@
                     key = notif1Info.key,
                     uid = notif1Info.uid,
                     statusBarChipIcon = notif1Info.icon,
-                    promotedContent =
-                        PromotedNotificationContentModel.Builder(notif1Info.key).build(),
+                    promotedContent = PromotedNotificationContentBuilder(notif1Info.key).build(),
                 )
             )
             activityManagerRepository.fake.setIsAppVisible(notif1Info.uid, isAppVisible = false)
@@ -443,8 +442,7 @@
                     key = notif2Info.key,
                     uid = notif2Info.uid,
                     statusBarChipIcon = notif2Info.icon,
-                    promotedContent =
-                        PromotedNotificationContentModel.Builder(notif2Info.key).build(),
+                    promotedContent = PromotedNotificationContentBuilder(notif2Info.key).build(),
                 )
             )
             activityManagerRepository.fake.setIsAppVisible(notif2Info.uid, isAppVisible = false)
@@ -482,16 +480,14 @@
                     key = notif1Info.key,
                     uid = notif1Info.uid,
                     statusBarChipIcon = notif1Info.icon,
-                    promotedContent =
-                        PromotedNotificationContentModel.Builder(notif1Info.key).build(),
+                    promotedContent = PromotedNotificationContentBuilder(notif1Info.key).build(),
                 )
             val notif2 =
                 activeNotificationModel(
                     key = notif2Info.key,
                     uid = notif2Info.uid,
                     statusBarChipIcon = notif2Info.icon,
-                    promotedContent =
-                        PromotedNotificationContentModel.Builder(notif2Info.key).build(),
+                    promotedContent = PromotedNotificationContentBuilder(notif2Info.key).build(),
                 )
             setNotifs(listOf(notif1, notif2))
             assertThat(latest!!.map { it.key }).containsExactly("notif1", "notif2").inOrder()
@@ -537,16 +533,14 @@
                     key = notif1Info.key,
                     uid = notif1Info.uid,
                     statusBarChipIcon = notif1Info.icon,
-                    promotedContent =
-                        PromotedNotificationContentModel.Builder(notif1Info.key).build(),
+                    promotedContent = PromotedNotificationContentBuilder(notif1Info.key).build(),
                 )
             val notif2 =
                 activeNotificationModel(
                     key = notif2Info.key,
                     uid = notif2Info.uid,
                     statusBarChipIcon = notif2Info.icon,
-                    promotedContent =
-                        PromotedNotificationContentModel.Builder(notif2Info.key).build(),
+                    promotedContent = PromotedNotificationContentBuilder(notif2Info.key).build(),
                 )
             setNotifs(listOf(notif1, notif2))
             assertThat(latest!!.map { it.key }).containsExactly("notif1", "notif2").inOrder()
@@ -567,8 +561,7 @@
                     key = notif3Info.key,
                     uid = notif3Info.uid,
                     statusBarChipIcon = notif3Info.icon,
-                    promotedContent =
-                        PromotedNotificationContentModel.Builder(notif3Info.key).build(),
+                    promotedContent = PromotedNotificationContentBuilder(notif3Info.key).build(),
                 )
             setNotifs(listOf(notif1, notif2, notif3))
 
@@ -597,8 +590,7 @@
                     key = notif1Info.key,
                     uid = notif1Info.uid,
                     statusBarChipIcon = notif1Info.icon,
-                    promotedContent =
-                        PromotedNotificationContentModel.Builder(notif1Info.key).build(),
+                    promotedContent = PromotedNotificationContentBuilder(notif1Info.key).build(),
                 )
             setNotifs(listOf(notif1))
 
@@ -609,8 +601,7 @@
                     key = notif2Info.key,
                     uid = notif2Info.uid,
                     statusBarChipIcon = notif2Info.icon,
-                    promotedContent =
-                        PromotedNotificationContentModel.Builder(notif2Info.key).build(),
+                    promotedContent = PromotedNotificationContentBuilder(notif2Info.key).build(),
                 )
             setNotifs(listOf(notif1, notif2))
 
@@ -637,7 +628,7 @@
 
             // WHEN notif2 gets an update
             val notif2NewPromotedContent =
-                PromotedNotificationContentModel.Builder("notif2").apply {
+                PromotedNotificationContentBuilder("notif2").applyToShared {
                     this.shortCriticalText = "Arrived"
                 }
             setNotifs(
@@ -662,8 +653,7 @@
                     key = notif3Info.key,
                     uid = notif3Info.uid,
                     statusBarChipIcon = notif3Info.icon,
-                    promotedContent =
-                        PromotedNotificationContentModel.Builder(notif3Info.key).build(),
+                    promotedContent = PromotedNotificationContentBuilder(notif3Info.key).build(),
                 )
             setNotifs(listOf(notif1, notif2, notif3))
 
@@ -710,8 +700,7 @@
                     activeNotificationModel(
                         key = "notif|uid1",
                         statusBarChipIcon = firstIcon,
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("notif|uid1").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif|uid1").build(),
                     )
                 )
             )
@@ -725,8 +714,7 @@
                     activeNotificationModel(
                         key = "notif|uid2",
                         statusBarChipIcon = secondIcon,
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("notif|uid2").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif|uid2").build(),
                     )
                 )
             )
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
index 8368fa6..eecdbbf 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
@@ -43,6 +43,7 @@
 import com.android.systemui.statusbar.notification.data.repository.UnconfinedFakeHeadsUpRowRepository
 import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
 import com.android.systemui.statusbar.notification.headsup.PinnedStatus
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.When
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
@@ -101,7 +102,7 @@
                     activeNotificationModel(
                         key = "notif",
                         statusBarChipIcon = null,
-                        promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif").build(),
                     )
                 )
             )
@@ -121,7 +122,7 @@
                     activeNotificationModel(
                         key = "notif",
                         statusBarChipIcon = null,
-                        promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif").build(),
                     )
                 )
             )
@@ -142,7 +143,7 @@
                         key = "notif",
                         appName = "Fake App Name",
                         statusBarChipIcon = icon,
-                        promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif").build(),
                     )
                 )
             )
@@ -172,7 +173,7 @@
                         key = notifKey,
                         appName = "Fake App Name",
                         statusBarChipIcon = null,
-                        promotedContent = PromotedNotificationContentModel.Builder(notifKey).build(),
+                        promotedContent = PromotedNotificationContentBuilder(notifKey).build(),
                     )
                 )
             )
@@ -195,7 +196,7 @@
             val latest by collectLastValue(underTest.chips)
 
             val promotedContentBuilder =
-                PromotedNotificationContentModel.Builder("notif").apply {
+                PromotedNotificationContentBuilder("notif").applyToShared {
                     this.colors =
                         PromotedNotificationContentModel.Colors(
                             backgroundColor = 56,
@@ -229,12 +230,12 @@
                     activeNotificationModel(
                         key = "notif1",
                         statusBarChipIcon = firstIcon,
-                        promotedContent = PromotedNotificationContentModel.Builder("notif1").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif1").build(),
                     ),
                     activeNotificationModel(
                         key = "notif2",
                         statusBarChipIcon = secondIcon,
-                        promotedContent = PromotedNotificationContentModel.Builder("notif2").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif2").build(),
                     ),
                     activeNotificationModel(
                         key = "notif3",
@@ -264,13 +265,12 @@
                     activeNotificationModel(
                         key = firstKey,
                         statusBarChipIcon = null,
-                        promotedContent = PromotedNotificationContentModel.Builder(firstKey).build(),
+                        promotedContent = PromotedNotificationContentBuilder(firstKey).build(),
                     ),
                     activeNotificationModel(
                         key = secondKey,
                         statusBarChipIcon = null,
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder(secondKey).build(),
+                        promotedContent = PromotedNotificationContentBuilder(secondKey).build(),
                     ),
                     activeNotificationModel(
                         key = thirdKey,
@@ -294,7 +294,7 @@
             fakeSystemClock.setCurrentTimeMillis(currentTime)
 
             val promotedContentBuilder =
-                PromotedNotificationContentModel.Builder("notif").apply {
+                PromotedNotificationContentBuilder("notif").applyToShared {
                     this.shortCriticalText = "Arrived"
                     this.time = When.Time(currentTime + 30.minutes.inWholeMilliseconds)
                 }
@@ -321,7 +321,7 @@
             val latest by collectLastValue(underTest.chips)
 
             val promotedContentBuilder =
-                PromotedNotificationContentModel.Builder("notif").apply { this.time = null }
+                PromotedNotificationContentBuilder("notif").applyToShared { this.time = null }
             setNotifs(
                 listOf(
                     activeNotificationModel(
@@ -346,7 +346,7 @@
             fakeSystemClock.setCurrentTimeMillis(currentTime)
 
             val promotedContentBuilder =
-                PromotedNotificationContentModel.Builder("notif").apply {
+                PromotedNotificationContentBuilder("notif").applyToShared {
                     this.wasPromotedAutomatically = true
                     this.time = When.Time(currentTime + 30.minutes.inWholeMilliseconds)
                 }
@@ -374,7 +374,7 @@
             fakeSystemClock.setCurrentTimeMillis(currentTime)
 
             val promotedContentBuilder =
-                PromotedNotificationContentModel.Builder("notif").apply {
+                PromotedNotificationContentBuilder("notif").applyToShared {
                     this.wasPromotedAutomatically = false
                     this.time = When.Time(currentTime + 30.minutes.inWholeMilliseconds)
                 }
@@ -402,7 +402,7 @@
             fakeSystemClock.setCurrentTimeMillis(currentTime)
 
             val promotedContentBuilder =
-                PromotedNotificationContentModel.Builder("notif").apply {
+                PromotedNotificationContentBuilder("notif").applyToShared {
                     this.time = When.Time(currentTime + 13.minutes.inWholeMilliseconds)
                 }
 
@@ -430,7 +430,7 @@
             fakeSystemClock.setCurrentTimeMillis(currentTime)
 
             val promotedContentBuilder =
-                PromotedNotificationContentModel.Builder("notif").apply {
+                PromotedNotificationContentBuilder("notif").applyToShared {
                     this.time = When.Time(currentTime + 500)
                 }
 
@@ -458,7 +458,7 @@
             fakeSystemClock.setCurrentTimeMillis(currentTime)
 
             val promotedContentBuilder =
-                PromotedNotificationContentModel.Builder("notif").apply {
+                PromotedNotificationContentBuilder("notif").applyToShared {
                     this.time = When.Time(currentTime)
                 }
 
@@ -486,7 +486,7 @@
             fakeSystemClock.setCurrentTimeMillis(currentTime)
 
             val promotedContentBuilder =
-                PromotedNotificationContentModel.Builder("notif").apply {
+                PromotedNotificationContentBuilder("notif").applyToShared {
                     this.time = When.Time(currentTime - 2.minutes.inWholeMilliseconds)
                 }
 
@@ -515,7 +515,7 @@
             fakeSystemClock.setCurrentTimeMillis(currentTime)
 
             val promotedContentBuilder =
-                PromotedNotificationContentModel.Builder("notif").apply {
+                PromotedNotificationContentBuilder("notif").applyToShared {
                     this.time = When.Time(currentTime + 3.minutes.inWholeMilliseconds)
                 }
 
@@ -555,7 +555,7 @@
             val whenElapsed = currentElapsed - 1.minutes.inWholeMilliseconds
 
             val promotedContentBuilder =
-                PromotedNotificationContentModel.Builder("notif").apply {
+                PromotedNotificationContentBuilder("notif").applyToShared {
                     this.time =
                         When.Chronometer(elapsedRealtimeMillis = whenElapsed, isCountDown = false)
                 }
@@ -592,7 +592,7 @@
             val whenElapsed = currentElapsed + 10.minutes.inWholeMilliseconds
 
             val promotedContentBuilder =
-                PromotedNotificationContentModel.Builder("notif").apply {
+                PromotedNotificationContentBuilder("notif").applyToShared {
                     this.time =
                         When.Chronometer(elapsedRealtimeMillis = whenElapsed, isCountDown = true)
                 }
@@ -623,7 +623,7 @@
             fakeSystemClock.setCurrentTimeMillis(currentTime)
 
             val promotedContentBuilder =
-                PromotedNotificationContentModel.Builder("notif").apply {
+                PromotedNotificationContentBuilder("notif").applyToShared {
                     this.time = When.Time(currentTime + 10.minutes.inWholeMilliseconds)
                 }
             setNotifs(
@@ -653,7 +653,7 @@
             fakeSystemClock.setCurrentTimeMillis(currentTime)
 
             val promotedContentBuilder =
-                PromotedNotificationContentModel.Builder("notif").apply {
+                PromotedNotificationContentBuilder("notif").applyToShared {
                     this.time = When.Time(currentTime + 10.minutes.inWholeMilliseconds)
                 }
             setNotifs(
@@ -690,11 +690,11 @@
             fakeSystemClock.setCurrentTimeMillis(currentTime)
 
             val promotedContentBuilder =
-                PromotedNotificationContentModel.Builder("notif").apply {
+                PromotedNotificationContentBuilder("notif").applyToShared {
                     this.time = When.Time(currentTime + 10.minutes.inWholeMilliseconds)
                 }
             val otherPromotedContentBuilder =
-                PromotedNotificationContentModel.Builder("other notif").apply {
+                PromotedNotificationContentBuilder("other notif").applyToShared {
                     this.time = When.Time(currentTime + 10.minutes.inWholeMilliseconds)
                 }
             val icon = createStatusBarIconViewOrNull()
@@ -738,7 +738,7 @@
             fakeSystemClock.setCurrentTimeMillis(currentTime)
 
             val promotedContentBuilder =
-                PromotedNotificationContentModel.Builder("notif").apply {
+                PromotedNotificationContentBuilder("notif").applyToShared {
                     this.time = When.Time(currentTime + 10.minutes.inWholeMilliseconds)
                 }
             setNotifs(
@@ -781,7 +781,7 @@
                     activeNotificationModel(
                         key,
                         statusBarChipIcon = createStatusBarIconViewOrNull(),
-                        promotedContent = PromotedNotificationContentModel.Builder(key).build(),
+                        promotedContent = PromotedNotificationContentBuilder(key).build(),
                     )
                 )
             )
@@ -809,7 +809,7 @@
                     activeNotificationModel(
                         key,
                         statusBarChipIcon = createStatusBarIconViewOrNull(),
-                        promotedContent = PromotedNotificationContentModel.Builder(key).build(),
+                        promotedContent = PromotedNotificationContentBuilder(key).build(),
                     )
                 )
             )
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
index 608a84b..83b3c9c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
@@ -63,7 +63,7 @@
 import com.android.systemui.statusbar.notification.data.repository.addNotif
 import com.android.systemui.statusbar.notification.data.repository.addNotifs
 import com.android.systemui.statusbar.notification.data.repository.removeNotif
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.statusbar.phone.ongoingcall.DisableChipsModernization
@@ -358,7 +358,7 @@
             addOngoingCallState(key = "call")
 
             val promotedContentBuilder =
-                PromotedNotificationContentModel.Builder("notif").apply {
+                PromotedNotificationContentBuilder("notif").applyToShared {
                     this.shortCriticalText = "Some text here"
                 }
             activeNotificationListRepository.addNotif(
@@ -741,7 +741,7 @@
                     activeNotificationModel(
                         key = "notif",
                         statusBarChipIcon = icon,
-                        promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif").build(),
                     )
                 )
             )
@@ -765,7 +765,7 @@
                     activeNotificationModel(
                         key = "notif",
                         statusBarChipIcon = icon,
-                        promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif").build(),
                     )
                 )
             )
@@ -791,14 +791,12 @@
                     activeNotificationModel(
                         key = "firstNotif",
                         statusBarChipIcon = firstIcon,
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("firstNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("firstNotif").build(),
                     ),
                     activeNotificationModel(
                         key = "secondNotif",
                         statusBarChipIcon = secondIcon,
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("secondNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("secondNotif").build(),
                     ),
                 )
             )
@@ -822,14 +820,12 @@
                     activeNotificationModel(
                         key = "firstNotif",
                         statusBarChipIcon = firstIcon,
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("firstNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("firstNotif").build(),
                     ),
                     activeNotificationModel(
                         key = "secondNotif",
                         statusBarChipIcon = secondIcon,
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("secondNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("secondNotif").build(),
                     ),
                 )
             )
@@ -857,20 +853,17 @@
                     activeNotificationModel(
                         key = "firstNotif",
                         statusBarChipIcon = firstIcon,
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("firstNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("firstNotif").build(),
                     ),
                     activeNotificationModel(
                         key = "secondNotif",
                         statusBarChipIcon = secondIcon,
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("secondNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("secondNotif").build(),
                     ),
                     activeNotificationModel(
                         key = "thirdNotif",
                         statusBarChipIcon = thirdIcon,
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("thirdNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("thirdNotif").build(),
                     ),
                 )
             )
@@ -896,26 +889,22 @@
                     activeNotificationModel(
                         key = "firstNotif",
                         statusBarChipIcon = firstIcon,
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("firstNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("firstNotif").build(),
                     ),
                     activeNotificationModel(
                         key = "secondNotif",
                         statusBarChipIcon = secondIcon,
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("secondNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("secondNotif").build(),
                     ),
                     activeNotificationModel(
                         key = "thirdNotif",
                         statusBarChipIcon = thirdIcon,
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("thirdNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("thirdNotif").build(),
                     ),
                     activeNotificationModel(
                         key = "fourthNotif",
                         statusBarChipIcon = fourthIcon,
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("fourthNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("fourthNotif").build(),
                     ),
                 )
             )
@@ -941,20 +930,17 @@
                     activeNotificationModel(
                         key = "firstNotif",
                         statusBarChipIcon = createStatusBarIconViewOrNull(),
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("firstNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("firstNotif").build(),
                     ),
                     activeNotificationModel(
                         key = "secondNotif",
                         statusBarChipIcon = createStatusBarIconViewOrNull(),
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("secondNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("secondNotif").build(),
                     ),
                     activeNotificationModel(
                         key = "thirdNotif",
                         statusBarChipIcon = createStatusBarIconViewOrNull(),
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("thirdNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("thirdNotif").build(),
                     ),
                 )
             )
@@ -973,26 +959,22 @@
                     activeNotificationModel(
                         key = "firstNotif",
                         statusBarChipIcon = createStatusBarIconViewOrNull(),
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("firstNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("firstNotif").build(),
                     ),
                     activeNotificationModel(
                         key = "secondNotif",
                         statusBarChipIcon = createStatusBarIconViewOrNull(),
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("secondNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("secondNotif").build(),
                     ),
                     activeNotificationModel(
                         key = "thirdNotif",
                         statusBarChipIcon = createStatusBarIconViewOrNull(),
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("thirdNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("thirdNotif").build(),
                     ),
                     activeNotificationModel(
                         key = "fourthNotif",
                         statusBarChipIcon = createStatusBarIconViewOrNull(),
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("fourthNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("fourthNotif").build(),
                     ),
                 )
             )
@@ -1016,14 +998,12 @@
                     activeNotificationModel(
                         key = "firstNotif",
                         statusBarChipIcon = firstIcon,
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("firstNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("firstNotif").build(),
                     ),
                     activeNotificationModel(
                         key = "secondNotif",
                         statusBarChipIcon = createStatusBarIconViewOrNull(),
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("secondNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("secondNotif").build(),
                     ),
                 )
             )
@@ -1050,20 +1030,17 @@
                     activeNotificationModel(
                         key = "firstNotif",
                         statusBarChipIcon = firstIcon,
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("firstNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("firstNotif").build(),
                     ),
                     activeNotificationModel(
                         key = "secondNotif",
                         statusBarChipIcon = secondIcon,
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("secondNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("secondNotif").build(),
                     ),
                     activeNotificationModel(
                         key = "thirdNotif",
                         statusBarChipIcon = thirdIcon,
-                        promotedContent =
-                            PromotedNotificationContentModel.Builder("thirdNotif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("thirdNotif").build(),
                     ),
                 )
             )
@@ -1092,7 +1069,7 @@
                 activeNotificationModel(
                     key = "notif",
                     statusBarChipIcon = createStatusBarIconViewOrNull(),
-                    promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                    promotedContent = PromotedNotificationContentBuilder("notif").build(),
                 )
             )
 
@@ -1114,14 +1091,14 @@
                 activeNotificationModel(
                     key = "notif1",
                     statusBarChipIcon = createStatusBarIconViewOrNull(),
-                    promotedContent = PromotedNotificationContentModel.Builder("notif1").build(),
+                    promotedContent = PromotedNotificationContentBuilder("notif1").build(),
                 )
             )
             activeNotificationListRepository.addNotif(
                 activeNotificationModel(
                     key = "notif2",
                     statusBarChipIcon = createStatusBarIconViewOrNull(),
-                    promotedContent = PromotedNotificationContentModel.Builder("notif2").build(),
+                    promotedContent = PromotedNotificationContentBuilder("notif2").build(),
                 )
             )
 
@@ -1143,14 +1120,14 @@
                 activeNotificationModel(
                     key = "notif1",
                     statusBarChipIcon = createStatusBarIconViewOrNull(),
-                    promotedContent = PromotedNotificationContentModel.Builder("notif1").build(),
+                    promotedContent = PromotedNotificationContentBuilder("notif1").build(),
                 )
             )
             activeNotificationListRepository.addNotif(
                 activeNotificationModel(
                     key = "notif2",
                     statusBarChipIcon = createStatusBarIconViewOrNull(),
-                    promotedContent = PromotedNotificationContentModel.Builder("notif2").build(),
+                    promotedContent = PromotedNotificationContentBuilder("notif2").build(),
                 )
             )
 
@@ -1178,7 +1155,7 @@
                 activeNotificationModel(
                     key = "notif",
                     statusBarChipIcon = notifIcon,
-                    promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                    promotedContent = PromotedNotificationContentBuilder("notif").build(),
                 )
             )
             addOngoingCallState(key = callNotificationKey)
@@ -1189,7 +1166,7 @@
                 activeNotificationModel(
                     key = "notif2",
                     statusBarChipIcon = notifIcon2,
-                    promotedContent = PromotedNotificationContentModel.Builder("notif2").build(),
+                    promotedContent = PromotedNotificationContentBuilder("notif2").build(),
                 )
             )
 
@@ -1214,7 +1191,7 @@
                     activeNotificationModel(
                         key = "notif",
                         statusBarChipIcon = notifIcon,
-                        promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif").build(),
                     )
                 )
             )
@@ -1265,7 +1242,7 @@
                 activeNotificationModel(
                     key = "notif",
                     statusBarChipIcon = notifIcon,
-                    promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                    promotedContent = PromotedNotificationContentBuilder("notif").build(),
                 )
             )
 
@@ -1304,7 +1281,7 @@
                 activeNotificationModel(
                     key = "notif",
                     statusBarChipIcon = notifIcon,
-                    promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                    promotedContent = PromotedNotificationContentBuilder("notif").build(),
                 )
             )
             // And everything else hidden
@@ -1382,7 +1359,7 @@
                     activeNotificationModel(
                         key = "notif1",
                         statusBarChipIcon = notif1Icon,
-                        promotedContent = PromotedNotificationContentModel.Builder("notif1").build(),
+                        promotedContent = PromotedNotificationContentBuilder("notif1").build(),
                     )
                 )
             )
@@ -1436,7 +1413,7 @@
                 activeNotificationModel(
                     key = "notif2",
                     statusBarChipIcon = notif2Icon,
-                    promotedContent = PromotedNotificationContentModel.Builder("notif2").build(),
+                    promotedContent = PromotedNotificationContentBuilder("notif2").build(),
                 )
             )
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt
index d3befa9..29bb29f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt
@@ -29,7 +29,7 @@
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
 import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
 import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder
 import com.android.systemui.statusbar.notification.shared.CallType
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
@@ -170,7 +170,7 @@
             val promoted1 =
                 activeNotificationModel(
                     key = "notif1",
-                    promotedContent = PromotedNotificationContentModel.Builder("notif1").build(),
+                    promotedContent = PromotedNotificationContentBuilder("notif1").build(),
                 )
             val notPromoted2 = activeNotificationModel(key = "notif2", promotedContent = null)
 
@@ -208,14 +208,14 @@
             val promoted1 =
                 activeNotificationModel(
                     key = "notif1",
-                    promotedContent = PromotedNotificationContentModel.Builder("notif1").build(),
+                    promotedContent = PromotedNotificationContentBuilder("notif1").build(),
                 )
             val notPromoted2 = activeNotificationModel(key = "notif2", promotedContent = null)
             val notPromoted3 = activeNotificationModel(key = "notif3", promotedContent = null)
             val promoted4 =
                 activeNotificationModel(
                     key = "notif4",
-                    promotedContent = PromotedNotificationContentModel.Builder("notif4").build(),
+                    promotedContent = PromotedNotificationContentBuilder("notif4").build(),
                 )
 
             activeNotificationListRepository.activeNotifications.value =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt
index 35b19c1..5c749e6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt
@@ -28,7 +28,8 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
 import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
 import com.android.systemui.statusbar.notification.shared.byKey
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.mock
@@ -130,7 +131,7 @@
             val promoted2 =
                 mockNotificationEntry(
                     "key2",
-                    promotedContent = PromotedNotificationContentModel.Builder("key2").build(),
+                    promotedContent = PromotedNotificationContentBuilder("key2").build(),
                 )
 
             underTest.setRenderedList(listOf(notPromoted1, promoted2))
@@ -149,7 +150,7 @@
     private fun mockNotificationEntry(
         key: String,
         rank: Int = 0,
-        promotedContent: PromotedNotificationContentModel? = null,
+        promotedContent: PromotedNotificationContentModels? = null,
     ): NotificationEntry {
         val nBuilder = Notification.Builder(context, "a")
         val notification = nBuilder.build()
@@ -165,7 +166,7 @@
             whenever(this.representativeEntry).thenReturn(this)
             whenever(this.ranking).thenReturn(RankingBuilder().setRank(rank).build())
             whenever(this.sbn).thenReturn(mockSbn)
-            whenever(this.promotedNotificationContentModel).thenReturn(promotedContent)
+            whenever(this.promotedNotificationContentModels).thenReturn(promotedContent)
         }
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt
index ee698ae..41120a1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt
@@ -33,8 +33,10 @@
 import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
 import com.android.systemui.statusbar.notification.domain.interactor.headsUpNotificationIconInteractor
 import com.android.systemui.statusbar.notification.promoted.domain.interactor.aodPromotedNotificationInteractor
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.Style.Base
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
 import com.android.systemui.statusbar.notification.shared.byIsAmbient
 import com.android.systemui.statusbar.notification.shared.byIsLastMessageFromReply
 import com.android.systemui.statusbar.notification.shared.byIsPromoted
@@ -354,6 +356,6 @@
 private fun promotedContent(
     key: String,
     style: PromotedNotificationContentModel.Style,
-): PromotedNotificationContentModel {
-    return PromotedNotificationContentModel.Builder(key).apply { this.style = style }.build()
+): PromotedNotificationContentModels {
+    return PromotedNotificationContentBuilder(key).applyToShared { this.style = style }.build()
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorImplTest.kt
index 0ac944a..cc016b9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorImplTest.kt
@@ -33,18 +33,21 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE
+import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_PUBLIC
 import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
 import com.android.systemui.statusbar.notification.promoted.AutomaticPromotionCoordinator.Companion.EXTRA_WAS_AUTOMATICALLY_PROMOTED
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.Style
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.When
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
 import com.android.systemui.statusbar.notification.row.RowImageInflater
 import com.android.systemui.testKosmos
 import com.android.systemui.util.time.fakeSystemClock
 import com.android.systemui.util.time.systemClock
 import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertNotNull
 import kotlin.time.Duration
 import kotlin.time.Duration.Companion.minutes
 import org.junit.Test
@@ -112,12 +115,43 @@
             setContentText(TEST_CONTENT_TEXT)
         }
 
-        val content = extractContent(entry)
+        val content = requireContent(entry)
 
-        assertThat(content).isNotNull()
-        assertThat(content?.subText).isEqualTo(TEST_SUB_TEXT)
-        assertThat(content?.title).isEqualTo(TEST_CONTENT_TITLE)
-        assertThat(content?.text).isEqualTo(TEST_CONTENT_TEXT)
+        content.privateVersion.apply {
+            assertThat(subText).isEqualTo(TEST_SUB_TEXT)
+            assertThat(title).isEqualTo(TEST_CONTENT_TITLE)
+            assertThat(text).isEqualTo(TEST_CONTENT_TEXT)
+        }
+
+        content.publicVersion.apply {
+            assertThat(subText).isNull()
+            assertThat(title).isNull()
+            assertThat(text).isNull()
+        }
+    }
+
+    @Test
+    @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
+    fun extractsContent_commonFields_noRedaction() {
+        val entry = createEntry {
+            setSubText(TEST_SUB_TEXT)
+            setContentTitle(TEST_CONTENT_TITLE)
+            setContentText(TEST_CONTENT_TEXT)
+        }
+
+        val content = requireContent(entry, redactionType = REDACTION_TYPE_NONE)
+
+        content.privateVersion.apply {
+            assertThat(subText).isEqualTo(TEST_SUB_TEXT)
+            assertThat(title).isEqualTo(TEST_CONTENT_TITLE)
+            assertThat(text).isEqualTo(TEST_CONTENT_TEXT)
+        }
+
+        content.publicVersion.apply {
+            assertThat(subText).isEqualTo(TEST_SUB_TEXT)
+            assertThat(title).isEqualTo(TEST_CONTENT_TITLE)
+            assertThat(text).isEqualTo(TEST_CONTENT_TEXT)
+        }
     }
 
     @Test
@@ -125,9 +159,9 @@
     fun extractContent_wasPromotedAutomatically_false() {
         val entry = createEntry { extras.putBoolean(EXTRA_WAS_AUTOMATICALLY_PROMOTED, false) }
 
-        val content = extractContent(entry)
+        val content = requireContent(entry).privateVersion
 
-        assertThat(content!!.wasPromotedAutomatically).isFalse()
+        assertThat(content.wasPromotedAutomatically).isFalse()
     }
 
     @Test
@@ -135,9 +169,9 @@
     fun extractContent_wasPromotedAutomatically_true() {
         val entry = createEntry { extras.putBoolean(EXTRA_WAS_AUTOMATICALLY_PROMOTED, true) }
 
-        val content = extractContent(entry)
+        val content = requireContent(entry).privateVersion
 
-        assertThat(content!!.wasPromotedAutomatically).isTrue()
+        assertThat(content.wasPromotedAutomatically).isTrue()
     }
 
     @Test
@@ -146,10 +180,9 @@
     fun extractContent_apiFlagOff_shortCriticalTextNotExtracted() {
         val entry = createEntry { setShortCriticalText(TEST_SHORT_CRITICAL_TEXT) }
 
-        val content = extractContent(entry)
+        val content = requireContent(entry).privateVersion
 
-        assertThat(content).isNotNull()
-        assertThat(content?.text).isNull()
+        assertThat(content.text).isNull()
     }
 
     @Test
@@ -161,10 +194,9 @@
     fun extractContent_apiFlagOn_shortCriticalTextExtracted() {
         val entry = createEntry { setShortCriticalText(TEST_SHORT_CRITICAL_TEXT) }
 
-        val content = extractContent(entry)
+        val content = requireContent(entry).privateVersion
 
-        assertThat(content).isNotNull()
-        assertThat(content?.shortCriticalText).isEqualTo(TEST_SHORT_CRITICAL_TEXT)
+        assertThat(content.shortCriticalText).isEqualTo(TEST_SHORT_CRITICAL_TEXT)
     }
 
     @Test
@@ -176,10 +208,9 @@
     fun extractContent_noShortCriticalTextSet_textIsNull() {
         val entry = createEntry { setShortCriticalText(null) }
 
-        val content = extractContent(entry)
+        val content = requireContent(entry).privateVersion
 
-        assertThat(content).isNotNull()
-        assertThat(content?.shortCriticalText).isNull()
+        assertThat(content.shortCriticalText).isNull()
     }
 
     @Test
@@ -379,17 +410,14 @@
             setWhen(providedCurrentTime)
         }
 
-        val content = extractContent(entry)
-
-        assertThat(content).isNotNull()
+        val content = requireContent(entry).privateVersion
 
         when (expected) {
-            ExpectedTime.Null -> assertThat(content?.time).isNull()
+            ExpectedTime.Null -> assertThat(content.time).isNull()
 
             ExpectedTime.Time -> {
-                val actual = content?.time as? When.Time
-                assertThat(actual).isNotNull()
-                assertThat(actual?.currentTimeMillis).isEqualTo(expectedCurrentTime)
+                val actual = assertNotNull(content.time as? When.Time)
+                assertThat(actual.currentTimeMillis).isEqualTo(expectedCurrentTime)
             }
 
             ExpectedTime.CountDown,
@@ -398,23 +426,24 @@
                     expectedCurrentTime + systemClock.elapsedRealtime() -
                         systemClock.currentTimeMillis()
 
-                val actual = content?.time as? When.Chronometer
-                assertThat(actual).isNotNull()
-                assertThat(actual?.elapsedRealtimeMillis).isEqualTo(expectedElapsedRealtime)
-                assertThat(actual?.isCountDown).isEqualTo(expected == ExpectedTime.CountDown)
+                val actual = assertNotNull(content.time as? When.Chronometer)
+                assertThat(actual.elapsedRealtimeMillis).isEqualTo(expectedElapsedRealtime)
+                assertThat(actual.isCountDown).isEqualTo(expected == ExpectedTime.CountDown)
             }
         }
     }
 
+    // TODO: Add tests for the style of the publicVersion once we implement that
+
     @Test
     @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
     fun extractContent_fromBaseStyle() {
         val entry = createEntry { setStyle(null) }
 
-        val content = extractContent(entry)
+        val content = requireContent(entry)
 
-        assertThat(content).isNotNull()
-        assertThat(content?.style).isEqualTo(Style.Base)
+        assertThat(content.privateVersion.style).isEqualTo(Style.Base)
+        assertThat(content.publicVersion.style).isEqualTo(Style.Base)
     }
 
     @Test
@@ -422,10 +451,10 @@
     fun extractContent_fromBigPictureStyle() {
         val entry = createEntry { setStyle(BigPictureStyle()) }
 
-        val content = extractContent(entry)
+        val content = requireContent(entry)
 
-        assertThat(content).isNotNull()
-        assertThat(content?.style).isEqualTo(Style.BigPicture)
+        assertThat(content.privateVersion.style).isEqualTo(Style.BigPicture)
+        assertThat(content.publicVersion.style).isEqualTo(Style.Base)
     }
 
     @Test
@@ -442,12 +471,15 @@
             )
         }
 
-        val content = extractContent(entry)
+        val content = requireContent(entry)
 
-        assertThat(content).isNotNull()
-        assertThat(content?.style).isEqualTo(Style.BigText)
-        assertThat(content?.title).isEqualTo(TEST_BIG_CONTENT_TITLE)
-        assertThat(content?.text).isEqualTo(TEST_BIG_TEXT)
+        assertThat(content.privateVersion.style).isEqualTo(Style.BigText)
+        assertThat(content.privateVersion.title).isEqualTo(TEST_BIG_CONTENT_TITLE)
+        assertThat(content.privateVersion.text).isEqualTo(TEST_BIG_TEXT)
+
+        assertThat(content.publicVersion.style).isEqualTo(Style.Base)
+        assertThat(content.publicVersion.title).isNull()
+        assertThat(content.publicVersion.text).isNull()
     }
 
     @Test
@@ -464,12 +496,15 @@
             )
         }
 
-        val content = extractContent(entry)
+        val content = requireContent(entry)
 
-        assertThat(content).isNotNull()
-        assertThat(content?.style).isEqualTo(Style.BigText)
-        assertThat(content?.title).isEqualTo(TEST_CONTENT_TITLE)
-        assertThat(content?.text).isEqualTo(TEST_BIG_TEXT)
+        assertThat(content.privateVersion.style).isEqualTo(Style.BigText)
+        assertThat(content.privateVersion.title).isEqualTo(TEST_CONTENT_TITLE)
+        assertThat(content.privateVersion.text).isEqualTo(TEST_BIG_TEXT)
+
+        assertThat(content.publicVersion.style).isEqualTo(Style.Base)
+        assertThat(content.publicVersion.title).isNull()
+        assertThat(content.publicVersion.text).isNull()
     }
 
     @Test
@@ -486,12 +521,15 @@
             )
         }
 
-        val content = extractContent(entry)
+        val content = requireContent(entry)
 
-        assertThat(content).isNotNull()
-        assertThat(content?.style).isEqualTo(Style.BigText)
-        assertThat(content?.title).isEqualTo(TEST_BIG_CONTENT_TITLE)
-        assertThat(content?.text).isEqualTo(TEST_CONTENT_TEXT)
+        assertThat(content.privateVersion.style).isEqualTo(Style.BigText)
+        assertThat(content.privateVersion.title).isEqualTo(TEST_BIG_CONTENT_TITLE)
+        assertThat(content.privateVersion.text).isEqualTo(TEST_CONTENT_TEXT)
+
+        assertThat(content.publicVersion.style).isEqualTo(Style.Base)
+        assertThat(content.publicVersion.title).isNull()
+        assertThat(content.publicVersion.text).isNull()
     }
 
     @Test
@@ -506,11 +544,14 @@
             )
         val entry = createEntry { setStyle(CallStyle.forOngoingCall(TEST_PERSON, hangUpIntent)) }
 
-        val content = extractContent(entry)
+        val content = requireContent(entry)
 
-        assertThat(content).isNotNull()
-        assertThat(content?.style).isEqualTo(Style.Call)
-        assertThat(content?.title).isEqualTo(TEST_PERSON_NAME)
+        assertThat(content.privateVersion.style).isEqualTo(Style.Call)
+        assertThat(content.privateVersion.title).isEqualTo(TEST_PERSON_NAME)
+
+        assertThat(content.publicVersion.style).isEqualTo(Style.Base)
+        assertThat(content.publicVersion.title).isNull()
+        assertThat(content.publicVersion.text).isNull()
     }
 
     @Test
@@ -524,13 +565,17 @@
             setStyle(ProgressStyle().addProgressSegment(Segment(100)).setProgress(75))
         }
 
-        val content = extractContent(entry)
+        val content = requireContent(entry)
 
-        assertThat(content).isNotNull()
-        assertThat(content?.style).isEqualTo(Style.Progress)
-        assertThat(content?.newProgress).isNotNull()
-        assertThat(content?.newProgress?.progress).isEqualTo(75)
-        assertThat(content?.newProgress?.progressMax).isEqualTo(100)
+        assertThat(content.privateVersion.style).isEqualTo(Style.Progress)
+        val newProgress = assertNotNull(content.privateVersion.newProgress)
+        assertThat(newProgress.progress).isEqualTo(75)
+        assertThat(newProgress.progressMax).isEqualTo(100)
+
+        assertThat(content.publicVersion.style).isEqualTo(Style.Base)
+        assertThat(content.publicVersion.title).isNull()
+        assertThat(content.publicVersion.text).isNull()
+        assertThat(content.publicVersion.newProgress).isNull()
     }
 
     @Test
@@ -540,10 +585,11 @@
             setStyle(MessagingStyle(TEST_PERSON).addMessage("message text", 0L, TEST_PERSON))
         }
 
-        val content = extractContent(entry)
+        val content = requireContent(entry)
 
-        assertThat(content).isNotNull()
-        assertThat(content?.style).isEqualTo(Style.Ineligible)
+        assertThat(content.privateVersion.style).isEqualTo(Style.Ineligible)
+
+        assertThat(content.publicVersion.style).isEqualTo(Style.Ineligible)
     }
 
     @Test
@@ -553,18 +599,13 @@
             setProgress(TEST_PROGRESS_MAX, TEST_PROGRESS, /* indeterminate= */ false)
         }
 
-        val content = extractContent(entry)
+        val content = requireContent(entry)
 
-        assertThat(content).isNotNull()
+        val oldProgress = assertNotNull(content.privateVersion.oldProgress)
 
-        val oldProgress = content?.oldProgress
-        assertThat(oldProgress).isNotNull()
-
-        assertThat(content).isNotNull()
-        assertThat(content?.oldProgress).isNotNull()
-        assertThat(content?.oldProgress?.progress).isEqualTo(TEST_PROGRESS)
-        assertThat(content?.oldProgress?.max).isEqualTo(TEST_PROGRESS_MAX)
-        assertThat(content?.oldProgress?.isIndeterminate).isFalse()
+        assertThat(oldProgress.progress).isEqualTo(TEST_PROGRESS)
+        assertThat(oldProgress.max).isEqualTo(TEST_PROGRESS_MAX)
+        assertThat(oldProgress.isIndeterminate).isFalse()
     }
 
     @Test
@@ -574,18 +615,25 @@
             setProgress(TEST_PROGRESS_MAX, TEST_PROGRESS, /* indeterminate= */ true)
         }
 
-        val content = extractContent(entry)
+        val content = requireContent(entry)
+        val oldProgress = assertNotNull(content.privateVersion.oldProgress)
 
-        assertThat(content).isNotNull()
-        assertThat(content?.oldProgress).isNotNull()
-        assertThat(content?.oldProgress?.progress).isEqualTo(TEST_PROGRESS)
-        assertThat(content?.oldProgress?.max).isEqualTo(TEST_PROGRESS_MAX)
-        assertThat(content?.oldProgress?.isIndeterminate).isTrue()
+        assertThat(oldProgress.progress).isEqualTo(TEST_PROGRESS)
+        assertThat(oldProgress.max).isEqualTo(TEST_PROGRESS_MAX)
+        assertThat(oldProgress.isIndeterminate).isTrue()
     }
 
-    private fun extractContent(entry: NotificationEntry): PromotedNotificationContentModel? {
+    private fun requireContent(
+        entry: NotificationEntry,
+        redactionType: Int = REDACTION_TYPE_PUBLIC,
+    ): PromotedNotificationContentModels = assertNotNull(extractContent(entry, redactionType))
+
+    private fun extractContent(
+        entry: NotificationEntry,
+        redactionType: Int = REDACTION_TYPE_PUBLIC,
+    ): PromotedNotificationContentModels? {
         val recoveredBuilder = Notification.Builder(context, entry.sbn.notification)
-        return underTest.extractContent(entry, recoveredBuilder, imageModelProvider)
+        return underTest.extractContent(entry, recoveredBuilder, redactionType, imageModelProvider)
     }
 
     private fun createEntry(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorTest.kt
index 873ab5b..42c3f66 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorTest.kt
@@ -44,7 +44,7 @@
 import com.android.systemui.statusbar.notification.data.repository.addNotif
 import com.android.systemui.statusbar.notification.domain.interactor.renderNotificationListInteractor
 import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder
 import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
 import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.addOngoingCallState
 import com.android.systemui.testKosmos
@@ -562,14 +562,14 @@
                 activeNotificationModel(
                     key = "notif1",
                     statusBarChipIcon = createStatusBarIconViewOrNull(),
-                    promotedContent = PromotedNotificationContentModel.Builder("notif1").build(),
+                    promotedContent = PromotedNotificationContentBuilder("notif1").build(),
                 )
             )
             activeNotificationListRepository.addNotif(
                 activeNotificationModel(
                     key = "notif2",
                     statusBarChipIcon = createStatusBarIconViewOrNull(),
-                    promotedContent = PromotedNotificationContentModel.Builder("notif2").build(),
+                    promotedContent = PromotedNotificationContentBuilder("notif2").build(),
                 )
             )
 
@@ -608,14 +608,14 @@
                 activeNotificationModel(
                     key = "notif1",
                     statusBarChipIcon = createStatusBarIconViewOrNull(),
-                    promotedContent = PromotedNotificationContentModel.Builder("notif1").build(),
+                    promotedContent = PromotedNotificationContentBuilder("notif1").build(),
                 )
             )
             activeNotificationListRepository.addNotif(
                 activeNotificationModel(
                     key = "notif2",
                     statusBarChipIcon = createStatusBarIconViewOrNull(),
-                    promotedContent = PromotedNotificationContentModel.Builder("notif2").build(),
+                    promotedContent = PromotedNotificationContentBuilder("notif2").build(),
                 )
             )
 
@@ -643,8 +643,7 @@
                 collectLastValue(underTest.aodPromotedNotification)
 
             // THEN the ron is first because the call has no content
-            assertThat(topPromotedNotificationContent?.identity?.key)
-                .isEqualTo("0|test_pkg|0|ron|0")
+            assertThat(topPromotedNotificationContent?.key).isEqualTo("0|test_pkg|0|ron|0")
         }
 
     @Test
@@ -663,8 +662,7 @@
                 collectLastValue(underTest.aodPromotedNotification)
 
             // THEN the call is the top notification
-            assertThat(topPromotedNotificationContent?.identity?.key)
-                .isEqualTo("0|test_pkg|0|call|0")
+            assertThat(topPromotedNotificationContent?.key).isEqualTo("0|test_pkg|0|call|0")
         }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java
index 99f2596..19b1046 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java
@@ -67,11 +67,11 @@
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips;
 import com.android.systemui.statusbar.notification.ConversationNotificationProcessor;
-import com.android.systemui.statusbar.notification.collection.EntryAdapter;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.promoted.FakePromotedNotificationContentExtractor;
 import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi;
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel;
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder;
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels;
 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.BindParams;
 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationCallback;
 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag;
@@ -389,8 +389,8 @@
     @Test
     @DisableFlags({PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME})
     public void testExtractsPromotedContent_notWhenBothFlagsDisabled() throws Exception {
-        final PromotedNotificationContentModel content =
-                new PromotedNotificationContentModel.Builder("key").build();
+        final PromotedNotificationContentModels content =
+                new PromotedNotificationContentBuilder("key").build();
         mPromotedNotificationContentExtractor.resetForEntry(mRow.getEntry(), content);
 
         inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow);
@@ -401,43 +401,43 @@
     @Test
     @EnableFlags(PromotedNotificationUi.FLAG_NAME)
     @DisableFlags(StatusBarNotifChips.FLAG_NAME)
-    public void testExtractsPromotedContent_whenPromotedNotificationUiFlagEnabled()
+    public void testExtractsPromotedContent_whePromotedNotificationUiFlagEnabled()
             throws Exception {
-        final PromotedNotificationContentModel content =
-                new PromotedNotificationContentModel.Builder("key").build();
+        final PromotedNotificationContentModels content =
+                new PromotedNotificationContentBuilder("key").build();
         mPromotedNotificationContentExtractor.resetForEntry(mRow.getEntry(), content);
 
         inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow);
 
         mPromotedNotificationContentExtractor.verifyOneExtractCall();
-        assertEquals(content, mRow.getEntry().getPromotedNotificationContentModel());
+        assertEquals(content, mRow.getEntry().getPromotedNotificationContentModels());
     }
 
     @Test
     @EnableFlags(StatusBarNotifChips.FLAG_NAME)
     @DisableFlags(PromotedNotificationUi.FLAG_NAME)
     public void testExtractsPromotedContent_whenStatusBarNotifChipsFlagEnabled() throws Exception {
-        final PromotedNotificationContentModel content =
-                new PromotedNotificationContentModel.Builder("key").build();
+        final PromotedNotificationContentModels content =
+                new PromotedNotificationContentBuilder("key").build();
         mPromotedNotificationContentExtractor.resetForEntry(mRow.getEntry(), content);
 
         inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow);
 
         mPromotedNotificationContentExtractor.verifyOneExtractCall();
-        assertEquals(content, mRow.getEntry().getPromotedNotificationContentModel());
+        assertEquals(content, mRow.getEntry().getPromotedNotificationContentModels());
     }
 
     @Test
     @EnableFlags({PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME})
     public void testExtractsPromotedContent_whenBothFlagsEnabled() throws Exception {
-        final PromotedNotificationContentModel content =
-                new PromotedNotificationContentModel.Builder("key").build();
+        final PromotedNotificationContentModels content =
+                new PromotedNotificationContentBuilder("key").build();
         mPromotedNotificationContentExtractor.resetForEntry(mRow.getEntry(), content);
 
         inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow);
 
         mPromotedNotificationContentExtractor.verifyOneExtractCall();
-        assertEquals(content, mRow.getEntry().getPromotedNotificationContentModel());
+        assertEquals(content, mRow.getEntry().getPromotedNotificationContentModels());
     }
 
     @Test
@@ -448,7 +448,7 @@
         inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow);
 
         mPromotedNotificationContentExtractor.verifyOneExtractCall();
-        assertNull(mRow.getEntry().getPromotedNotificationContentModel());
+        assertNull(mRow.getEntry().getPromotedNotificationContentModels());
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java
index 9536656..5ad4a4f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java
@@ -103,46 +103,6 @@
         row.resetMenu();
     }
 
-
-    @Test
-    public void testNoAppOpsInSlowSwipe() {
-        when(mRow.getShowSnooze()).thenReturn(false);
-        Settings.Global.putInt(mContext.getContentResolver(), SHOW_NEW_NOTIF_DISMISS, 0);
-
-        NotificationMenuRow row = new NotificationMenuRow(mContext, mPeopleNotificationIdentifier);
-        row.createMenu(mRow);
-
-        ViewGroup container = (ViewGroup) row.getMenuView();
-        // noti blocking
-        assertEquals(1, container.getChildCount());
-    }
-
-    @Test
-    public void testNoSnoozeInSlowSwipe() {
-        when(mRow.getShowSnooze()).thenReturn(false);
-        Settings.Global.putInt(mContext.getContentResolver(), SHOW_NEW_NOTIF_DISMISS, 0);
-
-        NotificationMenuRow row = new NotificationMenuRow(mContext, mPeopleNotificationIdentifier);
-        row.createMenu(mRow);
-
-        ViewGroup container = (ViewGroup) row.getMenuView();
-        // just for noti blocking
-        assertEquals(1, container.getChildCount());
-    }
-
-    @Test
-    public void testSnoozeInSlowSwipe() {
-        when(mRow.getShowSnooze()).thenReturn(true);
-        Settings.Global.putInt(mContext.getContentResolver(), SHOW_NEW_NOTIF_DISMISS, 0);
-
-        NotificationMenuRow row = new NotificationMenuRow(mContext, mPeopleNotificationIdentifier);
-        row.createMenu(mRow);
-
-        ViewGroup container = (ViewGroup) row.getMenuView();
-        // one for snooze and one for noti blocking
-        assertEquals(2, container.getChildCount());
-    }
-
     @Test
     public void testSlowSwipe_newDismiss() {
         when(mRow.getShowSnooze()).thenReturn(true);
@@ -237,6 +197,7 @@
                 new NotificationMenuRow(mContext, mPeopleNotificationIdentifier));
         doReturn(30f).when(row).getSnapBackThreshold();
         doReturn(50f).when(row).getDismissThreshold();
+        doReturn(70).when(row).getSpaceForMenu();
 
         when(row.isMenuOnLeft()).thenReturn(true);
         when(row.getTranslation()).thenReturn(40f);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
index 063a04a..dcba3e4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
@@ -44,7 +44,7 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.promoted.FakePromotedNotificationContentExtractor
 import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder
 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.BindParams
 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL
 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED
@@ -456,7 +456,7 @@
     @Test
     @DisableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
     fun testExtractsPromotedContent_notWhenBothFlagsDisabled() {
-        val content = PromotedNotificationContentModel.Builder("key").build()
+        val content = PromotedNotificationContentBuilder("key").build()
         promotedNotificationContentExtractor.resetForEntry(row.entry, content)
 
         inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row)
@@ -468,38 +468,38 @@
     @EnableFlags(PromotedNotificationUi.FLAG_NAME)
     @DisableFlags(StatusBarNotifChips.FLAG_NAME)
     fun testExtractsPromotedContent_whenPromotedNotificationUiFlagEnabled() {
-        val content = PromotedNotificationContentModel.Builder("key").build()
+        val content = PromotedNotificationContentBuilder("key").build()
         promotedNotificationContentExtractor.resetForEntry(row.entry, content)
 
         inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row)
 
         promotedNotificationContentExtractor.verifyOneExtractCall()
-        Assert.assertEquals(content, row.entry.promotedNotificationContentModel)
+        Assert.assertEquals(content, row.entry.promotedNotificationContentModels)
     }
 
     @Test
     @EnableFlags(StatusBarNotifChips.FLAG_NAME)
     @DisableFlags(PromotedNotificationUi.FLAG_NAME)
     fun testExtractsPromotedContent_whenStatusBarNotifChipsFlagEnabled() {
-        val content = PromotedNotificationContentModel.Builder("key").build()
+        val content = PromotedNotificationContentBuilder("key").build()
         promotedNotificationContentExtractor.resetForEntry(row.entry, content)
 
         inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row)
 
         promotedNotificationContentExtractor.verifyOneExtractCall()
-        Assert.assertEquals(content, row.entry.promotedNotificationContentModel)
+        Assert.assertEquals(content, row.entry.promotedNotificationContentModels)
     }
 
     @Test
     @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
     fun testExtractsPromotedContent_whenBothFlagsEnabled() {
-        val content = PromotedNotificationContentModel.Builder("key").build()
+        val content = PromotedNotificationContentBuilder("key").build()
         promotedNotificationContentExtractor.resetForEntry(row.entry, content)
 
         inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row)
 
         promotedNotificationContentExtractor.verifyOneExtractCall()
-        Assert.assertEquals(content, row.entry.promotedNotificationContentModel)
+        Assert.assertEquals(content, row.entry.promotedNotificationContentModels)
     }
 
     @Test
@@ -510,7 +510,7 @@
         inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row)
 
         promotedNotificationContentExtractor.verifyOneExtractCall()
-        Assert.assertNull(row.entry.promotedNotificationContentModel)
+        Assert.assertNull(row.entry.promotedNotificationContentModels)
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
index 1ea41de..7163539 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
@@ -186,7 +186,8 @@
                 .thenReturn(false);
         mBiometricUnlockController.onBiometricAuthenticated(UserHandle.USER_CURRENT,
                 BiometricSourceType.FINGERPRINT, true /* isStrongBiometric */);
-        verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean());
+        verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean(),
+                eq("BiometricUnlockController#MODE_SHOW_BOUNCER"));
         verify(mStatusBarKeyguardViewManager, never()).notifyKeyguardAuthenticated(anyBoolean());
         assertThat(mBiometricUnlockController.getMode())
                 .isEqualTo(BiometricUnlockController.MODE_SHOW_BOUNCER);
@@ -198,7 +199,8 @@
                 .thenReturn(false);
         mBiometricUnlockController.onBiometricAuthenticated(UserHandle.USER_CURRENT,
                 BiometricSourceType.FINGERPRINT, false /* isStrongBiometric */);
-        verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean());
+        verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean(),
+                eq("BiometricUnlockController#MODE_SHOW_BOUNCER"));
         assertThat(mBiometricUnlockController.getMode())
                 .isEqualTo(BiometricUnlockController.MODE_SHOW_BOUNCER);
         assertThat(mBiometricUnlockController.getBiometricType())
@@ -248,7 +250,8 @@
         mBiometricUnlockController.onBiometricAuthenticated(UserHandle.USER_CURRENT,
                 BiometricSourceType.FINGERPRINT, true /* isStrongBiometric */);
 
-        verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean());
+        verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean(),
+                eq("BiometricUnlockController#MODE_SHOW_BOUNCER"));
         verify(mStatusBarKeyguardViewManager).notifyKeyguardAuthenticated(eq(false));
         assertThat(mBiometricUnlockController.getMode())
                 .isEqualTo(BiometricUnlockController.MODE_UNLOCK_COLLAPSING);
@@ -327,7 +330,8 @@
         mBiometricUnlockController.onBiometricAuthenticated(UserHandle.USER_CURRENT,
                 BiometricSourceType.FACE, true /* isStrongBiometric */);
 
-        verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean());
+        verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean(),
+                eq("BiometricUnlockController#MODE_SHOW_BOUNCER"));
         assertThat(mBiometricUnlockController.getMode())
                 .isEqualTo(BiometricUnlockController.MODE_SHOW_BOUNCER);
     }
@@ -359,7 +363,8 @@
         mBiometricUnlockController.onBiometricAuthenticated(UserHandle.USER_CURRENT,
                 BiometricSourceType.FACE, true /* isStrongBiometric */);
 
-        verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean());
+        verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean(),
+                eq("BiometricUnlockController#MODE_SHOW_BOUNCER"));
         assertThat(mBiometricUnlockController.getMode())
                 .isEqualTo(BiometricUnlockController.MODE_NONE);
     }
@@ -438,17 +443,20 @@
 
         // WHEN udfps fails once - then don't show the bouncer yet
         mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT);
-        verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean());
+        verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean(),
+                eq("BiometricUnlockController#MODE_SHOW_BOUNCER"));
 
         // WHEN udfps fails the second time - then don't show the bouncer yet
         mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT);
-        verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean());
+        verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean(),
+                eq("BiometricUnlockController#MODE_SHOW_BOUNCER"));
 
         // WHEN udpfs fails the third time
         mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT);
 
         // THEN show the bouncer
-        verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(true);
+        verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(true,
+                "BiometricUnlockController#MODE_SHOW_BOUNCER");
     }
 
     @Test
@@ -460,14 +468,16 @@
         mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT);
         mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT);
         mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT);
-        verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean());
+        verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean(),
+                eq("BiometricUnlockController#MODE_SHOW_BOUNCER"));
 
         // WHEN lockout is received
         mBiometricUnlockController.onBiometricError(FingerprintManager.FINGERPRINT_ERROR_LOCKOUT,
                 "Lockout", BiometricSourceType.FINGERPRINT);
 
         // THEN show bouncer
-        verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(true);
+        verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(true,
+                "BiometricUnlockController#MODE_SHOW_BOUNCER");
     }
 
     @Test
@@ -544,7 +554,8 @@
                 BiometricSourceType.FINGERPRINT, true /* isStrongBiometric */);
 
         // THEN shows primary bouncer
-        verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean());
+        verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean(),
+                eq("BiometricUnlockController#MODE_SHOW_BOUNCER"));
     }
 
     @Test
@@ -554,7 +565,8 @@
                 BiometricSourceType.FACE, false /* isStrongBiometric */);
 
         // THEN shows primary bouncer
-        verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean());
+        verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean(),
+                eq("BiometricUnlockController#MODE_SHOW_BOUNCER"));
     }
 
     private void givenFingerprintModeUnlockCollapsing() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java
index 1cc2911..d9e2562 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java
@@ -124,7 +124,8 @@
         mRemoteInputCallback.onLockedRemoteInput(
                 mock(ExpandableNotificationRow.class), mock(View.class));
 
-        verify(mStatusBarKeyguardViewManager).showBouncer(true);
+        verify(mStatusBarKeyguardViewManager).showBouncer(true,
+                "StatusBarRemoteInputCallback#onLockedRemoteInput");
     }
     @Test
     @DisableFlags(ExpandHeadsUpOnInlineReply.FLAG_NAME)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
index c58b4bc..18074d5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
@@ -40,7 +40,7 @@
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
 import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
 import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
 import com.android.systemui.statusbar.notification.shared.CallType
 import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository
@@ -170,7 +170,7 @@
     @Test
     fun interactorHasOngoingCallNotif_repoHasPromotedContent() =
         testScope.runTest {
-            val promotedContent = PromotedNotificationContentModel.Builder("ongoingNotif").build()
+            val promotedContent = PromotedNotificationContentBuilder("ongoingNotif").build()
             setNotifOnRepo(
                 activeNotificationModel(
                     key = "ongoingNotif",
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt
index 84f1d5c..c071327a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt
@@ -29,7 +29,7 @@
 import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository
 import com.android.systemui.statusbar.gesture.swipeStatusBarAwayGestureHandler
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder
 import com.android.systemui.statusbar.phone.ongoingcall.EnableChipsModernization
 import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
 import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.addOngoingCallState
@@ -75,7 +75,7 @@
             val startTimeMs = 1000L
             val testIconView: StatusBarIconView = mock()
             val testIntent: PendingIntent = mock()
-            val testPromotedContent = PromotedNotificationContentModel.Builder(key).build()
+            val testPromotedContent = PromotedNotificationContentBuilder(key).build()
             addOngoingCallState(
                 key = key,
                 startTimeMs = startTimeMs,
@@ -106,7 +106,7 @@
             val startTimeMs = 1000L
             val testIconView: StatusBarIconView = mock()
             val testIntent: PendingIntent = mock()
-            val testPromotedContent = PromotedNotificationContentModel.Builder(key).build()
+            val testPromotedContent = PromotedNotificationContentBuilder(key).build()
             addOngoingCallState(
                 key = key,
                 startTimeMs = startTimeMs,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt
index 18a124c..033503f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt
@@ -52,6 +52,7 @@
 import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.FOLDABLE_DEVICE_STATE_HALF_OPEN
 import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.SCREEN_EVENT_TIMEOUT
 import com.android.systemui.unfold.DisplaySwitchLatencyTracker.DisplaySwitchLatencyEvent
+import com.android.systemui.unfold.data.repository.ScreenTimeoutPolicyRepository
 import com.android.systemui.unfold.data.repository.UnfoldTransitionRepositoryImpl
 import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor
 import com.android.systemui.unfoldedDeviceState
@@ -97,6 +98,8 @@
     private val animationStatusRepository = kosmos.fakeAnimationStatusRepository
     private val keyguardInteractor = mock<KeyguardInteractor>()
     private val displaySwitchLatencyLogger = mock<DisplaySwitchLatencyLogger>()
+    private val screenTimeoutPolicyRepository = mock<ScreenTimeoutPolicyRepository>()
+    private val screenTimeoutActive = MutableStateFlow(true)
     private val latencyTracker = mock<LatencyTracker>()
 
     private val deviceStateManager = kosmos.deviceStateManager
@@ -136,6 +139,7 @@
         whenever(resources.getIntArray(R.array.config_foldedDeviceStates))
             .thenReturn(nonEmptyClosedDeviceStatesArray)
         whenever(keyguardInteractor.isAodAvailable).thenReturn(isAodAvailable)
+        whenever(screenTimeoutPolicyRepository.screenTimeoutActive).thenReturn(screenTimeoutActive)
         animationStatusRepository.onAnimationStatusChanged(true)
         powerInteractor.setAwakeForTest()
         powerInteractor.setScreenPowerState(SCREEN_ON)
@@ -144,6 +148,7 @@
                 mockContext,
                 foldStateRepository,
                 powerInteractor,
+                screenTimeoutPolicyRepository,
                 unfoldTransitionInteractor,
                 animationStatusRepository,
                 keyguardInteractor,
@@ -196,6 +201,7 @@
                     mockContext,
                     foldStateRepository,
                     powerInteractor,
+                    screenTimeoutPolicyRepository,
                     unfoldTransitionInteractorWithEmptyProgressProvider,
                     animationStatusRepository,
                     keyguardInteractor,
@@ -625,6 +631,44 @@
         }
     }
 
+    @Test
+    fun displaySwitch_screenTimeoutActive_logsNoScreenWakelocks() {
+        testScope.runTest {
+            startInFoldedState(displaySwitchLatencyTracker)
+            screenTimeoutActive.value = true
+
+            startUnfolding()
+            advanceTimeBy(100.milliseconds)
+            finishUnfolding()
+
+            val event = capturedLogEvent()
+            assertThat(event.screenWakelockStatus)
+                .isEqualTo(
+                    SysUiStatsLog
+                        .DISPLAY_SWITCH_LATENCY_TRACKED__SCREEN_WAKELOCK_STATUS__SCREEN_WAKELOCK_STATUS_NO_WAKELOCKS
+                )
+        }
+    }
+
+    @Test
+    fun displaySwitch_screenTimeoutNotActive_logsHasScreenWakelocks() {
+        testScope.runTest {
+            startInFoldedState(displaySwitchLatencyTracker)
+            screenTimeoutActive.value = false
+
+            startUnfolding()
+            advanceTimeBy(100.milliseconds)
+            finishUnfolding()
+
+            val event = capturedLogEvent()
+            assertThat(event.screenWakelockStatus)
+                .isEqualTo(
+                    SysUiStatsLog
+                        .DISPLAY_SWITCH_LATENCY_TRACKED__SCREEN_WAKELOCK_STATUS__SCREEN_WAKELOCK_STATUS_HAS_SCREEN_WAKELOCKS
+                )
+        }
+    }
+
     private fun capturedLogEvent(): DisplaySwitchLatencyEvent {
         verify(displaySwitchLatencyLogger).log(capture(loggerArgumentCaptor))
         return loggerArgumentCaptor.value
@@ -662,6 +706,9 @@
             fromFoldableDeviceState = fromFoldableDeviceState,
             toFoldableDeviceState = toFoldableDeviceState,
             toState = toState,
+            screenWakelockStatus =
+                SysUiStatsLog
+                    .DISPLAY_SWITCH_LATENCY_TRACKED__SCREEN_WAKELOCK_STATUS__SCREEN_WAKELOCK_STATUS_NO_WAKELOCKS,
             trackingResult = SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TRACKING_RESULT__SUCCESS,
         )
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt
index 3eada25..0770641 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt
@@ -27,6 +27,8 @@
 import android.os.Process
 import android.os.UserHandle
 import android.os.UserManager
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import android.provider.Settings
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
@@ -34,6 +36,7 @@
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.KeyguardUpdateMonitorCallback
 import com.android.systemui.Flags as AConfigFlags
+import com.android.systemui.Flags.FLAG_USER_SWITCHER_ADD_SIGN_OUT_OPTION
 import com.android.systemui.GuestResetOrExitSessionReceiver
 import com.android.systemui.GuestResumeSessionReceiver
 import com.android.systemui.SysuiTestCase
@@ -68,6 +71,7 @@
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import junit.framework.Assert.assertNotNull
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.test.runCurrent
@@ -101,10 +105,13 @@
     @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver
     @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver
     @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+    @Mock private lateinit var userLogoutInteractor: UserLogoutInteractor
 
     private val kosmos = testKosmos()
+    private val logoutEnabledStateFlow = MutableStateFlow<Boolean>(false)
     private val testScope = kosmos.testScope
     private lateinit var spyContext: Context
+
     private lateinit var userRepository: FakeUserRepository
     private lateinit var keyguardReply: KeyguardInteractorFactory.WithDependencies
     private lateinit var keyguardRepository: FakeKeyguardRepository
@@ -118,6 +125,8 @@
         whenever(manager.getUserIcon(anyInt())).thenReturn(ICON)
         whenever(manager.canAddMoreUsers(any())).thenReturn(true)
 
+        whenever(userLogoutInteractor.isLogoutEnabled).thenReturn(logoutEnabledStateFlow)
+
         overrideResource(com.android.settingslib.R.drawable.ic_account_circle, GUEST_ICON)
         overrideResource(R.dimen.max_avatar_size, 10)
         overrideResource(
@@ -493,6 +502,42 @@
     }
 
     @Test
+    @DisableFlags(FLAG_USER_SWITCHER_ADD_SIGN_OUT_OPTION)
+    fun actions_logoutEnabled_flagDisabled_signOutIsNotShown() {
+        createUserInteractor()
+        testScope.runTest {
+            val userInfos = createUserInfos(count = 1, includeGuest = false)
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = false))
+            keyguardRepository.setKeyguardShowing(true)
+            logoutEnabledStateFlow.value = true
+
+            val value = collectLastValue(underTest.actions)
+
+            assertThat(value()).isEqualTo(emptyList<UserActionModel>())
+        }
+    }
+
+    @Test
+    @EnableFlags(FLAG_USER_SWITCHER_ADD_SIGN_OUT_OPTION)
+    fun actions_logoutEnabled_flagEnabled_signOutIsShown() {
+        createUserInteractor()
+        testScope.runTest {
+            val userInfos = createUserInfos(count = 1, includeGuest = false)
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = false))
+            keyguardRepository.setKeyguardShowing(true)
+            logoutEnabledStateFlow.value = true
+
+            val value = collectLastValue(underTest.actions)
+
+            assertThat(value()).isEqualTo(listOf(UserActionModel.SIGN_OUT))
+        }
+    }
+
+    @Test
     fun executeAction_addUser_dismissesDialogAndStartsActivity() {
         createUserInteractor()
         testScope.runTest {
@@ -569,14 +614,23 @@
             verify(uiEventLogger, times(1))
                 .log(MultiUserActionsEvent.CREATE_GUEST_FROM_USER_SWITCHER)
             assertThat(dialogRequests)
-                .contains(
-                    ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true),
-                )
+                .contains(ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true))
             verify(activityManager).switchUser(guestUserInfo.id)
         }
     }
 
     @Test
+    fun executeAction_signOut() {
+        createUserInteractor()
+        testScope.runTest {
+            underTest.executeAction(UserActionModel.SIGN_OUT)
+            runCurrent()
+
+            verify(userLogoutInteractor).logOut()
+        }
+    }
+
+    @Test
     fun selectUser_alreadySelectedGuestReSelected_exitGuestDialog() {
         createUserInteractor()
         testScope.runTest {
@@ -739,7 +793,7 @@
 
             fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
                 spyContext,
-                Intent(Intent.ACTION_LOCALE_CHANGED)
+                Intent(Intent.ACTION_LOCALE_CHANGED),
             )
             runCurrent()
 
@@ -972,7 +1026,7 @@
                     50,
                     "Work Profile",
                     /* iconPath= */ "",
-                    /* flags= */ UserInfo.FLAG_MANAGED_PROFILE
+                    /* flags= */ UserInfo.FLAG_MANAGED_PROFILE,
                 )
             )
             userRepository.setUserInfos(userInfos)
@@ -1010,7 +1064,7 @@
             userRepository.setSettings(
                 UserSwitcherSettingsModel(
                     isUserSwitcherEnabled = true,
-                    isAddUsersFromLockscreen = true
+                    isAddUsersFromLockscreen = true,
                 )
             )
 
@@ -1034,7 +1088,7 @@
             userRepository.setSettings(
                 UserSwitcherSettingsModel(
                     isUserSwitcherEnabled = true,
-                    isAddUsersFromLockscreen = true
+                    isAddUsersFromLockscreen = true,
                 )
             )
 
@@ -1068,7 +1122,7 @@
             whenever(
                     manager.hasUserRestrictionForUser(
                         UserManager.DISALLOW_ADD_USER,
-                        UserHandle.of(id)
+                        UserHandle.of(id),
                     )
                 )
                 .thenReturn(true)
@@ -1170,7 +1224,7 @@
             whenever(
                     manager.hasUserRestrictionForUser(
                         UserManager.DISALLOW_ADD_USER,
-                        UserHandle.of(0)
+                        UserHandle.of(0),
                     )
                 )
                 .thenReturn(true)
@@ -1195,7 +1249,7 @@
                 model = model,
                 id = index,
                 isSelected = index == selectedIndex,
-                isGuest = includeGuest && index == count - 1
+                isGuest = includeGuest && index == count - 1,
             )
         }
     }
@@ -1263,14 +1317,12 @@
         assertThat(record.isSwitchToEnabled).isEqualTo(isSwitchToEnabled)
     }
 
-    private fun assertRecordForAction(
-        record: UserRecord,
-        type: UserActionModel,
-    ) {
+    private fun assertRecordForAction(record: UserRecord, type: UserActionModel) {
         assertThat(record.isGuest).isEqualTo(type == UserActionModel.ENTER_GUEST_MODE)
         assertThat(record.isAddUser).isEqualTo(type == UserActionModel.ADD_USER)
         assertThat(record.isAddSupervisedUser)
             .isEqualTo(type == UserActionModel.ADD_SUPERVISED_USER)
+        assertThat(record.isSignOut).isEqualTo(type === UserActionModel.SIGN_OUT)
     }
 
     private fun createUserInteractor(startAsProcessUser: Boolean = true) {
@@ -1317,13 +1369,11 @@
                 featureFlags = kosmos.fakeFeatureFlagsClassic,
                 userRestrictionChecker = mock(),
                 processWrapper = kosmos.processWrapper,
+                userLogoutInteractor = userLogoutInteractor,
             )
     }
 
-    private fun createUserInfos(
-        count: Int,
-        includeGuest: Boolean,
-    ): List<UserInfo> {
+    private fun createUserInfos(count: Int, includeGuest: Boolean): List<UserInfo> {
         return (0 until count).map { index ->
             val isGuest = includeGuest && index == count - 1
             createUserInfo(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt
index 5d51c6d..d51e66d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt
@@ -44,9 +44,12 @@
 import com.android.systemui.user.domain.interactor.GuestUserInteractor
 import com.android.systemui.user.domain.interactor.HeadlessSystemUserMode
 import com.android.systemui.user.domain.interactor.RefreshUsersScheduler
+import com.android.systemui.user.domain.interactor.UserLogoutInteractor
 import com.android.systemui.user.domain.interactor.UserSwitcherInteractor
 import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.toList
@@ -78,13 +81,13 @@
     @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver
     @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver
     @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+    @Mock private lateinit var userLogoutInteractor: UserLogoutInteractor
 
     private lateinit var underTest: StatusBarUserChipViewModel
 
     private val userRepository = FakeUserRepository()
     private lateinit var guestUserInteractor: GuestUserInteractor
     private lateinit var refreshUsersScheduler: RefreshUsersScheduler
-
     private val testDispatcher = UnconfinedTestDispatcher()
     private val testScope = TestScope(testDispatcher)
 
@@ -92,6 +95,9 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
+        val logoutEnabledStateFlow = MutableStateFlow<Boolean>(false)
+        whenever(userLogoutInteractor.isLogoutEnabled).thenReturn(logoutEnabledStateFlow)
+
         doAnswer { invocation ->
                 val userId = invocation.arguments[0] as Int
                 when (userId) {
@@ -251,9 +257,7 @@
                     headlessSystemUserMode = headlessSystemUserMode,
                     applicationScope = testScope.backgroundScope,
                     telephonyInteractor =
-                        TelephonyInteractor(
-                            repository = FakeTelephonyRepository(),
-                        ),
+                        TelephonyInteractor(repository = FakeTelephonyRepository()),
                     broadcastDispatcher = fakeBroadcastDispatcher,
                     keyguardUpdateMonitor = keyguardUpdateMonitor,
                     backgroundDispatcher = testDispatcher,
@@ -263,7 +267,8 @@
                     guestUserInteractor = guestUserInteractor,
                     uiEventLogger = uiEventLogger,
                     userRestrictionChecker = mock(),
-                    processWrapper = ProcessWrapperFake(activityManager)
+                    processWrapper = ProcessWrapperFake(activityManager),
+                    userLogoutInteractor = userLogoutInteractor,
                 )
         )
     }
@@ -293,7 +298,7 @@
                 USER_NAME_0.text!!,
                 /* iconPath */ "",
                 /* flags */ UserInfo.FLAG_FULL,
-                /* userType */ UserManager.USER_TYPE_FULL_SYSTEM
+                /* userType */ UserManager.USER_TYPE_FULL_SYSTEM,
             )
 
         private val USER_1 =
@@ -302,7 +307,7 @@
                 USER_NAME_1.text!!,
                 /* iconPath */ "",
                 /* flags */ UserInfo.FLAG_FULL,
-                /* userType */ UserManager.USER_TYPE_FULL_SYSTEM
+                /* userType */ UserManager.USER_TYPE_FULL_SYSTEM,
             )
 
         private val USER_2 =
@@ -311,7 +316,7 @@
                 USER_NAME_2.text!!,
                 /* iconPath */ "",
                 /* flags */ UserInfo.FLAG_FULL,
-                /* userType */ UserManager.USER_TYPE_FULL_SYSTEM
+                /* userType */ UserManager.USER_TYPE_FULL_SYSTEM,
             )
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
index 8ff088f..087ccb8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
@@ -44,6 +44,7 @@
 import com.android.systemui.user.domain.interactor.GuestUserInteractor
 import com.android.systemui.user.domain.interactor.HeadlessSystemUserMode
 import com.android.systemui.user.domain.interactor.RefreshUsersScheduler
+import com.android.systemui.user.domain.interactor.UserLogoutInteractor
 import com.android.systemui.user.domain.interactor.UserSwitcherInteractor
 import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
 import com.android.systemui.user.shared.model.UserActionModel
@@ -51,6 +52,7 @@
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.toList
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
@@ -79,6 +81,7 @@
     @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver
     @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver
     @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+    @Mock private lateinit var userLogoutInteractor: UserLogoutInteractor
 
     private lateinit var underTest: UserSwitcherViewModel
 
@@ -94,6 +97,10 @@
         whenever(manager.canAddMoreUsers(any())).thenReturn(true)
         whenever(manager.getUserSwitchability(any()))
             .thenReturn(UserManager.SWITCHABILITY_STATUS_OK)
+
+        val logoutEnabledStateFlow = MutableStateFlow<Boolean>(false)
+        whenever(userLogoutInteractor.isLogoutEnabled).thenReturn(logoutEnabledStateFlow)
+
         overrideResource(
             com.android.internal.R.string.config_supervisedUserCreationPackage,
             SUPERVISED_USER_CREATION_PACKAGE,
@@ -113,15 +120,11 @@
                             UserInfo.FLAG_ADMIN or
                             UserInfo.FLAG_FULL,
                         UserManager.USER_TYPE_FULL_SYSTEM,
-                    ),
+                    )
                 )
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
-            userRepository.setSettings(
-                UserSwitcherSettingsModel(
-                    isUserSwitcherEnabled = true,
-                )
-            )
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
         }
 
         val refreshUsersScheduler =
@@ -163,9 +166,7 @@
                         headlessSystemUserMode = headlessSystemUserMode,
                         applicationScope = testScope.backgroundScope,
                         telephonyInteractor =
-                            TelephonyInteractor(
-                                repository = FakeTelephonyRepository(),
-                            ),
+                            TelephonyInteractor(repository = FakeTelephonyRepository()),
                         broadcastDispatcher = fakeBroadcastDispatcher,
                         keyguardUpdateMonitor = keyguardUpdateMonitor,
                         backgroundDispatcher = testDispatcher,
@@ -175,7 +176,8 @@
                         guestUserInteractor = guestUserInteractor,
                         uiEventLogger = uiEventLogger,
                         userRestrictionChecker = mock(),
-                        processWrapper = ProcessWrapperFake(activityManager)
+                        processWrapper = ProcessWrapperFake(activityManager),
+                        userLogoutInteractor = userLogoutInteractor,
                     ),
                 guestUserInteractor = guestUserInteractor,
             )
diff --git a/packages/SystemUI/res/drawable/unpin_icon.xml b/packages/SystemUI/res/drawable/unpin_icon.xml
new file mode 100644
index 0000000..4e2e158
--- /dev/null
+++ b/packages/SystemUI/res/drawable/unpin_icon.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="960"
+    android:viewportHeight="960"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M680,120L680,200L640,200L640,527L560,447L560,200L400,200L400,287L313,200L280,167L280,167L280,120L680,120ZM480,920L440,880L440,640L240,640L240,560L320,480L320,434L56,168L112,112L848,848L790,904L526,640L520,640L520,880L480,920ZM354,560L446,560L402,516L400,514L354,560ZM480,367L480,367L480,367L480,367ZM402,516L402,516L402,516L402,516Z"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/vector_drawable_progress_indeterminate_horizontal_trimmed.xml b/packages/SystemUI/res/drawable/vector_drawable_progress_indeterminate_horizontal_trimmed.xml
index aec204f..7f6dc49 100644
--- a/packages/SystemUI/res/drawable/vector_drawable_progress_indeterminate_horizontal_trimmed.xml
+++ b/packages/SystemUI/res/drawable/vector_drawable_progress_indeterminate_horizontal_trimmed.xml
@@ -38,7 +38,7 @@
             <path
                 android:name="rect"
                 android:pathData="M -144.0,-5.0 l 288.0,0 l 0,10.0 l -288.0,0 Z"
-                android:fillColor="?androidprv:attr/colorAccentPrimaryVariant" />
+                android:fillColor="@androidprv:color/materialColorPrimary" />
         </group>
     </group>
 </vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/activity_rear_display_enabled.xml b/packages/SystemUI/res/layout/activity_rear_display_enabled.xml
index f900626..6b633e0 100644
--- a/packages/SystemUI/res/layout/activity_rear_display_enabled.xml
+++ b/packages/SystemUI/res/layout/activity_rear_display_enabled.xml
@@ -56,6 +56,7 @@
         android:gravity="center_horizontal" />
 
     <TextView
+        android:id="@+id/seekbar_instructions"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:text="@string/rear_display_unfolded_front_screen_on_slide_to_cancel"
@@ -73,4 +74,13 @@
         android:background="@null"
         android:gravity="center_horizontal" />
 
+    <Button
+        android:id="@+id/cancel_button"
+        android:text="@string/cancel"
+        android:layout_width="@dimen/rear_display_animation_width_opened"
+        android:layout_height="wrap_content"
+        android:gravity="center_horizontal"
+        android:visibility="gone"
+        style="@style/Widget.Dialog.Button.BorderButton"/>
+
 </LinearLayout>
diff --git a/packages/SystemUI/res/layout/promoted_permission_guts.xml b/packages/SystemUI/res/layout/promoted_permission_guts.xml
new file mode 100644
index 0000000..50e5ae3c
--- /dev/null
+++ b/packages/SystemUI/res/layout/promoted_permission_guts.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright 2017, The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+
+<com.android.systemui.statusbar.notification.row.PromotedPermissionGutsContent
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:paddingTop="2dp"
+    android:paddingBottom="2dp"
+    android:background="@androidprv:color/materialColorSurfaceContainerHigh"
+    android:theme="@style/Theme.SystemUI"
+    >
+
+    <RelativeLayout
+        android:id="@+id/promoted_guts"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:minHeight="@dimen/notification_2025_min_height">
+
+        <ImageView
+            android:id="@+id/unpin_icon"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:src="@drawable/unpin_icon"
+            android:layout_alignParentTop="true"
+            android:layout_centerHorizontal="true"
+            android:padding="@dimen/notification_importance_button_padding"
+            />
+
+        <TextView
+            android:id="@+id/demote_explain"
+            android:layout_width="400dp"
+            android:layout_height="wrap_content"
+            android:layout_alignParentLeft="true"
+            android:layout_below="@id/unpin_icon"
+            android:layout_toLeftOf="@id/undo"
+            android:padding="@*android:dimen/notification_content_margin_end"
+            android:textColor="@androidprv:color/materialColorOnSurface"
+            android:minWidth="@dimen/min_clickable_item_size"
+            android:minHeight="@dimen/min_clickable_item_size"
+            style="@style/TextAppearance.NotificationInfo.Button" />
+
+        <TextView
+            android:id="@+id/undo"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_below="@id/unpin_icon"
+            android:layout_alignParentRight="true"
+            android:padding="@*android:dimen/notification_content_margin_end"
+            android:textColor="@androidprv:color/materialColorOnSurface"
+            android:minWidth="@dimen/min_clickable_item_size"
+            android:minHeight="@dimen/min_clickable_item_size"
+            android:text="@string/snooze_undo"
+            style="@style/TextAppearance.NotificationInfo.Button" />
+    </RelativeLayout>
+
+</com.android.systemui.statusbar.notification.row.PromotedPermissionGutsContent>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index d0ae307..7d98306 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -989,7 +989,7 @@
 
     <dimen name="keyguard_security_container_padding_top">20dp</dimen>
 
-    <dimen name="keyguard_translate_distance_on_swipe_up">-200dp</dimen>
+    <dimen name="keyguard_translate_distance_on_swipe_up">-180dp</dimen>
 
     <dimen name="keyguard_indication_margin_bottom">32dp</dimen>
     <dimen name="ambient_indication_margin_bottom">71dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index b627bdf..681bd53 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2551,6 +2551,9 @@
     <!-- Label for header of customize QS [CHAR LIMIT=60] -->
     <string name="drag_to_rearrange_tiles">Hold and drag to rearrange tiles</string>
 
+    <!-- Label for placing tiles in edit mode for QS [CHAR LIMIT=60] -->
+    <string name="tap_to_position_tile">Tap to position tile</string>
+
     <!-- Label for area where tiles can be dragged in to [CHAR LIMIT=60] -->
     <string name="drag_to_remove_tiles">Drag here to remove</string>
 
@@ -2592,6 +2595,12 @@
     <!-- Accessibility description of action to remove QS tile on click. It will read as "Double-tap to remove tile" in screen readers [CHAR LIMIT=NONE] -->
     <string name="accessibility_qs_edit_remove_tile_action">remove tile</string>
 
+    <!-- Accessibility description of action to select the QS tile to place on click. It will read as "Double-tap to toggle placement mode" in screen readers [CHAR LIMIT=NONE] -->
+    <string name="accessibility_qs_edit_toggle_placement_mode">toggle placement mode</string>
+
+    <!-- Accessibility description of action to toggle the QS tile selection. It will read as "Double-tap to toggle selection" in screen readers [CHAR LIMIT=NONE] -->
+    <string name="accessibility_qs_edit_toggle_selection">toggle selection</string>
+
     <!-- Accessibility action of action to add QS tile to end. It will read as "Double-tap to add tile to the last position" in screen readers [CHAR LIMIT=NONE] -->
     <string name="accessibility_qs_edit_tile_add_action">add tile to the last position</string>
 
@@ -4200,6 +4209,12 @@
         All Quick Settings tiles will reset to the device’s original settings
     </string>
 
+
+    <!-- Content of the Reset Tiles dialog in QS Edit mode. [CHAR LIMIT=NONE] -->
+    <string name="demote_explain_text">
+         <xliff:g id="application" example= "Superfast Food Delivery">%1$s</xliff:g> will no longer show Live Updates here. You can change this any time in Settings.
+    </string>
+
     <!-- Template that joins disabled message with the label for the voice over. [CHAR LIMIT=NONE] -->
     <string name="volume_slider_disabled_message_template"><xliff:g example="Notification" id="stream_name">%1$s</xliff:g>, <xliff:g example="Disabled because ring is muted" id="disabled_message">%2$s</xliff:g></string>
 </resources>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java
index ea73216..b8cd5be 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java
@@ -208,4 +208,19 @@
         }
         mMatrix.postTranslate(translateX, translateY);
     }
+
+    /**
+     * A factory that returns a new instance of the {@link PreviewPositionHelper}.
+     * <p>{@link PreviewPositionHelper} is a stateful helper, and hence when using it in distinct
+     * scenarios, prefer fetching an object using this factory</p>
+     * <p>Additionally, helpful for injecting mocks in tests</p>
+     */
+    public static class PreviewPositionHelperFactory {
+        /**
+         * Returns a new {@link PreviewPositionHelper} for use in a distinct scenario.
+         */
+        public PreviewPositionHelper create() {
+            return new PreviewPositionHelper();
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java
index 892851c..8a30714 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java
@@ -160,7 +160,7 @@
     /**
      * Shows the primary bouncer.
      */
-    void showPrimaryBouncer(boolean scrimmed);
+    void showPrimaryBouncer(boolean scrimmed, String reason);
 
     /**
      * When the primary bouncer is fully visible or is showing but animation didn't finish yet.
diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/scrim/BouncerScrimController.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/scrim/BouncerScrimController.java
index 6f2dd79..633c13e 100644
--- a/packages/SystemUI/src/com/android/systemui/ambient/touch/scrim/BouncerScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/scrim/BouncerScrimController.java
@@ -34,7 +34,7 @@
 
     @Override
     public void show(boolean scrimmed) {
-        mStatusBarKeyguardViewManager.showPrimaryBouncer(scrimmed);
+        mStatusBarKeyguardViewManager.showPrimaryBouncer(scrimmed, "BouncerScrimController#show");
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index dfe8eb2..659d3b4 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -880,7 +880,7 @@
                 Log.v(TAG, "aod lock icon long-press rejected by the falsing manager.");
                 return;
             }
-            mKeyguardViewManager.showPrimaryBouncer(true);
+            mKeyguardViewManager.showPrimaryBouncer(true, "UdfpsController#onAodInterrupt");
 
             // play the same haptic as the DeviceEntryIcon longpress
             if (mOverlay != null && mOverlay.getTouchOverlay() != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt
index 0c6d792..48e08fc 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt
@@ -135,7 +135,7 @@
     // TODO(b/243695312): Encapsulate all of the show logic for the bouncer.
     /** Show the bouncer if necessary and set the relevant states. */
     @JvmOverloads
-    fun show(isScrimmed: Boolean): Boolean {
+    fun show(isScrimmed: Boolean, reason: String): Boolean {
         // When the scene container framework is enabled, instead of calling this, call
         // SceneInteractor#changeScene(Scenes.Bouncer, ...).
         SceneContainerFlag.assertInLegacyMode()
@@ -176,6 +176,7 @@
                 return false
             }
 
+            Log.i(TAG, "Show primary bouncer requested, reason: $reason")
             repository.setPrimaryShowingSoon(true)
             if (usePrimaryBouncerPassiveAuthDelay()) {
                 Log.d(TAG, "delay bouncer, passive auth may succeed")
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/compose/gestures/EagerTap.kt b/packages/SystemUI/src/com/android/systemui/common/ui/compose/gestures/EagerTap.kt
new file mode 100644
index 0000000..078ea56
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/compose/gestures/EagerTap.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2025 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.common.ui.compose.gestures
+
+import androidx.compose.foundation.gestures.awaitEachGesture
+import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.foundation.gestures.waitForUpOrCancellation
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.pointer.PointerInputChange
+import androidx.compose.ui.input.pointer.PointerInputScope
+import kotlinx.coroutines.coroutineScope
+
+/**
+ * Detects taps and double taps without waiting for the double tap minimum delay in between
+ *
+ * Using [detectTapGestures] with both a single tap and a double tap defined will send only one of
+ * these event per user interaction. This variant will send the single tap at all times, with the
+ * optional double tap if the user pressed a second time in a short period of time.
+ *
+ * Warning: Use this only if you know that reporting a single tap followed by a double tap won't be
+ * a problem in your use case.
+ *
+ * @param doubleTapEnabled whether this should listen for double tap events. This value is captured
+ *   at the first down movement.
+ * @param onDoubleTap the double tap callback
+ * @param onTap the single tap callback
+ */
+suspend fun PointerInputScope.detectEagerTapGestures(
+    doubleTapEnabled: () -> Boolean,
+    onDoubleTap: (Offset) -> Unit,
+    onTap: () -> Unit,
+) = coroutineScope {
+    awaitEachGesture {
+        val down = awaitFirstDown()
+        down.consume()
+
+        // Capture whether double tap is enabled on first down as this state can change following
+        // the first tap
+        val isDoubleTapEnabled = doubleTapEnabled()
+
+        // wait for first tap up or long press
+        val upOrCancel = waitForUpOrCancellation()
+
+        if (upOrCancel != null) {
+            // tap was successful.
+            upOrCancel.consume()
+            onTap.invoke()
+
+            if (isDoubleTapEnabled) {
+                // check for second tap
+                val secondDown =
+                    withTimeoutOrNull(viewConfiguration.doubleTapTimeoutMillis) {
+                        val minUptime =
+                            upOrCancel.uptimeMillis + viewConfiguration.doubleTapMinTimeMillis
+                        var change: PointerInputChange
+                        // The second tap doesn't count if it happens before DoubleTapMinTime of the
+                        // first tap
+                        do {
+                            change = awaitFirstDown()
+                        } while (change.uptimeMillis < minUptime)
+                        change
+                    }
+
+                if (secondDown != null) {
+                    // Second tap down detected
+
+                    // Might have a long second press as the second tap
+                    val secondUp = waitForUpOrCancellation()
+                    if (secondUp != null) {
+                        secondUp.consume()
+                        onDoubleTap(secondUp.position)
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/DevicePosturingListener.kt b/packages/SystemUI/src/com/android/systemui/communal/DevicePosturingListener.kt
index 8bfec0a..af8a5fa2 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/DevicePosturingListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/DevicePosturingListener.kt
@@ -21,11 +21,8 @@
 import android.service.dreams.Flags.allowDreamWhenPostured
 import com.android.app.tracing.coroutines.launchInTraced
 import com.android.systemui.CoreStartable
-import com.android.systemui.common.domain.interactor.BatteryInteractor
-import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
 import com.android.systemui.communal.posturing.domain.interactor.PosturingInteractor
 import com.android.systemui.communal.posturing.shared.model.PosturedState
-import com.android.systemui.communal.shared.model.WhenToDream
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.log.dagger.CommunalTableLog
@@ -33,14 +30,10 @@
 import com.android.systemui.log.table.logDiffsForTable
 import com.android.systemui.statusbar.commandline.Command
 import com.android.systemui.statusbar.commandline.CommandRegistry
-import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
-import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
 import java.io.PrintWriter
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onEach
 
 @SysUISingleton
@@ -49,36 +42,19 @@
 constructor(
     private val commandRegistry: CommandRegistry,
     private val dreamManager: DreamManager,
-    private val posturingInteractor: PosturingInteractor,
-    communalSettingsInteractor: CommunalSettingsInteractor,
-    batteryInteractor: BatteryInteractor,
+    private val interactor: PosturingInteractor,
     @Background private val bgScope: CoroutineScope,
     @CommunalTableLog private val tableLogBuffer: TableLogBuffer,
 ) : CoreStartable {
     private val command = DevicePosturingCommand()
 
-    // Only subscribe to posturing if applicable to avoid running the posturing CHRE nanoapp
-    // if posturing signal is not needed.
-    private val postured =
-        allOf(
-                batteryInteractor.isDevicePluggedIn,
-                communalSettingsInteractor.whenToDream.map { it == WhenToDream.WHILE_POSTURED },
-            )
-            .flatMapLatestConflated { shouldListen ->
-                if (shouldListen) {
-                    posturingInteractor.postured
-                } else {
-                    flowOf(false)
-                }
-            }
-
     @SuppressLint("MissingPermission")
     override fun start() {
         if (!allowDreamWhenPostured()) {
             return
         }
 
-        postured
+        interactor.postured
             .distinctUntilChanged()
             .logDiffsForTable(
                 tableLogBuffer = tableLogBuffer,
@@ -102,7 +78,7 @@
 
             val state =
                 when (arg.lowercase()) {
-                    "true" -> PosturedState.Postured
+                    "true" -> PosturedState.Postured(confidence = 1f)
                     "false" -> PosturedState.NotPostured
                     "clear" -> PosturedState.Unknown
                     else -> {
@@ -111,7 +87,7 @@
                         null
                     }
                 }
-            state?.let { posturingInteractor.setValueForDebug(it) }
+            state?.let { interactor.setValueForDebug(it) }
         }
 
         override fun help(pw: PrintWriter) {
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractor.kt
index 678a5e2..20bfabd 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractor.kt
@@ -57,15 +57,7 @@
                         allOf(batteryInteractor.isDevicePluggedIn, dockManager.retrieveIsDocked())
                     }
                     WhenToStartHub.WHILE_CHARGING_AND_POSTURED -> {
-                        // Only listen to posturing if applicable to avoid running the posturing
-                        // CHRE nanoapp when not needed.
-                        batteryInteractor.isDevicePluggedIn.flatMapLatestConflated { isCharging ->
-                            if (isCharging) {
-                                posturingInteractor.postured
-                            } else {
-                                flowOf(false)
-                            }
-                        }
+                        allOf(batteryInteractor.isDevicePluggedIn, posturingInteractor.postured)
                     }
                     WhenToStartHub.NEVER -> flowOf(false)
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/posturing/data/model/PositionState.kt b/packages/SystemUI/src/com/android/systemui/communal/posturing/data/model/PositionState.kt
deleted file mode 100644
index 21b8dd7..0000000
--- a/packages/SystemUI/src/com/android/systemui/communal/posturing/data/model/PositionState.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2025 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.communal.posturing.data.model
-
-import androidx.annotation.FloatRange
-
-data class PositionState(
-    val stationary: StationaryState = StationaryState.Unknown,
-    val orientation: OrientationState = OrientationState.Unknown,
-) {
-    sealed interface StationaryState {
-        @get:FloatRange(from = 0.0, to = 1.0) val confidence: Float
-
-        data object Unknown : StationaryState {
-            override val confidence: Float = 0f
-        }
-
-        data class Stationary(override val confidence: Float) : StationaryState
-
-        data class NotStationary(override val confidence: Float) : StationaryState
-    }
-
-    sealed interface OrientationState {
-        @get:FloatRange(from = 0.0, to = 1.0) val confidence: Float
-
-        data object Unknown : OrientationState {
-            override val confidence: Float = 0f
-        }
-
-        data class Postured(override val confidence: Float) : OrientationState
-
-        data class NotPostured(override val confidence: Float) : OrientationState
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/posturing/data/repository/NoOpPosturingRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/posturing/data/repository/NoOpPosturingRepository.kt
index d826685..c5f357f 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/posturing/data/repository/NoOpPosturingRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/posturing/data/repository/NoOpPosturingRepository.kt
@@ -16,7 +16,7 @@
 
 package com.android.systemui.communal.posturing.data.repository
 
-import com.android.systemui.communal.posturing.data.model.PositionState
+import com.android.systemui.communal.posturing.shared.model.PosturedState
 import com.android.systemui.dagger.SysUISingleton
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
@@ -25,6 +25,6 @@
 
 @SysUISingleton
 class NoOpPosturingRepository @Inject constructor() : PosturingRepository {
-    override val positionState: Flow<PositionState> =
-        MutableStateFlow(PositionState()).asStateFlow()
+    override val posturedState: Flow<PosturedState> =
+        MutableStateFlow(PosturedState.Unknown).asStateFlow()
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/posturing/data/repository/PosturingRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/posturing/data/repository/PosturingRepository.kt
index 4de0a1e2..dae1a47 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/posturing/data/repository/PosturingRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/posturing/data/repository/PosturingRepository.kt
@@ -16,7 +16,7 @@
 
 package com.android.systemui.communal.posturing.data.repository
 
-import com.android.systemui.communal.posturing.data.model.PositionState
+import com.android.systemui.communal.posturing.shared.model.PosturedState
 import kotlinx.coroutines.flow.Flow
 
 /**
@@ -25,5 +25,5 @@
  */
 interface PosturingRepository {
     /** Whether the device is currently stationary and upright. */
-    val positionState: Flow<PositionState>
+    val posturedState: Flow<PosturedState>
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractor.kt
index e487590..cd81dea 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractor.kt
@@ -16,211 +16,26 @@
 
 package com.android.systemui.communal.posturing.domain.interactor
 
-import android.annotation.SuppressLint
-import android.hardware.Sensor
-import android.hardware.TriggerEvent
-import android.hardware.TriggerEventListener
-import android.service.dreams.Flags.allowDreamWhenPostured
-import com.android.systemui.communal.posturing.data.model.PositionState
 import com.android.systemui.communal.posturing.data.repository.PosturingRepository
 import com.android.systemui.communal.posturing.shared.model.PosturedState
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.core.Logger
-import com.android.systemui.log.dagger.CommunalLog
-import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
-import com.android.systemui.util.kotlin.slidingWindow
-import com.android.systemui.util.sensors.AsyncSensorManager
-import com.android.systemui.util.time.SystemClock
-import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
-import java.util.concurrent.atomic.AtomicBoolean
 import javax.inject.Inject
-import kotlin.time.Duration.Companion.seconds
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.filterNot
-import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.merge
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.flow.stateIn
 
 @SysUISingleton
-class PosturingInteractor
-@Inject
-constructor(
-    repository: PosturingRepository,
-    private val asyncSensorManager: AsyncSensorManager,
-    @Application private val applicationScope: CoroutineScope,
-    @Background private val bgDispatcher: CoroutineDispatcher,
-    @CommunalLog private val logBuffer: LogBuffer,
-    clock: SystemClock,
-) {
-    private val logger = Logger(logBuffer, TAG)
-
+class PosturingInteractor @Inject constructor(repository: PosturingRepository) {
     private val debugPostured = MutableStateFlow<PosturedState>(PosturedState.Unknown)
 
+    val postured: Flow<Boolean> =
+        combine(repository.posturedState, debugPostured) { postured, debugValue ->
+            debugValue.asBoolean() ?: postured.asBoolean() ?: false
+        }
+
     fun setValueForDebug(value: PosturedState) {
         debugPostured.value = value
     }
-
-    /**
-     * Detects whether or not the device is stationary, applying a sliding window smoothing
-     * algorithm.
-     */
-    private val stationarySmoothed: Flow<Boolean> =
-        merge(
-                observeTriggerSensor(Sensor.TYPE_PICK_UP_GESTURE)
-                    // If pickup detected, avoid triggering posturing at all within the sliding
-                    // window by emitting a negative infinity value.
-                    .map { Float.NEGATIVE_INFINITY }
-                    .onEach { logger.i("pickup gesture detected") },
-                observeTriggerSensor(Sensor.TYPE_SIGNIFICANT_MOTION)
-                    // If motion detected, avoid triggering posturing at all within the sliding
-                    // window by emitting a negative infinity value.
-                    .map { Float.NEGATIVE_INFINITY }
-                    .onEach { logger.i("significant motion detected") },
-                repository.positionState
-                    .map { it.stationary }
-                    .filterNot { it is PositionState.StationaryState.Unknown }
-                    .map { stationaryState ->
-                        if (stationaryState is PositionState.StationaryState.Stationary) {
-                            stationaryState.confidence
-                        } else {
-                            // If not stationary, then we should effectively disable posturing by
-                            // emitting the lowest possible confidence.
-                            Float.NEGATIVE_INFINITY
-                        }
-                    },
-            )
-            .slidingWindow(SLIDING_WINDOW_DURATION, clock)
-            .filterNot { it.isEmpty() }
-            .map { window ->
-                val avgStationaryConfidence = window.average()
-                logger.i({ "stationary confidence: $double1 | window: $str1" }) {
-                    str1 = window.formatWindowForDebugging()
-                    double1 = avgStationaryConfidence
-                }
-                avgStationaryConfidence > CONFIDENCE_THRESHOLD
-            }
-
-    /**
-     * Detects whether or not the device is in an upright orientation, applying a sliding window
-     * smoothing algorithm.
-     */
-    private val orientationSmoothed: Flow<Boolean> =
-        repository.positionState
-            .map { it.orientation }
-            .filterNot { it is PositionState.OrientationState.Unknown }
-            .map { orientationState ->
-                if (orientationState is PositionState.OrientationState.Postured) {
-                    orientationState.confidence
-                } else {
-                    // If not postured, then we should effectively disable posturing by
-                    // emitting the lowest possible confidence.
-                    Float.NEGATIVE_INFINITY
-                }
-            }
-            .slidingWindow(SLIDING_WINDOW_DURATION, clock)
-            .filterNot { it.isEmpty() }
-            .map { window ->
-                val avgOrientationConfidence = window.average()
-                logger.i({ "orientation confidence: $double1 | window: $str1" }) {
-                    str1 = window.formatWindowForDebugging()
-                    double1 = avgOrientationConfidence
-                }
-                avgOrientationConfidence > CONFIDENCE_THRESHOLD
-            }
-
-    /**
-     * Posturing is composed of the device being stationary and in the correct orientation. If both
-     * conditions are met, then consider it postured.
-     */
-    private val posturedSmoothed: Flow<PosturedState> =
-        allOf(stationarySmoothed, orientationSmoothed)
-            .map { postured ->
-                if (postured) {
-                    PosturedState.Postured
-                } else {
-                    PosturedState.NotPostured
-                }
-            }
-            .flowOn(bgDispatcher)
-            .stateIn(
-                scope = applicationScope,
-                // Avoid losing the smoothing history if the user plug/unplugs rapidly.
-                started =
-                    SharingStarted.WhileSubscribed(
-                        stopTimeoutMillis = STOP_TIMEOUT_AFTER_UNSUBSCRIBE.inWholeMilliseconds,
-                        replayExpirationMillis = 0,
-                    ),
-                initialValue = PosturedState.Unknown,
-            )
-
-    /**
-     * Whether the device is postured.
-     *
-     * NOTE: Due to smoothing, this signal may be delayed to ensure we have a stable reading before
-     * being considered postured.
-     */
-    val postured: Flow<Boolean> by lazy {
-        if (allowDreamWhenPostured()) {
-            combine(posturedSmoothed, debugPostured) { postured, debugValue ->
-                debugValue.asBoolean() ?: postured.asBoolean() ?: false
-            }
-        } else {
-            MutableStateFlow(false)
-        }
-    }
-
-    /**
-     * Helper for observing a trigger sensor, which automatically unregisters itself after it
-     * executes once.
-     */
-    private fun observeTriggerSensor(type: Int): Flow<Unit> = conflatedCallbackFlow {
-        val sensor = asyncSensorManager.getDefaultSensor(type)
-        val isRegistered = AtomicBoolean(false)
-
-        fun registerCallbackInternal(callback: TriggerEventListener) {
-            if (isRegistered.compareAndSet(false, true)) {
-                asyncSensorManager.requestTriggerSensor(callback, sensor)
-            }
-        }
-
-        val callback =
-            object : TriggerEventListener() {
-                override fun onTrigger(event: TriggerEvent) {
-                    trySend(Unit)
-                    if (isRegistered.getAndSet(false)) {
-                        registerCallbackInternal(this)
-                    }
-                }
-            }
-
-        if (sensor != null) {
-            registerCallbackInternal(callback)
-        }
-
-        awaitClose {
-            if (isRegistered.getAndSet(false)) {
-                asyncSensorManager.cancelTriggerSensor(callback, sensor)
-            }
-        }
-    }
-
-    companion object {
-        const val TAG = "PosturingInteractor"
-        val SLIDING_WINDOW_DURATION = 10.seconds
-        const val CONFIDENCE_THRESHOLD = 0.8f
-        val STOP_TIMEOUT_AFTER_UNSUBSCRIBE = 5.seconds
-    }
 }
 
 fun PosturedState.asBoolean(): Boolean? {
@@ -230,8 +45,3 @@
         PosturedState.Unknown -> null
     }
 }
-
-@SuppressLint("DefaultLocale")
-fun List<Float>.formatWindowForDebugging(): String {
-    return joinToString(prefix = "[", postfix = "]") { String.format("%.2f", it) }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/posturing/shared/model/PosturedState.kt b/packages/SystemUI/src/com/android/systemui/communal/posturing/shared/model/PosturedState.kt
index c71cf14..431ca67 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/posturing/shared/model/PosturedState.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/posturing/shared/model/PosturedState.kt
@@ -18,7 +18,7 @@
 
 sealed interface PosturedState {
     /** Represents postured state */
-    data object Postured : PosturedState
+    data class Postured(val confidence: Float) : PosturedState
 
     /** Represents unknown/uninitialized state */
     data object Unknown : PosturedState
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalLockIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalLockIconViewModel.kt
index 19eeabd..931639c 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalLockIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalLockIconViewModel.kt
@@ -130,7 +130,9 @@
         if (SceneContainerFlag.isEnabled) {
             deviceEntryInteractor.attemptDeviceEntry()
         } else {
-            keyguardViewController.get().showPrimaryBouncer(/* scrim */ true)
+            keyguardViewController
+                .get()
+                .showPrimaryBouncer(/* scrim */ true, "CommunalLockIconViewModel#onUserInteraction")
         }
         deviceEntrySourceInteractor.attemptEnterDeviceFromDeviceEntryIcon()
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 6db2ebc..099a7f0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -140,6 +140,7 @@
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor;
+import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor;
 import com.android.systemui.communal.ui.viewmodel.CommunalTransitionViewModel;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dagger.qualifiers.UiBackground;
@@ -364,6 +365,7 @@
     private final Lazy<NotificationShadeDepthController> mNotificationShadeDepthController;
     private final Lazy<ShadeController> mShadeController;
     private final Lazy<CommunalSceneInteractor> mCommunalSceneInteractor;
+    private final Lazy<CommunalSettingsInteractor> mCommunalSettingsInteractor;
     /*
      * Records the user id on request to go away, for validation when WM calls back to start the
      * exit animation.
@@ -1567,6 +1569,7 @@
             KeyguardInteractor keyguardInteractor,
             KeyguardTransitionBootInteractor transitionBootInteractor,
             Lazy<CommunalSceneInteractor> communalSceneInteractor,
+            Lazy<CommunalSettingsInteractor> communalSettingsInteractor,
             WindowManagerOcclusionManager wmOcclusionManager) {
         mContext = context;
         mUserTracker = userTracker;
@@ -1609,6 +1612,7 @@
         mKeyguardInteractor = keyguardInteractor;
         mTransitionBootInteractor = transitionBootInteractor;
         mCommunalSceneInteractor = communalSceneInteractor;
+        mCommunalSettingsInteractor = communalSettingsInteractor;
 
         mStatusBarStateController = statusBarStateController;
         statusBarStateController.addCallback(this);
@@ -2429,9 +2433,18 @@
     private void doKeyguardLocked(Bundle options) {
         // If the power button behavior requests to open the glanceable hub.
         if (options != null && options.getBoolean(EXTRA_TRIGGER_HUB)) {
-            // Set the hub to show immediately when the SysUI window shows, then continue to lock
-            // the device.
-            mCommunalSceneInteractor.get().showHubFromPowerButton();
+            if (mCommunalSettingsInteractor.get().getAutoOpenEnabled().getValue()) {
+                // Set the hub to show immediately when the SysUI window shows, then continue to
+                // lock the device.
+                mCommunalSceneInteractor.get().showHubFromPowerButton();
+            } else {
+                // If the hub is not available, go to sleep instead of locking. This can happen
+                // because the power button behavior does not check all possible reasons the hub
+                // might be disabled.
+                mPM.goToSleep(android.os.SystemClock.uptimeMillis(),
+                        PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON, 0);
+                return;
+            }
         }
 
         int currentUserId = mSelectedUserInteractor.getSelectedUserId();
@@ -3765,13 +3778,7 @@
                         Log.d(TAG, "Status bar manager is disabled for visible background users");
                     }
                 } else {
-                    try {
-                        mStatusBarService.disableForUser(flags, mStatusBarDisableToken,
-                                mContext.getPackageName(),
-                                mSelectedUserInteractor.getSelectedUserId());
-                    } catch (RemoteException e) {
-                        Log.d(TAG, "Failed to force clear flags", e);
-                    }
+                    statusBarServiceDisableForUser(flags, "Failed to force clear flags");
                 }
             }
 
@@ -3807,18 +3814,29 @@
 
                 // Handled in StatusBarDisableFlagsInteractor.
                 if (!KeyguardWmStateRefactor.isEnabled()) {
-                    try {
-                        mStatusBarService.disableForUser(flags, mStatusBarDisableToken,
-                                mContext.getPackageName(),
-                                mSelectedUserInteractor.getSelectedUserId());
-                    } catch (RemoteException e) {
-                        Log.d(TAG, "Failed to set disable flags: " + flags, e);
-                    }
+                    statusBarServiceDisableForUser(flags, "Failed to set disable flags: ");
                 }
             }
         }
     }
 
+    private void statusBarServiceDisableForUser(int flags, String loggingContext) {
+        Runnable runnable = () -> {
+            try {
+                mStatusBarService.disableForUser(flags, mStatusBarDisableToken,
+                        mContext.getPackageName(),
+                        mSelectedUserInteractor.getSelectedUserId());
+            } catch (RemoteException e) {
+                Log.d(TAG, loggingContext + " " + flags, e);
+            }
+        };
+        if (com.android.systemui.Flags.bouncerUiRevamp()) {
+            mUiBgExecutor.execute(runnable);
+        } else {
+            runnable.run();
+        }
+    }
+
     /**
      * Handle message sent by {@link #resetStateLocked}
      * @see #RESET
@@ -4099,12 +4117,23 @@
                 || aodShowing != mAodShowing || forceCallbacks;
         mShowing = showing;
         mAodShowing = aodShowing;
-        if (notifyDefaultDisplayCallbacks) {
-            notifyDefaultDisplayCallbacks(showing);
+
+        if (KeyguardWmReorderAtmsCalls.isEnabled()) {
+            if (updateActivityLockScreenState) {
+                updateActivityLockScreenState(showing, aodShowing, reason);
+            }
+            if (notifyDefaultDisplayCallbacks) {
+                notifyDefaultDisplayCallbacks(showing);
+            }
+        } else {
+            if (notifyDefaultDisplayCallbacks) {
+                notifyDefaultDisplayCallbacks(showing);
+            }
+            if (updateActivityLockScreenState) {
+                updateActivityLockScreenState(showing, aodShowing, reason);
+            }
         }
-        if (updateActivityLockScreenState) {
-            updateActivityLockScreenState(showing, aodShowing, reason);
-        }
+
     }
 
     private void notifyDefaultDisplayCallbacks(boolean showing) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardWmReorderAtmsCalls.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardWmReorderAtmsCalls.kt
new file mode 100644
index 0000000..7ac5281
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardWmReorderAtmsCalls.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard
+
+import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading or using the keyguard wm state refactor flag state. */
+@Suppress("NOTHING_TO_INLINE")
+object KeyguardWmReorderAtmsCalls {
+    /** The aconfig flag name */
+    const val FLAG_NAME = Flags.FLAG_KEYGUARD_WM_REORDER_ATMS_CALLS
+
+    /** A token used for dependency declaration */
+    val token: FlagToken
+        get() = FlagToken(FLAG_NAME, isEnabled)
+
+    /** Is the refactor enabled */
+    @JvmStatic
+    inline val isEnabled
+        get() = Flags.keyguardWmReorderAtmsCalls()
+
+    /**
+     * Called to ensure code is only run when the flag is enabled. This protects users from the
+     * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+     * build to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun isUnexpectedlyInLegacyMode() =
+        RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+    /**
+     * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+     * the flag is enabled to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardWmStateRefactor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardWmStateRefactor.kt
index ddccc5d..41d14b9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardWmStateRefactor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardWmStateRefactor.kt
@@ -20,7 +20,16 @@
 import com.android.systemui.flags.FlagToken
 import com.android.systemui.flags.RefactorFlagUtils
 
-/** Helper for reading or using the keyguard wm state refactor flag state. */
+/**
+ * Helper for reading or using the keyguard_wm_state_refactor flag state.
+ *
+ * keyguard_wm_state_refactor works both with and without flexiglass (scene_container), but
+ * flexiglass requires keyguard_wm_state_refactor. For this reason, this class will return isEnabled
+ * if either keyguard_wm_state_refactor OR scene_container are enabled. This enables us to roll out
+ * keyguard_wm_state_refactor independently of scene_container, while also ensuring that
+ * scene_container rolling out ahead of keyguard_wm_state_refactor causes code gated by
+ * KeyguardWmStateRefactor to be enabled as well.
+ */
 @Suppress("NOTHING_TO_INLINE")
 object KeyguardWmStateRefactor {
     /** The aconfig flag name */
@@ -30,10 +39,9 @@
     val token: FlagToken
         get() = FlagToken(FLAG_NAME, isEnabled)
 
-    /** Is the refactor enabled */
     @JvmStatic
     inline val isEnabled
-        get() = Flags.keyguardWmStateRefactor()
+        get() = Flags.keyguardWmStateRefactor() || Flags.sceneContainer()
 
     /**
      * Called to ensure code is only run when the flag is enabled. This protects users from the
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
index 6b1248b..1fe6eb9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
@@ -42,6 +42,7 @@
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.classifier.FalsingModule;
 import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor;
+import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor;
 import com.android.systemui.communal.ui.viewmodel.CommunalTransitionViewModel;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
@@ -182,6 +183,7 @@
             KeyguardInteractor keyguardInteractor,
             KeyguardTransitionBootInteractor transitionBootInteractor,
             Lazy<CommunalSceneInteractor> communalSceneInteractor,
+            Lazy<CommunalSettingsInteractor> communalSettingsInteractor,
             WindowManagerOcclusionManager windowManagerOcclusionManager) {
         return new KeyguardViewMediator(
                 context,
@@ -234,6 +236,7 @@
                 keyguardInteractor,
                 transitionBootInteractor,
                 communalSceneInteractor,
+                communalSettingsInteractor,
                 windowManagerOcclusionManager);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
index f8c7a86..f4e804a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
 import com.android.systemui.communal.shared.model.CommunalScenes
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyguard.KeyguardWmStateRefactor
@@ -62,6 +63,7 @@
     override val internalTransitionInteractor: InternalKeyguardTransitionInteractor,
     transitionInteractor: KeyguardTransitionInteractor,
     @Background private val scope: CoroutineScope,
+    @Application private val applicationScope: CoroutineScope,
     @Background bgDispatcher: CoroutineDispatcher,
     @Main mainDispatcher: CoroutineDispatcher,
     keyguardInteractor: KeyguardInteractor,
@@ -175,7 +177,7 @@
     private fun listenForLockscreenToPrimaryBouncerDragging() {
         if (SceneContainerFlag.isEnabled) return
         var transitionId: UUID? = null
-        scope.launch("$TAG#listenForLockscreenToPrimaryBouncerDragging") {
+        applicationScope.launch("$TAG#listenForLockscreenToPrimaryBouncerDragging") {
             shadeRepository.legacyShadeExpansion.collect { shadeExpansion ->
                 val statusBarState = keyguardInteractor.statusBarState.value
                 val isKeyguardUnlocked = keyguardInteractor.isKeyguardDismissible.value
@@ -204,7 +206,7 @@
                                 id,
                                 // This maps the shadeExpansion to a much faster curve, to match
                                 // the existing logic
-                                1f - MathUtils.constrainedMap(0f, 1f, 0.95f, 1f, shadeExpansion),
+                                1f - MathUtils.constrainedMap(0f, 1f, 0.88f, 1f, shadeExpansion),
                                 nextState,
                             )
                         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractor.kt
index 0a4022a..e6f8406 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractor.kt
@@ -159,7 +159,10 @@
                 if (alternateBouncerInteractor.canShowAlternateBouncer.value) {
                     alternateBouncerInteractor.forceShow()
                 } else {
-                    primaryBouncerInteractor.show(true)
+                    primaryBouncerInteractor.show(
+                        true,
+                        "KeyguardDismissInteractor#dismissKeyguardWithCallback",
+                    )
                 }
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index 7977000..2d5ff61 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -19,7 +19,6 @@
 import android.graphics.Point
 import android.util.Log
 import android.util.MathUtils
-import com.android.app.animation.Interpolators
 import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository
 import com.android.systemui.common.shared.model.NotificationContainerBounds
 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
@@ -371,9 +370,11 @@
                         currentKeyguardState == LOCKSCREEN &&
                         legacyShadeExpansion != 1f
                 ) {
-                    emit(MathUtils.constrainedMap(0f, 1f, 0.95f, 1f, legacyShadeExpansion))
+                    emit(MathUtils.constrainedMap(0f, 1f, 0.82f, 1f, legacyShadeExpansion))
                 } else if (
-                    (legacyShadeExpansion == 0f || legacyShadeExpansion == 1f) && !onGlanceableHub
+                    !onGlanceableHub &&
+                        isKeyguardDismissible &&
+                        (legacyShadeExpansion == 0f || legacyShadeExpansion == 1f)
                 ) {
                     // Resets alpha state
                     emit(1f)
@@ -401,15 +402,7 @@
                         // 0f and 1f need to be ignored in the legacy shade expansion. These can
                         // flip arbitrarily as the legacy shade is reset, and would cause the
                         // translation value to jump around unexpectedly.
-                        emit(
-                            MathUtils.lerp(
-                                translationDistance,
-                                0,
-                                Interpolators.FAST_OUT_LINEAR_IN.getInterpolation(
-                                    legacyShadeExpansion
-                                ),
-                            )
-                        )
+                        emit(MathUtils.lerp(translationDistance, 0, legacyShadeExpansion))
                     }
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt
index 6d9b276..ced96e9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt
@@ -136,7 +136,10 @@
                 return true
             }
             StatusBarState.KEYGUARD -> {
-                statusBarKeyguardViewManager.showPrimaryBouncer(true)
+                statusBarKeyguardViewManager.showPrimaryBouncer(
+                    true,
+                    "KeyguardKeyEventInteractor#collapseShadeLockedOrShowPrimaryBouncer",
+                )
                 return true
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt
index 68d595e..b4e9d82 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt
@@ -196,39 +196,50 @@
                 .distinctUntilChanged()
         }
 
-    private val lockscreenVisibilityWithScenes =
-        combine(
-                sceneInteractor.get().transitionState.flatMapLatestConflated {
-                    when (it) {
-                        is Idle -> {
-                            when (it.currentScene) {
-                                in keyguardContent -> flowOf(true)
-                                in nonKeyguardContent -> flowOf(false)
-                                in keyguardAgnosticContent -> isDeviceNotEnteredDirectly
-                                else ->
-                                    throw IllegalStateException("Unknown scene: ${it.currentScene}")
+    private val lockscreenVisibilityWithScenes: Flow<Boolean> =
+        // The scene container visibility into account as that will be forced to false when the
+        // device isn't yet provisioned (e.g. still in the setup wizard).
+        sceneInteractor.get().isVisible.flatMapLatestConflated { isVisible ->
+            if (isVisible) {
+                combine(
+                        sceneInteractor.get().transitionState.flatMapLatestConflated {
+                            when (it) {
+                                is Idle ->
+                                    when (it.currentScene) {
+                                        in keyguardContent -> flowOf(true)
+                                        in nonKeyguardContent -> flowOf(false)
+                                        in keyguardAgnosticContent -> isDeviceNotEnteredDirectly
+                                        else ->
+                                            throw IllegalStateException(
+                                                "Unknown scene: ${it.currentScene}"
+                                            )
+                                    }
+                                is Transition ->
+                                    when {
+                                        it.isTransitioningSets(from = keyguardContent) ->
+                                            flowOf(true)
+                                        it.isTransitioningSets(from = nonKeyguardContent) ->
+                                            flowOf(false)
+                                        it.isTransitioningSets(from = keyguardAgnosticContent) ->
+                                            isDeviceNotEnteredDirectly
+                                        else ->
+                                            throw IllegalStateException(
+                                                "Unknown content: ${it.fromContent}"
+                                            )
+                                    }
                             }
-                        }
-                        is Transition -> {
-                            when {
-                                it.isTransitioningSets(from = keyguardContent) -> flowOf(true)
-                                it.isTransitioningSets(from = nonKeyguardContent) -> flowOf(false)
-                                it.isTransitioningSets(from = keyguardAgnosticContent) ->
-                                    isDeviceNotEnteredDirectly
-                                else ->
-                                    throw IllegalStateException(
-                                        "Unknown content: ${it.fromContent}"
-                                    )
-                            }
-                        }
+                        },
+                        wakeToGoneInteractor.canWakeDirectlyToGone,
+                        ::Pair,
+                    )
+                    .map { (lockscreenVisibilityByTransitionState, canWakeDirectlyToGone) ->
+                        lockscreenVisibilityByTransitionState && !canWakeDirectlyToGone
                     }
-                },
-                wakeToGoneInteractor.canWakeDirectlyToGone,
-                ::Pair,
-            )
-            .map { (lockscreenVisibilityByTransitionState, canWakeDirectlyToGone) ->
-                lockscreenVisibilityByTransitionState && !canWakeDirectlyToGone
+            } else {
+                // Lockscreen is never visible when the scene container is invisible.
+                flowOf(false)
             }
+        }
 
     private val lockscreenVisibilityLegacy =
         combine(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
index 70a827d..1ea47ec 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
@@ -39,6 +39,7 @@
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.util.kotlin.DisposableHandles
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.DisposableHandle
 
@@ -56,6 +57,7 @@
     @JvmStatic
     fun bind(
         applicationScope: CoroutineScope,
+        mainImmediateDispatcher: CoroutineDispatcher,
         view: DeviceEntryIconView,
         viewModel: DeviceEntryIconViewModel,
         fgViewModel: DeviceEntryForegroundViewModel,
@@ -96,6 +98,32 @@
             }
 
         disposables +=
+            view.repeatWhenAttached(mainImmediateDispatcher) {
+                repeatOnLifecycle(Lifecycle.State.CREATED) {
+                    launch("$TAG#viewModel.useBackgroundProtection") {
+                        viewModel.useBackgroundProtection.collect { useBackgroundProtection ->
+                            if (useBackgroundProtection) {
+                                bgView.visibility = View.VISIBLE
+                            } else {
+                                bgView.visibility = View.GONE
+                            }
+                        }
+                    }
+                    launch("$TAG#viewModel.burnInOffsets") {
+                        viewModel.burnInOffsets.collect { burnInOffsets ->
+                            view.translationX = burnInOffsets.x.toFloat()
+                            view.translationY = burnInOffsets.y.toFloat()
+                            view.aodFpDrawable.progress = burnInOffsets.progress
+                        }
+                    }
+
+                    launch("$TAG#viewModel.deviceEntryViewAlpha") {
+                        viewModel.deviceEntryViewAlpha.collect { alpha -> view.alpha = alpha }
+                    }
+                }
+            }
+
+        disposables +=
             view.repeatWhenAttached {
                 // Repeat on CREATED so that the view will always observe the entire
                 // GONE => AOD transition (even though the view may not be visible until the middle
@@ -152,26 +180,6 @@
                             }
                         }
                     }
-                    launch("$TAG#viewModel.useBackgroundProtection") {
-                        viewModel.useBackgroundProtection.collect { useBackgroundProtection ->
-                            if (useBackgroundProtection) {
-                                bgView.visibility = View.VISIBLE
-                            } else {
-                                bgView.visibility = View.GONE
-                            }
-                        }
-                    }
-                    launch("$TAG#viewModel.burnInOffsets") {
-                        viewModel.burnInOffsets.collect { burnInOffsets ->
-                            view.translationX = burnInOffsets.x.toFloat()
-                            view.translationY = burnInOffsets.y.toFloat()
-                            view.aodFpDrawable.progress = burnInOffsets.progress
-                        }
-                    }
-
-                    launch("$TAG#viewModel.deviceEntryViewAlpha") {
-                        viewModel.deviceEntryViewAlpha.collect { alpha -> view.alpha = alpha }
-                    }
                 }
             }
 
@@ -212,7 +220,7 @@
             }
 
         disposables +=
-            bgView.repeatWhenAttached {
+            bgView.repeatWhenAttached(mainImmediateDispatcher) {
                 repeatOnLifecycle(Lifecycle.State.CREATED) {
                     launch("$TAG#bgViewModel.alpha") {
                         bgViewModel.alpha.collect { alpha -> bgView.alpha = alpha }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
index aeb3270..60460bf 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
@@ -135,7 +135,10 @@
                         } else if (
                             event.action == MotionEvent.ACTION_UP && !event.isTouchscreenSource()
                         ) {
-                            statusBarKeyguardViewManager?.showBouncer(true)
+                            statusBarKeyguardViewManager?.showBouncer(
+                                true,
+                                "KeyguardRootViewBinder: click on lockscreen",
+                            )
                             consumed = true
                         }
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt
index 58d482b..9c8f04b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt
@@ -28,6 +28,7 @@
 import com.android.systemui.biometrics.AuthController
 import com.android.systemui.customization.R as customR
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.shared.model.KeyguardSection
@@ -48,6 +49,7 @@
 import com.android.systemui.statusbar.VibratorHelper
 import dagger.Lazy
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.DisposableHandle
 
@@ -56,6 +58,7 @@
 @Inject
 constructor(
     @Application private val applicationScope: CoroutineScope,
+    @Main private val mainDispatcher: CoroutineDispatcher,
     private val authController: AuthController,
     private val windowManager: WindowManager,
     @ShadeDisplayAware private val context: Context,
@@ -91,6 +94,7 @@
             disposableHandle =
                 DeviceEntryIconViewBinder.bind(
                     applicationScope,
+                    mainDispatcher,
                     it,
                     deviceEntryIconViewModel.get(),
                     deviceEntryForegroundViewModel.get(),
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt
index 9038922..803e2c0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt
@@ -106,7 +106,10 @@
         }
 
     fun onTapped() {
-        statusBarKeyguardViewManager.showPrimaryBouncer(/* scrimmed */ true)
+        statusBarKeyguardViewManager.showPrimaryBouncer(
+            /* scrimmed */ true,
+            "AlternateBouncerUdfpsIconViewModel#onTapped",
+        )
     }
 
     val bgColor: Flow<Int> = deviceEntryBackgroundViewModel.color
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt
index cff6511..45f43bb 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt
@@ -47,7 +47,9 @@
 
     /** Reports the alternate bouncer visible state if the scene container flag is enabled. */
     val isVisible: Flow<Boolean> =
-        alternateBouncerInteractor.get().isVisible.onEach { SceneContainerFlag.unsafeAssertInNewMode() }
+        alternateBouncerInteractor.get().isVisible.onEach {
+            SceneContainerFlag.unsafeAssertInNewMode()
+        }
 
     /** Progress to a fully transitioned alternate bouncer. 1f represents fully transitioned. */
     val transitionToAlternateBouncerProgress: Flow<Float> =
@@ -63,7 +65,10 @@
         transitionToAlternateBouncerProgress.map { it == 1f }.distinctUntilChanged()
 
     fun onTapped() {
-        statusBarKeyguardViewManager.showPrimaryBouncer(/* scrimmed */ true)
+        statusBarKeyguardViewManager.showPrimaryBouncer(
+            /* scrimmed */ true,
+            "AlternateBouncerViewModel#onTapped",
+        )
     }
 
     fun onRemovedFromWindow() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt
index 13cd583..9b4bd67 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt
@@ -257,7 +257,9 @@
         if (SceneContainerFlag.isEnabled) {
             deviceEntryInteractor.attemptDeviceEntry()
         } else {
-            keyguardViewController.get().showPrimaryBouncer(/* scrim */ true)
+            keyguardViewController
+                .get()
+                .showPrimaryBouncer(/* scrim */ true, "DeviceEntryIconViewModel#onUserInteraction")
         }
         deviceEntrySourceInteractor.attemptEnterDeviceFromDeviceEntryIcon()
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt
index 9312bca..a0458f0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt
@@ -64,7 +64,7 @@
 
     val shortcutsAlpha: Flow<Float> =
         transitionAnimation.sharedFlow(
-            duration = FromLockscreenTransitionInteractor.TO_PRIMARY_BOUNCER_DURATION,
+            duration = 200.milliseconds,
             onStep = alphaForAnimationStep,
             // Rapid swipes to bouncer, and may end up skipping intermediate values that would've
             // caused a complete fade out of lockscreen elements. Ensure it goes to 0f.
diff --git a/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt b/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt
index 4559a7a..7b3f4c6 100644
--- a/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt
+++ b/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt
@@ -79,6 +79,7 @@
                 SceneContainerPluginState(
                     scene = idleState.currentScene,
                     overlays = idleState.currentOverlays,
+                    isVisible = sceneInteractor.get().isVisible.value,
                     invisibleDueToOcclusion = invisibleDueToOcclusion,
                 )
             )
@@ -100,12 +101,17 @@
             mapOf<Long, (SceneContainerPluginState) -> Boolean>(
                 SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE to
                     {
-                        it.scene != Scenes.Gone || it.overlays.isNotEmpty()
+                        when {
+                            !it.isVisible -> false
+                            it.scene != Scenes.Gone -> true
+                            it.overlays.isNotEmpty() -> true
+                            else -> false
+                        }
                     },
                 SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED to
                     {
                         when {
-                            it.invisibleDueToOcclusion -> false
+                            !it.isVisible -> false
                             it.scene == Scenes.Lockscreen -> true
                             it.scene == Scenes.Shade -> true
                             Overlays.NotificationsShade in it.overlays -> true
@@ -114,19 +120,23 @@
                     },
                 SYSUI_STATE_QUICK_SETTINGS_EXPANDED to
                     {
-                        it.scene == Scenes.QuickSettings ||
-                            Overlays.QuickSettingsShade in it.overlays
+                        when {
+                            !it.isVisible -> false
+                            it.scene == Scenes.QuickSettings -> true
+                            Overlays.QuickSettingsShade in it.overlays -> true
+                            else -> false
+                        }
                     },
-                SYSUI_STATE_BOUNCER_SHOWING to { Overlays.Bouncer in it.overlays },
+                SYSUI_STATE_BOUNCER_SHOWING to { it.isVisible && Overlays.Bouncer in it.overlays },
                 SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING to
                     {
-                        it.scene == Scenes.Lockscreen && !it.invisibleDueToOcclusion
+                        it.isVisible && it.scene == Scenes.Lockscreen
                     },
                 SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED to
                     {
                         it.scene == Scenes.Lockscreen && it.invisibleDueToOcclusion
                     },
-                SYSUI_STATE_COMMUNAL_HUB_SHOWING to { it.scene == Scenes.Communal },
+                SYSUI_STATE_COMMUNAL_HUB_SHOWING to { it.isVisible && it.scene == Scenes.Communal },
             )
     }
 
@@ -134,5 +144,6 @@
         val scene: SceneKey,
         val overlays: Set<OverlayKey>,
         val invisibleDueToOcclusion: Boolean,
+        val isVisible: Boolean,
     )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/model/SysUiStateExt.kt b/packages/SystemUI/src/com/android/systemui/model/SysUiStateExt.kt
index 1e18f24..1955356 100644
--- a/packages/SystemUI/src/com/android/systemui/model/SysUiStateExt.kt
+++ b/packages/SystemUI/src/com/android/systemui/model/SysUiStateExt.kt
@@ -16,8 +16,6 @@
 
 package com.android.systemui.model
 
-import com.android.systemui.dagger.qualifiers.DisplayId
-
 /**
  * In-bulk updates multiple flag values and commits the update.
  *
@@ -32,16 +30,8 @@
  *     SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING to (sceneKey == Scenes.Lockscreen),
  * )
  * ```
- *
- * You can inject [displayId] by injecting it using:
- * ```
- *     @DisplayId private val displayId: Int`,
- * ```
  */
-fun SysUiState.updateFlags(
-    @DisplayId displayId: Int,
-    vararg flagValuePairs: Pair<Long, Boolean>,
-) {
+fun SysUiState.updateFlags(vararg flagValuePairs: Pair<Long, Boolean>) {
     flagValuePairs.forEach { (flag, enabled) -> setFlag(flag, enabled) }
-    commitUpdate(displayId)
+    commitUpdate()
 }
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
index 8dc27bf4..0809401 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
@@ -86,7 +86,10 @@
      */
     private fun initializeKeyGestureEventHandler() {
         if (useKeyGestureEventHandler()) {
-            inputManager.registerKeyGestureEventHandler(callbacks)
+            inputManager.registerKeyGestureEventHandler(
+                listOf(KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES),
+                callbacks,
+            )
         }
     }
 
@@ -156,11 +159,8 @@
                 controller.updateNoteTaskForCurrentUserAndManagedProfiles()
             }
 
-            override fun handleKeyGestureEvent(
-                event: KeyGestureEvent,
-                focusedToken: IBinder?,
-            ): Boolean {
-                return this@NoteTaskInitializer.handleKeyGestureEvent(event)
+            override fun handleKeyGestureEvent(event: KeyGestureEvent, focusedToken: IBinder?) {
+                this@NoteTaskInitializer.handleKeyGestureEvent(event)
             }
         }
 
@@ -202,23 +202,19 @@
         return !isMultiPress && !isLongPress
     }
 
-    private fun handleKeyGestureEvent(event: KeyGestureEvent): Boolean {
-        // This method is on input hot path and should be kept lightweight. Shift all complex
-        // processing onto background executor wherever possible.
+    private fun handleKeyGestureEvent(event: KeyGestureEvent) {
         if (event.keyGestureType != KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES) {
-            return false
+            return
         }
         debugLog {
             "handleKeyGestureEvent: Received OPEN_NOTES gesture event from keycodes: " +
                 event.keycodes.contentToString()
         }
         if (event.keycodes.size == 1 && event.keycodes[0] == KEYCODE_STYLUS_BUTTON_TAIL) {
-            debugLog { "Note task triggered by stylus tail button" }
             backgroundExecutor.execute { controller.showNoteTask(TAIL_BUTTON) }
-            return true
+        } else {
+            backgroundExecutor.execute { controller.showNoteTask(KEYBOARD_SHORTCUT) }
         }
-        backgroundExecutor.execute { controller.showNoteTask(KEYBOARD_SHORTCUT) }
-        return true
     }
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
index b889c3e..9f04f69 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
@@ -19,6 +19,8 @@
 import android.annotation.SuppressLint
 import android.content.Context
 import android.content.res.Configuration
+import android.graphics.Canvas
+import android.graphics.Path
 import android.graphics.PointF
 import android.graphics.Rect
 import android.os.Bundle
@@ -125,7 +127,6 @@
 import com.android.systemui.qs.composefragment.SceneKeys.toIdleSceneKey
 import com.android.systemui.qs.composefragment.ui.GridAnchor
 import com.android.systemui.qs.composefragment.ui.NotificationScrimClipParams
-import com.android.systemui.qs.composefragment.ui.notificationScrimClip
 import com.android.systemui.qs.composefragment.ui.quickQuickSettingsToQuickSettings
 import com.android.systemui.qs.composefragment.ui.toEditMode
 import com.android.systemui.qs.composefragment.viewmodel.QSFragmentComposeViewModel
@@ -241,7 +242,7 @@
             FrameLayoutTouchPassthrough(
                 context,
                 { notificationScrimClippingParams.isEnabled },
-                { notificationScrimClippingParams.params.top },
+                snapshotFlow { notificationScrimClippingParams.params },
                 // Only allow scrolling when we are fully expanded. That way, we don't intercept
                 // swipes in lockscreen (when somehow QS is receiving touches).
                 { (scrollState.canScrollForward && viewModel.isQsFullyExpanded) || isCustomizing },
@@ -276,11 +277,6 @@
                                     }
                                 }
                                 .graphicsLayer { alpha = viewModel.viewAlpha }
-                                .thenIf(notificationScrimClippingParams.isEnabled) {
-                                    Modifier.notificationScrimClip {
-                                        notificationScrimClippingParams.params
-                                    }
-                                }
                                 .thenIf(!Flags.notificationShadeBlur()) {
                                     Modifier.offset {
                                         IntOffset(
@@ -1061,17 +1057,75 @@
 private class FrameLayoutTouchPassthrough(
     context: Context,
     private val clippingEnabledProvider: () -> Boolean,
-    private val clippingTopProvider: () -> Int,
+    private val clippingParams: Flow<NotificationScrimClipParams>,
     private val canScrollForwardQs: () -> Boolean,
     private val emitMotionEventForFalsing: () -> Unit,
 ) : FrameLayout(context) {
+
+    init {
+        repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                clippingParams.collect { currentClipParams = it }
+            }
+        }
+    }
+
+    private val currentClippingPath = Path()
+    private var lastWidth = -1
+        set(value) {
+            if (field != value) {
+                field = value
+                updateClippingPath()
+            }
+        }
+
+    private var currentClipParams = NotificationScrimClipParams()
+        set(value) {
+            if (field != value) {
+                field = value
+                updateClippingPath()
+            }
+        }
+
+    private fun updateClippingPath() {
+        currentClippingPath.rewind()
+        if (clippingEnabledProvider()) {
+            val right = width + currentClipParams.rightInset
+            val left = -currentClipParams.leftInset
+            val top = currentClipParams.top
+            val bottom = currentClipParams.bottom
+            currentClippingPath.addRoundRect(
+                left.toFloat(),
+                top.toFloat(),
+                right.toFloat(),
+                bottom.toFloat(),
+                currentClipParams.radius.toFloat(),
+                currentClipParams.radius.toFloat(),
+                Path.Direction.CW,
+            )
+        }
+        invalidate()
+    }
+
+    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+        super.onLayout(changed, left, top, right, bottom)
+        lastWidth = right - left
+    }
+
+    override fun dispatchDraw(canvas: Canvas) {
+        if (!currentClippingPath.isEmpty) {
+            canvas.clipOutPath(currentClippingPath)
+        }
+        super.dispatchDraw(canvas)
+    }
+
     override fun isTransformedTouchPointInView(
         x: Float,
         y: Float,
         child: View?,
         outLocalPoint: PointF?,
     ): Boolean {
-        return if (clippingEnabledProvider() && y + translationY > clippingTopProvider()) {
+        return if (clippingEnabledProvider() && y + translationY > currentClipParams.top) {
             false
         } else {
             super.isTransformedTouchPointInView(x, y, child, outLocalPoint)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt
deleted file mode 100644
index 3049a40..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.qs.composefragment.ui
-
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.drawWithContent
-import androidx.compose.ui.geometry.CornerRadius
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.graphics.BlendMode
-import androidx.compose.ui.graphics.ClipOp
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.CompositingStrategy
-import androidx.compose.ui.graphics.drawscope.clipRect
-import androidx.compose.ui.graphics.graphicsLayer
-
-/**
- * Clipping modifier for clipping out the notification scrim as it slides over QS. It will clip out
- * ([ClipOp.Difference]) a `RoundRect(-leftInset, top, width + rightInset, bottom, radius, radius)`
- * from the QS container.
- */
-fun Modifier.notificationScrimClip(clipParams: () -> NotificationScrimClipParams): Modifier {
-    return this.graphicsLayer { compositingStrategy = CompositingStrategy.Offscreen }
-        .drawWithContent {
-            drawContent()
-            val params = clipParams()
-            val left = -params.leftInset.toFloat()
-            val right = size.width + params.rightInset.toFloat()
-            val top = params.top.toFloat()
-            val bottom = params.bottom.toFloat()
-            val clipSize = Size(right - left, bottom - top)
-            if (!clipSize.isEmpty()) {
-                clipRect {
-                    drawRoundRect(
-                        color = Color.Black,
-                        cornerRadius = CornerRadius(params.radius.toFloat()),
-                        blendMode = BlendMode.Clear,
-                        topLeft = Offset(left, top),
-                        size = Size(right - left, bottom - top),
-                    )
-                }
-            }
-        }
-}
-
-/** Params for [notificationScrimClip]. */
-data class NotificationScrimClipParams(
-    val top: Int = 0,
-    val bottom: Int = 0,
-    val leftInset: Int = 0,
-    val rightInset: Int = 0,
-    val radius: Int = 0,
-)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClipParams.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClipParams.kt
new file mode 100644
index 0000000..db320d3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClipParams.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.composefragment.ui
+
+/** Params for [notificationScrimClip]. */
+data class NotificationScrimClipParams(
+    val top: Int = 0,
+    val bottom: Int = 0,
+    val leftInset: Int = 0,
+    val rightInset: Int = 0,
+    val radius: Int = 0,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt
index 405ce8a..005c8b2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt
@@ -35,7 +35,6 @@
 import androidx.compose.ui.draganddrop.toAndroidDragEvent
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.unit.IntRect
-import androidx.compose.ui.unit.center
 import androidx.compose.ui.unit.toRect
 import com.android.systemui.qs.panels.shared.model.SizedTile
 import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
@@ -44,6 +43,7 @@
 /** Holds the [TileSpec] of the tile being moved and receives drag and drop events. */
 interface DragAndDropState {
     val draggedCell: SizedTile<EditTileViewModel>?
+    val isDraggedCellRemovable: Boolean
     val draggedPosition: Offset
     val dragInProgress: Boolean
     val dragType: DragType?
@@ -76,7 +76,7 @@
 @Composable
 fun Modifier.dragAndDropRemoveZone(
     dragAndDropState: DragAndDropState,
-    onDrop: (TileSpec) -> Unit,
+    onDrop: (TileSpec, removalEnabled: Boolean) -> Unit,
 ): Modifier {
     val target =
         remember(dragAndDropState) {
@@ -87,13 +87,15 @@
 
                 override fun onDrop(event: DragAndDropEvent): Boolean {
                     return dragAndDropState.draggedCell?.let {
-                        onDrop(it.tile.tileSpec)
+                        onDrop(it.tile.tileSpec, dragAndDropState.isDraggedCellRemovable)
                         dragAndDropState.onDrop()
                         true
                     } ?: false
                 }
 
                 override fun onEntered(event: DragAndDropEvent) {
+                    if (!dragAndDropState.isDraggedCellRemovable) return
+
                     dragAndDropState.movedOutOfBounds()
                 }
             }
@@ -168,10 +170,10 @@
 }
 
 private fun insertAfter(item: LazyGridItemInfo, offset: Offset): Boolean {
-    // We want to insert the tile after the target if we're aiming at the right side of a large tile
+    // We want to insert the tile after the target if we're aiming at the end of a large tile
     // TODO(ostonge): Verify this behavior in RTL
-    val itemCenter = item.offset + item.size.center
-    return item.span != 1 && offset.x > itemCenter.x
+    val itemCenter = item.offset.x + item.size.width * .75
+    return item.span != 1 && offset.x > itemCenter
 }
 
 @OptIn(ExperimentalFoundationApi::class)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt
index 8688558..70f1674 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt
@@ -25,6 +25,7 @@
 import androidx.compose.runtime.toMutableStateList
 import androidx.compose.ui.geometry.Offset
 import com.android.systemui.qs.panels.shared.model.SizedTile
+import com.android.systemui.qs.panels.ui.compose.selection.PlacementEvent
 import com.android.systemui.qs.panels.ui.model.GridCell
 import com.android.systemui.qs.panels.ui.model.TileGridCell
 import com.android.systemui.qs.panels.ui.model.toGridCells
@@ -60,6 +61,11 @@
     override var dragType by mutableStateOf<DragType?>(null)
         private set
 
+    // A dragged cell can be removed if it was added in the drag movement OR if it's marked as
+    // removable
+    override val isDraggedCellRemovable: Boolean
+        get() = dragType == DragType.Add || draggedCell?.tile?.isRemovable ?: false
+
     override val dragInProgress: Boolean
         get() = draggedCell != null
 
@@ -76,10 +82,16 @@
         return _tiles.indexOfFirst { it is TileGridCell && it.tile.tileSpec == tileSpec }
     }
 
+    fun isRemovable(tileSpec: TileSpec): Boolean {
+        return _tiles.find {
+            it is TileGridCell && it.tile.tileSpec == tileSpec && it.tile.isRemovable
+        } != null
+    }
+
     /** Resize the tile corresponding to the [TileSpec] to [toIcon] */
     fun resizeTile(tileSpec: TileSpec, toIcon: Boolean) {
         val fromIndex = indexOf(tileSpec)
-        if (fromIndex != -1) {
+        if (fromIndex != INVALID_INDEX) {
             val cell = _tiles[fromIndex] as TileGridCell
 
             if (cell.isIcon == toIcon) return
@@ -97,9 +109,6 @@
     override fun onStarted(cell: SizedTile<EditTileViewModel>, dragType: DragType) {
         draggedCell = cell
         this.dragType = dragType
-
-        // Add spacers to the grid to indicate where the user can move a tile
-        regenerateGrid()
     }
 
     override fun onTargeting(target: Int, insertAfter: Boolean) {
@@ -111,7 +120,7 @@
         }
 
         val insertionIndex = if (insertAfter) target + 1 else target
-        if (fromIndex != -1) {
+        if (fromIndex != INVALID_INDEX) {
             val cell = _tiles.removeAt(fromIndex)
             regenerateGrid()
             _tiles.add(insertionIndex.coerceIn(0, _tiles.size), cell)
@@ -149,6 +158,43 @@
         regenerateGrid()
     }
 
+    /**
+     * Return the appropriate index to move the tile to for the placement [event]
+     *
+     * The grid includes spacers. As a result, indexes from the grid need to be translated to the
+     * corresponding index from [currentTileSpecs].
+     */
+    fun targetIndexForPlacement(event: PlacementEvent): Int {
+        val currentTileSpecs = tileSpecs()
+        return when (event) {
+            is PlacementEvent.PlaceToTileSpec -> {
+                currentTileSpecs.indexOf(event.targetSpec)
+            }
+            is PlacementEvent.PlaceToIndex -> {
+                if (event.targetIndex >= _tiles.size) {
+                    currentTileSpecs.size
+                } else if (event.targetIndex <= 0) {
+                    0
+                } else {
+                    // The index may point to a spacer, so first find the first tile located
+                    // after index, then use its position as a target
+                    val targetTile =
+                        _tiles.subList(event.targetIndex, _tiles.size).firstOrNull {
+                            it is TileGridCell
+                        } as? TileGridCell
+
+                    if (targetTile == null) {
+                        currentTileSpecs.size
+                    } else {
+                        val targetIndex = currentTileSpecs.indexOf(targetTile.tile.tileSpec)
+                        val fromIndex = currentTileSpecs.indexOf(event.movingSpec)
+                        if (fromIndex < targetIndex) targetIndex - 1 else targetIndex
+                    }
+                }
+            }
+        }
+    }
+
     /** Regenerate the list of [GridCell] with their new potential rows */
     private fun regenerateGrid() {
         _tiles.filterIsInstance<TileGridCell>().toGridCells(columns).let {
@@ -170,4 +216,8 @@
             _tiles.addAll(it)
         }
     }
+
+    companion object {
+        const val INVALID_INDEX = -1
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
index 46f05d0..f8eaa6c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
@@ -19,6 +19,7 @@
 package com.android.systemui.qs.panels.ui.compose.infinitegrid
 
 import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.animateColorAsState
 import androidx.compose.animation.animateContentSize
 import androidx.compose.animation.core.LinearEasing
 import androidx.compose.animation.core.animateDpAsState
@@ -34,6 +35,7 @@
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.clipScrollableContainer
 import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.detectTapGestures
 import androidx.compose.foundation.layout.Arrangement.spacedBy
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.BoxScope
@@ -78,6 +80,7 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.State
 import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.key
@@ -96,6 +99,7 @@
 import androidx.compose.ui.geometry.isSpecified
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.layout.MeasureScope
 import androidx.compose.ui.layout.layout
 import androidx.compose.ui.layout.onGloballyPositioned
@@ -125,6 +129,7 @@
 import com.android.systemui.qs.panels.ui.compose.DragAndDropState
 import com.android.systemui.qs.panels.ui.compose.DragType
 import com.android.systemui.qs.panels.ui.compose.EditTileListState
+import com.android.systemui.qs.panels.ui.compose.EditTileListState.Companion.INVALID_INDEX
 import com.android.systemui.qs.panels.ui.compose.dragAndDropRemoveZone
 import com.android.systemui.qs.panels.ui.compose.dragAndDropTileList
 import com.android.systemui.qs.panels.ui.compose.dragAndDropTileSource
@@ -152,7 +157,6 @@
 import com.android.systemui.qs.panels.ui.model.GridCell
 import com.android.systemui.qs.panels.ui.model.SpacerGridCell
 import com.android.systemui.qs.panels.ui.model.TileGridCell
-import com.android.systemui.qs.panels.ui.viewmodel.AvailableEditActions
 import com.android.systemui.qs.panels.ui.viewmodel.BounceableTileViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
 import com.android.systemui.qs.pipeline.shared.TileSpec
@@ -161,7 +165,6 @@
 import kotlin.math.abs
 import kotlin.math.roundToInt
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
 
 object TileType
@@ -225,7 +228,7 @@
     columns: Int,
     largeTilesSpan: Int,
     modifier: Modifier,
-    onAddTile: (TileSpec) -> Unit,
+    onAddTile: (TileSpec, Int) -> Unit,
     onRemoveTile: (TileSpec) -> Unit,
     onSetTiles: (List<TileSpec>) -> Unit,
     onResize: (TileSpec, toIcon: Boolean) -> Unit,
@@ -243,6 +246,15 @@
             null
         }
 
+    LaunchedEffect(selectionState.placementEvent) {
+        selectionState.placementEvent?.let { event ->
+            listState
+                .targetIndexForPlacement(event)
+                .takeIf { it != INVALID_INDEX }
+                ?.let { onAddTile(event.movingSpec, it) }
+        }
+    }
+
     Scaffold(
         containerColor = Color.Transparent,
         topBar = { EditModeTopBar(onStopEditing = onStopEditing, onReset = reset) },
@@ -272,29 +284,23 @@
                         .padding(top = innerPadding.calculateTopPadding())
                         .clipScrollableContainer(Orientation.Vertical)
                         .verticalScroll(scrollState)
-                        .dragAndDropRemoveZone(listState, onRemoveTile),
-            ) {
-                AnimatedContent(
-                    targetState = listState.dragInProgress || selectionState.selected,
-                    label = "QSEditHeader",
-                    contentAlignment = Alignment.Center,
-                    modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp),
-                ) { showRemoveTarget ->
-                    EditGridHeader {
-                        if (showRemoveTarget) {
-                            RemoveTileTarget {
-                                selectionState.selection?.let {
-                                    selectionState.unSelect()
-                                    onRemoveTile(it)
-                                }
+                        .dragAndDropRemoveZone(listState) { spec, removalEnabled ->
+                            if (removalEnabled) {
+                                // If removal is enabled, remove the tile
+                                onRemoveTile(spec)
+                            } else {
+                                // Otherwise submit the new tile ordering
+                                onSetTiles(listState.tileSpecs())
+                                selectionState.select(spec)
                             }
-                        } else {
-                            EditGridCenteredText(
-                                text = stringResource(id = R.string.drag_to_rearrange_tiles)
-                            )
-                        }
-                    }
-                }
+                        },
+            ) {
+                CurrentTilesGridHeader(
+                    listState,
+                    selectionState,
+                    onRemoveTile,
+                    modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp),
+                )
 
                 CurrentTilesGrid(
                     listState,
@@ -315,7 +321,7 @@
                     // Using the fully qualified name here as a workaround for AnimatedVisibility
                     // not being available from a Box
                     androidx.compose.animation.AnimatedVisibility(
-                        visible = !listState.dragInProgress,
+                        visible = !listState.dragInProgress && !selectionState.placementEnabled,
                         enter = fadeIn(),
                         exit = fadeOut(),
                     ) {
@@ -340,7 +346,7 @@
                                 availableTiles,
                                 selectionState,
                                 columns,
-                                onAddTile,
+                                { onAddTile(it, listState.tileSpecs().size) }, // Add to the end
                                 listState,
                             )
                         }
@@ -398,6 +404,76 @@
     }
 }
 
+private enum class EditModeHeaderState {
+    Remove,
+    Place,
+    Idle,
+}
+
+@Composable
+private fun rememberEditModeState(
+    listState: EditTileListState,
+    selectionState: MutableSelectionState,
+): State<EditModeHeaderState> {
+    val editGridHeaderState = remember { mutableStateOf(EditModeHeaderState.Idle) }
+    LaunchedEffect(
+        listState.dragInProgress,
+        selectionState.selected,
+        selectionState.placementEnabled,
+    ) {
+        val canRemove =
+            listState.isDraggedCellRemovable ||
+                selectionState.selection?.let { listState.isRemovable(it) } ?: false
+
+        editGridHeaderState.value =
+            when {
+                selectionState.placementEnabled -> EditModeHeaderState.Place
+                canRemove -> EditModeHeaderState.Remove
+                else -> EditModeHeaderState.Idle
+            }
+    }
+
+    return editGridHeaderState
+}
+
+@Composable
+private fun CurrentTilesGridHeader(
+    listState: EditTileListState,
+    selectionState: MutableSelectionState,
+    onRemoveTile: (TileSpec) -> Unit,
+    modifier: Modifier = Modifier,
+) {
+    val editGridHeaderState by rememberEditModeState(listState, selectionState)
+
+    AnimatedContent(
+        targetState = editGridHeaderState,
+        label = "QSEditHeader",
+        contentAlignment = Alignment.Center,
+        modifier = modifier,
+    ) { state ->
+        EditGridHeader {
+            when (state) {
+                EditModeHeaderState.Remove -> {
+                    RemoveTileTarget {
+                        selectionState.selection?.let {
+                            selectionState.unSelect()
+                            onRemoveTile(it)
+                        }
+                    }
+                }
+                EditModeHeaderState.Place -> {
+                    EditGridCenteredText(text = stringResource(id = R.string.tap_to_position_tile))
+                }
+                EditModeHeaderState.Idle -> {
+                    EditGridCenteredText(
+                        text = stringResource(id = R.string.drag_to_rearrange_tiles)
+                    )
+                }
+            }
+        }
+    }
+}
+
 @Composable
 private fun EditGridHeader(
     modifier: Modifier = Modifier,
@@ -484,8 +560,14 @@
                 }
                 .testTag(CURRENT_TILES_GRID_TEST_TAG),
     ) {
-        EditTiles(cells, listState, selectionState, coroutineScope, largeTilesSpan, onRemoveTile) {
-            resizingOperation ->
+        EditTiles(
+            cells,
+            listState,
+            selectionState,
+            coroutineScope,
+            largeTilesSpan,
+            onRemoveTile = onRemoveTile,
+        ) { resizingOperation ->
             when (resizingOperation) {
                 is TemporaryResizeOperation -> {
                     currentListState.resizeTile(resizingOperation.spec, resizingOperation.toIcon)
@@ -585,6 +667,7 @@
  * @param selectionState the [MutableSelectionState] for this grid
  * @param coroutineScope the [CoroutineScope] to be used for the tiles
  * @param largeTilesSpan the width used for large tiles
+ * @param onRemoveTile the callback when a tile is removed from this grid
  * @param onResize the callback when a tile has a new [ResizeOperation]
  */
 fun LazyGridScope.EditTiles(
@@ -628,12 +711,33 @@
                         modifier = Modifier.animateItem(),
                     )
                 }
-            is SpacerGridCell -> SpacerGridCell()
+            is SpacerGridCell ->
+                SpacerGridCell(
+                    Modifier.pointerInput(Unit) {
+                        detectTapGestures(onTap = { selectionState.onTap(index) })
+                    }
+                )
         }
     }
 }
 
 @Composable
+private fun rememberTileState(
+    tile: EditTileViewModel,
+    selectionState: MutableSelectionState,
+): State<TileState> {
+    val tileState = remember { mutableStateOf(TileState.None) }
+    val canShowRemovalBadge = tile.isRemovable
+
+    LaunchedEffect(selectionState.selection, selectionState.placementEnabled, canShowRemovalBadge) {
+        tileState.value =
+            selectionState.tileStateFor(tile.tileSpec, tileState.value, canShowRemovalBadge)
+    }
+
+    return tileState
+}
+
+@Composable
 private fun TileGridCell(
     cell: TileGridCell,
     index: Int,
@@ -646,29 +750,7 @@
     modifier: Modifier = Modifier,
 ) {
     val stateDescription = stringResource(id = R.string.accessibility_qs_edit_position, index + 1)
-    val canShowRemovalBadge = cell.tile.availableEditActions.contains(AvailableEditActions.REMOVE)
-    var tileState by remember { mutableStateOf(TileState.None) }
-
-    LaunchedEffect(selectionState.selection, canShowRemovalBadge) {
-        tileState =
-            when {
-                selectionState.selection == cell.tile.tileSpec -> {
-                    if (tileState == TileState.None && canShowRemovalBadge) {
-                        // The tile decoration is None if a tile is newly composed OR the removal
-                        // badge can't be shown.
-                        // For newly composed and selected tiles, such as dragged tiles or moved
-                        // tiles from resizing, introduce a short delay. This avoids clipping issues
-                        // on the border and resizing handle, as well as letting the selection
-                        // animation play correctly.
-                        delay(250)
-                    }
-                    TileState.Selected
-                }
-                canShowRemovalBadge -> TileState.Removable
-                else -> TileState.None
-            }
-    }
-
+    val tileState by rememberTileState(cell.tile, selectionState)
     val resizingState = rememberResizingState(cell.tile.tileSpec, cell.isIcon)
     val progress: () -> Float = {
         if (tileState == TileState.Selected) {
@@ -696,12 +778,16 @@
         with(LocalDensity.current) { (largeTilesSpan - 1) * TileArrangementPadding.roundToPx() }
     val colors = EditModeTileDefaults.editTileColors()
     val toggleSizeLabel = stringResource(R.string.accessibility_qs_edit_toggle_tile_size_action)
-    val clickLabel =
+    val togglePlacementModeLabel =
+        stringResource(R.string.accessibility_qs_edit_toggle_placement_mode)
+    val decorationClickLabel =
         when (tileState) {
-            TileState.None -> null
             TileState.Removable ->
                 stringResource(id = R.string.accessibility_qs_edit_remove_tile_action)
             TileState.Selected -> toggleSizeLabel
+            TileState.None,
+            TileState.Placeable,
+            TileState.GreyedOut -> null
         }
     InteractiveTileContainer(
         tileState = tileState,
@@ -720,8 +806,13 @@
                 coroutineScope.launch { resizingState.toggleCurrentValue() }
             }
         },
-        onClickLabel = clickLabel,
+        onClickLabel = decorationClickLabel,
     ) {
+        val placeableColor = MaterialTheme.colorScheme.primary.copy(alpha = .4f)
+        val backgroundColor by
+            animateColorAsState(
+                if (tileState == TileState.Placeable) placeableColor else colors.background
+            )
         Box(
             modifier
                 .fillMaxSize()
@@ -734,7 +825,11 @@
                             CustomAccessibilityAction(toggleSizeLabel) {
                                 onResize(FinalResizeOperation(cell.tile.tileSpec, !cell.isIcon))
                                 true
-                            }
+                            },
+                            CustomAccessibilityAction(togglePlacementModeLabel) {
+                                selectionState.togglePlacementMode(cell.tile.tileSpec)
+                                true
+                            },
                         )
                 }
                 .selectableTile(cell.tile.tileSpec, selectionState)
@@ -744,9 +839,14 @@
                     DragType.Move,
                     selectionState::unSelect,
                 )
-                .tileBackground(colors.background)
+                .tileBackground { backgroundColor }
         ) {
-            EditTile(tile = cell.tile, state = resizingState, progress = progress)
+            EditTile(
+                tile = cell.tile,
+                tileState = tileState,
+                state = resizingState,
+                progress = progress,
+            )
         }
     }
 }
@@ -791,7 +891,7 @@
                 } else {
                     Modifier
                 }
-            Box(draggableModifier.fillMaxSize().tileBackground(colors.background)) {
+            Box(draggableModifier.fillMaxSize().tileBackground { colors.background }) {
                 // Icon
                 SmallTileContent(
                     iconProvider = { cell.tile.icon },
@@ -834,11 +934,13 @@
 @Composable
 fun EditTile(
     tile: EditTileViewModel,
+    tileState: TileState,
     state: ResizingState,
     progress: () -> Float,
     colors: TileColors = EditModeTileDefaults.editTileColors(),
 ) {
     val iconSizeDiff = CommonTileDefaults.IconSize - CommonTileDefaults.LargeTileIconSize
+    val alpha by animateFloatAsState(if (tileState == TileState.GreyedOut) .4f else 1f)
     Row(
         horizontalArrangement = spacedBy(6.dp),
         verticalAlignment = Alignment.CenterVertically,
@@ -871,7 +973,8 @@
                         placeable.place(startPadding.roundToInt(), 0)
                     }
                 }
-                .largeTilePadding(),
+                .largeTilePadding()
+                .graphicsLayer { this.alpha = alpha },
     ) {
         // Icon
         Box(Modifier.size(ToggleTargetSize)) {
@@ -889,7 +992,7 @@
             label = tile.label.text,
             secondaryLabel = tile.appName?.text,
             colors = colors,
-            modifier = Modifier.weight(1f).graphicsLayer { alpha = progress() },
+            modifier = Modifier.weight(1f).graphicsLayer { this.alpha = progress() },
         )
     }
 }
@@ -908,9 +1011,9 @@
         CommonTileDefaults.TileStartPadding.toPx()
 }
 
-private fun Modifier.tileBackground(color: Color): Modifier {
+private fun Modifier.tileBackground(color: () -> Color): Modifier {
     // Clip tile contents from overflowing past the tile
-    return clip(RoundedCornerShape(InactiveCornerRadius)).drawBehind { drawRect(color) }
+    return clip(RoundedCornerShape(InactiveCornerRadius)).drawBehind { drawRect(color()) }
 }
 
 private object EditModeTileDefaults {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
index 984343a..233af54 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
@@ -42,7 +42,6 @@
 import com.android.systemui.qs.panels.ui.viewmodel.IconTilesViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.InfiniteGridViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
-import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor.Companion.POSITION_AT_END
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.qs.shared.ui.ElementKeys.toElementKey
 import com.android.systemui.res.R
@@ -171,7 +170,7 @@
             otherTiles = otherTiles,
             columns = columns,
             modifier = modifier,
-            onAddTile = { onAddTile(it, POSITION_AT_END) },
+            onAddTile = onAddTile,
             onRemoveTile = onRemoveTile,
             onSetTiles = onSetTiles,
             onResize = iconTilesViewModel::resize,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt
index 3dfde86..50b2955 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt
@@ -16,15 +16,17 @@
 
 package com.android.systemui.qs.panels.ui.compose.selection
 
-import androidx.compose.foundation.gestures.detectTapGestures
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.input.pointer.pointerInput
+import com.android.systemui.common.ui.compose.gestures.detectEagerTapGestures
 import com.android.systemui.qs.pipeline.shared.TileSpec
+import kotlinx.coroutines.delay
 
 /** Creates the state of the current selected tile that is remembered across compositions. */
 @Composable
@@ -38,6 +40,17 @@
     var selection by mutableStateOf<TileSpec?>(null)
         private set
 
+    /**
+     * Whether the current selection is in placement mode or not.
+     *
+     * A tile in placement mode can be positioned by tapping at the desired location in the grid.
+     */
+    var placementEnabled by mutableStateOf(false)
+        private set
+
+    /** Latest event from coming from placement mode. */
+    var placementEvent by mutableStateOf<PlacementEvent?>(null)
+
     val selected: Boolean
         get() = selection != null
 
@@ -47,37 +60,122 @@
 
     fun unSelect() {
         selection = null
+        exitPlacementMode()
     }
-}
 
-/**
- * Listens for click events to select/unselect the given [TileSpec]. Use this on current tiles as
- * they can be selected.
- */
-fun Modifier.selectableTile(
-    tileSpec: TileSpec,
-    selectionState: MutableSelectionState,
-    onClick: () -> Unit = {},
-): Modifier {
-    return pointerInput(Unit) {
-        detectTapGestures(
-            onTap = {
-                if (selectionState.selection == tileSpec) {
-                    selectionState.unSelect()
-                } else {
-                    selectionState.select(tileSpec)
+    /** Selects [tileSpec] and enable placement mode. */
+    fun enterPlacementMode(tileSpec: TileSpec) {
+        selection = tileSpec
+        placementEnabled = true
+    }
+
+    /** Disable placement mode but maintains current selection. */
+    private fun exitPlacementMode() {
+        placementEnabled = false
+    }
+
+    fun togglePlacementMode(tileSpec: TileSpec) {
+        if (placementEnabled) exitPlacementMode() else enterPlacementMode(tileSpec)
+    }
+
+    suspend fun tileStateFor(
+        tileSpec: TileSpec,
+        previousState: TileState,
+        canShowRemovalBadge: Boolean,
+    ): TileState {
+        return when {
+            placementEnabled && selection == tileSpec -> TileState.Placeable
+            placementEnabled -> TileState.GreyedOut
+            selection == tileSpec -> {
+                if (previousState == TileState.None && canShowRemovalBadge) {
+                    // The tile decoration is None if a tile is newly composed OR the removal
+                    // badge can't be shown.
+                    // For newly composed and selected tiles, such as dragged tiles or moved
+                    // tiles from resizing, introduce a short delay. This avoids clipping issues
+                    // on the border and resizing handle, as well as letting the selection
+                    // animation play correctly.
+                    delay(250)
                 }
-                onClick()
+                TileState.Selected
             }
-        )
+            canShowRemovalBadge -> TileState.Removable
+            else -> TileState.None
+        }
+    }
+
+    /**
+     * Tap callback on a tile.
+     *
+     * Tiles can be selected and placed using placement mode.
+     */
+    fun onTap(tileSpec: TileSpec) {
+        when {
+            placementEnabled && selection == tileSpec -> {
+                exitPlacementMode()
+            }
+            placementEnabled -> {
+                selection?.let { placementEvent = PlacementEvent.PlaceToTileSpec(it, tileSpec) }
+                exitPlacementMode()
+            }
+            selection == tileSpec -> {
+                unSelect()
+            }
+            else -> {
+                select(tileSpec)
+            }
+        }
+    }
+
+    /**
+     * Tap on a position.
+     *
+     * Use on grid items not associated with a [TileSpec], such as a spacer. Spacers can't be
+     * selected, but selections can be moved to their position.
+     */
+    fun onTap(index: Int) {
+        when {
+            placementEnabled -> {
+                selection?.let { placementEvent = PlacementEvent.PlaceToIndex(it, index) }
+                exitPlacementMode()
+            }
+            selected -> {
+                unSelect()
+            }
+        }
     }
 }
 
+// Not using data classes here as distinct placement events may have the same moving spec and target
+@Stable
+sealed interface PlacementEvent {
+    val movingSpec: TileSpec
+
+    /** Placement event corresponding to [movingSpec] moving to [targetSpec]'s position */
+    class PlaceToTileSpec(override val movingSpec: TileSpec, val targetSpec: TileSpec) :
+        PlacementEvent
+
+    /** Placement event corresponding to [movingSpec] moving to [targetIndex] */
+    class PlaceToIndex(override val movingSpec: TileSpec, val targetIndex: Int) : PlacementEvent
+}
+
 /**
- * Listens for click events to unselect any tile. Use this on available tiles as they can't be
- * selected.
+ * Listens for click events on selectable tiles.
+ *
+ * Use this on current tiles as they can be selected.
+ *
+ * @param tileSpec the [TileSpec] of the tile this modifier is applied to
+ * @param selectionState the [MutableSelectionState] representing the grid's selection
  */
 @Composable
-fun Modifier.clearSelectionTile(selectionState: MutableSelectionState): Modifier {
-    return pointerInput(Unit) { detectTapGestures(onTap = { selectionState.unSelect() }) }
+fun Modifier.selectableTile(tileSpec: TileSpec, selectionState: MutableSelectionState): Modifier {
+    return pointerInput(Unit) {
+        detectEagerTapGestures(
+            doubleTapEnabled = {
+                // Double tap enabled if where not in placement mode already
+                !selectionState.placementEnabled
+            },
+            onDoubleTap = { selectionState.enterPlacementMode(tileSpec) },
+            onTap = { selectionState.onTap(tileSpec) },
+        )
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt
index 57f63c7..8ffc4be 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.qs.panels.ui.compose.selection
 
 import androidx.compose.animation.animateColor
+import androidx.compose.animation.core.Animatable
 import androidx.compose.animation.core.Transition
 import androidx.compose.animation.core.animateFloat
 import androidx.compose.animation.core.animateFloatAsState
@@ -37,9 +38,13 @@
 import androidx.compose.material3.LocalMinimumInteractiveComponentSize
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.State
 import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.drawBehind
@@ -73,7 +78,9 @@
 import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.ResizingPillHeight
 import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.ResizingPillWidth
 import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.SelectedBorderWidth
+import com.android.systemui.qs.panels.ui.compose.selection.TileState.GreyedOut
 import com.android.systemui.qs.panels.ui.compose.selection.TileState.None
+import com.android.systemui.qs.panels.ui.compose.selection.TileState.Placeable
 import com.android.systemui.qs.panels.ui.compose.selection.TileState.Removable
 import com.android.systemui.qs.panels.ui.compose.selection.TileState.Selected
 import kotlin.math.cos
@@ -104,10 +111,11 @@
 ) {
     val transition: Transition<TileState> = updateTransition(tileState)
     val decorationColor by transition.animateColor()
-    val decorationAngle by transition.animateAngle()
+    val decorationAngle by animateAngle(tileState)
     val decorationSize by transition.animateSize()
     val decorationOffset by transition.animateOffset()
-    val decorationAlpha by transition.animateFloat { state -> if (state == None) 0f else 1f }
+    val decorationAlpha by
+        transition.animateFloat { state -> if (state == Removable || state == Selected) 1f else 0f }
     val badgeIconAlpha by transition.animateFloat { state -> if (state == Removable) 1f else 0f }
     val selectionBorderAlpha by
         transition.animateFloat { state -> if (state == Selected) 1f else 0f }
@@ -282,27 +290,61 @@
 }
 
 enum class TileState {
+    /** Tile is displayed as-is, no additional decoration needed. */
     None,
+    /** Tile can be removed by the user. This is displayed by a badge in the upper end corner. */
     Removable,
+    /**
+     * Tile is selected and resizable. One tile can be selected at a time in the grid. This is when
+     * we display the resizing handle and a highlighted border around the tile.
+     */
     Selected,
+    /**
+     * Tile placeable. This state means that the grid is in placement mode and this tile is
+     * selected. It should be highlighted to stand out in the grid.
+     */
+    Placeable,
+    /**
+     * Tile is faded out. This state means that the grid is in placement mode and this tile isn't
+     * selected. It serves as a target to place the selected tile.
+     */
+    GreyedOut,
 }
 
 @Composable
 private fun Transition<TileState>.animateColor(): State<Color> {
     return animateColor { state ->
         when (state) {
-            None -> Color.Transparent
+            None,
+            GreyedOut -> Color.Transparent
             Removable -> MaterialTheme.colorScheme.primaryContainer
-            Selected -> MaterialTheme.colorScheme.primary
+            Selected,
+            Placeable -> MaterialTheme.colorScheme.primary
         }
     }
 }
 
+/**
+ * Animate the angle of the tile decoration based on the previous state
+ *
+ * Some [TileState] don't have a visible decoration, and the angle should only animate when going
+ * between visible states.
+ */
 @Composable
-private fun Transition<TileState>.animateAngle(): State<Float> {
-    return animateFloat { state ->
-        if (state == Removable) BADGE_ANGLE_RAD else RESIZING_PILL_ANGLE_RAD
+private fun animateAngle(tileState: TileState): State<Float> {
+    val animatable = remember { Animatable(0f) }
+    var animate by remember { mutableStateOf(false) }
+    LaunchedEffect(tileState) {
+        val targetAngle = tileState.decorationAngle()
+
+        if (targetAngle == null) {
+            animate = false
+        } else {
+            if (animate) animatable.animateTo(targetAngle) else animatable.snapTo(targetAngle)
+            animate = true
+        }
     }
+    return animatable.asState()
 }
 
 @Composable
@@ -310,7 +352,9 @@
     return animateSize { state ->
         with(LocalDensity.current) {
             when (state) {
-                None -> Size.Zero
+                None,
+                Placeable,
+                GreyedOut -> Size.Zero
                 Removable -> Size(BadgeSize.toPx())
                 Selected -> Size(ResizingPillWidth.toPx(), ResizingPillHeight.toPx())
             }
@@ -323,7 +367,9 @@
     return animateOffset { state ->
         with(LocalDensity.current) {
             when (state) {
-                None -> Offset.Zero
+                None,
+                Placeable,
+                GreyedOut -> Offset.Zero
                 Removable -> Offset(BadgeXOffset.toPx(), BadgeYOffset.toPx())
                 Selected -> Offset(-SelectedBorderWidth.toPx(), 0f)
             }
@@ -331,6 +377,16 @@
     }
 }
 
+private fun TileState.decorationAngle(): Float? {
+    return when (this) {
+        Removable -> BADGE_ANGLE_RAD
+        Selected -> RESIZING_PILL_ANGLE_RAD
+        None,
+        Placeable,
+        GreyedOut -> null // No visible decoration
+    }
+}
+
 private fun Size(size: Float) = Size(size, size)
 
 private fun offsetForAngle(angle: Float, radius: Float, center: Offset): Offset {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditTileViewModel.kt
index be6ce5c..cf325f5 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditTileViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditTileViewModel.kt
@@ -66,6 +66,9 @@
 ) : CategoryAndName {
     override val name
         get() = label.text
+
+    val isRemovable
+        get() = availableEditActions.contains(AvailableEditActions.REMOVE)
 }
 
 enum class AvailableEditActions {
diff --git a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayCoreStartable.kt
index 263ef09e..5d4a774 100644
--- a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayCoreStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayCoreStartable.kt
@@ -19,14 +19,18 @@
 import android.content.Context
 import android.hardware.devicestate.DeviceStateManager
 import android.hardware.devicestate.feature.flags.Flags
+import android.os.Handler
+import android.view.accessibility.AccessibilityManager
 import androidx.annotation.VisibleForTesting
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.KeyguardUpdateMonitorCallback
 import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.display.domain.interactor.RearDisplayStateInteractor
 import com.android.systemui.statusbar.phone.SystemUIDialog
+import java.util.concurrent.atomic.AtomicBoolean
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Job
@@ -52,6 +56,8 @@
     private val rearDisplayInnerDialogDelegateFactory: RearDisplayInnerDialogDelegate.Factory,
     @Application private val scope: CoroutineScope,
     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+    private val accessibilityManager: AccessibilityManager,
+    @Background private val handler: Handler,
 ) : CoreStartable, AutoCloseable {
 
     companion object {
@@ -77,6 +83,12 @@
     override fun start() {
         if (Flags.deviceStateRdmV2()) {
             var dialog: SystemUIDialog? = null
+            var touchExplorationEnabled = AtomicBoolean(false)
+
+            accessibilityManager.addTouchExplorationStateChangeListener(
+                { enabled -> touchExplorationEnabled.set(enabled) },
+                handler,
+            )
 
             keyguardUpdateMonitor.registerCallback(keyguardCallback)
 
@@ -99,6 +111,7 @@
                                             rearDisplayInnerDialogDelegateFactory.create(
                                                 rearDisplayContext,
                                                 deviceStateManager::cancelStateRequest,
+                                                touchExplorationEnabled.get(),
                                             )
                                         dialog = delegate.createDialog().apply { show() }
                                     }
diff --git a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegate.kt
index f5facf4..96f1bd2 100644
--- a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegate.kt
@@ -20,7 +20,10 @@
 import android.content.Context
 import android.os.Bundle
 import android.view.MotionEvent
+import android.view.View
+import android.widget.Button
 import android.widget.SeekBar
+import android.widget.TextView
 import com.android.systemui.haptics.slider.HapticSlider
 import com.android.systemui.haptics.slider.HapticSliderPlugin
 import com.android.systemui.haptics.slider.HapticSliderViewBinder
@@ -45,6 +48,7 @@
 internal constructor(
     private val systemUIDialogFactory: SystemUIDialog.Factory,
     @Assisted private val rearDisplayContext: Context,
+    @Assisted private val touchExplorationEnabled: Boolean,
     private val vibratorHelper: VibratorHelper,
     private val msdlPlayer: MSDLPlayer,
     private val systemClock: SystemClock,
@@ -82,6 +86,7 @@
         fun create(
             rearDisplayContext: Context,
             onCanceledRunnable: Runnable,
+            touchExplorationEnabled: Boolean,
         ): RearDisplayInnerDialogDelegate
     }
 
@@ -95,11 +100,32 @@
 
     @SuppressLint("ClickableViewAccessibility")
     override fun onCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
+
         dialog.apply {
             setContentView(R.layout.activity_rear_display_enabled)
             setCanceledOnTouchOutside(false)
 
+            requireViewById<Button>(R.id.cancel_button).let { it ->
+                if (!touchExplorationEnabled) {
+                    return@let
+                }
+
+                it.visibility = View.VISIBLE
+                it.setOnClickListener { onCanceledRunnable.run() }
+            }
+
+            requireViewById<TextView>(R.id.seekbar_instructions).let { it ->
+                if (touchExplorationEnabled) {
+                    it.visibility = View.GONE
+                }
+            }
+
             requireViewById<SeekBar>(R.id.seekbar).let { it ->
+                if (touchExplorationEnabled) {
+                    it.visibility = View.GONE
+                    return@let
+                }
+
                 // Create and bind the HapticSliderPlugin
                 val hapticSliderPlugin =
                     HapticSliderPlugin(
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 3ad0867..06fc861 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
@@ -33,10 +33,8 @@
 import com.android.systemui.bouncer.shared.logging.BouncerUiEvent
 import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.classifier.FalsingCollectorActual
-import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dagger.qualifiers.DisplayId
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
@@ -82,6 +80,7 @@
 import com.android.systemui.util.kotlin.sample
 import com.android.systemui.util.printSection
 import com.android.systemui.util.println
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
 import com.google.android.msdl.data.model.MSDLToken
 import com.google.android.msdl.domain.MSDLPlayer
 import dagger.Lazy
@@ -123,7 +122,6 @@
     private val bouncerInteractor: BouncerInteractor,
     private val keyguardInteractor: KeyguardInteractor,
     private val sysUiState: SysUiState,
-    @DisplayId private val displayId: Int,
     private val sceneLogger: SceneLogger,
     @FalsingCollectorActual private val falsingCollector: FalsingCollector,
     private val falsingManager: FalsingManager,
@@ -197,7 +195,8 @@
                     return
                 }
 
-                printSection("Scene state") {
+                printSection("Framework state") {
+                    println("isVisible", sceneInteractor.isVisible.value)
                     println("currentScene", sceneInteractor.currentScene.value.debugName)
                     println(
                         "currentOverlays",
@@ -732,21 +731,26 @@
                     sceneInteractor.transitionState
                         .mapNotNull { it as? ObservableTransitionState.Idle }
                         .distinctUntilChanged(),
+                    sceneInteractor.isVisible,
                     occlusionInteractor.invisibleDueToOcclusion,
-                ) { idleState, invisibleDueToOcclusion ->
+                ) { idleState, isVisible, invisibleDueToOcclusion ->
                     SceneContainerPlugin.SceneContainerPluginState(
                         scene = idleState.currentScene,
                         overlays = idleState.currentOverlays,
+                        isVisible = isVisible,
                         invisibleDueToOcclusion = invisibleDueToOcclusion,
                     )
                 }
-                .collect { sceneContainerPluginState ->
+                .map { sceneContainerPluginState ->
+                    SceneContainerPlugin.EvaluatorByFlag.map { (flag, evaluator) ->
+                            flag to evaluator(sceneContainerPluginState)
+                        }
+                        .toMap()
+                }
+                .distinctUntilChanged()
+                .collect { flags ->
                     sysUiState.updateFlags(
-                        displayId,
-                        *SceneContainerPlugin.EvaluatorByFlag.map { (flag, evaluator) ->
-                                flag to evaluator.invoke(sceneContainerPluginState)
-                            }
-                            .toTypedArray(),
+                        *(flags.entries.map { (key, value) -> key to value }).toTypedArray()
                     )
                 }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 24e7976..b906242 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -206,6 +206,7 @@
 import com.google.android.msdl.domain.MSDLPlayer;
 
 import dagger.Lazy;
+
 import kotlin.Unit;
 
 import kotlinx.coroutines.CoroutineDispatcher;
@@ -4267,7 +4268,8 @@
                     == AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD.getId()
                     || action
                     == AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_UP.getId()) {
-                mStatusBarKeyguardViewManager.showPrimaryBouncer(true);
+                mStatusBarKeyguardViewManager.showPrimaryBouncer(true,
+                        "NotificationPanelViewController#performAccessibilityAction");
                 return true;
             }
             return super.performAccessibilityAction(host, action, args);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index e44701d..4daf61a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -64,6 +64,7 @@
 
 import com.android.internal.annotations.KeepForWeakReference;
 import com.android.internal.os.SomeArgs;
+import com.android.internal.statusbar.DisableStates;
 import com.android.internal.statusbar.IAddTileResultCallback;
 import com.android.internal.statusbar.IStatusBar;
 import com.android.internal.statusbar.IUndoMediaTransferCallback;
@@ -85,6 +86,7 @@
 import java.io.OutputStream;
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.Map;
 
 /**
  * This class takes the functions from IStatusBar that come in on
@@ -184,6 +186,8 @@
     private static final int MSG_TOGGLE_QUICK_SETTINGS_PANEL = 82 << MSG_SHIFT;
     private static final int MSG_WALLET_ACTION_LAUNCH_GESTURE = 83 << MSG_SHIFT;
     private static final int MSG_DISPLAY_REMOVE_SYSTEM_DECORATIONS = 85 << MSG_SHIFT;
+    private static final int MSG_DISABLE_ALL  = 86 << MSG_SHIFT;
+
     public static final int FLAG_EXCLUDE_NONE = 0;
     public static final int FLAG_EXCLUDE_SEARCH_PANEL = 1 << 0;
     public static final int FLAG_EXCLUDE_RECENTS_PANEL = 1 << 1;
@@ -654,7 +658,8 @@
 
     /**
      * Called to notify that disable flags are updated.
-     * @see Callbacks#disable(int, int, int, boolean).
+     * @see Callbacks#disable(int, int, int, boolean)
+     * @see Callbacks#disableForAllDisplays(DisableStates)
      */
     public void disable(int displayId, @DisableFlags int state1, @Disable2Flags int state2,
             boolean animate) {
@@ -682,6 +687,27 @@
         disable(displayId, state1, state2, true);
     }
 
+    @Override
+    public void disableForAllDisplays(DisableStates disableStates) throws RemoteException {
+        synchronized (mLock) {
+            for (Map.Entry<Integer, Pair<Integer, Integer>> displaysWithStates :
+                    disableStates.displaysWithStates.entrySet()) {
+                int displayId = displaysWithStates.getKey();
+                Pair<Integer, Integer> states = displaysWithStates.getValue();
+                setDisabled(displayId, states.first, states.second);
+            }
+            mHandler.removeMessages(MSG_DISABLE_ALL);
+            Message msg = mHandler.obtainMessage(MSG_DISABLE_ALL, disableStates);
+            if (Looper.myLooper() == mHandler.getLooper()) {
+                // If its the right looper execute immediately so hides can be handled quickly.
+                mHandler.handleMessage(msg);
+                msg.recycle();
+            } else {
+                msg.sendToTarget();
+            }
+        }
+    }
+
     /**
      * Apply current disable flags by {@link CommandQueue#disable(int, int, int, boolean)}.
      *
@@ -1552,6 +1578,21 @@
                                 args.argi4 != 0 /* animate */);
                     }
                     break;
+                case MSG_DISABLE_ALL:
+                    DisableStates disableStates = (DisableStates) msg.obj;
+                    boolean animate = disableStates.animate;
+                    Map<Integer, Pair<Integer, Integer>> displaysWithDisableStates =
+                            disableStates.displaysWithStates;
+                    for (Map.Entry<Integer, Pair<Integer, Integer>> displayWithDisableStates :
+                            displaysWithDisableStates.entrySet()) {
+                        int displayId = displayWithDisableStates.getKey();
+                        Pair<Integer, Integer> states = displayWithDisableStates.getValue();
+                        for (int i = 0; i < mCallbacks.size(); i++) {
+                            mCallbacks.get(i).disable(displayId, states.first, states.second,
+                                    animate);
+                        }
+                    }
+                    break;
                 case MSG_EXPAND_NOTIFICATIONS:
                     for (int i = 0; i < mCallbacks.size(); i++) {
                         mCallbacks.get(i).animateExpandNotificationsPanel();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
index fce5a16..e292bcf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
@@ -24,6 +24,7 @@
 import android.util.Log
 import android.util.MathUtils
 import android.view.Choreographer
+import android.view.Display
 import android.view.View
 import androidx.annotation.VisibleForTesting
 import androidx.dynamicanimation.animation.FloatPropertyCompat
@@ -42,7 +43,9 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.shade.ShadeExpansionChangeEvent
 import com.android.systemui.shade.ShadeExpansionListener
+import com.android.systemui.shade.data.repository.ShadeDisplaysRepository
 import com.android.systemui.shade.domain.interactor.ShadeModeInteractor
+import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround
 import com.android.systemui.statusbar.phone.BiometricUnlockController
 import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK
 import com.android.systemui.statusbar.phone.DozeParameters
@@ -52,6 +55,7 @@
 import com.android.systemui.wallpapers.domain.interactor.WallpaperInteractor
 import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor
 import com.android.wm.shell.appzoomout.AppZoomOut
+import dagger.Lazy
 import java.io.PrintWriter
 import java.util.Optional
 import javax.inject.Inject
@@ -83,6 +87,7 @@
     private val appZoomOutOptional: Optional<AppZoomOut>,
     @Application private val applicationScope: CoroutineScope,
     dumpManager: DumpManager,
+    private val shadeDisplaysRepository: Lazy<ShadeDisplaysRepository>,
 ) : ShadeExpansionListener, Dumpable {
     companion object {
         private const val WAKE_UP_ANIMATION_ENABLED = true
@@ -228,6 +233,14 @@
 
     private data class WakeAndUnlockBlurData(val radius: Float, val useZoom: Boolean = true)
 
+    private val isShadeOnDefaultDisplay: Boolean
+        get() =
+            if (ShadeWindowGoesAround.isEnabled) {
+                shadeDisplaysRepository.get().displayId.value == Display.DEFAULT_DISPLAY
+            } else {
+                true
+            }
+
     /** Blur radius of the wake and unlock animation on this frame, and whether to zoom out. */
     private var wakeAndUnlockBlurData = WakeAndUnlockBlurData(0f)
         set(value) {
@@ -265,9 +278,14 @@
         var blur = shadeRadius.toInt()
         // If the blur comes from waking up, we don't want to zoom out the background
         val zoomOut =
-            if (shadeRadius != wakeAndUnlockBlurData.radius || wakeAndUnlockBlurData.useZoom)
-                blurRadiusToZoomOut(blurRadius = shadeRadius)
-            else 0f
+            when {
+                // When the shade is in another display, we don't want to zoom out the background.
+                // Only the default display is supported right now.
+                !isShadeOnDefaultDisplay -> 0f
+                shadeRadius != wakeAndUnlockBlurData.radius || wakeAndUnlockBlurData.useZoom ->
+                    blurRadiusToZoomOut(blurRadius = shadeRadius)
+                else -> 0f
+            }
         // Make blur be 0 if it is necessary to stop blur effect.
         if (scrimsVisible) {
             if (!Flags.notificationShadeBlur()) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt
index 1f2079d..356731c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.statusbar.chips.notification.domain.model
 
 import com.android.systemui.statusbar.StatusBarIconView
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
 
 /** Modeling all the data needed to render a status bar notification chip. */
 data class NotificationChipModel(
@@ -25,7 +25,7 @@
     /** The user-readable name of the app that posted this notification. */
     val appName: String,
     val statusBarChipIconView: StatusBarIconView?,
-    val promotedContent: PromotedNotificationContentModel,
+    val promotedContent: PromotedNotificationContentModels,
     /** The time when the notification first appeared as promoted. */
     val creationTime: Long,
     /** True if the app managing this notification is currently visible to the user. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt
index b303751..dfbd12d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt
@@ -72,6 +72,8 @@
         headsUpState: TopPinnedState
     ): OngoingActivityChipModel.Active {
         StatusBarNotifChips.unsafeAssertInNewMode()
+        // Chips are never shown when locked, so it's safe to use the version with sensitive content
+        val chipContent = promotedContent.privateVersion
         val contentDescription = getContentDescription(this.appName)
         val icon =
             if (this.statusBarChipIconView != null) {
@@ -123,21 +125,18 @@
             )
         }
 
-        if (this.promotedContent.shortCriticalText != null) {
+        if (chipContent.shortCriticalText != null) {
             return OngoingActivityChipModel.Active.Text(
                 key = this.key,
                 icon = icon,
                 colors = colors,
-                text = this.promotedContent.shortCriticalText,
+                text = chipContent.shortCriticalText,
                 onClickListenerLegacy = onClickListenerLegacy,
                 clickBehavior = clickBehavior,
             )
         }
 
-        if (
-            Flags.promoteNotificationsAutomatically() &&
-                this.promotedContent.wasPromotedAutomatically
-        ) {
+        if (Flags.promoteNotificationsAutomatically() && chipContent.wasPromotedAutomatically) {
             // When we're promoting notifications automatically, the `when` time set on the
             // notification will likely just be set to the current time, which would cause the chip
             // to always show "now". We don't want early testers to get that experience since it's
@@ -151,7 +150,7 @@
             )
         }
 
-        if (this.promotedContent.time == null) {
+        if (chipContent.time == null) {
             return OngoingActivityChipModel.Active.IconOnly(
                 key = this.key,
                 icon = icon,
@@ -161,17 +160,17 @@
             )
         }
 
-        when (this.promotedContent.time) {
+        when (chipContent.time) {
             is PromotedNotificationContentModel.When.Time -> {
                 return if (
-                    this.promotedContent.time.currentTimeMillis >=
+                    chipContent.time.currentTimeMillis >=
                         systemClock.currentTimeMillis() + FUTURE_TIME_THRESHOLD_MILLIS
                 ) {
                     OngoingActivityChipModel.Active.ShortTimeDelta(
                         key = this.key,
                         icon = icon,
                         colors = colors,
-                        time = this.promotedContent.time.currentTimeMillis,
+                        time = chipContent.time.currentTimeMillis,
                         onClickListenerLegacy = onClickListenerLegacy,
                         clickBehavior = clickBehavior,
                     )
@@ -198,8 +197,8 @@
                     key = this.key,
                     icon = icon,
                     colors = colors,
-                    startTimeMs = this.promotedContent.time.elapsedRealtimeMillis,
-                    isEventInFuture = this.promotedContent.time.isCountDown,
+                    startTimeMs = chipContent.time.elapsedRealtimeMillis,
+                    isEventInFuture = chipContent.time.isCountDown,
                     onClickListenerLegacy = onClickListenerLegacy,
                     clickBehavior = clickBehavior,
                 )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
index 4558017..b5ab092 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
@@ -68,6 +68,7 @@
 import com.android.systemui.statusbar.notification.headsup.PinnedStatus;
 import com.android.systemui.statusbar.notification.icon.IconPack;
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel;
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowController;
 import com.android.systemui.statusbar.notification.row.NotificationGuts;
@@ -198,7 +199,7 @@
 
     // TODO(b/377565433): Move into NotificationContentModel during/after
     //  NotificationRowContentBinderRefactor.
-    private PromotedNotificationContentModel mPromotedNotificationContentModel;
+    private PromotedNotificationContentModels mPromotedNotificationContentModels;
 
     /**
      * True if both
@@ -1106,9 +1107,9 @@
      * Gets the content needed to render this notification as a promoted notification on various
      * surfaces (like status bar chips and AOD).
      */
-    public PromotedNotificationContentModel getPromotedNotificationContentModel() {
+    public PromotedNotificationContentModels getPromotedNotificationContentModels() {
         if (PromotedNotificationContentModel.featureFlagEnabled()) {
-            return mPromotedNotificationContentModel;
+            return mPromotedNotificationContentModels;
         } else {
             Log.wtf(TAG, "getting promoted content without feature flag enabled", new Throwable());
             return null;
@@ -1127,10 +1128,10 @@
      * Sets the content needed to render this notification as a promoted notification on various
      * surfaces (like status bar chips and AOD).
      */
-    public void setPromotedNotificationContentModel(
-            @Nullable PromotedNotificationContentModel promotedNotificationContentModel) {
+    public void setPromotedNotificationContentModels(
+            @Nullable PromotedNotificationContentModels promotedNotificationContentModels) {
         if (PromotedNotificationContentModel.featureFlagEnabled()) {
-            this.mPromotedNotificationContentModel = promotedNotificationContentModel;
+            this.mPromotedNotificationContentModels = promotedNotificationContentModels;
         } else {
             Log.wtf(TAG, "setting promoted content without feature flag enabled", new Throwable());
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
index a0eab43..26b86f9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
@@ -15,6 +15,8 @@
  */
 package com.android.systemui.statusbar.notification.collection.coordinator
 
+import com.android.systemui.Flags.notificationSkipSilentUpdates
+
 import android.app.Notification
 import android.app.Notification.GROUP_ALERT_SUMMARY
 import android.util.ArrayMap
@@ -465,15 +467,32 @@
                             }
                         hunMutator.updateNotification(posted.key, pinnedStatus)
                     }
-                } else {
+                } else { // shouldHeadsUpEver = false
                     if (posted.isHeadsUpEntry) {
-                        // We don't want this to be interrupting anymore, let's remove it
-                        // If the notification is pinned by the user, the only way a user can un-pin
-                        // it is by tapping the status bar notification chip. Since that's a clear
-                        // user action, we should remove the HUN immediately instead of waiting for
-                        // any sort of minimum timeout.
-                        val shouldRemoveImmediately = posted.isPinnedByUser
-                        hunMutator.removeNotification(posted.key, shouldRemoveImmediately)
+                        if (notificationSkipSilentUpdates()) {
+                            if (posted.isPinnedByUser) {
+                                // We don't want this to be interrupting anymore, let's remove it
+                                // If the notification is pinned by the user, the only way a user
+                                // can un-pin it by tapping the status bar notification chip. Since
+                                // that's a clear user action, we should remove the HUN immediately
+                                // instead of waiting for any sort of minimum timeout.
+                                // TODO(b/401068530) Ensure that status bar chip HUNs are not
+                                //  removed for silent update
+                                hunMutator.removeNotification(posted.key,
+                                    /* releaseImmediately= */ true)
+                            } else {
+                                // Do NOT remove HUN for non-user update.
+                                // Let the HUN show for its remaining duration.
+                            }
+                        } else {
+                            // We don't want this to be interrupting anymore, let's remove it
+                            // If the notification is pinned by the user, the only way a user can
+                            // un-pin it is by tapping the status bar notification chip. Since
+                            // that's a clear user action, we should remove the HUN immediately
+                            // instead of waiting for any sort of minimum timeout.
+                            val shouldRemoveImmediately = posted.isPinnedByUser
+                            hunMutator.removeNotification(posted.key, shouldRemoveImmediately)
+                        }
                     } else {
                         // Don't let the bind finish
                         cancelHeadsUpBind(posted.entry)
@@ -573,24 +592,34 @@
                                 isBinding = isBinding,
                             )
                     }
-                // Handle cancelling heads up here, rather than in the OnBeforeFinalizeFilter, so
-                // that
-                // work can be done before the ShadeListBuilder is run. This prevents re-entrant
-                // behavior between this Coordinator, HeadsUpManager, and VisualStabilityManager.
-                if (posted?.shouldHeadsUpEver == false) {
-                    if (posted.isHeadsUpEntry) {
-                        // We don't want this to be interrupting anymore, let's remove it
-                        mHeadsUpManager.removeNotification(
-                            posted.key,
-                            /* removeImmediately= */ false,
-                            "onEntryUpdated",
-                        )
-                    } else if (posted.isBinding) {
+                if (notificationSkipSilentUpdates()) {
+                    // TODO(b/403703828) Move canceling to OnBeforeFinalizeFilter, since we are not
+                    //  removing from HeadsUpManager and don't need to deal with re-entrant behavior
+                    //  between HeadsUpCoordinator, HeadsUpManager, and VisualStabilityManager.
+                    if (posted?.shouldHeadsUpEver == false
+                        && !posted.isHeadsUpEntry && posted.isBinding) {
                         // Don't let the bind finish
                         cancelHeadsUpBind(posted.entry)
                     }
+                } else {
+                    // Handle cancelling heads up here, rather than in the OnBeforeFinalizeFilter,
+                    // so that work can be done before the ShadeListBuilder is run. This prevents
+                    // re-entrant behavior between this Coordinator, HeadsUpManager, and
+                    // VisualStabilityManager.
+                    if (posted?.shouldHeadsUpEver == false) {
+                        if (posted.isHeadsUpEntry) {
+                            // We don't want this to be interrupting anymore, let's remove it
+                            mHeadsUpManager.removeNotification(
+                                posted.key,
+                                /* removeImmediately= */ false,
+                                "onEntryUpdated",
+                            )
+                        } else if (posted.isBinding) {
+                            // Don't let the bind finish
+                            cancelHeadsUpBind(posted.entry)
+                        }
+                    }
                 }
-
                 // Update last updated time for this entry
                 setUpdateTime(entry, mSystemClock.currentTimeMillis())
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
index 1e5aa01..6042bff 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
@@ -83,7 +83,6 @@
 import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor;
 import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractorImpl;
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel;
-import com.android.systemui.statusbar.notification.row.NotificationActionClickManager;
 import com.android.systemui.statusbar.notification.row.NotificationEntryProcessorFactory;
 import com.android.systemui.statusbar.notification.row.NotificationEntryProcessorFactoryLooperImpl;
 import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
@@ -325,7 +324,7 @@
         if (PromotedNotificationContentModel.featureFlagEnabled()) {
             return implProvider.get();
         } else {
-            return (entry, recoveredBuilder, imageModelProvider) -> null;
+            return (entry, recoveredBuilder, redactionType, imageModelProvider) -> null;
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt
index 2f9d86b..e7cc342 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt
@@ -36,6 +36,7 @@
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationEntryModel
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationGroupModel
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
@@ -141,7 +142,7 @@
     private fun NotificationEntry.toModel(): ActiveNotificationModel {
         val promotedContent =
             if (PromotedNotificationContentModel.featureFlagEnabled()) {
-                promotedNotificationContentModel
+                promotedNotificationContentModels
             } else {
                 null
             }
@@ -199,7 +200,7 @@
     isGroupSummary: Boolean,
     bucket: Int,
     callType: CallType,
-    promotedContent: PromotedNotificationContentModel?,
+    promotedContent: PromotedNotificationContentModels?,
 ): ActiveNotificationModel {
     return individuals[key]?.takeIf {
         it.isCurrent(
@@ -281,7 +282,7 @@
     isGroupSummary: Boolean,
     bucket: Int,
     callType: CallType,
-    promotedContent: PromotedNotificationContentModel?,
+    promotedContent: PromotedNotificationContentModels?,
 ): Boolean {
     return when {
         key != this.key -> false
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
index d09546f..3caaf54 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
@@ -67,6 +67,7 @@
     private FooterViewButton mSettingsButton;
     private FooterViewButton mHistoryButton;
     private boolean mShouldBeHidden;
+    private boolean mIsBlurSupported;
 
     // Footer label
     private TextView mSeenNotifsFooterTextView;
@@ -390,15 +391,20 @@
 
         if (!notificationFooterBackgroundTintOptimization()) {
             if (notificationShadeBlur()) {
-                Color backgroundColor = Color.valueOf(
-                        SurfaceEffectColors.surfaceEffect1(getContext()));
-                scHigh = ColorUtils.setAlphaComponent(backgroundColor.toArgb(), 0xFF);
-                // Apply alpha on background drawables.
-                int backgroundAlpha = (int) (backgroundColor.alpha() * 0xFF);
-                clearAllBg.setAlpha(backgroundAlpha);
-                settingsBg.setAlpha(backgroundAlpha);
-                if (historyBg != null) {
-                    historyBg.setAlpha(backgroundAlpha);
+                if (mIsBlurSupported) {
+                    Color backgroundColor = Color.valueOf(
+                            SurfaceEffectColors.surfaceEffect1(getContext()));
+                    scHigh = ColorUtils.setAlphaComponent(backgroundColor.toArgb(), 0xFF);
+                    // Apply alpha on background drawables.
+                    int backgroundAlpha = (int) (backgroundColor.alpha() * 0xFF);
+                    clearAllBg.setAlpha(backgroundAlpha);
+                    settingsBg.setAlpha(backgroundAlpha);
+                    if (historyBg != null) {
+                        historyBg.setAlpha(backgroundAlpha);
+                    }
+                } else {
+                    scHigh = mContext.getColor(
+                            com.android.internal.R.color.materialColorSurfaceContainer);
                 }
             } else {
                 scHigh = mContext.getColor(
@@ -438,6 +444,16 @@
         }
     }
 
+    public void setIsBlurSupported(boolean isBlurSupported) {
+        if (notificationShadeBlur()) {
+            if (mIsBlurSupported == isBlurSupported) {
+                return;
+            }
+            mIsBlurSupported = isBlurSupported;
+            updateColors();
+        }
+    }
+
     @Override
     @NonNull
     public ExpandableViewState createExpandableViewState() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
index 3383ce9..3213754 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
@@ -20,6 +20,7 @@
 import androidx.lifecycle.lifecycleScope
 import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.internal.jank.InteractionJankMonitor
+import com.android.systemui.Flags.notificationShadeBlur
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.statusbar.notification.NotificationActivityStarter
 import com.android.systemui.statusbar.notification.NotificationActivityStarter.SettingsIntent
@@ -81,6 +82,14 @@
             launch { bindHistoryButton(footer, viewModel, notificationActivityStarter) }
         }
         launch { bindMessage(footer, viewModel) }
+
+        if (notificationShadeBlur()) {
+            launch {
+                viewModel.isBlurSupported.collect { supported ->
+                    footer.setIsBlurSupported(supported)
+                }
+            }
+        }
     }
 
     private suspend fun bindClearAllButton(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
index c895c419..c1fc72c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
@@ -31,6 +31,7 @@
 import com.android.systemui.util.ui.AnimatableEvent
 import com.android.systemui.util.ui.AnimatedValue
 import com.android.systemui.util.ui.toAnimatedValueFlow
+import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
 import kotlinx.coroutines.flow.Flow
@@ -48,6 +49,7 @@
     notificationSettingsInteractor: NotificationSettingsInteractor,
     seenNotificationsInteractor: SeenNotificationsInteractor,
     shadeInteractor: ShadeInteractor,
+    windowRootViewBlurInteractor: WindowRootViewBlurInteractor,
 ) {
     /** A message to show instead of the footer buttons. */
     val message: FooterMessageViewModel =
@@ -119,6 +121,8 @@
         }
     }
 
+    val isBlurSupported = windowRootViewBlurInteractor.isBlurCurrentlySupported
+
     private val manageOrHistoryButtonText: Flow<Int> =
         notificationSettingsInteractor.isNotificationHistoryEnabled.map { shouldLaunchHistory ->
             if (shouldLaunchHistory) R.string.manage_notifications_history_text
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt
index 27b2788..a8a7e88 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt
@@ -40,6 +40,8 @@
 import com.android.systemui.Flags
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.shade.ShadeDisplayAware
+import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE
+import com.android.systemui.statusbar.NotificationLockscreenUserManager.RedactionType
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.promoted.AutomaticPromotionCoordinator.Companion.EXTRA_AUTOMATICALLY_EXTRACTED_SHORT_CRITICAL_TEXT
 import com.android.systemui.statusbar.notification.promoted.AutomaticPromotionCoordinator.Companion.EXTRA_WAS_AUTOMATICALLY_PROMOTED
@@ -48,6 +50,7 @@
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.OldProgress
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.Style
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.When
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
 import com.android.systemui.statusbar.notification.row.shared.ImageModel
 import com.android.systemui.statusbar.notification.row.shared.ImageModelProvider
 import com.android.systemui.statusbar.notification.row.shared.ImageModelProvider.ImageSizeClass.MediumSquare
@@ -60,8 +63,9 @@
     fun extractContent(
         entry: NotificationEntry,
         recoveredBuilder: Notification.Builder,
+        @RedactionType redactionType: Int,
         imageModelProvider: ImageModelProvider,
-    ): PromotedNotificationContentModel?
+    ): PromotedNotificationContentModels?
 }
 
 @SysUISingleton
@@ -76,8 +80,9 @@
     override fun extractContent(
         entry: NotificationEntry,
         recoveredBuilder: Notification.Builder,
+        @RedactionType redactionType: Int,
         imageModelProvider: ImageModelProvider,
-    ): PromotedNotificationContentModel? {
+    ): PromotedNotificationContentModels? {
         if (!PromotedNotificationContentModel.featureFlagEnabled()) {
             logger.logExtractionSkipped(entry, "feature flags disabled")
             return null
@@ -95,7 +100,55 @@
             return null
         }
 
-        val contentBuilder = PromotedNotificationContentModel.Builder(entry.key)
+        val privateVersion =
+            extractPrivateContent(
+                key = entry.key,
+                notification = notification,
+                recoveredBuilder = recoveredBuilder,
+                lastAudiblyAlertedMs = entry.lastAudiblyAlertedMs,
+                imageModelProvider = imageModelProvider,
+            )
+        val publicVersion =
+            if (redactionType == REDACTION_TYPE_NONE) {
+                privateVersion
+            } else {
+                if (notification.publicVersion == null) {
+                    privateVersion.toDefaultPublicVersion()
+                } else {
+                    // TODO(b/400991304): implement extraction for [Notification.publicVersion]
+                    privateVersion.toDefaultPublicVersion()
+                }
+            }
+        return PromotedNotificationContentModels(
+                privateVersion = privateVersion,
+                publicVersion = publicVersion,
+            )
+            .also { logger.logExtractionSucceeded(entry, it) }
+    }
+
+    private fun PromotedNotificationContentModel.toDefaultPublicVersion():
+        PromotedNotificationContentModel =
+        PromotedNotificationContentModel.Builder(key = identity.key).let {
+            it.style = if (style == Style.Ineligible) Style.Ineligible else Style.Base
+            it.smallIcon = smallIcon
+            it.iconLevel = iconLevel
+            it.appName = appName
+            it.time = time
+            it.lastAudiblyAlertedMs = lastAudiblyAlertedMs
+            it.profileBadgeResId = profileBadgeResId
+            it.colors = colors
+            it.build()
+        }
+
+    private fun extractPrivateContent(
+        key: String,
+        notification: Notification,
+        recoveredBuilder: Notification.Builder,
+        lastAudiblyAlertedMs: Long,
+        imageModelProvider: ImageModelProvider,
+    ): PromotedNotificationContentModel {
+
+        val contentBuilder = PromotedNotificationContentModel.Builder(key)
 
         // TODO: Pitch a fit if style is unsupported or mandatory fields are missing once
         // FLAG_PROMOTED_ONGOING is set reliably and we're not testing status bar chips.
@@ -108,7 +161,7 @@
         contentBuilder.subText = notification.subText()
         contentBuilder.time = notification.extractWhen()
         contentBuilder.shortCriticalText = notification.shortCriticalText()
-        contentBuilder.lastAudiblyAlertedMs = entry.lastAudiblyAlertedMs
+        contentBuilder.lastAudiblyAlertedMs = lastAudiblyAlertedMs
         contentBuilder.profileBadgeResId = null // TODO
         contentBuilder.title = notification.title(recoveredBuilder.style)
         contentBuilder.text = notification.text(recoveredBuilder.style)
@@ -124,7 +177,7 @@
 
         recoveredBuilder.extractStyleContent(notification, contentBuilder, imageModelProvider)
 
-        return contentBuilder.build().also { logger.logExtractionSucceeded(entry, it) }
+        return contentBuilder.build()
     }
 
     private fun Notification.smallIconModel(imageModelProvider: ImageModelProvider): ImageModel? =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLogger.kt
index 5f9678a..6b6203d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLogger.kt
@@ -23,7 +23,7 @@
 import com.android.systemui.log.core.LogLevel.INFO
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.logKey
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
 import javax.inject.Inject
 
 @OptIn(ExperimentalStdlibApi::class)
@@ -56,7 +56,7 @@
 
     fun logExtractionSucceeded(
         entry: NotificationEntry,
-        content: PromotedNotificationContentModel,
+        content: PromotedNotificationContentModels,
     ) {
         buffer.log(
             EXTRACTION_TAG,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt
index ec4ee45..d9778bd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.util.kotlin.FlowDumperImpl
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.map
 
 @SysUISingleton
@@ -34,6 +35,16 @@
     /** The content to show as the promoted notification on AOD */
     val content: Flow<PromotedNotificationContentModel?> =
         promotedNotificationsInteractor.aodPromotedNotification
+            .map {
+                // TODO(b/400991304): show the private version when unlocked
+                it?.publicVersion
+            }
+            .distinctUntilNewInstance()
 
     val isPresent: Flow<Boolean> = content.map { it != null }.dumpWhileCollecting("isPresent")
+
+    /**
+     * Returns flow where all subsequent repetitions of the same object instance are filtered out.
+     */
+    private fun <T> Flow<T>.distinctUntilNewInstance() = distinctUntilChanged { a, b -> a === b }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt
index 96d41f1..08e7528 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt
@@ -25,8 +25,8 @@
 import com.android.systemui.statusbar.chips.screenrecord.domain.interactor.ScreenRecordChipInteractor
 import com.android.systemui.statusbar.chips.screenrecord.domain.model.ScreenRecordChipModel
 import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.Style.Ineligible
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
 import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
 import javax.inject.Inject
@@ -201,13 +201,13 @@
      * The top promoted notification represented by a chip, with the order determined by the order
      * of the chips, not the notifications.
      */
-    private val topPromotedChipNotification: Flow<PromotedNotificationContentModel?> =
+    private val topPromotedChipNotification: Flow<PromotedNotificationContentModels?> =
         orderedChipNotifications
             .map { list -> list.firstNotNullOfOrNull { it.promotedContent } }
             .distinctUntilNewInstance()
 
     /** This is the AOD promoted notification, which should avoid regular changing. */
-    val aodPromotedNotification: Flow<PromotedNotificationContentModel?> =
+    val aodPromotedNotification: Flow<PromotedNotificationContentModels?> =
         combine(
                 topPromotedChipNotification,
                 activeNotificationsInteractor.topLevelRepresentativeNotifications,
@@ -229,13 +229,13 @@
             .flowOn(backgroundDispatcher)
 
     private fun List<ActiveNotificationModel>.firstAodEligibleOrNull():
-        PromotedNotificationContentModel? {
+        PromotedNotificationContentModels? {
         return this.firstNotNullOfOrNull { it.promotedContent?.takeIfAodEligible() }
     }
 
-    private fun PromotedNotificationContentModel.takeIfAodEligible():
-        PromotedNotificationContentModel? {
-        return this.takeUnless { it.style == Ineligible }
+    private fun PromotedNotificationContentModels.takeIfAodEligible():
+        PromotedNotificationContentModels? {
+        return this.takeUnless { it.privateVersion.style == Ineligible }
     }
 
     /**
@@ -251,7 +251,7 @@
      */
     private data class NotifAndPromotedContent(
         val key: String,
-        val promotedContent: PromotedNotificationContentModel?,
+        val promotedContent: PromotedNotificationContentModels?,
     ) {
         /**
          * Define the equals of this object to only check the reference equality of the promoted
@@ -269,7 +269,7 @@
         /** Define the hashCode to be very quick, even if it increases collisions. */
         override fun hashCode(): Int {
             var result = key.hashCode()
-            result = 31 * result + (promotedContent?.identity?.hashCode() ?: 0)
+            result = 31 * result + (promotedContent?.key?.hashCode() ?: 0)
             return result
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt
index 57b0720..ffacf62 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt
@@ -29,6 +29,31 @@
 import com.android.systemui.statusbar.notification.row.LazyImage
 import com.android.systemui.statusbar.notification.row.shared.ImageModel
 
+data class PromotedNotificationContentModels(
+    /** The potentially redacted version of the content that will be exposed to the public */
+    val publicVersion: PromotedNotificationContentModel,
+    /** The unredacted version of the content that will be kept private */
+    val privateVersion: PromotedNotificationContentModel,
+) {
+    val key: String
+        get() = privateVersion.identity.key
+
+    init {
+        check(publicVersion.identity.key == privateVersion.identity.key) {
+            "public and private models must have the same key"
+        }
+    }
+
+    fun toRedactedString(): String {
+        val publicVersionString =
+            "==privateVersion".takeIf { privateVersion === publicVersion }
+                ?: publicVersion.toRedactedString()
+        return ("PromotedNotificationContentModels(" +
+            "privateVersion=${privateVersion.toRedactedString()}, " +
+            "publicVersion=$publicVersionString)")
+    }
+}
+
 /**
  * The content needed to render a promoted notification to surfaces besides the notification stack,
  * like the skeleton view on AOD or the status bar chip.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index dfadf74..bef3c69 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -2686,30 +2686,58 @@
     }
 
     /**
+     * Whether to allow dismissal with the whole-row translation animation.
+     *
+     * If true, either animation is permissible.
+     * If false, usingRTX behavior is forbidden, only clipping animation should be used.
+     *
+     * Usually either is OK, except for promoted notifications, where we always need to
+     * dismiss with content clipping/partial translation animation instead, so that we
+     * can show the demotion options.
+     * @return
+     */
+    private boolean allowDismissUsingRowTranslationX() {
+        if (Flags.permissionHelperInlineUiRichOngoing()) {
+            return !isPromotedOngoing();
+        } else {
+            // Don't change behavior unless the flag is on.
+            return true;
+        }
+    }
+
+    /**
      * Set the dismiss behavior of the view.
      *
      * @param usingRowTranslationX {@code true} if the view should translate using regular
      *                             translationX, otherwise the contents will be
      *                             translated.
+     * @param forceUpdateChildren {@code true} to force initialization, {@code false} if lazy
+     *                             behavior is OK.
      */
     @Override
-    public void setDismissUsingRowTranslationX(boolean usingRowTranslationX) {
-        if (usingRowTranslationX != mDismissUsingRowTranslationX) {
+    public void setDismissUsingRowTranslationX(boolean usingRowTranslationX,
+            boolean forceUpdateChildren) {
+        // Before updating dismiss behavior, make sure this is an allowable configuration for this
+        // notification.
+        usingRowTranslationX = usingRowTranslationX && allowDismissUsingRowTranslationX();
+
+        if (forceUpdateChildren || (usingRowTranslationX != mDismissUsingRowTranslationX)) {
             // In case we were already transitioning, let's switch over!
             float previousTranslation = getTranslation();
             if (previousTranslation != 0) {
                 setTranslation(0);
             }
-            super.setDismissUsingRowTranslationX(usingRowTranslationX);
+            super.setDismissUsingRowTranslationX(usingRowTranslationX, forceUpdateChildren);
             if (previousTranslation != 0) {
                 setTranslation(previousTranslation);
             }
+
             if (mChildrenContainer != null) {
                 List<ExpandableNotificationRow> notificationChildren =
                         mChildrenContainer.getAttachedChildren();
                 for (int i = 0; i < notificationChildren.size(); i++) {
                     ExpandableNotificationRow child = notificationChildren.get(i);
-                    child.setDismissUsingRowTranslationX(usingRowTranslationX);
+                    child.setDismissUsingRowTranslationX(usingRowTranslationX, forceUpdateChildren);
                 }
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
index 80cf818..6c990df 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
@@ -292,7 +292,8 @@
      *                             translationX, otherwise the contents will be
      *                             translated.
      */
-    public void setDismissUsingRowTranslationX(boolean usingRowTranslationX) {
+    public void setDismissUsingRowTranslationX(boolean usingRowTranslationX,
+            boolean forceUpdateChildren) {
         mDismissUsingRowTranslationX = usingRowTranslationX;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
index d97e25f..57ceafc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
@@ -60,6 +60,7 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor;
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel;
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels;
 import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation;
 import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation;
 import com.android.systemui.statusbar.notification.row.shared.ImageModelProvider;
@@ -1003,7 +1004,7 @@
         row.mImageModelIndex = result.mRowImageInflater.getNewImageIndex();
 
         if (PromotedNotificationContentModel.featureFlagEnabled()) {
-            entry.setPromotedNotificationContentModel(result.mPromotedContent);
+            entry.setPromotedNotificationContentModels(result.mPromotedContent);
         }
 
         boolean setRepliesAndActions = true;
@@ -1387,9 +1388,9 @@
                 mLogger.logAsyncTaskProgress(logKey, "extracting promoted notification content");
                 final ImageModelProvider imageModelProvider =
                         result.mRowImageInflater.useForContentModel();
-                final PromotedNotificationContentModel promotedContent =
+                final PromotedNotificationContentModels promotedContent =
                         mPromotedNotificationContentExtractor.extractContent(mEntry,
-                                recoveredBuilder, imageModelProvider);
+                                recoveredBuilder, mBindParams.redactionType, imageModelProvider);
                 mLogger.logAsyncTaskProgress(logKey, "extracted promoted notification content: "
                         + promotedContent);
 
@@ -1503,7 +1504,7 @@
     static class InflationProgress {
         RowImageInflater mRowImageInflater;
 
-        PromotedNotificationContentModel mPromotedContent;
+        PromotedNotificationContentModels mPromotedContent;
 
         private RemoteViews newContentView;
         private RemoteViews newHeadsUpView;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
index cdb78d9..f4e01bf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
@@ -29,6 +29,7 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings;
@@ -309,6 +310,7 @@
         });
 
         View gutsView = item.getGutsView();
+
         try {
             if (gutsView instanceof NotificationSnooze) {
                 initializeSnoozeView(row, (NotificationSnooze) gutsView);
@@ -322,6 +324,8 @@
                         (PartialConversationInfo) gutsView);
             } else if (gutsView instanceof FeedbackInfo) {
                 initializeFeedbackInfo(row, (FeedbackInfo) gutsView);
+            } else if (gutsView instanceof PromotedPermissionGutsContent) {
+                initializeDemoteView(row, (PromotedPermissionGutsContent) gutsView);
             }
             return true;
         } catch (Exception e) {
@@ -351,6 +355,31 @@
     }
 
     /**
+     * Sets up the {@link NotificationSnooze} inside the notification row's guts.
+     *
+     * @param row view to set up the guts for
+     * @param demoteGuts view to set up/bind within {@code row}
+     */
+    private void initializeDemoteView(
+            final ExpandableNotificationRow row,
+            PromotedPermissionGutsContent demoteGuts) {
+        StatusBarNotification sbn = row.getEntry().getSbn();
+        demoteGuts.setStatusBarNotification(sbn);
+        demoteGuts.setOnDemoteAction(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                try {
+                    // TODO(b/391661009): Signal AutomaticPromotionCoordinator here
+                    mNotificationManager.setCanBePromoted(
+                            sbn.getPackageName(), sbn.getUid(), false, true);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Couldn't revoke live update permission", e);
+                }
+            }
+        });
+    }
+
+    /**
      * Sets up the {@link FeedbackInfo} inside the notification row's guts.
      *
      * @param row view to set up the guts for
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java
index c03dc27..f494a4c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java
@@ -272,6 +272,7 @@
         } else if (personNotifType >= PeopleNotificationIdentifier.TYPE_FULL_PERSON) {
             mInfoItem = createConversationItem(mContext);
         } else if (android.app.Flags.uiRichOngoing()
+                && android.app.Flags.apiRichOngoing()
                 && Flags.permissionHelperUiRichOngoing()
                 && sbn.getNotification().isPromotedOngoing()) {
             mInfoItem = createPromotedItem(mContext);
@@ -284,6 +285,15 @@
         }
         mRightMenuItems.add(mInfoItem);
         mRightMenuItems.add(mFeedbackItem);
+        boolean isPromotedOngoing = NotificationBundleUi.isEnabled()
+                ? mParent.getEntryAdapter().isPromotedOngoing()
+                : mParent.getEntryLegacy().isPromotedOngoing();
+        if (android.app.Flags.uiRichOngoing() && Flags.permissionHelperInlineUiRichOngoing()
+                && isPromotedOngoing) {
+            mRightMenuItems.add(createDemoteItem(mContext));
+        }
+
+
         mLeftMenuItems.addAll(mRightMenuItems);
 
         populateMenuViews();
@@ -305,15 +315,19 @@
         } else {
             mMenuContainer = new FrameLayout(mContext);
         }
+
         final int showDismissSetting =  Settings.Global.getInt(mContext.getContentResolver(),
                 Settings.Global.SHOW_NEW_NOTIF_DISMISS, /* default = */ 1);
         final boolean newFlowHideShelf = showDismissSetting == 1;
-        if (newFlowHideShelf) {
-            return;
-        }
-        List<MenuItem> menuItems = mOnLeft ? mLeftMenuItems : mRightMenuItems;
-        for (int i = 0; i < menuItems.size(); i++) {
-            addMenuView(menuItems.get(i), mMenuContainer);
+
+        // Populate menu items if we are using the new permission helper (U+) or if we are using
+        // the very old dismiss setting (SC-).
+        // TODO: SHOW_NEW_NOTIF_DISMISS==0 case can likely be removed.
+        if (Flags.permissionHelperInlineUiRichOngoing() || !newFlowHideShelf) {
+            List<MenuItem> menuItems = mOnLeft ? mLeftMenuItems : mRightMenuItems;
+            for (int i = 0; i < menuItems.size(); i++) {
+                addMenuView(menuItems.get(i), mMenuContainer);
+            }
         }
     }
 
@@ -679,6 +693,15 @@
         return snooze;
     }
 
+    static MenuItem createDemoteItem(Context context) {
+        PromotedPermissionGutsContent demoteContent =
+                (PromotedPermissionGutsContent) LayoutInflater.from(context).inflate(
+                R.layout.promoted_permission_guts, null, false);
+        MenuItem info = new NotificationMenuItem(context, null, demoteContent,
+                R.drawable.unpin_icon);
+        return info;
+    }
+
     static NotificationMenuItem createConversationItem(Context context) {
         Resources res = context.getResources();
         String infoDescription = res.getString(R.string.notification_menu_gear_description);
@@ -686,7 +709,7 @@
                 (NotificationConversationInfo) LayoutInflater.from(context).inflate(
                         R.layout.notification_conversation_info, null, false);
         return new NotificationMenuItem(context, infoDescription, infoContent,
-                R.drawable.ic_settings);
+                NotificationMenuItem.OMIT_FROM_SWIPE_MENU);
     }
 
     static NotificationMenuItem createPromotedItem(Context context) {
@@ -696,7 +719,7 @@
                 (PromotedNotificationInfo) LayoutInflater.from(context).inflate(
                         R.layout.promoted_notification_info, null, false);
         return new NotificationMenuItem(context, infoDescription, infoContent,
-                R.drawable.ic_settings);
+                NotificationMenuItem.OMIT_FROM_SWIPE_MENU);
     }
 
     static NotificationMenuItem createPartialConversationItem(Context context) {
@@ -706,7 +729,7 @@
                 (PartialConversationInfo) LayoutInflater.from(context).inflate(
                         R.layout.partial_conversation_info, null, false);
         return new NotificationMenuItem(context, infoDescription, infoContent,
-                R.drawable.ic_settings);
+                NotificationMenuItem.OMIT_FROM_SWIPE_MENU);
     }
 
     static NotificationMenuItem createInfoItem(Context context) {
@@ -718,14 +741,14 @@
         NotificationInfo infoContent = (NotificationInfo) LayoutInflater.from(context).inflate(
                 layoutId, null, false);
         return new NotificationMenuItem(context, infoDescription, infoContent,
-                R.drawable.ic_settings);
+                NotificationMenuItem.OMIT_FROM_SWIPE_MENU);
     }
 
     static MenuItem createFeedbackItem(Context context) {
         FeedbackInfo feedbackContent = (FeedbackInfo) LayoutInflater.from(context).inflate(
                 R.layout.feedback_info, null, false);
         MenuItem info = new NotificationMenuItem(context, null, feedbackContent,
-                -1 /*don't show in slow swipe menu */);
+                NotificationMenuItem.OMIT_FROM_SWIPE_MENU);
         return info;
     }
 
@@ -762,6 +785,10 @@
 
     @Override
     public boolean isWithinSnapMenuThreshold() {
+        if (getSpaceForMenu() == 0) {
+            // don't snap open if there are no items
+            return false;
+        }
         float translation = getTranslation();
         float snapBackThreshold = getSnapBackThreshold();
         float targetRight = getDismissThreshold();
@@ -803,6 +830,10 @@
     }
 
     public static class NotificationMenuItem implements MenuItem {
+
+        // Constant signaling that this MenuItem should not appear in slow swipe.
+        public static final int OMIT_FROM_SWIPE_MENU = -1;
+
         View mMenuView;
         GutsContent mGutsContent;
         String mContentDescription;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
index ae52db8..4f1b905 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
@@ -53,6 +53,7 @@
 import com.android.systemui.statusbar.notification.logKey
 import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
 import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED
 import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_EXPANDED
 import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP
@@ -595,7 +596,7 @@
         val rowImageInflater: RowImageInflater,
         val remoteViews: NewRemoteViews,
         val contentModel: NotificationContentModel,
-        val promotedContent: PromotedNotificationContentModel?,
+        val promotedContent: PromotedNotificationContentModels?,
     ) {
 
         var inflatedContentView: View? = null
@@ -700,7 +701,12 @@
                     )
                     val imageModelProvider = rowImageInflater.useForContentModel()
                     promotedNotificationContentExtractor
-                        .extractContent(entry, builder, imageModelProvider)
+                        .extractContent(
+                            entry,
+                            builder,
+                            bindParams.redactionType,
+                            imageModelProvider,
+                        )
                         .also {
                             logger.logAsyncTaskProgress(
                                 entry.logKey,
@@ -1519,7 +1525,7 @@
 
             entry.setContentModel(result.contentModel)
             if (PromotedNotificationContentModel.featureFlagEnabled()) {
-                entry.promotedNotificationContentModel = result.promotedContent
+                entry.promotedNotificationContentModels = result.promotedContent
             }
 
             result.inflatedSmartReplyState?.let { row.privateLayout.setInflatedSmartReplyState(it) }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfo.java
index 01ee788..769f0b5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfo.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfo.java
@@ -80,6 +80,7 @@
                 assistantFeedbackController, metricsLogger, onCloseClick);
 
         mNotificationManager = iNotificationManager;
+
         mPackageDemotionInteractor = packageDemotionInteractor;
 
         bindDemote(entry.getSbn(), pkg);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedPermissionGutsContent.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedPermissionGutsContent.java
new file mode 100644
index 0000000..222a1f4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedPermissionGutsContent.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.service.notification.StatusBarNotification;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.systemui.res.R;
+
+/**
+ * This GutsContent shows an explanatory interstitial telling the user they've just revoked this
+ * app's permission to post Promoted/Live notifications.
+ * If the guts are dismissed without further action, the revocation is committed.
+ * If the user hits undo, the permission is not revoked.
+ */
+public class PromotedPermissionGutsContent extends LinearLayout
+        implements NotificationGuts.GutsContent, View.OnClickListener {
+
+    private static final String TAG = "SnoozyPromotedGuts";
+
+    private NotificationGuts mGutsContainer;
+    private StatusBarNotification mSbn;
+
+    private TextView mUndoButton;
+
+    private MetricsLogger mMetricsLogger = new MetricsLogger();
+    private OnClickListener mDemoteAction;
+
+    public PromotedPermissionGutsContent(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mUndoButton = (TextView) findViewById(R.id.undo);
+        mUndoButton.setOnClickListener(this);
+        mUndoButton.setContentDescription(
+                getContext().getString(R.string.snooze_undo_content_description));
+
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        dispatchConfigurationChanged(getResources().getConfiguration());
+    }
+
+    /**
+     * Update the content description of the snooze view based on the snooze option and whether the
+     * snooze options are expanded or not.
+     * For example, this will be something like "Collapsed\u2029Snooze for 1 hour". The paragraph
+     * separator is added to introduce a break in speech, to match what TalkBack does by default
+     * when you e.g. press on a notification.
+     */
+    private void updateContentDescription() {
+        //
+    }
+
+
+    @Override
+    public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+        if (super.performAccessibilityActionInternal(action, arguments)) {
+            return true;
+        }
+        if (action == R.id.action_snooze_undo) {
+            undoDemote(mUndoButton);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * TODO docs
+     * @param sbn
+     */
+    public void setStatusBarNotification(StatusBarNotification sbn) {
+        mSbn = sbn;
+        TextView demoteExplanation = (TextView) findViewById(R.id.demote_explain);
+        demoteExplanation.setText(mContext.getResources().getString(R.string.demote_explain_text,
+                mSbn.getPackageName()));
+    }
+
+    @Override
+    public void onClick(View v) {
+        if (mGutsContainer != null) {
+            mGutsContainer.resetFalsingCheck();
+        }
+        final int id = v.getId();
+        if (id == R.id.undo) {
+            undoDemote(v);
+        }
+
+    }
+
+    private void undoDemote(View v) {
+        // Don't commit the demote action, instead log the undo and dismiss the view.
+        mGutsContainer.closeControls(v, /* save= */ false);
+    }
+
+    @Override
+    public int getActualHeight() {
+        return getHeight();
+    }
+
+    @Override
+    public boolean willBeRemoved() {
+        return false;
+    }
+
+    @Override
+    public View getContentView() {
+        return this;
+    }
+
+    @Override
+    public void setGutsParent(NotificationGuts guts) {
+        mGutsContainer = guts;
+    }
+
+    @Override
+    public boolean handleCloseControls(boolean save, boolean force) {
+        if (!save) {
+            // Undo changes and let the guts handle closing the view
+            return false;
+        } else {
+            // Commit demote action.
+            mDemoteAction.onClick(this);
+            return false;
+        }
+    }
+
+    @Override
+    public boolean isLeavebehind() {
+        return true;
+    }
+
+    @Override
+    public boolean shouldBeSavedOnClose() {
+        return true;
+    }
+
+    @Override
+    public boolean needsFalsingProtection() {
+        return false;
+    }
+
+    public void setOnDemoteAction(OnClickListener demoteAction) {
+        mDemoteAction = demoteAction;
+    }
+
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt
index 487cbce..9652738 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt
@@ -20,6 +20,7 @@
 import android.util.Log
 import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
 import com.android.systemui.statusbar.notification.stack.PriorityBucket
 
 /**
@@ -88,7 +89,7 @@
      * The content needed to render this as a promoted notification on various surfaces, or null if
      * this notification cannot be rendered as a promoted notification.
      */
-    val promotedContent: PromotedNotificationContentModel?,
+    val promotedContent: PromotedNotificationContentModels?,
 ) : ActiveNotificationEntryModel() {
     init {
         if (!PromotedNotificationContentModel.featureFlagEnabled()) {
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 9fea750..503256a 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
@@ -3220,8 +3220,7 @@
         updateAnimationState(child);
         updateChronometerForChild(child);
         if (child instanceof ExpandableNotificationRow row) {
-            row.setDismissUsingRowTranslationX(mDismissUsingRowTranslationX);
-
+            row.setDismissUsingRowTranslationX(mDismissUsingRowTranslationX, /* force= */ true);
         }
     }
 
@@ -6157,7 +6156,7 @@
                 View child = getChildAt(i);
                 if (child instanceof ExpandableNotificationRow) {
                     ((ExpandableNotificationRow) child).setDismissUsingRowTranslationX(
-                            dismissUsingRowTranslationX);
+                            dismissUsingRowTranslationX, /* force= */ false);
                 }
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
index e4e56c5..8a5b221 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
@@ -527,7 +527,8 @@
                 break;
             case MODE_SHOW_BOUNCER:
                 Trace.beginSection("MODE_SHOW_BOUNCER");
-                mKeyguardViewController.showPrimaryBouncer(true);
+                mKeyguardViewController.showPrimaryBouncer(true,
+                        "BiometricUnlockController#MODE_SHOW_BOUNCER");
                 Trace.endSection();
                 break;
             case MODE_WAKE_AND_UNLOCK_FROM_DREAM:
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 9d9f01b..e617254 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -2407,11 +2407,12 @@
                 }
 
                 if (needsBouncer) {
-                    Log.d(TAG, "showBouncerOrLockScreenIfKeyguard, showingBouncer");
+                    var reason = "CentralSurfacesImpl#showBouncerOrLockScreenIfKeyguard";
                     if (SceneContainerFlag.isEnabled()) {
-                        mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */);
+                        mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */,
+                                reason);
                     } else {
-                        mStatusBarKeyguardViewManager.showBouncer(true /* scrimmed */);
+                        mStatusBarKeyguardViewManager.showBouncer(true /* scrimmed */, reason);
                     }
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index f3d7202..d68f7df 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -538,10 +538,7 @@
     private void handleBlurSupportedChanged(boolean isBlurSupported) {
         this.mIsBlurSupported = isBlurSupported;
         if (Flags.bouncerUiRevamp()) {
-            // TODO: animate blur fallback when the bouncer is pulled up.
-            for (ScrimState state : ScrimState.values()) {
-                state.setDefaultScrimAlpha(getDefaultScrimAlpha(true));
-            }
+            updateDefaultScrimAlphas();
             if (isBlurSupported) {
                 ScrimState.BOUNCER_SCRIMMED.setNotifBlurRadius(mBlurConfig.getMaxBlurRadiusPx());
             } else {
@@ -549,17 +546,7 @@
             }
         }
         if (Flags.notificationShadeBlur()) {
-            float inFrontAlpha = mInFrontAlpha;
-            float behindAlpha = mBehindAlpha;
-            float notifAlpha = mNotificationsAlpha;
-
             mState.prepare(mState);
-            applyState();
-            startScrimAnimation(mScrimBehind, behindAlpha);
-            startScrimAnimation(mNotificationsScrim, notifAlpha);
-            startScrimAnimation(mScrimInFront, inFrontAlpha);
-            dispatchBackScrimState(mScrimBehind.getViewAlpha());
-        } else if (Flags.bouncerUiRevamp()) {
             applyAndDispatchState();
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index 8c44fe5..5123409 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -669,7 +669,8 @@
          * show if any subsequent events are to be handled.
          */
         if (!SceneContainerFlag.isEnabled() && beginShowingBouncer(event)) {
-            mPrimaryBouncerInteractor.show(/* isScrimmed= */false);
+            mPrimaryBouncerInteractor.show(/* isScrimmed= */false,
+                    TAG + "#onPanelExpansionChanged");
         }
 
         if (!primaryBouncerIsOrWillBeShowing()) {
@@ -714,7 +715,8 @@
      * Shows the notification keyguard or the bouncer depending on
      * {@link #needsFullscreenBouncer()}.
      */
-    protected void showBouncerOrKeyguard(boolean hideBouncerWhenShowing, boolean isFalsingReset) {
+    protected void showBouncerOrKeyguard(boolean hideBouncerWhenShowing, boolean isFalsingReset,
+            String reason) {
         boolean showBouncer = needsFullscreenBouncer() && !mDozing;
         if (Flags.simPinRaceConditionOnRestart()) {
             showBouncer = showBouncer && !mIsSleeping;
@@ -726,11 +728,11 @@
                     mCentralSurfaces.hideKeyguard();
                     mSceneInteractorLazy.get().showOverlay(
                             Overlays.Bouncer,
-                            "StatusBarKeyguardViewManager.showBouncerOrKeyguard"
+                            TAG + "#showBouncerOrKeyguard"
                     );
                 } else {
                     if (Flags.simPinRaceConditionOnRestart()) {
-                        if (mPrimaryBouncerInteractor.show(/* isScrimmed= */ true)) {
+                        if (mPrimaryBouncerInteractor.show(/* isScrimmed= */ true, reason)) {
                             mAttemptsToShowBouncer = 0;
                             mCentralSurfaces.hideKeyguard();
                         } else {
@@ -744,19 +746,19 @@
                                         + mAttemptsToShowBouncer++);
                                 mExecutor.executeDelayed(() ->
                                         showBouncerOrKeyguard(hideBouncerWhenShowing,
-                                            isFalsingReset),
+                                            isFalsingReset, reason),
                                         500);
                             }
                         }
                     } else {
                         mCentralSurfaces.hideKeyguard();
-                        mPrimaryBouncerInteractor.show(/* isScrimmed= */ true);
+                        mPrimaryBouncerInteractor.show(/* isScrimmed= */ true, reason);
                     }
                 }
             } else if (!isFalsingReset) {
                 // Falsing resets can cause this to flicker, so don't reset in this case
                 Log.i(TAG, "Sim bouncer is already showing, issuing a refresh");
-                mPrimaryBouncerInteractor.show(/* isScrimmed= */ true);
+                mPrimaryBouncerInteractor.show(/* isScrimmed= */ true, reason);
 
             }
         } else {
@@ -776,7 +778,7 @@
      *                 false when the user will be dragging it and translation should be deferred
      *                 {@see KeyguardBouncer#show(boolean, boolean)}
      */
-    public void showBouncer(boolean scrimmed) {
+    public void showBouncer(boolean scrimmed, String reason) {
         if (SceneContainerFlag.isEnabled()) {
             mDeviceEntryInteractorLazy.get().attemptDeviceEntry();
             return;
@@ -787,7 +789,7 @@
             mAlternateBouncerInteractor.forceShow();
             updateAlternateBouncerShowing(mAlternateBouncerInteractor.isVisibleState());
         } else {
-            showPrimaryBouncer(scrimmed);
+            showPrimaryBouncer(scrimmed, reason);
         }
     }
 
@@ -810,8 +812,10 @@
      *
      * @param scrimmed true when the bouncer should show scrimmed, false when the user will be
      * dragging it and translation should be deferred {@see KeyguardBouncer#show(boolean, boolean)}
+     * @param reason string description for what is causing the bouncer to be requested
      */
-    public void showPrimaryBouncer(boolean scrimmed) {
+    @Override
+    public void showPrimaryBouncer(boolean scrimmed, String reason) {
         hideAlternateBouncer(
                 /* updateScrim= */ false,
                 // When the scene framework is on, don't ever clear the pending dismiss action from
@@ -823,7 +827,7 @@
                         "primary bouncer requested"
                 );
             } else {
-                mPrimaryBouncerInteractor.show(scrimmed);
+                mPrimaryBouncerInteractor.show(scrimmed, reason);
             }
         }
         updateStates();
@@ -870,7 +874,7 @@
                 );
             }
 
-            showBouncer(true);
+            showBouncer(true, TAG + "#dismissWithAction");
             Trace.endSection();
             return;
         }
@@ -919,10 +923,11 @@
                     if (SceneContainerFlag.isEnabled()) {
                         mSceneInteractorLazy.get().showOverlay(
                                 Overlays.Bouncer,
-                                "StatusBarKeyguardViewManager.dismissWithAction"
+                                TAG + "#dismissWithAction"
                         );
                     } else {
-                        mPrimaryBouncerInteractor.show(/* isScrimmed= */ true);
+                        mPrimaryBouncerInteractor.show(/* isScrimmed= */ true,
+                                TAG + "#dismissWithAction, afterKeyguardGone");
                     }
                 } else {
                     // after authentication success, run dismiss action with the option to defer
@@ -932,10 +937,11 @@
                     if (SceneContainerFlag.isEnabled()) {
                         mSceneInteractorLazy.get().showOverlay(
                                 Overlays.Bouncer,
-                                "StatusBarKeyguardViewManager.dismissWithAction"
+                                TAG + "#dismissWithAction"
                         );
                     } else {
-                        mPrimaryBouncerInteractor.show(/* isScrimmed= */ true);
+                        mPrimaryBouncerInteractor.show(/* isScrimmed= */ true,
+                                TAG + "#dismissWithAction");
                     }
                     // bouncer will handle the dismiss action, so we no longer need to track it here
                     mAfterKeyguardGoneAction = null;
@@ -992,7 +998,7 @@
                     }
                 }
             } else {
-                showBouncerOrKeyguard(hideBouncerWhenShowing, isFalsingReset);
+                showBouncerOrKeyguard(hideBouncerWhenShowing, isFalsingReset, "reset");
             }
             if (!SceneContainerFlag.isEnabled() && hideBouncerWhenShowing && isBouncerShowing()) {
                 hideAlternateBouncer(true);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
index 8389aab..85fc9d4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
@@ -156,7 +156,8 @@
         if (!row.isPinned()) {
             mStatusBarStateController.setLeaveOpenOnKeyguardHide(true);
         }
-        mStatusBarKeyguardViewManager.showBouncer(true /* scrimmed */);
+        mStatusBarKeyguardViewManager.showBouncer(true /* scrimmed */,
+                "StatusBarRemoteInputCallback#onLockedRemoteInput");
         mPendingRemoteInputView = clicked;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
index 61b7d80..b7eada1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
@@ -38,7 +38,7 @@
 import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore
 import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler
 import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
 import com.android.systemui.statusbar.notification.shared.CallType
 import com.android.systemui.statusbar.phone.ongoingcall.data.repository.OngoingCallRepository
@@ -347,7 +347,7 @@
          * If the call notification also meets promoted notification criteria, this field is filled
          * in with the content related to promotion. Otherwise null.
          */
-        val promotedContent: PromotedNotificationContentModel?,
+        val promotedContent: PromotedNotificationContentModels?,
         /** True if the call is currently ongoing (as opposed to incoming, screening, etc.). */
         val isOngoing: Boolean,
         /** True if the user has swiped away the status bar while in this phone call. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt
index 322dfff..9546d37 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt
@@ -18,7 +18,7 @@
 
 import android.app.PendingIntent
 import com.android.systemui.statusbar.StatusBarIconView
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
 
 /** Represents the state of any ongoing calls. */
 sealed interface OngoingCallModel {
@@ -47,7 +47,7 @@
         val intent: PendingIntent?,
         val notificationKey: String,
         val appName: String,
-        val promotedContent: PromotedNotificationContentModel?,
+        val promotedContent: PromotedNotificationContentModels?,
         val isAppVisible: Boolean,
     ) : OngoingCallModel
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt
index 4c6374b..efab21f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt
@@ -37,7 +37,7 @@
     protected open val users: List<UserRecord>
         get() = controller.users.filter {
             (!controller.isKeyguardShowing || !it.isRestricted) &&
-                (controller.isUserSwitcherEnabled || it.isCurrent)
+                (controller.isUserSwitcherEnabled || it.isCurrent || it.isSignOut)
         }
 
     init {
@@ -109,6 +109,7 @@
                     item.isAddUser,
                     item.isGuest,
                     item.isAddSupervisedUser,
+                    item.isSignOut,
                     isTablet,
                     item.isManageUsers,
                 )
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyLogger.kt b/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyLogger.kt
index 1cc7a31..5541c50 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyLogger.kt
@@ -50,7 +50,7 @@
                 onScreenTurningOnToOnDrawnMs,
                 onDrawnToOnScreenTurnedOnMs,
                 trackingResult,
-                screenWakelockstatus
+                screenWakelockStatus,
             )
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt
index 5800d5e..336e8d1 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt
@@ -36,11 +36,11 @@
 import com.android.systemui.power.shared.model.WakefulnessModel
 import com.android.systemui.power.shared.model.WakefulnessState
 import com.android.systemui.shared.system.SysUiStatsLog
-import com.android.systemui.unfold.DisplaySwitchLatencyTracker.DisplaySwitchLatencyEvent
 import com.android.systemui.unfold.DisplaySwitchLatencyTracker.TrackingResult.CORRUPTED
 import com.android.systemui.unfold.DisplaySwitchLatencyTracker.TrackingResult.SUCCESS
 import com.android.systemui.unfold.DisplaySwitchLatencyTracker.TrackingResult.TIMED_OUT
 import com.android.systemui.unfold.dagger.UnfoldSingleThreadBg
+import com.android.systemui.unfold.data.repository.ScreenTimeoutPolicyRepository
 import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionStarted
 import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor
 import com.android.systemui.util.Compile
@@ -80,6 +80,7 @@
     private val context: Context,
     private val deviceStateRepository: DeviceStateRepository,
     private val powerInteractor: PowerInteractor,
+    private val screenTimeoutPolicyRepository: ScreenTimeoutPolicyRepository,
     private val unfoldTransitionInteractor: UnfoldTransitionInteractor,
     private val animationStatusRepository: AnimationStatusRepository,
     private val keyguardInteractor: KeyguardInteractor,
@@ -287,7 +288,18 @@
         log { "fromFoldableDeviceState=$fromFoldableDeviceState" }
         instantForTrack(TAG) { "fromFoldableDeviceState=$fromFoldableDeviceState" }
 
-        return copy(fromFoldableDeviceState = fromFoldableDeviceState)
+        val screenTimeoutActive = screenTimeoutPolicyRepository.screenTimeoutActive.value
+        val screenWakelockStatus =
+            if (screenTimeoutActive) {
+                NO_SCREEN_WAKELOCKS
+            } else {
+                HAS_SCREEN_WAKELOCKS
+            }
+
+        return copy(
+            fromFoldableDeviceState = fromFoldableDeviceState,
+            screenWakelockStatus = screenWakelockStatus
+        )
     }
 
     private fun DisplaySwitchLatencyEvent.withAfterFields(
@@ -344,7 +356,7 @@
         val onDrawnToOnScreenTurnedOnMs: Int = VALUE_UNKNOWN,
         val trackingResult: Int =
             SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TRACKING_RESULT__UNKNOWN_RESULT,
-        val screenWakelockstatus: Int =
+        val screenWakelockStatus: Int =
             SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__SCREEN_WAKELOCK_STATUS__SCREEN_WAKELOCK_STATUS_UNKNOWN,
     )
 
@@ -372,5 +384,10 @@
             SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_OPENED
         private const val FOLDABLE_DEVICE_STATE_FLIPPED =
             SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_FLIPPED
+
+        private const val HAS_SCREEN_WAKELOCKS =
+            SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__SCREEN_WAKELOCK_STATUS__SCREEN_WAKELOCK_STATUS_HAS_SCREEN_WAKELOCKS
+        private const val NO_SCREEN_WAKELOCKS =
+            SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__SCREEN_WAKELOCK_STATUS__SCREEN_WAKELOCK_STATUS_NO_WAKELOCKS
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/data/repository/ScreenTimeoutPolicyRepository.kt b/packages/SystemUI/src/com/android/systemui/unfold/data/repository/ScreenTimeoutPolicyRepository.kt
new file mode 100644
index 0000000..7979394
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/unfold/data/repository/ScreenTimeoutPolicyRepository.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2025 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.unfold.data.repository
+
+import android.os.PowerManager
+import android.view.Display
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import java.util.concurrent.Executor
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.stateIn
+
+/** Repository to get screen timeout updates */
+@SysUISingleton
+class ScreenTimeoutPolicyRepository
+@Inject
+constructor(
+    private val powerManager: PowerManager,
+    @Background private val executor: Executor,
+    @Background private val scope: CoroutineScope,
+) {
+
+    /** Stores true if there is an active screen timeout */
+    val screenTimeoutActive: StateFlow<Boolean> =
+        conflatedCallbackFlow {
+                val listener =
+                    PowerManager.ScreenTimeoutPolicyListener { screenTimeoutPolicy ->
+                        trySend(screenTimeoutPolicy == PowerManager.SCREEN_TIMEOUT_ACTIVE)
+                    }
+                powerManager.addScreenTimeoutPolicyListener(
+                    Display.DEFAULT_DISPLAY,
+                    executor,
+                    listener,
+                )
+                awaitClose {
+                    powerManager.removeScreenTimeoutPolicyListener(
+                        Display.DEFAULT_DISPLAY,
+                        listener,
+                    )
+                }
+            }
+            .stateIn(scope, started = SharingStarted.Eagerly, initialValue = true)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt b/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt
index d4fb563..e16a51a 100644
--- a/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt
@@ -42,6 +42,8 @@
     @JvmField val isSwitchToEnabled: Boolean = false,
     /** Whether this record represents an option to add another supervised user to the device. */
     @JvmField val isAddSupervisedUser: Boolean = false,
+    /** Whether this record represents an option to sign out of the current user. */
+    @JvmField val isSignOut: Boolean = false,
     /**
      * An enforcing admin, if the user action represented by this record is disabled by the admin.
      * If not disabled, this is `null`.
@@ -49,7 +51,7 @@
     @JvmField val enforcedAdmin: RestrictedLockUtils.EnforcedAdmin? = null,
 
     /** Whether this record is to go to the Settings page to manage users. */
-    @JvmField val isManageUsers: Boolean = false
+    @JvmField val isManageUsers: Boolean = false,
 ) {
     /** Returns a new instance of [UserRecord] with its [isCurrent] set to the given value. */
     fun copyWithIsCurrent(isCurrent: Boolean): UserRecord {
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt
index 163288b..b82aefc 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt
@@ -38,6 +38,7 @@
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.KeyguardUpdateMonitorCallback
 import com.android.systemui.Flags.switchUserOnBg
+import com.android.systemui.Flags.userSwitcherAddSignOutOption
 import com.android.systemui.SystemUISecondaryUserService
 import com.android.systemui.animation.Expandable
 import com.android.systemui.broadcast.BroadcastDispatcher
@@ -110,6 +111,7 @@
     private val uiEventLogger: UiEventLogger,
     private val userRestrictionChecker: UserRestrictionChecker,
     private val processWrapper: ProcessWrapper,
+    private val userLogoutInteractor: UserLogoutInteractor,
 ) {
     /**
      * Defines interface for classes that can be notified when the state of users on the device is
@@ -242,6 +244,12 @@
                         ) {
                             add(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
                         }
+                        if (
+                            userSwitcherAddSignOutOption() &&
+                                userLogoutInteractor.isLogoutEnabled.value
+                        ) {
+                            add(UserActionModel.SIGN_OUT)
+                        }
                     }
                 }
                 .flowOn(backgroundDispatcher)
@@ -261,7 +269,8 @@
                                 action = it,
                                 selectedUserId = selectedUserInfo.id,
                                 isRestricted =
-                                    it != UserActionModel.ENTER_GUEST_MODE &&
+                                    it != UserActionModel.SIGN_OUT &&
+                                        it != UserActionModel.ENTER_GUEST_MODE &&
                                         it != UserActionModel.NAVIGATE_TO_USER_MANAGEMENT &&
                                         !settings.isAddUsersFromLockscreen,
                             )
@@ -499,6 +508,10 @@
                     Intent(Settings.ACTION_USER_SETTINGS),
                     /* dismissShade= */ true,
                 )
+            UserActionModel.SIGN_OUT -> {
+                dismissDialog()
+                applicationScope.launch { userLogoutInteractor.logOut() }
+            }
         }
     }
 
@@ -583,9 +596,10 @@
             actionType = action,
             isRestricted = isRestricted,
             isSwitchToEnabled =
-                canSwitchUsers(selectedUserId = selectedUserId, isAction = true) &&
-                    // If the user is auto-created is must not be currently resetting.
-                    !(isGuestUserAutoCreated && isGuestUserResetting),
+                action == UserActionModel.SIGN_OUT ||
+                    (canSwitchUsers(selectedUserId = selectedUserId, isAction = true) &&
+                        // If the user is auto-created is must not be currently resetting.
+                        !(isGuestUserAutoCreated && isGuestUserResetting)),
             userRestrictionChecker = userRestrictionChecker,
         )
     }
diff --git a/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt
index 80139bd..23ca4ce 100644
--- a/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt
@@ -74,6 +74,7 @@
             isGuest = actionType == UserActionModel.ENTER_GUEST_MODE,
             isAddUser = actionType == UserActionModel.ADD_USER,
             isAddSupervisedUser = actionType == UserActionModel.ADD_SUPERVISED_USER,
+            isSignOut = actionType == UserActionModel.SIGN_OUT,
             isRestricted = isRestricted,
             isSwitchToEnabled = isSwitchToEnabled,
             enforcedAdmin =
@@ -94,6 +95,7 @@
             record.isAddSupervisedUser -> UserActionModel.ADD_SUPERVISED_USER
             record.isGuest -> UserActionModel.ENTER_GUEST_MODE
             record.isManageUsers -> UserActionModel.NAVIGATE_TO_USER_MANAGEMENT
+            record.isSignOut -> UserActionModel.SIGN_OUT
             else -> error("Not a known action: $record")
         }
     }
@@ -105,15 +107,14 @@
     private fun getEnforcedAdmin(
         context: Context,
         selectedUserId: Int,
-        userRestrictionChecker: UserRestrictionChecker
+        userRestrictionChecker: UserRestrictionChecker,
     ): EnforcedAdmin? {
         val admin =
             userRestrictionChecker.checkIfRestrictionEnforced(
                 context,
                 UserManager.DISALLOW_ADD_USER,
                 selectedUserId,
-            )
-                ?: return null
+            ) ?: return null
 
         return if (
             !userRestrictionChecker.hasBaseUserRestriction(
@@ -145,11 +146,6 @@
         val unscaledOrNull = manager.getUserIcon(userInfo.id) ?: return null
 
         val avatarSize = context.resources.getDimensionPixelSize(R.dimen.max_avatar_size)
-        return Bitmap.createScaledBitmap(
-            unscaledOrNull,
-            avatarSize,
-            avatarSize,
-            /* filter= */ true,
-        )
+        return Bitmap.createScaledBitmap(unscaledOrNull, avatarSize, avatarSize, /* filter= */ true)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt
index 09cef1e..e7a3c23 100644
--- a/packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt
@@ -41,6 +41,7 @@
         isAddUser: Boolean,
         isGuest: Boolean,
         isAddSupervisedUser: Boolean,
+        isSignOut: Boolean,
         isTablet: Boolean = false,
         isManageUsers: Boolean,
     ): Int {
@@ -52,6 +53,8 @@
             com.android.settingslib.R.drawable.ic_account_circle
         } else if (isAddSupervisedUser) {
             com.android.settingslib.R.drawable.ic_add_supervised_user
+        } else if (isSignOut) {
+            com.android.internal.R.drawable.ic_logout
         } else if (isManageUsers) {
             R.drawable.ic_manage_users
         } else {
@@ -81,6 +84,7 @@
                         isGuestUserResetting = isGuestUserResetting,
                         isAddUser = record.isAddUser,
                         isAddSupervisedUser = record.isAddSupervisedUser,
+                        isSignOut = record.isSignOut,
                         isTablet = isTablet,
                         isManageUsers = record.isManageUsers,
                     )
@@ -111,10 +115,11 @@
         isGuestUserResetting: Boolean,
         isAddUser: Boolean,
         isAddSupervisedUser: Boolean,
+        isSignOut: Boolean,
         isTablet: Boolean = false,
         isManageUsers: Boolean,
     ): Int {
-        check(isGuest || isAddUser || isAddSupervisedUser || isManageUsers)
+        check(isGuest || isAddUser || isAddSupervisedUser || isManageUsers || isSignOut)
 
         return when {
             isGuest && isGuestUserAutoCreated && isGuestUserResetting ->
@@ -124,6 +129,7 @@
             isGuest -> com.android.internal.R.string.guest_name
             isAddUser -> com.android.settingslib.R.string.user_add_user
             isAddSupervisedUser -> R.string.add_user_supervised
+            isSignOut -> com.android.internal.R.string.global_action_logout
             isManageUsers -> R.string.manage_users
             else -> error("This should never happen!")
         }
diff --git a/packages/SystemUI/src/com/android/systemui/user/shared/model/UserActionModel.kt b/packages/SystemUI/src/com/android/systemui/user/shared/model/UserActionModel.kt
index 823bf74..7f67d76 100644
--- a/packages/SystemUI/src/com/android/systemui/user/shared/model/UserActionModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/shared/model/UserActionModel.kt
@@ -22,4 +22,5 @@
     ADD_USER,
     ADD_SUPERVISED_USER,
     NAVIGATE_TO_USER_MANAGEMENT,
+    SIGN_OUT,
 }
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
index 4089889..2e3af1c 100644
--- a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
@@ -129,9 +129,7 @@
             cancelButtonClicked || executedActionFinish || userSwitched
         }
 
-    private fun toViewModel(
-        model: UserModel,
-    ): UserViewModel {
+    private fun toViewModel(model: UserModel): UserViewModel {
         return UserViewModel(
             viewKey = model.id,
             name =
@@ -152,14 +150,13 @@
         )
     }
 
-    private fun toViewModel(
-        model: UserActionModel,
-    ): UserActionViewModel {
+    private fun toViewModel(model: UserActionModel): UserActionViewModel {
         return UserActionViewModel(
             viewKey = model.ordinal.toLong(),
             iconResourceId =
                 LegacyUserUiHelper.getUserSwitcherActionIconResourceId(
                     isAddSupervisedUser = model == UserActionModel.ADD_SUPERVISED_USER,
+                    isSignOut = model == UserActionModel.SIGN_OUT,
                     isAddUser = model == UserActionModel.ADD_USER,
                     isGuest = model == UserActionModel.ENTER_GUEST_MODE,
                     isManageUsers = model == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
@@ -171,6 +168,7 @@
                     isGuestUserAutoCreated = guestUserInteractor.isGuestUserAutoCreated,
                     isGuestUserResetting = guestUserInteractor.isGuestUserResetting,
                     isAddSupervisedUser = model == UserActionModel.ADD_SUPERVISED_USER,
+                    isSignOut = model == UserActionModel.SIGN_OUT,
                     isAddUser = model == UserActionModel.ADD_USER,
                     isManageUsers = model == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
                     isTablet = true,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt
index 60345a3..6cc8238 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt
@@ -249,10 +249,12 @@
             var factory = controllerFactory(controller)
             underTest.register(factory.cookie, factory, testScope)
             assertEquals(2, testShellTransitions.remotes.size)
+            assertTrue(testShellTransitions.remotesForTakeover.isEmpty())
 
             factory = controllerFactory(controller)
             underTest.register(factory.cookie, factory, testScope)
             assertEquals(4, testShellTransitions.remotes.size)
+            assertTrue(testShellTransitions.remotesForTakeover.isEmpty())
         }
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index 206654a..9b314f2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -1500,6 +1500,7 @@
                 mKosmos.getKeyguardInteractor(),
                 mKeyguardTransitionBootInteractor,
                 mKosmos::getCommunalSceneInteractor,
+                mKosmos::getCommunalSettingsInteractor,
                 mock(WindowManagerOcclusionManager.class));
         mViewMediator.mUserChangedCallback = mUserTrackerCallback;
         mViewMediator.start();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTestKt.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTestKt.kt
index 86f7966..d6b778f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTestKt.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTestKt.kt
@@ -35,8 +35,14 @@
 import com.android.systemui.animation.activityTransitionAnimator
 import com.android.systemui.broadcast.broadcastDispatcher
 import com.android.systemui.classifier.falsingCollector
+import com.android.systemui.common.data.repository.batteryRepository
+import com.android.systemui.common.data.repository.fake
+import com.android.systemui.communal.data.model.FEATURE_AUTO_OPEN
+import com.android.systemui.communal.data.model.SuppressionReason
 import com.android.systemui.communal.data.repository.communalSceneRepository
 import com.android.systemui.communal.domain.interactor.communalSceneInteractor
+import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
+import com.android.systemui.communal.domain.interactor.setCommunalV2Enabled
 import com.android.systemui.communal.shared.model.CommunalScenes
 import com.android.systemui.communal.ui.viewmodel.communalTransitionViewModel
 import com.android.systemui.concurrency.fakeExecutor
@@ -81,8 +87,11 @@
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.any
 import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.kotlin.anyOrNull
 import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
 
 /** Kotlin version of KeyguardViewMediatorTest to allow for coroutine testing. */
 @SmallTest
@@ -152,6 +161,7 @@
                 keyguardInteractor,
                 keyguardTransitionBootInteractor,
                 { communalSceneInteractor },
+                { communalSettingsInteractor },
                 mock<WindowManagerOcclusionManager>(),
             )
         }
@@ -164,6 +174,10 @@
     @Test
     fun doKeyguardTimeout_changesCommunalScene() =
         kosmos.runTest {
+            // Hub is enabled and hub condition is active.
+            setCommunalV2Enabled(true)
+            enableHubOnCharging()
+
             // doKeyguardTimeout message received.
             val timeoutOptions = Bundle()
             timeoutOptions.putBoolean(KeyguardViewMediator.EXTRA_TRIGGER_HUB, true)
@@ -174,4 +188,56 @@
             assertThat(communalSceneRepository.currentScene.value)
                 .isEqualTo(CommunalScenes.Communal)
         }
+
+    @Test
+    fun doKeyguardTimeout_communalNotAvailable_sleeps() =
+        kosmos.runTest {
+            // Hub disabled.
+            setCommunalV2Enabled(false)
+
+            // doKeyguardTimeout message received.
+            val timeoutOptions = Bundle()
+            timeoutOptions.putBoolean(KeyguardViewMediator.EXTRA_TRIGGER_HUB, true)
+            underTest.doKeyguardTimeout(timeoutOptions)
+            testableLooper.processAllMessages()
+
+            // Sleep is requested.
+            verify(powerManager)
+                .goToSleep(anyOrNull(), eq(PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON), eq(0))
+
+            // Hub scene is not changed.
+            assertThat(communalSceneRepository.currentScene.value).isEqualTo(CommunalScenes.Blank)
+        }
+
+    @Test
+    fun doKeyguardTimeout_hubConditionNotActive_sleeps() =
+        kosmos.runTest {
+            // Communal enabled, but hub condition set to never.
+            setCommunalV2Enabled(true)
+            disableHubShowingAutomatically()
+
+            // doKeyguardTimeout message received.
+            val timeoutOptions = Bundle()
+            timeoutOptions.putBoolean(KeyguardViewMediator.EXTRA_TRIGGER_HUB, true)
+            underTest.doKeyguardTimeout(timeoutOptions)
+            testableLooper.processAllMessages()
+
+            // Sleep is requested.
+            verify(powerManager)
+                .goToSleep(anyOrNull(), eq(PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON), eq(0))
+
+            // Hub scene is not changed.
+            assertThat(communalSceneRepository.currentScene.value).isEqualTo(CommunalScenes.Blank)
+        }
+
+    private fun Kosmos.enableHubOnCharging() {
+        communalSettingsInteractor.setSuppressionReasons(emptyList())
+        batteryRepository.fake.setDevicePluggedIn(true)
+    }
+
+    private fun Kosmos.disableHubShowingAutomatically() {
+        communalSettingsInteractor.setSuppressionReasons(
+            listOf(SuppressionReason.ReasonUnknown(FEATURE_AUTO_OPEN))
+        )
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt
index df24bff..78a4fbe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt
@@ -31,11 +31,13 @@
 import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryBackgroundViewModel
 import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryForegroundViewModel
 import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryIconViewModel
+import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.res.R
 import com.android.systemui.shade.NotificationPanelView
 import com.android.systemui.statusbar.VibratorHelper
+import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -68,6 +70,7 @@
         underTest =
             DefaultDeviceEntrySection(
                 TestScope().backgroundScope,
+                testKosmos().testDispatcher,
                 authController,
                 windowManager,
                 context,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
index 8a4f1ad..5596cc7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
@@ -38,8 +38,6 @@
 import com.android.systemui.settings.FakeUserTracker
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.util.concurrency.FakeExecutor
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.mockito.withArgCaptor
@@ -52,11 +50,14 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
+import org.mockito.Mockito.anyList
 import org.mockito.Mockito.never
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyNoMoreInteractions
 import org.mockito.MockitoAnnotations.initMocks
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
 
 /** atest SystemUITests:NoteTaskInitializerTest */
 @OptIn(InternalNoteTaskApi::class)
@@ -180,6 +181,18 @@
 
     @Test
     @EnableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
+    fun initialize_keyGestureTypeOpenNotes_isRegistered() {
+        val underTest = createUnderTest(isEnabled = true, bubbles = bubbles)
+        underTest.initialize()
+        verify(inputManager)
+            .registerKeyGestureEventHandler(
+                eq(listOf(KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES)),
+                any(),
+            )
+    }
+
+    @Test
+    @EnableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
     fun handlesShortcut_keyGestureTypeOpenNotes() {
         val gestureEvent =
             KeyGestureEvent.Builder()
@@ -189,12 +202,12 @@
         val underTest = createUnderTest(isEnabled = true, bubbles = bubbles)
         underTest.initialize()
         val callback = withArgCaptor {
-            verify(inputManager).registerKeyGestureEventHandler(capture())
+            verify(inputManager).registerKeyGestureEventHandler(anyList(), capture())
         }
 
-        assertThat(callback.handleKeyGestureEvent(gestureEvent, null)).isTrue()
-
+        callback.handleKeyGestureEvent(gestureEvent, null)
         executor.runAllReady()
+
         verify(controller).showNoteTask(eq(KEYBOARD_SHORTCUT))
     }
 
@@ -203,19 +216,19 @@
     fun handlesShortcut_stylusTailButton() {
         val gestureEvent =
             KeyGestureEvent.Builder()
-                .setKeycodes(intArrayOf(KeyEvent.KEYCODE_STYLUS_BUTTON_TAIL))
+                .setKeycodes(intArrayOf(KEYCODE_STYLUS_BUTTON_TAIL))
                 .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES)
                 .setAction(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
                 .build()
         val underTest = createUnderTest(isEnabled = true, bubbles = bubbles)
         underTest.initialize()
         val callback = withArgCaptor {
-            verify(inputManager).registerKeyGestureEventHandler(capture())
+            verify(inputManager).registerKeyGestureEventHandler(anyList(), capture())
         }
 
-        assertThat(callback.handleKeyGestureEvent(gestureEvent, null)).isTrue()
-
+        callback.handleKeyGestureEvent(gestureEvent, null)
         executor.runAllReady()
+
         verify(controller).showNoteTask(eq(TAIL_BUTTON))
     }
 
@@ -224,19 +237,19 @@
     fun ignoresUnrelatedShortcuts() {
         val gestureEvent =
             KeyGestureEvent.Builder()
-                .setKeycodes(intArrayOf(KeyEvent.KEYCODE_STYLUS_BUTTON_TAIL))
+                .setKeycodes(intArrayOf(KEYCODE_STYLUS_BUTTON_TAIL))
                 .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME)
                 .setAction(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
                 .build()
         val underTest = createUnderTest(isEnabled = true, bubbles = bubbles)
         underTest.initialize()
         val callback = withArgCaptor {
-            verify(inputManager).registerKeyGestureEventHandler(capture())
+            verify(inputManager).registerKeyGestureEventHandler(anyList(), capture())
         }
 
-        assertThat(callback.handleKeyGestureEvent(gestureEvent, null)).isFalse()
-
+        callback.handleKeyGestureEvent(gestureEvent, null)
         executor.runAllReady()
+
         verify(controller, never()).showNoteTask(any())
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
index 7e42ec7..1551375f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
@@ -37,6 +37,7 @@
 import com.android.systemui.qs.panels.shared.model.SizedTile
 import com.android.systemui.qs.panels.shared.model.SizedTileImpl
 import com.android.systemui.qs.panels.ui.compose.infinitegrid.DefaultEditTileGrid
+import com.android.systemui.qs.panels.ui.viewmodel.AvailableEditActions
 import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.qs.shared.model.TileCategory
@@ -63,7 +64,7 @@
                 columns = 4,
                 largeTilesSpan = 4,
                 modifier = Modifier.fillMaxSize(),
-                onAddTile = {},
+                onAddTile = { _, _ -> },
                 onRemoveTile = {},
                 onSetTiles = onSetTiles,
                 onResize = { _, _ -> },
@@ -84,7 +85,7 @@
         }
         composeRule.waitForIdle()
 
-        listState.onStarted(TestEditTiles[0], DragType.Add)
+        listState.onStarted(TestEditTiles[0], DragType.Move)
 
         // Tile is being dragged, it should be replaced with a placeholder
         composeRule.onNodeWithContentDescription("tileA").assertDoesNotExist()
@@ -103,6 +104,45 @@
     }
 
     @Test
+    fun nonRemovableDraggedTile_removeHeaderShouldNotExist() {
+        val nonRemovableTile = createEditTile("tileA", isRemovable = false)
+        val listState = EditTileListState(listOf(nonRemovableTile), columns = 4, largeTilesSpan = 2)
+        composeRule.setContent { EditTileGridUnderTest(listState) {} }
+        composeRule.waitForIdle()
+
+        listState.onStarted(nonRemovableTile, DragType.Move)
+
+        // Tile is being dragged, it should be replaced with a placeholder
+        composeRule.onNodeWithContentDescription("tileA").assertDoesNotExist()
+
+        // Remove drop zone should not appear
+        composeRule.onNodeWithText("Remove").assertDoesNotExist()
+    }
+
+    @Test
+    fun droppedNonRemovableDraggedTile_shouldStayInGrid() {
+        val nonRemovableTile = createEditTile("tileA", isRemovable = false)
+        val listState = EditTileListState(listOf(nonRemovableTile), columns = 4, largeTilesSpan = 2)
+        composeRule.setContent { EditTileGridUnderTest(listState) {} }
+        composeRule.waitForIdle()
+
+        listState.onStarted(nonRemovableTile, DragType.Move)
+
+        // Tile is being dragged, it should be replaced with a placeholder
+        composeRule.onNodeWithContentDescription("tileA").assertDoesNotExist()
+
+        // Remove drop zone should not appear
+        composeRule.onNodeWithText("Remove").assertDoesNotExist()
+
+        // Drop tile outside of the grid
+        listState.movedOutOfBounds()
+        listState.onDrop()
+
+        // Tile A is still in the grid
+        composeRule.assertGridContainsExactly(CURRENT_TILES_GRID_TEST_TAG, listOf("tileA"))
+    }
+
+    @Test
     fun draggedTile_shouldChangePosition() {
         var tiles by mutableStateOf(TestEditTiles)
         val listState = EditTileListState(tiles, columns = 4, largeTilesSpan = 2)
@@ -113,7 +153,11 @@
         }
         composeRule.waitForIdle()
 
-        listState.onStarted(TestEditTiles[0], DragType.Add)
+        listState.onStarted(TestEditTiles[0], DragType.Move)
+
+        // Remove drop zone should appear
+        composeRule.onNodeWithText("Remove").assertExists()
+
         listState.onTargeting(1, false)
         listState.onDrop()
 
@@ -141,7 +185,11 @@
         }
         composeRule.waitForIdle()
 
-        listState.onStarted(TestEditTiles[0], DragType.Add)
+        listState.onStarted(TestEditTiles[0], DragType.Move)
+
+        // Remove drop zone should appear
+        composeRule.onNodeWithText("Remove").assertExists()
+
         listState.movedOutOfBounds()
         listState.onDrop()
 
@@ -169,7 +217,11 @@
         }
         composeRule.waitForIdle()
 
-        listState.onStarted(createEditTile("tile_new"), DragType.Add)
+        listState.onStarted(createEditTile("tile_new", isRemovable = false), DragType.Add)
+
+        // Remove drop zone should appear
+        composeRule.onNodeWithText("Remove").assertExists()
+
         // Insert after tileD, which is at index 4
         // [ a ] [ b ] [ c ] [ empty ]
         // [ tile d ] [ e ]
@@ -193,7 +245,10 @@
         private const val CURRENT_TILES_GRID_TEST_TAG = "CurrentTilesGrid"
         private const val AVAILABLE_TILES_GRID_TEST_TAG = "AvailableTilesGrid"
 
-        private fun createEditTile(tileSpec: String): SizedTile<EditTileViewModel> {
+        private fun createEditTile(
+            tileSpec: String,
+            isRemovable: Boolean = true,
+        ): SizedTile<EditTileViewModel> {
             return SizedTileImpl(
                 EditTileViewModel(
                     tileSpec = TileSpec.create(tileSpec),
@@ -205,7 +260,8 @@
                     label = AnnotatedString(tileSpec),
                     appName = null,
                     isCurrent = true,
-                    availableEditActions = emptySet(),
+                    availableEditActions =
+                        if (isRemovable) setOf(AvailableEditActions.REMOVE) else emptySet(),
                     category = TileCategory.UNKNOWN,
                 ),
                 getWidth(tileSpec),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/EditModeTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/EditModeTest.kt
index 9d4a425..acb441c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/EditModeTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/EditModeTest.kt
@@ -23,6 +23,7 @@
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.doubleClick
 import androidx.compose.ui.test.junit4.ComposeContentTestRule
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onAllNodesWithText
@@ -30,6 +31,7 @@
 import androidx.compose.ui.test.onNodeWithContentDescription
 import androidx.compose.ui.test.onNodeWithText
 import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performTouchInput
 import androidx.compose.ui.text.AnnotatedString
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
@@ -40,6 +42,7 @@
 import com.android.systemui.qs.panels.shared.model.SizedTile
 import com.android.systemui.qs.panels.shared.model.SizedTileImpl
 import com.android.systemui.qs.panels.ui.compose.infinitegrid.DefaultEditTileGrid
+import com.android.systemui.qs.panels.ui.viewmodel.AvailableEditActions
 import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.qs.shared.model.TileCategory
@@ -53,8 +56,8 @@
     @get:Rule val composeRule = createComposeRule()
 
     @Composable
-    private fun EditTileGridUnderTest() {
-        var tiles by remember { mutableStateOf(TestEditTiles) }
+    private fun EditTileGridUnderTest(sizedTiles: List<SizedTile<EditTileViewModel>>) {
+        var tiles by remember { mutableStateOf(sizedTiles) }
         val (currentTiles, otherTiles) = tiles.partition { it.tile.isCurrent }
         val listState = EditTileListState(currentTiles, columns = 4, largeTilesSpan = 2)
 
@@ -65,7 +68,7 @@
                 columns = 4,
                 largeTilesSpan = 4,
                 modifier = Modifier.fillMaxSize(),
-                onAddTile = { tiles = tiles.add(it) },
+                onAddTile = { spec, _ -> tiles = tiles.add(spec) },
                 onRemoveTile = { tiles = tiles.remove(it) },
                 onSetTiles = {},
                 onResize = { _, _ -> },
@@ -77,7 +80,7 @@
 
     @Test
     fun clickAvailableTile_shouldAdd() {
-        composeRule.setContent { EditTileGridUnderTest() }
+        composeRule.setContent { EditTileGridUnderTest(TestEditTiles) }
         composeRule.waitForIdle()
 
         composeRule.onNodeWithContentDescription("tileF").performClick() // Tap to add
@@ -93,7 +96,7 @@
 
     @Test
     fun clickRemoveTarget_shouldRemoveSelection() {
-        composeRule.setContent { EditTileGridUnderTest() }
+        composeRule.setContent { EditTileGridUnderTest(TestEditTiles) }
         composeRule.waitForIdle()
 
         // Selects first "tileA", i.e. the one in the current grid
@@ -110,6 +113,36 @@
         )
     }
 
+    @Test
+    fun selectNonRemovableTile_removeTargetShouldHide() {
+        val nonRemovableTile = createEditTile("tileA", isRemovable = false)
+        composeRule.setContent { EditTileGridUnderTest(listOf(nonRemovableTile)) }
+        composeRule.waitForIdle()
+
+        // Selects first "tileA", i.e. the one in the current grid
+        composeRule.onAllNodesWithText("tileA").onFirst().performClick()
+
+        // Assert the remove target isn't shown
+        composeRule.onNodeWithText("Remove").assertDoesNotExist()
+    }
+
+    @Test
+    fun placementMode_shouldRepositionTile() {
+        composeRule.setContent { EditTileGridUnderTest(TestEditTiles) }
+        composeRule.waitForIdle()
+
+        // Double tap first "tileA", i.e. the one in the current grid
+        composeRule.onAllNodesWithText("tileA").onFirst().performTouchInput { doubleClick() }
+
+        // Tap on tileE to position tileA in its spot
+        composeRule.onAllNodesWithText("tileE").onFirst().performClick()
+
+        // Assert tileA moved to tileE's position
+        composeRule.assertCurrentTilesGridContainsExactly(
+            listOf("tileB", "tileC", "tileD_large", "tileE", "tileA")
+        )
+    }
+
     private fun ComposeContentTestRule.assertCurrentTilesGridContainsExactly(specs: List<String>) =
         assertGridContainsExactly(CURRENT_TILES_GRID_TEST_TAG, specs)
 
@@ -148,6 +181,7 @@
         private fun createEditTile(
             tileSpec: String,
             isCurrent: Boolean = true,
+            isRemovable: Boolean = true,
         ): SizedTile<EditTileViewModel> {
             return SizedTileImpl(
                 EditTileViewModel(
@@ -160,7 +194,8 @@
                     label = AnnotatedString(tileSpec),
                     appName = null,
                     isCurrent = isCurrent,
-                    availableEditActions = emptySet(),
+                    availableEditActions =
+                        if (isRemovable) setOf(AvailableEditActions.REMOVE) else emptySet(),
                     category = TileCategory.UNKNOWN,
                 ),
                 getWidth(tileSpec),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt
index 5e76000..274c44c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt
@@ -69,7 +69,7 @@
                 columns = 4,
                 largeTilesSpan = 4,
                 modifier = Modifier.fillMaxSize(),
-                onAddTile = {},
+                onAddTile = { _, _ -> },
                 onRemoveTile = {},
                 onSetTiles = {},
                 onResize = onResize,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayCoreStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayCoreStartableTest.kt
index 997cf41..f4d0c26 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayCoreStartableTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayCoreStartableTest.kt
@@ -18,9 +18,11 @@
 
 import android.hardware.devicestate.feature.flags.Flags.FLAG_DEVICE_STATE_RDM_V2
 import android.hardware.display.rearDisplay
+import android.os.fakeExecutorHandler
 import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
 import android.view.Display
+import android.view.accessibility.accessibilityManager
 import androidx.test.filters.SmallTest
 import com.android.keyguard.keyguardUpdateMonitor
 import com.android.systemui.SysuiTestCase
@@ -62,6 +64,8 @@
             kosmos.rearDisplayInnerDialogDelegateFactory,
             kosmos.testScope,
             kosmos.keyguardUpdateMonitor,
+            kosmos.accessibilityManager,
+            kosmos.fakeExecutorHandler,
         )
 
     @Before
@@ -69,7 +73,7 @@
         whenever(kosmos.rearDisplay.flags).thenReturn(Display.FLAG_REAR)
         whenever(kosmos.rearDisplay.displayAdjustments)
             .thenReturn(mContext.display.displayAdjustments)
-        whenever(kosmos.rearDisplayInnerDialogDelegateFactory.create(any(), any()))
+        whenever(kosmos.rearDisplayInnerDialogDelegateFactory.create(any(), any(), any()))
             .thenReturn(mockDelegate)
         whenever(mockDelegate.createDialog()).thenReturn(mockDialog)
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegateTest.kt
index fc76616..477e5ba 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegateTest.kt
@@ -17,7 +17,10 @@
 package com.android.systemui.reardisplay
 
 import android.testing.TestableLooper
+import android.view.View
+import android.widget.Button
 import android.widget.SeekBar
+import android.widget.TextView
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.haptics.msdl.msdlPlayer
@@ -28,6 +31,7 @@
 import com.android.systemui.statusbar.phone.systemUIDialogDotFactory
 import com.android.systemui.testKosmos
 import com.android.systemui.util.time.systemClock
+import com.google.common.truth.Truth.assertThat
 import junit.framework.Assert.assertFalse
 import junit.framework.Assert.assertTrue
 import org.junit.Test
@@ -49,6 +53,7 @@
             RearDisplayInnerDialogDelegate(
                 kosmos.systemUIDialogDotFactory,
                 mContext,
+                false /* touchExplorationEnabled */,
                 kosmos.vibratorHelper,
                 kosmos.msdlPlayer,
                 kosmos.systemClock,
@@ -68,6 +73,7 @@
         RearDisplayInnerDialogDelegate(
                 kosmos.systemUIDialogDotFactory,
                 mContext,
+                false /* touchExplorationEnabled */,
                 kosmos.vibratorHelper,
                 kosmos.msdlPlayer,
                 kosmos.systemClock,
@@ -78,6 +84,9 @@
             .apply {
                 show()
                 val seekbar = findViewById<SeekBar>(R.id.seekbar)
+                assertThat(seekbar.visibility).isEqualTo(View.VISIBLE)
+                assertThat(findViewById<TextView>(R.id.seekbar_instructions).visibility)
+                    .isEqualTo(View.VISIBLE)
                 seekbar.progress = 50
                 seekbar.progress = 100
                 verify(mockCallback).run()
@@ -90,6 +99,7 @@
         RearDisplayInnerDialogDelegate(
                 kosmos.systemUIDialogDotFactory,
                 mContext,
+                false /* touchExplorationEnabled */,
                 kosmos.vibratorHelper,
                 kosmos.msdlPlayer,
                 kosmos.systemClock,
@@ -118,4 +128,33 @@
         // Progress is reset
         verify(mockSeekbar).setProgress(eq(0))
     }
+
+    @Test
+    fun testTouchExplorationEnabled() {
+        val mockCallback = mock<Runnable>()
+
+        RearDisplayInnerDialogDelegate(
+                kosmos.systemUIDialogDotFactory,
+                mContext,
+                true /* touchExplorationEnabled */,
+                kosmos.vibratorHelper,
+                kosmos.msdlPlayer,
+                kosmos.systemClock,
+            ) {
+                mockCallback.run()
+            }
+            .createDialog()
+            .apply {
+                show()
+                assertThat(findViewById<SeekBar>(R.id.seekbar).visibility).isEqualTo(View.GONE)
+                assertThat(findViewById<TextView>(R.id.seekbar_instructions).visibility)
+                    .isEqualTo(View.GONE)
+
+                val cancelButton = findViewById<Button>(R.id.cancel_button)
+                assertThat(cancelButton.visibility).isEqualTo(View.VISIBLE)
+
+                cancelButton.performClick()
+                verify(mockCallback).run()
+            }
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
index 2ea4e7f..bc7ab9d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
@@ -582,7 +582,7 @@
     public void testIconScrollXAfterTranslationAndReset() throws Exception {
         ExpandableNotificationRow group = mNotificationTestHelper.createGroup();
 
-        group.setDismissUsingRowTranslationX(false);
+        group.setDismissUsingRowTranslationX(false, false);
         group.setTranslation(50);
         assertEquals(50, -group.getEntry().getIcons().getShelfIcon().getScrollX());
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index 0d99c0e..320a87e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -176,6 +176,7 @@
     private FakeKeyguardStateController mKeyguardStateController =
             spy(new FakeKeyguardStateController());
     private final FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock());
+    private final static String TEST_REASON = "reason";
 
     @Mock
     private ViewRootImpl mViewRootImpl;
@@ -272,14 +273,15 @@
         mStatusBarKeyguardViewManager.dismissWithAction(
                 action, cancelAction, false /* afterKeyguardGone */);
         verify(mPrimaryBouncerInteractor).setDismissAction(eq(action), eq(cancelAction));
-        verify(mPrimaryBouncerInteractor).show(eq(true));
+        verify(mPrimaryBouncerInteractor).show(eq(true),
+                eq("StatusBarKeyguardViewManager#dismissWithAction"));
     }
 
     @Test
     public void showPrimaryBouncer_onlyWhenShowing() {
         mStatusBarKeyguardViewManager.hide(0 /* startTime */, 0 /* fadeoutDuration */);
-        mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */);
-        verify(mPrimaryBouncerInteractor, never()).show(anyBoolean());
+        mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */, TEST_REASON);
+        verify(mPrimaryBouncerInteractor, never()).show(anyBoolean(), eq(TEST_REASON));
         verify(mDeviceEntryInteractor, never()).attemptDeviceEntry();
         verify(mSceneInteractor, never()).changeScene(any(), any());
     }
@@ -289,8 +291,8 @@
         mStatusBarKeyguardViewManager.hide(0 /* startTime */, 0 /* fadeoutDuration */);
         when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
                 KeyguardSecurityModel.SecurityMode.Password);
-        mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */);
-        verify(mPrimaryBouncerInteractor, never()).show(anyBoolean());
+        mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */, TEST_REASON);
+        verify(mPrimaryBouncerInteractor, never()).show(anyBoolean(), eq(TEST_REASON));
         verify(mDeviceEntryInteractor, never()).attemptDeviceEntry();
         verify(mSceneInteractor, never()).changeScene(any(), any());
     }
@@ -298,8 +300,8 @@
     @Test
     @DisableSceneContainer
     public void showBouncer_showsTheBouncer() {
-        mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */);
-        verify(mPrimaryBouncerInteractor).show(eq(true));
+        mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */, TEST_REASON);
+        verify(mPrimaryBouncerInteractor).show(eq(true), eq(TEST_REASON));
     }
 
     @Test
@@ -344,19 +346,20 @@
     public void onPanelExpansionChanged_showsBouncerWhenSwiping() {
         mKeyguardStateController.setCanDismissLockScreen(false);
         mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
-        verify(mPrimaryBouncerInteractor).show(eq(false));
+        verify(mPrimaryBouncerInteractor).show(eq(false),
+                eq("StatusBarKeyguardViewManager#onPanelExpansionChanged"));
 
         // But not when it's already visible
         reset(mPrimaryBouncerInteractor);
         when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true);
         mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
-        verify(mPrimaryBouncerInteractor, never()).show(eq(false));
+        verify(mPrimaryBouncerInteractor, never()).show(eq(false), eq(TEST_REASON));
 
         // Or animating away
         reset(mPrimaryBouncerInteractor);
         when(mPrimaryBouncerInteractor.isAnimatingAway()).thenReturn(true);
         mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
-        verify(mPrimaryBouncerInteractor, never()).show(eq(false));
+        verify(mPrimaryBouncerInteractor, never()).show(eq(false), eq(TEST_REASON));
     }
 
     @Test
@@ -546,7 +549,7 @@
         when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true);
 
         // WHEN showBouncer is called
-        mStatusBarKeyguardViewManager.showPrimaryBouncer(true);
+        mStatusBarKeyguardViewManager.showPrimaryBouncer(true, TEST_REASON);
 
         // THEN alt bouncer should be hidden
         verify(mAlternateBouncerInteractor).hide();
@@ -571,10 +574,10 @@
 
         // WHEN showGenericBouncer is called
         final boolean scrimmed = true;
-        mStatusBarKeyguardViewManager.showBouncer(scrimmed);
+        mStatusBarKeyguardViewManager.showBouncer(scrimmed, TEST_REASON);
 
         // THEN regular bouncer is shown
-        verify(mPrimaryBouncerInteractor).show(eq(scrimmed));
+        verify(mPrimaryBouncerInteractor).show(eq(scrimmed), eq(TEST_REASON));
     }
 
     @Test
@@ -835,7 +838,7 @@
         when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(false);
 
         // WHEN request to show primary bouncer
-        mStatusBarKeyguardViewManager.showPrimaryBouncer(true);
+        mStatusBarKeyguardViewManager.showPrimaryBouncer(true, TEST_REASON);
 
         // THEN the scrim isn't updated from StatusBarKeyguardViewManager
         verify(mCentralSurfaces, never()).updateScrimController();
@@ -847,9 +850,9 @@
     public void testShowBouncerOrKeyguard_needsFullScreen() {
         when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
                 KeyguardSecurityModel.SecurityMode.SimPin);
-        mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, false);
+        mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, false, TEST_REASON);
         verify(mCentralSurfaces).hideKeyguard();
-        verify(mPrimaryBouncerInteractor).show(true);
+        verify(mPrimaryBouncerInteractor).show(true, TEST_REASON);
     }
 
     @Test
@@ -859,7 +862,7 @@
         when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
                 KeyguardSecurityModel.SecurityMode.SimPin);
         // Returning false means unable to show the bouncer
-        when(mPrimaryBouncerInteractor.show(true)).thenReturn(false);
+        when(mPrimaryBouncerInteractor.show(true, TEST_REASON)).thenReturn(false);
         when(mKeyguardTransitionInteractor.getTransitionState().getValue().getTo())
                 .thenReturn(KeyguardState.LOCKSCREEN);
         mStatusBarKeyguardViewManager.onStartedWakingUp();
@@ -868,8 +871,8 @@
         // Advance past reattempts
         mStatusBarKeyguardViewManager.setAttemptsToShowBouncer(10);
 
-        mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, false);
-        verify(mPrimaryBouncerInteractor).show(true);
+        mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, false, TEST_REASON);
+        verify(mPrimaryBouncerInteractor).show(true, TEST_REASON);
         verify(mCentralSurfaces).showKeyguard();
     }
 
@@ -884,7 +887,7 @@
         reset(mCentralSurfaces);
         reset(mPrimaryBouncerInteractor);
         mStatusBarKeyguardViewManager.showBouncerOrKeyguard(
-                /* hideBouncerWhenShowing= */true, false);
+                /* hideBouncerWhenShowing= */true, false, TEST_REASON);
         verify(mCentralSurfaces).showKeyguard();
         verify(mPrimaryBouncerInteractor).hide();
     }
@@ -897,9 +900,9 @@
         when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
                 KeyguardSecurityModel.SecurityMode.SimPin);
         when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true);
-        mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, isFalsingReset);
+        mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, isFalsingReset, TEST_REASON);
         verify(mCentralSurfaces, never()).hideKeyguard();
-        verify(mPrimaryBouncerInteractor).show(true);
+        verify(mPrimaryBouncerInteractor).show(true, TEST_REASON);
     }
 
     @Test
@@ -909,24 +912,24 @@
         when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
                 KeyguardSecurityModel.SecurityMode.SimPin);
         when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true);
-        mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, isFalsingReset);
+        mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, isFalsingReset, TEST_REASON);
         verify(mCentralSurfaces, never()).hideKeyguard();
 
         // Do not refresh the full screen bouncer if the call is from falsing
-        verify(mPrimaryBouncerInteractor, never()).show(true);
+        verify(mPrimaryBouncerInteractor, never()).show(true, TEST_REASON);
     }
 
     @Test
     @EnableSceneContainer
     public void showBouncer_attemptDeviceEntry() {
-        mStatusBarKeyguardViewManager.showBouncer(false);
+        mStatusBarKeyguardViewManager.showBouncer(false, TEST_REASON);
         verify(mDeviceEntryInteractor).attemptDeviceEntry();
     }
 
     @Test
     @EnableSceneContainer
     public void showPrimaryBouncer() {
-        mStatusBarKeyguardViewManager.showPrimaryBouncer(false);
+        mStatusBarKeyguardViewManager.showPrimaryBouncer(false, TEST_REASON);
         verify(mSceneInteractor).showOverlay(eq(Overlays.Bouncer), anyString());
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java
index 846db63..2facc1c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java
@@ -283,6 +283,9 @@
     }
 
     public FakeBroadcastDispatcher getFakeBroadcastDispatcher() {
+        if (mSysuiDependency == null) {
+            return null;
+        }
         return mSysuiDependency.getFakeBroadcastDispatcher();
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCaseExt.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCaseExt.kt
index d3dccb0..c86ba6c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCaseExt.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCaseExt.kt
@@ -18,9 +18,22 @@
 
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testCase
+import com.android.systemui.kosmos.useStandardTestDispatcher
 
 fun SysuiTestCase.testKosmos(): Kosmos = Kosmos().apply { testCase = this@testKosmos }
 
+/**
+ * This should not be called directly. Instead, you can use:
+ * - testKosmos() to use the default dispatcher (which will soon be unconfined, see go/thetiger)
+ * - testKosmos().useStandardTestDispatcher() to explicitly choose the standard dispatcher
+ * - testKosmos().useUnconfinedTestDispatcher() to explicitly choose the unconfined dispatcher
+ *
+ * For details, see go/thetiger
+ */
+@Deprecated("Do not call this directly.  Use testKosmos() with dispatcher functions if needed.")
+fun SysuiTestCase.testKosmosLegacy(): Kosmos =
+    Kosmos().useStandardTestDispatcher().apply { testCase = this@testKosmosLegacy }
+
 /** Run [f] on the main thread and return its result once completed. */
 fun <T : Any> SysuiTestCase.runOnMainThreadAndWaitForIdleSync(f: () -> T): T {
     lateinit var result: T
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/posturing/data/repository/FakePosturingRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/posturing/data/repository/FakePosturingRepository.kt
index 47b1bf5..8a597a6 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/posturing/data/repository/FakePosturingRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/posturing/data/repository/FakePosturingRepository.kt
@@ -16,18 +16,18 @@
 
 package com.android.systemui.communal.posturing.data.repository
 
-import com.android.systemui.communal.posturing.data.model.PositionState
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.asSharedFlow
+import com.android.systemui.communal.posturing.shared.model.PosturedState
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
 
 class FakePosturingRepository : PosturingRepository {
-    private val _postured = MutableSharedFlow<PositionState>()
+    private val _postured = MutableStateFlow<PosturedState>(PosturedState.Unknown)
 
-    override val positionState: Flow<PositionState> = _postured.asSharedFlow()
+    override val posturedState: StateFlow<PosturedState> = _postured.asStateFlow()
 
-    suspend fun emitPositionState(state: PositionState) {
-        _postured.emit(state)
+    fun setPosturedState(state: PosturedState) {
+        _postured.value = state
     }
 }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractorKosmos.kt
index 792346eb..53c9c64 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractorKosmos.kt
@@ -18,27 +18,6 @@
 
 import com.android.systemui.communal.posturing.data.repository.posturingRepository
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.advanceTimeBy
-import com.android.systemui.kosmos.applicationCoroutineScope
-import com.android.systemui.kosmos.runCurrent
-import com.android.systemui.kosmos.testDispatcher
-import com.android.systemui.log.logcatLogBuffer
-import com.android.systemui.util.sensors.asyncSensorManager
-import com.android.systemui.util.time.systemClock
 
 val Kosmos.posturingInteractor by
-    Kosmos.Fixture<PosturingInteractor> {
-        PosturingInteractor(
-            repository = posturingRepository,
-            asyncSensorManager = asyncSensorManager,
-            applicationScope = applicationCoroutineScope,
-            bgDispatcher = testDispatcher,
-            logBuffer = logcatLogBuffer("PosturingInteractor"),
-            clock = systemClock,
-        )
-    }
-
-fun Kosmos.advanceTimeBySlidingWindowAndRun() {
-    advanceTimeBy(PosturingInteractor.SLIDING_WINDOW_DURATION)
-    runCurrent()
-}
+    Kosmos.Fixture<PosturingInteractor> { PosturingInteractor(repository = posturingRepository) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt
index 511bede..41dddce 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt
@@ -33,6 +33,7 @@
             transitionInteractor = keyguardTransitionInteractor,
             internalTransitionInteractor = internalKeyguardTransitionInteractor,
             scope = applicationCoroutineScope,
+            applicationScope = applicationCoroutineScope,
             bgDispatcher = testDispatcher,
             mainDispatcher = testDispatcher,
             keyguardInteractor = keyguardInteractor,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt
index ae28022..0443329 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt
@@ -25,7 +25,6 @@
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.kosmos.Kosmos.Fixture
 import kotlin.coroutines.CoroutineContext
-import kotlin.time.Duration
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.StateFlow
@@ -33,7 +32,6 @@
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
-import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runCurrent
 import org.mockito.kotlin.verify
 
@@ -74,8 +72,6 @@
 
 fun Kosmos.runCurrent() = testScope.runCurrent()
 
-fun Kosmos.advanceTimeBy(duration: Duration) = testScope.advanceTimeBy(duration)
-
 fun <T> Kosmos.collectLastValue(flow: Flow<T>) = testScope.collectLastValue(flow)
 
 fun <T> Kosmos.collectValues(flow: Flow<T>): FlowValue<List<T>> = testScope.collectValues(flow)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt
index 7a9b052..349e670 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt
@@ -46,7 +46,6 @@
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.session.shared.shadeSessionStorage
 import com.android.systemui.scene.shared.logger.sceneLogger
-import com.android.systemui.settings.displayTracker
 import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.shade.domain.interactor.shadeModeInteractor
 import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
@@ -65,7 +64,6 @@
         bouncerInteractor = bouncerInteractor,
         keyguardInteractor = keyguardInteractor,
         sysUiState = sysUiState,
-        displayId = displayTracker.defaultDisplayId,
         sceneLogger = sceneLogger,
         falsingCollector = falsingCollector,
         falsingManager = falsingManager,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt
index 165f85d..4af4e80 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt
@@ -19,7 +19,7 @@
 import android.app.PendingIntent
 import android.graphics.drawable.Icon
 import com.android.systemui.statusbar.StatusBarIconView
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
 import com.android.systemui.statusbar.notification.shared.CallType
 import com.android.systemui.statusbar.notification.stack.BUCKET_UNKNOWN
@@ -49,7 +49,7 @@
     contentIntent: PendingIntent? = null,
     bucket: Int = BUCKET_UNKNOWN,
     callType: CallType = CallType.None,
-    promotedContent: PromotedNotificationContentModel? = null,
+    promotedContent: PromotedNotificationContentModels? = null,
 ) =
     ActiveNotificationModel(
         key = key,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelKosmos.kt
index 99323db..ebe20af 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelKosmos.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.shared.notifications.domain.interactor.notificationSettingsInteractor
 import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
 import com.android.systemui.statusbar.notification.domain.interactor.seenNotificationsInteractor
+import com.android.systemui.window.domain.interactor.windowRootViewBlurInteractor
 
 val Kosmos.footerViewModel by Fixture {
     FooterViewModel(
@@ -29,6 +30,7 @@
         notificationSettingsInteractor = notificationSettingsInteractor,
         seenNotificationsInteractor = seenNotificationsInteractor,
         shadeInteractor = shadeInteractor,
+        windowRootViewBlurInteractor = windowRootViewBlurInteractor,
     )
 }
 val Kosmos.footerViewModelFactory: FooterViewModel.Factory by Fixture {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/FakePromotedNotificationContentExtractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/FakePromotedNotificationContentExtractor.kt
index 8fdf5db..aaa86aa 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/FakePromotedNotificationContentExtractor.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/FakePromotedNotificationContentExtractor.kt
@@ -17,21 +17,23 @@
 package com.android.systemui.statusbar.notification.promoted
 
 import android.app.Notification
+import com.android.systemui.statusbar.NotificationLockscreenUserManager.RedactionType
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
 import com.android.systemui.statusbar.notification.row.shared.ImageModelProvider
 import org.junit.Assert
 
 class FakePromotedNotificationContentExtractor : PromotedNotificationContentExtractor {
     @JvmField
-    val contentForEntry = mutableMapOf<NotificationEntry, PromotedNotificationContentModel?>()
+    val contentForEntry = mutableMapOf<NotificationEntry, PromotedNotificationContentModels?>()
     @JvmField val extractCalls = mutableListOf<Pair<NotificationEntry, Notification.Builder>>()
 
     override fun extractContent(
         entry: NotificationEntry,
         recoveredBuilder: Notification.Builder,
+        @RedactionType redactionType: Int,
         imageModelProvider: ImageModelProvider,
-    ): PromotedNotificationContentModel? {
+    ): PromotedNotificationContentModels? {
         extractCalls.add(entry to recoveredBuilder)
 
         if (contentForEntry.isEmpty()) {
@@ -44,7 +46,7 @@
         }
     }
 
-    fun resetForEntry(entry: NotificationEntry, content: PromotedNotificationContentModel?) {
+    fun resetForEntry(entry: NotificationEntry, content: PromotedNotificationContentModels?) {
         contentForEntry.clear()
         contentForEntry[entry] = content
         extractCalls.clear()
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt
index 2b3158d..c4542c4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt
@@ -19,6 +19,7 @@
 import android.app.Notification
 import android.content.applicationContext
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.row.RowImageInflater
 import com.android.systemui.statusbar.notification.row.shared.skeletonImageTransform
@@ -39,9 +40,10 @@
         promotedNotificationContentExtractor.extractContent(
             entry,
             Notification.Builder.recoverBuilder(applicationContext, entry.sbn.notification),
+            REDACTION_TYPE_NONE,
             RowImageInflater.newInstance(previousIndex = null, reinflating = false)
                 .useForContentModel(),
         )
-    entry.promotedNotificationContentModel =
+    entry.promotedNotificationContentModels =
         requireNotNull(extractedContent) { "extractContent returned null" }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentBuilder.kt
new file mode 100644
index 0000000..6916d560
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentBuilder.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.promoted.shared.model
+
+class PromotedNotificationContentBuilder(val key: String) {
+    private val sharedBuilder = PromotedNotificationContentModel.Builder(key)
+
+    fun applyToShared(
+        block: PromotedNotificationContentModel.Builder.() -> Unit
+    ): PromotedNotificationContentBuilder {
+        sharedBuilder.apply(block)
+        return this
+    }
+
+    fun build(): PromotedNotificationContentModels {
+        val sharedModel = sharedBuilder.build()
+        return PromotedNotificationContentModels(sharedModel, sharedModel)
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt
index 3e96fd7..e5e1a83 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt
@@ -26,7 +26,7 @@
 import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
 import com.android.systemui.statusbar.notification.data.repository.addNotif
 import com.android.systemui.statusbar.notification.data.repository.removeNotif
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
 import com.android.systemui.statusbar.notification.shared.CallType
 import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
 import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository
@@ -39,7 +39,7 @@
     intent: PendingIntent? = null,
     notificationKey: String = "test",
     appName: String = "",
-    promotedContent: PromotedNotificationContentModel? = null,
+    promotedContent: PromotedNotificationContentModels? = null,
     isAppVisible: Boolean = false,
 ) =
     OngoingCallModel.InCall(
@@ -77,7 +77,7 @@
         key: String = "notif",
         startTimeMs: Long = 1000L,
         statusBarChipIconView: StatusBarIconView? = createStatusBarIconViewOrNull(),
-        promotedContent: PromotedNotificationContentModel? = null,
+        promotedContent: PromotedNotificationContentModels? = null,
         contentIntent: PendingIntent? = null,
         uid: Int = DEFAULT_UID,
         appName: String = "Fake name",
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt
index 1504df4..6767300 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt
@@ -55,5 +55,6 @@
             uiEventLogger = uiEventLogger,
             userRestrictionChecker = userRestrictionChecker,
             processWrapper = processWrapper,
+            userLogoutInteractor = userLogoutInteractor,
         )
     }
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 69ea12d..39c1fa7 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -529,14 +529,7 @@
     }
 
     private InputManager.KeyGestureEventHandler mKeyGestureEventHandler =
-            new InputManager.KeyGestureEventHandler() {
-                @Override
-                public boolean handleKeyGestureEvent(
-                        @NonNull KeyGestureEvent event,
-                        @Nullable IBinder focusedToken) {
-                    return AccessibilityManagerService.this.handleKeyGestureEvent(event);
-                }
-            };
+            (event, focusedToken) -> AccessibilityManagerService.this.handleKeyGestureEvent(event);
 
     @VisibleForTesting
     AccessibilityManagerService(
@@ -652,7 +645,11 @@
         new AccessibilityContentObserver(mMainHandler).register(
                 mContext.getContentResolver());
         if (enableTalkbackAndMagnifierKeyGestures()) {
-            mInputManager.registerKeyGestureEventHandler(mKeyGestureEventHandler);
+            List<Integer> supportedGestures = List.of(
+                    KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION,
+                    KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK);
+            mInputManager.registerKeyGestureEventHandler(supportedGestures,
+                    mKeyGestureEventHandler);
         }
         if (com.android.settingslib.flags.Flags.hearingDevicesInputRoutingControl()) {
             if (mHearingDeviceNotificationController != null) {
@@ -701,13 +698,13 @@
     }
 
     @VisibleForTesting
-    boolean handleKeyGestureEvent(KeyGestureEvent event) {
+    void handleKeyGestureEvent(KeyGestureEvent event) {
         final boolean complete =
                 event.getAction() == KeyGestureEvent.ACTION_GESTURE_COMPLETE
                         && !event.isCancelled();
         final int gestureType = event.getKeyGestureType();
         if (!complete) {
-            return false;
+            return;
         }
 
         String targetName;
@@ -718,7 +715,7 @@
             case KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK:
                 targetName = mContext.getString(R.string.config_defaultSelectToSpeakService);
                 if (targetName.isEmpty()) {
-                    return false;
+                    return;
                 }
 
                 final ComponentName targetServiceComponent = TextUtils.isEmpty(targetName)
@@ -730,7 +727,7 @@
                             userState.getInstalledServiceInfoLocked(targetServiceComponent);
                 }
                 if (accessibilityServiceInfo == null) {
-                    return false;
+                    return;
                 }
 
                 // Skip enabling if a warning dialog is required for the feature.
@@ -740,11 +737,13 @@
                     Slog.w(LOG_TAG,
                             "Accessibility warning is required before this service can be "
                                     + "activated automatically via KEY_GESTURE shortcut.");
-                    return false;
+                    return;
                 }
                 break;
             default:
-                return false;
+                Slog.w(LOG_TAG, "Received a key gesture " + event
+                        + " that was not registered by this handler");
+                return;
         }
 
         List<String> shortcutTargets = getAccessibilityShortcutTargets(
@@ -763,14 +762,12 @@
             // this will be a separate dialog that appears that requires the user to confirm
             // which will resolve this race condition. For now, just require two presses the
             // first time it is activated.
-            return true;
+            return;
         }
 
         final int displayId = event.getDisplayId() != INVALID_DISPLAY
                 ? event.getDisplayId() : getLastNonProxyTopFocusedDisplayId();
         performAccessibilityShortcutInternal(displayId, KEY_GESTURE, targetName);
-
-        return true;
     }
 
     @Override
diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
index 0b9c45d..60343e9 100644
--- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
+++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
@@ -152,9 +152,20 @@
                     if (direction == AutoclickScrollPanel.DIRECTION_EXIT) {
                         return;
                     }
-                    // For direction buttons, perform scroll action immediately.
-                    if (hovered && direction != AutoclickScrollPanel.DIRECTION_NONE) {
-                        handleScroll(direction);
+
+                    // Handle all non-exit buttons when hovered.
+                    if (hovered) {
+                        // Clear the indicator.
+                        if (mAutoclickIndicatorScheduler != null) {
+                            mAutoclickIndicatorScheduler.cancel();
+                            if (mAutoclickIndicatorView != null) {
+                                mAutoclickIndicatorView.clearIndicator();
+                            }
+                        }
+                        // Perform scroll action.
+                        if (direction != DIRECTION_NONE) {
+                            handleScroll(direction);
+                        }
                     }
                 }
             };
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 6b3661a..a8bb523 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -186,6 +186,7 @@
 import android.media.audiopolicy.AudioProductStrategy;
 import android.media.audiopolicy.AudioVolumeGroup;
 import android.media.audiopolicy.IAudioPolicyCallback;
+import android.media.audiopolicy.IAudioVolumeChangeDispatcher;
 import android.media.permission.ClearCallingIdentityContext;
 import android.media.permission.SafeCloseable;
 import android.media.projection.IMediaProjection;
@@ -1388,6 +1389,7 @@
         mUseVolumeGroupAliases = mContext.getResources().getBoolean(
                 com.android.internal.R.bool.config_handleVolumeAliasesUsingVolumeGroups);
 
+        mAudioVolumeChangeHandler = new AudioVolumeChangeHandler(mAudioSystem);
         // Initialize volume
         // Priority 1 - Android Property
         // Priority 2 - Audio Policy Service
@@ -4452,6 +4454,27 @@
         }
     }
 
+    //================================
+    // Audio Volume Change Dispatcher
+    //================================
+    private final AudioVolumeChangeHandler mAudioVolumeChangeHandler;
+
+    /** @see AudioManager#registerVolumeGroupCallback(executor, callback) */
+    @android.annotation.EnforcePermission(
+            android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)
+    public void registerAudioVolumeCallback(IAudioVolumeChangeDispatcher callback) {
+        super.registerAudioVolumeCallback_enforcePermission();
+        mAudioVolumeChangeHandler.registerListener(callback);
+    }
+
+    /** @see AudioManager#unregisterVolumeGroupCallback(callback) */
+    @android.annotation.EnforcePermission(
+            android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)
+    public void unregisterAudioVolumeCallback(IAudioVolumeChangeDispatcher callback) {
+        super.unregisterAudioVolumeCallback_enforcePermission();
+        mAudioVolumeChangeHandler.unregisterListener(callback);
+    }
+
     @Override
     @android.annotation.EnforcePermission(anyOf = {
             MODIFY_AUDIO_SETTINGS_PRIVILEGED, MODIFY_AUDIO_ROUTING })
diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
index a6267c1..ced5fae 100644
--- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java
+++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
@@ -23,6 +23,7 @@
 import android.media.AudioMixerAttributes;
 import android.media.AudioSystem;
 import android.media.IDevicesForAttributesCallback;
+import android.media.INativeAudioVolumeGroupCallback;
 import android.media.ISoundDose;
 import android.media.ISoundDoseCallback;
 import android.media.audiopolicy.AudioMix;
@@ -758,6 +759,29 @@
     }
 
     /**
+     * Same as {@link AudioSystem#registerAudioVolumeGroupCallback(INativeAudioVolumeGroupCallback)}
+     * @param callback to register
+     * @return {@link #SUCCESS} if successfully registered.
+     *
+     * @hide
+     */
+    public int registerAudioVolumeGroupCallback(INativeAudioVolumeGroupCallback callback) {
+        return AudioSystem.registerAudioVolumeGroupCallback(callback);
+    }
+
+    /**
+     * Same as
+     * {@link AudioSystem#unregisterAudioVolumeGroupCallback(INativeAudioVolumeGroupCallback)}.
+     * @param callback to register
+     * @return {@link #SUCCESS} if successfully registered.
+     *
+     * @hide
+     */
+    public int unregisterAudioVolumeGroupCallback(INativeAudioVolumeGroupCallback callback) {
+        return AudioSystem.unregisterAudioVolumeGroupCallback(callback);
+    }
+
+    /**
      * Part of AudioService dump
      * @param pw
      */
diff --git a/services/core/java/com/android/server/audio/AudioVolumeChangeHandler.java b/services/core/java/com/android/server/audio/AudioVolumeChangeHandler.java
new file mode 100644
index 0000000..2bb4301
--- /dev/null
+++ b/services/core/java/com/android/server/audio/AudioVolumeChangeHandler.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.audio;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.media.INativeAudioVolumeGroupCallback;
+import android.media.audio.common.AudioVolumeGroupChangeEvent;
+import android.media.audiopolicy.IAudioVolumeChangeDispatcher;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+
+/**
+ * The AudioVolumeChangeHandler handles AudioVolume callbacks invoked by native
+ * {@link INativeAudioVolumeGroupCallback} callback.
+ */
+/* private package */ class AudioVolumeChangeHandler {
+    private static final String TAG = "AudioVolumeChangeHandler";
+
+    private final Object mLock = new Object();
+    @GuardedBy("mLock")
+    private final RemoteCallbackList<IAudioVolumeChangeDispatcher> mListeners =
+            new RemoteCallbackList<>();
+    private final @NonNull AudioSystemAdapter mAudioSystem;
+    private @Nullable AudioVolumeGroupCallback mAudioVolumeGroupCallback;
+
+    AudioVolumeChangeHandler(@NonNull AudioSystemAdapter asa) {
+        mAudioSystem = asa;
+    }
+
+    @GuardedBy("mLock")
+    private void lazyInitLocked() {
+        mAudioVolumeGroupCallback = new AudioVolumeGroupCallback();
+        mAudioSystem.registerAudioVolumeGroupCallback(mAudioVolumeGroupCallback);
+    }
+
+    private void sendAudioVolumeGroupChangedToClients(int groupId, int index) {
+        RemoteCallbackList<IAudioVolumeChangeDispatcher> listeners;
+        int nbDispatchers;
+        synchronized (mLock) {
+            listeners = mListeners;
+            nbDispatchers = mListeners.beginBroadcast();
+        }
+        for (int i = 0; i < nbDispatchers; i++) {
+            try {
+                listeners.getBroadcastItem(i).onAudioVolumeGroupChanged(groupId, index);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Failed to broadcast Volume Changed event");
+            }
+        }
+        synchronized (mLock) {
+            mListeners.finishBroadcast();
+        }
+    }
+
+   /**
+    * @param cb the {@link IAudioVolumeChangeDispatcher} to register
+    */
+    public void registerListener(@NonNull IAudioVolumeChangeDispatcher cb) {
+        Preconditions.checkNotNull(cb, "Volume group callback must not be null");
+        synchronized (mLock) {
+            if (mAudioVolumeGroupCallback == null) {
+                lazyInitLocked();
+            }
+            mListeners.register(cb);
+        }
+    }
+
+   /**
+    * @param cb the {@link IAudioVolumeChangeDispatcher} to unregister
+    */
+    public void unregisterListener(@NonNull IAudioVolumeChangeDispatcher cb) {
+        Preconditions.checkNotNull(cb, "Volume group callback must not be null");
+        synchronized (mLock) {
+            mListeners.unregister(cb);
+        }
+    }
+
+    private final class AudioVolumeGroupCallback extends INativeAudioVolumeGroupCallback.Stub {
+        public void onAudioVolumeGroupChanged(AudioVolumeGroupChangeEvent volumeEvent) {
+            Slog.v(TAG, "onAudioVolumeGroupChanged volumeEvent=" + volumeEvent);
+            sendAudioVolumeGroupChangedToClients(volumeEvent.groupId, volumeEvent.flags);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index a28069b..95e58e1 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -684,8 +684,9 @@
             final var backupManager = new BackupManager(mContext);
             Consumer<Pair<DisplayTopology, DisplayTopologyGraph>> topologyChangedCallback =
                     update -> {
-                        if (mInputManagerInternal != null) {
-                            mInputManagerInternal.setDisplayTopology(update.second);
+                        DisplayTopologyGraph graph = update.second;
+                        if (mInputManagerInternal != null && graph != null) {
+                            mInputManagerInternal.setDisplayTopology(graph);
                         }
                         deliverTopologyUpdate(update.first);
                     };
@@ -3647,7 +3648,7 @@
 
     private void deliverTopologyUpdate(DisplayTopology topology) {
         if (DEBUG) {
-            Slog.d(TAG, "Delivering topology update");
+            Slog.d(TAG, "Delivering topology update: " + topology);
         }
         if (Trace.isTagEnabled(Trace.TRACE_TAG_POWER)) {
             Trace.instant(Trace.TRACE_TAG_POWER, "deliverTopologyUpdate");
@@ -4209,13 +4210,18 @@
 
         public boolean mWifiDisplayScanRequested;
 
-        // A single pending event.
+        // A single pending display event.
         private record Event(int displayId, @DisplayEvent int event) { };
 
-        // The list of pending events.  This is null until there is a pending event to be saved.
-        // This is only used if {@link deferDisplayEventsWhenFrozen()} is true.
+        // The list of pending display events. This is null until there is a pending event to be
+        // saved. This is only used if {@link deferDisplayEventsWhenFrozen()} is true.
         @GuardedBy("mCallback")
-        private ArrayList<Event> mPendingEvents;
+        @Nullable
+        private ArrayList<Event> mPendingDisplayEvents;
+
+        @GuardedBy("mCallback")
+        @Nullable
+        private DisplayTopology mPendingTopology;
 
         // Process states: a process is ready to receive events if it is neither cached nor
         // frozen.
@@ -4285,7 +4291,10 @@
          */
         @GuardedBy("mCallback")
         private boolean hasPendingAndIsReadyLocked() {
-            return isReadyLocked() && mPendingEvents != null && !mPendingEvents.isEmpty() && mAlive;
+            boolean pendingDisplayEvents = mPendingDisplayEvents != null
+                    && !mPendingDisplayEvents.isEmpty();
+            boolean pendingTopology = mPendingTopology != null;
+            return isReadyLocked() && (pendingDisplayEvents || pendingTopology) && mAlive;
         }
 
         /**
@@ -4366,7 +4375,8 @@
                     // occurs as the client is transitioning to ready but pending events have not
                     // been dispatched.  The new event must be added to the pending list to
                     // preserve event ordering.
-                    if (!isReadyLocked() || (mPendingEvents != null && !mPendingEvents.isEmpty())) {
+                    if (!isReadyLocked() || (mPendingDisplayEvents != null
+                            && !mPendingDisplayEvents.isEmpty())) {
                         // The client is interested in the event but is not ready to receive it.
                         // Put the event on the pending list.
                         addDisplayEvent(displayId, event);
@@ -4453,13 +4463,13 @@
         // This is only used if {@link deferDisplayEventsWhenFrozen()} is true.
         @GuardedBy("mCallback")
         private void addDisplayEvent(int displayId, int event) {
-            if (mPendingEvents == null) {
-                mPendingEvents = new ArrayList<>();
+            if (mPendingDisplayEvents == null) {
+                mPendingDisplayEvents = new ArrayList<>();
             }
-            if (!mPendingEvents.isEmpty()) {
+            if (!mPendingDisplayEvents.isEmpty()) {
                 // Ignore redundant events. Further optimization is possible by merging adjacent
                 // events.
-                Event last = mPendingEvents.get(mPendingEvents.size() - 1);
+                Event last = mPendingDisplayEvents.get(mPendingDisplayEvents.size() - 1);
                 if (last.displayId == displayId && last.event == event) {
                     if (DEBUG) {
                         Slog.d(TAG, "Ignore redundant display event " + displayId + "/" + event
@@ -4468,12 +4478,13 @@
                     return;
                 }
             }
-            mPendingEvents.add(new Event(displayId, event));
+            mPendingDisplayEvents.add(new Event(displayId, event));
         }
 
         /**
          * @return {@code false} if RemoteException happens; otherwise {@code true} for
-         * success.
+         * success. This returns true even if the update was deferred because the remote client is
+         * cached or frozen.
          */
         boolean notifyTopologyUpdateAsync(DisplayTopology topology) {
             if ((mInternalEventFlagsMask.get()
@@ -4490,6 +4501,18 @@
                 // The client is not interested in this event, so do nothing.
                 return true;
             }
+
+            if (deferDisplayEventsWhenFrozen()) {
+                synchronized (mCallback) {
+                    // Save the new update if the client frozen or cached (not ready).
+                    if (!isReadyLocked()) {
+                        // The client is interested in the update but is not ready to receive it.
+                        mPendingTopology = topology;
+                        return true;
+                    }
+                }
+            }
+
             return transmitTopologyUpdate(topology);
         }
 
@@ -4514,37 +4537,54 @@
         // would be unusual to do so.  The method returns true on success.
         // This is only used if {@link deferDisplayEventsWhenFrozen()} is true.
         public boolean dispatchPending() {
-            Event[] pending;
+            Event[] pendingDisplayEvents = null;
+            DisplayTopology pendingTopology;
             synchronized (mCallback) {
-                if (mPendingEvents == null || mPendingEvents.isEmpty() || !mAlive) {
+                if (!mAlive) {
                     return true;
                 }
                 if (!isReadyLocked()) {
                     return false;
                 }
-                pending = new Event[mPendingEvents.size()];
-                pending = mPendingEvents.toArray(pending);
-                mPendingEvents.clear();
+
+                if (mPendingDisplayEvents != null && !mPendingDisplayEvents.isEmpty()) {
+                    pendingDisplayEvents = new Event[mPendingDisplayEvents.size()];
+                    pendingDisplayEvents = mPendingDisplayEvents.toArray(pendingDisplayEvents);
+                    mPendingDisplayEvents.clear();
+                }
+
+                pendingTopology = mPendingTopology;
+                mPendingTopology = null;
             }
             try {
-                for (int i = 0; i < pending.length; i++) {
-                    Event displayEvent = pending[i];
-                    if (DEBUG) {
-                        Slog.d(TAG, "Send pending display event #" + i + " "
-                                + displayEvent.displayId + "/"
-                                + displayEvent.event + " to " + mUid + "/" + mPid);
-                    }
+                if (pendingDisplayEvents != null) {
+                    for (int i = 0; i < pendingDisplayEvents.length; i++) {
+                        Event displayEvent = pendingDisplayEvents[i];
+                        if (DEBUG) {
+                            Slog.d(TAG, "Send pending display event #" + i + " "
+                                    + displayEvent.displayId + "/"
+                                    + displayEvent.event + " to " + mUid + "/" + mPid);
+                        }
 
-                    if (!shouldReceiveRefreshRateWithChangeUpdate(displayEvent.event)) {
-                        continue;
-                    }
+                        if (!shouldReceiveRefreshRateWithChangeUpdate(displayEvent.event)) {
+                            continue;
+                        }
 
-                    transmitDisplayEvent(displayEvent.displayId, displayEvent.event);
+                        transmitDisplayEvent(displayEvent.displayId, displayEvent.event);
+                    }
                 }
+
+                if (pendingTopology != null) {
+                    if (DEBUG) {
+                        Slog.d(TAG, "Send pending topology: " + pendingTopology
+                                + " to " + mUid + "/" + mPid);
+                    }
+                    mCallback.onTopologyChanged(pendingTopology);
+                }
+
                 return true;
             } catch (RemoteException ex) {
-                Slog.w(TAG, "Failed to notify process "
-                        + mPid + " that display topology changed, assuming it died.", ex);
+                Slog.w(TAG, "Failed to notify process " + mPid + ", assuming it died.", ex);
                 binderDied();
                 return false;
 
@@ -4556,11 +4596,12 @@
             if (deferDisplayEventsWhenFrozen()) {
                 final String fmt =
                         "mPid=%d mUid=%d mWifiDisplayScanRequested=%s"
-                        + " cached=%s frozen=%s pending=%d";
+                        + " cached=%s frozen=%s pendingDisplayEvents=%d pendingTopology=%b";
                 synchronized (mCallback) {
                     return formatSimple(fmt,
                             mPid, mUid, mWifiDisplayScanRequested, mCached, mFrozen,
-                            (mPendingEvents == null) ? 0 : mPendingEvents.size());
+                            (mPendingDisplayEvents == null) ? 0 : mPendingDisplayEvents.size(),
+                            mPendingTopology != null);
                 }
             } else {
                 final String fmt =
diff --git a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
index c37733b..2c90e19 100644
--- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
@@ -156,6 +156,8 @@
     private SparseArray<Display.Mode> mDefaultModeByDisplay;
     // a map from display id to display device config
     private SparseArray<DisplayDeviceConfig> mDisplayDeviceConfigByDisplay = new SparseArray<>();
+    // set containing connected external display ids
+    private final Set<Integer> mExternalDisplaysConnected = new HashSet<>();
 
     private SparseBooleanArray mHasArrSupport;
 
@@ -425,7 +427,7 @@
             // Some external displays physical refresh rate modes are slightly above 60hz.
             // SurfaceFlinger will not enable these display modes unless it is configured to allow
             // render rate at least at this frame rate.
-            if (mDisplayObserver.isExternalDisplayLocked(displayId)) {
+            if (isExternalDisplayLocked(displayId)) {
                 primarySummary.maxRenderFrameRate = Math.max(baseMode.getRefreshRate(),
                         primarySummary.maxRenderFrameRate);
                 appRequestSummary.maxRenderFrameRate = Math.max(baseMode.getRefreshRate(),
@@ -653,6 +655,10 @@
         }
     }
 
+    boolean isExternalDisplayLocked(int displayId) {
+        return mExternalDisplaysConnected.contains(displayId);
+    }
+
     private static String switchingTypeToString(@DisplayManager.SwitchingType int type) {
         switch (type) {
             case DisplayManager.SWITCHING_TYPE_NONE:
@@ -694,6 +700,11 @@
     }
 
     @VisibleForTesting
+    void addExternalDisplayId(int externalDisplayId) {
+        mExternalDisplaysConnected.add(externalDisplayId);
+    }
+
+    @VisibleForTesting
     void injectBrightnessObserver(BrightnessObserver brightnessObserver) {
         mBrightnessObserver = brightnessObserver;
     }
@@ -1210,7 +1221,7 @@
         @GuardedBy("mLock")
         private void updateRefreshRateSettingLocked(float minRefreshRate, float peakRefreshRate,
                 float defaultRefreshRate, int displayId) {
-            if (mDisplayObserver.isExternalDisplayLocked(displayId)) {
+            if (isExternalDisplayLocked(displayId)) {
                 if (mLoggingEnabled) {
                     Slog.d(TAG, "skip updateRefreshRateSettingLocked for external display "
                             + displayId);
@@ -1309,20 +1320,25 @@
         public void setAppRequest(int displayId, int modeId, float requestedRefreshRate,
                 float requestedMinRefreshRateRange, float requestedMaxRefreshRateRange) {
             Display.Mode requestedMode;
+            boolean isExternalDisplay;
             synchronized (mLock) {
                 requestedMode = findModeLocked(displayId, modeId, requestedRefreshRate);
+                isExternalDisplay = isExternalDisplayLocked(displayId);
             }
 
             Vote frameRateVote = getFrameRateVote(
                     requestedMinRefreshRateRange, requestedMaxRefreshRateRange);
             Vote baseModeRefreshRateVote = getBaseModeVote(requestedMode, requestedRefreshRate);
-            Vote sizeVote = getSizeVote(requestedMode);
 
             mVotesStorage.updateVote(displayId, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE,
                     frameRateVote);
             mVotesStorage.updateVote(displayId, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
                     baseModeRefreshRateVote);
-            mVotesStorage.updateVote(displayId, Vote.PRIORITY_APP_REQUEST_SIZE, sizeVote);
+
+            if (!isExternalDisplay) {
+                Vote sizeVote = getSizeVote(requestedMode);
+                mVotesStorage.updateVote(displayId, Vote.PRIORITY_APP_REQUEST_SIZE, sizeVote);
+            }
         }
 
         private Display.Mode findModeLocked(int displayId, int modeId, float requestedRefreshRate) {
@@ -1420,7 +1436,6 @@
         private int mExternalDisplayPeakHeight;
         private int mExternalDisplayPeakRefreshRate;
         private final boolean mRefreshRateSynchronizationEnabled;
-        private final Set<Integer> mExternalDisplaysConnected = new HashSet<>();
 
         DisplayObserver(Context context, Handler handler, VotesStorage votesStorage,
                 Injector injector) {
@@ -1541,10 +1556,6 @@
             }
         }
 
-        boolean isExternalDisplayLocked(int displayId) {
-            return mExternalDisplaysConnected.contains(displayId);
-        }
-
         @Nullable
         private DisplayInfo getDisplayInfo(int displayId) {
             DisplayInfo info = new DisplayInfo();
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index 41b0b4d..a2d0654 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -252,17 +252,22 @@
     static final AudioDeviceAttributes AUDIO_OUTPUT_DEVICE_HDMI_EARC =
             new AudioDeviceAttributes(AudioDeviceAttributes.ROLE_OUTPUT,
                     AudioDeviceInfo.TYPE_HDMI_EARC, "");
+    static final AudioDeviceAttributes AUDIO_OUTPUT_DEVICE_LINE_DIGITAL =
+            new AudioDeviceAttributes(AudioDeviceAttributes.ROLE_OUTPUT,
+            AudioDeviceInfo.TYPE_LINE_DIGITAL, "");
 
     // Audio output devices used for absolute volume behavior
     private static final List<AudioDeviceAttributes> AVB_AUDIO_OUTPUT_DEVICES =
             List.of(AUDIO_OUTPUT_DEVICE_HDMI,
                     AUDIO_OUTPUT_DEVICE_HDMI_ARC,
-                    AUDIO_OUTPUT_DEVICE_HDMI_EARC);
+                    AUDIO_OUTPUT_DEVICE_HDMI_EARC,
+                    AUDIO_OUTPUT_DEVICE_LINE_DIGITAL);
 
     // Audio output devices used for absolute volume behavior on TV panels
     private static final List<AudioDeviceAttributes> TV_AVB_AUDIO_OUTPUT_DEVICES =
             List.of(AUDIO_OUTPUT_DEVICE_HDMI_ARC,
-                    AUDIO_OUTPUT_DEVICE_HDMI_EARC);
+                    AUDIO_OUTPUT_DEVICE_HDMI_EARC,
+                    AUDIO_OUTPUT_DEVICE_LINE_DIGITAL);
 
     // Audio output devices used for absolute volume behavior on Playback devices
     private static final List<AudioDeviceAttributes> PLAYBACK_AVB_AUDIO_OUTPUT_DEVICES =
diff --git a/services/core/java/com/android/server/input/AppLaunchShortcutManager.java b/services/core/java/com/android/server/input/AppLaunchShortcutManager.java
index 8c028bc..eb102294 100644
--- a/services/core/java/com/android/server/input/AppLaunchShortcutManager.java
+++ b/services/core/java/com/android/server/input/AppLaunchShortcutManager.java
@@ -111,7 +111,7 @@
         mContext = context;
     }
 
-    public void systemRunning() {
+    public void init() {
         loadShortcuts();
     }
 
diff --git a/services/core/java/com/android/server/input/InputGestureManager.java b/services/core/java/com/android/server/input/InputGestureManager.java
index 67e1ccc..e6d7190 100644
--- a/services/core/java/com/android/server/input/InputGestureManager.java
+++ b/services/core/java/com/android/server/input/InputGestureManager.java
@@ -94,9 +94,9 @@
         mContext = context;
     }
 
-    public void systemRunning() {
+    public void init(List<InputGestureData> bookmarks) {
         initSystemShortcuts();
-        blockListBookmarkedTriggers();
+        blockListBookmarkedTriggers(bookmarks);
     }
 
     private void initSystemShortcuts() {
@@ -263,10 +263,9 @@
         }
     }
 
-    private void blockListBookmarkedTriggers() {
+    private void blockListBookmarkedTriggers(List<InputGestureData> bookmarks) {
         synchronized (mGestureLock) {
-            InputManager im = Objects.requireNonNull(mContext.getSystemService(InputManager.class));
-            for (InputGestureData bookmark : im.getAppLaunchBookmarks()) {
+            for (InputGestureData bookmark : bookmarks) {
                 mBlockListedTriggers.add(bookmark.getTrigger());
             }
         }
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 6e6d00d..29e04e7 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -2751,18 +2751,23 @@
     @SuppressLint("MissingPermission")
     private void initKeyGestures() {
         InputManager im = Objects.requireNonNull(mContext.getSystemService(InputManager.class));
-        im.registerKeyGestureEventHandler(new InputManager.KeyGestureEventHandler() {
-            @Override
-            public boolean handleKeyGestureEvent(@NonNull KeyGestureEvent event,
-                    @Nullable IBinder focussedToken) {
-                return InputManagerService.this.handleKeyGestureEvent(event);
-            }
-        });
+        List<Integer> supportedGestures = List.of(
+                KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_UP,
+                KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_DOWN,
+                KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_TOGGLE,
+                KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK,
+                KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_BOUNCE_KEYS,
+                KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MOUSE_KEYS,
+                KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_STICKY_KEYS,
+                KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_SLOW_KEYS
+        );
+        im.registerKeyGestureEventHandler(supportedGestures,
+                (event, focusedToken) -> InputManagerService.this.handleKeyGestureEvent(event));
     }
 
     @SuppressLint("MissingPermission")
     @VisibleForTesting
-    boolean handleKeyGestureEvent(@NonNull KeyGestureEvent event) {
+    void handleKeyGestureEvent(@NonNull KeyGestureEvent event) {
         int deviceId = event.getDeviceId();
         boolean complete = event.getAction() == KeyGestureEvent.ACTION_GESTURE_COMPLETE
                 && !event.isCancelled();
@@ -2771,20 +2776,20 @@
                 if (complete) {
                     mKeyboardBacklightController.incrementKeyboardBacklight(deviceId);
                 }
-                return true;
+                break;
             case KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_DOWN:
                 if (complete) {
                     mKeyboardBacklightController.decrementKeyboardBacklight(deviceId);
                 }
-                return true;
+                break;
             case KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_TOGGLE:
                 // TODO(b/367748270): Add functionality to turn keyboard backlight on/off.
-                return true;
+                break;
             case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK:
                 if (complete) {
                     mNative.toggleCapsLock(deviceId);
                 }
-                return true;
+                break;
             case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_BOUNCE_KEYS:
                 if (complete) {
                     final boolean bounceKeysEnabled =
@@ -2792,7 +2797,6 @@
                     InputSettings.setAccessibilityBounceKeysThreshold(mContext,
                             bounceKeysEnabled ? 0
                                     : InputSettings.DEFAULT_BOUNCE_KEYS_THRESHOLD_MILLIS);
-                    return true;
                 }
                 break;
             case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MOUSE_KEYS:
@@ -2800,7 +2804,6 @@
                     final boolean mouseKeysEnabled = InputSettings.isAccessibilityMouseKeysEnabled(
                             mContext);
                     InputSettings.setAccessibilityMouseKeysEnabled(mContext, !mouseKeysEnabled);
-                    return true;
                 }
                 break;
             case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_STICKY_KEYS:
@@ -2808,7 +2811,6 @@
                     final boolean stickyKeysEnabled =
                             InputSettings.isAccessibilityStickyKeysEnabled(mContext);
                     InputSettings.setAccessibilityStickyKeysEnabled(mContext, !stickyKeysEnabled);
-                    return true;
                 }
                 break;
             case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_SLOW_KEYS:
@@ -2817,14 +2819,13 @@
                             InputSettings.isAccessibilitySlowKeysEnabled(mContext);
                     InputSettings.setAccessibilitySlowKeysThreshold(mContext,
                             slowKeysEnabled ? 0 : InputSettings.DEFAULT_SLOW_KEYS_THRESHOLD_MILLIS);
-                    return true;
                 }
                 break;
             default:
-                return false;
-
+                Log.w(TAG, "Received a key gesture " + event
+                        + " that was not registered by this handler");
+                break;
         }
-        return false;
     }
 
     // Native callback.
@@ -3147,11 +3148,14 @@
 
     @Override
     @PermissionManuallyEnforced
-    public void registerKeyGestureHandler(@NonNull IKeyGestureHandler handler) {
+    public void registerKeyGestureHandler(int[] keyGesturesToHandle,
+            @NonNull IKeyGestureHandler handler) {
         enforceManageKeyGesturePermission();
 
         Objects.requireNonNull(handler);
-        mKeyGestureController.registerKeyGestureHandler(handler, Binder.getCallingPid());
+        Objects.requireNonNull(keyGesturesToHandle);
+        mKeyGestureController.registerKeyGestureHandler(keyGesturesToHandle, handler,
+                Binder.getCallingPid());
     }
 
     @Override
diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java
index 395c773..5de432e 100644
--- a/services/core/java/com/android/server/input/KeyGestureController.java
+++ b/services/core/java/com/android/server/input/KeyGestureController.java
@@ -58,6 +58,7 @@
 import android.util.Log;
 import android.util.Slog;
 import android.util.SparseArray;
+import android.util.SparseIntArray;
 import android.view.Display;
 import android.view.InputDevice;
 import android.view.KeyCharacterMap;
@@ -79,11 +80,11 @@
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.util.ArrayDeque;
+import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
-import java.util.TreeMap;
 
 /**
  * A thread-safe component of {@link InputManagerService} responsible for managing callbacks when a
@@ -166,11 +167,14 @@
     private final SparseArray<KeyGestureEventListenerRecord>
             mKeyGestureEventListenerRecords = new SparseArray<>();
 
-    // List of currently registered key gesture event handler keyed by process pid. The map sorts
-    // in the order of preference of the handlers, and we prioritize handlers in system server
-    // over external handlers..
+    // Map of currently registered key gesture event handlers keyed by pid.
     @GuardedBy("mKeyGestureHandlerRecords")
-    private final TreeMap<Integer, KeyGestureHandlerRecord> mKeyGestureHandlerRecords;
+    private final SparseArray<KeyGestureHandlerRecord> mKeyGestureHandlerRecords =
+            new SparseArray<>();
+
+    // Currently supported key gestures mapped to pid that registered the corresponding handler.
+    @GuardedBy("mKeyGestureHandlerRecords")
+    private final SparseIntArray mSupportedKeyGestureToPidMap = new SparseIntArray();
 
     private final ArrayDeque<KeyGestureEvent> mLastHandledEvents = new ArrayDeque<>();
 
@@ -193,18 +197,6 @@
         mHandler = new Handler(looper, this::handleMessage);
         mIoHandler = new Handler(ioLooper, this::handleIoMessage);
         mSystemPid = Process.myPid();
-        mKeyGestureHandlerRecords = new TreeMap<>((p1, p2) -> {
-            if (Objects.equals(p1, p2)) {
-                return 0;
-            }
-            if (p1 == mSystemPid) {
-                return -1;
-            } else if (p2 == mSystemPid) {
-                return 1;
-            } else {
-                return Integer.compare(p1, p2);
-            }
-        });
         mKeyCombinationManager = new KeyCombinationManager(mHandler);
         mSettingsObserver = new SettingsObserver(mHandler);
         mAppLaunchShortcutManager = new AppLaunchShortcutManager(mContext);
@@ -450,8 +442,8 @@
 
     public void systemRunning() {
         mSettingsObserver.observe();
-        mAppLaunchShortcutManager.systemRunning();
-        mInputGestureManager.systemRunning();
+        mAppLaunchShortcutManager.init();
+        mInputGestureManager.init(mAppLaunchShortcutManager.getBookmarks());
         initKeyGestures();
 
         int userId;
@@ -465,22 +457,24 @@
     @SuppressLint("MissingPermission")
     private void initKeyGestures() {
         InputManager im = Objects.requireNonNull(mContext.getSystemService(InputManager.class));
-        im.registerKeyGestureEventHandler((event, focusedToken) -> {
-            switch (event.getKeyGestureType()) {
-                case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD:
-                    if (event.getAction() == KeyGestureEvent.ACTION_GESTURE_START) {
-                        mHandler.removeMessages(MSG_ACCESSIBILITY_SHORTCUT);
-                        mHandler.sendMessageDelayed(
-                                mHandler.obtainMessage(MSG_ACCESSIBILITY_SHORTCUT),
-                                getAccessibilityShortcutTimeout());
+        im.registerKeyGestureEventHandler(
+                List.of(KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD),
+                (event, focusedToken) -> {
+                    if (event.getKeyGestureType()
+                            == KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD) {
+                        if (event.getAction() == KeyGestureEvent.ACTION_GESTURE_START) {
+                            mHandler.removeMessages(MSG_ACCESSIBILITY_SHORTCUT);
+                            mHandler.sendMessageDelayed(
+                                    mHandler.obtainMessage(MSG_ACCESSIBILITY_SHORTCUT),
+                                    getAccessibilityShortcutTimeout());
+                        } else {
+                            mHandler.removeMessages(MSG_ACCESSIBILITY_SHORTCUT);
+                        }
                     } else {
-                        mHandler.removeMessages(MSG_ACCESSIBILITY_SHORTCUT);
+                        Log.w(TAG, "Received a key gesture " + event
+                                + " that was not registered by this handler");
                     }
-                    return true;
-                default:
-                    return false;
-            }
-        });
+                });
     }
 
     public boolean interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
@@ -590,10 +584,11 @@
             return true;
         }
         if (result.appLaunchData() != null) {
-            return handleKeyGesture(deviceId, new int[]{keyCode}, metaState,
+            handleKeyGesture(deviceId, new int[]{keyCode}, metaState,
                     KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION,
-                    KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId,
-                    focusedToken, /* flags = */0, result.appLaunchData());
+                    KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, focusedToken, /* flags = */
+                    0, result.appLaunchData());
+            return true;
         }
 
         // Handle system shortcuts
@@ -601,11 +596,11 @@
             InputGestureData systemShortcut = mInputGestureManager.getSystemShortcutForKeyEvent(
                     event);
             if (systemShortcut != null) {
-                return handleKeyGesture(deviceId, new int[]{keyCode}, metaState,
+                handleKeyGesture(deviceId, new int[]{keyCode}, metaState,
                         systemShortcut.getAction().keyGestureType(),
-                        KeyGestureEvent.ACTION_GESTURE_COMPLETE,
-                        displayId, focusedToken, /* flags = */0,
-                        systemShortcut.getAction().appLaunchData());
+                        KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId,
+                        focusedToken, /* flags = */0, systemShortcut.getAction().appLaunchData());
+                return true;
             }
         }
 
@@ -687,11 +682,11 @@
                 return true;
             case KeyEvent.KEYCODE_SEARCH:
                 if (firstDown && mSearchKeyBehavior == SEARCH_KEY_BEHAVIOR_TARGET_ACTIVITY) {
-                    return handleKeyGesture(deviceId, new int[]{keyCode}, /* modifierState = */0,
+                    handleKeyGesture(deviceId, new int[]{keyCode}, /* modifierState = */0,
                             KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH,
                             KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId,
                             focusedToken, /* flags = */0, /* appLaunchData = */null);
-
+                    return true;
                 }
                 break;
             case KeyEvent.KEYCODE_SETTINGS:
@@ -782,11 +777,12 @@
                         if (KeyEvent.metaStateHasModifiers(
                                 shiftlessModifiers, KeyEvent.META_ALT_ON)) {
                             mPendingHideRecentSwitcher = true;
-                            return handleKeyGesture(deviceId, new int[]{keyCode},
+                            handleKeyGesture(deviceId, new int[]{keyCode},
                                     KeyEvent.META_ALT_ON,
                                     KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER,
                                     KeyGestureEvent.ACTION_GESTURE_START, displayId,
                                     focusedToken, /* flags = */0, /* appLaunchData = */null);
+                            return true;
                         }
                     }
                 }
@@ -803,21 +799,23 @@
                 } else {
                     if (mPendingHideRecentSwitcher) {
                         mPendingHideRecentSwitcher = false;
-                        return handleKeyGesture(deviceId, new int[]{KeyEvent.KEYCODE_TAB},
+                        handleKeyGesture(deviceId, new int[]{KeyEvent.KEYCODE_TAB},
                                 KeyEvent.META_ALT_ON,
                                 KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER,
                                 KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId,
                                 focusedToken, /* flags = */0, /* appLaunchData = */null);
+                        return true;
                     }
 
                     // Toggle Caps Lock on META-ALT.
                     if (mPendingCapsLockToggle) {
                         mPendingCapsLockToggle = false;
-                        return handleKeyGesture(deviceId, new int[]{KeyEvent.KEYCODE_META_LEFT,
+                        handleKeyGesture(deviceId, new int[]{KeyEvent.KEYCODE_META_LEFT,
                                         KeyEvent.KEYCODE_ALT_LEFT}, /* modifierState = */0,
                                 KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK,
                                 KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId,
                                 focusedToken, /* flags = */0, /* appLaunchData = */null);
+                        return true;
                     }
                 }
                 break;
@@ -885,11 +883,11 @@
             if (customGesture == null) {
                 return false;
             }
-            return handleKeyGesture(deviceId, new int[]{keyCode}, metaState,
+            handleKeyGesture(deviceId, new int[]{keyCode}, metaState,
                     customGesture.getAction().keyGestureType(),
-                    KeyGestureEvent.ACTION_GESTURE_COMPLETE,
-                    displayId, focusedToken, /* flags = */0,
-                    customGesture.getAction().appLaunchData());
+                    KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, focusedToken,
+                    /* flags = */0, customGesture.getAction().appLaunchData());
+            return true;
         }
         return false;
     }
@@ -908,7 +906,7 @@
                     // Handle keyboard layout switching. (CTRL + SPACE)
                     if (KeyEvent.metaStateHasModifiers(metaState & ~KeyEvent.META_SHIFT_MASK,
                             KeyEvent.META_CTRL_ON)) {
-                        return handleKeyGesture(deviceId, new int[]{keyCode},
+                        handleKeyGesture(deviceId, new int[]{keyCode},
                                 KeyEvent.META_CTRL_ON | (event.isShiftPressed()
                                         ? KeyEvent.META_SHIFT_ON : 0),
                                 KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH,
@@ -921,7 +919,7 @@
                 if (down && KeyEvent.metaStateHasModifiers(metaState,
                         KeyEvent.META_CTRL_ON | KeyEvent.META_ALT_ON)) {
                     // Intercept the Accessibility keychord (CTRL + ALT + Z) for keyboard users.
-                    return handleKeyGesture(deviceId, new int[]{keyCode},
+                    handleKeyGesture(deviceId, new int[]{keyCode},
                             KeyEvent.META_CTRL_ON | KeyEvent.META_ALT_ON,
                             KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT,
                             KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId,
@@ -930,7 +928,7 @@
                 break;
             case KeyEvent.KEYCODE_SYSRQ:
                 if (down && repeatCount == 0) {
-                    return handleKeyGesture(deviceId, new int[]{keyCode}, /* modifierState = */0,
+                    handleKeyGesture(deviceId, new int[]{keyCode}, /* modifierState = */0,
                             KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT,
                             KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId,
                             focusedToken, /* flags = */0, /* appLaunchData = */null);
@@ -938,7 +936,7 @@
                 break;
             case KeyEvent.KEYCODE_ESCAPE:
                 if (down && KeyEvent.metaStateHasNoModifiers(metaState) && repeatCount == 0) {
-                    return handleKeyGesture(deviceId, new int[]{keyCode}, /* modifierState = */0,
+                    handleKeyGesture(deviceId, new int[]{keyCode}, /* modifierState = */0,
                             KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS,
                             KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId,
                             focusedToken, /* flags = */0, /* appLaunchData = */null);
@@ -964,29 +962,31 @@
     }
 
     @VisibleForTesting
-    boolean handleKeyGesture(int deviceId, int[] keycodes, int modifierState,
+    void handleKeyGesture(int deviceId, int[] keycodes, int modifierState,
             @KeyGestureEvent.KeyGestureType int gestureType, int action, int displayId,
             @Nullable IBinder focusedToken, int flags, @Nullable AppLaunchData appLaunchData) {
-        return handleKeyGesture(createKeyGestureEvent(deviceId, keycodes,
-                modifierState, gestureType, action, displayId, flags, appLaunchData), focusedToken);
+        handleKeyGesture(
+                createKeyGestureEvent(deviceId, keycodes, modifierState, gestureType, action,
+                        displayId, flags, appLaunchData), focusedToken);
     }
 
-    private boolean handleKeyGesture(AidlKeyGestureEvent event, @Nullable IBinder focusedToken) {
+    private void handleKeyGesture(AidlKeyGestureEvent event, @Nullable IBinder focusedToken) {
         if (mVisibleBackgroundUsersEnabled && event.displayId != DEFAULT_DISPLAY
                 && shouldIgnoreGestureEventForVisibleBackgroundUser(event.gestureType,
                 event.displayId)) {
-            return false;
+            return;
         }
         synchronized (mKeyGestureHandlerRecords) {
-            for (KeyGestureHandlerRecord handler : mKeyGestureHandlerRecords.values()) {
-                if (handler.handleKeyGesture(event, focusedToken)) {
-                    Message msg = Message.obtain(mHandler, MSG_NOTIFY_KEY_GESTURE_EVENT, event);
-                    mHandler.sendMessage(msg);
-                    return true;
-                }
+            int index = mSupportedKeyGestureToPidMap.indexOfKey(event.gestureType);
+            if (index < 0) {
+                Log.i(TAG, "Key gesture: " + event.gestureType + " is not supported");
+                return;
             }
+            int pid = mSupportedKeyGestureToPidMap.valueAt(index);
+            mKeyGestureHandlerRecords.get(pid).handleKeyGesture(event, focusedToken);
+            Message msg = Message.obtain(mHandler, MSG_NOTIFY_KEY_GESTURE_EVENT, event);
+            mHandler.sendMessage(msg);
         }
-        return false;
     }
 
     private boolean shouldIgnoreGestureEventForVisibleBackgroundUser(
@@ -1285,12 +1285,23 @@
 
     /** Register the key gesture event handler for a process. */
     @BinderThread
-    public void registerKeyGestureHandler(IKeyGestureHandler handler, int pid) {
+    public void registerKeyGestureHandler(int[] keyGesturesToHandle, IKeyGestureHandler handler,
+            int pid) {
         synchronized (mKeyGestureHandlerRecords) {
             if (mKeyGestureHandlerRecords.get(pid) != null) {
                 throw new IllegalStateException("The calling process has already registered "
                         + "a KeyGestureHandler.");
             }
+            if (keyGesturesToHandle.length == 0) {
+                throw new IllegalArgumentException("No key gestures provided for pid = " + pid);
+            }
+            for (int gestureType : keyGesturesToHandle) {
+                if (mSupportedKeyGestureToPidMap.indexOfKey(gestureType) >= 0) {
+                    throw new IllegalArgumentException(
+                            "Key gesture " + gestureType + " is already registered by pid = "
+                                    + mSupportedKeyGestureToPidMap.get(gestureType));
+                }
+            }
             KeyGestureHandlerRecord record = new KeyGestureHandlerRecord(pid, handler);
             try {
                 handler.asBinder().linkToDeath(record, 0);
@@ -1298,6 +1309,9 @@
                 throw new RuntimeException(ex);
             }
             mKeyGestureHandlerRecords.put(pid, record);
+            for (int gestureType : keyGesturesToHandle) {
+                mSupportedKeyGestureToPidMap.put(gestureType, pid);
+            }
         }
     }
 
@@ -1315,7 +1329,7 @@
                         + "KeyGestureHandler.");
             }
             record.mKeyGestureHandler.asBinder().unlinkToDeath(record, 0);
-            mKeyGestureHandlerRecords.remove(pid);
+            onKeyGestureHandlerRemoved(pid);
         }
     }
 
@@ -1328,9 +1342,14 @@
         return mAppLaunchShortcutManager.getBookmarks();
     }
 
-    private void onKeyGestureHandlerDied(int pid) {
+    private void onKeyGestureHandlerRemoved(int pid) {
         synchronized (mKeyGestureHandlerRecords) {
             mKeyGestureHandlerRecords.remove(pid);
+            for (int i = mSupportedKeyGestureToPidMap.size() - 1; i >= 0; i--) {
+                if (mSupportedKeyGestureToPidMap.valueAt(i) == pid) {
+                    mSupportedKeyGestureToPidMap.removeAt(i);
+                }
+            }
         }
     }
 
@@ -1369,18 +1388,17 @@
             if (DEBUG) {
                 Slog.d(TAG, "Key gesture event handler for pid " + mPid + " died.");
             }
-            onKeyGestureHandlerDied(mPid);
+            onKeyGestureHandlerRemoved(mPid);
         }
 
-        public boolean handleKeyGesture(AidlKeyGestureEvent event, IBinder focusedToken) {
+        public void handleKeyGesture(AidlKeyGestureEvent event, IBinder focusedToken) {
             try {
-                return mKeyGestureHandler.handleKeyGesture(event, focusedToken);
+                mKeyGestureHandler.handleKeyGesture(event, focusedToken);
             } catch (RemoteException ex) {
                 Slog.w(TAG, "Failed to send key gesture to process " + mPid
                         + ", assuming it died.", ex);
                 binderDied();
             }
-            return false;
         }
     }
 
@@ -1479,18 +1497,21 @@
             }
         }
         ipw.println("}");
-        ipw.print("mKeyGestureHandlerRecords = {");
         synchronized (mKeyGestureHandlerRecords) {
-            int i = mKeyGestureHandlerRecords.size() - 1;
-            for (int processId : mKeyGestureHandlerRecords.keySet()) {
-                ipw.print(processId);
-                if (i > 0) {
+            ipw.print("mKeyGestureHandlerRecords = {");
+            int size = mKeyGestureHandlerRecords.size();
+            for (int i = 0; i < size; i++) {
+                int pid = mKeyGestureHandlerRecords.keyAt(i);
+                ipw.print(pid);
+                if (i < size - 1) {
                     ipw.print(", ");
                 }
-                i--;
             }
+            ipw.println("}");
+            ipw.println("mSupportedKeyGestures = " + Arrays.toString(
+                    mSupportedKeyGestureToPidMap.copyKeys()));
         }
-        ipw.println("}");
+
         ipw.decreaseIndent();
         ipw.println("Last handled KeyGestureEvents: ");
         ipw.increaseIndent();
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index fde9165..2066dbc 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -1826,8 +1826,14 @@
             @NonNull UserData userData) {
         final int userId = userData.mUserId;
         if (userData.mCurClient == client) {
-            hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow, 0 /* flags */,
-                    SoftInputShowHideReason.HIDE_REMOVE_CLIENT, userId);
+            if (Flags.refactorInsetsController()) {
+                final var statsToken = createStatsTokenForFocusedClient(false /* show */,
+                        SoftInputShowHideReason.HIDE_REMOVE_CLIENT, userId);
+                setImeVisibilityOnFocusedWindowClient(false, userData, statsToken);
+            } else {
+                hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow, 0 /* flags */,
+                        SoftInputShowHideReason.HIDE_REMOVE_CLIENT, userId);
+            }
             if (userData.mBoundToMethod) {
                 userData.mBoundToMethod = false;
                 final var userBindingController = userData.mBindingController;
@@ -2097,8 +2103,14 @@
         }
 
         if (visibilityStateComputer.getImePolicy().isImeHiddenByDisplayPolicy()) {
-            hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow, 0 /* flags */,
-                    SoftInputShowHideReason.HIDE_DISPLAY_IME_POLICY_HIDE, userId);
+            if (Flags.refactorInsetsController()) {
+                final var statsToken = createStatsTokenForFocusedClient(false /* show */,
+                        SoftInputShowHideReason.HIDE_DISPLAY_IME_POLICY_HIDE, userId);
+                setImeVisibilityOnFocusedWindowClient(false, userData, statsToken);
+            } else {
+                hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow, 0 /* flags */,
+                        SoftInputShowHideReason.HIDE_DISPLAY_IME_POLICY_HIDE, userId);
+            }
             return InputBindResult.NO_IME;
         }
 
@@ -3855,8 +3867,17 @@
                         Slog.w(TAG, "If you need to impersonate a foreground user/profile from"
                                 + " a background user, use EditorInfo.targetInputMethodUser with"
                                 + " INTERACT_ACROSS_USERS_FULL permission.");
-                        hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow,
-                                0 /* flags */, SoftInputShowHideReason.HIDE_INVALID_USER, userId);
+
+                        if (Flags.refactorInsetsController()) {
+                            final var statsToken = createStatsTokenForFocusedClient(
+                                    false /* show */, SoftInputShowHideReason.HIDE_INVALID_USER,
+                                    userId);
+                            setImeVisibilityOnFocusedWindowClient(false, userData, statsToken);
+                        } else {
+                            hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow,
+                                    0 /* flags */, SoftInputShowHideReason.HIDE_INVALID_USER,
+                                    userId);
+                        }
                         return InputBindResult.INVALID_USER;
                     }
 
@@ -4993,7 +5014,6 @@
                         setImeVisibilityOnFocusedWindowClient(false, userData,
                                 null /* TODO(b/353463205) check statsToken */);
                     } else {
-
                         hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow,
                                 0 /* flags */, reason, userId);
                     }
@@ -6688,8 +6708,9 @@
                     final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
                     final var userData = getUserData(userId);
                     if (Flags.refactorInsetsController()) {
-                        setImeVisibilityOnFocusedWindowClient(false, userData,
-                                null /* TODO(b329229469) initialize statsToken here? */);
+                        final var statsToken = createStatsTokenForFocusedClient(false /* show */,
+                                SoftInputShowHideReason.HIDE_RESET_SHELL_COMMAND, userId);
+                        setImeVisibilityOnFocusedWindowClient(false, userData, statsToken);
                     } else {
                         hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow,
                                 0 /* flags */,
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java
index ccb9e3e..bbf7732 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java
@@ -33,6 +33,7 @@
 import android.hardware.contexthub.Reason;
 import android.hardware.location.ContextHubTransaction;
 import android.hardware.location.IContextHubTransactionCallback;
+import android.hardware.location.NanoAppState;
 import android.os.Binder;
 import android.os.IBinder;
 import android.os.PowerManager;
@@ -48,6 +49,7 @@
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
@@ -182,8 +184,11 @@
                 long expiryMillis = RELIABLE_MESSAGE_DUPLICATE_DETECTION_TIMEOUT.toMillis();
                 if (nowMillis >= nextEntry.getValue() + expiryMillis) {
                     iterator.remove();
+                } else {
+                    // Safe to break since LinkedHashMap is insertion-ordered, so the next entry
+                    // will have a later timestamp and will not be expired.
+                    break;
                 }
-                break;
             }
 
             return mRxMessageHistoryMap.containsKey(message.getMessageSequenceNumber());
@@ -276,6 +281,7 @@
 
         int sessionId = mEndpointManager.reserveSessionId();
         EndpointInfo halEndpointInfo = ContextHubServiceUtil.convertHalEndpointInfo(destination);
+        Log.d(TAG, "openSession: sessionId=" + sessionId);
 
         synchronized (mOpenSessionLock) {
             try {
@@ -301,6 +307,7 @@
             throw new IllegalArgumentException(
                     "Unknown session ID in closeSession: id=" + sessionId);
         }
+        Log.d(TAG, "closeSession: sessionId=" + sessionId + " reason=" + reason);
         mEndpointManager.halCloseEndpointSession(
                 sessionId, ContextHubServiceUtil.toHalReason(reason));
     }
@@ -373,12 +380,43 @@
                 try {
                     mHubInterface.sendMessageToEndpoint(sessionId, halMessage);
                 } catch (RemoteException e) {
-                    Log.w(TAG, "Exception while sending message on session " + sessionId, e);
+                    Log.e(
+                            TAG,
+                            "Exception while sending message on session "
+                                    + sessionId
+                                    + ", closing session",
+                            e);
+                    notifySessionClosedToBoth(sessionId, Reason.UNSPECIFIED);
                 }
             } else {
+                IContextHubTransactionCallback wrappedCallback =
+                        new IContextHubTransactionCallback.Stub() {
+                            @Override
+                            public void onQueryResponse(int result, List<NanoAppState> appStates)
+                                    throws RemoteException {
+                                Log.w(TAG, "Unexpected onQueryResponse callback");
+                            }
+
+                            @Override
+                            public void onTransactionComplete(int result) throws RemoteException {
+                                callback.onTransactionComplete(result);
+                                if (result != ContextHubTransaction.RESULT_SUCCESS) {
+                                    Log.e(
+                                            TAG,
+                                            "Failed to send reliable message "
+                                                    + message
+                                                    + ", closing session");
+                                    notifySessionClosedToBoth(sessionId, Reason.UNSPECIFIED);
+                                }
+                            }
+                        };
                 ContextHubServiceTransaction transaction =
                         mTransactionManager.createSessionMessageTransaction(
-                                mHubInterface, sessionId, halMessage, mPackageName, callback);
+                                mHubInterface,
+                                sessionId,
+                                halMessage,
+                                mPackageName,
+                                wrappedCallback);
                 try {
                     mTransactionManager.addTransaction(transaction);
                     info.setReliableMessagePending(transaction.getMessageSequenceNumber());
@@ -445,10 +483,7 @@
                     int id = mSessionMap.keyAt(i);
                     HubEndpointInfo target = mSessionMap.get(id).getRemoteEndpointInfo();
                     if (!hasEndpointPermissions(target)) {
-                        mEndpointManager.halCloseEndpointSessionNoThrow(
-                                id, Reason.PERMISSION_DENIED);
-                        onCloseEndpointSession(id, Reason.PERMISSION_DENIED);
-                        // Resource cleanup is done in onCloseEndpointSession
+                        notifySessionClosedToBoth(id, Reason.PERMISSION_DENIED);
                     }
                 }
             }
@@ -532,8 +567,17 @@
 
     /* package */ void onMessageReceived(int sessionId, HubMessage message) {
         byte errorCode = onMessageReceivedInternal(sessionId, message);
-        if (errorCode != ErrorCode.OK && message.isResponseRequired()) {
-            sendMessageDeliveryStatus(sessionId, message.getMessageSequenceNumber(), errorCode);
+        if (errorCode != ErrorCode.OK) {
+            Log.e(TAG, "Failed to send message to endpoint: " + message + ", closing session");
+            if (message.isResponseRequired()) {
+                sendMessageDeliveryStatus(sessionId, message.getMessageSequenceNumber(), errorCode);
+            } else {
+                notifySessionClosedToBoth(
+                        sessionId,
+                        (errorCode == ErrorCode.PERMISSION_DENIED)
+                                ? Reason.PERMISSION_DENIED
+                                : Reason.UNSPECIFIED);
+            }
         }
     }
 
@@ -800,4 +844,16 @@
                         + "-0x"
                         + Long.toHexString(endpoint.getIdentifier().getEndpoint()));
     }
+
+    /**
+     * Notifies to both the HAL and the app that a session has been closed.
+     *
+     * @param sessionId The ID of the session that was closed
+     * @param halReason The HAL reason for closing the session
+     */
+    private void notifySessionClosedToBoth(int sessionId, byte halReason) {
+        Log.d(TAG, "notifySessionClosedToBoth: sessionId=" + sessionId + ", reason=" + halReason);
+        mEndpointManager.halCloseEndpointSessionNoThrow(sessionId, halReason);
+        onCloseEndpointSession(sessionId, halReason);
+    }
 }
diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
index 58cf29b..c174451 100644
--- a/services/core/java/com/android/server/media/MediaSessionService.java
+++ b/services/core/java/com/android/server/media/MediaSessionService.java
@@ -192,9 +192,15 @@
     private final Map<Integer, Set<MediaSessionRecordImpl>> mUserEngagedSessionsForFgs =
             new HashMap<>();
 
-    /* Maps uid with all media notifications associated to it */
+    /**
+     * Maps UIDs to their associated media notifications: UID -> (Notification ID ->
+     * {@link android.service.notification.StatusBarNotification}).
+     * Each UID maps to a collection of notifications, identified by their
+     * {@link android.service.notification.StatusBarNotification#getId()}.
+     */
     @GuardedBy("mLock")
-    private final Map<Integer, Set<StatusBarNotification>> mMediaNotifications = new HashMap<>();
+    private final Map<Integer, Map<String, StatusBarNotification>> mMediaNotifications =
+            new HashMap<>();
 
     // The FullUserRecord of the current users. (i.e. The foreground user that isn't a profile)
     // It's always not null after the MediaSessionService is started.
@@ -737,7 +743,8 @@
         }
         synchronized (mLock) {
             int uid = mediaSessionRecord.getUid();
-            for (StatusBarNotification sbn : mMediaNotifications.getOrDefault(uid, Set.of())) {
+            for (StatusBarNotification sbn : mMediaNotifications.getOrDefault(uid,
+                    Map.of()).values()) {
                 if (mediaSessionRecord.isLinkedToNotification(sbn.getNotification())) {
                     setFgsActiveLocked(mediaSessionRecord, sbn);
                     return;
@@ -771,7 +778,7 @@
             int uid, MediaSessionRecordImpl record) {
         synchronized (mLock) {
             for (StatusBarNotification sbn :
-                    mMediaNotifications.getOrDefault(uid, Set.of())) {
+                    mMediaNotifications.getOrDefault(uid, Map.of()).values()) {
                 if (record.isLinkedToNotification(sbn.getNotification())) {
                     return sbn;
                 }
@@ -794,7 +801,8 @@
             for (MediaSessionRecordImpl record :
                     mUserEngagedSessionsForFgs.getOrDefault(uid, Set.of())) {
                 for (StatusBarNotification sbn :
-                        mMediaNotifications.getOrDefault(uid, Set.of())) {
+                        mMediaNotifications.getOrDefault(uid, Map.of()).values()) {
+                    //
                     if (record.isLinkedToNotification(sbn.getNotification())) {
                         // A user engaged session linked with a media notification is found.
                         // We shouldn't call stop FGS in this case.
@@ -3262,8 +3270,12 @@
                 return;
             }
             synchronized (mLock) {
-                mMediaNotifications.putIfAbsent(uid, new HashSet<>());
-                mMediaNotifications.get(uid).add(sbn);
+                Map<String, StatusBarNotification> notifications = mMediaNotifications.get(uid);
+                if (notifications == null) {
+                    notifications = new HashMap<>();
+                    mMediaNotifications.put(uid, notifications);
+                }
+                notifications.put(sbn.getKey(), sbn);
                 MediaSessionRecordImpl userEngagedRecord =
                         getUserEngagedMediaSessionRecordForNotification(uid, postedNotification);
                 if (userEngagedRecord != null) {
@@ -3287,10 +3299,10 @@
                 return;
             }
             synchronized (mLock) {
-                Set<StatusBarNotification> uidMediaNotifications = mMediaNotifications.get(uid);
-                if (uidMediaNotifications != null) {
-                    uidMediaNotifications.remove(sbn);
-                    if (uidMediaNotifications.isEmpty()) {
+                Map<String, StatusBarNotification> notifications = mMediaNotifications.get(uid);
+                if (notifications != null) {
+                    notifications.remove(sbn.getKey());
+                    if (notifications.isEmpty()) {
                         mMediaNotifications.remove(uid);
                     }
                 }
diff --git a/services/core/java/com/android/server/media/quality/MediaQualityUtils.java b/services/core/java/com/android/server/media/quality/MediaQualityUtils.java
index cf8b703..05aac55 100644
--- a/services/core/java/com/android/server/media/quality/MediaQualityUtils.java
+++ b/services/core/java/com/android/server/media/quality/MediaQualityUtils.java
@@ -18,13 +18,20 @@
 
 import android.content.ContentValues;
 import android.database.Cursor;
+import android.hardware.tv.mediaquality.ColorRange;
+import android.hardware.tv.mediaquality.ColorSpace;
+import android.hardware.tv.mediaquality.ColorTemperature;
 import android.hardware.tv.mediaquality.DolbyAudioProcessing;
 import android.hardware.tv.mediaquality.DtsVirtualX;
+import android.hardware.tv.mediaquality.Gamma;
 import android.hardware.tv.mediaquality.ParameterDefaultValue;
 import android.hardware.tv.mediaquality.ParameterName;
 import android.hardware.tv.mediaquality.ParameterRange;
 import android.hardware.tv.mediaquality.PictureParameter;
+import android.hardware.tv.mediaquality.PictureQualityEventType;
+import android.hardware.tv.mediaquality.QualityLevel;
 import android.hardware.tv.mediaquality.SoundParameter;
+import android.media.quality.MediaQualityContract;
 import android.media.quality.MediaQualityContract.BaseParameters;
 import android.media.quality.MediaQualityContract.PictureQuality;
 import android.media.quality.MediaQualityContract.SoundQuality;
@@ -371,7 +378,7 @@
         }
         List<PictureParameter> pictureParams = new ArrayList<>();
         if (params.containsKey(PictureQuality.PARAMETER_BRIGHTNESS)) {
-            pictureParams.add(PictureParameter.brightness(params.getLong(
+            pictureParams.add(PictureParameter.brightness((float) params.getDouble(
                     PictureQuality.PARAMETER_BRIGHTNESS)));
             params.remove(PictureQuality.PARAMETER_BRIGHTNESS);
         }
@@ -441,28 +448,46 @@
             params.remove(PictureQuality.PARAMETER_COLOR_TUNER_BLUE_GAIN);
         }
         if (params.containsKey(PictureQuality.PARAMETER_NOISE_REDUCTION)) {
-            pictureParams.add(PictureParameter.noiseReduction(
-                    (byte) params.getInt(PictureQuality.PARAMETER_NOISE_REDUCTION)));
+            String noiseReductionString = params.getString(
+                    PictureQuality.PARAMETER_NOISE_REDUCTION);
+            if (noiseReductionString != null) {
+                byte noiseReductionByte = mapQualityLevel(noiseReductionString);
+                pictureParams.add(PictureParameter.noiseReduction(noiseReductionByte));
+            }
             params.remove(PictureQuality.PARAMETER_NOISE_REDUCTION);
         }
         if (params.containsKey(PictureQuality.PARAMETER_MPEG_NOISE_REDUCTION)) {
-            pictureParams.add(PictureParameter.mpegNoiseReduction(
-                    (byte) params.getInt(PictureQuality.PARAMETER_MPEG_NOISE_REDUCTION)));
+            String mpegNoiseReductionString = params.getString(
+                    PictureQuality.PARAMETER_MPEG_NOISE_REDUCTION);
+            if (mpegNoiseReductionString != null) {
+                byte mpegNoiseReductionByte = mapQualityLevel(mpegNoiseReductionString);
+                pictureParams.add(PictureParameter.mpegNoiseReduction(mpegNoiseReductionByte));
+            }
             params.remove(PictureQuality.PARAMETER_MPEG_NOISE_REDUCTION);
         }
         if (params.containsKey(PictureQuality.PARAMETER_FLESH_TONE)) {
-            pictureParams.add(PictureParameter.fleshTone(
-                    (byte) params.getInt(PictureQuality.PARAMETER_FLESH_TONE)));
+            String fleshToneString = params.getString(PictureQuality.PARAMETER_FLESH_TONE);
+            if (fleshToneString != null) {
+                byte fleshToneByte = mapQualityLevel(fleshToneString);
+                pictureParams.add(PictureParameter.fleshTone(fleshToneByte));
+            }
             params.remove(PictureQuality.PARAMETER_FLESH_TONE);
         }
         if (params.containsKey(PictureQuality.PARAMETER_DECONTOUR)) {
-            pictureParams.add(PictureParameter.deContour(
-                    (byte) params.getInt(PictureQuality.PARAMETER_DECONTOUR)));
+            String decontourString = params.getString(PictureQuality.PARAMETER_DECONTOUR);
+            if (decontourString != null) {
+                byte decontourByte = mapQualityLevel(decontourString);
+                pictureParams.add(PictureParameter.deContour(decontourByte));
+            }
             params.remove(PictureQuality.PARAMETER_DECONTOUR);
         }
         if (params.containsKey(PictureQuality.PARAMETER_DYNAMIC_LUMA_CONTROL)) {
-            pictureParams.add(PictureParameter.dynamicLumaControl(
-                    (byte) params.getInt(PictureQuality.PARAMETER_DYNAMIC_LUMA_CONTROL)));
+            String dynamicLunaControlString = params.getString(
+                    PictureQuality.PARAMETER_DYNAMIC_LUMA_CONTROL);
+            if (dynamicLunaControlString != null) {
+                byte dynamicLunaControlByte = mapQualityLevel(dynamicLunaControlString);
+                pictureParams.add(PictureParameter.dynamicLumaControl(dynamicLunaControlByte));
+            }
             params.remove(PictureQuality.PARAMETER_DYNAMIC_LUMA_CONTROL);
         }
         if (params.containsKey(PictureQuality.PARAMETER_FILM_MODE)) {
@@ -481,9 +506,48 @@
             params.remove(PictureQuality.PARAMETER_COLOR_TUNE);
         }
         if (params.containsKey(PictureQuality.PARAMETER_COLOR_TEMPERATURE)) {
-            pictureParams.add(PictureParameter.colorTemperature(
-                    (byte) params.getInt(
-                            PictureQuality.PARAMETER_COLOR_TEMPERATURE)));
+            String colorTemperatureString = params.getString(
+                    PictureQuality.PARAMETER_COLOR_TEMPERATURE);
+            if (colorTemperatureString != null) {
+                byte colorTemperatureByte;
+                switch (colorTemperatureString) {
+                    case MediaQualityContract.COLOR_TEMP_USER:
+                        colorTemperatureByte = ColorTemperature.USER;
+                        break;
+                    case MediaQualityContract.COLOR_TEMP_COOL:
+                        colorTemperatureByte = ColorTemperature.COOL;
+                        break;
+                    case MediaQualityContract.COLOR_TEMP_STANDARD:
+                        colorTemperatureByte = ColorTemperature.STANDARD;
+                        break;
+                    case MediaQualityContract.COLOR_TEMP_WARM:
+                        colorTemperatureByte = ColorTemperature.WARM;
+                        break;
+                    case MediaQualityContract.COLOR_TEMP_USER_HDR10PLUS:
+                        colorTemperatureByte = ColorTemperature.USER_HDR10PLUS;
+                        break;
+                    case MediaQualityContract.COLOR_TEMP_COOL_HDR10PLUS:
+                        colorTemperatureByte = ColorTemperature.COOL_HDR10PLUS;
+                        break;
+                    case MediaQualityContract.COLOR_TEMP_STANDARD_HDR10PLUS:
+                        colorTemperatureByte = ColorTemperature.STANDARD_HDR10PLUS;
+                        break;
+                    case MediaQualityContract.COLOR_TEMP_WARM_HDR10PLUS:
+                        colorTemperatureByte = ColorTemperature.WARM_HDR10PLUS;
+                        break;
+                    case MediaQualityContract.COLOR_TEMP_FMMSDR:
+                        colorTemperatureByte = ColorTemperature.FMMSDR;
+                        break;
+                    case MediaQualityContract.COLOR_TEMP_FMMHDR:
+                        colorTemperatureByte = ColorTemperature.FMMHDR;
+                        break;
+                    default:
+                        colorTemperatureByte = ColorTemperature.STANDARD;
+                        Log.e("PictureParams", "Invalid color_temp string: "
+                                + colorTemperatureString);
+                }
+                pictureParams.add(PictureParameter.colorTemperature(colorTemperatureByte));
+            }
             params.remove(PictureQuality.PARAMETER_COLOR_TEMPERATURE);
         }
         if (params.containsKey(PictureQuality.PARAMETER_GLOBAL_DIMMING)) {
@@ -517,8 +581,26 @@
             params.remove(PictureQuality.PARAMETER_COLOR_TUNER_BLUE_GAIN);
         }
         if (params.containsKey(PictureQuality.PARAMETER_LEVEL_RANGE)) {
-            pictureParams.add(PictureParameter.levelRange(
-                    (byte) params.getInt(PictureQuality.PARAMETER_LEVEL_RANGE)));
+            String levelRangeString = params.getString(PictureQuality.PARAMETER_LEVEL_RANGE);
+            if (levelRangeString != null) {
+                byte levelRangeByte;
+                switch (levelRangeString) {
+                    case "AUTO":
+                        levelRangeByte = ColorRange.AUTO;
+                        break;
+                    case "LIMITED":
+                        levelRangeByte = ColorRange.LIMITED;
+                        break;
+                    case "FULL":
+                        levelRangeByte = ColorRange.FULL;
+                        break;
+                    default:
+                        levelRangeByte = ColorRange.AUTO;
+                        Log.e("PictureParams", "Invalid color_range string: "
+                                + levelRangeString);
+                }
+                pictureParams.add(PictureParameter.levelRange(levelRangeByte));
+            }
             params.remove(PictureQuality.PARAMETER_LEVEL_RANGE);
         }
         if (params.containsKey(PictureQuality.PARAMETER_GAMUT_MAPPING)) {
@@ -547,13 +629,61 @@
             params.remove(PictureQuality.PARAMETER_CVRR);
         }
         if (params.containsKey(PictureQuality.PARAMETER_HDMI_RGB_RANGE)) {
-            pictureParams.add(PictureParameter.hdmiRgbRange(
-                    (byte) params.getInt(PictureQuality.PARAMETER_HDMI_RGB_RANGE)));
+            String hdmiRgbRangeString = params.getString(PictureQuality.PARAMETER_HDMI_RGB_RANGE);
+            if (hdmiRgbRangeString != null) {
+                byte hdmiRgbRangeByte;
+                switch (hdmiRgbRangeString) {
+                    case "AUTO":
+                        hdmiRgbRangeByte = ColorRange.AUTO;
+                        break;
+                    case "LIMITED":
+                        hdmiRgbRangeByte = ColorRange.LIMITED;
+                        break;
+                    case "FULL":
+                        hdmiRgbRangeByte = ColorRange.FULL;
+                        break;
+                    default:
+                        hdmiRgbRangeByte = ColorRange.AUTO;
+                        Log.e("PictureParams", "Invalid hdmi_rgb_range string: "
+                                + hdmiRgbRangeByte);
+                }
+                pictureParams.add(PictureParameter.hdmiRgbRange(hdmiRgbRangeByte));
+            }
             params.remove(PictureQuality.PARAMETER_HDMI_RGB_RANGE);
         }
         if (params.containsKey(PictureQuality.PARAMETER_COLOR_SPACE)) {
-            pictureParams.add(PictureParameter.colorSpace(
-                    (byte) params.getInt(PictureQuality.PARAMETER_COLOR_SPACE)));
+            String colorSpaceString = params.getString(PictureQuality.PARAMETER_COLOR_SPACE);
+            if (colorSpaceString != null) {
+                byte colorSpaceByte;
+                switch (colorSpaceString) {
+                    case "AUTO":
+                        colorSpaceByte = ColorSpace.AUTO;
+                        break;
+                    case "S_RGB_BT_709":
+                        colorSpaceByte = ColorSpace.S_RGB_BT_709;
+                        break;
+                    case "DCI":
+                        colorSpaceByte = ColorSpace.DCI;
+                        break;
+                    case "ADOBE_RGB":
+                        colorSpaceByte = ColorSpace.ADOBE_RGB;
+                        break;
+                    case "BT2020":
+                        colorSpaceByte = ColorSpace.BT2020;
+                        break;
+                    case "ON":
+                        colorSpaceByte = ColorSpace.ON;
+                        break;
+                    case "OFF":
+                        colorSpaceByte = ColorSpace.OFF;
+                        break;
+                    default:
+                        colorSpaceByte = ColorSpace.OFF;
+                        Log.e("PictureParams", "Invalid color_space string: "
+                                + colorSpaceString);
+                }
+                pictureParams.add(PictureParameter.colorSpace(colorSpaceByte));
+            }
             params.remove(PictureQuality.PARAMETER_COLOR_SPACE);
         }
         if (params.containsKey(PictureQuality.PARAMETER_PANEL_INIT_MAX_LUMINCE_NITS)) {
@@ -567,8 +697,25 @@
             params.remove(PictureQuality.PARAMETER_PANEL_INIT_MAX_LUMINCE_VALID);
         }
         if (params.containsKey(PictureQuality.PARAMETER_GAMMA)) {
-            pictureParams.add(PictureParameter.gamma(
-                    (byte) params.getInt(PictureQuality.PARAMETER_GAMMA)));
+            String gammaString = params.getString(PictureQuality.PARAMETER_GAMMA);
+            if (gammaString != null) {
+                byte gammaByte;
+                switch (gammaString) {
+                    case "DARK":
+                        gammaByte = Gamma.DARK;
+                        break;
+                    case "MIDDLE":
+                        gammaByte = Gamma.MIDDLE;
+                        break;
+                    case "BRIGHT":
+                        gammaByte = Gamma.BRIGHT;
+                        break;
+                    default:
+                        gammaByte = Gamma.DARK;
+                        Log.e("PictureParams", "Invalid gamma string: " + gammaString);
+                }
+                pictureParams.add(PictureParameter.gamma(gammaByte));
+            }
             params.remove(PictureQuality.PARAMETER_GAMMA);
         }
         if (params.containsKey(PictureQuality.PARAMETER_COLOR_TEMPERATURE_RED_OFFSET)) {
@@ -602,13 +749,19 @@
             params.remove(PictureQuality.PARAMETER_ELEVEN_POINT_BLUE);
         }
         if (params.containsKey(PictureQuality.PARAMETER_LOW_BLUE_LIGHT)) {
-            pictureParams.add(PictureParameter.lowBlueLight(
-                    (byte) params.getInt(PictureQuality.PARAMETER_LOW_BLUE_LIGHT)));
+            String lowBlueLightString = params.getString(PictureQuality.PARAMETER_LOW_BLUE_LIGHT);
+            if (lowBlueLightString != null) {
+                byte lowBlueLightByte = mapQualityLevel(lowBlueLightString);
+                pictureParams.add(PictureParameter.lowBlueLight(lowBlueLightByte));
+            }
             params.remove(PictureQuality.PARAMETER_LOW_BLUE_LIGHT);
         }
         if (params.containsKey(PictureQuality.PARAMETER_LD_MODE)) {
-            pictureParams.add(PictureParameter.LdMode(
-                    (byte) params.getInt(PictureQuality.PARAMETER_LD_MODE)));
+            String ldModeString = params.getString(PictureQuality.PARAMETER_LD_MODE);
+            if (ldModeString != null) {
+                byte ldModeByte = mapQualityLevel(ldModeString);
+                pictureParams.add(PictureParameter.LdMode(ldModeByte));
+            }
             params.remove(PictureQuality.PARAMETER_LD_MODE);
         }
         if (params.containsKey(PictureQuality.PARAMETER_OSD_RED_GAIN)) {
@@ -767,8 +920,44 @@
             params.remove(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_FLESH);
         }
         if (params.containsKey(PictureQuality.PARAMETER_PICTURE_QUALITY_EVENT_TYPE)) {
-            pictureParams.add(PictureParameter.pictureQualityEventType(
-                    (byte) params.getInt(PictureQuality.PARAMETER_PICTURE_QUALITY_EVENT_TYPE)));
+            String pictureQualityEventTypeString = params.getString(
+                    PictureQuality.PARAMETER_PICTURE_QUALITY_EVENT_TYPE);
+            if (pictureQualityEventTypeString != null) {
+                byte pictureQualityEventTypeByte;
+                switch (pictureQualityEventTypeString) {
+                    case "NONE":
+                        pictureQualityEventTypeByte = PictureQualityEventType.NONE;
+                        break;
+                    case "BBD_RESULT":
+                        pictureQualityEventTypeByte = PictureQualityEventType.BBD_RESULT;
+                        break;
+                    case "VIDEO_DELAY_CHANGE":
+                        pictureQualityEventTypeByte = PictureQualityEventType.VIDEO_DELAY_CHANGE;
+                        break;
+                    case "CAPTUREPOINT_INFO_CHANGE":
+                        pictureQualityEventTypeByte =
+                                PictureQualityEventType.CAPTUREPOINT_INFO_CHANGE;
+                        break;
+                    case "VIDEOPATH_CHANGE":
+                        pictureQualityEventTypeByte = PictureQualityEventType.VIDEOPATH_CHANGE;
+                        break;
+                    case "EXTRA_FRAME_CHANGE":
+                        pictureQualityEventTypeByte = PictureQualityEventType.EXTRA_FRAME_CHANGE;
+                        break;
+                    case "DOLBY_IQ_CHANGE":
+                        pictureQualityEventTypeByte = PictureQualityEventType.DOLBY_IQ_CHANGE;
+                        break;
+                    case "DOLBY_APO_CHANGE":
+                        pictureQualityEventTypeByte = PictureQualityEventType.DOLBY_APO_CHANGE;
+                        break;
+                    default:
+                        pictureQualityEventTypeByte = PictureQualityEventType.NONE;
+                        Log.e("PictureParams", "Invalid event type string: "
+                                + pictureQualityEventTypeString);
+                }
+                pictureParams.add(
+                        PictureParameter.pictureQualityEventType(pictureQualityEventTypeByte));
+            }
             params.remove(PictureQuality.PARAMETER_PICTURE_QUALITY_EVENT_TYPE);
         }
         return pictureParams.toArray(new PictureParameter[0]);
@@ -1657,6 +1846,19 @@
         return colIndex != -1 ? cursor.getString(colIndex) : null;
     }
 
+    private static byte mapQualityLevel(String qualityLevel) {
+        return switch (qualityLevel) {
+            case MediaQualityContract.LEVEL_OFF -> QualityLevel.OFF;
+            case MediaQualityContract.LEVEL_LOW -> QualityLevel.LOW;
+            case MediaQualityContract.LEVEL_MEDIUM -> QualityLevel.MEDIUM;
+            case MediaQualityContract.LEVEL_HIGH -> QualityLevel.HIGH;
+            default -> {
+                Log.e("PictureParams", "Invalid noise_reduction string: " + qualityLevel);
+                yield QualityLevel.OFF;
+            }
+        };
+    }
+
     private MediaQualityUtils() {
 
     }
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index acdc79f..e02ec6a 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -3077,7 +3077,8 @@
         }
 
         if (succeeded) {
-            Slog.i(TAG, "installation completed:" + packageName);
+            Slog.i(TAG, "installation completed for package:" + packageName
+                    + ". Final code path: " + pkgSetting.getPath().getPath());
 
             if (Flags.aslInApkAppMetadataSource()
                     && pkgSetting.getAppMetadataSource() == APP_METADATA_SOURCE_APK) {
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 38d4587..2744721 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -115,7 +115,6 @@
 import static com.android.server.wm.WindowManagerPolicyProto.WINDOW_MANAGER_DRAW_COMPLETE;
 
 import android.accessibilityservice.AccessibilityService;
-import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.app.ActivityManager;
@@ -268,6 +267,7 @@
 import java.io.FileReader;
 import java.io.IOException;
 import java.io.PrintWriter;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
@@ -311,6 +311,7 @@
     static final int SHORT_PRESS_POWER_LOCK_OR_SLEEP = 6;
     static final int SHORT_PRESS_POWER_DREAM_OR_SLEEP = 7;
     static final int SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP = 8;
+    static final int SHORT_PRESS_POWER_DREAM_OR_AWAKE_OR_SLEEP = 9;
 
     // must match: config_LongPressOnPowerBehavior in config.xml
     // The config value can be overridden using Settings.Global.POWER_BUTTON_LONG_PRESS
@@ -1234,8 +1235,10 @@
                     break;
                 }
                 case SHORT_PRESS_POWER_DREAM_OR_SLEEP: {
-                    attemptToDreamFromShortPowerButtonPress(
-                            true,
+                    attemptToDreamOrAwakeFromShortPowerButtonPress(
+                            /* isScreenOn */ true,
+                            /* awakeWhenDream */ false,
+                            /* noDreamAction */
                             () -> sleepDefaultDisplayFromPowerButton(eventTime, 0));
                     break;
                 }
@@ -1269,13 +1272,22 @@
                         lockNow(options);
                     } else {
                         // If the hub cannot be run, attempt to dream instead.
-                        attemptToDreamFromShortPowerButtonPress(
+                        attemptToDreamOrAwakeFromShortPowerButtonPress(
                                 /* isScreenOn */ true,
+                                /* awakeWhenDream */ false,
                                 /* noDreamAction */
                                 () -> sleepDefaultDisplayFromPowerButton(eventTime, 0));
                     }
                     break;
                 }
+                case SHORT_PRESS_POWER_DREAM_OR_AWAKE_OR_SLEEP: {
+                    attemptToDreamOrAwakeFromShortPowerButtonPress(
+                            /* isScreenOn */ true,
+                            /* awakeWhenDream */ true,
+                            /* noDreamAction */
+                            () -> sleepDefaultDisplayFromPowerButton(eventTime, 0));
+                    break;
+                }
             }
         }
     }
@@ -1319,15 +1331,18 @@
     }
 
     /**
-     * Attempt to dream from a power button press.
+     * Attempt to dream, awake or sleep from a power button press.
      *
      * @param isScreenOn Whether the screen is currently on.
+     * @param awakeWhenDream When it's set to {@code true}, awake the device from dreaming.
+     *        Otherwise, go to sleep.
      * @param noDreamAction The action to perform if dreaming is not possible.
      */
-    private void attemptToDreamFromShortPowerButtonPress(
-            boolean isScreenOn, Runnable noDreamAction) {
+    private void attemptToDreamOrAwakeFromShortPowerButtonPress(
+            boolean isScreenOn, boolean awakeWhenDream, Runnable noDreamAction) {
         if (mShortPressOnPowerBehavior != SHORT_PRESS_POWER_DREAM_OR_SLEEP
-                && mShortPressOnPowerBehavior != SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP) {
+                && mShortPressOnPowerBehavior != SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP
+                && mShortPressOnPowerBehavior != SHORT_PRESS_POWER_DREAM_OR_AWAKE_OR_SLEEP) {
             // If the power button behavior isn't one that should be able to trigger the dream, give
             // up.
             noDreamAction.run();
@@ -1335,9 +1350,24 @@
         }
 
         final DreamManagerInternal dreamManagerInternal = getDreamManagerInternal();
-        if (dreamManagerInternal == null || !dreamManagerInternal.canStartDreaming(isScreenOn)) {
-            Slog.d(TAG, "Can't start dreaming when attempting to dream from short power"
-                    + " press (isScreenOn=" + isScreenOn + ")");
+        if (dreamManagerInternal == null) {
+            Slog.d(TAG,
+                    "Can't access dream manager dreaming when attempting to start or stop dream "
+                    + "from short power press (isScreenOn="
+                            + isScreenOn + ", awakeWhenDream=" + awakeWhenDream + ")");
+            noDreamAction.run();
+            return;
+        }
+
+        if (!dreamManagerInternal.canStartDreaming(isScreenOn)) {
+            if (awakeWhenDream && dreamManagerInternal.isDreaming()) {
+                dreamManagerInternal.stopDream(false /*immediate*/, "short press power" /*reason*/);
+                return;
+            }
+            Slog.d(TAG,
+                    "Can't start dreaming and the device is not dreaming when attempting to start "
+                    + "or stop dream from short power press (isScreenOn="
+                            + isScreenOn + ", awakeWhenDream=" + awakeWhenDream + ")");
             noDreamAction.run();
             return;
         }
@@ -2312,6 +2342,10 @@
             return ActivityManager.getService();
         }
 
+        LockPatternUtils getLockPatternUtils() {
+            return new LockPatternUtils(mContext);
+        }
+
         ButtonOverridePermissionChecker getButtonOverridePermissionChecker() {
             return new ButtonOverridePermissionChecker();
         }
@@ -2360,7 +2394,7 @@
         mAccessibilityShortcutController = injector.getAccessibilityShortcutController(
                 mContext, new Handler(), mCurrentUserId);
         mGlobalActionsFactory = injector.getGlobalActionsFactory();
-        mLockPatternUtils = new LockPatternUtils(mContext);
+        mLockPatternUtils = injector.getLockPatternUtils();
         mLogger = new MetricsLogger();
 
         Resources res = mContext.getResources();
@@ -4240,19 +4274,51 @@
         if (!useKeyGestureEventHandler()) {
             return;
         }
-        mInputManager.registerKeyGestureEventHandler((event, focusedToken) -> {
-            boolean handled = PhoneWindowManager.this.handleKeyGestureEvent(event,
-                    focusedToken);
-            if (handled && !event.isCancelled() && Arrays.stream(event.getKeycodes()).anyMatch(
-                    (keycode) -> keycode == KeyEvent.KEYCODE_POWER)) {
-                mPowerKeyHandled = true;
-            }
-            return handled;
-        });
+        List<Integer> supportedGestures = new ArrayList<>(List.of(
+                KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS,
+                KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH,
+                KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT,
+                KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT,
+                KeyGestureEvent.KEY_GESTURE_TYPE_HOME,
+                KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS,
+                KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN,
+                KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL,
+                KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT,
+                KeyGestureEvent.KEY_GESTURE_TYPE_TRIGGER_BUG_REPORT,
+                KeyGestureEvent.KEY_GESTURE_TYPE_BACK,
+                KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION,
+                KeyGestureEvent.KEY_GESTURE_TYPE_DESKTOP_MODE,
+                KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT,
+                KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT,
+                KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER,
+                KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_UP,
+                KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_DOWN,
+                KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER,
+                KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS,
+                KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS,
+                KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH,
+                KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH,
+                KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT,
+                KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS,
+                KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION,
+                KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_DO_NOT_DISTURB,
+                KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD,
+                KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD,
+                KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS,
+                KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT
+        ));
+        if (enableTalkbackAndMagnifierKeyGestures()) {
+            supportedGestures.add(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_TALKBACK);
+        }
+        if (enableVoiceAccessKeyGestures()) {
+            supportedGestures.add(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS);
+        }
+        mInputManager.registerKeyGestureEventHandler(supportedGestures,
+                PhoneWindowManager.this::handleKeyGestureEvent);
     }
 
     @VisibleForTesting
-    boolean handleKeyGestureEvent(KeyGestureEvent event, IBinder focusedToken) {
+    void handleKeyGestureEvent(KeyGestureEvent event, IBinder focusedToken) {
         boolean start = event.getAction() == KeyGestureEvent.ACTION_GESTURE_START;
         boolean complete = event.getAction() == KeyGestureEvent.ACTION_GESTURE_COMPLETE
                 && !event.isCancelled();
@@ -4262,12 +4328,16 @@
         int modifierState = event.getModifierState();
         boolean keyguardOn = keyguardOn();
         boolean canLaunchApp = isUserSetupComplete() && !keyguardOn;
+        if (!event.isCancelled() && Arrays.stream(event.getKeycodes()).anyMatch(
+                (keycode) -> keycode == KeyEvent.KEYCODE_POWER)) {
+            mPowerKeyHandled = true;
+        }
         switch (gestureType) {
             case KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS:
                 if (complete) {
                     showRecentApps(false);
                 }
-                return true;
+                break;
             case KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH:
                 if (!keyguardOn) {
                     if (start) {
@@ -4276,7 +4346,7 @@
                         toggleRecentApps();
                     }
                 }
-                return true;
+                break;
             case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT:
             case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT:
                 if (complete && canLaunchApp) {
@@ -4284,33 +4354,33 @@
                             deviceId, SystemClock.uptimeMillis(),
                             AssistUtils.INVOCATION_TYPE_UNKNOWN);
                 }
-                return true;
+                break;
             case KeyGestureEvent.KEY_GESTURE_TYPE_HOME:
                 if (complete) {
                     // Post to main thread to avoid blocking input pipeline.
                     mHandler.post(() -> handleShortPressOnHome(displayId));
                 }
-                return true;
+                break;
             case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS:
                 if (complete && canLaunchApp) {
                     showSystemSettings();
                 }
-                return true;
+                break;
             case KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN:
                 if (complete) {
                     lockNow(null /* options */);
                 }
-                return true;
+                break;
             case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL:
                 if (complete) {
                     toggleNotificationPanel();
                 }
-                return true;
+                break;
             case KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT:
                 if (complete) {
                     interceptScreenshotChord(SCREENSHOT_KEY_OTHER, 0 /*pressDelay*/);
                 }
-                return true;
+                break;
             case KeyGestureEvent.KEY_GESTURE_TYPE_TRIGGER_BUG_REPORT:
                 if (complete && mEnableBugReportKeyboardShortcut) {
                     try {
@@ -4321,12 +4391,12 @@
                         Slog.d(TAG, "Error taking bugreport", e);
                     }
                 }
-                return true;
+                break;
             case KeyGestureEvent.KEY_GESTURE_TYPE_BACK:
                 if (complete) {
                     injectBackGesture(SystemClock.uptimeMillis());
                 }
-                return true;
+                break;
             case KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION:
                 if (complete) {
                     StatusBarManagerInternal statusbar = getStatusBarManagerInternal();
@@ -4335,7 +4405,7 @@
                                 getTargetDisplayIdForKeyGestureEvent(event));
                     }
                 }
-                return true;
+                break;
             case KeyGestureEvent.KEY_GESTURE_TYPE_DESKTOP_MODE:
                 if (complete) {
                     StatusBarManagerInternal statusbar = getStatusBarManagerInternal();
@@ -4344,24 +4414,24 @@
                                 getTargetDisplayIdForKeyGestureEvent(event));
                     }
                 }
-                return true;
+                break;
             case KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT:
                 if (complete) {
                     moveFocusedTaskToStageSplit(getTargetDisplayIdForKeyGestureEvent(event),
                             true /* leftOrTop */);
                 }
-                return true;
+                break;
             case KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT:
                 if (complete) {
                     moveFocusedTaskToStageSplit(getTargetDisplayIdForKeyGestureEvent(event),
                             false /* leftOrTop */);
                 }
-                return true;
+                break;
             case KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER:
                 if (complete) {
                     toggleKeyboardShortcutsMenu(deviceId);
                 }
-                return true;
+                break;
             case KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_UP:
             case KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_DOWN:
                 if (complete) {
@@ -4369,32 +4439,32 @@
                             gestureType == KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_UP ? 1 : -1;
                     changeDisplayBrightnessValue(displayId, direction);
                 }
-                return true;
+                break;
             case KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER:
                 if (start) {
                     showRecentApps(true);
                 } else {
                     hideRecentApps(true, false);
                 }
-                return true;
+                break;
             case KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS:
             case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS:
                 if (complete && isKeyEventForCurrentUser(event.getDisplayId(),
                         event.getKeycodes()[0], "launchAllAppsViaA11y")) {
                     launchAllAppsAction();
                 }
-                return true;
+                break;
             case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH:
                 if (complete && canLaunchApp) {
                     launchTargetSearchActivity();
                 }
-                return true;
+                break;
             case KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH:
                 if (complete) {
                     int direction = (modifierState & KeyEvent.META_SHIFT_MASK) != 0 ? -1 : 1;
                     sendSwitchKeyboardLayout(displayId, focusedToken, direction);
                 }
-                return true;
+                break;
             case KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD:
                 if (start) {
                     // Screenshot chord is pressed: Wait for long press delay before taking
@@ -4404,14 +4474,14 @@
                 } else {
                     cancelPendingScreenshotChordAction();
                 }
-                return true;
+                break;
             case KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD:
                 if (start) {
                     interceptRingerToggleChord();
                 } else {
                     cancelPendingRingerToggleChordAction();
                 }
-                return true;
+                break;
             case KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS:
                 if (start) {
                     performHapticFeedback(
@@ -4421,40 +4491,34 @@
                 } else {
                     cancelGlobalActionsAction();
                 }
-                return true;
+                break;
             case KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT:
                 if (start) {
                     interceptBugreportGestureTv();
                 } else {
                     cancelBugreportGestureTv();
                 }
-                return true;
+                break;
             case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT:
                 if (complete && mAccessibilityShortcutController.isAccessibilityShortcutAvailable(
                         isKeyguardLocked())) {
                     mHandler.sendMessage(mHandler.obtainMessage(MSG_ACCESSIBILITY_SHORTCUT));
                 }
-                return true;
+                break;
             case KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS:
                 if (complete) {
                     mContext.closeSystemDialogs();
                 }
-                return true;
+                break;
             case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_TALKBACK:
-                if (enableTalkbackAndMagnifierKeyGestures()) {
-                    if (complete) {
-                        mTalkbackShortcutController.toggleTalkback(mCurrentUserId,
-                                TalkbackShortcutController.ShortcutSource.KEYBOARD);
-                    }
-                    return true;
+                if (complete) {
+                    mTalkbackShortcutController.toggleTalkback(mCurrentUserId,
+                            TalkbackShortcutController.ShortcutSource.KEYBOARD);
                 }
                 break;
             case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS:
-                if (enableVoiceAccessKeyGestures()) {
-                    if (complete) {
-                        mVoiceAccessShortcutController.toggleVoiceAccess(mCurrentUserId);
-                    }
-                    return true;
+                if (complete) {
+                    mVoiceAccessShortcutController.toggleVoiceAccess(mCurrentUserId);
                 }
                 break;
             case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION:
@@ -4463,7 +4527,7 @@
                         && mModifierShortcutManager.launchApplication(data)) {
                     dismissKeyboardShortcutsMenu();
                 }
-                return true;
+                break;
             case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_DO_NOT_DISTURB:
                 NotificationManager nm = getNotificationService();
                 if (nm != null) {
@@ -4472,9 +4536,12 @@
                                     : Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, null,
                             "Key gesture DND", true);
                 }
-                return true;
+                break;
+            default:
+                Log.w(TAG, "Received a key gesture " + event
+                        + " that was not registered by this handler");
+                break;
         }
-        return false;
     }
 
     private void changeDisplayBrightnessValue(int displayId, int direction) {
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index 798c794..0f6cc24 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -87,6 +87,7 @@
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.IndentingPrintWriter;
+import android.util.IntArray;
 import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArray;
@@ -102,6 +103,7 @@
 import com.android.internal.inputmethod.SoftInputShowHideReason;
 import com.android.internal.logging.InstanceId;
 import com.android.internal.os.TransferPipe;
+import com.android.internal.statusbar.DisableStates;
 import com.android.internal.statusbar.IAddTileResultCallback;
 import com.android.internal.statusbar.ISessionListener;
 import com.android.internal.statusbar.IStatusBar;
@@ -124,6 +126,7 @@
 import com.android.server.power.ShutdownCheckPoints;
 import com.android.server.power.ShutdownThread;
 import com.android.server.wm.ActivityTaskManagerInternal;
+import com.android.systemui.shared.Flags;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -1344,48 +1347,76 @@
         return mTracingEnabled;
     }
 
-    // TODO(b/117478341): make it aware of multi-display if needed.
+    /**
+     * Disable status bar features. Pass the bitwise-or of the {@code #DISABLE_*} flags.
+     * To re-enable everything, pass {@code #DISABLE_NONE}.
+     *
+     * Warning: Only pass {@code #DISABLE_*} flags into this function, do not use
+     * {@code #DISABLE2_*} flags.
+     */
     @Override
     public void disable(int what, IBinder token, String pkg) {
         disableForUser(what, token, pkg, mCurrentUserId);
     }
 
-    // TODO(b/117478341): make it aware of multi-display if needed.
+    /**
+     * Disable status bar features for a given user. Pass the bitwise-or of the
+     * {@code #DISABLE_*} flags. To re-enable everything, pass {@code #DISABLE_NONE}.
+     *
+     * Warning: Only pass {@code #DISABLE_*} flags into this function, do not use
+     * {@code #DISABLE2_*} flags.
+     */
     @Override
     public void disableForUser(int what, IBinder token, String pkg, int userId) {
         enforceStatusBar();
         enforceValidCallingUser();
 
         synchronized (mLock) {
-            disableLocked(DEFAULT_DISPLAY, userId, what, token, pkg, 1);
+            if (Flags.statusBarConnectedDisplays()) {
+                IntArray displayIds = new IntArray();
+                for (int i = 0; i < mDisplayUiState.size(); i++) {
+                    displayIds.add(mDisplayUiState.keyAt(i));
+                }
+                disableAllDisplaysLocked(displayIds, userId, what, token, pkg, /* whichFlag= */ 1);
+            } else {
+                disableLocked(DEFAULT_DISPLAY, userId, what, token, pkg, /* whichFlag= */ 1);
+            }
         }
     }
 
-    // TODO(b/117478341): make it aware of multi-display if needed.
     /**
-     * Disable additional status bar features. Pass the bitwise-or of the DISABLE2_* flags.
-     * To re-enable everything, pass {@link #DISABLE2_NONE}.
+     * Disable additional status bar features. Pass the bitwise-or of the {@code #DISABLE2_*} flags.
+     * To re-enable everything, pass {@code #DISABLE2_NONE}.
      *
-     * Warning: Only pass DISABLE2_* flags into this function, do not use DISABLE_* flags.
+     * Warning: Only pass {@code #DISABLE2_*} flags into this function, do not use
+     * {@code #DISABLE_*} flags.
      */
     @Override
     public void disable2(int what, IBinder token, String pkg) {
         disable2ForUser(what, token, pkg, mCurrentUserId);
     }
 
-    // TODO(b/117478341): make it aware of multi-display if needed.
     /**
-     * Disable additional status bar features for a given user. Pass the bitwise-or of the
-     * DISABLE2_* flags. To re-enable everything, pass {@link #DISABLE_NONE}.
+     * Disable additional status bar features for a given user. Pass the bitwise-or
+     * of the {@code #DISABLE2_*} flags. To re-enable everything, pass {@code #DISABLE2_NONE}.
      *
-     * Warning: Only pass DISABLE2_* flags into this function, do not use DISABLE_* flags.
+     * Warning: Only pass {@code #DISABLE2_*} flags into this function, do not use
+     * {@code #DISABLE_*}  flags.
      */
     @Override
     public void disable2ForUser(int what, IBinder token, String pkg, int userId) {
         enforceStatusBar();
 
         synchronized (mLock) {
-            disableLocked(DEFAULT_DISPLAY, userId, what, token, pkg, 2);
+            if (Flags.statusBarConnectedDisplays()) {
+                IntArray displayIds = new IntArray();
+                for (int i = 0; i < mDisplayUiState.size(); i++) {
+                    displayIds.add(mDisplayUiState.keyAt(i));
+                }
+                disableAllDisplaysLocked(displayIds, userId, what, token, pkg, /* whichFlag= */ 2);
+            } else {
+                disableLocked(DEFAULT_DISPLAY, userId, what, token, pkg, /* whichFlag= */ 2);
+            }
         }
     }
 
@@ -1414,6 +1445,42 @@
         }
     }
 
+    // This method batches disable state across all displays into a single remote call
+    // (IStatusBar#disableForAllDisplays) for efficiency and calls
+    // NotificationDelegate#onSetDisabled only if any display's disable state changes.
+    private void disableAllDisplaysLocked(IntArray displayIds, int userId, int what, IBinder token,
+            String pkg, int whichFlag) {
+        // It's important that the the callback and the call to mBar get done
+        // in the same order when multiple threads are calling this function
+        // so they are paired correctly.  The messages on the handler will be
+        // handled in the order they were enqueued, but will be outside the lock.
+        manageDisableListLocked(userId, what, token, pkg, whichFlag);
+
+        // Ensure state for the current user is applied, even if passed a non-current user.
+        final int net1 = gatherDisableActionsLocked(mCurrentUserId, 1);
+        final int net2 = gatherDisableActionsLocked(mCurrentUserId, 2);
+
+        IStatusBar bar = mBar;
+        Map<Integer, Pair<Integer, Integer>> displaysWithNewDisableStates = new HashMap<>();
+        for (int displayId : displayIds.toArray()) {
+            final UiState state = getUiState(displayId);
+            if (!state.disableEquals(net1, net2)) {
+                state.setDisabled(net1, net2);
+                displaysWithNewDisableStates.put(displayId, new Pair(net1, net2));
+            }
+        }
+        if (bar != null) {
+            try {
+                bar.disableForAllDisplays(new DisableStates(displaysWithNewDisableStates));
+            } catch (RemoteException ex) {
+                Slog.e(TAG, "Unable to disable Status bar.", ex);
+            }
+        }
+        if (!displaysWithNewDisableStates.isEmpty()) {
+            mHandler.post(() -> mNotificationDelegate.onSetDisabled(net1));
+        }
+    }
+
     /**
      * Get the currently applied disable flags, in the form of one Pair<Integer, Integer>.
      *
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 5bb64bc..ec17d13 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -35,8 +35,6 @@
 import static android.content.Intent.FLAG_ACTIVITY_TASK_ON_HOME;
 import static android.content.pm.ActivityInfo.FLAG_RELINQUISH_TASK_IDENTITY;
 import static android.content.pm.ActivityInfo.FLAG_SHOW_FOR_ALL_USERS;
-import static android.content.pm.ActivityInfo.FORCE_NON_RESIZE_APP;
-import static android.content.pm.ActivityInfo.FORCE_RESIZE_APP;
 import static android.content.pm.ActivityInfo.RESIZE_MODE_FORCE_RESIZABLE_LANDSCAPE_ONLY;
 import static android.content.pm.ActivityInfo.RESIZE_MODE_FORCE_RESIZABLE_PORTRAIT_ONLY;
 import static android.content.pm.ActivityInfo.RESIZE_MODE_FORCE_RESIZABLE_PRESERVE_ORIENTATION;
@@ -51,7 +49,6 @@
 import static android.view.SurfaceControl.METADATA_TASK_ID;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
 import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
-import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES;
 import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_FLAG_APP_CRASHED;
 import static android.view.WindowManager.TRANSIT_OPEN;
@@ -132,7 +129,6 @@
 import android.app.PictureInPictureParams;
 import android.app.TaskInfo;
 import android.app.WindowConfiguration;
-import android.app.compat.CompatChanges;
 import android.content.ComponentName;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
@@ -514,10 +510,16 @@
     boolean mIsPerceptible = false;
 
     /**
-     * Whether the compatibility overrides that change the resizability of the app should be allowed
-     * for the specific app.
+     * Whether the task has been forced resizable, which is determined by the
+     *  activity that started this task.
      */
-    boolean mAllowForceResizeOverride = true;
+    private boolean mForceResizeOverride;
+
+    /**
+     * Whether the task has been forced non-resizable, which is determined by
+     * the activity that started this task.
+     */
+    private boolean mForceNonResizeOverride;
 
     private static final int TRANSLUCENT_TIMEOUT_MSG = FIRST_ACTIVITY_TASK_MSG + 1;
 
@@ -675,7 +677,6 @@
             intent = _intent;
             mMinWidth = minWidth;
             mMinHeight = minHeight;
-            updateAllowForceResizeOverride();
         }
         mAtmService.getTaskChangeNotificationController().notifyTaskCreated(_taskId, realActivity);
         mHandler = new ActivityTaskHandler(mTaskSupervisor.mLooper);
@@ -946,6 +947,7 @@
             mCallingPackage = r.launchedFromPackage;
             mCallingFeatureId = r.launchedFromFeatureId;
             setIntent(intent != null ? intent : r.intent, info != null ? info : r.info);
+            updateForceResizeOverrides(r);
         }
         setLockTaskAuth(r);
     }
@@ -1038,7 +1040,6 @@
             mTaskSupervisor.mRecentTasks.remove(this);
             mTaskSupervisor.mRecentTasks.add(this);
         }
-        updateAllowForceResizeOverride();
     }
 
     /** Sets the original minimal width and height. */
@@ -1855,15 +1856,14 @@
                 -1 /* don't check PID */, -1 /* don't check UID */, this);
     }
 
-    private void updateAllowForceResizeOverride() {
-        try {
-            mAllowForceResizeOverride = mAtmService.mContext.getPackageManager().getPropertyAsUser(
-                    PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES,
-                    getBasePackageName(), null /* className */, mUserId).getBoolean();
-        } catch (PackageManager.NameNotFoundException e) {
-            // Package not found or property not defined, reset to default value.
-            mAllowForceResizeOverride = true;
-        }
+    private void updateForceResizeOverrides(@NonNull ActivityRecord r) {
+        final AppCompatResizeOverrides resizeOverrides = r.mAppCompatController
+                .getResizeOverrides();
+        mForceResizeOverride = resizeOverrides.shouldOverrideForceResizeApp()
+                || r.isUniversalResizeable()
+                || r.mAppCompatController.getAspectRatioOverrides()
+                    .hasFullscreenOverride();
+        mForceNonResizeOverride = resizeOverrides.shouldOverrideForceNonResizeApp();
     }
 
     /**
@@ -2882,17 +2882,8 @@
         final boolean forceResizable = mAtmService.mForceResizableActivities
                 && getActivityType() == ACTIVITY_TYPE_STANDARD;
         if (forceResizable) return true;
-
-        final UserHandle userHandle = UserHandle.getUserHandleForUid(mUserId);
-        final boolean forceResizableOverride = mAllowForceResizeOverride
-                && CompatChanges.isChangeEnabled(
-                        FORCE_RESIZE_APP, getBasePackageName(), userHandle);
-        final boolean forceNonResizableOverride = mAllowForceResizeOverride
-                && CompatChanges.isChangeEnabled(
-                        FORCE_NON_RESIZE_APP, getBasePackageName(), userHandle);
-
-        if (forceNonResizableOverride) return false;
-        return forceResizableOverride || ActivityInfo.isResizeableMode(mResizeMode)
+        if (mForceNonResizeOverride) return false;
+        return mForceResizeOverride || ActivityInfo.isResizeableMode(mResizeMode)
                 || (mSupportsPictureInPicture && checkPictureInPictureSupport);
     }
 
diff --git a/services/core/java/com/android/server/wm/TaskFpsCallbackController.java b/services/core/java/com/android/server/wm/TaskFpsCallbackController.java
index 8c79875..665c5cf 100644
--- a/services/core/java/com/android/server/wm/TaskFpsCallbackController.java
+++ b/services/core/java/com/android/server/wm/TaskFpsCallbackController.java
@@ -16,7 +16,6 @@
 
 package com.android.server.wm;
 
-import android.content.Context;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.window.ITaskFpsCallback;
@@ -25,12 +24,10 @@
 
 final class TaskFpsCallbackController {
 
-    private final Context mContext;
     private final HashMap<IBinder, Long> mTaskFpsCallbacks;
     private final HashMap<IBinder, IBinder.DeathRecipient> mDeathRecipients;
 
-    TaskFpsCallbackController(Context context) {
-        mContext = context;
+    TaskFpsCallbackController() {
         mTaskFpsCallbacks = new HashMap<>();
         mDeathRecipients = new HashMap<>();
     }
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index ebf8d35..a9bb690 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -1449,7 +1449,7 @@
         mPresentationController = new PresentationController();
 
         mBlurController = new BlurController(mContext, mPowerManager);
-        mTaskFpsCallbackController = new TaskFpsCallbackController(mContext);
+        mTaskFpsCallbackController = new TaskFpsCallbackController();
         mAccessibilityController = new AccessibilityController(this);
         mScreenRecordingCallbackController = new ScreenRecordingCallbackController(this);
         mSystemPerformanceHinter = new SystemPerformanceHinter(mContext, displayId -> {
diff --git a/services/credentials/java/com/android/server/credentials/MetricUtilities.java b/services/credentials/java/com/android/server/credentials/MetricUtilities.java
index ac4aac6..11edb93 100644
--- a/services/credentials/java/com/android/server/credentials/MetricUtilities.java
+++ b/services/credentials/java/com/android/server/credentials/MetricUtilities.java
@@ -383,7 +383,9 @@
                     /* api_name */
                     initialPhaseMetric.getApiName(),
                     /* primary_candidates_indicated */
-                    candidatePrimaryProviderList
+                    candidatePrimaryProviderList,
+                    /* api_prepared */
+                    initialPhaseMetric.hasApiUsedPrepareFlow()
             );
         } catch (Exception e) {
             Slog.w(TAG, "Unexpected error during candidate provider uid metric emit: " + e);
@@ -442,7 +444,9 @@
                     /* autofill_session_id */
                     initialPhaseMetric.getAutofillSessionId(),
                     /* autofill_request_id */
-                    initialPhaseMetric.getAutofillRequestId()
+                    initialPhaseMetric.getAutofillRequestId(),
+                    /* api_prepared */
+                    initialPhaseMetric.hasApiUsedPrepareFlow()
             );
         } catch (Exception e) {
             Slog.w(TAG, "Unexpected error during initial metric emit: " + e);
diff --git a/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java b/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java
index d60807c..2d4360e 100644
--- a/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java
@@ -27,6 +27,7 @@
 import android.credentials.IGetCredentialCallback;
 import android.credentials.IPrepareGetCredentialCallback;
 import android.credentials.PrepareGetCredentialResponseInternal;
+import android.credentials.flags.Flags;
 import android.credentials.selection.GetCredentialProviderData;
 import android.credentials.selection.ProviderData;
 import android.credentials.selection.RequestInfo;
@@ -60,7 +61,12 @@
         int numTypes = (request.getCredentialOptions().stream()
                 .map(CredentialOption::getType).collect(
                         Collectors.toSet())).size(); // Dedupe type strings
-        mRequestSessionMetric.collectGetFlowInitialMetricInfo(request);
+        if (!Flags.fixMetricDuplicationEmits()) {
+            mRequestSessionMetric.collectGetFlowInitialMetricInfo(request);
+        } else {
+            mRequestSessionMetric.collectGetFlowInitialMetricInfo(request,
+                    /*isApiPrepared=*/ true);
+        }
         mPrepareGetCredentialCallback = prepareGetCredentialCallback;
 
         Slog.i(TAG, "PrepareGetRequestSession constructed.");
diff --git a/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java b/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java
index 8a4e86c..811b97a 100644
--- a/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java
+++ b/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java
@@ -55,6 +55,9 @@
     // The request id of autofill if the request is from autofill, defaults to -1
     private int mAutofillRequestId = -1;
 
+    // Indicates if this API call used the prepare flow, defaults to false
+    private boolean mApiUsedPrepareFlow = false;
+
 
     public InitialPhaseMetric(int sessionIdTrackOne) {
         mSessionIdCaller = sessionIdTrackOne;
@@ -173,4 +176,17 @@
     public int[] getUniqueRequestCounts() {
         return mRequestCounts.values().stream().mapToInt(Integer::intValue).toArray();
     }
+
+    /* ------ API Prepared ------ */
+
+    public void setApiUsedPrepareFlow(boolean apiUsedPrepareFlow) {
+        mApiUsedPrepareFlow = apiUsedPrepareFlow;
+    }
+
+    /**
+     * @return a boolean indicating if this API call utilized a prepare flow
+     */
+    public boolean hasApiUsedPrepareFlow() {
+        return mApiUsedPrepareFlow;
+    }
 }
diff --git a/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java b/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java
index 619a568..dc1747f 100644
--- a/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java
+++ b/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java
@@ -225,6 +225,22 @@
     }
 
     /**
+     * Collects initializations for Get flow metrics.
+     *
+     * @param request the get credential request containing information to parse for metrics
+     * @param isApiPrepared indicates this API flow utilized the 'prepare' flow
+     */
+    public void collectGetFlowInitialMetricInfo(GetCredentialRequest request,
+            boolean isApiPrepared) {
+        try {
+            collectGetFlowInitialMetricInfo(request);
+            mInitialPhaseMetric.setApiUsedPrepareFlow(isApiPrepared);
+        } catch (Exception e) {
+            Slog.i(TAG, "Unexpected error collecting get flow initial metric: " + e);
+        }
+    }
+
+    /**
      * During browsing, where multiple entries can be selected, this collects the browsing phase
      * metric information. This is emitted together with the final phase, and the recursive path
      * with authentication entries, which may occur in rare circumstances, are captured.
diff --git a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt
index 232bb83..5a140d5 100644
--- a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt
@@ -1753,6 +1753,13 @@
         }
         val appIdPermissionFlags = newState.mutateUserState(userId)!!.mutateAppIdPermissionFlags()
         val permissionFlags = appIdPermissionFlags.mutateOrPut(appId) { MutableIndexedMap() }
+        // for debugging possible races TODO(b/401768134)
+        oldState.userStates[userId]?.appIdPermissionFlags[appId]?.map?.let {
+            if (permissionFlags.map === it) {
+                throw IllegalStateException("Unexpected sharing between old/new state")
+            }
+        }
+
         permissionFlags.putWithDefault(permissionName, newFlags, 0)
         if (permissionFlags.isEmpty()) {
             appIdPermissionFlags -= appId
diff --git a/services/tests/displayservicetests/Android.bp b/services/tests/displayservicetests/Android.bp
index 36ea241..c85053d 100644
--- a/services/tests/displayservicetests/Android.bp
+++ b/services/tests/displayservicetests/Android.bp
@@ -51,6 +51,7 @@
 
     data: [
         ":DisplayManagerTestApp",
+        ":TopologyTestApp",
     ],
 
     certificate: "platform",
diff --git a/services/tests/displayservicetests/AndroidManifest.xml b/services/tests/displayservicetests/AndroidManifest.xml
index 205ff05..76f219b 100644
--- a/services/tests/displayservicetests/AndroidManifest.xml
+++ b/services/tests/displayservicetests/AndroidManifest.xml
@@ -29,6 +29,7 @@
     <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
     <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
     <uses-permission android:name="android.permission.MANAGE_USB" />
+    <uses-permission android:name="android.permission.MANAGE_DISPLAYS" />
 
     <!-- Permissions needed for DisplayTransformManagerTest -->
     <uses-permission android:name="android.permission.CHANGE_CONFIGURATION" />
diff --git a/services/tests/displayservicetests/AndroidTest.xml b/services/tests/displayservicetests/AndroidTest.xml
index f3697bb..2fe3723 100644
--- a/services/tests/displayservicetests/AndroidTest.xml
+++ b/services/tests/displayservicetests/AndroidTest.xml
@@ -28,6 +28,7 @@
         <option name="cleanup-apks" value="true" />
         <option name="install-arg" value="-t" />
         <option name="test-file-name" value="DisplayManagerTestApp.apk" />
+        <option name="test-file-name" value="TopologyTestApp.apk" />
     </target_preparer>
 
     <option name="test-tag" value="DisplayServiceTests" />
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayEventDeliveryTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayEventDeliveryTest.java
index 1f45792..bf4b613 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayEventDeliveryTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayEventDeliveryTest.java
@@ -16,7 +16,6 @@
 
 package com.android.server.display;
 
-import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC;
 import static android.util.DisplayMetrics.DENSITY_HIGH;
@@ -27,19 +26,11 @@
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeTrue;
 
-import android.app.ActivityManager;
-import android.app.Instrumentation;
-import android.content.Context;
 import android.content.Intent;
-import android.hardware.display.DisplayManager;
 import android.hardware.display.VirtualDisplay;
-import android.os.BinderProxy;
 import android.os.Handler;
-import android.os.HandlerThread;
 import android.os.Looper;
 import android.os.Message;
-import android.os.Messenger;
-import android.platform.test.annotations.AppModeSdkSandbox;
 import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.platform.test.flag.junit.CheckFlagsRule;
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
@@ -48,10 +39,7 @@
 
 import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
-import androidx.test.platform.app.InstrumentationRegistry;
 
-import com.android.compatibility.common.util.SystemUtil;
-import com.android.compatibility.common.util.TestUtils;
 import com.android.server.am.Flags;
 
 import org.junit.After;
@@ -63,9 +51,7 @@
 import org.junit.runners.Parameterized.Parameter;
 import org.junit.runners.Parameterized.Parameters;
 
-import java.io.IOException;
 import java.util.Arrays;
-import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
 
@@ -73,8 +59,7 @@
  * Tests that applications can receive display events correctly.
  */
 @RunWith(Parameterized.class)
-@AppModeSdkSandbox(reason = "Allow test in the SDK sandbox (does not prevent other modes).")
-public class DisplayEventDeliveryTest {
+public class DisplayEventDeliveryTest extends EventDeliveryTestBase {
     private static final String TAG = "DisplayEventDeliveryTest";
 
     @Rule
@@ -85,37 +70,17 @@
     private static final int WIDTH = 720;
     private static final int HEIGHT = 480;
 
-    private static final int MESSAGE_LAUNCHED = 1;
-    private static final int MESSAGE_CALLBACK = 2;
-
     private static final int DISPLAY_ADDED = 1;
     private static final int DISPLAY_CHANGED = 2;
     private static final int DISPLAY_REMOVED = 3;
 
-    private static final long DISPLAY_EVENT_TIMEOUT_MSEC = 100;
-    private static final long TEST_FAILURE_TIMEOUT_MSEC = 10000;
-
     private static final String TEST_PACKAGE =
             "com.android.servicestests.apps.displaymanagertestapp";
     private static final String TEST_ACTIVITY = TEST_PACKAGE + ".DisplayEventActivity";
     private static final String TEST_DISPLAYS = "DISPLAYS";
-    private static final String TEST_MESSENGER = "MESSENGER";
 
     private final Object mLock = new Object();
 
-    private Instrumentation mInstrumentation;
-    private Context mContext;
-    private DisplayManager mDisplayManager;
-    private ActivityManager mActivityManager;
-    private ActivityManager.OnUidImportanceListener mUidImportanceListener;
-    private CountDownLatch mLatchActivityLaunch;
-    private CountDownLatch mLatchActivityCached;
-    private HandlerThread mHandlerThread;
-    private Handler mHandler;
-    private Messenger mMessenger;
-    private int mPid;
-    private int mUid;
-
     /**
      * Array of DisplayBundle. The test handler uses it to check if certain display events have
      * been sent to DisplayEventActivity.
@@ -167,7 +132,7 @@
          */
         public void assertNoDisplayEvents() {
             try {
-                assertNull(mExpectations.poll(DISPLAY_EVENT_TIMEOUT_MSEC, TimeUnit.MILLISECONDS));
+                assertNull(mExpectations.poll(EVENT_TIMEOUT_MSEC, TimeUnit.MILLISECONDS));
             } catch (InterruptedException e) {
                 throw new RuntimeException(e);
             }
@@ -239,37 +204,17 @@
     }
 
     @Before
-    public void setUp() throws Exception {
-        mInstrumentation = InstrumentationRegistry.getInstrumentation();
-        mContext = mInstrumentation.getContext();
-        mDisplayManager = mContext.getSystemService(DisplayManager.class);
-        mLatchActivityLaunch = new CountDownLatch(1);
-        mLatchActivityCached = new CountDownLatch(1);
-        mActivityManager = mContext.getSystemService(ActivityManager.class);
-        mUidImportanceListener = (uid, importance) -> {
-            if (uid == mUid && importance == IMPORTANCE_CACHED) {
-                Log.d(TAG, "Listener " + uid + " becomes " + importance);
-                mLatchActivityCached.countDown();
-            }
-        };
-        SystemUtil.runWithShellPermissionIdentity(() ->
-                mActivityManager.addOnUidImportanceListener(mUidImportanceListener,
-                        IMPORTANCE_CACHED));
+    public void setUp() {
+        super.setUp();
         // The lock is not functionally necessary but eliminates lint error messages.
         synchronized (mLock) {
             mDisplayBundles = new SparseArray<>();
         }
-        mHandlerThread = new HandlerThread("handler");
-        mHandlerThread.start();
-        mHandler = new TestHandler(mHandlerThread.getLooper());
-        mMessenger = new Messenger(mHandler);
-        mPid = 0;
     }
 
     @After
     public void tearDown() throws Exception {
-        mActivityManager.removeOnUidImportanceListener(mUidImportanceListener);
-        mHandlerThread.quitSafely();
+        super.tearDown();
         synchronized (mLock) {
             for (int i = 0; i < mDisplayBundles.size(); i++) {
                 DisplayBundle bundle = mDisplayBundles.valueAt(i);
@@ -278,7 +223,31 @@
             }
             mDisplayBundles.clear();
         }
-        SystemUtil.runShellCommand(mInstrumentation, "am force-stop " + TEST_PACKAGE);
+    }
+
+    @Override
+    protected String getTag() {
+        return TAG;
+    }
+
+    @Override
+    protected Handler getHandler(Looper looper) {
+        return new TestHandler(looper);
+    }
+
+    @Override
+    protected String getTestPackage() {
+        return TEST_PACKAGE;
+    }
+
+    @Override
+    protected String getTestActivity() {
+        return TEST_ACTIVITY;
+    }
+
+    @Override
+    protected void putExtra(Intent intent) {
+        intent.putExtra(TEST_DISPLAYS, mDisplayCount);
     }
 
     /**
@@ -291,42 +260,8 @@
     }
 
     /**
-     * Return true if the freezer is enabled on this platform and if freezer notifications are
-     * supported.  It is not enough to test that the freezer notification feature is enabled
-     * because some devices do not have the necessary kernel support.
-     */
-    private boolean isAppFreezerEnabled() {
-        try {
-            return mActivityManager.getService().isAppFreezerEnabled()
-                    && android.os.Flags.binderFrozenStateChangeCallback()
-                    && BinderProxy.isFrozenStateChangeCallbackSupported();
-        } catch (Exception e) {
-            Log.e(TAG, "isAppFreezerEnabled() failed: " + e);
-            return false;
-        }
-    }
-
-    private void waitForProcessFreeze(int pid, long timeoutMs) {
-        // TODO: Add a listener to monitor freezer state changes.
-        SystemUtil.runWithShellPermissionIdentity(() -> {
-            TestUtils.waitUntil("Timed out waiting for test process to be frozen; pid=" + pid,
-                    (int) TimeUnit.MILLISECONDS.toSeconds(timeoutMs),
-                    () -> mActivityManager.isProcessFrozen(pid));
-        });
-    }
-
-    private void waitForProcessUnfreeze(int pid, long timeoutMs) {
-        // TODO: Add a listener to monitor freezer state changes.
-        SystemUtil.runWithShellPermissionIdentity(() -> {
-            TestUtils.waitUntil("Timed out waiting for test process to be frozen; pid=" + pid,
-                    (int) TimeUnit.MILLISECONDS.toSeconds(timeoutMs),
-                    () -> !mActivityManager.isProcessFrozen(pid));
-        });
-    }
-
-    /**
-     * Create virtual displays, change their configurations and release them.  The number of
-     * displays is set by the {@link #mDisplays} variable.
+     * Create virtual displays, change their configurations and release them. The number of
+     * displays is set by the {@link #data()} parameter.
      */
     private void testDisplayEventsInternal(boolean cached, boolean frozen) {
         Log.d(TAG, "Start test testDisplayEvents " + mDisplayCount + " " + cached + " " + frozen);
@@ -445,110 +380,6 @@
     }
 
     /**
-     * Launch the test activity that would listen to display events. Return its process ID.
-     */
-    private int launchTestActivity() {
-        Intent intent = new Intent(Intent.ACTION_MAIN);
-        intent.setClassName(TEST_PACKAGE, TEST_ACTIVITY);
-        intent.putExtra(TEST_MESSENGER, mMessenger);
-        intent.putExtra(TEST_DISPLAYS, mDisplayCount);
-        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        SystemUtil.runWithShellPermissionIdentity(
-                () -> {
-                    mContext.startActivity(intent);
-                },
-                android.Manifest.permission.START_ACTIVITIES_FROM_SDK_SANDBOX);
-        waitLatch(mLatchActivityLaunch);
-
-        try {
-            String cmd = "pidof " + TEST_PACKAGE;
-            String result = SystemUtil.runShellCommand(mInstrumentation, cmd);
-            return Integer.parseInt(result.trim());
-        } catch (IOException e) {
-            fail("failed to get pid of test package");
-            return 0;
-        } catch (NumberFormatException e) {
-            fail("failed to parse pid " + e);
-            return 0;
-        }
-    }
-
-    /**
-     * Bring the test activity back to top
-     */
-    private void bringTestActivityTop() {
-        Intent intent = new Intent(Intent.ACTION_MAIN);
-        intent.setClassName(TEST_PACKAGE, TEST_ACTIVITY);
-        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
-        SystemUtil.runWithShellPermissionIdentity(
-                () -> {
-                    mContext.startActivity(intent);
-                },
-                android.Manifest.permission.START_ACTIVITIES_FROM_SDK_SANDBOX);
-    }
-
-    /**
-     * Bring the test activity into cached mode by launching another 2 apps
-     */
-    private void makeTestActivityCached() {
-        // Launch another activity to bring the test activity into background
-        Intent intent = new Intent(Intent.ACTION_MAIN);
-        intent.setClass(mContext, SimpleActivity.class);
-        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
-
-        // Launch another activity to bring the test activity into cached mode
-        Intent intent2 = new Intent(Intent.ACTION_MAIN);
-        intent2.setClass(mContext, SimpleActivity2.class);
-        intent2.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        SystemUtil.runWithShellPermissionIdentity(
-                () -> {
-                    mInstrumentation.startActivitySync(intent);
-                    mInstrumentation.startActivitySync(intent2);
-                },
-                android.Manifest.permission.START_ACTIVITIES_FROM_SDK_SANDBOX);
-        waitLatch(mLatchActivityCached);
-    }
-
-    // Sleep, ignoring interrupts.
-    private void pause(int s) {
-        try { Thread.sleep(s * 1000); } catch (Exception e) { }
-    }
-
-    /**
-     * Freeze the test activity.
-     */
-    private void makeTestActivityFrozen(int pid) {
-        // The delay here is meant to allow pending binder transactions to drain.  A process
-        // cannot be frozen if it has pending binder transactions, and attempting to freeze such a
-        // process more than a few times will result in the system killing the process.
-        pause(5);
-        try {
-            String cmd = "am freeze --sticky ";
-            SystemUtil.runShellCommand(mInstrumentation, cmd + TEST_PACKAGE);
-        } catch (IOException e) {
-            fail(e.toString());
-        }
-        // Wait for the freeze to complete in the kernel and for the frozen process
-        // notification to settle out.
-        waitForProcessFreeze(pid, 5 * 1000);
-    }
-
-    /**
-     * Freeze the test activity.
-     */
-    private void makeTestActivityUnfrozen(int pid) {
-        try {
-            String cmd = "am unfreeze --sticky ";
-            SystemUtil.runShellCommand(mInstrumentation, cmd + TEST_PACKAGE);
-        } catch (IOException e) {
-            fail(e.toString());
-        }
-        // Wait for the freeze to complete in the kernel and for the frozen process
-        // notification to settle out.
-        waitForProcessUnfreeze(pid, 5 * 1000);
-    }
-
-    /**
      * Create a virtual display
      *
      * @param name The name of the new virtual display
@@ -560,15 +391,4 @@
                 VIRTUAL_DISPLAY_FLAG_PUBLIC | VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
                 /* flags: a public virtual display that another app can access */);
     }
-
-    /**
-     * Wait for CountDownLatch with timeout
-     */
-    private void waitLatch(CountDownLatch latch) {
-        try {
-            latch.await(TEST_FAILURE_TIMEOUT_MSEC, TimeUnit.MILLISECONDS);
-        } catch (InterruptedException e) {
-            throw new RuntimeException(e);
-        }
-    }
 }
diff --git a/services/tests/displayservicetests/src/com/android/server/display/EventDeliveryTestBase.java b/services/tests/displayservicetests/src/com/android/server/display/EventDeliveryTestBase.java
new file mode 100644
index 0000000..2911b9b
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/EventDeliveryTestBase.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display;
+
+import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED;
+
+import static org.junit.Assert.fail;
+
+import android.app.ActivityManager;
+import android.app.Instrumentation;
+import android.content.Context;
+import android.content.Intent;
+import android.hardware.display.DisplayManager;
+import android.os.BinderProxy;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Messenger;
+import android.platform.test.annotations.AppModeSdkSandbox;
+import android.util.Log;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.compatibility.common.util.SystemUtil;
+import com.android.compatibility.common.util.TestUtils;
+
+import java.io.IOException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@AppModeSdkSandbox(reason = "Allow test in the SDK sandbox (does not prevent other modes).")
+public abstract class EventDeliveryTestBase {
+    protected static final int MESSAGE_LAUNCHED = 1;
+    protected static final int MESSAGE_CALLBACK = 2;
+
+    protected static final long EVENT_TIMEOUT_MSEC = 100;
+    protected static final long TEST_FAILURE_TIMEOUT_MSEC = 10000;
+
+    private static final String TEST_MESSENGER = "MESSENGER";
+
+    private Instrumentation mInstrumentation;
+    private Context mContext;
+    protected DisplayManager mDisplayManager;
+    private ActivityManager mActivityManager;
+    private ActivityManager.OnUidImportanceListener mUidImportanceListener;
+    protected CountDownLatch mLatchActivityLaunch;
+    private CountDownLatch mLatchActivityCached;
+    private HandlerThread mHandlerThread;
+    private Handler mHandler;
+    private Messenger mMessenger;
+    protected int mPid;
+    protected int mUid;
+
+    protected abstract String getTag();
+
+    protected abstract Handler getHandler(Looper looper);
+
+    protected abstract String getTestPackage();
+
+    protected abstract String getTestActivity();
+
+    protected abstract void putExtra(Intent intent);
+
+    protected void setUp() {
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+        mContext = mInstrumentation.getContext();
+        mDisplayManager = mContext.getSystemService(DisplayManager.class);
+        mLatchActivityLaunch = new CountDownLatch(1);
+        mLatchActivityCached = new CountDownLatch(1);
+        mActivityManager = mContext.getSystemService(ActivityManager.class);
+        mUidImportanceListener = (uid, importance) -> {
+            if (uid == mUid && importance == IMPORTANCE_CACHED) {
+                Log.d(getTag(), "Listener " + uid + " becomes " + importance);
+                mLatchActivityCached.countDown();
+            }
+        };
+        SystemUtil.runWithShellPermissionIdentity(() ->
+                mActivityManager.addOnUidImportanceListener(mUidImportanceListener,
+                        IMPORTANCE_CACHED));
+        mHandlerThread = new HandlerThread("handler");
+        mHandlerThread.start();
+        mHandler = getHandler(mHandlerThread.getLooper());
+        mMessenger = new Messenger(mHandler);
+        mPid = 0;
+    }
+
+    protected void tearDown() throws Exception {
+        mActivityManager.removeOnUidImportanceListener(mUidImportanceListener);
+        mHandlerThread.quitSafely();
+        SystemUtil.runShellCommand(mInstrumentation, "am force-stop " + getTestPackage());
+    }
+
+    /**
+     * Return true if the freezer is enabled on this platform and if freezer notifications are
+     * supported.  It is not enough to test that the freezer notification feature is enabled
+     * because some devices do not have the necessary kernel support.
+     */
+    protected boolean isAppFreezerEnabled() {
+        try {
+            return ActivityManager.getService().isAppFreezerEnabled()
+                    && android.os.Flags.binderFrozenStateChangeCallback()
+                    && BinderProxy.isFrozenStateChangeCallbackSupported();
+        } catch (Exception e) {
+            Log.e(getTag(), "isAppFreezerEnabled() failed: " + e);
+            return false;
+        }
+    }
+
+    private void waitForProcessFreeze(int pid, long timeoutMs) {
+        // TODO: Add a listener to monitor freezer state changes.
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            TestUtils.waitUntil(
+                    "Timed out waiting for test process to be frozen; pid=" + pid,
+                    (int) TimeUnit.MILLISECONDS.toSeconds(timeoutMs),
+                    () -> mActivityManager.isProcessFrozen(pid));
+        });
+    }
+
+    private void waitForProcessUnfreeze(int pid, long timeoutMs) {
+        // TODO: Add a listener to monitor freezer state changes.
+        SystemUtil.runWithShellPermissionIdentity(() -> {
+            TestUtils.waitUntil("Timed out waiting for test process to be frozen; pid=" + pid,
+                    (int) TimeUnit.MILLISECONDS.toSeconds(timeoutMs),
+                    () -> !mActivityManager.isProcessFrozen(pid));
+        });
+    }
+
+    /**
+     * Launch the test activity that would listen to events. Return its process ID.
+     */
+    protected int launchTestActivity() {
+        Intent intent = new Intent(Intent.ACTION_MAIN);
+        intent.setClassName(getTestPackage(), getTestActivity());
+        intent.putExtra(TEST_MESSENGER, mMessenger);
+        putExtra(intent);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        SystemUtil.runWithShellPermissionIdentity(
+                () -> {
+                    mContext.startActivity(intent);
+                },
+                android.Manifest.permission.START_ACTIVITIES_FROM_SDK_SANDBOX);
+        waitLatch(mLatchActivityLaunch);
+
+        try {
+            String cmd = "pidof " + getTestPackage();
+            String result = SystemUtil.runShellCommand(mInstrumentation, cmd);
+            return Integer.parseInt(result.trim());
+        } catch (IOException e) {
+            fail("failed to get pid of test package");
+            return 0;
+        } catch (NumberFormatException e) {
+            fail("failed to parse pid " + e);
+            return 0;
+        }
+    }
+
+    /**
+     * Bring the test activity back to top
+     */
+    protected void bringTestActivityTop() {
+        Intent intent = new Intent(Intent.ACTION_MAIN);
+        intent.setClassName(getTestPackage(), getTestActivity());
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
+        SystemUtil.runWithShellPermissionIdentity(
+                () -> {
+                    mContext.startActivity(intent);
+                },
+                android.Manifest.permission.START_ACTIVITIES_FROM_SDK_SANDBOX);
+    }
+
+
+    /**
+     * Bring the test activity into cached mode by launching another 2 apps
+     */
+    protected void makeTestActivityCached() {
+        // Launch another activity to bring the test activity into background
+        Intent intent = new Intent(Intent.ACTION_MAIN);
+        intent.setClass(mContext, SimpleActivity.class);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
+
+        // Launch another activity to bring the test activity into cached mode
+        Intent intent2 = new Intent(Intent.ACTION_MAIN);
+        intent2.setClass(mContext, SimpleActivity2.class);
+        intent2.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        SystemUtil.runWithShellPermissionIdentity(
+                () -> {
+                    mInstrumentation.startActivitySync(intent);
+                    mInstrumentation.startActivitySync(intent2);
+                },
+                android.Manifest.permission.START_ACTIVITIES_FROM_SDK_SANDBOX);
+        waitLatch(mLatchActivityCached);
+    }
+
+    // Sleep, ignoring interrupts.
+    private void pause(int s) {
+        try {
+            Thread.sleep(s * 1000L);
+        } catch (Exception ignored) { }
+    }
+
+    /**
+     * Freeze the test activity.
+     */
+    protected void makeTestActivityFrozen(int pid) {
+        // The delay here is meant to allow pending binder transactions to drain.  A process
+        // cannot be frozen if it has pending binder transactions, and attempting to freeze such a
+        // process more than a few times will result in the system killing the process.
+        pause(5);
+        try {
+            String cmd = "am freeze --sticky ";
+            SystemUtil.runShellCommand(mInstrumentation, cmd + getTestPackage());
+        } catch (IOException e) {
+            fail(e.toString());
+        }
+        // Wait for the freeze to complete in the kernel and for the frozen process
+        // notification to settle out.
+        waitForProcessFreeze(pid, 5 * 1000);
+    }
+
+    /**
+     * Freeze the test activity.
+     */
+    protected void makeTestActivityUnfrozen(int pid) {
+        try {
+            String cmd = "am unfreeze --sticky ";
+            SystemUtil.runShellCommand(mInstrumentation, cmd + getTestPackage());
+        } catch (IOException e) {
+            fail(e.toString());
+        }
+        // Wait for the freeze to complete in the kernel and for the frozen process
+        // notification to settle out.
+        waitForProcessUnfreeze(pid, 5 * 1000);
+    }
+
+    /**
+     * Wait for CountDownLatch with timeout
+     */
+    private void waitLatch(CountDownLatch latch) {
+        try {
+            latch.await(TEST_FAILURE_TIMEOUT_MSEC, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/services/tests/displayservicetests/src/com/android/server/display/TopologyUpdateDeliveryTest.java b/services/tests/displayservicetests/src/com/android/server/display/TopologyUpdateDeliveryTest.java
new file mode 100644
index 0000000..5fd248d
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/TopologyUpdateDeliveryTest.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import android.content.Intent;
+import android.hardware.display.DisplayTopology;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Tests that applications can receive topology updates correctly.
+ */
+public class TopologyUpdateDeliveryTest extends EventDeliveryTestBase {
+    private static final String TAG = TopologyUpdateDeliveryTest.class.getSimpleName();
+
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+
+    private static final String TEST_PACKAGE = "com.android.servicestests.apps.topologytestapp";
+    private static final String TEST_ACTIVITY = TEST_PACKAGE + ".TopologyUpdateActivity";
+
+    // Topology updates we expect to receive before timeout
+    private final LinkedBlockingQueue<DisplayTopology> mExpectations = new LinkedBlockingQueue<>();
+
+    /**
+     * Add the received topology update from the test activity to the queue
+     *
+     * @param topology The corresponding topology update
+     */
+    private void addTopologyUpdate(DisplayTopology topology) {
+        Log.d(TAG, "Received " + topology);
+        mExpectations.offer(topology);
+    }
+
+    /**
+     * Assert that there isn't any unexpected display event from the test activity
+     */
+    private void assertNoTopologyUpdates() {
+        try {
+            assertNull(mExpectations.poll(EVENT_TIMEOUT_MSEC, TimeUnit.MILLISECONDS));
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Wait for the expected topology update from the test activity
+     *
+     * @param expect The expected topology update
+     */
+    private void waitTopologyUpdate(DisplayTopology expect) {
+        while (true) {
+            try {
+                DisplayTopology update = mExpectations.poll(TEST_FAILURE_TIMEOUT_MSEC,
+                        TimeUnit.MILLISECONDS);
+                assertNotNull(update);
+                if (expect.equals(update)) {
+                    Log.d(TAG, "Found " + update);
+                    return;
+                }
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+
+    private class TestHandler extends Handler {
+        TestHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(@NonNull Message msg) {
+            switch (msg.what) {
+                case MESSAGE_LAUNCHED:
+                    mPid = msg.arg1;
+                    mUid = msg.arg2;
+                    Log.d(TAG, "Launched " + mPid + " " + mUid);
+                    mLatchActivityLaunch.countDown();
+                    break;
+                case MESSAGE_CALLBACK:
+                    DisplayTopology topology = (DisplayTopology) msg.obj;
+                    Log.d(TAG, "Callback " + topology);
+                    addTopologyUpdate(topology);
+                    break;
+                default:
+                    fail("Unexpected value: " + msg.what);
+                    break;
+            }
+        }
+    }
+
+    @Before
+    public void setUp() {
+        super.setUp();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Override
+    protected String getTag() {
+        return TAG;
+    }
+
+    @Override
+    protected Handler getHandler(Looper looper) {
+        return new TestHandler(looper);
+    }
+
+    @Override
+    protected String getTestPackage() {
+        return TEST_PACKAGE;
+    }
+
+    @Override
+    protected String getTestActivity() {
+        return TEST_ACTIVITY;
+    }
+
+    @Override
+    protected void putExtra(Intent intent) { }
+
+    private void testTopologyUpdateInternal(boolean cached, boolean frozen) {
+        Log.d(TAG, "Start test testTopologyUpdate " + cached + " " + frozen);
+        // Launch activity and start listening to topology updates
+        int pid = launchTestActivity();
+
+        // The test activity in cached or frozen mode won't receive the pending topology updates.
+        if (cached) {
+            makeTestActivityCached();
+        }
+        if (frozen) {
+            makeTestActivityFrozen(pid);
+        }
+
+        // Change the topology
+        int primaryDisplayId = 3;
+        DisplayTopology.TreeNode root = new DisplayTopology.TreeNode(primaryDisplayId,
+                /* width= */ 600, /* height= */ 400, DisplayTopology.TreeNode.POSITION_LEFT,
+                /* offset= */ 0);
+        DisplayTopology.TreeNode child = new DisplayTopology.TreeNode(/* displayId= */ 1,
+                /* width= */ 800, /* height= */ 600, DisplayTopology.TreeNode.POSITION_LEFT,
+                /* offset= */ 0);
+        root.addChild(child);
+        DisplayTopology topology = new DisplayTopology(root, primaryDisplayId);
+        mDisplayManager.setDisplayTopology(topology);
+
+        if (cached || frozen) {
+            assertNoTopologyUpdates();
+        } else {
+            waitTopologyUpdate(topology);
+        }
+
+        // Unfreeze the test activity, if it was frozen.
+        if (frozen) {
+            makeTestActivityUnfrozen(pid);
+        }
+
+        if (cached || frozen) {
+            // Always ensure the test activity is not cached.
+            bringTestActivityTop();
+
+            // The test activity becomes non-cached and should receive the pending topology updates
+            waitTopologyUpdate(topology);
+        }
+    }
+
+    @Test
+    @RequiresFlagsEnabled(com.android.server.display.feature.flags.Flags.FLAG_DISPLAY_TOPOLOGY)
+    public void testTopologyUpdate() {
+        testTopologyUpdateInternal(false, false);
+    }
+
+    /**
+     * The app is moved to cached and the test verifies that no updates are delivered to the cached
+     * app.
+     */
+    @Test
+    @RequiresFlagsEnabled(com.android.server.display.feature.flags.Flags.FLAG_DISPLAY_TOPOLOGY)
+    public void testTopologyUpdateCached() {
+        testTopologyUpdateInternal(true, false);
+    }
+
+    /**
+     * The app is frozen and the test verifies that no updates are delivered to the frozen app.
+     */
+    @RequiresFlagsEnabled({com.android.server.am.Flags.FLAG_DEFER_DISPLAY_EVENTS_WHEN_FROZEN,
+            com.android.server.display.feature.flags.Flags.FLAG_DISPLAY_TOPOLOGY})
+    @Test
+    public void testTopologyUpdateFrozen() {
+        assumeTrue(isAppFreezerEnabled());
+        testTopologyUpdateInternal(false, true);
+    }
+
+    /**
+     * The app is cached and frozen and the test verifies that no updates are delivered to the app.
+     */
+    @RequiresFlagsEnabled({com.android.server.am.Flags.FLAG_DEFER_DISPLAY_EVENTS_WHEN_FROZEN,
+            com.android.server.display.feature.flags.Flags.FLAG_DISPLAY_TOPOLOGY})
+    @Test
+    public void testTopologyUpdateCachedFrozen() {
+        assumeTrue(isAppFreezerEnabled());
+        testTopologyUpdateInternal(true, true);
+    }
+}
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/AppRequestObserverTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/AppRequestObserverTest.kt
index 1f3f19f..2187285 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/mode/AppRequestObserverTest.kt
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/AppRequestObserverTest.kt
@@ -89,6 +89,39 @@
         assertThat(renderRateVote).isEqualTo(testCase.expectedRenderRateVote)
     }
 
+    @Test
+    fun testAppRequestVote_externalDisplay() {
+        val displayModeDirector = DisplayModeDirector(
+            context, testHandler, mockInjector, mockFlags, mockDisplayDeviceConfigProvider)
+        val modes = arrayOf(
+            Display.Mode(1, 1000, 1000, 60f),
+            Display.Mode(2, 1000, 1000, 90f),
+        )
+
+        displayModeDirector.injectAppSupportedModesByDisplay(
+            SparseArray<Array<Display.Mode>>().apply {
+                append(Display.DEFAULT_DISPLAY, modes)
+            })
+        displayModeDirector.injectDefaultModeByDisplay(SparseArray<Display.Mode>().apply {
+            append(Display.DEFAULT_DISPLAY, modes[0])
+        })
+        displayModeDirector.addExternalDisplayId(Display.DEFAULT_DISPLAY)
+
+        displayModeDirector.appRequestObserver.setAppRequest(Display.DEFAULT_DISPLAY, 1, 0f, 0f, 0f)
+
+        val baseModeVote = displayModeDirector.getVote(Display.DEFAULT_DISPLAY,
+            Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE)
+        assertThat(baseModeVote).isEqualTo(BaseModeRefreshRateVote(60f))
+
+        val sizeVote = displayModeDirector.getVote(Display.DEFAULT_DISPLAY,
+            Vote.PRIORITY_APP_REQUEST_SIZE)
+        assertThat(sizeVote).isNull()
+
+        val renderRateVote = displayModeDirector.getVote(Display.DEFAULT_DISPLAY,
+            Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE)
+        assertThat(renderRateVote).isNull()
+    }
+
     enum class AppRequestTestCase(
         val ignoreRefreshRateRequest: Boolean,
         val modeId: Int,
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index d702cae..067fba9 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -31,6 +31,7 @@
 
         "test-apps/SuspendTestApp/src/**/*.java",
         "test-apps/DisplayManagerTestApp/src/**/*.java",
+        "test-apps/TopologyTestApp/src/**/*.java",
     ],
 
     static_libs: [
@@ -141,6 +142,7 @@
 
     data: [
         ":DisplayManagerTestApp",
+        ":TopologyTestApp",
         ":SimpleServiceTestApp1",
         ":SimpleServiceTestApp2",
         ":SimpleServiceTestApp3",
diff --git a/services/tests/servicestests/AndroidTest.xml b/services/tests/servicestests/AndroidTest.xml
index 5298251..9a49834 100644
--- a/services/tests/servicestests/AndroidTest.xml
+++ b/services/tests/servicestests/AndroidTest.xml
@@ -36,6 +36,7 @@
         <option name="cleanup-apks" value="true" />
         <option name="install-arg" value="-t" />
         <option name="test-file-name" value="DisplayManagerTestApp.apk" />
+        <option name="test-file-name" value="TopologyTestApp.apk" />
         <option name="test-file-name" value="FrameworksServicesTests.apk" />
         <option name="test-file-name" value="SuspendTestApp.apk" />
         <option name="test-file-name" value="SimpleServiceTestApp1.apk" />
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java
index 99c922c..df77866 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java
@@ -25,6 +25,7 @@
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -866,6 +867,23 @@
 
     @Test
     @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
+    public void scrollPanelController_directionalButtonsHideIndicator() {
+        injectFakeMouseActionHoverMoveEvent();
+
+        // Create a spy on the real object to verify method calls.
+        AutoclickIndicatorView spyIndicatorView = spy(mController.mAutoclickIndicatorView);
+        mController.mAutoclickIndicatorView = spyIndicatorView;
+
+        // Simulate hover on direction button.
+        mController.mScrollPanelController.onHoverButtonChange(
+                AutoclickScrollPanel.DIRECTION_UP, true);
+
+        // Verify clearIndicator was called.
+        verify(spyIndicatorView).clearIndicator();
+    }
+
+    @Test
+    @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
     public void hoverOnAutoclickPanel_rightClickType_forceTriggerLeftClick() {
         MotionEventCaptor motionEventCaptor = new MotionEventCaptor();
         mController.setNext(motionEventCaptor);
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioVolumeChangeHandlerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioVolumeChangeHandlerTest.java
new file mode 100644
index 0000000..f252a98
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioVolumeChangeHandlerTest.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.audio;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.media.INativeAudioVolumeGroupCallback;
+import android.media.audio.common.AudioVolumeGroupChangeEvent;
+import android.media.audiopolicy.IAudioVolumeChangeDispatcher;
+import android.os.IBinder;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@MediumTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class AudioVolumeChangeHandlerTest {
+    private static final long DEFAULT_TIMEOUT_MS = 1000;
+
+    private AudioSystemAdapter mSpyAudioSystem;
+
+    AudioVolumeChangeHandler mAudioVolumeChangedHandler;
+
+    private final IAudioVolumeChangeDispatcher.Stub mMockDispatcher =
+            mock(IAudioVolumeChangeDispatcher.Stub.class);
+
+    @Before
+    public void setUp() {
+        mSpyAudioSystem = spy(new NoOpAudioSystemAdapter());
+        when(mMockDispatcher.asBinder()).thenReturn(mock(IBinder.class));
+        mAudioVolumeChangedHandler = new AudioVolumeChangeHandler(mSpyAudioSystem);
+    }
+
+    @Test
+    public void registerListener_withInvalidCallback() {
+        IAudioVolumeChangeDispatcher.Stub nullCb = null;
+        NullPointerException thrown = assertThrows(NullPointerException.class, () -> {
+            mAudioVolumeChangedHandler.registerListener(nullCb);
+        });
+
+        assertWithMessage("Exception for invalid registration").that(thrown).hasMessageThat()
+                .contains("Volume group callback");
+    }
+
+    @Test
+    public void unregisterListener_withInvalidCallback() {
+        IAudioVolumeChangeDispatcher.Stub nullCb = null;
+        mAudioVolumeChangedHandler.registerListener(mMockDispatcher);
+
+        NullPointerException thrown = assertThrows(NullPointerException.class, () -> {
+            mAudioVolumeChangedHandler.unregisterListener(nullCb);
+        });
+
+        assertWithMessage("Exception for invalid un-registration").that(thrown).hasMessageThat()
+                .contains("Volume group callback");
+    }
+
+    @Test
+    public void registerListener() {
+        mAudioVolumeChangedHandler.registerListener(mMockDispatcher);
+
+        verify(mSpyAudioSystem).registerAudioVolumeGroupCallback(any());
+    }
+
+    @Test
+    public void onAudioVolumeGroupChanged() throws Exception {
+        mAudioVolumeChangedHandler.registerListener(mMockDispatcher);
+        AudioVolumeGroupChangeEvent volEvent = new AudioVolumeGroupChangeEvent();
+        volEvent.groupId = 666;
+        volEvent.flags = AudioVolumeGroupChangeEvent.VOLUME_FLAG_FROM_KEY;
+
+        captureRegisteredNativeCallback().onAudioVolumeGroupChanged(volEvent);
+
+        verify(mMockDispatcher,  timeout(DEFAULT_TIMEOUT_MS)).onAudioVolumeGroupChanged(
+                eq(volEvent.groupId), eq(volEvent.flags));
+    }
+
+    @Test
+    public void onAudioVolumeGroupChanged_withMultipleCallback() throws Exception {
+        int callbackCount = 10;
+        List<IAudioVolumeChangeDispatcher.Stub> validCbs =
+                new ArrayList<IAudioVolumeChangeDispatcher.Stub>();
+        for (int i = 0; i < callbackCount; i++) {
+            IAudioVolumeChangeDispatcher.Stub cb = mock(IAudioVolumeChangeDispatcher.Stub.class);
+            when(cb.asBinder()).thenReturn(mock(IBinder.class));
+            validCbs.add(cb);
+        }
+        for (IAudioVolumeChangeDispatcher.Stub cb : validCbs) {
+            mAudioVolumeChangedHandler.registerListener(cb);
+        }
+        AudioVolumeGroupChangeEvent volEvent = new AudioVolumeGroupChangeEvent();
+        volEvent.groupId = 666;
+        volEvent.flags = AudioVolumeGroupChangeEvent.VOLUME_FLAG_FROM_KEY;
+        captureRegisteredNativeCallback().onAudioVolumeGroupChanged(volEvent);
+
+        for (IAudioVolumeChangeDispatcher.Stub cb : validCbs) {
+            verify(cb,  timeout(DEFAULT_TIMEOUT_MS)).onAudioVolumeGroupChanged(
+                    eq(volEvent.groupId), eq(volEvent.flags));
+        }
+    }
+
+    private INativeAudioVolumeGroupCallback captureRegisteredNativeCallback() {
+        ArgumentCaptor<INativeAudioVolumeGroupCallback> nativeAudioVolumeGroupCallbackCaptor =
+                ArgumentCaptor.forClass(INativeAudioVolumeGroupCallback.class);
+        verify(mSpyAudioSystem, timeout(DEFAULT_TIMEOUT_MS))
+                .registerAudioVolumeGroupCallback(nativeAudioVolumeGroupCallbackCaptor.capture());
+        return nativeAudioVolumeGroupCallbackCaptor.getValue();
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java
index 43b1ec3..87cd156 100644
--- a/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java
+++ b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java
@@ -19,7 +19,9 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.times;
@@ -29,6 +31,7 @@
 import android.content.Context;
 import android.hardware.contexthub.EndpointInfo;
 import android.hardware.contexthub.ErrorCode;
+import android.hardware.contexthub.HubEndpoint;
 import android.hardware.contexthub.HubEndpointInfo;
 import android.hardware.contexthub.HubEndpointInfo.HubEndpointIdentifier;
 import android.hardware.contexthub.HubMessage;
@@ -385,6 +388,49 @@
         unregisterExampleEndpoint(endpoint);
     }
 
+    @Test
+    public void testUnreliableMessageFailureClosesSession() throws RemoteException {
+        IContextHubEndpoint endpoint = registerExampleEndpoint();
+        int sessionId = openTestSession(endpoint);
+
+        doThrow(new RemoteException("Intended exception in test"))
+                .when(mMockCallback)
+                .onMessageReceived(anyInt(), any(HubMessage.class));
+        mEndpointManager.onMessageReceived(sessionId, SAMPLE_UNRELIABLE_MESSAGE);
+        ArgumentCaptor<HubMessage> messageCaptor = ArgumentCaptor.forClass(HubMessage.class);
+        verify(mMockCallback).onMessageReceived(eq(sessionId), messageCaptor.capture());
+        assertThat(messageCaptor.getValue()).isEqualTo(SAMPLE_UNRELIABLE_MESSAGE);
+
+        verify(mMockEndpointCommunications).closeEndpointSession(sessionId, Reason.UNSPECIFIED);
+        verify(mMockCallback).onSessionClosed(sessionId, HubEndpoint.REASON_FAILURE);
+        assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE);
+
+        unregisterExampleEndpoint(endpoint);
+    }
+
+    @Test
+    public void testSendUnreliableMessageFailureClosesSession() throws RemoteException {
+        IContextHubEndpoint endpoint = registerExampleEndpoint();
+        int sessionId = openTestSession(endpoint);
+
+        doThrow(new RemoteException("Intended exception in test"))
+                .when(mMockEndpointCommunications)
+                .sendMessageToEndpoint(anyInt(), any(Message.class));
+        endpoint.sendMessage(sessionId, SAMPLE_UNRELIABLE_MESSAGE, /* callback= */ null);
+        ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
+        verify(mMockEndpointCommunications)
+                .sendMessageToEndpoint(eq(sessionId), messageCaptor.capture());
+        Message message = messageCaptor.getValue();
+        assertThat(message.type).isEqualTo(SAMPLE_UNRELIABLE_MESSAGE.getMessageType());
+        assertThat(message.content).isEqualTo(SAMPLE_UNRELIABLE_MESSAGE.getMessageBody());
+
+        verify(mMockEndpointCommunications).closeEndpointSession(sessionId, Reason.UNSPECIFIED);
+        verify(mMockCallback).onSessionClosed(sessionId, HubEndpoint.REASON_FAILURE);
+        assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE);
+
+        unregisterExampleEndpoint(endpoint);
+    }
+
     /** A helper method to create a session and validates reliable message sending. */
     private void testMessageTransactionInternal(
             IContextHubEndpoint endpoint, boolean deliverMessageStatus) throws RemoteException {
diff --git a/services/tests/servicestests/src/com/android/server/statusbar/StatusBarManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/statusbar/StatusBarManagerServiceTest.java
index 148c968..6d682cc 100644
--- a/services/tests/servicestests/src/com/android/server/statusbar/StatusBarManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/statusbar/StatusBarManagerServiceTest.java
@@ -36,6 +36,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.nullable;
@@ -69,16 +70,20 @@
 import android.os.Looper;
 import android.os.RemoteException;
 import android.os.UserHandle;
+import android.platform.test.annotations.EnableFlags;
 import android.service.quicksettings.TileService;
 import android.testing.TestableContext;
+import android.util.Pair;
 
 import androidx.test.InstrumentationRegistry;
 
+import com.android.internal.statusbar.DisableStates;
 import com.android.internal.statusbar.IAddTileResultCallback;
 import com.android.internal.statusbar.IStatusBar;
 import com.android.server.LocalServices;
 import com.android.server.policy.GlobalActionsProvider;
 import com.android.server.wm.ActivityTaskManagerInternal;
+import com.android.systemui.shared.Flags;
 
 import libcore.junit.util.compat.CoreCompatChangeRule;
 
@@ -105,6 +110,7 @@
             TEST_SERVICE);
     private static final CharSequence APP_NAME = "AppName";
     private static final CharSequence TILE_LABEL = "Tile label";
+    private static final int SECONDARY_DISPLAY_ID = 2;
 
     @Rule
     public final TestableContext mContext =
@@ -749,6 +755,40 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_STATUS_BAR_CONNECTED_DISPLAYS)
+    public void testDisableForAllDisplays() throws Exception {
+        int user1Id = 0;
+        mockUidCheck();
+        mockCurrentUserCheck(user1Id);
+
+        mStatusBarManagerService.onDisplayAdded(SECONDARY_DISPLAY_ID);
+
+        int expectedFlags = DISABLE_MASK & DISABLE_BACK;
+        String pkg = mContext.getPackageName();
+
+        // before disabling
+        assertEquals(DISABLE_NONE,
+                mStatusBarManagerService.getDisableFlags(mMockStatusBar, user1Id)[0]);
+
+        // disable
+        mStatusBarManagerService.disable(expectedFlags, mMockStatusBar, pkg);
+
+        ArgumentCaptor<DisableStates> disableStatesCaptor = ArgumentCaptor.forClass(
+                DisableStates.class);
+        verify(mMockStatusBar).disableForAllDisplays(disableStatesCaptor.capture());
+        DisableStates capturedDisableStates = disableStatesCaptor.getValue();
+        assertTrue(capturedDisableStates.animate);
+        assertEquals(capturedDisableStates.displaysWithStates.size(), 2);
+        Pair<Integer, Integer> display0States = capturedDisableStates.displaysWithStates.get(0);
+        assertEquals((int) display0States.first, expectedFlags);
+        assertEquals((int) display0States.second, 0);
+        Pair<Integer, Integer> display2States = capturedDisableStates.displaysWithStates.get(
+                SECONDARY_DISPLAY_ID);
+        assertEquals((int) display2States.first, expectedFlags);
+        assertEquals((int) display2States.second, 0);
+    }
+
+    @Test
     public void testSetHomeDisabled() throws Exception {
         int expectedFlags = DISABLE_MASK & DISABLE_HOME;
         String pkg = mContext.getPackageName();
@@ -851,6 +891,40 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_STATUS_BAR_CONNECTED_DISPLAYS)
+    public void testDisable2ForAllDisplays() throws Exception {
+        int user1Id = 0;
+        mockUidCheck();
+        mockCurrentUserCheck(user1Id);
+
+        mStatusBarManagerService.onDisplayAdded(SECONDARY_DISPLAY_ID);
+
+        int expectedFlags = DISABLE2_MASK & DISABLE2_NOTIFICATION_SHADE;
+        String pkg = mContext.getPackageName();
+
+        // before disabling
+        assertEquals(DISABLE_NONE,
+                mStatusBarManagerService.getDisableFlags(mMockStatusBar, user1Id)[0]);
+
+        // disable
+        mStatusBarManagerService.disable2(expectedFlags, mMockStatusBar, pkg);
+
+        ArgumentCaptor<DisableStates> disableStatesCaptor = ArgumentCaptor.forClass(
+                DisableStates.class);
+        verify(mMockStatusBar).disableForAllDisplays(disableStatesCaptor.capture());
+        DisableStates capturedDisableStates = disableStatesCaptor.getValue();
+        assertTrue(capturedDisableStates.animate);
+        assertEquals(capturedDisableStates.displaysWithStates.size(), 2);
+        Pair<Integer, Integer> display0States = capturedDisableStates.displaysWithStates.get(0);
+        assertEquals((int) display0States.first, 0);
+        assertEquals((int) display0States.second, expectedFlags);
+        Pair<Integer, Integer> display2States = capturedDisableStates.displaysWithStates.get(
+                SECONDARY_DISPLAY_ID);
+        assertEquals((int) display2States.first, 0);
+        assertEquals((int) display2States.second, expectedFlags);
+    }
+
+    @Test
     public void testSetQuickSettingsDisabled2() throws Exception {
         int expectedFlags = DISABLE2_MASK & DISABLE2_QUICK_SETTINGS;
         String pkg = mContext.getPackageName();
diff --git a/services/tests/servicestests/test-apps/TopologyTestApp/Android.bp b/services/tests/servicestests/test-apps/TopologyTestApp/Android.bp
new file mode 100644
index 0000000..dcf9cc21
--- /dev/null
+++ b/services/tests/servicestests/test-apps/TopologyTestApp/Android.bp
@@ -0,0 +1,38 @@
+// Copyright (C) 2025 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 {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test_helper_app {
+    name: "TopologyTestApp",
+
+    srcs: ["**/*.java"],
+
+    dex_preopt: {
+        enabled: false,
+    },
+    optimize: {
+        enabled: false,
+    },
+
+    platform_apis: true,
+    certificate: "platform",
+}
diff --git a/services/tests/servicestests/test-apps/TopologyTestApp/AndroidManifest.xml b/services/tests/servicestests/test-apps/TopologyTestApp/AndroidManifest.xml
new file mode 100644
index 0000000..dad2315
--- /dev/null
+++ b/services/tests/servicestests/test-apps/TopologyTestApp/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2025 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.servicestests.apps.topologytestapp">
+
+    <uses-permission android:name="android.permission.MANAGE_DISPLAYS" />
+
+    <application android:label="TopologyUpdateTestApp">
+        <activity android:name="com.android.servicestests.apps.topologytestapp.TopologyUpdateActivity"
+                  android:exported="true" />
+    </application>
+
+</manifest>
diff --git a/services/tests/servicestests/test-apps/TopologyTestApp/OWNERS b/services/tests/servicestests/test-apps/TopologyTestApp/OWNERS
new file mode 100644
index 0000000..e9557f8
--- /dev/null
+++ b/services/tests/servicestests/test-apps/TopologyTestApp/OWNERS
@@ -0,0 +1,3 @@
+# Bug component: 345010
+
+include /services/core/java/com/android/server/display/OWNERS
diff --git a/services/tests/servicestests/test-apps/TopologyTestApp/src/com/android/servicestests/apps/topologytestapp/TopologyUpdateActivity.java b/services/tests/servicestests/test-apps/TopologyTestApp/src/com/android/servicestests/apps/topologytestapp/TopologyUpdateActivity.java
new file mode 100644
index 0000000..b35ba3c
--- /dev/null
+++ b/services/tests/servicestests/test-apps/TopologyTestApp/src/com/android/servicestests/apps/topologytestapp/TopologyUpdateActivity.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2025 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.servicestests.apps.topologytestapp;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.DisplayTopology;
+import android.os.Bundle;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.Process;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.util.function.Consumer;
+
+/**
+ * A simple activity listening to topology updates
+ */
+public class TopologyUpdateActivity extends Activity {
+    public static final int MESSAGE_LAUNCHED = 1;
+    public static final int MESSAGE_CALLBACK = 2;
+
+    private static final String TAG = TopologyUpdateActivity.class.getSimpleName();
+
+    private static final String TEST_MESSENGER = "MESSENGER";
+
+    private Messenger mMessenger;
+    private DisplayManager mDisplayManager;
+    private final Consumer<DisplayTopology> mTopologyListener = this::callback;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        Intent intent = getIntent();
+        mMessenger = intent.getParcelableExtra(TEST_MESSENGER, Messenger.class);
+        mDisplayManager = getApplicationContext().getSystemService(DisplayManager.class);
+        mDisplayManager.registerTopologyListener(getMainExecutor(), mTopologyListener);
+        launched();
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        mDisplayManager.unregisterTopologyListener(mTopologyListener);
+    }
+
+    private void launched() {
+        try {
+            Message msg = Message.obtain();
+            msg.what = MESSAGE_LAUNCHED;
+            msg.arg1 = android.os.Process.myPid();
+            msg.arg2 = Process.myUid();
+            Log.d(TAG, "Launched");
+            mMessenger.send(msg);
+        } catch (RemoteException e) {
+            e.rethrowAsRuntimeException();
+        }
+    }
+
+    private void callback(DisplayTopology topology) {
+        try {
+            Message msg = Message.obtain();
+            msg.what = MESSAGE_CALLBACK;
+            msg.obj = topology;
+            Log.d(TAG, "Msg " + topology);
+            mMessenger.send(msg);
+        } catch (RemoteException e) {
+            e.rethrowAsRuntimeException();
+        }
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java
index fcdf88f..0495e96 100644
--- a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java
@@ -39,8 +39,6 @@
 import com.android.hardware.input.Flags;
 import com.android.internal.annotations.Keep;
 
-import junit.framework.Assert;
-
 import junitparams.JUnitParamsRunner;
 import junitparams.Parameters;
 
@@ -433,112 +431,94 @@
 
     @Test
     public void testKeyGestureRecentApps() {
-        Assert.assertTrue(
-                sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS));
+        sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS);
         mPhoneWindowManager.assertShowRecentApps();
     }
 
     @Test
     public void testKeyGestureAppSwitch() {
-        Assert.assertTrue(
-                sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH));
+        sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH);
         mPhoneWindowManager.assertToggleRecentApps();
     }
 
     @Test
     public void testKeyGestureLaunchAssistant() {
-        Assert.assertTrue(
-                sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT));
+        sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT);
         mPhoneWindowManager.assertSearchManagerLaunchAssist();
     }
 
     @Test
     public void testKeyGestureLaunchVoiceAssistant() {
-        Assert.assertTrue(
-                sendKeyGestureEventComplete(
-                        KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT));
+        sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT);
         mPhoneWindowManager.assertSearchManagerLaunchAssist();
     }
 
     @Test
     public void testKeyGestureGoHome() {
-        Assert.assertTrue(
-                sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_HOME));
+        sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_HOME);
         mPhoneWindowManager.assertGoToHomescreen();
     }
 
     @Test
     public void testKeyGestureLaunchSystemSettings() {
-        Assert.assertTrue(
-                sendKeyGestureEventComplete(
-                        KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS));
+        sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS);
         mPhoneWindowManager.assertLaunchSystemSettings();
     }
 
     @Test
     public void testKeyGestureLock() {
-        Assert.assertTrue(
-                sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN));
+        sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN);
         mPhoneWindowManager.assertLockedAfterAppTransitionFinished();
     }
 
     @Test
     public void testKeyGestureToggleNotificationPanel() throws RemoteException {
-        Assert.assertTrue(
-                sendKeyGestureEventComplete(
-                        KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL));
+        sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL);
         mPhoneWindowManager.assertTogglePanel();
     }
 
     @Test
     public void testKeyGestureScreenshot() {
-        Assert.assertTrue(
-                sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT));
+        sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT);
         mPhoneWindowManager.assertTakeScreenshotCalled();
     }
 
     @Test
     public void testKeyGestureTriggerBugReport() throws RemoteException {
-        Assert.assertTrue(
-                sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TRIGGER_BUG_REPORT));
+        sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TRIGGER_BUG_REPORT);
         mPhoneWindowManager.assertTakeBugreport(true);
     }
 
     @Test
     public void testKeyGestureBack() {
-        Assert.assertTrue(sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_BACK));
+        sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_BACK);
         mPhoneWindowManager.assertBackEventInjected();
     }
 
     @Test
     public void testKeyGestureMultiWindowNavigation() {
-        Assert.assertTrue(sendKeyGestureEventComplete(
-                KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION));
+        sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION);
         mPhoneWindowManager.assertMoveFocusedTaskToFullscreen();
     }
 
     @Test
     public void testKeyGestureDesktopMode() {
-        Assert.assertTrue(
-                sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_DESKTOP_MODE));
+        sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_DESKTOP_MODE);
         mPhoneWindowManager.assertMoveFocusedTaskToDesktop();
     }
 
     @Test
     public void testKeyGestureSplitscreenNavigation() {
-        Assert.assertTrue(sendKeyGestureEventComplete(
-                KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT));
+        sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT);
         mPhoneWindowManager.assertMoveFocusedTaskToStageSplit(true);
 
-        Assert.assertTrue(sendKeyGestureEventComplete(
-                KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT));
+        sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT);
         mPhoneWindowManager.assertMoveFocusedTaskToStageSplit(false);
     }
 
     @Test
     public void testKeyGestureShortcutHelper() {
-        Assert.assertTrue(sendKeyGestureEventComplete(
-                KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER));
+        sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER);
         mPhoneWindowManager.assertToggleShortcutsMenu();
     }
 
@@ -549,173 +529,139 @@
 
         for (int i = 0; i < currentBrightness.length; i++) {
             mPhoneWindowManager.prepareBrightnessDecrease(currentBrightness[i]);
-            Assert.assertTrue(
-                    sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_DOWN));
+            sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_DOWN);
             mPhoneWindowManager.verifyNewBrightness(newBrightness[i]);
         }
     }
 
     @Test
     public void testKeyGestureRecentAppSwitcher() {
-        Assert.assertTrue(sendKeyGestureEventStart(
-                KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER));
+        sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER);
         mPhoneWindowManager.assertShowRecentApps();
-
-        Assert.assertTrue(sendKeyGestureEventComplete(
-                KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER));
+        sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER);
         mPhoneWindowManager.assertHideRecentApps();
     }
 
     @Test
     public void testKeyGestureLanguageSwitch() {
-        Assert.assertTrue(
-                sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH));
+        sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH);
         mPhoneWindowManager.assertSwitchKeyboardLayout(1, DEFAULT_DISPLAY);
 
-        Assert.assertTrue(
-                sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH,
-                        KeyEvent.META_SHIFT_ON));
+        sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH,
+                KeyEvent.META_SHIFT_ON);
         mPhoneWindowManager.assertSwitchKeyboardLayout(-1, DEFAULT_DISPLAY);
     }
 
     @Test
     public void testKeyGestureLaunchSearch() {
-        Assert.assertTrue(
-                sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH));
+        sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH);
         mPhoneWindowManager.assertLaunchSearch();
     }
 
     @Test
     public void testKeyGestureScreenshotChord() {
-        Assert.assertTrue(
-                sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD));
+        sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD);
         mPhoneWindowManager.moveTimeForward(500);
-        Assert.assertTrue(
-                sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD));
+        sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD);
         mPhoneWindowManager.assertTakeScreenshotCalled();
     }
 
     @Test
     public void testKeyGestureScreenshotChordCancelled() {
-        Assert.assertTrue(
-                sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD));
-        Assert.assertTrue(
-                sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD));
+        sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD);
+        sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD);
         mPhoneWindowManager.assertTakeScreenshotNotCalled();
     }
 
     @Test
     public void testKeyGestureRingerToggleChord() {
         mPhoneWindowManager.overridePowerVolumeUp(POWER_VOLUME_UP_BEHAVIOR_MUTE);
-        Assert.assertTrue(
-                sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD));
+        sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD);
         mPhoneWindowManager.moveTimeForward(500);
-        Assert.assertTrue(
-                sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD));
+        sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD);
         mPhoneWindowManager.assertVolumeMute();
     }
 
     @Test
     public void testKeyGestureRingerToggleChordCancelled() {
         mPhoneWindowManager.overridePowerVolumeUp(POWER_VOLUME_UP_BEHAVIOR_MUTE);
-        Assert.assertTrue(
-                sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD));
-        Assert.assertTrue(
-                sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD));
+        sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD);
+        sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD);
         mPhoneWindowManager.assertVolumeNotMuted();
     }
 
     @Test
     public void testKeyGestureGlobalAction() {
         mPhoneWindowManager.overridePowerVolumeUp(POWER_VOLUME_UP_BEHAVIOR_GLOBAL_ACTIONS);
-        Assert.assertTrue(
-                sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS));
+        sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS);
         mPhoneWindowManager.moveTimeForward(500);
-        Assert.assertTrue(
-                sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS));
+        sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS);
         mPhoneWindowManager.assertShowGlobalActionsCalled();
     }
 
     @Test
     public void testKeyGestureGlobalActionCancelled() {
         mPhoneWindowManager.overridePowerVolumeUp(POWER_VOLUME_UP_BEHAVIOR_GLOBAL_ACTIONS);
-        Assert.assertTrue(
-                sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS));
-        Assert.assertTrue(
-                sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS));
+        sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS);
+        sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS);
         mPhoneWindowManager.assertShowGlobalActionsNotCalled();
     }
 
     @Test
     public void testKeyGestureTvTriggerBugReport() {
-        Assert.assertTrue(
-                sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT));
+        sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT);
         mPhoneWindowManager.moveTimeForward(1000);
-        Assert.assertTrue(
-                sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT));
+        sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT);
         mPhoneWindowManager.assertBugReportTakenForTv();
     }
 
     @Test
     public void testKeyGestureTvTriggerBugReportCancelled() {
-        Assert.assertTrue(
-                sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT));
-        Assert.assertTrue(
-                sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT));
+        sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT);
+        sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT);
         mPhoneWindowManager.assertBugReportNotTakenForTv();
     }
 
     @Test
     public void testKeyGestureAccessibilityShortcut() {
-        Assert.assertTrue(
-                sendKeyGestureEventComplete(
-                        KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT));
+        sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT);
         mPhoneWindowManager.assertAccessibilityKeychordCalled();
     }
 
     @Test
     public void testKeyGestureCloseAllDialogs() {
-        Assert.assertTrue(
-                sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS));
+        sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS);
         mPhoneWindowManager.assertCloseAllDialogs();
     }
 
     @Test
     @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES)
     public void testKeyGestureToggleTalkback() {
-        Assert.assertTrue(
-                sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_TALKBACK));
+        sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_TALKBACK);
         mPhoneWindowManager.assertTalkBack(true);
 
-        Assert.assertTrue(
-                sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_TALKBACK));
+        sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_TALKBACK);
         mPhoneWindowManager.assertTalkBack(false);
     }
 
     @Test
     @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_VOICE_ACCESS_KEY_GESTURES)
     public void testKeyGestureToggleVoiceAccess() {
-        Assert.assertTrue(
-                sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS));
+        sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS);
         mPhoneWindowManager.assertVoiceAccess(true);
 
-        Assert.assertTrue(
-                sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS));
+        sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS);
         mPhoneWindowManager.assertVoiceAccess(false);
     }
 
     @Test
     public void testKeyGestureToggleDoNotDisturb() {
         mPhoneWindowManager.overrideZenMode(Settings.Global.ZEN_MODE_OFF);
-        Assert.assertTrue(
-                sendKeyGestureEventComplete(
-                        KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_DO_NOT_DISTURB));
+        sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_DO_NOT_DISTURB);
         mPhoneWindowManager.assertZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS);
 
         mPhoneWindowManager.overrideZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS);
-        Assert.assertTrue(
-                sendKeyGestureEventComplete(
-                        KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_DO_NOT_DISTURB));
+        sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_DO_NOT_DISTURB);
         mPhoneWindowManager.assertZenMode(Settings.Global.ZEN_MODE_OFF);
     }
 
diff --git a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java
index 32a3b7f..f3d5e39 100644
--- a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java
@@ -35,6 +35,7 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
 import static com.android.server.policy.PhoneWindowManager.EXTRA_TRIGGER_HUB;
+import static com.android.server.policy.PhoneWindowManager.SHORT_PRESS_POWER_DREAM_OR_AWAKE_OR_SLEEP;
 import static com.android.server.policy.PhoneWindowManager.SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -43,6 +44,7 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyList;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 
@@ -63,6 +65,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.util.test.LocalServiceKeeperRule;
+import com.android.internal.widget.LockPatternUtils;
 import com.android.server.input.InputManagerInternal;
 import com.android.server.pm.UserManagerInternal;
 import com.android.server.policy.keyguard.KeyguardServiceDelegate;
@@ -119,6 +122,8 @@
     private DisplayPolicy mDisplayPolicy;
     @Mock
     private KeyguardServiceDelegate mKeyguardServiceDelegate;
+    @Mock
+    private LockPatternUtils mLockPatternUtils;
 
     @Before
     public void setUp() {
@@ -146,7 +151,7 @@
 
         mPhoneWindowManager.mKeyguardDelegate = mKeyguardServiceDelegate;
         final InputManager im = mock(InputManager.class);
-        doNothing().when(im).registerKeyGestureEventHandler(any());
+        doNothing().when(im).registerKeyGestureEventHandler(anyList(), any());
         doReturn(im).when(mContext).getSystemService(eq(Context.INPUT_SERVICE));
     }
 
@@ -253,6 +258,7 @@
     @Test
     public void powerPress_hubOrDreamOrSleep_goesToSleepFromDream() {
         when(mDisplayPolicy.isAwake()).thenReturn(true);
+        when(mLockPatternUtils.isLockScreenDisabled(anyInt())).thenReturn(false);
         initPhoneWindowManager();
 
         // Set power button behavior.
@@ -274,6 +280,7 @@
     @Test
     public void powerPress_hubOrDreamOrSleep_hubAvailableLocks() {
         when(mDisplayPolicy.isAwake()).thenReturn(true);
+        when(mLockPatternUtils.isLockScreenDisabled(anyInt())).thenReturn(false);
         mContext.getTestablePermissions().setPermission(android.Manifest.permission.DEVICE_POWER,
                 PERMISSION_GRANTED);
         initPhoneWindowManager();
@@ -302,6 +309,7 @@
     @Test
     public void powerPress_hubOrDreamOrSleep_hubNotAvailableDreams() {
         when(mDisplayPolicy.isAwake()).thenReturn(true);
+        when(mLockPatternUtils.isLockScreenDisabled(anyInt())).thenReturn(false);
         initPhoneWindowManager();
 
         // Set power button behavior.
@@ -322,6 +330,77 @@
         verify(mDreamManagerInternal).requestDream();
     }
 
+    @Test
+    public void powerPress_dreamOrAwakeOrSleep_awakeFromDream() {
+        when(mDisplayPolicy.isAwake()).thenReturn(true);
+        initPhoneWindowManager();
+
+        // Set power button behavior.
+        Settings.Global.putInt(mContext.getContentResolver(),
+                Settings.Global.POWER_BUTTON_SHORT_PRESS,
+                SHORT_PRESS_POWER_DREAM_OR_AWAKE_OR_SLEEP);
+        mPhoneWindowManager.updateSettings(null);
+
+        // Can not dream when device is dreaming.
+        when(mDreamManagerInternal.canStartDreaming(any(Boolean.class))).thenReturn(false);
+        // Device is dreaming.
+        when(mDreamManagerInternal.isDreaming()).thenReturn(true);
+
+        // Power button pressed.
+        int eventTime = 0;
+        mPhoneWindowManager.powerPress(eventTime, 1, 0);
+
+        // Dream is stopped.
+        verify(mDreamManagerInternal)
+                .stopDream(false /*immediate*/, "short press power" /*reason*/);
+    }
+
+    @Test
+    public void powerPress_dreamOrAwakeOrSleep_canNotDreamGoToSleep() {
+        when(mDisplayPolicy.isAwake()).thenReturn(true);
+        initPhoneWindowManager();
+
+        // Set power button behavior.
+        Settings.Global.putInt(mContext.getContentResolver(),
+                Settings.Global.POWER_BUTTON_SHORT_PRESS,
+                SHORT_PRESS_POWER_DREAM_OR_AWAKE_OR_SLEEP);
+        mPhoneWindowManager.updateSettings(null);
+
+        // Can not dream for other reasons.
+        when(mDreamManagerInternal.canStartDreaming(any(Boolean.class))).thenReturn(false);
+        // Device is not dreaming.
+        when(mDreamManagerInternal.isDreaming()).thenReturn(false);
+
+        // Power button pressed.
+        int eventTime = 0;
+        mPhoneWindowManager.powerPress(eventTime, 1, 0);
+
+        // Device goes to sleep.
+        verify(mPowerManager).goToSleep(eventTime, PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON, 0);
+    }
+
+    @Test
+    public void powerPress_dreamOrAwakeOrSleep_dreamFromActive() {
+        when(mDisplayPolicy.isAwake()).thenReturn(true);
+        initPhoneWindowManager();
+
+        // Set power button behavior.
+        Settings.Global.putInt(mContext.getContentResolver(),
+                Settings.Global.POWER_BUTTON_SHORT_PRESS,
+                SHORT_PRESS_POWER_DREAM_OR_AWAKE_OR_SLEEP);
+        mPhoneWindowManager.updateSettings(null);
+
+        // Can dream when active.
+        when(mDreamManagerInternal.canStartDreaming(any(Boolean.class))).thenReturn(true);
+
+        // Power button pressed.
+        int eventTime = 0;
+        mPhoneWindowManager.powerPress(eventTime, 1, 0);
+
+        // Dream is requested.
+        verify(mDreamManagerInternal).requestDream();
+    }
+
     private void initPhoneWindowManager() {
         mPhoneWindowManager.mDefaultDisplayPolicy = mDisplayPolicy;
         mPhoneWindowManager.mDefaultDisplayRotation = mock(DisplayRotation.class);
@@ -345,6 +424,11 @@
             return mKeyguardServiceDelegate;
         }
 
+        @Override
+        LockPatternUtils getLockPatternUtils() {
+            return mLockPatternUtils;
+        }
+
         /**
          * {@code WindowWakeUpPolicy} registers a local service in its constructor, easier to just
          * mock it out so we don't have to unregister it after every test.
diff --git a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
index c57adfd..f89c6f6 100644
--- a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
+++ b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
@@ -238,33 +238,33 @@
         sendKeyCombination(new int[]{keyCode}, durationMillis, false, DEFAULT_DISPLAY);
     }
 
-    boolean sendKeyGestureEventStart(int gestureType) {
-        return mPhoneWindowManager.sendKeyGestureEvent(
+    void sendKeyGestureEventStart(int gestureType) {
+        mPhoneWindowManager.sendKeyGestureEvent(
                 new KeyGestureEvent.Builder().setKeyGestureType(gestureType).setAction(
                         KeyGestureEvent.ACTION_GESTURE_START).build());
     }
 
-    boolean sendKeyGestureEventComplete(int gestureType) {
-        return mPhoneWindowManager.sendKeyGestureEvent(
+    void sendKeyGestureEventComplete(int gestureType) {
+        mPhoneWindowManager.sendKeyGestureEvent(
                 new KeyGestureEvent.Builder().setKeyGestureType(gestureType).setAction(
                         KeyGestureEvent.ACTION_GESTURE_COMPLETE).build());
     }
 
-    boolean sendKeyGestureEventCancel(int gestureType) {
-        return mPhoneWindowManager.sendKeyGestureEvent(
+    void sendKeyGestureEventCancel(int gestureType) {
+        mPhoneWindowManager.sendKeyGestureEvent(
                 new KeyGestureEvent.Builder().setKeyGestureType(gestureType).setAction(
                         KeyGestureEvent.ACTION_GESTURE_COMPLETE).setFlags(
                         KeyGestureEvent.FLAG_CANCELLED).build());
     }
 
-    boolean sendKeyGestureEventComplete(int gestureType, int modifierState) {
-        return mPhoneWindowManager.sendKeyGestureEvent(
+    void sendKeyGestureEventComplete(int gestureType, int modifierState) {
+        mPhoneWindowManager.sendKeyGestureEvent(
                 new KeyGestureEvent.Builder().setModifierState(modifierState).setKeyGestureType(
                         gestureType).setAction(KeyGestureEvent.ACTION_GESTURE_COMPLETE).build());
     }
 
-    boolean sendKeyGestureEventComplete(int keycode, int modifierState, int gestureType) {
-        return mPhoneWindowManager.sendKeyGestureEvent(
+    void sendKeyGestureEventComplete(int keycode, int modifierState, int gestureType) {
+        mPhoneWindowManager.sendKeyGestureEvent(
                 new KeyGestureEvent.Builder().setKeycodes(new int[]{keycode}).setModifierState(
                         modifierState).setKeyGestureType(gestureType).setAction(
                         KeyGestureEvent.ACTION_GESTURE_COMPLETE).build());
diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
index e56fd3c..7b6d361 100644
--- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
+++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
@@ -49,6 +49,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyList;
 import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.CALLS_REAL_METHODS;
 import static org.mockito.Mockito.after;
@@ -353,7 +354,7 @@
         doReturn(mAppOpsManager).when(mContext).getSystemService(eq(AppOpsManager.class));
         doReturn(mDisplayManager).when(mContext).getSystemService(eq(DisplayManager.class));
         doReturn(mInputManager).when(mContext).getSystemService(eq(InputManager.class));
-        doNothing().when(mInputManager).registerKeyGestureEventHandler(any());
+        doNothing().when(mInputManager).registerKeyGestureEventHandler(anyList(), any());
         doNothing().when(mInputManager).unregisterKeyGestureEventHandler(any());
         doReturn(mPackageManager).when(mContext).getPackageManager();
         doReturn(mSensorPrivacyManager).when(mContext).getSystemService(
@@ -476,8 +477,8 @@
         mPhoneWindowManager.interceptUnhandledKey(event, mInputToken);
     }
 
-    boolean sendKeyGestureEvent(KeyGestureEvent event) {
-        return mPhoneWindowManager.handleKeyGestureEvent(event, mInputToken);
+    void sendKeyGestureEvent(KeyGestureEvent event) {
+        mPhoneWindowManager.handleKeyGestureEvent(event, mInputToken);
     }
 
     /**
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
index 044aacc..b617f02 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
@@ -100,8 +100,6 @@
 import com.android.modules.utils.TypedXmlPullParser;
 import com.android.modules.utils.TypedXmlSerializer;
 
-import libcore.junit.util.compat.CoreCompatChangeRule;
-
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Rule;
@@ -414,79 +412,96 @@
     }
 
     @Test
-    @CoreCompatChangeRule.EnableCompatChanges({ActivityInfo.FORCE_RESIZE_APP})
-    public void testIsResizeable_nonResizeable_forceResize_overridesEnabled_Resizeable() {
+    public void testIsResizeable_nonResizeable_forceResize_overridesEnabled_resizeable() {
         final Task task = new TaskBuilder(mSupervisor)
                 .setCreateActivity(true)
-                .setComponent(
-                        ComponentName.createRelative(mContext, SizeCompatTests.class.getName()))
                 .build();
         task.setResizeMode(RESIZE_MODE_UNRESIZEABLE);
+        final ActivityRecord activity = task.getRootActivity();
+        final AppCompatResizeOverrides resizeOverrides =
+                activity.mAppCompatController.getResizeOverrides();
+        spyOn(activity);
+        spyOn(resizeOverrides);
+        doReturn(true).when(resizeOverrides).shouldOverrideForceResizeApp();
+        task.intent = null;
+        task.setIntent(activity);
         // Override should take effect and task should be resizeable.
         assertTrue(task.getTaskInfo().isResizeable);
     }
 
     @Test
-    @CoreCompatChangeRule.EnableCompatChanges({ActivityInfo.FORCE_RESIZE_APP})
-    public void testIsResizeable_nonResizeable_forceResize_overridesDisabled_nonResizeable() {
-        final Task task = new TaskBuilder(mSupervisor)
-                .setCreateActivity(true)
-                .setComponent(
-                        ComponentName.createRelative(mContext, SizeCompatTests.class.getName()))
-                .build();
-        task.setResizeMode(RESIZE_MODE_UNRESIZEABLE);
-
-        // Disallow resize overrides.
-        task.mAllowForceResizeOverride = false;
-
-        // Override should not take effect and task should be un-resizeable.
-        assertFalse(task.getTaskInfo().isResizeable);
-    }
-
-    @Test
-    @CoreCompatChangeRule.EnableCompatChanges({ActivityInfo.FORCE_NON_RESIZE_APP})
     public void testIsResizeable_resizeable_forceNonResize_overridesEnabled_nonResizeable() {
         final Task task = new TaskBuilder(mSupervisor)
                 .setCreateActivity(true)
-                .setComponent(
-                        ComponentName.createRelative(mContext, SizeCompatTests.class.getName()))
                 .build();
         task.setResizeMode(RESIZE_MODE_RESIZEABLE);
+        final ActivityRecord activity = task.getRootActivity();
+        final AppCompatResizeOverrides resizeOverrides =
+                activity.mAppCompatController.getResizeOverrides();
+        spyOn(activity);
+        spyOn(resizeOverrides);
+        doReturn(true).when(resizeOverrides).shouldOverrideForceNonResizeApp();
+        task.intent = null;
+        task.setIntent(activity);
 
         // Override should take effect and task should be un-resizeable.
         assertFalse(task.getTaskInfo().isResizeable);
     }
 
     @Test
-    @CoreCompatChangeRule.EnableCompatChanges({ActivityInfo.FORCE_NON_RESIZE_APP})
-    public void testIsResizeable_resizeable_forceNonResize_overridesDisabled_Resizeable() {
+    public void testIsResizeable_resizeableTask_fullscreenOverride_resizeable() {
         final Task task = new TaskBuilder(mSupervisor)
                 .setCreateActivity(true)
-                .setComponent(
-                        ComponentName.createRelative(mContext, SizeCompatTests.class.getName()))
                 .build();
-        task.setResizeMode(RESIZE_MODE_RESIZEABLE);
+        task.setResizeMode(RESIZE_MODE_UNRESIZEABLE);
+        final ActivityRecord activity = task.getRootActivity();
+        final AppCompatAspectRatioOverrides aspectRatioOverrides =
+                activity.mAppCompatController.getAspectRatioOverrides();
+        spyOn(aspectRatioOverrides);
+        doReturn(true).when(aspectRatioOverrides).hasFullscreenOverride();
+        task.intent = null;
+        task.setIntent(activity);
 
-        // Disallow resize overrides.
-        task.mAllowForceResizeOverride = false;
-
-        // Override should not take effect and task should be resizeable.
+        // Override should take effect and task should be resizeable.
         assertTrue(task.getTaskInfo().isResizeable);
     }
 
     @Test
-    @CoreCompatChangeRule.EnableCompatChanges({ActivityInfo.FORCE_NON_RESIZE_APP})
-    public void testIsResizeable_systemWideForceResize_compatForceNonResize__Resizeable() {
+    public void testIsResizeable_resizeableTask_universalResizeable_resizeable() {
         final Task task = new TaskBuilder(mSupervisor)
                 .setCreateActivity(true)
-                .setComponent(
-                        ComponentName.createRelative(mContext, SizeCompatTests.class.getName()))
+                .build();
+        task.setResizeMode(RESIZE_MODE_UNRESIZEABLE);
+        final ActivityRecord activity = task.getRootActivity();
+        spyOn(activity);
+        doReturn(true).when(activity).isUniversalResizeable();
+        task.intent = null;
+        task.setIntent(activity);
+
+        // Override should take effect and task should be resizeable.
+        assertTrue(task.getTaskInfo().isResizeable);
+    }
+
+    @Test
+    public void testIsResizeable_systemWideForceResize_compatForceNonResize_resizeable() {
+        final Task task = new TaskBuilder(mSupervisor)
+                .setCreateActivity(true)
+                .setComponent(ComponentName.createRelative(mContext, TaskTests.class.getName()))
                 .build();
         task.setResizeMode(RESIZE_MODE_RESIZEABLE);
 
         // Set system-wide force resizeable override.
         task.mAtmService.mForceResizableActivities = true;
 
+        final ActivityRecord activity = task.getRootActivity();
+        final AppCompatResizeOverrides resizeOverrides =
+                activity.mAppCompatController.getResizeOverrides();
+        spyOn(activity);
+        spyOn(resizeOverrides);
+        doReturn(true).when(resizeOverrides).shouldOverrideForceNonResizeApp();
+        task.intent = null;
+        task.setIntent(activity);
+
         // System wide override should tak priority over app compat override so the task should
         // remain resizeable.
         assertTrue(task.getTaskInfo().isResizeable);
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index 1491510..19574fd 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -11493,17 +11493,17 @@
                         + "target=GERAN|UTRAN|EUTRAN|NGRAN|IWLAN, type=allowed"});
         PersistableBundle auto_data_switch_rat_signal_score_string_bundle = new PersistableBundle();
         auto_data_switch_rat_signal_score_string_bundle.putIntArray(
-                "NR_SA_MMWAVE", new int[]{10000, 13227, 16000, 18488, 20017});
+                "NR_SA_MMWAVE", new int[]{6300, 10227, 16000, 18488, 19017});
         auto_data_switch_rat_signal_score_string_bundle.putIntArray(
-                "NR_NSA_MMWAVE", new int[]{8000, 10227, 12488, 15017, 15278});
+                "NR_NSA_MMWAVE", new int[]{5700, 9227, 12488, 13517, 15978});
         auto_data_switch_rat_signal_score_string_bundle.putIntArray(
                 "LTE", new int[]{3731, 5965, 8618, 11179, 13384});
         auto_data_switch_rat_signal_score_string_bundle.putIntArray(
-                "LTE_CA", new int[]{3831, 6065, 8718, 11379, 13484});
+                "LTE_CA", new int[]{3831, 6065, 8718, 11379, 14484});
         auto_data_switch_rat_signal_score_string_bundle.putIntArray(
-                "NR_SA", new int[]{5288, 6795, 6955, 7562, 9713});
+                "NR_SA", new int[]{2288, 6795, 6955, 7562, 15484});
         auto_data_switch_rat_signal_score_string_bundle.putIntArray(
-                "NR_NSA", new int[]{5463, 6827, 8029, 9007, 9428});
+                "NR_NSA", new int[]{2463, 6827, 8029, 9007, 15884});
         auto_data_switch_rat_signal_score_string_bundle.putIntArray(
                 "UMTS", new int[]{100, 169, 183, 192, 300});
         auto_data_switch_rat_signal_score_string_bundle.putIntArray(
diff --git a/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt b/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt
index 794fd02..c62bd0b 100644
--- a/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt
+++ b/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt
@@ -18,12 +18,10 @@
 
 import android.content.Context
 import android.content.ContextWrapper
-import android.os.IBinder
 import android.platform.test.annotations.Presubmit
 import android.platform.test.flag.junit.SetFlagsRule
 import android.view.KeyEvent
 import androidx.test.core.app.ApplicationProvider
-import com.android.server.testutils.any
 import com.android.test.input.MockInputManagerRule
 import org.junit.Before
 import org.junit.Rule
@@ -37,6 +35,7 @@
 import kotlin.test.assertNotNull
 import kotlin.test.assertNull
 import kotlin.test.fail
+import org.junit.Assert.assertThrows
 
 /**
  * Tests for [InputManager.KeyGestureEventHandler].
@@ -82,7 +81,7 @@
 
         // Handle key gesture handler registration.
         doAnswer {
-            val listener = it.getArgument(0) as IKeyGestureHandler
+            val listener = it.getArgument(1) as IKeyGestureHandler
             if (registeredListener != null &&
                     registeredListener!!.asBinder() != listener.asBinder()) {
                 // There can only be one registered key gesture handler per process.
@@ -90,7 +89,7 @@
             }
             registeredListener = listener
             null
-        }.`when`(inputManagerRule.mock).registerKeyGestureHandler(any())
+        }.`when`(inputManagerRule.mock).registerKeyGestureHandler(Mockito.any(), Mockito.any())
 
         // Handle key gesture handler being unregistered.
         doAnswer {
@@ -101,7 +100,7 @@
             }
             registeredListener = null
             null
-        }.`when`(inputManagerRule.mock).unregisterKeyGestureHandler(any())
+        }.`when`(inputManagerRule.mock).unregisterKeyGestureHandler(Mockito.any())
     }
 
     private fun handleKeyGestureEvent(event: KeyGestureEvent) {
@@ -121,11 +120,12 @@
         var callbackCount = 0
 
         // Add a key gesture event listener
-        inputManager.registerKeyGestureEventHandler(KeyGestureHandler { event, _ ->
+        inputManager.registerKeyGestureEventHandler(
+            listOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME)
+        ) { event, _ ->
             assertEquals(HOME_GESTURE_EVENT, event)
             callbackCount++
-            true
-        })
+        }
 
         // Request handling for key gesture event will notify the handler.
         handleKeyGestureEvent(HOME_GESTURE_EVENT)
@@ -135,29 +135,41 @@
     @Test
     fun testAddingHandlersRegistersInternalCallbackHandler() {
         // Set up two callbacks.
-        val callback1 = KeyGestureHandler { _, _ -> false }
-        val callback2 = KeyGestureHandler { _, _ -> false }
+        val callback1 = InputManager.KeyGestureEventHandler { _, _ -> }
+        val callback2 = InputManager.KeyGestureEventHandler { _, _ -> }
 
         assertNull(registeredListener)
 
         // Adding the handler should register the callback with InputManagerService.
-        inputManager.registerKeyGestureEventHandler(callback1)
+        inputManager.registerKeyGestureEventHandler(
+            listOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME),
+            callback1
+        )
         assertNotNull(registeredListener)
 
         // Adding another handler should not register new internal listener.
         val currListener = registeredListener
-        inputManager.registerKeyGestureEventHandler(callback2)
+        inputManager.registerKeyGestureEventHandler(
+            listOf(KeyGestureEvent.KEY_GESTURE_TYPE_BACK),
+            callback2
+        )
         assertEquals(currListener, registeredListener)
     }
 
     @Test
     fun testRemovingHandlersUnregistersInternalCallbackHandler() {
         // Set up two callbacks.
-        val callback1 = KeyGestureHandler { _, _ -> false }
-        val callback2 = KeyGestureHandler { _, _ -> false }
+        val callback1 = InputManager.KeyGestureEventHandler { _, _ -> }
+        val callback2 = InputManager.KeyGestureEventHandler { _, _ -> }
 
-        inputManager.registerKeyGestureEventHandler(callback1)
-        inputManager.registerKeyGestureEventHandler(callback2)
+        inputManager.registerKeyGestureEventHandler(
+            listOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME),
+            callback1
+        )
+        inputManager.registerKeyGestureEventHandler(
+            listOf(KeyGestureEvent.KEY_GESTURE_TYPE_BACK),
+            callback2
+        )
 
         // Only removing all handlers should remove the internal callback
         inputManager.unregisterKeyGestureEventHandler(callback1)
@@ -172,47 +184,74 @@
         var callbackCount1 = 0
         var callbackCount2 = 0
         // Handler 1 captures all home gestures
-        val callback1 = KeyGestureHandler { event, _ ->
+        val callback1 = InputManager.KeyGestureEventHandler { event, _ ->
             callbackCount1++
-            event.keyGestureType == KeyGestureEvent.KEY_GESTURE_TYPE_HOME
+            assertEquals(KeyGestureEvent.KEY_GESTURE_TYPE_HOME, event.keyGestureType)
         }
-        // Handler 2 captures all gestures
-        val callback2 = KeyGestureHandler { _, _ ->
+        // Handler 2 captures all back gestures
+        val callback2 = InputManager.KeyGestureEventHandler { event, _ ->
             callbackCount2++
-            true
+            assertEquals(KeyGestureEvent.KEY_GESTURE_TYPE_BACK, event.keyGestureType)
         }
 
         // Add both key gesture event handlers
-        inputManager.registerKeyGestureEventHandler(callback1)
-        inputManager.registerKeyGestureEventHandler(callback2)
+        inputManager.registerKeyGestureEventHandler(
+            listOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME),
+            callback1
+        )
+        inputManager.registerKeyGestureEventHandler(
+            listOf(KeyGestureEvent.KEY_GESTURE_TYPE_BACK),
+            callback2
+        )
 
-        // Request handling for key gesture event, should notify callbacks in order. So, only the
-        // first handler should receive a callback since it captures the event.
+        // Request handling for home key gesture event, should notify only callback1
         handleKeyGestureEvent(HOME_GESTURE_EVENT)
         assertEquals(1, callbackCount1)
         assertEquals(0, callbackCount2)
 
-        // Second handler should receive the event since the first handler doesn't capture the event
+        // Request handling for back key gesture event, should notify only callback2
         handleKeyGestureEvent(BACK_GESTURE_EVENT)
-        assertEquals(2, callbackCount1)
+        assertEquals(1, callbackCount1)
         assertEquals(1, callbackCount2)
 
         inputManager.unregisterKeyGestureEventHandler(callback1)
-        // Request handling for key gesture event, should still trigger callback2 but not callback1.
+
+        // Request handling for home key gesture event, should not trigger callback2
         handleKeyGestureEvent(HOME_GESTURE_EVENT)
-        assertEquals(2, callbackCount1)
-        assertEquals(2, callbackCount2)
+        assertEquals(1, callbackCount1)
+        assertEquals(1, callbackCount2)
     }
 
-    inner class KeyGestureHandler(
-        private var handler: (event: KeyGestureEvent, token: IBinder?) -> Boolean
-    ) : InputManager.KeyGestureEventHandler {
+    @Test
+    fun testUnableToRegisterSameHandlerTwice() {
+        val handler = InputManager.KeyGestureEventHandler { _, _ -> }
 
-        override fun handleKeyGestureEvent(
-            event: KeyGestureEvent,
-            focusedToken: IBinder?
-        ): Boolean {
-            return handler(event, focusedToken)
+        inputManager.registerKeyGestureEventHandler(
+            listOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME),
+            handler
+        )
+
+        assertThrows(IllegalArgumentException::class.java) {
+            inputManager.registerKeyGestureEventHandler(
+                listOf(KeyGestureEvent.KEY_GESTURE_TYPE_BACK), handler
+            )
+        }
+    }
+
+    @Test
+    fun testUnableToRegisterSameGestureTwice() {
+        val handler1 = InputManager.KeyGestureEventHandler { _, _ -> }
+        val handler2 = InputManager.KeyGestureEventHandler { _, _ -> }
+
+        inputManager.registerKeyGestureEventHandler(
+            listOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME),
+            handler1
+        )
+
+        assertThrows(IllegalArgumentException::class.java) {
+            inputManager.registerKeyGestureEventHandler(
+                listOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME), handler2
+            )
         }
     }
 }
diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
index 4f1fb64..163dda8 100644
--- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
+++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
@@ -63,6 +63,7 @@
 import org.junit.Assert.assertArrayEquals
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertNull
+import org.junit.Assert.assertThrows
 import org.junit.Assert.assertTrue
 import org.junit.Before
 import org.junit.Rule
@@ -107,7 +108,10 @@
         const val SETTINGS_KEY_BEHAVIOR_SETTINGS_ACTIVITY = 0
         const val SETTINGS_KEY_BEHAVIOR_NOTIFICATION_PANEL = 1
         const val SETTINGS_KEY_BEHAVIOR_NOTHING = 2
+        const val SYSTEM_PID = 0
         const val TEST_PID = 10
+        const val RANDOM_PID1 = 11
+        const val RANDOM_PID2 = 12
     }
 
     @JvmField
@@ -170,6 +174,7 @@
                     return atomicFile
                 }
             })
+        startNewInputGlobalTestSession()
     }
 
     @After
@@ -199,17 +204,22 @@
         val correctIm = context.getSystemService(InputManager::class.java)!!
         val virtualDevice = correctIm.getInputDevice(KeyCharacterMap.VIRTUAL_KEYBOARD)!!
         val kcm = virtualDevice.keyCharacterMap!!
-        inputManagerGlobalSession = InputManagerGlobal.createTestSession(iInputManager)
-        val inputManager = InputManager(context)
-        Mockito.`when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE)))
-            .thenReturn(inputManager)
-
         val keyboardDevice = InputDevice.Builder().setId(DEVICE_ID).build()
         Mockito.`when`(iInputManager.inputDeviceIds).thenReturn(intArrayOf(DEVICE_ID))
         Mockito.`when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardDevice)
         ExtendedMockito.`when`(KeyCharacterMap.load(Mockito.anyInt())).thenReturn(kcm)
     }
 
+    private fun startNewInputGlobalTestSession() {
+        if (this::inputManagerGlobalSession.isInitialized) {
+            inputManagerGlobalSession.close()
+        }
+        inputManagerGlobalSession = InputManagerGlobal.createTestSession(iInputManager)
+        val inputManager = InputManager(context)
+        Mockito.`when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE)))
+            .thenReturn(inputManager)
+    }
+
     private fun setupKeyGestureController() {
         keyGestureController =
             KeyGestureController(
@@ -225,13 +235,14 @@
                         return accessibilityShortcutController
                     }
                 })
-        Mockito.`when`(iInputManager.registerKeyGestureHandler(Mockito.any()))
+        Mockito.`when`(iInputManager.registerKeyGestureHandler(Mockito.any(), Mockito.any()))
             .thenAnswer {
                 val args = it.arguments
                 if (args[0] != null) {
                     keyGestureController.registerKeyGestureHandler(
-                        args[0] as IKeyGestureHandler,
-                        TEST_PID
+                        args[0] as IntArray,
+                        args[1] as IKeyGestureHandler,
+                        SYSTEM_PID
                     )
                 }
         }
@@ -285,59 +296,6 @@
         )
     }
 
-    @Test
-    fun testKeyGestureEvent_multipleGestureHandlers() {
-        setupKeyGestureController()
-
-        // Set up two callbacks.
-        var callbackCount1 = 0
-        var callbackCount2 = 0
-        var selfCallback = 0
-        val externalHandler1 = KeyGestureHandler { _, _ ->
-            callbackCount1++
-            true
-        }
-        val externalHandler2 = KeyGestureHandler { _, _ ->
-            callbackCount2++
-            true
-        }
-        val selfHandler = KeyGestureHandler { _, _ ->
-            selfCallback++
-            false
-        }
-
-        // Register key gesture handler: External process (last in priority)
-        keyGestureController.registerKeyGestureHandler(externalHandler1, currentPid + 1)
-
-        // Register key gesture handler: External process (second in priority)
-        keyGestureController.registerKeyGestureHandler(externalHandler2, currentPid - 1)
-
-        // Register key gesture handler: Self process (first in priority)
-        keyGestureController.registerKeyGestureHandler(selfHandler, currentPid)
-
-        keyGestureController.handleKeyGesture(/* deviceId = */ 0, intArrayOf(KeyEvent.KEYCODE_HOME),
-            /* modifierState = */ 0, KeyGestureEvent.KEY_GESTURE_TYPE_HOME,
-            KeyGestureEvent.ACTION_GESTURE_COMPLETE, /* displayId */ 0,
-            /* focusedToken = */ null, /* flags = */ 0, /* appLaunchData = */null
-        )
-
-        assertEquals(
-            "Self handler should get callbacks first",
-            1,
-            selfCallback
-        )
-        assertEquals(
-            "Higher priority handler should get callbacks first",
-            1,
-            callbackCount2
-        )
-        assertEquals(
-            "Lower priority handler should not get callbacks if already handled",
-            0,
-            callbackCount1
-        )
-    }
-
     class TestData(
         val name: String,
         val keys: IntArray,
@@ -789,10 +747,6 @@
     )
     fun testCustomKeyGesturesNotAllowedForSystemGestures(test: TestData) {
         setupKeyGestureController()
-        // Need to re-init so that bookmarks are correctly blocklisted
-        Mockito.`when`(iInputManager.getAppLaunchBookmarks())
-            .thenReturn(keyGestureController.appLaunchBookmarks)
-        keyGestureController.systemRunning()
 
         val builder = InputGestureData.Builder()
             .setKeyGestureType(test.expectedKeyGestureType)
@@ -1163,9 +1117,6 @@
             KeyEvent.KEYCODE_FULLSCREEN
         )
 
-        val handler = KeyGestureHandler { _, _ -> false }
-        keyGestureController.registerKeyGestureHandler(handler, 0)
-
         for (key in testKeys) {
             sendKeys(intArrayOf(key), assertNotSentToApps = true)
         }
@@ -1179,6 +1130,7 @@
         testKeyGestureNotProduced(
             "SEARCH -> Default Search",
             intArrayOf(KeyEvent.KEYCODE_SEARCH),
+            intArrayOf(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH)
         )
     }
 
@@ -1207,6 +1159,10 @@
         testKeyGestureNotProduced(
             "SETTINGS -> Do Nothing",
             intArrayOf(KeyEvent.KEYCODE_SETTINGS),
+            intArrayOf(
+                KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH,
+                KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL
+            )
         )
     }
 
@@ -1290,28 +1246,6 @@
                 )
             ),
             TestData(
-                "VOLUME_DOWN + VOLUME_UP -> Accessibility Chord",
-                intArrayOf(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_UP),
-                KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD,
-                intArrayOf(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_UP),
-                0,
-                intArrayOf(
-                    KeyGestureEvent.ACTION_GESTURE_START,
-                    KeyGestureEvent.ACTION_GESTURE_COMPLETE
-                )
-            ),
-            TestData(
-                "BACK + DPAD_DOWN -> Accessibility Chord(for TV)",
-                intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN),
-                KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD,
-                intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN),
-                0,
-                intArrayOf(
-                    KeyGestureEvent.ACTION_GESTURE_START,
-                    KeyGestureEvent.ACTION_GESTURE_COMPLETE
-                )
-            ),
-            TestData(
                 "BACK + DPAD_CENTER -> TV Trigger Bug Report",
                 intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_CENTER),
                 KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT,
@@ -1428,9 +1362,11 @@
         testLooper.dispatchAll()
 
         // Reinitialize the gesture controller simulating a login/logout for the user.
+        startNewInputGlobalTestSession()
         setupKeyGestureController()
         keyGestureController.setCurrentUserId(userId)
         testLooper.dispatchAll()
+
         val savedInputGestures = keyGestureController.getCustomInputGestures(userId, null)
         assertEquals(
             "Test: $test doesn't produce correct number of saved input gestures",
@@ -1469,6 +1405,7 @@
 
         // Delete the old data and reinitialize the controller simulating a "fresh" install.
         tempFile.delete()
+        startNewInputGlobalTestSession()
         setupKeyGestureController()
         keyGestureController.setCurrentUserId(userId)
         testLooper.dispatchAll()
@@ -1541,9 +1478,12 @@
         val handledEvents = mutableListOf<KeyGestureEvent>()
         val handler = KeyGestureHandler { event, _ ->
             handledEvents.add(KeyGestureEvent(event))
-            true
         }
-        keyGestureController.registerKeyGestureHandler(handler, 0)
+        keyGestureController.registerKeyGestureHandler(
+            intArrayOf(test.expectedKeyGestureType),
+            handler,
+            TEST_PID
+        )
         handledEvents.clear()
 
         keyGestureController.handleTouchpadGesture(test.touchpadGestureType)
@@ -1570,7 +1510,7 @@
             event.appLaunchData
         )
 
-        keyGestureController.unregisterKeyGestureHandler(handler, 0)
+        keyGestureController.unregisterKeyGestureHandler(handler, TEST_PID)
     }
 
     @Test
@@ -1591,9 +1531,11 @@
         testLooper.dispatchAll()
 
         // Reinitialize the gesture controller simulating a login/logout for the user.
+        startNewInputGlobalTestSession()
         setupKeyGestureController()
         keyGestureController.setCurrentUserId(userId)
         testLooper.dispatchAll()
+
         val savedInputGestures = keyGestureController.getCustomInputGestures(userId, null)
         assertEquals(
             "Test: $test doesn't produce correct number of saved input gestures",
@@ -1627,6 +1569,7 @@
 
         // Delete the old data and reinitialize the controller simulating a "fresh" install.
         tempFile.delete()
+        startNewInputGlobalTestSession()
         setupKeyGestureController()
         keyGestureController.setCurrentUserId(userId)
         testLooper.dispatchAll()
@@ -1699,13 +1642,97 @@
         Mockito.verify(accessibilityShortcutController, never()).performAccessibilityShortcut()
     }
 
+    @Test
+    fun testUnableToRegisterFromSamePidTwice() {
+        setupKeyGestureController()
+
+        val handler1 = KeyGestureHandler { _, _ -> }
+        val handler2 = KeyGestureHandler { _, _ -> }
+        keyGestureController.registerKeyGestureHandler(
+            intArrayOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME),
+            handler1,
+            RANDOM_PID1
+        )
+
+        assertThrows(IllegalStateException::class.java) {
+            keyGestureController.registerKeyGestureHandler(
+                intArrayOf(KeyGestureEvent.KEY_GESTURE_TYPE_BACK),
+                handler2,
+                RANDOM_PID1
+            )
+        }
+    }
+
+    @Test
+    fun testUnableToRegisterSameGestureTwice() {
+        setupKeyGestureController()
+
+        val handler1 = KeyGestureHandler { _, _ -> }
+        val handler2 = KeyGestureHandler { _, _ -> }
+        keyGestureController.registerKeyGestureHandler(
+            intArrayOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME),
+            handler1,
+            RANDOM_PID1
+        )
+
+        assertThrows(IllegalArgumentException::class.java) {
+            keyGestureController.registerKeyGestureHandler(
+                intArrayOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME),
+                handler2,
+                RANDOM_PID2
+            )
+        }
+    }
+
+    @Test
+    fun testUnableToRegisterEmptyListOfGestures() {
+        setupKeyGestureController()
+
+        val handler = KeyGestureHandler { _, _ -> }
+
+        assertThrows(IllegalArgumentException::class.java) {
+            keyGestureController.registerKeyGestureHandler(
+                intArrayOf(),
+                handler,
+                RANDOM_PID1
+            )
+        }
+    }
+
+    @Test
+    fun testGestureHandlerNotCalledOnceUnregistered() {
+        setupKeyGestureController()
+
+        var callbackCount = 0
+        val handler1 = KeyGestureHandler { _, _ -> callbackCount++ }
+        keyGestureController.registerKeyGestureHandler(
+            intArrayOf(KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS),
+            handler1,
+            TEST_PID
+        )
+        sendKeys(intArrayOf(KeyEvent.KEYCODE_RECENT_APPS))
+        assertEquals(1, callbackCount)
+
+        keyGestureController.unregisterKeyGestureHandler(
+            handler1,
+            TEST_PID
+        )
+
+        // Callback should not be sent after unregister
+        sendKeys(intArrayOf(KeyEvent.KEYCODE_RECENT_APPS))
+        assertEquals(1, callbackCount)
+    }
+
     private fun testKeyGestureInternal(test: TestData) {
         val handledEvents = mutableListOf<KeyGestureEvent>()
         val handler = KeyGestureHandler { event, _ ->
             handledEvents.add(KeyGestureEvent(event))
-            true
         }
-        keyGestureController.registerKeyGestureHandler(handler, 0)
+        keyGestureController.registerKeyGestureHandler(
+            intArrayOf(test.expectedKeyGestureType),
+            handler,
+            TEST_PID
+        )
         handledEvents.clear()
 
         sendKeys(test.keys)
@@ -1744,16 +1771,19 @@
             )
         }
 
-        keyGestureController.unregisterKeyGestureHandler(handler, 0)
+        keyGestureController.unregisterKeyGestureHandler(handler, TEST_PID)
     }
 
-    private fun testKeyGestureNotProduced(testName: String, testKeys: IntArray) {
+    private fun testKeyGestureNotProduced(
+        testName: String,
+        testKeys: IntArray,
+        possibleGestures: IntArray
+    ) {
         var handledEvents = mutableListOf<KeyGestureEvent>()
         val handler = KeyGestureHandler { event, _ ->
             handledEvents.add(KeyGestureEvent(event))
-            true
         }
-        keyGestureController.registerKeyGestureHandler(handler, 0)
+        keyGestureController.registerKeyGestureHandler(possibleGestures, handler, TEST_PID)
         handledEvents.clear()
 
         sendKeys(testKeys)
@@ -1823,10 +1853,10 @@
     }
 
     inner class KeyGestureHandler(
-        private var handler: (event: AidlKeyGestureEvent, token: IBinder?) -> Boolean
+        private var handler: (event: AidlKeyGestureEvent, token: IBinder?) -> Unit
     ) : IKeyGestureHandler.Stub() {
-        override fun handleKeyGesture(event: AidlKeyGestureEvent, token: IBinder?): Boolean {
-            return handler(event, token)
+        override fun handleKeyGesture(event: AidlKeyGestureEvent, token: IBinder?) {
+            handler(event, token)
         }
     }
 }
diff --git a/tests/Input/src/com/android/test/input/AnrTest.kt b/tests/Input/src/com/android/test/input/AnrTest.kt
index f8cb86b..3ad3763 100644
--- a/tests/Input/src/com/android/test/input/AnrTest.kt
+++ b/tests/Input/src/com/android/test/input/AnrTest.kt
@@ -16,6 +16,7 @@
 package com.android.test.input
 
 import android.app.ActivityManager
+import android.app.ActivityTaskManager
 import android.app.ApplicationExitInfo
 import android.app.Instrumentation
 import android.content.Intent
@@ -28,6 +29,7 @@
 import android.server.wm.CtsWindowInfoUtils.getWindowCenter
 import android.server.wm.CtsWindowInfoUtils.waitForWindowOnTop
 import android.testing.PollingCheck
+import android.util.Log
 import android.view.InputEvent
 import android.view.MotionEvent
 import android.view.MotionEvent.ACTION_DOWN
@@ -46,21 +48,19 @@
 import java.time.Duration
 import java.util.concurrent.LinkedBlockingQueue
 import java.util.function.Supplier
-import org.junit.After
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertTrue
 import org.junit.Assert.fail
-import org.junit.Before
+import org.junit.Assume.assumeTrue
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
 /**
- * Click on the center of the window identified by the provided window token.
- * The click is performed using "UinputTouchScreen" device.
- * If the touchscreen device is closed too soon, it may cause the click to be dropped. Therefore,
- * the provided runnable can ensure that the click is delivered before the device is closed, thus
- * avoiding this race.
+ * Click on the center of the window identified by the provided window token. The click is performed
+ * using "UinputTouchScreen" device. If the touchscreen device is closed too soon, it may cause the
+ * click to be dropped. Therefore, the provided runnable can ensure that the click is delivered
+ * before the device is closed, thus avoiding this race.
  */
 private fun clickOnWindow(
     token: IBinder,
@@ -104,6 +104,10 @@
     private val remoteInputEvents = LinkedBlockingQueue<InputEvent>()
     private val verifier = BlockingQueueEventVerifier(remoteInputEvents)
 
+    // Some devices don't support ANR error dialogs, such as cars, TVs, etc.
+    private val anrDialogsAreSupported =
+        ActivityTaskManager.currentUiModeSupportsErrorDialogs(instrumentation.targetContext)
+
     val binder =
         object : IAnrTestService.Stub() {
             override fun provideActivityInfo(token: IBinder, displayId: Int, pid: Int) {
@@ -121,34 +125,37 @@
 
     @get:Rule val debugInputRule = DebugInputRule()
 
-    @Before
-    fun setUp() {
-        startUnresponsiveActivity()
-        PACKAGE_NAME = UnresponsiveGestureMonitorActivity::class.java.getPackage()!!.getName()
-    }
-
-    @After fun tearDown() {}
-
     @Test
     @DebugInputRule.DebugInput(bug = 339924248)
     fun testGestureMonitorAnr_Close() {
+        startUnresponsiveActivity()
+        val timestamp = System.currentTimeMillis()
         triggerAnr()
-        clickCloseAppOnAnrDialog()
+        if (anrDialogsAreSupported) {
+            clickCloseAppOnAnrDialog()
+        } else {
+            Log.i(TAG, "The device does not support ANR dialogs, skipping check for ANR window")
+            // We still want to wait for the app to get killed by the ActivityManager
+        }
+        waitForNewExitReasonAfter(timestamp)
     }
 
     @Test
     @DebugInputRule.DebugInput(bug = 339924248)
     fun testGestureMonitorAnr_Wait() {
+        assumeTrue(anrDialogsAreSupported)
+        startUnresponsiveActivity()
         triggerAnr()
         clickWaitOnAnrDialog()
         SystemClock.sleep(500) // Wait at least 500ms after tapping on wait
         // ANR dialog should reappear after a delay - find the close button on it to verify
+        val timestamp = System.currentTimeMillis()
         clickCloseAppOnAnrDialog()
+        waitForNewExitReasonAfter(timestamp)
     }
 
     private fun clickCloseAppOnAnrDialog() {
         // Find anr dialog and kill app
-        val timestamp = System.currentTimeMillis()
         val uiDevice: UiDevice = UiDevice.getInstance(instrumentation)
         val closeAppButton: UiObject2? =
             uiDevice.wait(Until.findObject(By.res("android:id/aerr_close")), 20000)
@@ -157,14 +164,6 @@
             return
         }
         closeAppButton.click()
-        /**
-         * We must wait for the app to be fully closed before exiting this test. This is because
-         * another test may again invoke 'am start' for the same activity. If the 1st process that
-         * got ANRd isn't killed by the time second 'am start' runs, the killing logic will apply to
-         * the newly launched 'am start' instance, and the second test will fail because the
-         * unresponsive activity will never be launched.
-         */
-        waitForNewExitReasonAfter(timestamp)
     }
 
     private fun clickWaitOnAnrDialog() {
@@ -180,16 +179,27 @@
     }
 
     private fun getExitReasons(): List<ApplicationExitInfo> {
+        val packageName = UnresponsiveGestureMonitorActivity::class.java.getPackage()!!.name
         lateinit var infos: List<ApplicationExitInfo>
         instrumentation.runOnMainSync {
             val am = instrumentation.getContext().getSystemService(ActivityManager::class.java)!!
-            infos = am.getHistoricalProcessExitReasons(PACKAGE_NAME, remotePid!!, NO_MAX)
+            infos = am.getHistoricalProcessExitReasons(packageName, remotePid!!, NO_MAX)
         }
         return infos
     }
 
+    /**
+     * We must wait for the app to be fully closed before exiting this test. This is because another
+     * test may again invoke 'am start' for the same activity. If the 1st process that got ANRd
+     * isn't killed by the time second 'am start' runs, the killing logic will apply to the newly
+     * launched 'am start' instance, and the second test will fail because the unresponsive activity
+     * will never be launched.
+     *
+     * Also, we must ensure that we wait until it's killed, so that the next test can launch this
+     * activity again.
+     */
     private fun waitForNewExitReasonAfter(timestamp: Long) {
-        PollingCheck.waitFor {
+        PollingCheck.waitFor(Duration.ofSeconds(20).toMillis() * Build.HW_TIMEOUT_MULTIPLIER) {
             val reasons = getExitReasons()
             !reasons.isEmpty() && reasons[0].timestamp >= timestamp
         }
@@ -199,16 +209,15 @@
     }
 
     private fun triggerAnr() {
-        clickOnWindow(
-            remoteWindowToken!!,
-            remoteDisplayId!!,
-            instrumentation,
-        ) { verifier.assertReceivedMotion(withMotionAction(ACTION_DOWN)) }
+        clickOnWindow(remoteWindowToken!!, remoteDisplayId!!, instrumentation) {
+            verifier.assertReceivedMotion(withMotionAction(ACTION_DOWN))
+        }
 
         SystemClock.sleep(DISPATCHING_TIMEOUT.toLong()) // default ANR timeout for gesture monitors
     }
 
     private fun startUnresponsiveActivity() {
+        remoteWindowToken = null
         val intent =
             Intent(instrumentation.targetContext, UnresponsiveGestureMonitorActivity::class.java)
         intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NEW_DOCUMENT
@@ -218,12 +227,17 @@
         instrumentation.targetContext.startActivity(intent)
         // first, wait for the token to become valid
         PollingCheck.check(
-                "UnresponsiveGestureMonitorActivity failed to call 'provideActivityInfo'",
-                Duration.ofSeconds(5).toMillis()) { remoteWindowToken != null }
+            "UnresponsiveGestureMonitorActivity failed to call 'provideActivityInfo'",
+            Duration.ofSeconds(10).toMillis() * Build.HW_TIMEOUT_MULTIPLIER,
+        ) {
+            remoteWindowToken != null
+        }
         // next, wait for the window of the activity to get on top
         // we could combine the two checks above, but the current setup makes it easier to detect
         // errors
-        assertTrue("Remote activity window did not become visible",
-          waitForWindowOnTop(Duration.ofSeconds(5), Supplier { remoteWindowToken }))
+        assertTrue(
+            "Remote activity window did not become visible",
+            waitForWindowOnTop(Duration.ofSeconds(5), Supplier { remoteWindowToken }),
+        )
     }
 }
diff --git a/tools/aapt2/ResourcesInternal.proto b/tools/aapt2/ResourcesInternal.proto
index f4735a2..380c5f2 100644
--- a/tools/aapt2/ResourcesInternal.proto
+++ b/tools/aapt2/ResourcesInternal.proto
@@ -50,8 +50,11 @@
   // Any symbols this file auto-generates/exports (eg. @+id/foo in an XML file).
   repeated Symbol exported_symbol = 5;
 
-  // The status of the flag the file is behind if any
+  // The status of the read only flag the file is behind if any
   uint32 flag_status = 6;
   bool flag_negated = 7;
   string flag_name = 8;
+
+  // Whether the file uses read/write feature flags
+  bool uses_readwrite_feature_flags = 9;
 }
diff --git a/tools/aapt2/cmd/Compile.cpp b/tools/aapt2/cmd/Compile.cpp
index a5e18d35..3b4f542 100644
--- a/tools/aapt2/cmd/Compile.cpp
+++ b/tools/aapt2/cmd/Compile.cpp
@@ -407,6 +407,45 @@
   return true;
 }
 
+class FindReadWriteFlagsVisitor : public xml::Visitor {
+ public:
+  FindReadWriteFlagsVisitor(const FeatureFlagValues& feature_flag_values)
+      : feature_flag_values_(feature_flag_values) {
+  }
+
+  void Visit(xml::Element* node) override {
+    if (had_flags_) {
+      return;
+    }
+    auto* attr = node->FindAttribute(xml::kSchemaAndroid, xml::kAttrFeatureFlag);
+    if (attr != nullptr) {
+      std::string_view flag_name = util::TrimWhitespace(attr->value);
+      if (flag_name.starts_with('!')) {
+        flag_name = flag_name.substr(1);
+      }
+      if (auto it = feature_flag_values_.find(flag_name); it != feature_flag_values_.end()) {
+        if (!it->second.read_only) {
+          had_flags_ = true;
+          return;
+        }
+      } else {
+        // Flag not passed to aapt2, must evaluate at runtime
+        had_flags_ = true;
+        return;
+      }
+    }
+    VisitChildren(node);
+  }
+
+  bool HadFlags() const {
+    return had_flags_;
+  }
+
+ private:
+  bool had_flags_ = false;
+  const FeatureFlagValues& feature_flag_values_;
+};
+
 static bool CompileXml(IAaptContext* context, const CompileOptions& options,
                        const ResourcePathData& path_data, io::IFile* file, IArchiveWriter* writer,
                        const std::string& output_path) {
@@ -436,6 +475,10 @@
   xmlres->file.type = ResourceFile::Type::kProtoXml;
   xmlres->file.flag = ParseFlag(path_data.flag_name);
 
+  FindReadWriteFlagsVisitor visitor(options.feature_flag_values);
+  xmlres->root->Accept(&visitor);
+  xmlres->file.uses_readwrite_feature_flags = visitor.HadFlags();
+
   if (xmlres->file.flag) {
     std::string error;
     auto flag_status = GetFlagStatus(xmlres->file.flag, options.feature_flag_values, &error);
diff --git a/tools/aapt2/cmd/Convert.h b/tools/aapt2/cmd/Convert.h
index 9452e58..5576ec0 100644
--- a/tools/aapt2/cmd/Convert.h
+++ b/tools/aapt2/cmd/Convert.h
@@ -38,14 +38,14 @@
         "--enable-sparse-encoding",
         "Enables encoding sparse entries using a binary search tree.\n"
         "This decreases APK size at the cost of resource retrieval performance.\n"
-        "Only applies sparse encoding to Android O+ resources or all resources if minSdk of "
-        "the APK is O+",
+        "Only applies sparse encoding if minSdk of the APK is >= 32",
         &enable_sparse_encoding_);
-    AddOptionalSwitch("--force-sparse-encoding",
-                      "Enables encoding sparse entries using a binary search tree.\n"
-                      "This decreases APK size at the cost of resource retrieval performance.\n"
-                      "Applies sparse encoding to all resources regardless of minSdk.",
-                      &force_sparse_encoding_);
+    AddOptionalSwitch(
+        "--force-sparse-encoding",
+        "Enables encoding sparse entries using a binary search tree.\n"
+        "This decreases APK size at the cost of resource retrieval performance.\n"
+        "Only applies sparse encoding if minSdk of the APK is >= 32 or is not set",
+        &force_sparse_encoding_);
     AddOptionalSwitch(
         "--enable-compact-entries",
         "This decreases APK size by using compact resource entries for simple data types.",
diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp
index 755dbb6..0e18ee2 100644
--- a/tools/aapt2/cmd/Link.cpp
+++ b/tools/aapt2/cmd/Link.cpp
@@ -615,6 +615,8 @@
             file_op.xml_to_flatten->file.source = file_ref->GetSource();
             file_op.xml_to_flatten->file.name =
                 ResourceName(pkg->name, type->named_type, entry->name);
+            file_op.xml_to_flatten->file.uses_readwrite_feature_flags =
+                config_value->uses_readwrite_feature_flags;
           }
 
           // NOTE(adamlesinski): Explicitly construct a StringPiece here, or
@@ -647,6 +649,17 @@
             }
           }
 
+          FeatureFlagsFilterOptions flags_filter_options;
+          // Don't fail on unrecognized flags or flags without values as these flags might be
+          // defined and have a value by the time they are evaluated at runtime.
+          flags_filter_options.fail_on_unrecognized_flags = false;
+          flags_filter_options.flags_must_have_value = false;
+          flags_filter_options.remove_disabled_elements = true;
+          FeatureFlagsFilter flags_filter(options_.feature_flag_values, flags_filter_options);
+          if (!flags_filter.Consume(context_, file_op.xml_to_flatten.get())) {
+            return 1;
+          }
+
           std::vector<std::unique_ptr<xml::XmlResource>> versioned_docs =
               LinkAndVersionXmlFile(table, &file_op);
           if (versioned_docs.empty()) {
@@ -673,6 +686,7 @@
 
               // Update the output format of this XML file.
               file_ref->type = XmlFileTypeForOutputFormat(options_.output_format);
+
               bool result = table->AddResource(
                   NewResourceBuilder(file.name)
                       .SetValue(std::move(file_ref), file.config)
@@ -685,14 +699,6 @@
               }
             }
 
-            FeatureFlagsFilterOptions flags_filter_options;
-            flags_filter_options.fail_on_unrecognized_flags = false;
-            flags_filter_options.flags_must_have_value = false;
-            FeatureFlagsFilter flags_filter(options_.feature_flag_values, flags_filter_options);
-            if (!flags_filter.Consume(context_, doc.get())) {
-              return 1;
-            }
-
             error |= !FlattenXml(context_, *doc, dst_path, options_.keep_raw_values,
                                  false /*utf16*/, options_.output_format, archive_writer);
           }
diff --git a/tools/aapt2/cmd/Link.h b/tools/aapt2/cmd/Link.h
index 9779788..54a8c86 100644
--- a/tools/aapt2/cmd/Link.h
+++ b/tools/aapt2/cmd/Link.h
@@ -164,9 +164,12 @@
     AddOptionalSwitch("--no-resource-removal", "Disables automatic removal of resources without\n"
             "defaults. Use this only when building runtime resource overlay packages.",
         &options_.no_resource_removal);
-    AddOptionalSwitch("--enable-sparse-encoding",
-                      "This decreases APK size at the cost of resource retrieval performance.",
-                      &options_.use_sparse_encoding);
+    AddOptionalSwitch(
+        "--enable-sparse-encoding",
+        "Enables encoding sparse entries using a binary search tree.\n"
+        "This decreases APK size at the cost of resource retrieval performance.\n"
+        "Only applies sparse encoding if minSdk of the APK is >= 32",
+        &options_.use_sparse_encoding);
     AddOptionalSwitch("--enable-compact-entries",
         "This decreases APK size by using compact resource entries for simple data types.",
         &options_.table_flattener_options.use_compact_entries);
diff --git a/tools/aapt2/cmd/Optimize.h b/tools/aapt2/cmd/Optimize.h
index 012b0f2..a8f547e 100644
--- a/tools/aapt2/cmd/Optimize.h
+++ b/tools/aapt2/cmd/Optimize.h
@@ -108,14 +108,14 @@
         "--enable-sparse-encoding",
         "Enables encoding sparse entries using a binary search tree.\n"
         "This decreases APK size at the cost of resource retrieval performance.\n"
-        "Only applies sparse encoding to Android O+ resources or all resources if minSdk of "
-        "the APK is O+",
+        "Only applies sparse encoding if minSdk of the APK is >= 32",
         &options_.enable_sparse_encoding);
-    AddOptionalSwitch("--force-sparse-encoding",
-                      "Enables encoding sparse entries using a binary search tree.\n"
-                      "This decreases APK size at the cost of resource retrieval performance.\n"
-                      "Applies sparse encoding to all resources regardless of minSdk.",
-                      &options_.force_sparse_encoding);
+    AddOptionalSwitch(
+        "--force-sparse-encoding",
+        "Enables encoding sparse entries using a binary search tree.\n"
+        "This decreases APK size at the cost of resource retrieval performance.\n"
+        "Only applies sparse encoding if minSdk of the APK is >= 32 or is not set",
+        &options_.force_sparse_encoding);
     AddOptionalSwitch(
         "--enable-compact-entries",
         "This decreases APK size by using compact resource entries for simple data types.",
diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp
index 50144ae..d19c9f2 100644
--- a/tools/aapt2/format/binary/TableFlattener.cpp
+++ b/tools/aapt2/format/binary/TableFlattener.cpp
@@ -197,13 +197,16 @@
     bool sparse_encode = sparse_entries_ == SparseEntriesMode::Enabled ||
                          sparse_entries_ == SparseEntriesMode::Forced;
 
-    if (sparse_entries_ == SparseEntriesMode::Forced ||
-        (context_->GetMinSdkVersion() == 0 && config.sdkVersion == 0)) {
-      // Sparse encode if forced or sdk version is not set in context and config.
-    } else {
-      // Otherwise, only sparse encode if the entries will be read on platforms S_V2+.
-      sparse_encode = sparse_encode && (context_->GetMinSdkVersion() >= SDK_S_V2);
-    }
+    // Only sparse encode if the entries will be read on platforms S_V2+. Sparse encoding
+    // is not supported on older platforms (b/197642721, b/197976367).
+    //
+    // We also allow sparse encoding for minSdk is 0 (not set) if sparse encoding is forced,
+    // in order to support Bundletool's usage of aapt2 where minSdk is not set in splits.
+    bool meets_min_sdk_requirement_for_sparse_encoding =
+        (context_->GetMinSdkVersion() >= SDK_S_V2) ||
+        (context_->GetMinSdkVersion() == 0 && sparse_entries_ == SparseEntriesMode::Forced);
+
+    sparse_encode = sparse_encode && meets_min_sdk_requirement_for_sparse_encoding;
 
     // Only sparse encode if the offsets are representable in 2 bytes.
     sparse_encode = sparse_encode && short_offsets;
diff --git a/tools/aapt2/format/binary/TableFlattener_test.cpp b/tools/aapt2/format/binary/TableFlattener_test.cpp
index 9156b96..0e8aae1 100644
--- a/tools/aapt2/format/binary/TableFlattener_test.cpp
+++ b/tools/aapt2/format/binary/TableFlattener_test.cpp
@@ -15,6 +15,7 @@
  */
 
 #include "format/binary/TableFlattener.h"
+#include <string>
 
 #include "android-base/stringprintf.h"
 #include "androidfw/TypeWrappers.h"
@@ -326,6 +327,28 @@
   return table;
 }
 
+static void CheckSparseEntries(IAaptContext* context, const ConfigDescription& sparse_config,
+                               const std::string& sparse_contents) {
+  ResourceTable sparse_table;
+  BinaryResourceParser parser(context->GetDiagnostics(), &sparse_table, Source("test.arsc"),
+                              sparse_contents.data(), sparse_contents.size());
+  ASSERT_TRUE(parser.Parse());
+
+  auto value = test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_0",
+                                                        sparse_config);
+  ASSERT_THAT(value, NotNull());
+  EXPECT_EQ(0u, value->value.data);
+
+  ASSERT_THAT(test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_1",
+                                                       sparse_config),
+              IsNull());
+
+  value = test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_4",
+                                                   sparse_config);
+  ASSERT_THAT(value, NotNull());
+  EXPECT_EQ(4u, value->value.data);
+}
+
 TEST_F(TableFlattenerTest, FlattenSparseEntryWithMinSdkSV2) {
   std::unique_ptr<IAaptContext> context = test::ContextBuilder()
                                               .SetCompilationPackage("android")
@@ -347,29 +370,56 @@
 
   EXPECT_GT(no_sparse_contents.size(), sparse_contents.size());
 
-  // Attempt to parse the sparse contents.
-
-  ResourceTable sparse_table;
-  BinaryResourceParser parser(context->GetDiagnostics(), &sparse_table, Source("test.arsc"),
-                              sparse_contents.data(), sparse_contents.size());
-  ASSERT_TRUE(parser.Parse());
-
-  auto value = test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_0",
-                                                        sparse_config);
-  ASSERT_THAT(value, NotNull());
-  EXPECT_EQ(0u, value->value.data);
-
-  ASSERT_THAT(test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_1",
-                                                       sparse_config),
-              IsNull());
-
-  value = test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_4",
-                                                   sparse_config);
-  ASSERT_THAT(value, NotNull());
-  EXPECT_EQ(4u, value->value.data);
+  CheckSparseEntries(context.get(), sparse_config, sparse_contents);
 }
 
-TEST_F(TableFlattenerTest, FlattenSparseEntryWithConfigSdkVersionSV2) {
+TEST_F(TableFlattenerTest, FlattenSparseEntryWithMinSdkSV2AndForced) {
+  std::unique_ptr<IAaptContext> context = test::ContextBuilder()
+                                              .SetCompilationPackage("android")
+                                              .SetPackageId(0x01)
+                                              .SetMinSdkVersion(SDK_S_V2)
+                                              .Build();
+
+  const ConfigDescription sparse_config = test::ParseConfigOrDie("en-rGB");
+  auto table_in = BuildTableWithSparseEntries(context.get(), sparse_config, 0.25f);
+
+  TableFlattenerOptions options;
+  options.sparse_entries = SparseEntriesMode::Forced;
+
+  std::string no_sparse_contents;
+  ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &no_sparse_contents));
+
+  std::string sparse_contents;
+  ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents));
+
+  EXPECT_GT(no_sparse_contents.size(), sparse_contents.size());
+
+  CheckSparseEntries(context.get(), sparse_config, sparse_contents);
+}
+
+TEST_F(TableFlattenerTest, FlattenSparseEntryWithMinSdkBeforeSV2) {
+  std::unique_ptr<IAaptContext> context = test::ContextBuilder()
+                                              .SetCompilationPackage("android")
+                                              .SetPackageId(0x01)
+                                              .SetMinSdkVersion(SDK_LOLLIPOP)
+                                              .Build();
+
+  const ConfigDescription sparse_config = test::ParseConfigOrDie("en-rGB");
+  auto table_in = BuildTableWithSparseEntries(context.get(), sparse_config, 0.25f);
+
+  TableFlattenerOptions options;
+  options.sparse_entries = SparseEntriesMode::Enabled;
+
+  std::string no_sparse_contents;
+  ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &no_sparse_contents));
+
+  std::string sparse_contents;
+  ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents));
+
+  EXPECT_EQ(no_sparse_contents.size(), sparse_contents.size());
+}
+
+TEST_F(TableFlattenerTest, FlattenSparseEntryWithMinSdkBeforeSV2AndConfigSdkVersionSV2) {
   std::unique_ptr<IAaptContext> context = test::ContextBuilder()
                                               .SetCompilationPackage("android")
                                               .SetPackageId(0x01)
@@ -391,7 +441,7 @@
   EXPECT_EQ(no_sparse_contents.size(), sparse_contents.size());
 }
 
-TEST_F(TableFlattenerTest, FlattenSparseEntryRegardlessOfMinSdkWhenForced) {
+TEST_F(TableFlattenerTest, FlattenSparseEntryWithMinSdkBeforeSV2AndForced) {
   std::unique_ptr<IAaptContext> context = test::ContextBuilder()
                                               .SetCompilationPackage("android")
                                               .SetPackageId(0x01)
@@ -410,7 +460,7 @@
   std::string sparse_contents;
   ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents));
 
-  EXPECT_GT(no_sparse_contents.size(), sparse_contents.size());
+  EXPECT_EQ(no_sparse_contents.size(), sparse_contents.size());
 }
 
 TEST_F(TableFlattenerTest, FlattenSparseEntryWithSdkVersionNotSet) {
@@ -429,28 +479,28 @@
   std::string sparse_contents;
   ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents));
 
+  EXPECT_EQ(no_sparse_contents.size(), sparse_contents.size());
+}
+
+TEST_F(TableFlattenerTest, FlattenSparseEntryWithSdkVersionNotSetAndForced) {
+  std::unique_ptr<IAaptContext> context =
+      test::ContextBuilder().SetCompilationPackage("android").SetPackageId(0x01).Build();
+
+  const ConfigDescription sparse_config = test::ParseConfigOrDie("en-rGB");
+  auto table_in = BuildTableWithSparseEntries(context.get(), sparse_config, 0.25f);
+
+  TableFlattenerOptions options;
+  options.sparse_entries = SparseEntriesMode::Forced;
+
+  std::string no_sparse_contents;
+  ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &no_sparse_contents));
+
+  std::string sparse_contents;
+  ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents));
+
   EXPECT_GT(no_sparse_contents.size(), sparse_contents.size());
 
-  // Attempt to parse the sparse contents.
-
-  ResourceTable sparse_table;
-  BinaryResourceParser parser(context->GetDiagnostics(), &sparse_table, Source("test.arsc"),
-                              sparse_contents.data(), sparse_contents.size());
-  ASSERT_TRUE(parser.Parse());
-
-  auto value = test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_0",
-                                                        sparse_config);
-  ASSERT_THAT(value, NotNull());
-  EXPECT_EQ(0u, value->value.data);
-
-  ASSERT_THAT(test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_1",
-                                                       sparse_config),
-              IsNull());
-
-  value = test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_4",
-                                                   sparse_config);
-  ASSERT_THAT(value, NotNull());
-  EXPECT_EQ(4u, value->value.data);
+  CheckSparseEntries(context.get(), sparse_config, sparse_contents);
 }
 
 TEST_F(TableFlattenerTest, DoNotUseSparseEntryForDenseConfig) {
diff --git a/tools/aapt2/format/proto/ProtoDeserialize.cpp b/tools/aapt2/format/proto/ProtoDeserialize.cpp
index 91ec348..b893655 100644
--- a/tools/aapt2/format/proto/ProtoDeserialize.cpp
+++ b/tools/aapt2/format/proto/ProtoDeserialize.cpp
@@ -640,6 +640,7 @@
   out_file->name = name_ref.ToResourceName();
   out_file->source.path = pb_file.source_path();
   out_file->type = DeserializeFileReferenceTypeFromPb(pb_file.type());
+  out_file->uses_readwrite_feature_flags = pb_file.uses_readwrite_feature_flags();
 
   out_file->flag_status = (FlagStatus)pb_file.flag_status();
   if (!pb_file.flag_name().empty()) {
diff --git a/tools/aapt2/format/proto/ProtoSerialize.cpp b/tools/aapt2/format/proto/ProtoSerialize.cpp
index fcc77d5..da99c4f 100644
--- a/tools/aapt2/format/proto/ProtoSerialize.cpp
+++ b/tools/aapt2/format/proto/ProtoSerialize.cpp
@@ -767,6 +767,7 @@
     out_file->set_flag_negated(file.flag->negated);
     out_file->set_flag_name(file.flag->name);
   }
+  out_file->set_uses_readwrite_feature_flags(file.uses_readwrite_feature_flags);
 
   for (const SourcedResourceName& exported : file.exported_symbols) {
     pb::internal::CompiledFile_Symbol* pb_symbol = out_file->add_exported_symbol();
diff --git a/tools/aapt2/link/FlaggedResources_test.cpp b/tools/aapt2/link/FlaggedResources_test.cpp
index 47a71fe..4dcb850 100644
--- a/tools/aapt2/link/FlaggedResources_test.cpp
+++ b/tools/aapt2/link/FlaggedResources_test.cpp
@@ -226,9 +226,11 @@
       }
     }
   }
+
   ASSERT_TRUE(found) << "No entry for layout1 at v36 with FLAG_USES_FEATURE_FLAGS bit set";
-  // There should only be 1 entry that has the FLAG_USES_FEATURE_FLAGS bit of flags set to 1
-  ASSERT_EQ(fields_flagged, 1);
+  // There should only be 2 entry that has the FLAG_USES_FEATURE_FLAGS bit of flags set to 1, the
+  // three versions of the layout file that has flags
+  ASSERT_EQ(fields_flagged, 3);
 }
 
 }  // namespace aapt
diff --git a/tools/aapt2/link/FlaggedXmlVersioner.cpp b/tools/aapt2/link/FlaggedXmlVersioner.cpp
index 8a3337c..626cae7 100644
--- a/tools/aapt2/link/FlaggedXmlVersioner.cpp
+++ b/tools/aapt2/link/FlaggedXmlVersioner.cpp
@@ -35,10 +35,6 @@
     VisitChildren(node);
   }
 
-  bool HadFlags() const {
-    return had_flags_;
-  }
-
  private:
   bool FixupOrShouldRemove(const std::unique_ptr<xml::Node>& node) {
     if (auto* el = NodeCast<Element>(node.get())) {
@@ -47,7 +43,6 @@
         return false;
       }
 
-      had_flags_ = true;
       // This class assumes all flags are disabled so we want to remove any elements behind flags
       // unless the flag specification is negated. In the negated case we remove the featureFlag
       // attribute because we have already determined whether we are keeping the element or not.
@@ -62,56 +57,27 @@
 
     return false;
   }
-
-  bool had_flags_ = false;
-};
-
-// An xml visitor that goes through the a doc and determines if any elements are behind a flag.
-class FindFlagsVisitor : public xml::Visitor {
- public:
-  void Visit(xml::Element* node) override {
-    if (had_flags_) {
-      return;
-    }
-    auto* attr = node->FindAttribute(xml::kSchemaAndroid, xml::kAttrFeatureFlag);
-    if (attr != nullptr) {
-      had_flags_ = true;
-      return;
-    }
-    VisitChildren(node);
-  }
-
-  bool HadFlags() const {
-    return had_flags_;
-  }
-
-  bool had_flags_ = false;
 };
 
 std::vector<std::unique_ptr<xml::XmlResource>> FlaggedXmlVersioner::Process(IAaptContext* context,
                                                                             xml::XmlResource* doc) {
   std::vector<std::unique_ptr<xml::XmlResource>> docs;
-  if ((static_cast<ApiVersion>(doc->file.config.sdkVersion) >= SDK_BAKLAVA) ||
-      (static_cast<ApiVersion>(context->GetMinSdkVersion()) >= SDK_BAKLAVA)) {
+  if (!doc->file.uses_readwrite_feature_flags) {
+    docs.push_back(doc->Clone());
+  } else if ((static_cast<ApiVersion>(doc->file.config.sdkVersion) >= SDK_BAKLAVA) ||
+             (static_cast<ApiVersion>(context->GetMinSdkVersion()) >= SDK_BAKLAVA)) {
     // Support for read/write flags was added in baklava so if the doc will only get used on
     // baklava or later we can just return the original doc.
     docs.push_back(doc->Clone());
-    FindFlagsVisitor visitor;
-    doc->root->Accept(&visitor);
-    docs.back()->file.uses_readwrite_feature_flags = visitor.HadFlags();
   } else {
     auto preBaklavaVersion = doc->Clone();
     AllDisabledFlagsVisitor visitor;
     preBaklavaVersion->root->Accept(&visitor);
-    preBaklavaVersion->file.uses_readwrite_feature_flags = false;
     docs.push_back(std::move(preBaklavaVersion));
 
-    if (visitor.HadFlags()) {
-      auto baklavaVersion = doc->Clone();
-      baklavaVersion->file.config.sdkVersion = SDK_BAKLAVA;
-      baklavaVersion->file.uses_readwrite_feature_flags = true;
-      docs.push_back(std::move(baklavaVersion));
-    }
+    auto baklavaVersion = doc->Clone();
+    baklavaVersion->file.config.sdkVersion = SDK_BAKLAVA;
+    docs.push_back(std::move(baklavaVersion));
   }
   return docs;
 }
diff --git a/tools/aapt2/link/FlaggedXmlVersioner_test.cpp b/tools/aapt2/link/FlaggedXmlVersioner_test.cpp
index 0c1314f..0dc4642 100644
--- a/tools/aapt2/link/FlaggedXmlVersioner_test.cpp
+++ b/tools/aapt2/link/FlaggedXmlVersioner_test.cpp
@@ -101,6 +101,7 @@
         <TextView android:featureFlag="package.flag" /><TextView /><TextView />
       </LinearLayout>)");
   doc->file.config.sdkVersion = SDK_GINGERBREAD;
+  doc->file.uses_readwrite_feature_flags = true;
 
   FlaggedXmlVersioner versioner;
   auto results = versioner.Process(context_.get(), doc.get());
@@ -131,6 +132,7 @@
       <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android">
         <TextView android:featureFlag="package.flag" /><TextView /><TextView />
       </LinearLayout>)");
+  doc->file.uses_readwrite_feature_flags = true;
 
   FlaggedXmlVersioner versioner;
   auto results = versioner.Process(context_.get(), doc.get());
@@ -162,6 +164,7 @@
         <TextView android:featureFlag="!package.flag" /><TextView /><TextView />
       </LinearLayout>)");
   doc->file.config.sdkVersion = SDK_GINGERBREAD;
+  doc->file.uses_readwrite_feature_flags = true;
 
   FlaggedXmlVersioner versioner;
   auto results = versioner.Process(context_.get(), doc.get());
@@ -192,6 +195,7 @@
       <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android">
         <TextView android:featureFlag="!package.flag" /><TextView /><TextView />
       </LinearLayout>)");
+  doc->file.uses_readwrite_feature_flags = true;
 
   FlaggedXmlVersioner versioner;
   auto results = versioner.Process(context_.get(), doc.get());
diff --git a/tools/aapt2/link/TableMerger.cpp b/tools/aapt2/link/TableMerger.cpp
index 1d4adc4..17f3323 100644
--- a/tools/aapt2/link/TableMerger.cpp
+++ b/tools/aapt2/link/TableMerger.cpp
@@ -295,6 +295,8 @@
           dst_config_value =
               dst_entry->FindOrCreateValue(src_config_value->config, src_config_value->product);
         }
+        dst_config_value->uses_readwrite_feature_flags |=
+            src_config_value->uses_readwrite_feature_flags;
 
         // Continue if we're taking the new resource.
         CloningValueTransformer cloner(&main_table_->string_pool);
@@ -378,12 +380,13 @@
   file_ref->file = file;
   file_ref->SetFlagStatus(file_desc.flag_status);
   file_ref->SetFlag(file_desc.flag);
-
   ResourceTablePackage* pkg = table.FindOrCreatePackage(file_desc.name.package);
-  pkg->FindOrCreateType(file_desc.name.type)
-      ->FindOrCreateEntry(file_desc.name.entry)
-      ->FindOrCreateValue(file_desc.config, {})
-      ->value = std::move(file_ref);
+  ResourceConfigValue* config_value = pkg->FindOrCreateType(file_desc.name.type)
+                                          ->FindOrCreateEntry(file_desc.name.entry)
+                                          ->FindOrCreateValue(file_desc.config, {});
+
+  config_value->value = std::move(file_ref);
+  config_value->uses_readwrite_feature_flags = file_desc.uses_readwrite_feature_flags;
 
   return DoMerge(file->GetSource(), pkg, false /*mangle*/, overlay /*overlay*/, true /*allow_new*/);
 }
diff --git a/tools/aapt2/readme.md b/tools/aapt2/readme.md
index 6bdbaae..413f817 100644
--- a/tools/aapt2/readme.md
+++ b/tools/aapt2/readme.md
@@ -5,6 +5,10 @@
   2017. This README will be updated more frequently in the future.
 - Added a new flag `--no-compress-fonts`. This can significantly speed up loading fonts from APK
   assets, at the cost of increasing the storage size of the APK.
+- Changed the behavior of `--enable-sparse-encoding`. Sparse encoding is only applied if the
+  minSdkVersion is >= 32.
+- Changed the behavior of `--force-sparse-encoding`. Sparse encoding is only applied if the
+  minSdkVersion is >= 32 or is not set.
 
 ## Version 2.19
 - Added navigation resource type.